Skip to content

Commit 9e8b066

Browse files
committed
Remove transient TypeScript build artifact
1 parent 1a23e0f commit 9e8b066

32 files changed

+920
-0
lines changed

web/.env.example

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
AUTH_SECRET=replace-with-openssl-rand-32
2+
AUTH_URL=http://localhost:3000
3+
4+
# OAuth providers
5+
AUTH_GITHUB_ID=
6+
AUTH_GITHUB_SECRET=
7+
AUTH_GOOGLE_ID=
8+
AUTH_GOOGLE_SECRET=
9+
10+
# ARIV backend endpoint (Python service / API)
11+
ARIV_API_BASE_URL=http://localhost:8000
12+
ARIV_API_KEY=
13+
14+
# Rate limiting
15+
RATE_LIMIT_WINDOW_MS=60000
16+
RATE_LIMIT_MAX_REQUESTS=30

web/README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# ARIV Web Console (Next.js + Vercel)
2+
3+
Production-ready ChatGPT-inspired control plane for ARIV with authentication, API proxying, and secure defaults.
4+
5+
## Project structure
6+
7+
```text
8+
web/
9+
├── app/
10+
│ ├── api/
11+
│ │ ├── auth/[...nextauth]/route.ts
12+
│ │ ├── command/route.ts
13+
│ │ ├── config/route.ts
14+
│ │ └── logs/route.ts
15+
│ ├── settings/page.tsx
16+
│ ├── globals.css
17+
│ ├── layout.tsx
18+
│ └── page.tsx
19+
├── components/
20+
│ ├── chat/chat-window.tsx
21+
│ ├── layout/sidebar.tsx
22+
│ └── ui/{button,input,switch,textarea}.tsx
23+
├── lib/
24+
│ ├── store/chat-store.ts
25+
│ ├── ariv.ts
26+
│ ├── env.ts
27+
│ ├── rate-limit.ts
28+
│ ├── security.ts
29+
│ └── utils.ts
30+
├── types/{index,next-auth}.d.ts
31+
├── auth.ts
32+
├── middleware.ts
33+
├── .env.example
34+
├── next.config.js
35+
├── package.json
36+
├── tailwind.config.ts
37+
└── tsconfig.json
38+
```
39+
40+
## Security controls included
41+
42+
- Auth.js (NextAuth v5) with GitHub/Google OAuth providers.
43+
- JWT-backed sessions with protected API routes in middleware.
44+
- Request throttling using a per-IP + per-route rate limiter.
45+
- Command/config input validation using Zod schemas.
46+
- Output/input sanitization for potentially unsafe content.
47+
- Hardened response headers (CSP, X-Frame-Options, nosniff, etc.).
48+
- Secrets only from environment variables.
49+
50+
## Local development
51+
52+
1. Install dependencies:
53+
```bash
54+
cd web
55+
npm install
56+
```
57+
2. Configure env vars:
58+
```bash
59+
cp .env.example .env.local
60+
```
61+
3. Run:
62+
```bash
63+
npm run dev
64+
```
65+
66+
## ARIV backend contract
67+
68+
The Next.js APIs are secure proxies and expect an ARIV backend service exposing:
69+
70+
- `POST /api/command``{ output: string }`
71+
- `GET /api/config` / `PUT /api/config`
72+
- `GET /api/logs``{ entries: string[] }`
73+
74+
Point `ARIV_API_BASE_URL` to this backend URL.
75+
76+
## Deploy to Vercel
77+
78+
1. Push repository.
79+
2. Import the project in Vercel and set **Root Directory** to `web`.
80+
3. Add env vars from `.env.example`:
81+
- `AUTH_SECRET` (required, min 32 chars)
82+
- OAuth provider keys
83+
- `ARIV_API_BASE_URL` (+ optional `ARIV_API_KEY`)
84+
- Rate limiting values
85+
4. Deploy.
86+
87+
### Recommended production settings
88+
89+
- Restrict OAuth redirect domains to your production origin.
90+
- Use managed Redis/Upstash for distributed rate limiting (replace in-memory limiter).
91+
- Monitor logs and enable Vercel WAF + Bot Protection.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { handlers } from "@/auth";
2+
3+
export const { GET, POST } = handlers;

web/app/api/command/route.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { NextResponse } from "next/server";
2+
import { forwardToAriv } from "@/lib/ariv";
3+
import { commandSchema, sanitizeText } from "@/lib/security";
4+
5+
export async function POST(request: Request) {
6+
try {
7+
const payload = commandSchema.parse(await request.json());
8+
9+
const result = await forwardToAriv<{ output: string }>("/api/command", {
10+
method: "POST",
11+
body: JSON.stringify({
12+
command: sanitizeText(payload.command),
13+
sessionId: payload.sessionId
14+
})
15+
});
16+
17+
return NextResponse.json(result);
18+
} catch (error) {
19+
return NextResponse.json(
20+
{ error: error instanceof Error ? error.message : "Invalid request" },
21+
{ status: 400 }
22+
);
23+
}
24+
}

web/app/api/config/route.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { NextResponse } from "next/server";
2+
import { forwardToAriv } from "@/lib/ariv";
3+
import { configSchema } from "@/lib/security";
4+
5+
export async function GET() {
6+
try {
7+
const config = await forwardToAriv("/api/config", { method: "GET" });
8+
return NextResponse.json(config);
9+
} catch (error) {
10+
return NextResponse.json(
11+
{ error: error instanceof Error ? error.message : "Failed to fetch config" },
12+
{ status: 500 }
13+
);
14+
}
15+
}
16+
17+
export async function PUT(request: Request) {
18+
try {
19+
const payload = configSchema.parse(await request.json());
20+
const updated = await forwardToAriv("/api/config", {
21+
method: "PUT",
22+
body: JSON.stringify(payload)
23+
});
24+
return NextResponse.json(updated);
25+
} catch (error) {
26+
return NextResponse.json(
27+
{ error: error instanceof Error ? error.message : "Failed to update config" },
28+
{ status: 400 }
29+
);
30+
}
31+
}

web/app/api/logs/route.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { NextResponse } from "next/server";
2+
import { forwardToAriv } from "@/lib/ariv";
3+
4+
export async function GET() {
5+
try {
6+
const logs = await forwardToAriv<{ entries: string[] }>("/api/logs", { method: "GET" });
7+
return NextResponse.json(logs);
8+
} catch (error) {
9+
return NextResponse.json(
10+
{ error: error instanceof Error ? error.message : "Failed to fetch logs" },
11+
{ status: 500 }
12+
);
13+
}
14+
}

web/app/globals.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
2+
3+
@tailwind base;
4+
@tailwind components;
5+
@tailwind utilities;
6+
7+
:root {
8+
color-scheme: dark;
9+
}
10+
11+
body {
12+
@apply bg-background text-foreground antialiased;
13+
font-family: 'Inter', system-ui, sans-serif;
14+
}
15+
16+
* {
17+
@apply border-border;
18+
}

web/app/layout.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { Metadata } from "next";
2+
import "./globals.css";
3+
4+
export const metadata: Metadata = {
5+
title: "ARIV Console",
6+
description: "Secure web control plane for ARIV"
7+
};
8+
9+
export default function RootLayout({ children }: { children: React.ReactNode }) {
10+
return (
11+
<html lang="en" className="dark">
12+
<body>{children}</body>
13+
</html>
14+
);
15+
}

web/app/page.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Sidebar } from "@/components/layout/sidebar";
2+
import { ChatWindow } from "@/components/chat/chat-window";
3+
4+
export default function HomePage() {
5+
return (
6+
<main className="flex h-screen overflow-hidden">
7+
<Sidebar />
8+
<section className="flex-1 p-4 md:p-6">
9+
<div className="mx-auto flex h-full w-full max-w-5xl flex-col">
10+
<header className="mb-4">
11+
<h2 className="text-xl font-semibold">ARIV Command Center</h2>
12+
<p className="text-sm text-muted">ChatGPT-inspired interface for secure control and configuration.</p>
13+
</header>
14+
<ChatWindow />
15+
</div>
16+
</section>
17+
</main>
18+
);
19+
}

web/app/settings/page.tsx

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { useForm } from "react-hook-form";
5+
import { zodResolver } from "@hookform/resolvers/zod";
6+
import { z } from "zod";
7+
import { Sidebar } from "@/components/layout/sidebar";
8+
import { Input } from "@/components/ui/input";
9+
import { Switch } from "@/components/ui/switch";
10+
import { Button } from "@/components/ui/button";
11+
import { useChatStore } from "@/lib/store/chat-store";
12+
13+
const formSchema = z.object({
14+
model: z.string().min(1),
15+
temperature: z.coerce.number().min(0).max(2),
16+
streaming: z.boolean(),
17+
safeMode: z.boolean(),
18+
telemetry: z.boolean()
19+
});
20+
21+
type FormData = z.infer<typeof formSchema>;
22+
23+
export default function SettingsPage() {
24+
const { config, setConfig } = useChatStore();
25+
const [status, setStatus] = useState<string>("");
26+
27+
const form = useForm<FormData>({
28+
resolver: zodResolver(formSchema),
29+
defaultValues: config
30+
});
31+
32+
useEffect(() => {
33+
form.reset(config);
34+
}, [config, form]);
35+
36+
const onSubmit = form.handleSubmit(async (values) => {
37+
const res = await fetch("/api/config", {
38+
method: "PUT",
39+
headers: { "Content-Type": "application/json" },
40+
body: JSON.stringify(values)
41+
});
42+
43+
if (!res.ok) {
44+
setStatus("Failed to save settings.");
45+
return;
46+
}
47+
48+
setConfig(values);
49+
setStatus("Configuration saved securely.");
50+
});
51+
52+
return (
53+
<main className="flex h-screen overflow-hidden">
54+
<Sidebar />
55+
<section className="flex-1 p-4 md:p-6">
56+
<div className="mx-auto max-w-2xl rounded-xl border border-border bg-panel p-6">
57+
<h2 className="mb-1 text-xl font-semibold">ARIV Configuration</h2>
58+
<p className="mb-6 text-sm text-muted">Manage model behavior and safety settings.</p>
59+
60+
<form onSubmit={onSubmit} className="space-y-5">
61+
<div>
62+
<label className="mb-2 block text-sm">Model Name</label>
63+
<Input {...form.register("model")} />
64+
</div>
65+
66+
<div>
67+
<label className="mb-2 block text-sm">Temperature</label>
68+
<Input type="number" step="0.1" min="0" max="2" {...form.register("temperature")} />
69+
</div>
70+
71+
<div className="flex items-center justify-between rounded-md border border-border p-3">
72+
<span className="text-sm">Enable Streaming Responses</span>
73+
<Switch checked={form.watch("streaming")} onCheckedChange={(v) => form.setValue("streaming", v)} />
74+
</div>
75+
76+
<div className="flex items-center justify-between rounded-md border border-border p-3">
77+
<span className="text-sm">Safe Mode (recommended)</span>
78+
<Switch checked={form.watch("safeMode")} onCheckedChange={(v) => form.setValue("safeMode", v)} />
79+
</div>
80+
81+
<div className="flex items-center justify-between rounded-md border border-border p-3">
82+
<span className="text-sm">Telemetry</span>
83+
<Switch checked={form.watch("telemetry")} onCheckedChange={(v) => form.setValue("telemetry", v)} />
84+
</div>
85+
86+
<Button type="submit">Save Configuration</Button>
87+
{status ? <p className="text-sm text-muted">{status}</p> : null}
88+
</form>
89+
</div>
90+
</section>
91+
</main>
92+
);
93+
}

0 commit comments

Comments
 (0)