Changeset 2d66d66 in Klonkt for src/views/pages


Ignore:
Timestamp:
07/19/2026 04:17:44 PM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
33e1dbd
Parents:
4407c67
git-author:
Robin <roboburr@…> (07/19/2026 04:17:27 PM)
git-committer:
Robin <roboburr@…> (07/19/2026 04:17:44 PM)
Message:

Feature: revoke connected OAuth apps from the account page

You issue C2S bearer tokens (Shaer, etc.) but had no way to see or revoke them.
The account page now has a 'Connected apps' section listing every authorization
(app name via the client join, site, scope, last used) with a Revoke button.
Already-issued tokens appear because they were always stored (hashed) with the
user/client/site; the bearer is never kept, so revocation is keyed on the safe
token_hash and scoped to the owner (you cannot revoke someone else's).

  • OAuthService.listAuthorizations(userId) / revokeAuthorization(userId, hash).
  • account.js: authorizations passed to the page; POST /account/oauth/revoke.
  • account.ejs: the section + styles; i18n NL/EN/DE. Visible to viewers too (revoking your own app access is a safety action).

83 tests green. Live-verified: two apps listed on /account, revoke one -> it is
gone and the other stays, token count drops in the DB.

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

File:
1 edited

Legend:

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

    r4407c67 r2d66d66  
    117117  <%# ── PASSWORD ──────────────────────────────────────────── %>
    118118  <%# In viewer mode there is no password section (nothing to change). %>
     119  <%# ── CONNECTED APPS (OAuth C2S) ─────────────────────────── %>
     120  <section class="ax-card">
     121    <div class="ax-card-title"><%= t('acct.oauth_apps') %></div>
     122    <p class="ax-tagline" style="margin:0 0 .6rem"><%= t('acct.oauth_hint') %></p>
     123    <% if (!authorizations || !authorizations.length) { %>
     124      <p class="ax-oauth-empty"><%= t('acct.oauth_none') %></p>
     125    <% } else { %>
     126      <ul class="ax-oauth-list">
     127        <% authorizations.forEach(function(a){ %>
     128          <li class="ax-oauth-item">
     129            <div class="ax-oauth-info">
     130              <strong><%= a.client_name || t('acct.oauth_unknown_app') %></strong>
     131              <span class="ax-oauth-meta">@<%= a.site_slug %> · <%= a.scope || 'c2s' %> · <%= t('acct.oauth_last_used') %> <%= a.last_used_at ? formatDate(a.last_used_at) : t('acct.oauth_never') %></span>
     132            </div>
     133            <form action="/account/oauth/revoke" method="post">
     134              <input type="hidden" name="token_hash" value="<%= a.token_hash %>">
     135              <button type="submit" class="ax-btn ax-btn-danger"><%= t('acct.oauth_revoke') %></button>
     136            </form>
     137          </li>
     138        <% }); %>
     139      </ul>
     140    <% } %>
     141  </section>
     142
    119143  <% if (canMutate) { %>
    120144  <% if (hasPassword) { %>
     
    331355  .ax-profile-id { flex-direction: column; align-items: flex-start; gap: 0.75rem; text-align: left; }
    332356}
     357
     358.ax-oauth-empty { margin: 0; color: var(--ink-soft, #888); }
     359.ax-oauth-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .6rem; }
     360.ax-oauth-item { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: .7rem .85rem; border-radius: 12px; background: var(--paper-2, rgba(0,0,0,.04)); }
     361.ax-oauth-info { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
     362.ax-oauth-info strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
     363.ax-oauth-meta { font-size: .8rem; color: var(--ink-soft, #888); }
     364@media (max-width: 480px) {
     365  .ax-oauth-item { flex-direction: column; align-items: stretch; }
     366}
    333367</style>
Note: See TracChangeset for help on using the changeset viewer.