Changeset 9e27d64 in Klonkt for src/views/pages


Ignore:
Timestamp:
06/14/2026 07:31:23 AM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
6351545
Parents:
32cc601
Message:

auth: password admin + per-instance Google for listeners (no broker)

Robin's choice: every self-hoster has their own password admin account,
and can optionally let listeners log in to comment using their OWN Google
client. No central broker (that would tie every customer site to Robin's
Google Cloud -> systemic risk on abuse).

  • Admin = username/password (bcrypt). First-time setup via /auth/register (only when there are 0 users); closed afterwards. No public registration.
  • Forgot password: /auth/reset-request -> email (if SMTP configured) with reset link; CLI break-glass npm run reset-admin always works (no email needed).
  • Change password (logged in) restored in /account.
  • Google = per-instance own credentials, OPTIONAL, listeners only -> always role member, never admin (god/admin email is rejected; google_sub mismatch too).
  • config/google.js back to direct Google OAuth; config/mailer.js new (nodemailer).
  • jose removed from deps; nodemailer added.

Security review (workflow) incorporated:

  • Reset token no longer in production logs (dev only).
  • Reset link from PUBLIC_BASE_URL instead of X-Forwarded-Host (host poisoning).
  • Reset tokens stored SHA-256-hashed in the DB.
  • Same-origin check on all state-modifying POSTs (CSRF layer on top of sameSite-lax).
  • Login always runs one bcrypt comparison (no timing enumeration).

Co-Authored-By: Claude <noreply@…>

Location:
src/views/pages
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • src/views/pages/account.ejs

    r32cc601 r9e27d64  
    6565  </section>
    6666
    67   <%# ── INLOG ─────────────────────────────────────────────── %>
     67  <%# ── WACHTWOORD ────────────────────────────────────────── %>
     68  <% if (hasPassword) { %>
     69  <section class="ax-card">
     70    <div class="ax-card-title">Wachtwoord wijzigen</div>
     71    <form action="/account/password" method="post" class="ax-form">
     72      <label class="ax-field">
     73        <span>Huidig wachtwoord</span>
     74        <input type="password" name="current" required autocomplete="current-password">
     75      </label>
     76      <div class="ax-row ax-row-2">
     77        <label class="ax-field">
     78          <span>Nieuw wachtwoord <small>(min 8 tekens)</small></span>
     79          <input type="password" name="new_password" required minlength="8" autocomplete="new-password">
     80        </label>
     81        <label class="ax-field">
     82          <span>Bevestig nieuw wachtwoord</span>
     83          <input type="password" name="confirm" required minlength="8" autocomplete="new-password">
     84        </label>
     85      </div>
     86      <div class="ax-form-actions">
     87        <button type="submit" class="ax-btn ax-btn-primary">🔒 Wachtwoord wijzigen</button>
     88      </div>
     89    </form>
     90  </section>
     91  <% } else { %>
    6892  <section class="ax-card">
    6993    <div class="ax-card-title">Inloggen</div>
    7094    <p class="ax-tagline" style="margin:0">Je bent ingelogd via Google (<%= account.email %>). Er is geen apart wachtwoord.</p>
    7195  </section>
     96  <% } %>
    7297
    7398</div>
  • src/views/pages/auth-login.ejs

    r32cc601 r9e27d64  
    11<div class="container auth-page">
    22  <h1>Inloggen</h1>
     3  <% if (typeof success !== 'undefined' && success) { %><div class="alert alert-success"><%= success %></div><% } %>
    34  <% if (error) { %><div class="alert alert-error"><%= error %></div><% } %>
    45
    5   <div class="auth-form auth-google">
    6     <% if (googleReady) { %>
    7       <p class="auth-intro">Log in met je Google-account om mee te praten.</p>
    8       <a class="btn btn-google" href="/auth/google<%= (typeof next !== 'undefined' && next) ? '?next=' + encodeURIComponent(next) : '' %>">
    9         <svg class="g-icon" width="18" height="18" viewBox="0 0 18 18" aria-hidden="true">
    10           <path fill="#4285F4" d="M17.64 9.2c0-.64-.06-1.25-.16-1.84H9v3.48h4.84a4.14 4.14 0 0 1-1.8 2.72v2.26h2.92c1.7-1.57 2.68-3.88 2.68-6.62z"/>
    11           <path fill="#34A853" d="M9 18c2.43 0 4.47-.8 5.96-2.18l-2.92-2.26c-.8.54-1.84.86-3.04.86-2.34 0-4.32-1.58-5.03-3.7H.96v2.33A9 9 0 0 0 9 18z"/>
    12           <path fill="#FBBC05" d="M3.97 10.72a5.4 5.4 0 0 1 0-3.44V4.95H.96a9 9 0 0 0 0 8.1l3.01-2.33z"/>
    13           <path fill="#EA4335" d="M9 3.58c1.32 0 2.5.45 3.44 1.35l2.58-2.58A9 9 0 0 0 .96 4.95l3.01 2.33C4.68 5.16 6.66 3.58 9 3.58z"/>
    14         </svg>
    15         <span>Inloggen met Google</span>
    16       </a>
    17     <% } else { %>
    18       <p class="auth-intro">Inloggen via Google is op deze site nog niet ingesteld.</p>
     6  <form method="post" action="/auth/login" class="auth-form">
     7    <% if (typeof next !== 'undefined' && next) { %>
     8      <input type="hidden" name="next" value="<%= next %>">
    199    <% } %>
    20   </div>
     10    <label>
     11      <span>Gebruikersnaam of e-mail</span>
     12      <input type="text" name="username" value="<%= username %>" required autofocus autocomplete="username">
     13    </label>
     14    <label>
     15      <span>Wachtwoord</span>
     16      <input type="password" name="password" required autocomplete="current-password">
     17    </label>
     18    <button type="submit" class="btn btn-primary">Inloggen</button>
     19    <p class="auth-link auth-link-muted"><a href="/auth/reset-request">Wachtwoord vergeten?</a></p>
     20  </form>
     21
     22  <% if (googleReady) { %>
     23    <div class="auth-divider"><span>of als luisteraar</span></div>
     24    <a class="btn btn-google" href="/auth/google<%= (typeof next !== 'undefined' && next) ? '?next=' + encodeURIComponent(next) : '' %>">
     25      <svg class="g-icon" width="18" height="18" viewBox="0 0 18 18" aria-hidden="true">
     26        <path fill="#4285F4" d="M17.64 9.2c0-.64-.06-1.25-.16-1.84H9v3.48h4.84a4.14 4.14 0 0 1-1.8 2.72v2.26h2.92c1.7-1.57 2.68-3.88 2.68-6.62z"/>
     27        <path fill="#34A853" d="M9 18c2.43 0 4.47-.8 5.96-2.18l-2.92-2.26c-.8.54-1.84.86-3.04.86-2.34 0-4.32-1.58-5.03-3.7H.96v2.33A9 9 0 0 0 9 18z"/>
     28        <path fill="#FBBC05" d="M3.97 10.72a5.4 5.4 0 0 1 0-3.44V4.95H.96a9 9 0 0 0 0 8.1l3.01-2.33z"/>
     29        <path fill="#EA4335" d="M9 3.58c1.32 0 2.5.45 3.44 1.35l2.58-2.58A9 9 0 0 0 .96 4.95l3.01 2.33C4.68 5.16 6.66 3.58 9 3.58z"/>
     30      </svg>
     31      <span>Inloggen met Google</span>
     32    </a>
     33  <% } %>
    2134</div>
  • src/views/pages/auth-register.ejs

    r32cc601 r9e27d64  
    11<div class="container auth-page">
    2   <h1>Register</h1>
     2  <h1>Beheerder aanmaken</h1>
     3  <p class="auth-intro">Eerste keer opzetten — maak je beheerdersaccount aan. Dit kan maar één keer.</p>
    34  <% if (error) { %><div class="alert alert-error"><%= error %></div><% } %>
    45  <form method="post" action="/auth/register" class="auth-form">
     
    78    <% } %>
    89    <label>
    9       <span>Username (3-32 chars, letters/numbers/_-)</span>
     10      <span>Gebruikersnaam (3-32 tekens, letters/cijfers/_-)</span>
    1011      <input type="text" name="username" value="<%= username %>" required autofocus autocomplete="username" pattern="[a-zA-Z0-9_-]{3,32}">
    1112    </label>
    1213    <label>
    13       <span>Email</span>
     14      <span>E-mail</span>
    1415      <input type="email" name="email" value="<%= email %>" required autocomplete="email">
    1516    </label>
    1617    <label>
    17       <span>Password (min 8 chars)</span>
     18      <span>Wachtwoord (min 8 tekens)</span>
    1819      <input type="password" name="password" required minlength="8" autocomplete="new-password">
    1920    </label>
    20     <button type="submit" class="btn btn-primary">Create account</button>
    21     <p class="auth-link">Already registered?
    22       <a href="/auth/login<%= (typeof next !== 'undefined' && next) ? '?next=' + encodeURIComponent(next) : '' %>">Login</a>
    23     </p>
     21    <button type="submit" class="btn btn-primary">Beheerder aanmaken</button>
    2422  </form>
    2523</div>
  • src/views/pages/auth-reset-request.ejs

    r32cc601 r9e27d64  
    11<div class="container auth-page">
    2   <h1>Reset password</h1>
     2  <h1>Wachtwoord resetten</h1>
    33
    44  <% if (sent) { %>
    55    <div class="alert alert-success">
    6       If an account exists for that email, a reset link has been sent.
     6      Als er een account bestaat voor dat e-mailadres, is er een reset-link verstuurd.
    77    </div>
    88    <% if (devResetUrl) { %>
    99      <div class="alert alert-info">
    10         <strong>Dev mode:</strong> reset link below (no email server configured).
     10        <strong>Geen e-mailserver ingesteld</strong> — reset-link hieronder.
    1111        <p style="margin: 0.5rem 0 0; word-break: break-all;">
    1212          <a href="<%= devResetUrl %>"><%= devResetUrl %></a>
     
    1414      </div>
    1515    <% } %>
    16     <p class="auth-link"><a href="/auth/login">&larr; Back to login</a></p>
     16    <% if (typeof mailer !== 'undefined' && !mailer) { %>
     17      <p class="auth-link-muted">Geen e-mail ingesteld? De beheerder kan ook <code>npm run reset-admin</code> draaien op de server.</p>
     18    <% } %>
     19    <p class="auth-link"><a href="/auth/login">&larr; Terug naar inloggen</a></p>
    1720  <% } else { %>
    18     <p class="auth-tagline">Enter your email and we'll send a reset link.</p>
     21    <p class="auth-tagline">Vul je e-mail in; we sturen een reset-link.</p>
    1922
    2023    <% if (error) { %><div class="alert alert-error"><%= error %></div><% } %>
     
    2225    <form method="post" action="/auth/reset-request" class="auth-form">
    2326      <label>
    24         <span>Email</span>
     27        <span>E-mail</span>
    2528        <input type="email" name="email" required autofocus autocomplete="email">
    2629      </label>
    27       <button type="submit" class="btn btn-primary">Send reset link</button>
    28       <p class="auth-link"><a href="/auth/login">&larr; Back to login</a></p>
     30      <button type="submit" class="btn btn-primary">Stuur reset-link</button>
     31      <p class="auth-link"><a href="/auth/login">&larr; Terug naar inloggen</a></p>
    2932    </form>
    3033  <% } %>
  • src/views/pages/auth-reset.ejs

    r32cc601 r9e27d64  
    11<div class="container auth-page">
    2   <h1>Reset password</h1>
     2  <h1>Wachtwoord resetten</h1>
    33
    44  <% if (!token) { %>
    55    <% if (error) { %><div class="alert alert-error"><%= error %></div><% } %>
    66    <p class="auth-link">
    7       <a href="/auth/reset-request">Request a new reset link</a>
     7      <a href="/auth/reset-request">Vraag een nieuwe reset-link aan</a>
    88      &middot;
    9       <a href="/auth/login">Back to login</a>
     9      <a href="/auth/login">Terug naar inloggen</a>
    1010    </p>
    1111  <% } else { %>
    12     <p class="auth-tagline">Set a new password for <strong><%= username %></strong>.</p>
     12    <p class="auth-tagline">Stel een nieuw wachtwoord in voor <strong><%= username %></strong>.</p>
    1313
    1414    <% if (error) { %><div class="alert alert-error"><%= error %></div><% } %>
     
    1616    <form method="post" action="/auth/reset/<%= token %>" class="auth-form">
    1717      <label>
    18         <span>New password (min 8 chars)</span>
     18        <span>Nieuw wachtwoord (min 8 tekens)</span>
    1919        <input type="password" name="new_password" required minlength="8" autofocus autocomplete="new-password">
    2020      </label>
    2121      <label>
    22         <span>Confirm new password</span>
     22        <span>Bevestig nieuw wachtwoord</span>
    2323        <input type="password" name="confirm" required minlength="8" autocomplete="new-password">
    2424      </label>
    25       <button type="submit" class="btn btn-primary">Set password</button>
     25      <button type="submit" class="btn btn-primary">Wachtwoord instellen</button>
    2626    </form>
    2727  <% } %>
  • src/views/pages/welcome.ejs

    r32cc601 r9e27d64  
    55  <% if (!user) { %>
    66    <div class="welcome-actions">
    7       <a href="/auth/login" class="btn btn-primary">Inloggen met Google</a>
     7      <a href="/auth/register" class="btn btn-primary">Beheerder aanmaken</a>
     8      <a href="/auth/login" class="btn">Al een account? Inloggen</a>
    89    </div>
    9     <p class="welcome-note">De eerste gebruiker die inlogt wordt <strong>god</strong> en krijgt automatisch een eigen site.</p>
     10    <p class="welcome-note">Maak je beheerdersaccount aan om te beginnen — daarna is registratie gesloten.</p>
    1011  <% } else { %>
    1112    <p>Hi <%= user.username %>! No site is configured yet.</p>
Note: See TracChangeset for help on using the changeset viewer.