feat: add authentication for OSS (#167)

* feat: add authentication for OSS

Fixes #157 and #156

* fix: fix token generation

* fix: limit fastapi workers to 1
This commit is contained in:
Abhishek 2026-02-20 18:21:24 +05:30 committed by GitHub
parent 0791975864
commit 642cc34e8c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 994 additions and 303 deletions

View file

@ -0,0 +1,27 @@
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
const OSS_TOKEN_COOKIE = 'dograh_auth_token';
const OSS_USER_COOKIE = 'dograh_auth_user';
export async function POST() {
const cookieStore = await cookies();
cookieStore.set(OSS_TOKEN_COOKIE, '', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 0,
path: '/',
});
cookieStore.set(OSS_USER_COOKIE, '', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 0,
path: '/',
});
return NextResponse.json({ success: true });
}

View file

@ -1,19 +1,18 @@
/*
Provides authentication token to LocalProviderWrapper once loaded
in the browser
in the browser.
Returns 401 if no token cookie exists (user needs to log in).
*/
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
const OSS_TOKEN_COOKIE = 'dograh_oss_token';
const OSS_USER_COOKIE = 'dograh_oss_user';
import { getAuthProvider } from '@/lib/auth/config';
function generateOSSToken(): string {
return `oss_${Date.now()}_${crypto.randomUUID()}`;
}
const OSS_TOKEN_COOKIE = 'dograh_auth_token';
const OSS_USER_COOKIE = 'dograh_auth_user';
export async function GET() {
const authProvider = process.env.NEXT_PUBLIC_AUTH_PROVIDER || 'stack';
const authProvider = await getAuthProvider();
// Only handle OSS mode
if (authProvider !== 'local') {
@ -21,40 +20,17 @@ export async function GET() {
}
const cookieStore = await cookies();
let token = cookieStore.get(OSS_TOKEN_COOKIE)?.value;
let user = cookieStore.get(OSS_USER_COOKIE)?.value;
const token = cookieStore.get(OSS_TOKEN_COOKIE)?.value;
const user = cookieStore.get(OSS_USER_COOKIE)?.value;
// If no token exists, create one
// If no token exists, return 401 (user needs to sign up or log in)
if (!token) {
token = generateOSSToken();
user = JSON.stringify({
id: token,
name: 'Local User',
provider: 'local',
organizationId: `org_${token}`,
});
// Set cookies
cookieStore.set(OSS_TOKEN_COOKIE, token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30, // 30 days
path: '/',
});
cookieStore.set(OSS_USER_COOKIE, user, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30, // 30 days
path: '/',
});
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
// Return the auth info as JSON (safe to expose to client)
// Return the auth info as JSON
return NextResponse.json({
token,
user: JSON.parse(user!),
user: user ? JSON.parse(user) : { id: token, name: 'Local User', provider: 'local' },
});
}

View file

@ -0,0 +1,33 @@
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
const OSS_TOKEN_COOKIE = 'dograh_auth_token';
const OSS_USER_COOKIE = 'dograh_auth_user';
export async function POST(request: NextRequest) {
const { token, user } = await request.json();
if (!token) {
return NextResponse.json({ error: 'Missing token' }, { status: 400 });
}
const cookieStore = await cookies();
cookieStore.set(OSS_TOKEN_COOKIE, token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30,
path: '/',
});
cookieStore.set(OSS_USER_COOKIE, JSON.stringify(user), {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30,
path: '/',
});
return NextResponse.json({ success: true });
}

View file

@ -0,0 +1,8 @@
import { NextResponse } from 'next/server';
import { getAuthProvider } from '@/lib/auth/config';
export async function GET() {
const provider = await getAuthProvider();
return NextResponse.json({ provider });
}

View file

@ -6,28 +6,50 @@ import type { HealthResponse } from "@/client/types.gen";
// Import version from package.json at build time
import packageJson from "../../../../../package.json";
// Internal/local URLs that are not reachable from the browser
const INTERNAL_HOST_RE = /^https?:\/\/(localhost|127\.0\.0\.1|api)(:\d+)?(\/|$)/;
function isInternalUrl(url: string | undefined | null): boolean {
return !url || INTERNAL_HOST_RE.test(url);
}
export async function GET() {
const uiVersion = packageJson.version || "dev";
// Fetch backend version and config from health endpoint
let apiVersion = "unknown";
let backendApiEndpoint: string | null = null;
let deploymentMode = "oss";
let authProvider = "local";
try {
const response = await healthApiV1HealthGet();
if (response.data) {
const data = response.data as HealthResponse;
apiVersion = data.version;
// Pass through the backend's own endpoint for display purposes
backendApiEndpoint = data.backend_api_endpoint;
deploymentMode = data.deployment_mode;
authProvider = data.auth_provider;
}
} catch {
// Backend might not be reachable during build or in some deployments
apiVersion = "unavailable";
}
// For the API client base URL: prefer BACKEND_URL env, fall back to
// health endpoint value. Skip internal/Docker-only URLs (e.g. http://api:8000)
// that aren't reachable from the browser — the client will keep using
// window.location.origin via the Next.js proxy instead.
const clientCandidate = process.env.BACKEND_URL || backendApiEndpoint;
const clientApiBaseUrl = isInternalUrl(clientCandidate) ? 'http://localhost:8000' : clientCandidate;
return NextResponse.json({
ui: uiVersion,
api: apiVersion,
backendApiEndpoint,
clientApiBaseUrl,
deploymentMode,
authProvider,
});
}