feat(auth): implement better authentication system with user sessions and password management

- Added user, session, account, and verification models to Prisma schema.
- Integrated better-auth for user authentication with email and password.
- Created auth middleware for session validation.
- Implemented auth context and client in frontend for managing user sessions.
- Added login and settings pages for user authentication and password management.
- Updated routes to include authentication checks and user-specific content.
- Enhanced UI components to reflect authentication state.
This commit is contained in:
Jose Selesan
2026-05-29 16:03:38 -03:00
parent de57ea4bd7
commit 9482fea7f2
29 changed files with 621 additions and 21 deletions

View File

@@ -1,6 +1,14 @@
import { createRootRoute } from "@tanstack/react-router";
import { Layout } from "@/components/layout/Layout";
import { createRootRouteWithContext, Outlet, redirect } from "@tanstack/react-router";
import type { RouterContext } from "@/router";
export const Route = createRootRoute({
component: Layout,
export const Route = createRootRouteWithContext<RouterContext>()({
beforeLoad: async ({ context, location }) => {
if (!context.auth.isAuthenticated && location.pathname !== "/login") {
throw redirect({ to: "/login" });
}
if (context.auth.isAuthenticated && location.pathname === "/login") {
throw redirect({ to: "/" });
}
},
component: Outlet,
});

View File

@@ -0,0 +1,6 @@
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { Layout } from "@/components/layout/Layout";
export const Route = createFileRoute("/_authenticated")({
component: Layout,
});

View File

@@ -1,6 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { ExpensesPage } from "@/features/expenses/ExpensesPage";
export const Route = createFileRoute("/expenses")({
export const Route = createFileRoute("/_authenticated/expenses")({
component: ExpensesPage,
});

View File

@@ -1,6 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { DashboardPage } from "@/features/dashboard/DashboardPage";
export const Route = createFileRoute("/")({
export const Route = createFileRoute("/_authenticated/")({
component: DashboardPage,
});

View File

@@ -1,6 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { BeloPage } from "@/features/belo/BeloPage";
export const Route = createFileRoute("/quotes/belo")({
export const Route = createFileRoute("/_authenticated/quotes/belo")({
component: BeloPage,
});

View File

@@ -1,6 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { QuotesPage } from "@/features/quotes/QuotesPage";
export const Route = createFileRoute("/quotes/")({
export const Route = createFileRoute("/_authenticated/quotes/")({
component: QuotesPage,
});

View File

@@ -1,6 +1,6 @@
import { createFileRoute, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute("/quotes")({
export const Route = createFileRoute("/_authenticated/quotes")({
component: QuotesLayout,
});

View File

@@ -0,0 +1,107 @@
import { useState } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useAuth } from "@/lib/auth-context";
export const Route = createFileRoute("/_authenticated/settings")({
component: SettingsPage,
});
function SettingsPage() {
const auth = useAuth();
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
setSuccess(false);
if (newPassword !== confirmPassword) {
setError("Las contraseñas no coinciden");
return;
}
setLoading(true);
try {
await auth.changePassword(currentPassword, newPassword);
setSuccess(true);
setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
} catch (err: unknown) {
const message =
err instanceof Error
? err.message
: typeof err === "object" && err && "message" in err
? String((err as { message: unknown }).message)
: "Error al cambiar la contraseña";
setError(message);
} finally {
setLoading(false);
}
}
return (
<div className="mx-auto max-w-md space-y-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Configuración</h1>
<p className="text-sm text-muted-foreground">
Cambiar contraseña
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">Email</label>
<Input
value={auth.session?.user.email ?? ""}
readOnly
className="bg-muted"
tabIndex={-1}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Contraseña actual</label>
<Input
type="password"
placeholder="••••••••"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Nueva contraseña</label>
<Input
type="password"
placeholder="••••••••"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Confirmar nueva contraseña</label>
<Input
type="password"
placeholder="••••••••"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
{error && (
<p className="text-sm text-destructive">{error}</p>
)}
{success && (
<p className="text-sm text-emerald-600">Contraseña actualizada correctamente</p>
)}
<Button type="submit" className="w-full" disabled={loading || !currentPassword || !newPassword || !confirmPassword}>
{loading ? "Guardando..." : "Cambiar contraseña"}
</Button>
</form>
</div>
);
}

View File

@@ -0,0 +1,95 @@
import { useState } from "react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { Lock } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useAuth } from "@/lib/auth-context";
export const Route = createFileRoute("/login")({
component: LoginPage,
});
function LoginPage() {
const auth = useAuth();
const navigate = useNavigate();
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
setLoading(true);
try {
await auth.signIn(password);
navigate({ to: "/" });
} catch (err: unknown) {
const message =
err instanceof Error
? err.message
: typeof err === "object" && err && "message" in err
? String((err as { message: unknown }).message)
: "Error al iniciar sesión";
setError(message);
} finally {
setLoading(false);
}
}
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-background via-background to-muted/50 p-4">
<div className="w-full max-w-sm">
<div className="rounded-xl border bg-card p-8 shadow-lg">
<div className="mb-8 flex flex-col items-center gap-3">
<div className="flex size-12 items-center justify-center rounded-full bg-primary/10">
<Lock className="size-6 text-primary" />
</div>
<div className="text-center">
<h1 className="text-xl font-semibold tracking-tight">
Personal Admin
</h1>
<p className="mt-1 text-sm text-muted-foreground">
Ingresá tu contraseña para continuar
</p>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">Email</label>
<Input
value="jselesan@gmail.com"
readOnly
className="bg-muted text-muted-foreground"
tabIndex={-1}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Contraseña</label>
<Input
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoFocus
className="text-base"
/>
</div>
{error && (
<p className="text-sm text-destructive">{error}</p>
)}
<Button
type="submit"
className="w-full"
disabled={loading || !password}
>
{loading ? "Ingresando..." : "Ingresar"}
</Button>
</form>
</div>
<p className="mt-4 text-center text-xs text-muted-foreground">
App privada acceso restringido
</p>
</div>
</div>
);
}