Files
Adriano b913a2bf90 registri dei piani in git (AI-OS#309)
Refs gitea:Adriano/AI-OS#310

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 10:36:26 +02:00

4.2 KiB

Task 7: Pagina di login pubblica /login

Files:

  • Create: src/pages/login.astro
  • Create: src/lib/safe-next.ts
  • Test: tests/safe-next.test.ts

Interfaces:

  • Consumes: login, SESSION_COOKIE (auth.ts), rateLimit (rate-limit.ts).

  • Produces: safeNext(next: string | null): string — ritorna un path interno sicuro o '/'.

  • Step 1: Test di safeNext

Crea tests/safe-next.test.ts:

import { describe, it, expect } from 'vitest';
import { safeNext } from '../src/lib/safe-next';

describe('safeNext', () => {
  it('accetta path interni', () => {
    expect(safeNext('/about')).toBe('/about');
    expect(safeNext('/blog/x?y=1')).toBe('/blog/x?y=1');
  });
  it('rifiuta esterni, protocol-relative, api e vuoti', () => {
    expect(safeNext(null)).toBe('/');
    expect(safeNext('')).toBe('/');
    expect(safeNext('//evil.com')).toBe('/');
    expect(safeNext('https://evil.com')).toBe('/');
    expect(safeNext('/api/admin/posts')).toBe('/');
    expect(safeNext('javascript:alert(1)')).toBe('/');
    expect(safeNext('not-a-path')).toBe('/');
  });
});
  • Step 2: Esegui e verifica il fallimento

Run: npm test -- tests/safe-next.test.ts Expected: FAIL — modulo inesistente.

  • Step 3: Implementa safeNext

Crea src/lib/safe-next.ts:

// 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;
}
  • Step 4: Esegui e verifica il successo

Run: npm test -- tests/safe-next.test.ts Expected: PASS.

  • Step 5: Crea la pagina /login

Crea src/pages/login.astro:

---
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>
  • Step 6: Verifica build

Run: npm run build Expected: build OK.

  • Step 7: Commit
git add src/pages/login.astro src/lib/safe-next.ts tests/safe-next.test.ts
git commit -m "feat(login): pagina pubblica /login con next sicuro"