TTMT.ManageWebGUI/src/components/forms/login-form.tsx

143 lines
5.6 KiB
TypeScript
Raw Normal View History

2025-12-23 10:57:55 +07:00
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import type { LoginResquest } from "@/types/auth";
import { useMutation } from "@tanstack/react-query";
2026-04-07 13:20:46 +07:00
import { buildGoogleOAuthLoginUrl, login } from "@/services/auth.service";
2025-12-23 10:57:55 +07:00
import { useState } from "react";
import { useNavigate, useRouter } from "@tanstack/react-router";
import { Route } from "@/routes/(auth)/login";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { useAuth } from "@/hooks/useAuth";
import { LoaderCircle } from "lucide-react";
export function LoginForm({ className }: React.ComponentProps<"form">) {
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [formData, setFormData] = useState<LoginResquest>({
username: "",
password: "",
});
const auth = useAuth();
const router = useRouter();
const navigate = useNavigate();
const search = Route.useSearch() as { redirect?: string };
const mutation = useMutation({
mutationFn: login,
async onSuccess(data) {
localStorage.setItem("accesscontrol.auth.user", data.username!);
localStorage.setItem("token", data.token!);
localStorage.setItem("name", data.name!);
localStorage.setItem("acs", (data.access ?? "").toString());
localStorage.setItem("role", data.role.roleName ?? "");
localStorage.setItem("priority", (data.role.priority ?? 0).toString());
auth.setAuthenticated(true);
auth.login(data.username!);
await router.invalidate();
await navigate({ to: search.redirect || "/dashboard" });
},
onError(error) {
setErrorMessage(error.message || "Login failed");
}
});
2026-04-07 13:20:46 +07:00
const handleGoogleLogin = () => {
const returnUrl = new URL("/oauth/callback", window.location.origin);
2026-04-01 16:46:33 +07:00
if (search.redirect) {
returnUrl.searchParams.set("redirect", search.redirect);
}
2026-04-07 13:20:46 +07:00
window.location.assign(buildGoogleOAuthLoginUrl(returnUrl.toString()));
2026-04-01 16:46:33 +07:00
};
2025-12-23 10:57:55 +07:00
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setErrorMessage(null);
mutation.mutate(formData);
};
return (
<div className={cn("flex flex-col gap-6", className)}>
<Card>
2026-04-01 16:46:33 +07:00
<CardHeader className="text-center">
<CardTitle className="text-2xl font-semibold tracking-tight flex items-center justify-center gap-3">
<img src="/soict_logo.png" alt="SOICT logo" className="h-7 w-auto object-contain" />
<span>Computer Management</span>
2025-12-23 10:57:55 +07:00
</CardTitle>
<CardDescription>Hệ thống quản phòng máy thực hành</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<div className="grid gap-6">
<div className="grid gap-3">
<Label htmlFor="email">Tên đăng nhập</Label>
<Input
id="email"
type="text"
autoFocus
required
value={formData.username}
onChange={(e) =>
setFormData({ ...formData, username: e.target.value })
}
/>
</div>
<div className="grid gap-3">
<div className="flex items-center">
<Label htmlFor="password">Mật khẩu</Label>
</div>
<Input
id="password"
type="password"
required
value={formData.password}
onChange={(e) =>
setFormData({ ...formData, password: e.target.value })
}
/>
</div>
{errorMessage && (
<div className="text-destructive text-sm font-medium">{errorMessage}</div>
)}
{mutation.isPending ? (
<Button className="w-full" disabled>
<LoaderCircle className="w-4 h-4 mr-1 animate-spin" />
Đang đăng nhập
</Button>
) : (
<Button type="submit" className="w-full">
Đăng nhập
</Button>
)}
2026-04-01 16:46:33 +07:00
<div className="text-center text-sm text-muted-foreground">Hoặc</div>
2026-04-07 13:20:46 +07:00
<Button type="button" variant="outline" className="w-full gap-2" onClick={handleGoogleLogin}>
2026-04-01 16:46:33 +07:00
<svg viewBox="0 0 24 24" aria-hidden="true" className="h-4 w-4">
2026-04-07 13:20:46 +07:00
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.02 5.02 0 0 1-2.18 3.29v2.74h3.52c2.05-1.89 3.3-4.67 3.3-8.04Z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.52-2.74c-.98.66-2.23 1.06-3.76 1.06-2.89 0-5.33-1.95-6.2-4.56H2.18v2.84A11 11 0 0 0 12 23Z"
/>
<path
fill="#FBBC05"
d="M5.8 14.1A6.62 6.62 0 0 1 5.45 12c0-.73.13-1.44.35-2.1V7.06H2.18A11 11 0 0 0 1 12c0 1.77.42 3.44 1.18 4.94L5.8 14.1Z"
/>
<path
fill="#EA4335"
d="M12 5.34c1.61 0 3.05.56 4.18 1.64l3.14-3.14C17.45 2.09 14.97 1 12 1a11 11 0 0 0-9.82 6.06L5.8 9.9C6.67 7.29 9.11 5.34 12 5.34Z"
/>
2026-04-01 16:46:33 +07:00
</svg>
2026-04-07 13:20:46 +07:00
Đăng nhập với Google
2026-04-01 16:46:33 +07:00
</Button>
2025-12-23 10:57:55 +07:00
</div>
</form>
</CardContent>
</Card>
</div>
);
}