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:
arsam
2026-09-18 23:11:11 +03:30
parent 283ab2c102
commit 6313345fab
+367
View File
@@ -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",
},
};