diff --git a/components/Roles.vue b/components/Roles.vue index 08c4d2d7b..4e05a836f 100644 --- a/components/Roles.vue +++ b/components/Roles.vue @@ -59,6 +59,7 @@ export default { 'code', 'org', 'impersonate', + 'community', ], }; }, diff --git a/routes/profile.vue b/routes/profile.vue index 06d702538..f3c013d63 100644 --- a/routes/profile.vue +++ b/routes/profile.vue @@ -142,7 +142,7 @@ -
+
@@ -150,17 +150,45 @@
Impersonate Audit log +
+
+ Authenticators +
+ +

+ + Include expired ones + +

+
    + +
+
+
@@ -276,6 +304,9 @@ export default mainPronoun.extend({ hasSus: false, contentWarningDismissed: false, + + authenticators: undefined, + showExpiredAuthenticators: false, }; }, head() { @@ -342,6 +373,9 @@ export default mainPronoun.extend({ } this.terms = await this.$axios.$get('/terms'); } + if (this.$isGranted('*') || this.$isGranted('community')) { + this.authenticators = await this.$axios.$get(`/admin/authenticators/${this.user.id}`); + } }, methods: { async generateCard(dark) { @@ -397,6 +431,20 @@ export default mainPronoun.extend({ await this.$router.push(`/${this.$config.user.route}`); setTimeout(() => window.location.reload(), 500); }, + authenticatorIcon(type) { + switch (type) { + case 'email': + case 'changedEmail': + return 'envelope'; + case 'indieauth': + return 'indieauth.png'; + case 'mfa_secret': + case 'mfa_recovery': + return 'mobile'; + default: + return `b:${type}`; + } + }, }, }); diff --git a/server/routes/admin.js b/server/routes/admin.js index 3066cc23a..a3b46afb2 100644 --- a/server/routes/admin.js +++ b/server/routes/admin.js @@ -1,7 +1,7 @@ import { Router } from 'express'; import SQL from 'sql-template-strings'; import avatar from '../avatar.ts'; -import { buildDict, now, shuffle, handleErrorAsync } from '../../src/helpers.ts'; +import { buildDict, now, shuffle, handleErrorAsync, filterObjectKeys } from '../../src/helpers.ts'; import allLocales from '../../locale/locales.ts'; import fs from 'fs'; import { caches } from '../../src/cache.js'; @@ -657,4 +657,27 @@ router.get('/admin/audit-log/:username/:id', handleErrorAsync(async (req, res) = `)); })); +router.get('/admin/authenticators/:id', handleErrorAsync(async (req, res) => { + if (!req.isGranted('community') && !req.isGranted('*')) { + return res.status(401).json({ error: 'Unauthorised' }); + } + + const authenticators = (await req.db.all(SQL` + SELECT * FROM authenticators + WHERE userId = ${req.params.id} + ORDER BY id DESC + `)).map((auth) => { + delete auth.userId; + + const payload = JSON.parse(auth.payload); + auth.payload = typeof payload === 'string' + ? null + : filterObjectKeys(payload, ['id', 'email', 'name', 'instance', 'username']); + + return auth; + }); + + return res.json(authenticators); +})); + export default router; diff --git a/src/helpers.ts b/src/helpers.ts index 4b938ac09..5d3be744d 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -258,7 +258,7 @@ export const randomItemWeighted = (array: T[]): T => { export const randomNumber = (min: number, max: number): number => Math.floor(Math.random() * (max - min + 1)) + min; -const RESTRICTED_AREAS = ['code', 'org', 'impersonate']; +const RESTRICTED_AREAS = ['code', 'org', 'impersonate', 'community']; export const isGranted = (user: Pick, locale: string | null, area: string = ''): boolean => { if (area === '*') { @@ -502,3 +502,12 @@ export const parseUserJwt = (token: string): string | JwtPayload | null => { return null; } }; + +export const filterObjectKeys = , K extends keyof T>(obj: T, keysToKeep: K[]): Pick => { + return keysToKeep.reduce((filteredObj, key) => { + if (key in obj) { + filteredObj[key] = obj[key]; + } + return filteredObj; + }, {} as Pick); +};