Import source: src/features/auth/auth-screen
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Eye, EyeOff, KeyRound, LockKeyhole, Server, ShieldCheck, Wifi } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { Logo } from "@/components/common/logo";
|
||||
import { AmbientBackground } from "@/components/shell/ambient-background";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { panelService } from "@/api/services/panel-service";
|
||||
|
||||
const authHighlights = [
|
||||
{ icon: ShieldCheck, label: "Secure authentication flows" },
|
||||
{ icon: KeyRound, label: "Typed frontend API contracts" },
|
||||
{ icon: Server, label: "Backend-ready service wiring" },
|
||||
];
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email("Enter a valid email address."),
|
||||
password: z.string().min(8, "Password must be at least 8 characters."),
|
||||
remember: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type LoginValues = z.infer<typeof loginSchema>;
|
||||
|
||||
export type AuthMode =
|
||||
| "login"
|
||||
| "logout"
|
||||
| "forgot-password"
|
||||
| "reset-password"
|
||||
| "two-factor"
|
||||
| "recovery-code"
|
||||
| "passkey"
|
||||
| "device-verification"
|
||||
| "session-expired"
|
||||
| "account-locked"
|
||||
| "invitation"
|
||||
| "setup";
|
||||
|
||||
export function AuthScreen({ mode }: { mode: AuthMode }) {
|
||||
const router = useRouter();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [mockError, setMockError] = useState("");
|
||||
const form = useForm<LoginValues>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
defaultValues: { email: "owner@example.com", password: "", remember: true },
|
||||
});
|
||||
|
||||
async function onLogin(values: LoginValues) {
|
||||
setLoading(true);
|
||||
setMockError("");
|
||||
const response = await panelService.auth
|
||||
.login(values.email, values.password)
|
||||
.catch((error: unknown): Awaited<ReturnType<typeof panelService.auth.login>> => {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "The backend API is unreachable.";
|
||||
return {
|
||||
data: null,
|
||||
meta: { requestId: "client_network_error" },
|
||||
error: {
|
||||
code: "NETWORK_ERROR",
|
||||
message,
|
||||
fields: {},
|
||||
},
|
||||
};
|
||||
});
|
||||
setLoading(false);
|
||||
if (response.error) {
|
||||
setMockError(response.error.message);
|
||||
for (const [field, message] of Object.entries(response.error.fields ?? {})) {
|
||||
if (field === "email" || field === "password") {
|
||||
form.setError(field, { message });
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (response.data?.challenge === "totp") {
|
||||
router.push("/two-factor");
|
||||
return;
|
||||
}
|
||||
if (values.email.includes("locked")) {
|
||||
router.push("/account-locked");
|
||||
return;
|
||||
}
|
||||
if (values.password.toLowerCase().includes("error")) {
|
||||
setMockError("The supplied credentials were rejected.");
|
||||
return;
|
||||
}
|
||||
router.push("/dashboard");
|
||||
}
|
||||
|
||||
if (mode !== "login") return <SecondaryAuthScreen mode={mode} />;
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<Logo className="mb-3 h-10 w-40" />
|
||||
<CardTitle>Sign in to CoaxysPanel</CardTitle>
|
||||
<CardDescription>Server Hosting Control Center</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mb-4 grid gap-2 rounded-md border border-border/60 bg-muted/40 p-3 text-xs">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<Server className="h-4 w-4" /> srv-dal-01.coaxys.net
|
||||
</span>
|
||||
<Badge variant="success">Online</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Wifi className="h-4 w-4" /> Secure connection verified
|
||||
</div>
|
||||
</div>
|
||||
<form className="space-y-4" onSubmit={form.handleSubmit(onLogin)}>
|
||||
<label className="block space-y-1 text-sm">
|
||||
<span className="font-medium">Email</span>
|
||||
<Input autoComplete="username" {...form.register("email")} />
|
||||
{form.formState.errors.email ? (
|
||||
<span className="text-xs text-destructive">
|
||||
{form.formState.errors.email.message}
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
<label className="block space-y-1 text-sm">
|
||||
<span className="font-medium">Password</span>
|
||||
<span className="relative block">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="current-password"
|
||||
{...form.register("password")}
|
||||
className="pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-2 text-muted-foreground"
|
||||
onClick={() => setShowPassword((value) => !value)}
|
||||
aria-label={showPassword ? "Hide password" : "Show password"}
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
</span>
|
||||
{form.formState.errors.password ? (
|
||||
<span className="text-xs text-destructive">
|
||||
{form.formState.errors.password.message}
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<label className="flex items-center gap-2">
|
||||
<input type="checkbox" {...form.register("remember")} />
|
||||
Remember this device
|
||||
</label>
|
||||
<Link href="/forgot-password" className="text-primary hover:underline">
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
{mockError ? (
|
||||
<div className="anim-rise rounded-md border border-destructive/30 bg-destructive/10 p-2 text-sm text-destructive">
|
||||
{mockError}
|
||||
</div>
|
||||
) : null}
|
||||
<Button className="w-full" disabled={loading}>
|
||||
{loading ? "Signing in..." : "Sign in"}
|
||||
</Button>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button type="button" asChild variant="outline">
|
||||
<Link href="/passkey">
|
||||
<KeyRound className="h-4 w-4" /> Passkey
|
||||
</Link>
|
||||
</Button>
|
||||
<Button type="button" asChild variant="outline">
|
||||
<Link href="/two-factor">
|
||||
<LockKeyhole className="h-4 w-4" /> 2FA
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function SecondaryAuthScreen({ mode }: { mode: Exclude<AuthMode, "login"> }) {
|
||||
const content = authCopy[mode];
|
||||
return (
|
||||
<AuthLayout>
|
||||
<Card className="w-full max-w-lg">
|
||||
<CardHeader>
|
||||
<Logo className="mb-3 h-10 w-40" />
|
||||
<CardTitle>{content.title}</CardTitle>
|
||||
<CardDescription>{content.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="rounded-md border border-border/60 bg-muted/40 p-4">
|
||||
<div className="mb-3 flex items-center gap-2 text-sm font-medium">
|
||||
<ShieldCheck className="h-4 w-4 text-primary" />
|
||||
{content.status}
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
{content.fields.map((field) => (
|
||||
<label key={field} className="space-y-1 text-sm">
|
||||
<span className="font-medium">{field}</span>
|
||||
<Input
|
||||
placeholder={field}
|
||||
type={field.toLowerCase().includes("password") ? "password" : "text"}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/login">Back to login</Link>
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<Link href={mode === "logout" ? "/login" : "/dashboard"}>{content.action}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<main className="relative grid min-h-screen grid-cols-1 lg:grid-cols-[1fr_520px]">
|
||||
<AmbientBackground />
|
||||
<section className="relative hidden overflow-hidden p-10 lg:flex lg:flex-col lg:justify-between xl:p-14">
|
||||
<Image
|
||||
src="/art/auth-glass.jpg"
|
||||
alt=""
|
||||
fill
|
||||
priority
|
||||
sizes="(min-width: 1024px) 60vw, 0px"
|
||||
className="hidden object-cover object-center opacity-45 dark:block"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 hidden bg-gradient-to-r from-background/40 via-transparent to-background dark:block"
|
||||
/>
|
||||
<div className="anim-rise relative">
|
||||
<Logo className="h-12 w-48" />
|
||||
<h1 className="text-gradient mt-14 max-w-xl font-display text-5xl font-semibold leading-[1.05] tracking-tight xl:text-6xl">
|
||||
CoaxysPanel
|
||||
</h1>
|
||||
<p className="mt-5 max-w-xl text-lg text-muted-foreground">
|
||||
Dense, secure hosting administration for websites, domains, files, databases, mail,
|
||||
automation, and server operations.
|
||||
</p>
|
||||
</div>
|
||||
<div className="stagger relative grid max-w-xl gap-3 text-sm">
|
||||
{authHighlights.map(({ icon: Icon, label }) => (
|
||||
<div key={label} className="glass-panel flex items-center gap-3 rounded-lg p-3.5">
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
|
||||
<Icon className="h-4 w-4" />
|
||||
</span>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className="anim-rise flex items-center justify-center p-4 sm:p-6">
|
||||
{children}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const authCopy: Record<
|
||||
Exclude<AuthMode, "login">,
|
||||
{ title: string; description: string; status: string; fields: string[]; action: string }
|
||||
> = {
|
||||
logout: {
|
||||
title: "You are signed out",
|
||||
description: "The mock session was closed locally.",
|
||||
status: "Session token cleared in the frontend state.",
|
||||
fields: [],
|
||||
action: "Return to login",
|
||||
},
|
||||
"forgot-password": {
|
||||
title: "Forgot password",
|
||||
description: "Request a reset link for your CoaxysPanel account.",
|
||||
status: "A mock reset email will be queued.",
|
||||
fields: ["Email address"],
|
||||
action: "Send reset link",
|
||||
},
|
||||
"reset-password": {
|
||||
title: "Reset password",
|
||||
description: "Choose a new password after validating the reset token.",
|
||||
status: "Password policy and strength validation are active.",
|
||||
fields: ["Reset token", "New password", "Confirm password"],
|
||||
action: "Reset password",
|
||||
},
|
||||
"two-factor": {
|
||||
title: "Two-factor authentication",
|
||||
description: "Enter the six-digit code from your authenticator app.",
|
||||
status: "TOTP challenge required for this mock account.",
|
||||
fields: ["Authentication code"],
|
||||
action: "Verify code",
|
||||
},
|
||||
"recovery-code": {
|
||||
title: "Recovery code challenge",
|
||||
description: "Use a saved recovery code when the authenticator is unavailable.",
|
||||
status: "Recovery-code validation is simulated.",
|
||||
fields: ["Recovery code"],
|
||||
action: "Verify recovery code",
|
||||
},
|
||||
passkey: {
|
||||
title: "Passkey authentication",
|
||||
description: "Use a passkey registered with this account.",
|
||||
status: "Browser passkey ceremony is represented as a mock flow.",
|
||||
fields: ["Account email"],
|
||||
action: "Use passkey",
|
||||
},
|
||||
"device-verification": {
|
||||
title: "Device verification",
|
||||
description: "Confirm this device before opening a privileged session.",
|
||||
status: "Verification code sent to the mock contact method.",
|
||||
fields: ["Verification code"],
|
||||
action: "Trust device",
|
||||
},
|
||||
"session-expired": {
|
||||
title: "Session expired",
|
||||
description: "Your session reached the configured timeout.",
|
||||
status: "Re-authentication is required.",
|
||||
fields: ["Password"],
|
||||
action: "Resume session",
|
||||
},
|
||||
"account-locked": {
|
||||
title: "Account locked",
|
||||
description: "Too many failed login attempts were detected.",
|
||||
status: "A support operator can unlock the account after verification.",
|
||||
fields: ["Unlock request note"],
|
||||
action: "Request unlock",
|
||||
},
|
||||
invitation: {
|
||||
title: "Accept invitation",
|
||||
description: "Join a CoaxysPanel team with the assigned role.",
|
||||
status: "Invitation token and role preview are ready.",
|
||||
fields: ["Invitation token", "Display name", "Password"],
|
||||
action: "Accept invitation",
|
||||
},
|
||||
setup: {
|
||||
title: "First-run setup",
|
||||
description:
|
||||
"Configure the first server, account, security policy, branding, and notifications.",
|
||||
status: "Setup wizard writes only to mock frontend state.",
|
||||
fields: ["Server hostname", "Admin email", "Primary domain", "Timezone"],
|
||||
action: "Finish setup",
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user