Add LDAP and improved authentication configuration.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 9111ef36-26c8-4085-84ca-a35dc1fec1b5
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7083d608-d6d3-4a6a-9a27-6286c5109627/75b5c5a0-cf3f-429b-bffd-c6bd4e6c914d.jpg
This commit is contained in:
alphaeusmote
2025-04-10 03:15:38 +00:00
parent 3a902bc645
commit 8131136bfb
2 changed files with 246 additions and 53 deletions
+242 -50
View File
@@ -3,7 +3,7 @@ import { useForm } from "react-hook-form";
import { z } from "zod";
import { useAuth } from "@/hooks/use-auth";
import { useOrganization } from "@/context/organization-context";
import { Redirect } from "wouter";
import { Redirect, useLocation } from "wouter";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
@@ -11,8 +11,12 @@ import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ThemeToggle } from "@/components/shared/theme-toggle";
import { useTheme } from "@/hooks/use-theme";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Separator } from "@/components/ui/separator";
import { Loader2 } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
const loginSchema = z.object({
username: z.string().min(3, "Username must be at least 3 characters"),
@@ -27,10 +31,36 @@ const registerSchema = z.object({
organizationId: z.string().optional(),
});
// Define LDAP login schema
const ldapSchema = z.object({
username: z.string().min(1, "Username is required"),
password: z.string().min(1, "Password is required"),
});
// Type for Auth config from API
interface AuthConfig {
localAuthEnabled: boolean;
registrationEnabled: boolean;
ldapEnabled: boolean;
oidcEnabled: boolean;
oidcButtonText: string;
}
export default function AuthPage() {
const { user, loginMutation, registerMutation } = useAuth();
const { theme } = useTheme();
const { organizations, isLoading: orgsLoading } = useOrganization();
const [location, setLocation] = useLocation();
// Fetch authentication configuration
const { data: authConfig, isLoading: configLoading } = useQuery<AuthConfig>({
queryKey: ['/api/auth/config'],
staleTime: 1000 * 60 * 5, // 5 minutes
});
// State for managing LDAP login
const [ldapLoginPending, setLdapLoginPending] = useState(false);
const [authError, setAuthError] = useState<string | null>(null);
const loginForm = useForm<z.infer<typeof loginSchema>>({
resolver: zodResolver(loginSchema),
@@ -40,6 +70,14 @@ export default function AuthPage() {
},
});
const ldapForm = useForm<z.infer<typeof ldapSchema>>({
resolver: zodResolver(ldapSchema),
defaultValues: {
username: "",
password: "",
},
});
const registerForm = useForm<z.infer<typeof registerSchema>>({
resolver: zodResolver(registerSchema),
defaultValues: {
@@ -52,11 +90,53 @@ export default function AuthPage() {
});
const onLoginSubmit = (values: z.infer<typeof loginSchema>) => {
loginMutation.mutate(values);
setAuthError(null);
loginMutation.mutate(values, {
onError: (error: any) => {
setAuthError(error.message || "Login failed. Please check your credentials.");
}
});
};
const onLdapSubmit = async (values: z.infer<typeof ldapSchema>) => {
try {
setLdapLoginPending(true);
setAuthError(null);
const response = await fetch('/api/auth/ldap', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(values),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'LDAP authentication failed');
}
// Force refresh user data
window.location.href = '/';
} catch (error: any) {
setAuthError(error.message || "LDAP login failed. Please check your credentials.");
} finally {
setLdapLoginPending(false);
}
};
const onRegisterSubmit = (values: z.infer<typeof registerSchema>) => {
registerMutation.mutate(values);
setAuthError(null);
registerMutation.mutate(values, {
onError: (error: any) => {
setAuthError(error.message || "Registration failed. Please try again.");
}
});
};
// Function to handle OIDC login
const handleOidcLogin = () => {
window.location.href = '/api/auth/oidc';
};
// Redirect if already logged in
@@ -91,61 +171,173 @@ export default function AuthPage() {
</div>
<CardTitle className="text-2xl">Welcome</CardTitle>
<CardDescription>
Sign in to your account or create a new one
{configLoading
? "Loading..."
: authConfig?.registrationEnabled
? "Sign in to your account or create a new one"
: "Sign in to your account"
}
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="login" className="w-full">
<TabsList className="grid w-full grid-cols-2 mb-4">
<TabsList className={`grid w-full ${(!configLoading && authConfig?.registrationEnabled) ? 'grid-cols-2' : 'grid-cols-1'} mb-4`}>
<TabsTrigger value="login">Login</TabsTrigger>
<TabsTrigger value="register">Register</TabsTrigger>
{(!configLoading && authConfig?.registrationEnabled) && (
<TabsTrigger value="register">Register</TabsTrigger>
)}
</TabsList>
<TabsContent value="login">
<Form {...loginForm}>
<form onSubmit={loginForm.handleSubmit(onLoginSubmit)} className="space-y-4">
<FormField
control={loginForm.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username or Email</FormLabel>
<FormControl>
<Input placeholder="Enter your username or email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={loginForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full"
disabled={loginMutation.isPending}
>
{loginMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Logging in...
</>
) : (
"Login"
)}
</Button>
</form>
</Form>
{authError && (
<Alert variant="destructive" className="mb-4">
<AlertTitle>Authentication Error</AlertTitle>
<AlertDescription>{authError}</AlertDescription>
</Alert>
)}
{configLoading ? (
<div className="flex justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
) : (
<>
{/* Local Authentication */}
{authConfig?.localAuthEnabled && (
<Form {...loginForm}>
<form onSubmit={loginForm.handleSubmit(onLoginSubmit)} className="space-y-4">
<FormField
control={loginForm.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username or Email</FormLabel>
<FormControl>
<Input placeholder="Enter your username or email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={loginForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full"
disabled={loginMutation.isPending}
>
{loginMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Logging in...
</>
) : (
"Login"
)}
</Button>
</form>
</Form>
)}
{/* LDAP Authentication */}
{authConfig?.ldapEnabled && (
<>
{authConfig?.localAuthEnabled && (
<div className="my-6 flex items-center">
<div className="flex-1 h-px bg-border"></div>
<p className="mx-4 text-sm text-muted-foreground">Or login with LDAP</p>
<div className="flex-1 h-px bg-border"></div>
</div>
)}
<Form {...ldapForm}>
<form onSubmit={ldapForm.handleSubmit(onLdapSubmit)} className="space-y-4">
<FormField
control={ldapForm.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>LDAP Username</FormLabel>
<FormControl>
<Input placeholder="Enter your LDAP username" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={ldapForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>LDAP Password</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full"
disabled={ldapLoginPending}
variant="outline"
>
{ldapLoginPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
LDAP Authentication...
</>
) : (
"Login with LDAP"
)}
</Button>
</form>
</Form>
</>
)}
{/* OIDC Authentication */}
{authConfig?.oidcEnabled && (
<>
{(authConfig?.localAuthEnabled || authConfig?.ldapEnabled) && (
<div className="my-6 flex items-center">
<div className="flex-1 h-px bg-border"></div>
<p className="mx-4 text-sm text-muted-foreground">Or</p>
<div className="flex-1 h-px bg-border"></div>
</div>
)}
<Button
onClick={handleOidcLogin}
className="w-full"
variant="secondary"
>
{authConfig?.oidcButtonText || "Sign in with OpenID Connect"}
</Button>
</>
)}
{!authConfig?.localAuthEnabled && !authConfig?.ldapEnabled && !authConfig?.oidcEnabled && (
<Alert className="mb-4">
<AlertTitle>Authentication Error</AlertTitle>
<AlertDescription>No authentication methods are enabled. Please contact your administrator.</AlertDescription>
</Alert>
)}
</>
)}
</TabsContent>
<TabsContent value="register">
+4 -3
View File
@@ -12,7 +12,7 @@ import { z } from "zod";
import 'dotenv/config';
// Authentication configuration from environment variables
const CONFIG = {
export const CONFIG = {
// Local auth configuration
LOCAL_AUTH_ENABLED: process.env.LOCAL_AUTH_ENABLED !== 'false', // Enabled by default
DISABLE_REGISTRATION: process.env.DISABLE_REGISTRATION === 'true', // Disabled by default
@@ -36,7 +36,8 @@ const CONFIG = {
OIDC_CLIENT_SECRET: process.env.OIDC_CLIENT_SECRET || '',
OIDC_CALLBACK_URL: process.env.OIDC_CALLBACK_URL || 'http://localhost:5000/api/auth/oidc/callback',
OIDC_SCOPE: process.env.OIDC_SCOPE || 'openid profile email',
OIDC_AUTO_REGISTER: process.env.OIDC_AUTO_REGISTER === 'true'
OIDC_AUTO_REGISTER: process.env.OIDC_AUTO_REGISTER === 'true',
OIDC_BUTTON_TEXT: process.env.OIDC_BUTTON_TEXT || 'Sign in with OpenID Connect'
};
declare global {
@@ -408,7 +409,7 @@ export function setupAuth(app: Express) {
registrationEnabled: !CONFIG.DISABLE_REGISTRATION,
ldapEnabled: CONFIG.LDAP_ENABLED,
oidcEnabled: CONFIG.OIDC_ENABLED,
oidcButtonText: process.env.OIDC_BUTTON_TEXT || "Sign in with OpenID Connect"
oidcButtonText: CONFIG.OIDC_BUTTON_TEXT
});
});