Changeset 2d66d66 in Klonkt
- Timestamp:
- 07/19/2026 04:17:44 PM (7 weeks ago)
- 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)
- Files:
-
- 7 edited
-
CHANGELOG.de.md (modified) (1 diff)
-
CHANGELOG.md (modified) (1 diff)
-
CHANGELOG.nl.md (modified) (1 diff)
-
src/routes/account.js (modified) (2 diffs)
-
src/services/OAuthService.js (modified) (1 diff)
-
src/services/i18n.js (modified) (3 diffs)
-
src/views/pages/account.ejs (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
CHANGELOG.de.md
r4407c67 r2d66d66 7 7 8 8 ### Hinzugefügt 9 - **Verbundene Apps auf deiner Kontoseite widerrufen.** Ein Abschnitt 10 "Verbundene Apps" listet jede App auf, der du per OAuth Zugriff gegeben hast 11 (Name, Seite, Scope, zuletzt genutzt), mit einem Widerrufen-Knopf. Bereits 12 vergebene Tokens erscheinen ebenfalls, da sie immer (gehasht) gespeichert 13 waren; das Token selbst wird nie behalten, daher erfolgt der Widerruf über den 14 Token-Hash. 9 15 - **Der Kontoinhaber kann seine eigenen Follower und Gefolgten über C2S lesen.** 10 16 Die `followers`- und `following`-Sammlungen bleiben für die Öffentlichkeit -
CHANGELOG.md
r4407c67 r2d66d66 7 7 8 8 ### Added 9 - **Revoke connected apps from your account page.** A "Connected apps" section 10 lists every app you authorized over OAuth (name, site, scope, last used) with 11 a Revoke button. Tokens you already granted show up too, since they were 12 always stored (hashed); the bearer itself is never kept, so revocation is keyed 13 on the token hash. 9 14 - **The account owner can read their own followers and following over C2S.** The 10 15 `followers` and `following` collections stay count-only for the public -
CHANGELOG.nl.md
r4407c67 r2d66d66 7 7 8 8 ### Toegevoegd 9 - **Trek verbonden apps in vanaf je accountpagina.** Een sectie "Verbonden apps" 10 toont elke app die je via OAuth toegang gaf (naam, site, scope, laatst 11 gebruikt) met een Intrekken-knop. Al eerder uitgegeven tokens verschijnen ook, 12 want die stonden altijd al (gehasht) opgeslagen; het token zelf bewaren we 13 nooit, dus intrekken gaat op de token-hash. 9 14 - **De account-eigenaar kan zijn eigen followers en following lezen via C2S.** De 10 15 `followers`- en `following`-collecties blijven count-only voor het publiek -
src/routes/account.js
r4407c67 r2d66d66 23 23 import { getPrimarySite } from '../middleware/site.js'; 24 24 import { renderPage } from '../middleware/render.js'; 25 import OAuth from '../services/OAuthService.js'; 26 import { t } from '../services/i18n.js'; 25 27 import { requireAuth } from '../middleware/auth.js'; 26 28 import { toWebp } from '../services/ImageWebpService.js'; … … 75 77 // Display fallback: when you have no own account avatar, show your site's photo. 76 78 siteAvatar: editableSite ? editableSite.profile_photo : null, 79 // OAuth apps (C2S) this user has authorized, so they can revoke them here. 80 authorizations: OAuth.listAuthorizations(req.session.user.id), 77 81 success: req.query.success || null, 78 82 error: req.query.error || null, 79 83 }); 84 }); 85 86 // ==================== REVOKE AN OAUTH APP AUTHORIZATION ==================== 87 router.post('/oauth/revoke', requireAuth, (req, res) => { 88 const lang = req.session.lang || (req.session.user && req.session.user.lang) || 'nl'; 89 const ok = OAuth.revokeAuthorization(req.session.user.id, req.body.token_hash); 90 const msg = ok ? t(lang, 'acct.oauth_revoked') : t(lang, 'acct.oauth_revoke_none'); 91 res.redirect('/account?' + (ok ? 'success' : 'error') + '=' + encodeURIComponent(msg)); 80 92 }); 81 93 -
src/services/OAuthService.js
r4407c67 r2d66d66 109 109 } 110 110 111 export default { registerClient, getClient, createCode, exchangeCode, verifyBearer, revokeToken, validRedirectUri }; 111 // The active authorizations (bearer tokens) a user has granted, with the app 112 // name and the site each is scoped to. The bearer itself is never stored, so 113 // revocation is keyed on token_hash: safe to render, you cannot derive the 114 // token from its hash. 115 export function listAuthorizations(userId) { 116 return db.prepare(` 117 SELECT t.token_hash, t.site_slug, t.scope, t.created_at, t.last_used_at, c.client_name 118 FROM oauth_tokens t 119 LEFT JOIN oauth_clients c ON c.client_id = t.client_id 120 WHERE t.user_id = ? 121 ORDER BY t.created_at DESC 122 `).all(String(userId || '')); 123 } 124 125 // Revoke one authorization, scoped to the owner so a user can only revoke their 126 // own tokens. Returns true when a row was removed. 127 export function revokeAuthorization(userId, tokenHash) { 128 try { 129 const r = db.prepare('DELETE FROM oauth_tokens WHERE token_hash = ? AND user_id = ?') 130 .run(String(tokenHash || ''), String(userId || '')); 131 return r.changes > 0; 132 } catch { return false; } 133 } 134 135 export default { 136 registerClient, getClient, createCode, exchangeCode, verifyBearer, revokeToken, validRedirectUri, 137 listAuthorizations, revokeAuthorization, 138 }; -
src/services/i18n.js
r4407c67 r2d66d66 744 744 'acct.back_home': 'Terug naar home', 745 745 'acct.title': 'Account', 746 'acct.subtitle': 'Profiel en avatar.', 746 'acct.subtitle': 'Profiel en avatar.', 'acct.oauth_apps': 'Verbonden apps', 'acct.oauth_hint': 'Apps die je toegang tot je account hebt gegeven via OAuth. Trek in wat je niet meer vertrouwt of gebruikt.', 'acct.oauth_none': 'Nog geen apps verbonden.', 'acct.oauth_unknown_app': 'Onbekende app', 'acct.oauth_last_used': 'laatst gebruikt', 'acct.oauth_never': 'nooit', 'acct.oauth_revoke': 'Intrekken', 'acct.oauth_revoked': 'App-toegang ingetrokken.', 'acct.oauth_revoke_none': 'Die toegang bestond niet meer.', 747 747 'acct.viewer_mode': 'Kijker-modus', 748 748 'acct.viewer_note': 'Dit is een demo-account. Je kunt alles bekijken, maar niets wijzigen — ook geen foto of bio.', … … 1671 1671 'acct.back_home': 'Back to home', 1672 1672 'acct.title': 'Account', 1673 'acct.subtitle': 'Profile and avatar.', 1673 'acct.subtitle': 'Profile and avatar.', 'acct.oauth_apps': 'Connected apps', 'acct.oauth_hint': 'Apps you granted access to your account via OAuth. Revoke anything you no longer trust or use.', 'acct.oauth_none': 'No apps connected yet.', 'acct.oauth_unknown_app': 'Unknown app', 'acct.oauth_last_used': 'last used', 'acct.oauth_never': 'never', 'acct.oauth_revoke': 'Revoke', 'acct.oauth_revoked': 'App access revoked.', 'acct.oauth_revoke_none': 'That access no longer existed.', 1674 1674 'acct.viewer_mode': 'Viewer mode', 1675 1675 'acct.viewer_note': 'This is a demo account. You can view everything, but change nothing — not even your photo or bio.', … … 2598 2598 'acct.back_home': 'Zurück zur Startseite', 2599 2599 'acct.title': 'Konto', 2600 'acct.subtitle': 'Profil und Avatar.', 2600 'acct.subtitle': 'Profil und Avatar.', 'acct.oauth_apps': 'Verbundene Apps', 'acct.oauth_hint': 'Apps, denen du \u00fcber OAuth Zugriff auf dein Konto gegeben hast. Widerrufe, was du nicht mehr vertraust oder nutzt.', 'acct.oauth_none': 'Noch keine Apps verbunden.', 'acct.oauth_unknown_app': 'Unbekannte App', 'acct.oauth_last_used': 'zuletzt genutzt', 'acct.oauth_never': 'nie', 'acct.oauth_revoke': 'Widerrufen', 'acct.oauth_revoked': 'App-Zugriff widerrufen.', 'acct.oauth_revoke_none': 'Dieser Zugriff bestand nicht mehr.', 2601 2601 'acct.viewer_mode': 'Betrachter-Modus', 2602 2602 'acct.viewer_note': 'Dies ist ein Demo-Konto. Du kannst alles ansehen, aber nichts ändern — auch kein Foto oder keine Bio.', -
src/views/pages/account.ejs
r4407c67 r2d66d66 117 117 <%# ── PASSWORD ──────────────────────────────────────────── %> 118 118 <%# 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 119 143 <% if (canMutate) { %> 120 144 <% if (hasPassword) { %> … … 331 355 .ax-profile-id { flex-direction: column; align-items: flex-start; gap: 0.75rem; text-align: left; } 332 356 } 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 } 333 367 </style>
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)