Added comprehensive authentication and authorization system: Authentication System: - Better Auth integration with session management - Login/logout pages and API routes - Middleware for route protection - Auth utilities and client libraries User Management: - User list, detail, and invite pages - User API endpoints (CRUD operations) - Session management for users - Profile settings page Role-Based Access Control: - Role management pages (list, create, edit) - Permission system with granular controls - Role assignment to users - Role API endpoints Admin Features: - Audit log page for tracking system events - Admin settings page - Audit service for logging user actions Additional Features: - Quotes management pages and components - SalesBldr API integration - Email service for notifications Configuration & Documentation: - Updated docker-compose.yml - MCP server configuration (mcp.json) - CVE-2025-55182 security review documentation - Standards guide and PRD documents - Re-enabling authentication documentation Database Migrations: - 012: Auth tables (users, sessions, accounts, verifications) - 013: Role tables (roles, permissions, role_permissions, user_roles) - 014: Admin settings table UI Updates: - Updated dashboard layout - Enhanced app layout with auth integration
132 lines
3.6 KiB
TypeScript
132 lines
3.6 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { z } from "zod";
|
|
import { Loader2, ShieldCheck } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { useRouter } from "next/navigation";
|
|
import { authClient } from "@/lib/auth-client";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/components/ui/form";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
|
|
const formSchema = z.object({
|
|
code: z.string().length(6, "Code must be 6 digits").regex(/^\d+$/, "Code must be numeric"),
|
|
trustDevice: z.boolean(),
|
|
});
|
|
|
|
type FormData = z.infer<typeof formSchema>;
|
|
|
|
interface TwoFactorFormProps {
|
|
callbackURL?: string;
|
|
}
|
|
|
|
export function TwoFactorForm({ callbackURL = "/" }: TwoFactorFormProps) {
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const router = useRouter();
|
|
|
|
const form = useForm<FormData>({
|
|
resolver: zodResolver(formSchema),
|
|
defaultValues: {
|
|
code: "",
|
|
trustDevice: true,
|
|
},
|
|
});
|
|
|
|
async function onSubmit(data: FormData) {
|
|
setIsLoading(true);
|
|
try {
|
|
const result = await authClient.twoFactor.verifyTotp({
|
|
code: data.code,
|
|
trustDevice: data.trustDevice,
|
|
});
|
|
|
|
if (result.error) {
|
|
toast.error(result.error.message || "Invalid verification code");
|
|
form.setError("code", { message: "Invalid code" });
|
|
return;
|
|
}
|
|
|
|
toast.success("Verified successfully!");
|
|
router.push(callbackURL);
|
|
} catch (error) {
|
|
toast.error("An unexpected error occurred");
|
|
console.error("2FA verification error:", error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
<FormField
|
|
control={form.control}
|
|
name="code"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Verification Code</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder="000000"
|
|
maxLength={6}
|
|
className="text-center text-2xl tracking-widest font-mono"
|
|
{...field}
|
|
disabled={isLoading}
|
|
autoComplete="one-time-code"
|
|
inputMode="numeric"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="trustDevice"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
|
<FormControl>
|
|
<Checkbox
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
disabled={isLoading}
|
|
/>
|
|
</FormControl>
|
|
<div className="space-y-1 leading-none">
|
|
<FormLabel className="text-sm font-normal">
|
|
Trust this device for 30 days
|
|
</FormLabel>
|
|
</div>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<Button type="submit" className="w-full" disabled={isLoading}>
|
|
{isLoading ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Verifying...
|
|
</>
|
|
) : (
|
|
<>
|
|
<ShieldCheck className="mr-2 h-4 w-4" />
|
|
Verify
|
|
</>
|
|
)}
|
|
</Button>
|
|
</form>
|
|
</Form>
|
|
);
|
|
}
|