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:
@@ -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,
|
||||
});
|
||||
|
||||
6
apps/frontend/src/routes/_authenticated.tsx
Normal file
6
apps/frontend/src/routes/_authenticated.tsx
Normal 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,
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/quotes")({
|
||||
export const Route = createFileRoute("/_authenticated/quotes")({
|
||||
component: QuotesLayout,
|
||||
});
|
||||
|
||||
107
apps/frontend/src/routes/_authenticated/settings.tsx
Normal file
107
apps/frontend/src/routes/_authenticated/settings.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
95
apps/frontend/src/routes/login.tsx
Normal file
95
apps/frontend/src/routes/login.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user