feat(login): pagina pubblica /login con next sicuro

This commit is contained in:
2026-07-05 21:59:50 +02:00
parent 5ce10da9bd
commit 6b85872515
3 changed files with 78 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
// Path interno sicuro per il redirect post-login: deve iniziare con "/", non essere
// protocol-relative ("//"), non puntare alle API. Tutto il resto → home.
export function safeNext(next: string | null): string {
if (!next || !next.startsWith('/') || next.startsWith('//')) return '/';
if (next.startsWith('/api/')) return '/';
return next;
}
+53
View File
@@ -0,0 +1,53 @@
---
import Base from '../layouts/Base.astro';
import { getDb } from '../lib/db';
import { login, getSessionUser, SESSION_COOKIE } from '../lib/auth';
import { rateLimit } from '../lib/rate-limit';
import { safeNext } from '../lib/safe-next';
export const prerender = false;
const dest = safeNext(Astro.url.searchParams.get('next'));
// Già loggato → via dal login.
const existing = Astro.cookies.get(SESSION_COOKIE)?.value;
if (existing && getSessionUser(getDb(), existing)) return Astro.redirect(dest);
let error = '';
if (Astro.request.method === 'POST') {
if (!rateLimit(`login:${Astro.clientAddress}`, 5, 15 * 60 * 1000)) {
error = 'Troppi tentativi: riprova tra 15 minuti.';
} else {
const form = await Astro.request.formData();
const token = login(getDb(), String(form.get('username') ?? ''), String(form.get('password') ?? ''));
if (token) {
Astro.cookies.set(SESSION_COOKIE, token, {
httpOnly: true, sameSite: 'lax', path: '/',
secure: import.meta.env.PROD, maxAge: 7 * 24 * 3600,
});
return Astro.redirect(safeNext(String(form.get('next') ?? '') || dest));
}
error = 'Credenziali non valide.';
}
}
---
<Base title="Accedi" description="Area riservata InsanityLab">
<section class="section">
<div class="container login-wrap">
<h1 class="section-heading">Accedi</h1>
{error && <p class="login-err">{error}</p>}
<form method="post" class="login-form">
<input type="hidden" name="next" value={dest} />
<input class="login-field" name="username" placeholder="Utente" required autocomplete="username" />
<input class="login-field" type="password" name="password" placeholder="Password" required autocomplete="current-password" />
<button class="btn btn--dark" type="submit">Entra</button>
</form>
</div>
</section>
</Base>
<style>
.login-wrap { max-width: 380px; margin: 0 auto; }
.login-form { display: grid; gap: 14px; margin-top: 20px; }
.login-field { padding: 12px; border: 1px solid #ccc; font: inherit; }
.login-err { color: #a33; }
</style>