Next.js SaaS
Come implementare l'autenticazione e l'autorizzazione basata sul ruolo in Next.js
Realizza accesso con GitHub, sessioni nel database e ruoli negli spazi di lavoro con Better Auth e PostgreSQL. Proteggi pagine, Server Actions e route API.
Realizzeremo l'autenticazione per una dashboard di progetti: accesso con GitHub, sessione in PostgreSQL e ruoli owner e admin degli spazi di lavoro autorizzati a eliminare progetti, mentre i viewer possono solo leggerli. Gli stessi controlli di permessi proteggeranno pagina, Server Action e API HTTP.
Definisci esattamente cosa può fare un utente autenticato
Supponi che Maya sia owner dello spazio di lavoro Northstar e abbia accesso viewer ad Acme. L'accesso è lo stesso in entrambi, ma i permessi sono diversi. Un solo campo user.role non può rappresentarlo: concedere a Maya il ruolo owner globalmente le darebbe controllo anche su Acme.
Salva il ruolo nella relazione tra utente e spazio di lavoro. Una richiesta è consentita solo se la sessione è valida, la relazione è attiva, il ruolo permette l'operazione e il progetto richiesto appartiene a quello spazio di lavoro. Conoscere l'ID di un progetto non soddisfa nessuna di queste condizioni.
Questa implementazione usa l'App Router di Next.js 16, TypeScript, Better Auth 1.7 e PostgreSQL nell'ambiente di esecuzione Node.js. Parti da un'app TypeScript esistente con alias di importazione @/*, un database PostgreSQL e Node.js 24. L'esempio implementa lettura ed eliminazione dei progetti; inviti agli spazi di lavoro e modifica dei ruoli richiedono operazioni protette proprie.
| Ruolo | Leggi i progetti | Elimina progetti | Modifica le appartenenze |
|---|---|---|---|
| Responsabile | Sì | Sì | Sì |
| Amministratore | Sì | Sì | No |
| Lettore | Sì | No | No |
| Nessuna appartenenza attiva | No | No | No |
One protected request
Browser cookie
→ Better Auth verifies the session
→ application gets the verified user ID
→ PostgreSQL loads membership for the requested workspace
→ permission check + workspace-scoped project query
→ return only the fields the page needsConfigura un vero provider di autenticazione
Better Auth gestirà il callback GitHub, i cookie di sessione e le proprie tabelle di autenticazione. La nostra applicazione gestirà le tabelle degli spazi di lavoro e le decisioni di autorizzazione. Separare queste responsabilità permette di cambiare un ruolo nello spazio di lavoro senza modificare l'identità dell'utente.
Installa i pacchetti nell'app e salva nei commit il lockfile risultante. Genera BETTER_AUTH_SECRET con openssl rand -base64 32. Crea un'app OAuth GitHub con callback locale http://localhost:3000/api/auth/callback/github; usa un'app OAuth separata e un callback HTTPS per la produzione.
Install dependencies
npm install better-auth@1.7.6 pg zod server-only
npm install -D @types/pgImposta la configurazione del server
Aggiungi questi valori a un file .env escluso dal controllo di versione. Sostituisci i segnaposto con le credenziali del database e OAuth. Nessuna di queste variabili richiede il prefisso NEXT_PUBLIC_.
.env
DATABASE_URL=postgresql://USER:PASSWORD@localhost:5432/auth_demo
BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_SECRET=REPLACE_WITH_A_GENERATED_SECRET
GITHUB_CLIENT_ID=REPLACE_WITH_YOUR_CLIENT_ID
GITHUB_CLIENT_SECRET=REPLACE_WITH_YOUR_CLIENT_SECRETRiusa un unico pool di connessioni per processo Node
Creare un Pool dentro ogni richiesta genererebbe ogni volta un nuovo budget di connessioni. Questo modulo mantiene un unico pool, anche durante i ricaricamenti in sviluppo. Con quattro repliche applicative e max: 10, l'app può usare fino a 40 connessioni; anche migrazioni e altri servizi richiedono capacità.
lib/db.ts
import { Pool } from "pg";
const globalDb = globalThis as unknown as { authDemoPool?: Pool };
export const db = globalDb.authDemoPool ?? new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
connectionTimeoutMillis: 5_000,
idleTimeoutMillis: 30_000,
});
if (process.env.NODE_ENV !== "production") globalDb.authDemoPool = db;Usa sessioni nel database con scadenza fissa
In questo esempio scegliamo una durata assoluta di sette giorni e disabilitiamo il rinnovo della sessione. La cache dei cookie rimane disabilitata, quindi la verifica della sessione consulta il database. Sono politiche consapevoli: un cookie rubato può essere revocato centralmente e un browser attivo deve effettuare di nuovo l'accesso dopo sette giorni.
Un token firmato autosufficiente può ridurre le letture del database, ma un ruolo o una sessione copiati al suo interno rimangono validi fino alla scadenza, salvo l'aggiunta di un controllo della revoca. Le sessioni nel database sono adatte a questa dashboard perché i cambi di ruolo e la disconnessione di un dispositivo smarrito devono avere effetto sui controlli successivi.
lib/auth.ts
import { betterAuth } from "better-auth";
import { db } from "@/lib/db";
export const auth = betterAuth({
database: db,
baseURL: process.env.BETTER_AUTH_URL!,
secret: process.env.BETTER_AUTH_SECRET!,
trustedOrigins: [process.env.BETTER_AUTH_URL!],
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
session: {
expiresIn: 60 * 60 * 24 * 7,
disableSessionRefresh: true,
cookieCache: { enabled: false },
},
});Monta gli endpoint di autenticazione
La route catch-all gestisce richieste di accesso, callback, sessione e uscita. Genera lo schema della libreria dopo aver creato la configurazione, esamina il SQL e applicalo al database di sviluppo. Il comando CLI è npx auth@latest generate; conserva la migrazione generata nella cronologia delle migrazioni dell'app. Usa una versione della CLI compatibile con la versione fissata di Better Auth.
app/api/auth/[...all]/route.ts
import { toNextJsHandler } from "better-auth/next-js";
import { auth } from "@/lib/auth";
export const runtime = "nodejs";
export const { GET, POST } = toNextJsHandler(auth);Segui il login dal browser alla sessione
Il browser avvia l'accesso tramite la nostra route di autenticazione e viene reindirizzato a GitHub. Il callback restituisce un codice di autorizzazione. Better Auth valida il flusso OAuth, scambia il codice sul server, associa l'identità del provider a un utente locale e crea una sessione nel database. Il browser riceve un cookie di sessione e torna al sito.
Il token di accesso GitHub e la sessione dell'app hanno funzioni diverse. Il token del provider serve a comunicare con GitHub. La dashboard usa la sessione dell'app per identificare chi chiama. Né un indirizzo email fornito dal client né un nome utente GitHub dimostrano l'appartenenza a uno spazio di lavoro.
Usa il client sotto da app/sign-in/page.tsx. Il callback torna alla home page esistente; dopo aver creato lo spazio di lavoro di esempio nella sezione successiva, apri l'URL del suo progetto.
lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient();Accedi e gestisci una richiesta fallita
L'accesso con GitHub crea l'utente locale al primo utilizzo. Un nuovo utente non ha accesso agli spazi di lavoro finché l'onboarding non ne crea uno o un invito autorizzato non gli concede l'appartenenza.
app/sign-in/page.tsx
"use client";
import { useState } from "react";
import { authClient } from "@/lib/auth-client";
export default function SignInPage() {
const [pending, setPending] = useState(false);
const [error, setError] = useState("");
async function signIn() {
setPending(true);
setError("");
try {
const result = await authClient.signIn.social({
provider: "github",
callbackURL: "/",
});
if (result.error) {
setError("Sign-in failed. Please try again.");
setPending(false);
}
} catch {
setError("Could not reach the sign-in service.");
setPending(false);
}
}
return (
<main>
<button onClick={signIn} disabled={pending}>
{pending ? "Redirecting…" : "Continue with GitHub"}
</button>
{error && <p role="alert">{error}</p>}
</main>
);
}Salva i ruoli nelle appartenenze agli spazi di lavoro
Applica questa migrazione applicativa dopo quella generata da Better Auth. La libreria di autenticazione gestisce le tabelle user, account, session e verification. Le tabelle sotto fanno riferimento alla sua tabella user PostgreSQL predefinita, senza aggiungerle un ruolo globale.
La chiave composta dell'appartenenza permette un ruolo per utente in ogni spazio di lavoro. workspace_id del progetto è obbligatorio. Questi vincoli impediscono appartenenze duplicate e proprietà orfane; non effettuano automaticamente l'autorizzazione. Ogni query sui progetti richiede comunque il confine dello spazio di lavoro.
migrations/002_workspaces.sql
CREATE TABLE app_workspace (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL
);
CREATE TABLE app_membership (
workspace_id uuid NOT NULL REFERENCES app_workspace(id) ON DELETE CASCADE,
user_id text NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
role text NOT NULL CHECK (role IN ('owner', 'admin', 'viewer')),
status text NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'suspended')),
PRIMARY KEY (workspace_id, user_id)
);
CREATE TABLE app_project (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id uuid NOT NULL REFERENCES app_workspace(id) ON DELETE CASCADE,
name text NOT NULL
);
CREATE INDEX app_project_workspace_idx ON app_project(workspace_id);
CREATE TABLE app_audit_event (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
workspace_id uuid NOT NULL REFERENCES app_workspace(id),
actor_id text NOT NULL,
operation text NOT NULL,
resource_id uuid NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);Crea uno spazio di lavoro per l'esempio guidato locale
Accedi una volta e trova il tuo ID utente locale nel database di autenticazione. In psql, imposta demo_user_id su quell'ID ed esegui le istruzioni sotto. Sono dati iniziali di sviluppo inseriti dall'operatore, non un endpoint che accetta ID utente o ruolo dal browser.
L'onboarding di produzione deve creare lo spazio di lavoro e il primo owner in un'unica transazione, ricavando l'ID dell'owner dalla sessione verificata. Accettare un invito deve collegarlo all'identità destinataria e consumare un token monouso con scadenza. Un utente autenticato non deve mai poter scegliere uno spazio di lavoro esistente e concedersi accesso owner.
Development seed — run in psql after setting demo_user_id
-- First: SELECT id, email FROM "user";
-- Then: \set demo_user_id 'PASTE_YOUR_LOCAL_USER_ID'
BEGIN;
INSERT INTO app_workspace (id, name)
VALUES ('11111111-1111-4111-8111-111111111111', 'Northstar');
INSERT INTO app_membership (workspace_id, user_id, role)
VALUES ('11111111-1111-4111-8111-111111111111', :'demo_user_id', 'owner');
INSERT INTO app_project (id, workspace_id, name)
VALUES (
'22222222-2222-4222-8222-222222222222',
'11111111-1111-4111-8111-111111111111',
'Website'
);
COMMIT;
-- Open /workspaces/11111111-1111-4111-8111-111111111111/projects/22222222-2222-4222-8222-222222222222Definisci i permessi una sola volta e nega l'accesso ai ruoli sconosciuti
Nomina l'operazione nel codice applicativo: project:read o project:delete. Traduci i ruoli in questi permessi in un unico modulo. Pagina e server possono usare la stessa mappatura, ma solo la verifica corrente dell'appartenenza sul server può autorizzare una richiesta.
Questa politica restituisce false per un ruolo non riconosciuto. È importante durante le migrazioni e quando un valore inatteso raggiunge l'applicazione. Non trattare un ruolo mancante o sconosciuto come l'impostazione predefinita più permissiva.
lib/permissions.ts
export type Permission =
| "project:read"
| "project:delete"
| "membership:manage";
const permissions: Record<string, readonly Permission[]> = {
owner: ["project:read", "project:delete", "membership:manage"],
admin: ["project:read", "project:delete"],
viewer: ["project:read"],
};
export function can(role: string, permission: Permission): boolean {
return Object.hasOwn(permissions, role)
&& permissions[role].includes(permission);
}Dove si ferma il controllo RBAC
Il ruolo determina se un membro può eliminare progetti nello spazio di lavoro. workspace_id del progetto determina a quali progetti si applica la regola. Sono condizioni distinte e devono essere entrambe soddisfatte.
Se in futuro il prodotto permette ai membri di eliminare solo i progetti creati da loro, aggiungi un controllo created_by alla query protetta. La sola verifica del ruolo non esprime questa regola di appartenenza. Allo stesso modo, il ruolo admin di uno spazio di lavoro non deve diventare implicitamente un permesso di usare una console interna di assistenza su tutti i clienti.
Inserisci la verifica della sessione nell'accesso ai dati protetti
Un reindirizzamento nel layout può migliorare la navigazione, ma un Route Handler o una Server Action possono essere chiamati direttamente. Inserisci la verifica dell'identità nella funzione che legge i dati protetti. Ogni chiamante riceve così gli stessi controlli.
requireUserId risolve il cookie ricevuto tramite Better Auth. Restituisce solo l'ID utente e genera un errore 401 se non esiste una sessione valida. Gli errori del database o del provider devono poter far fallire la richiesta; non devono ripiegare su un utente anonimo o precedentemente privilegiato.
La query del progetto unisce l'appartenenza del chiamante allo spazio di lavoro richiesto. Un progetto Acme non può essere recuperato inserendone l'ID in un URL Northstar. Qui appartenenza mancante e progetto mancante restituiscono entrambi 404, così l'endpoint non rivela l'esistenza del progetto di un altro spazio di lavoro.
lib/access.ts
import "server-only";
import { headers } from "next/headers";
import { auth } from "@/lib/auth";
export class AccessError extends Error {
readonly status: 401 | 403 | 404;
constructor(status: 401 | 403 | 404) {
super(status === 401 ? "Sign in required"
: status === 403 ? "Forbidden" : "Not found");
this.status = status;
}
}
export async function requireUserId() {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) throw new AccessError(401);
return session.user.id;
}Limita la lettura e restituisci un risultato ridotto
Zod valida la struttura degli identificatori; il join sull'appartenenza stabilisce l'accesso. Un UUID sintatticamente valido resta un input non attendibile. I segnaposto SQL mantengono questi valori separati dal testo della query.
Restituisci i campi del progetto da visualizzare e un flag canDelete. La pagina non richiede token di sessione, token del provider, riga dell'utente o record completo dell'appartenenza. Questo riduce anche il danno di una serializzazione accidentale in un Client Component.
lib/projects.ts — read operation
import "server-only";
import { z } from "zod";
import { db } from "@/lib/db";
import { AccessError, requireUserId } from "@/lib/access";
import { can } from "@/lib/permissions";
export const projectInput = z.object({
workspaceId: z.string().uuid(),
projectId: z.string().uuid(),
});
export async function getProject(input: unknown) {
const userId = await requireUserId();
const { workspaceId, projectId } = projectInput.parse(input);
const result = await db.query<{
id: string; name: string; role: string;
}>(
`SELECT p.id, p.name, m.role
FROM app_project p
JOIN app_membership m ON m.workspace_id = p.workspace_id
WHERE p.id = $1 AND p.workspace_id = $2
AND m.user_id = $3 AND m.status = 'active'`,
[projectId, workspaceId, userId],
);
const row = result.rows[0];
if (!row || !can(row.role, "project:read")) throw new AccessError(404);
return {
id: row.id,
name: row.name,
canDelete: can(row.role, "project:delete"),
};
}Proteggi la pagina e fai riflettere i permessi all'interfaccia
Chiama getProject dal Server Component. Un visitatore anonimo viene indirizzato all'accesso; un progetto non accessibile mostra la pagina di contenuto non trovato. Il modulo di eliminazione appare solo se il ruolo corrente permette di eliminare.
I campi nascosti trasportano gli ID destinatari e rendono facile collegare il modulo, ma non conferiscono autorità. Un viewer può aggiungere il modulo negli strumenti del browser o inviare da sé la richiesta; l'operazione di modifica nella sezione successiva deve rifiutarla.
canDelete descrive l'accesso al momento del rendering della pagina. Se un owner riduce il ruolo dell'utente in un'altra scheda, la pagina precedente può ancora mostrare Elimina. La correttezza dipende da una nuova verifica dell'appartenenza al momento della scrittura.
app/workspaces/[workspaceId]/projects/[projectId]/page.tsx
import { notFound, redirect } from "next/navigation";
import { ZodError } from "zod";
import { AccessError } from "@/lib/access";
import { getProject } from "@/lib/projects";
import { deleteProjectAction } from "./actions";
export const runtime = "nodejs";
export default async function ProjectPage({
params,
}: {
params: Promise<{ workspaceId: string; projectId: string }>;
}) {
const ids = await params;
const project = await getProject(ids).catch((error: unknown) => {
if (error instanceof AccessError && error.status === 401) {
redirect("/sign-in");
}
if (error instanceof AccessError || error instanceof ZodError) notFound();
throw error;
});
return (
<main>
<h1>{project.name}</h1>
{project.canDelete && (
<form action={deleteProjectAction}>
<input type="hidden" name="workspaceId" value={ids.workspaceId} />
<input type="hidden" name="projectId" value={ids.projectId} />
<button type="submit">Delete project</button>
</form>
)}
</main>
);
}Riusa i controlli in un'API HTTP
L'API importa le stesse funzioni getProject e deleteProject. Un secondo punto d'ingresso non può quindi omettere accidentalmente i controlli di permessi o spazio di lavoro. Usa 401 per una sessione non valida, 403 per un membro attivo senza il permesso dell'operazione e 404 per una risorsa non accessibile.
L'autenticazione tramite cookie richiede anche protezione CSRF. Better Auth protegge i propri endpoint, ma non questo gestore DELETE personalizzato. Questa API del browser, usata dalla stessa origine, richiede un'intestazione Origin che corrisponda esattamente all'origine configurata dell'applicazione. Origini mancanti, null e diverse vengono rifiutate prima della modifica. I client macchina richiedono un percorso di autenticazione progettato separatamente, anziché un'eccezione che indebolisca questo endpoint basato sui cookie.
Next.js aggiunge controlli Origin/Host alle Server Actions. Mantieni attive queste protezioni e limita strettamente le origini dei proxy attendibili. CORS controlla quali script del browser possono leggere una risposta; non verifica il permesso di scrittura di una richiesta autenticata tramite cookie.
app/api/workspaces/[workspaceId]/projects/[projectId]/route.ts
import { ZodError } from "zod";
import { AccessError } from "@/lib/access";
import { getProject, deleteProject } from "@/lib/projects";
export const runtime = "nodejs";
const privateHeaders = { "Cache-Control": "private, no-store" };
type Context = {
params: Promise<{ workspaceId: string; projectId: string }>;
};
function errorResponse(error: unknown) {
if (error instanceof AccessError) {
return Response.json({ error: error.message }, {
status: error.status, headers: privateHeaders,
});
}
if (error instanceof ZodError) {
return Response.json({ error: "Invalid project identifiers" }, {
status: 400, headers: privateHeaders,
});
}
// Send unexpected failures to server monitoring; do not expose SQL/errors.
return Response.json({ error: "Request failed" }, {
status: 500, headers: privateHeaders,
});
}
export async function GET(_request: Request, { params }: Context) {
try {
return Response.json(await getProject(await params), {
headers: privateHeaders,
});
} catch (error) {
return errorResponse(error);
}
}
export async function DELETE(request: Request, { params }: Context) {
const expectedOrigin = new URL(process.env.BETTER_AUTH_URL!).origin;
if (request.headers.get("origin") !== expectedOrigin) {
return Response.json({ error: "Forbidden origin" }, {
status: 403, headers: privateHeaders,
});
}
try {
await deleteProject(await params);
return new Response(null, { status: 204, headers: privateHeaders });
} catch (error) {
return errorResponse(error);
}
}Rendi prevedibili revoca e richieste simultanee
La sessione identifica chi chiama. L'appartenenza determina cosa può fare in quel momento. Poiché ogni operazione protetta legge l'appartenenza, passare un admin a viewer influisce sulla verifica dei permessi successiva anche se la sua sessione di sette giorni rimane valida. Rimuovere l'appartenenza blocca l'accesso a quello spazio di lavoro, conservando l'accesso agli altri.
Una richiesta già autorizzata è diversa. Se l'eliminazione acquisisce per prima il lock sull'appartenenza, può eseguire il commit prima che una riduzione concorrente del ruolo termini. Se il cambio di ruolo acquisisce per primo il lock, l'eliminazione attende e verifica poi il ruolo aggiornato. La garanzia è una decisione ordinata nel database, non la cancellazione di lavoro già autorizzato.
La revoca della sessione ha un confine simile: invalidare una sessione impedisce il successo delle verifiche successive. Non richiama una risposta HTTP già inviata, non cancella dati nel browser e non annulla automaticamente un'operazione che ha già superato requireUserId. Azioni finanziarie o distruttive sull'intero account possono richiedere una nuova verifica di autenticazione e un confine transazionale più rigoroso.
Proteggi anche l'operazione di cambio ruolo
Un futuro endpoint di gestione dei ruoli deve caricare l'appartenenza dell'utente che agisce, richiedere membership:manage e limitare l'appartenenza destinataria allo stesso spazio di lavoro. Non deve mai accettare un ruolo dell'attore dichiarato nel JSON. Applica la stessa regola alla creazione degli inviti, alla rimozione degli utenti e alla sospensione.
Impedisci la rimozione dell'ultimo owner. Due richieste simultanee possono ciascuna vedere un altro owner e rimuoverli entrambi se la verifica non è serializzata. Un approccio pratico è bloccare la riga dello spazio di lavoro con FOR UPDATE, verificare l'attore e il numero di owner, poi modificare le appartenenze in un'unica transazione. Ogni percorso che cambia la proprietà deve acquisire quel lock nello stesso ordine.
Verifica le operazioni negate con due spazi di lavoro
Un accesso riuscito come owner dimostra ben poco sull'autorizzazione. Crea Northstar e Acme in un database temporaneo. Assegna allo stesso utente accesso admin a Northstar e viewer ad Acme, poi crea un progetto in ciascuno. Crea anche un secondo utente senza alcuna appartenenza.
Inizia con un test eseguibile della politica, poi verifica i percorsi del database e HTTP. Per un rifiuto, la verifica essenziale riguarda sia la risposta sia lo stato invariato: il progetto deve ancora esistere e non deve essere stato aggiunto alcun evento di audit di eliminazione riuscita.
| Richiesta o modifica | Risultato previsto | Che cosa prova |
|---|---|---|
| Nessuna sessione o sessione revocata | 401 dall'API; la pagina reindirizza all'accesso | Ogni punto d'ingresso verifica l'identità. |
| Un admin di Northstar elimina un progetto Northstar | 204; un progetto rimosso; un evento di audit | Il percorso consentito esegue il commit come un'unica unità. |
| Lo stesso utente elimina in Acme come viewer | 403; resta il progetto | I ruoli sono limitati alle appartenenze. |
| URL Northstar con l'ID del progetto Acme | 404; il progetto Acme rimane | La scrittura usa sia l'ID dello spazio di lavoro sia quello del progetto. |
| Un non membro richiede un ID di progetto noto | 404 senza campi del progetto | Conoscere un ID non concede accesso. |
| Riduci il ruolo admin dopo il rendering del modulo di eliminazione | Invio successivo rifiutato; il progetto rimane | Il server ignora i permessi obsoleti dell'interfaccia. |
| DELETE con Origin mancante o diverso | 403; resta il progetto | L'endpoint personalizzato basato sui cookie verifica l'origine per la protezione CSRF. |
| L'inserimento nell'audit fallisce dopo l'istruzione DELETE | Rollback della transazione; il progetto rimane | Un errore non può produrre un commit parziale dell'operazione. |
tests/permissions.test.mjs — run with node --test tests/permissions.test.mjs on Node.js 24
import test from "node:test";
import assert from "node:assert/strict";
import { can } from "../lib/permissions.ts";
test("workspace roles grant only their declared permissions", () => {
const cases = [
["owner", "project:delete", true],
["owner", "membership:manage", true],
["admin", "project:delete", true],
["admin", "membership:manage", false],
["viewer", "project:read", true],
["viewer", "project:delete", false],
["unknown", "project:read", false],
["constructor", "project:read", false],
];
for (const [role, permission, expected] of cases) {
assert.equal(can(role, permission), expected, role + ": " + permission);
}
});Simula la concorrenza anziché presumere che il lock funzioni
Usa due connessioni al database. Sulla prima, avvia una transazione e aggiorna il ruolo del membro a viewer senza eseguire il commit. Avvia l'eliminazione sulla seconda connessione. Deve attendere su SELECT … FOR SHARE. Esegui il commit del cambio di ruolo; l'eliminazione deve riprendere, rilevare viewer e restituire un rifiuto.
Ripeti facendo acquisire per prima all'eliminazione il lock sull'appartenenza. Il cambio a un ruolo inferiore deve attendere il commit dell'eliminazione. Mantieni il ritardo sotto il timeout del lock di tre secondi dell'esempio. Questo test mostra esattamente quale operazione prevale e verifica il comportamento concorrente che un test unitario sui permessi non può coprire.
Chiama anche direttamente la Server Action usando la richiesta acquisita dal browser dopo il cambio di ruolo. Verificare solo i pulsanti visibili farebbe perdere il punto d'ingresso che un attaccante può ancora invocare.
Gestisci il sistema di autenticazione dopo il lancio
Tutte le repliche richiedono lo stesso segreto di autenticazione, URL dell'applicazione, database e configurazione OAuth. Conserva questi valori nei segreti dell'ambiente di esecuzione, usa HTTPS e registra il callback di produzione esatto. Una sessione creata sulla replica A deve essere verificabile sulla replica B. Distribuisci le modifiche allo schema tramite migrazioni esaminate, così vecchia e nuova versione dell'applicazione possono coesistere durante la distribuzione.
Quando esegui più repliche, limita il traffico di autenticazione in un punto condiviso; contatori indipendenti in memoria moltiplicano il limite effettivo. Configura il nodo edge per sovrascrivere le intestazioni attendibili dell'IP client e impedisci accessi diretti che possano falsificarle. Monitora errori di callback, verifiche di sessione non riuscite, operazioni negate ed esaurimento del pool senza registrare cookie o token dei provider.
Questo esempio guidato usa l'accesso GitHub, quindi verifica della password e recupero dell'account partono da quel provider. Se in seguito abiliti email e password, implementa anche consegna verificata delle email, token di ripristino monouso con scadenza, limiti ai tentativi e revoca delle sessioni dopo modifiche sensibili alle credenziali. Aggiungere il solo campo password non crea un sistema di recupero.
Per esportazioni lunghe e job in background, salva utente iniziale e spazio di lavoro, poi definisci se i permessi vengono verificati di nuovo all'esecuzione del job e al download del risultato. Una richiesta autorizzata oggi non deve creare un link di download senza restrizioni che funzioni ancora dopo che l'utente ha lasciato lo spazio di lavoro.
Il risultato è un percorso di autorizzazione piccolo ed esplicito: sessione verificata, appartenenza corrente, operazione consentita e query limitata al suo ambito. Ogni endpoint aggiuntivo deve chiamare quel percorso e ogni ulteriore permesso deve avere un test di rifiuto.