Files
personal-ai-assistant/frontend/src/components/auth/register-form.tsx
dolgolyov.alexei 7c752cae6b Phase 1: Foundation — backend auth, frontend shell, Docker setup
Backend (FastAPI):
- App factory with async SQLAlchemy 2.0 + PostgreSQL
- Alembic migration for users and sessions tables
- JWT auth (access + refresh tokens, bcrypt passwords)
- Auth endpoints: register, login, refresh, logout, me
- Admin seed script, role-based access deps

Frontend (React + TypeScript):
- Vite + Tailwind CSS + shadcn/ui theme (health-oriented palette)
- i18n with English and Russian translations
- Zustand auth/UI stores with localStorage persistence
- Axios client with automatic token refresh on 401
- Login/register pages, protected routing
- App layout: collapsible sidebar, header with theme/language toggles
- Dashboard with placeholder stats

Infrastructure:
- Docker Compose (postgres, backend, frontend, nginx)
- Nginx reverse proxy with WebSocket support
- Dev override with hot reload

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:25:02 +03:00

151 lines
5.2 KiB
TypeScript

import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
import { useAuthStore } from "@/stores/auth-store";
import { register } from "@/api/auth";
export function RegisterForm() {
const { t } = useTranslation();
const navigate = useNavigate();
const setAuth = useAuthStore((s) => s.setAuth);
const [email, setEmail] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [fullName, setFullName] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
if (password.length < 8) {
setError(t("auth.errors.passwordMinLength"));
return;
}
if (password !== confirmPassword) {
setError(t("auth.errors.passwordMismatch"));
return;
}
if (!/^[a-zA-Z0-9_-]{3,50}$/.test(username)) {
setError(t("auth.errors.usernameFormat"));
return;
}
setLoading(true);
try {
const data = await register(email, username, password, fullName || undefined);
setAuth(data.user, data.access_token, data.refresh_token);
navigate("/");
} catch (err: unknown) {
const msg =
(err as { response?: { data?: { detail?: string } } })?.response?.data?.detail ||
t("auth.errors.emailExists");
setError(msg);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="rounded-md bg-destructive/10 px-4 py-3 text-sm text-destructive">
{error}
</div>
)}
<div className="space-y-2">
<label htmlFor="email" className="text-sm font-medium">
{t("auth.email")}
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
placeholder="name@example.com"
/>
</div>
<div className="space-y-2">
<label htmlFor="username" className="text-sm font-medium">
{t("auth.username")}
</label>
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
/>
</div>
<div className="space-y-2">
<label htmlFor="fullName" className="text-sm font-medium">
{t("auth.fullName")}
</label>
<input
id="fullName"
type="text"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
/>
</div>
<div className="space-y-2">
<label htmlFor="password" className="text-sm font-medium">
{t("auth.password")}
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={8}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
/>
</div>
<div className="space-y-2">
<label htmlFor="confirmPassword" className="text-sm font-medium">
{t("auth.confirmPassword")}
</label>
<input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
/>
</div>
<button
type="submit"
disabled={loading}
className="inline-flex h-10 w-full items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50 transition-colors"
>
{loading ? t("common.loading") : t("auth.register")}
</button>
<p className="text-center text-sm text-muted-foreground">
{t("auth.hasAccount")}{" "}
<Link to="/login" className="text-primary hover:underline">
{t("auth.login")}
</Link>
</p>
</form>
);
}