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

@ -2,6 +2,10 @@
import { Loader2 } from 'lucide-react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useAuth } from '@/lib/auth';
import Footer from './Footer';
@ -12,16 +16,19 @@ const SignIn = dynamic(
);
export default function SignInClient() {
const authProvider = process.env.NEXT_PUBLIC_AUTH_PROVIDER || 'stack';
const { provider } = useAuth();
const router = useRouter();
if (authProvider !== 'stack') {
useEffect(() => {
if (provider === 'local') {
router.replace('/auth/login');
}
}, [provider, router]);
if (provider !== 'stack') {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4">Local Authentication</h1>
<p className="text-gray-600">Local authentication is enabled. No sign-in required.</p>
</div>
<Footer />
<Loader2 className="w-5 h-5 animate-spin text-gray-600" />
</div>
);
}

View file

@ -14,7 +14,6 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { isOSSMode } from "@/lib/utils";
import { NodeContent } from "./common/NodeContent";
import { NodeEditDialog } from "./common/NodeEditDialog";
@ -350,21 +349,19 @@ const StartCallEditForm = ({
Add Global Prompt
</Label>
</div>
{!isOSSMode() && (
<div className="flex items-center space-x-2">
<Switch
id="detect-voicemail"
checked={detectVoicemail}
onCheckedChange={setDetectVoicemail}
/>
<Label htmlFor="detect-voicemail">
Detect Voicemail
</Label>
<Label className="text-xs text-muted-foreground">
Automatically detect and end call if voicemail is reached.
</Label>
</div>
)}
<div className="flex items-center space-x-2">
<Switch
id="detect-voicemail"
checked={detectVoicemail}
onCheckedChange={setDetectVoicemail}
/>
<Label htmlFor="detect-voicemail">
Detect Voicemail
</Label>
<Label className="text-xs text-muted-foreground">
Automatically detect and end call if voicemail is reached.
</Label>
</div>
<div className="flex flex-col space-y-2">
<div className="flex items-center space-x-2">
<Switch

View file

@ -36,8 +36,7 @@ export const TriggerNode = memo(({ data, selected, id }: TriggerNodeProps) => {
const [triggerPath] = useState(() => data.trigger_path ?? crypto.randomUUID());
// Get backend URL from app config (fetched from backend health endpoint)
// Falls back to env variable, then to localhost for local development
const backendUrl = config?.backendApiEndpoint || process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8000";
const backendUrl = config?.backendApiEndpoint || "http://localhost:8000";
const endpoint = `${backendUrl}/api/v1/public/agent/${triggerPath}`;
// Copy state for button feedback

View file

@ -21,8 +21,8 @@ const AppLayout: React.FC<AppLayoutProps> = ({
const pathname = usePathname();
// Check if current route should have sidebar
// Hide sidebar for root (/) and /handler routes (Stack Auth routes)
const shouldShowSidebar = pathname !== "/" && !pathname.startsWith("/handler");
// Hide sidebar for root (/), /handler routes (Stack Auth routes), and /auth routes
const shouldShowSidebar = pathname !== "/" && !pathname.startsWith("/handler") && !pathname.startsWith("/auth");
// Check if we're in workflow editor mode or superadmin runs - collapse sidebar by default
const isWorkflowEditor = /^\/workflow\/\d+/.test(pathname);

View file

@ -8,7 +8,6 @@ import {
CircleDollarSign,
Database,
FileText,
HelpCircle,
Home,
Key,
LogOut,
@ -24,7 +23,7 @@ import {
} from "lucide-react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import React from "react";
import React, { useRef } from "react";
import ThemeToggle from "@/components/ThemeSwitcher";
import { Button } from "@/components/ui/button";
@ -57,6 +56,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useAppConfig } from "@/context/AppConfigContext";
import type { LocalUser } from "@/lib/auth";
import { useAuth } from "@/lib/auth";
import { cn } from "@/lib/utils";
@ -75,7 +75,14 @@ export function AppSidebar() {
const { config } = useAppConfig();
// Get selected team for Stack auth (cast to Team type from Stack)
const selectedTeam = provider === "stack" && getSelectedTeam ? getSelectedTeam() as Team | null : null;
// Stabilize the reference so SelectedTeamSwitcher only sees a change when the team ID changes,
// preventing unnecessary PATCH calls to Stack Auth on every route navigation.
const selectedTeamRef = useRef<Team | null>(null);
const rawSelectedTeam = provider === "stack" && getSelectedTeam ? getSelectedTeam() as Team | null : null;
if (rawSelectedTeam?.id !== selectedTeamRef.current?.id) {
selectedTeamRef.current = rawSelectedTeam;
}
const selectedTeam = selectedTeamRef.current;
// Version info from app config context
const versionInfo = config ? { ui: config.uiVersion, api: config.apiVersion } : null;
@ -358,54 +365,45 @@ export function AppSidebar() {
)}>
{/* Bottom Actions */}
<div className="space-y-2">
{/* Get Help - for OSS mode */}
{/* User Button - for local/OSS mode */}
{provider !== "stack" && (
<>
{state === "collapsed" ? (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="w-full hover:bg-accent hover:text-accent-foreground"
asChild
>
<a
href="https://github.com/dograh-hq/dograh/issues/new/choose"
target="_blank"
rel="noopener noreferrer"
>
<HelpCircle className="h-4 w-4" />
<span className="sr-only">Get Help</span>
</a>
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>Get Help</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<Button
variant="ghost"
className="w-full justify-start hover:bg-accent hover:text-accent-foreground"
asChild
>
<a
href="https://github.com/dograh-hq/dograh/issues/new/choose"
target="_blank"
rel="noopener noreferrer"
>
<HelpCircle className="h-4 w-4" />
<span className="ml-2">Get Help</span>
</a>
</Button>
)}
</>
<div className={cn(
"flex",
state === "collapsed" ? "justify-center" : "justify-start"
)}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-full h-8 w-8 cursor-pointer">
<span className="text-xs font-medium">
{(user?.displayName || (user as LocalUser | undefined)?.email || "")
.split(/[\s@]/)
.filter(Boolean)
.slice(0, 2)
.map((s: string) => s[0]?.toUpperCase())
.join("")
|| "U"}
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start" className="w-56">
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
{(user as LocalUser | undefined)?.email && (
<p className="text-xs text-muted-foreground">{(user as LocalUser).email}</p>
)}
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => logout()} className="cursor-pointer">
<LogOut className="mr-2 h-4 w-4" />
Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
{/* User Button - at the bottom */}
{/* User Button - for Stack auth */}
{provider === "stack" && (
<div className={cn(
"flex",

View file

@ -6,6 +6,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useAppConfig } from '@/context/AppConfigContext';
import { useAuth } from '@/lib/auth';
import logger from '@/lib/logger';
@ -45,6 +46,7 @@ export function LiveAudioPlayer({
const nextStartTimeRef = useRef(0);
const animationFrameRef = useRef<number | undefined>(undefined);
const isConnectingRef = useRef(false);
const { config } = useAppConfig();
const { user, getAccessToken } = useAuth();
// Auto-start streaming when session starts
@ -98,7 +100,7 @@ export function LiveAudioPlayer({
const accessToken = await getAccessToken();
// Create WebSocket connection
const baseUrl = process.env.NEXT_PUBLIC_BACKEND_URL?.replace('http', 'ws') || 'ws://localhost:8000';
const baseUrl = (config?.backendApiEndpoint || 'http://localhost:8000').replace(/^http/, 'ws');
const wsUrl = `${baseUrl}/api/v1/looptalk/test-sessions/${testSessionId}/audio-stream?role=${audioRole}&token=${encodeURIComponent(accessToken || '')}`;
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
@ -199,7 +201,7 @@ export function LiveAudioPlayer({
} finally {
isConnectingRef.current = false;
}
}, [testSessionId, audioRole, user, getAccessToken, volume, monitorAudioLevel]); // Removed connectionStatus to avoid loops
}, [testSessionId, audioRole, user, getAccessToken, volume, monitorAudioLevel, config]); // Removed connectionStatus to avoid loops
const disconnect = useCallback(() => {
if (wsRef.current) {