@col/auth-pages-starter
Versie: 1.1.0 Toegevoegd: 2026-08-03
Herkomst
Geëxtraheerd uit supplier-onboarding-vercel: de vier auth-pagina's
(app/login, app/activate/[token], app/forgot-password,
app/reset-password/[token]) en de drie bijbehorende API-routes
(app/api/auth/{activate,forgot-password,reset-password}). Elke nieuwe
Coloriginz-app met eigen gebruikersaccounts (dus niet uitsluitend SSO) heeft
dezelfde vier schermen en dezelfde drie routes nodig — tot nu toe alleen als
compositie binnen de onboarding-portal aanwezig.
De onboarding-portal is meteen ook consument #1: stap 1 van de extractie
verving de losse <img>-logo's op drie van de vier pagina's door
@col/brand-logo (de loginpagina deed dat al), zodat de bron die naar de
registry gekopieerd is identiek is aan wat live staat.
Waarvoor
Vier complete pagina's en drie API-routes voor wachtwoord-gebaseerde
authenticatie: inloggen (met optionele Microsoft SSO-knop erboven),
accountactivatie via een tokenlink, "wachtwoord vergeten" en "wachtwoord
resetten". Gebruik dit als startpunt voor elke app die eigen
gebruikersaccounts met wachtwoord nodig heeft. Gebruik het niet als de
app uitsluitend SSO gebruikt — dan is alleen @col/sso-button nodig, niet
de wachtwoordvelden en de drie routes eromheen.
Dit is nadrukkelijk glue, geen component: alle zeven bestanden zijn van
de consumer zodra ze geïnstalleerd zijn (registry:page / registry:file,
niet registry:component). Een update van dit item overschrijft ze nooit —
npx shadcn add @col/auth-pages-starter --overwrite is alleen voor de
eerste installatie.
Je hoeft dit niet over te nemen
Wat hier de standaard is, is hoe het eruitziet: de opbouw van de kaart, de volgorde van logo, titel, SSO-knop en velden, de tussenruimtes. Dat hoort tussen apps hetzelfde te zijn.
De code is een startpunt, geen voorschrift. Past hij niet bij je stack, bouw
hem dan na op basis van de preview op /auth-pages-starter — dat is precies
waarvoor die preview bestaat. Ook een gedeeltelijke overname is prima: alleen de
loginpagina, of alleen de vier schermen zonder de API-routes.
Dat is geen theorie. floriday-app draait op base-ui in plaats van Radix en
kon de componenten van @col/demo-mode daarom niet letterlijk gebruiken; die app
heeft ze nagebouwd met hetzelfde uiterlijk en gedrag. Precies de bedoeling.
Twee dingen zijn wél bindend, omdat ze anders per app uiteenlopen:
- het uiterlijk van de schermen
- het contract onder "Wat je app moet leveren" als je de meegeleverde API-routes gebruikt
Wat je app moet leveren
Elk van de zeven bestanden gaat uit van modules die in col-design-system
niet bestaan. Zonder onderstaande bouwt de app niet.
UI / i18n
LanguageSelectorop@/components/ui/language-selector— de loginpagina en de "success"-stand van forgot-password gebruiken hem in de kaartheader. Je hoeft die niet zelf te bouwen:@col/language-selectorlevert hem, maar opcomponents/nav/language-selector. Pas dan het importpad in de pagina's aan, of installeer het item en verplaats het bestand.useLanguage()op@/lib/i18n-context, met eent(key)die dot-notatie paden opzoekt. Alle vier pagina's gebruiken uitsluitend sleutels onderauth.*, plus incidenteelcommon.or,common.loading,common.erroren (alleen in demo-mode, zie hieronder)demo.*. Sleutels die de teksten nodig hebben, per pagina:auth.login.*:title,subtitle,email,emailPlaceholder,password,submit,submitting,errorGeneric,forgotPassword,emailLogin,ssoButton,ssoLoading,ssoErrors.{AccountNotFound,NoEmail,OAuthCallback,OAuthSignin}auth.activate.*enauth.resetPassword.*(identieke sleutelset op beide):title,subtitle,password,passwordPlaceholder,confirmPassword,confirmPlaceholder,submit,submitting,success,successMessage,errorMinLength,errorUppercase,errorSpecialChar,errorMismatch,errorGenericauth.forgotPassword.*:title,subtitle,email,emailPlaceholder,submit,submitting,success,successMessage,backToLogin- Bewuste bron-eigenaardigheid, meegekopieerd: de terugknop op
reset-password gebruikt
t('auth.forgotPassword.backToLogin'), niet een eigenauth.resetPassword.backToLogin. Dat is geen bug in de extractie — het stond zo in de bron — maar wel iets om recht te zetten als de app een eigen resetPassword-vertaalgroep opzet.
getLabelFromHostname(hostname)engetLabelConfig(label)op@/lib/label-config. Alle vier pagina's detecteren het merk viawindow.location.hostnamein eenuseEffecten lezenlogoPath,roundLogoenloginBguit de config. Zonder multi-merk-behoefte volstaat een config met één label dat altijd teruggegeven wordt.LOGO_BASE64op@/lib/logo-base64— het default-logo als base64-string, getoond zolang het hostname-effect nog niet is gedraaid en als fallback voor het standaardmerk.
Componenten uit dit design system
@col/auth-shell (schermlayout), @col/brand-logo (logo-weergave),
@col/sso-button (Microsoft-knop) en, sinds 1.1.0, @col/rate-limit
(de limiter achter de drie routes hieronder) zijn registryDependencies en
worden automatisch meegeïnstalleerd — landt op lib/rate-limit.ts, zie
docs/items/rate-limit.md.
Daarnaast staan de shadcn-primitives alert, button, card, input, label
en separator in de dependencies. Let op bij een bestaande app: die worden
opgehaald in hun huidige upstream-versie en overschrijven de kopie die de app al
heeft. Bij de onboarding-portal zou dat AlertTitle van een <h5> naar een
<div> brengen, met een andere layout — die kopie is ouder dan stock shadcn.
Controleer na installatie dus ook git diff src/components/ui/.
Auth
- NextAuth met een
credentials-provider (signIn('credentials', {email, password, redirect:false})op de loginpagina) en, optioneel, eenazure-ad-provider (signIn('azure-ad', {callbackUrl}), alleen zichtbaar alsprocess.env.NEXT_PUBLIC_AZURE_AD_ENABLED === 'true'). Zonder SSO toont de pagina gewoon nooit de knop — er is geen aparte code path nodig.
Backend-contract van de drie routes
Alle drie routes zijn POST, ratelimiten via rateLimit(key, max, windowMs) uit @col/rate-limit (@/lib/rate-limit, sinds 1.1.0 een
registryDependency van dit item — je hoeft er niets voor te doen), en
valideren de body met een Zod-schema uit @/lib/validations. De sleutel is
${ip}:<route> — het IP komt uit x-forwarded-for / x-real-ip, de
route-naam is de vaste string uit de tabel hieronder.
Vóór 1.1.0 riepen deze routes een rateLimit(ip, key, max, windowMs) aan
uit een @/lib/rate-limit dat niet in dit item zat. Dat compileerde in de
bronapp (die had zelf zo'n bestand geschreven) maar niet in een nieuwe app
die alleen @col/auth-pages-starter installeerde — precies het gat dat
@col/rate-limit als registryDependency dicht. Zie
docs/items/rate-limit.md voor de beperkingen van de
limiter zelf (in-memory, per serverless-instantie op Vercel).
| Route | Zod-schema | Body → | Succes | Faalstatussen |
|---|---|---|---|---|
POST /api/auth/activate |
activateSchema |
{ token, password } |
{ success: true } |
400 ongeldige input / onbekend token, 410 verlopen token, 429 rate limit |
POST /api/auth/forgot-password |
forgotPasswordSchema |
{ email } |
altijd { success: true, message } (voorkomt account-enumeratie) |
429 rate limit |
POST /api/auth/reset-password |
resetPasswordSchema |
{ token, password } |
{ success: true } |
400 ongeldige input / onbekend token, 410 verlopen token, 429 rate limit |
Prisma User
De routes gaan uit van minimaal:
| Veld | Type | Gebruikt door |
|---|---|---|
email |
String |
forgot-password (opzoeken, case-insensitive) |
passwordHash |
String? |
forgot-password (alleen versturen als gezet) |
activationToken |
String? @unique |
activate + reset-password (hergebruikt veld — reset is een tweede toepassing van dezelfde tokenflow) |
activationExpiresAt |
DateTime? |
activate + forgot-password + reset-password |
isActive |
Boolean |
activate (zet op true) |
firstName |
String |
forgot-password (in de e-mail) |
preferredLanguage |
String? |
forgot-password (taal van de e-mail, valt terug op 'nl') |
labels |
String[]? |
forgot-password (user.labels?.[0], merk voor de e-mail-branding) |
Dit is de kale set die de gekopieerde routes nodig hebben. Een app zonder
merken of i18n kan labels/preferredLanguage negeren, maar moet dan de
route-code aanpassen — de schema's noch de route zelf maken die velden
optioneel in gedrag, alleen in het Prisma-type.
Overig
bcryptjs—bcrypt.hash(password, 10)in activate en reset-password.uuid(v4 as uuidv4) — nieuw reset-token in forgot-password.@/lib/db— Prisma client singleton, geëxporteerd alsprisma.@/lib/email—sendPasswordResetEmail({ to, firstName, resetToken, expiresAt, language, label }).@/lib/i18n— typeLanguage.- Zod-schema's
activateSchema,forgotPasswordSchema,resetPasswordSchemaop@/lib/validations, elk met minimaal het wachtwoordbeleid dat de pagina's client-side ook afdwingen: 14+ tekens, minstens één hoofdletter, minstens één niet-alfanumeriek teken.
Demo-mode blok op de loginpagina (optioneel, bron-specifiek)
Onder process.env.NEXT_PUBLIC_DEMO_MODE === 'true' toont de loginpagina
een blok met vier hardcoded demo-accounts (admin@demo.nl / demo123, enz.)
en een Ethereal-testmailbox-verwijzing. Dat is onboarding-portal-specifieke
demo-content, geen generiek onderdeel van dit item — verwijder of vervang dit
blok in login-page.tsx zodra de app eigen demo-accounts heeft, of laat de
env var simpelweg altijd false als er geen demo-mode is.
Bestanden
| Bestand | Landt in | Soort |
|---|---|---|
login-page.tsx |
app/login/page.tsx |
glue — van de app |
activate-page.tsx |
app/activate/[token]/page.tsx |
glue — van de app |
forgot-password-page.tsx |
app/forgot-password/page.tsx |
glue — van de app |
reset-password-page.tsx |
app/reset-password/[token]/page.tsx |
glue — van de app |
activate-route.ts |
app/api/auth/activate/route.ts |
glue — van de app |
forgot-password-route.ts |
app/api/auth/forgot-password/route.ts |
glue — van de app |
reset-password-route.ts |
app/api/auth/reset-password/route.ts |
glue — van de app |
Upstream shadcn-afhankelijkheden: alert, button, card, input,
label, separator. Registry-afhankelijkheden: @col/auth-shell,
@col/brand-logo, @col/sso-button, @col/rate-limit (sinds 1.1.0). Geen
cssVars.
Gebruikt door
| App | Sinds versie | Opmerkingen |
|---|---|---|
supplier-onboarding-vercel (Onboarding Portal) |
1.0.0 | Bronapp. De vier pagina's op productie zijn de basis van dit item; ze zijn niet via shadcn add geïnstalleerd (dit item bestond nog niet toen ze gebouwd werden) maar wel byte-voor-byte de bron ervan. |
Bewuste keuzes
@col/rate-limitalsregistryDependency, niet als eigen bestand van dit item (1.1.0). De routes riepenrateLimit()al aan sinds de eerste extractie, maar het bestand erachter zat niet mee — zie de opmerking bij "Backend-contract" hierboven. Het hoort niet in dit item zelf, want KCB gebruikt dezelfde limiter voor een niet-auth-route: eenregistryDependencyhoudt de implementatie op één plek en overschrijfbaar vianpx shadcn add @col/rate-limit --overwrite, zonder dat dit item zelf (dat eenmalig is, als glue) daarvoor opnieuw geïnstalleerd hoeft te worden.registry:page/registry:file, nietregistry:component. Deze bestanden zijn compositie-eindpunten (App Router-routes), niet herbruikbare bouwstenen — een consumer past ze per definitie aan zodra de auth-flow ook maar iets afwijkt.--overwriteop een tweedeaddzou dan app-specifieke logica wissen; door het type klopt de shadcn CLI's eigen "beheerd vs. van jou"-onderscheid daarmee.- Byte-getrouw gekopieerd, op de startpunt-commentaarblokken na — met één
uitzondering sinds 1.1.0. Geen enkele import, prop of tekst is verder
aangepast tijdens de extractie — inclusief de eigenaardigheid dat
reset-password de vertaalsleutel van forgot-password hergebruikt voor zijn
terugknop. Elke andere afwijking van de bron zou de round-trip-verificatie
(bron vs. teruggeïnstalleerd resultaat) laten falen, en dat is precies het
bewijs dat de extractie klopt. De ene bewuste uitzondering: de drie routes
riepen
rateLimit(ip, key, max, windowMs)aan zoals de bron dat op het moment van extractie deed; dat is aangepast naarrateLimit(\${ip}:key`, max, windowMs)— het contract van@col/rate-limit— zodra dat item eenregistryDependency` werd. De bronapp zelf gebruikt op dit moment nog de oude signatuur op zijn eigen, niet uit de registry gehaalde kopie. verify-item.mjskreeg eennext.config.jsin de wegwerp-sandbox. Dit was het eerste item metregistry:page-bestanden. Zonder eennext.config.*-bestand detecteert de shadcn CLI geen framework (valt terug op"manual"), en voorregistry:page-bestanden resolvet het target dan naar een lege string — de pagina's landen dan stil nergens, zonder foutmelding. De vaste sandbox raakt geen ander item: die hadden allemaal alleenregistry:component/registry:file/registry:lib, waar frameworkdetectie niet in het pad zit.
Bekende beperkingen
- Geen enkele backend-afhankelijkheid (Prisma-model, NextAuth-config, e-mailverzending, i18n-vertaalbestanden) zit in dit item. Dat is bewust — zie "Wat je app moet leveren" — maar betekent dat de app na installatie niet bouwt totdat al die stukken zelf zijn ingevuld.
- Het demo-mode-blok op de loginpagina is bron-specifiek (zie hierboven) en moet met de hand verwijderd of aangepast worden; er is geen prop of flag om het generiek te maken zonder de bestandsinhoud te wijzigen.
- Geen preview-pagina met levende voorbeelden (zoals
@col/sso-buttonwel heeft) — de pagina's hebben een echte NextAuth-sessie en Prisma-database nodig om te renderen. De preview-pagina van dit item (/auth-pages-starterin deze repo) toont daarom de bestandenlijst en verwachtingen, geen werkend scherm.