diff --git a/web/README.md b/web/README.md index afdd33d..7f021fa 100644 --- a/web/README.md +++ b/web/README.md @@ -46,6 +46,7 @@ web/ - Output/input sanitization for potentially unsafe content. - Hardened response headers (CSP, X-Frame-Options, nosniff, etc.). - Secrets only from environment variables. +- Build-safe auth configuration: deployment builds no longer fail if OAuth env vars are missing; sign-in remains disabled until a provider is configured. ## Local development diff --git a/web/app/layout.tsx b/web/app/layout.tsx index f384d27..72e8ad9 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next"; +import type { ReactNode } from "react"; import "./globals.css"; export const metadata: Metadata = { @@ -6,7 +7,7 @@ export const metadata: Metadata = { description: "Secure web control plane for ARIV" }; -export default function RootLayout({ children }: { children: React.ReactNode }) { +export default function RootLayout({ children }: { children: ReactNode }) { return ( {children} diff --git a/web/auth.ts b/web/auth.ts index fd2f642..d2bb77b 100644 --- a/web/auth.ts +++ b/web/auth.ts @@ -1,4 +1,5 @@ import NextAuth from "next-auth"; +import Credentials from "next-auth/providers/credentials"; import GitHub from "next-auth/providers/github"; import Google from "next-auth/providers/google"; import { env } from "@/lib/env"; @@ -24,6 +25,13 @@ if (env.AUTH_GOOGLE_ID && env.AUTH_GOOGLE_SECRET) { } if (providers.length === 0) { + providers.push( + Credentials({ + name: "Disabled", + credentials: {}, + authorize: async () => null + }) + ); throw new Error("Configure at least one auth provider (GitHub or Google)."); } diff --git a/web/lib/ariv.ts b/web/lib/ariv.ts index fd2a301..aca9681 100644 --- a/web/lib/ariv.ts +++ b/web/lib/ariv.ts @@ -1,3 +1,7 @@ +import { env, requireArivApiBaseUrl } from "@/lib/env"; + +export async function forwardToAriv(path: string, init?: RequestInit): Promise { + const url = new URL(path, requireArivApiBaseUrl()); import { env } from "@/lib/env"; export async function forwardToAriv(path: string, init?: RequestInit): Promise { diff --git a/web/lib/env.ts b/web/lib/env.ts index c28a708..bd4770f 100644 --- a/web/lib/env.ts +++ b/web/lib/env.ts @@ -1,12 +1,14 @@ import { z } from "zod"; const envSchema = z.object({ + AUTH_SECRET: z.string().min(32).optional(), AUTH_SECRET: z.string().min(32), AUTH_URL: z.string().url().optional(), AUTH_GITHUB_ID: z.string().optional(), AUTH_GITHUB_SECRET: z.string().optional(), AUTH_GOOGLE_ID: z.string().optional(), AUTH_GOOGLE_SECRET: z.string().optional(), + ARIV_API_BASE_URL: z.string().url().optional(), ARIV_API_BASE_URL: z.string().url(), ARIV_API_KEY: z.string().optional(), RATE_LIMIT_WINDOW_MS: z.coerce.number().default(60000), @@ -25,3 +27,11 @@ export const env = envSchema.parse({ RATE_LIMIT_WINDOW_MS: process.env.RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX_REQUESTS: process.env.RATE_LIMIT_MAX_REQUESTS }); + +export function requireArivApiBaseUrl() { + if (!env.ARIV_API_BASE_URL) { + throw new Error("Missing ARIV_API_BASE_URL environment variable."); + } + + return env.ARIV_API_BASE_URL; +} diff --git a/web/middleware.ts b/web/middleware.ts index b2c751e..2b1b895 100644 --- a/web/middleware.ts +++ b/web/middleware.ts @@ -1,3 +1,4 @@ +import { NextResponse } from "next/server"; import { NextResponse, type NextRequest } from "next/server"; import { auth } from "@/auth"; import { env } from "@/lib/env"; diff --git a/web/next.config.js b/web/next.config.js index d597ee2..5723f9b 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -9,6 +9,14 @@ const nextConfig = { reactStrictMode: true, headers: async () => [ { + source: "/(.*)", + headers: [ + { key: "X-Frame-Options", value: "DENY" }, + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" }, + { + key: "Content-Security-Policy", source: '/(.*)', headers: [ { key: 'X-Frame-Options', value: 'DENY' },