Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions web/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
AUTH_SECRET=replace-with-openssl-rand-32
AUTH_URL=http://localhost:3000

# OAuth providers
AUTH_GITHUB_ID=
AUTH_GITHUB_SECRET=
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=

# ARIV backend endpoint (Python service / API)
ARIV_API_BASE_URL=http://localhost:8000
ARIV_API_KEY=

# Rate limiting
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX_REQUESTS=30
91 changes: 91 additions & 0 deletions web/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# ARIV Web Console (Next.js + Vercel)

Production-ready ChatGPT-inspired control plane for ARIV with authentication, API proxying, and secure defaults.

## Project structure

```text
web/
├── app/
│ ├── api/
│ │ ├── auth/[...nextauth]/route.ts
│ │ ├── command/route.ts
│ │ ├── config/route.ts
│ │ └── logs/route.ts
│ ├── settings/page.tsx
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── components/
│ ├── chat/chat-window.tsx
│ ├── layout/sidebar.tsx
│ └── ui/{button,input,switch,textarea}.tsx
├── lib/
│ ├── store/chat-store.ts
│ ├── ariv.ts
│ ├── env.ts
│ ├── rate-limit.ts
│ ├── security.ts
│ └── utils.ts
├── types/{index,next-auth}.d.ts
├── auth.ts
├── middleware.ts
├── .env.example
├── next.config.js
├── package.json
├── tailwind.config.ts
└── tsconfig.json
```

## Security controls included

- Auth.js (NextAuth v5) with GitHub/Google OAuth providers.
- JWT-backed sessions with protected API routes in middleware.
- Request throttling using a per-IP + per-route rate limiter.
- Command/config input validation using Zod schemas.
- Output/input sanitization for potentially unsafe content.
- Hardened response headers (CSP, X-Frame-Options, nosniff, etc.).
- Secrets only from environment variables.

## Local development

1. Install dependencies:
```bash
cd web
npm install
```
2. Configure env vars:
```bash
cp .env.example .env.local
```
3. Run:
```bash
npm run dev
```

## ARIV backend contract

The Next.js APIs are secure proxies and expect an ARIV backend service exposing:

- `POST /api/command` → `{ output: string }`
- `GET /api/config` / `PUT /api/config`
- `GET /api/logs` → `{ entries: string[] }`

Point `ARIV_API_BASE_URL` to this backend URL.

## Deploy to Vercel

1. Push repository.
2. Import the project in Vercel and set **Root Directory** to `web`.
3. Add env vars from `.env.example`:
- `AUTH_SECRET` (required, min 32 chars)
- OAuth provider keys
- `ARIV_API_BASE_URL` (+ optional `ARIV_API_KEY`)
- Rate limiting values
4. Deploy.

### Recommended production settings

- Restrict OAuth redirect domains to your production origin.
- Use managed Redis/Upstash for distributed rate limiting (replace in-memory limiter).
- Monitor logs and enable Vercel WAF + Bot Protection.
3 changes: 3 additions & 0 deletions web/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { handlers } from "@/auth";

export const { GET, POST } = handlers;
24 changes: 24 additions & 0 deletions web/app/api/command/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NextResponse } from "next/server";
import { forwardToAriv } from "@/lib/ariv";
import { commandSchema, sanitizeText } from "@/lib/security";

export async function POST(request: Request) {
try {
const payload = commandSchema.parse(await request.json());

const result = await forwardToAriv<{ output: string }>("/api/command", {
method: "POST",
body: JSON.stringify({
command: sanitizeText(payload.command),
sessionId: payload.sessionId
})
});

return NextResponse.json(result);
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Invalid request" },
{ status: 400 }
);
}
}
31 changes: 31 additions & 0 deletions web/app/api/config/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NextResponse } from "next/server";
import { forwardToAriv } from "@/lib/ariv";
import { configSchema } from "@/lib/security";

export async function GET() {
try {
const config = await forwardToAriv("/api/config", { method: "GET" });
return NextResponse.json(config);
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Failed to fetch config" },
{ status: 500 }
);
}
}

export async function PUT(request: Request) {
try {
const payload = configSchema.parse(await request.json());
const updated = await forwardToAriv("/api/config", {
method: "PUT",
body: JSON.stringify(payload)
});
return NextResponse.json(updated);
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Failed to update config" },
{ status: 400 }
);
}
}
14 changes: 14 additions & 0 deletions web/app/api/logs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { NextResponse } from "next/server";
import { forwardToAriv } from "@/lib/ariv";

export async function GET() {
try {
const logs = await forwardToAriv<{ entries: string[] }>("/api/logs", { method: "GET" });
return NextResponse.json(logs);
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Failed to fetch logs" },
{ status: 500 }
);
}
}
18 changes: 18 additions & 0 deletions web/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');

@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
color-scheme: dark;
}

body {
@apply bg-background text-foreground antialiased;
font-family: 'Inter', system-ui, sans-serif;
}

* {
@apply border-border;
}
15 changes: 15 additions & 0 deletions web/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
title: "ARIV Console",
description: "Secure web control plane for ARIV"
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className="dark">
<body>{children}</body>
</html>
);
}
19 changes: 19 additions & 0 deletions web/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Sidebar } from "@/components/layout/sidebar";
import { ChatWindow } from "@/components/chat/chat-window";

export default function HomePage() {
return (
<main className="flex h-screen overflow-hidden">
<Sidebar />
<section className="flex-1 p-4 md:p-6">
<div className="mx-auto flex h-full w-full max-w-5xl flex-col">
<header className="mb-4">
<h2 className="text-xl font-semibold">ARIV Command Center</h2>
<p className="text-sm text-muted">ChatGPT-inspired interface for secure control and configuration.</p>
</header>
<ChatWindow />
</div>
</section>
</main>
);
}
93 changes: 93 additions & 0 deletions web/app/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"use client";

import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Sidebar } from "@/components/layout/sidebar";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button";
import { useChatStore } from "@/lib/store/chat-store";

const formSchema = z.object({
model: z.string().min(1),
temperature: z.coerce.number().min(0).max(2),
streaming: z.boolean(),
safeMode: z.boolean(),
telemetry: z.boolean()
});

type FormData = z.infer<typeof formSchema>;

export default function SettingsPage() {
const { config, setConfig } = useChatStore();
const [status, setStatus] = useState<string>("");

const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: config
});

useEffect(() => {
form.reset(config);
}, [config, form]);

const onSubmit = form.handleSubmit(async (values) => {
const res = await fetch("/api/config", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values)
});

if (!res.ok) {
setStatus("Failed to save settings.");
return;
}

setConfig(values);
setStatus("Configuration saved securely.");
});

return (
<main className="flex h-screen overflow-hidden">
<Sidebar />
<section className="flex-1 p-4 md:p-6">
<div className="mx-auto max-w-2xl rounded-xl border border-border bg-panel p-6">
<h2 className="mb-1 text-xl font-semibold">ARIV Configuration</h2>
<p className="mb-6 text-sm text-muted">Manage model behavior and safety settings.</p>

<form onSubmit={onSubmit} className="space-y-5">
<div>
<label className="mb-2 block text-sm">Model Name</label>
<Input {...form.register("model")} />
</div>

<div>
<label className="mb-2 block text-sm">Temperature</label>
<Input type="number" step="0.1" min="0" max="2" {...form.register("temperature")} />
</div>

<div className="flex items-center justify-between rounded-md border border-border p-3">
<span className="text-sm">Enable Streaming Responses</span>
<Switch checked={form.watch("streaming")} onCheckedChange={(v) => form.setValue("streaming", v)} />
</div>

<div className="flex items-center justify-between rounded-md border border-border p-3">
<span className="text-sm">Safe Mode (recommended)</span>
<Switch checked={form.watch("safeMode")} onCheckedChange={(v) => form.setValue("safeMode", v)} />
</div>

<div className="flex items-center justify-between rounded-md border border-border p-3">
<span className="text-sm">Telemetry</span>
<Switch checked={form.watch("telemetry")} onCheckedChange={(v) => form.setValue("telemetry", v)} />
</div>

<Button type="submit">Save Configuration</Button>
{status ? <p className="text-sm text-muted">{status}</p> : null}
</form>
</div>
</section>
</main>
);
}
45 changes: 45 additions & 0 deletions web/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";
import { env } from "@/lib/env";

const providers = [];

if (env.AUTH_GITHUB_ID && env.AUTH_GITHUB_SECRET) {
providers.push(
GitHub({
clientId: env.AUTH_GITHUB_ID,
clientSecret: env.AUTH_GITHUB_SECRET
})
);
}

if (env.AUTH_GOOGLE_ID && env.AUTH_GOOGLE_SECRET) {
providers.push(
Google({
clientId: env.AUTH_GOOGLE_ID,
clientSecret: env.AUTH_GOOGLE_SECRET
})
);
}

if (providers.length === 0) {
throw new Error("Configure at least one auth provider (GitHub or Google).");
}

export const { handlers, signIn, signOut, auth } = NextAuth({
providers,
secret: env.AUTH_SECRET,
session: { strategy: "jwt" },
pages: {
signIn: "/"
},
callbacks: {
async session({ session, token }) {
if (session.user) {
session.user.id = token.sub ?? "";
}
return session;
}
}
});
Loading