mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-10 08:05:22 +02:00
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:
parent
0791975864
commit
642cc34e8c
48 changed files with 994 additions and 303 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue