mirror of
https://github.com/freedbygrace/DynamoDNS.git
synced 2026-08-31 20:58:05 +00:00
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:
+242
-50
@@ -3,7 +3,7 @@ import { useForm } from "react-hook-form";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { useOrganization } from "@/context/organization-context";
|
import { useOrganization } from "@/context/organization-context";
|
||||||
import { Redirect } from "wouter";
|
import { Redirect, useLocation } from "wouter";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { ThemeToggle } from "@/components/shared/theme-toggle";
|
import { ThemeToggle } from "@/components/shared/theme-toggle";
|
||||||
import { useTheme } from "@/hooks/use-theme";
|
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 { Loader2 } from "lucide-react";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
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({
|
const loginSchema = z.object({
|
||||||
username: z.string().min(3, "Username must be at least 3 characters"),
|
username: z.string().min(3, "Username must be at least 3 characters"),
|
||||||
@@ -27,10 +31,36 @@ const registerSchema = z.object({
|
|||||||
organizationId: z.string().optional(),
|
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() {
|
export default function AuthPage() {
|
||||||
const { user, loginMutation, registerMutation } = useAuth();
|
const { user, loginMutation, registerMutation } = useAuth();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const { organizations, isLoading: orgsLoading } = useOrganization();
|
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>>({
|
const loginForm = useForm<z.infer<typeof loginSchema>>({
|
||||||
resolver: zodResolver(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>>({
|
const registerForm = useForm<z.infer<typeof registerSchema>>({
|
||||||
resolver: zodResolver(registerSchema),
|
resolver: zodResolver(registerSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -52,11 +90,53 @@ export default function AuthPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onLoginSubmit = (values: z.infer<typeof loginSchema>) => {
|
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>) => {
|
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
|
// Redirect if already logged in
|
||||||
@@ -91,61 +171,173 @@ export default function AuthPage() {
|
|||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-2xl">Welcome</CardTitle>
|
<CardTitle className="text-2xl">Welcome</CardTitle>
|
||||||
<CardDescription>
|
<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>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Tabs defaultValue="login" className="w-full">
|
<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="login">Login</TabsTrigger>
|
||||||
<TabsTrigger value="register">Register</TabsTrigger>
|
{(!configLoading && authConfig?.registrationEnabled) && (
|
||||||
|
<TabsTrigger value="register">Register</TabsTrigger>
|
||||||
|
)}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="login">
|
<TabsContent value="login">
|
||||||
<Form {...loginForm}>
|
{authError && (
|
||||||
<form onSubmit={loginForm.handleSubmit(onLoginSubmit)} className="space-y-4">
|
<Alert variant="destructive" className="mb-4">
|
||||||
<FormField
|
<AlertTitle>Authentication Error</AlertTitle>
|
||||||
control={loginForm.control}
|
<AlertDescription>{authError}</AlertDescription>
|
||||||
name="username"
|
</Alert>
|
||||||
render={({ field }) => (
|
)}
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Username or Email</FormLabel>
|
{configLoading ? (
|
||||||
<FormControl>
|
<div className="flex justify-center py-8">
|
||||||
<Input placeholder="Enter your username or email" {...field} />
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||||
</FormControl>
|
</div>
|
||||||
<FormMessage />
|
) : (
|
||||||
</FormItem>
|
<>
|
||||||
)}
|
{/* Local Authentication */}
|
||||||
/>
|
{authConfig?.localAuthEnabled && (
|
||||||
<FormField
|
<Form {...loginForm}>
|
||||||
control={loginForm.control}
|
<form onSubmit={loginForm.handleSubmit(onLoginSubmit)} className="space-y-4">
|
||||||
name="password"
|
<FormField
|
||||||
render={({ field }) => (
|
control={loginForm.control}
|
||||||
<FormItem>
|
name="username"
|
||||||
<FormLabel>Password</FormLabel>
|
render={({ field }) => (
|
||||||
<FormControl>
|
<FormItem>
|
||||||
<Input type="password" placeholder="••••••••" {...field} />
|
<FormLabel>Username or Email</FormLabel>
|
||||||
</FormControl>
|
<FormControl>
|
||||||
<FormMessage />
|
<Input placeholder="Enter your username or email" {...field} />
|
||||||
</FormItem>
|
</FormControl>
|
||||||
)}
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
<Button
|
)}
|
||||||
type="submit"
|
/>
|
||||||
className="w-full"
|
<FormField
|
||||||
disabled={loginMutation.isPending}
|
control={loginForm.control}
|
||||||
>
|
name="password"
|
||||||
{loginMutation.isPending ? (
|
render={({ field }) => (
|
||||||
<>
|
<FormItem>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<FormLabel>Password</FormLabel>
|
||||||
Logging in...
|
<FormControl>
|
||||||
</>
|
<Input type="password" placeholder="••••••••" {...field} />
|
||||||
) : (
|
</FormControl>
|
||||||
"Login"
|
<FormMessage />
|
||||||
)}
|
</FormItem>
|
||||||
</Button>
|
)}
|
||||||
</form>
|
/>
|
||||||
</Form>
|
<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>
|
||||||
|
|
||||||
<TabsContent value="register">
|
<TabsContent value="register">
|
||||||
|
|||||||
+4
-3
@@ -12,7 +12,7 @@ import { z } from "zod";
|
|||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
|
|
||||||
// Authentication configuration from environment variables
|
// Authentication configuration from environment variables
|
||||||
const CONFIG = {
|
export const CONFIG = {
|
||||||
// Local auth configuration
|
// Local auth configuration
|
||||||
LOCAL_AUTH_ENABLED: process.env.LOCAL_AUTH_ENABLED !== 'false', // Enabled by default
|
LOCAL_AUTH_ENABLED: process.env.LOCAL_AUTH_ENABLED !== 'false', // Enabled by default
|
||||||
DISABLE_REGISTRATION: process.env.DISABLE_REGISTRATION === 'true', // Disabled 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_CLIENT_SECRET: process.env.OIDC_CLIENT_SECRET || '',
|
||||||
OIDC_CALLBACK_URL: process.env.OIDC_CALLBACK_URL || 'http://localhost:5000/api/auth/oidc/callback',
|
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_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 {
|
declare global {
|
||||||
@@ -408,7 +409,7 @@ export function setupAuth(app: Express) {
|
|||||||
registrationEnabled: !CONFIG.DISABLE_REGISTRATION,
|
registrationEnabled: !CONFIG.DISABLE_REGISTRATION,
|
||||||
ldapEnabled: CONFIG.LDAP_ENABLED,
|
ldapEnabled: CONFIG.LDAP_ENABLED,
|
||||||
oidcEnabled: CONFIG.OIDC_ENABLED,
|
oidcEnabled: CONFIG.OIDC_ENABLED,
|
||||||
oidcButtonText: process.env.OIDC_BUTTON_TEXT || "Sign in with OpenID Connect"
|
oidcButtonText: CONFIG.OIDC_BUTTON_TEXT
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user