Index: src/routes/account.js
===================================================================
--- src/routes/account.js	(revision 4407c67257b1d061f0376ef1ea63ea597c308e4b)
+++ src/routes/account.js	(revision 2d66d66df4fdd568ae1d3ba78850058c55572590)
@@ -23,4 +23,6 @@
 import { getPrimarySite } from '../middleware/site.js';
 import { renderPage } from '../middleware/render.js';
+import OAuth from '../services/OAuthService.js';
+import { t } from '../services/i18n.js';
 import { requireAuth } from '../middleware/auth.js';
 import { toWebp } from '../services/ImageWebpService.js';
@@ -75,7 +77,17 @@
     // Display fallback: when you have no own account avatar, show your site's photo.
     siteAvatar: editableSite ? editableSite.profile_photo : null,
+    // OAuth apps (C2S) this user has authorized, so they can revoke them here.
+    authorizations: OAuth.listAuthorizations(req.session.user.id),
     success: req.query.success || null,
     error: req.query.error || null,
   });
+});
+
+// ==================== REVOKE AN OAUTH APP AUTHORIZATION ====================
+router.post('/oauth/revoke', requireAuth, (req, res) => {
+  const lang = req.session.lang || (req.session.user && req.session.user.lang) || 'nl';
+  const ok = OAuth.revokeAuthorization(req.session.user.id, req.body.token_hash);
+  const msg = ok ? t(lang, 'acct.oauth_revoked') : t(lang, 'acct.oauth_revoke_none');
+  res.redirect('/account?' + (ok ? 'success' : 'error') + '=' + encodeURIComponent(msg));
 });
 
