Merge branch 'admin-display-authenticators' into 'main'

(admin) display authenticators

See merge request PronounsPage/PronounsPage!460
This commit is contained in:
Andrea Vos 2024-05-23 18:19:00 +00:00
commit cbd466517b
4 changed files with 84 additions and 3 deletions

View File

@ -59,6 +59,7 @@ export default {
'code', 'code',
'org', 'org',
'impersonate', 'impersonate',
'community',
], ],
}; };
}, },

View File

@ -142,7 +142,7 @@
</template> </template>
</div> </div>
<div v-if="$isGranted('*')" class="list-group list-group-flare mt-3"> <div v-if="$isGranted('*') || $isGranted('community')" class="list-group list-group-flare mt-3">
<div class="list-group-item pt-3"> <div class="list-group-item pt-3">
<h5> <h5>
<Icon v="user-cog" /> <Icon v="user-cog" />
@ -150,17 +150,45 @@
</h5> </h5>
</div> </div>
<a <a
v-if="$isGranted('*')"
href="#" href="#"
class="list-group-item list-group-item-action list-group-item-hoverable small" class="list-group-item list-group-item-action list-group-item-hoverable small"
@click.prevent="impersonate()" @click.prevent="impersonate()"
><Icon v="user-secret" /> Impersonate ><Icon v="user-secret" /> Impersonate
</a> </a>
<nuxt-link <nuxt-link
v-if="$isGranted('*')"
:to="`/admin/audit-log/${user.username}/${user.id}`" :to="`/admin/audit-log/${user.username}/${user.id}`"
class="list-group-item list-group-item-action list-group-item-hoverable small" class="list-group-item list-group-item-action list-group-item-hoverable small"
> >
<Icon v="file-search" /> Audit log <Icon v="file-search" /> Audit log
</nuxt-link> </nuxt-link>
<div class="list-group-item pt-3">
<h6>
<Icon v="key" /> Authenticators
</h6>
<Loading :value="authenticators">
<p>
<a v-if="!showExpiredAuthenticators" href="#" @click.prevent="showExpiredAuthenticators = true">
Include expired ones
</a>
</p>
<ul>
<template v-for="authenticator in authenticators">
<li
v-if="showExpiredAuthenticators || authenticator.validUntil === null"
:class="authenticator.validUntil === null ? '' : 'small text-muted'"
>
<Tooltip :text="authenticator.type">
<Icon :v="authenticatorIcon(authenticator.type)" />
</Tooltip>
{{ $datetime($ulidTime(authenticator.id)) }}
<pre><code>{{ authenticator.payload }}</code></pre>
</li>
</template>
</ul>
</Loading>
</div>
</div> </div>
</div> </div>
@ -276,6 +304,9 @@ export default mainPronoun.extend({
hasSus: false, hasSus: false,
contentWarningDismissed: false, contentWarningDismissed: false,
authenticators: undefined,
showExpiredAuthenticators: false,
}; };
}, },
head() { head() {
@ -342,6 +373,9 @@ export default mainPronoun.extend({
} }
this.terms = await this.$axios.$get('/terms'); 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: { methods: {
async generateCard(dark) { async generateCard(dark) {
@ -397,6 +431,20 @@ export default mainPronoun.extend({
await this.$router.push(`/${this.$config.user.route}`); await this.$router.push(`/${this.$config.user.route}`);
setTimeout(() => window.location.reload(), 500); 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}`;
}
},
}, },
}); });
</script> </script>

View File

@ -1,7 +1,7 @@
import { Router } from 'express'; import { Router } from 'express';
import SQL from 'sql-template-strings'; import SQL from 'sql-template-strings';
import avatar from '../avatar.ts'; 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 allLocales from '../../locale/locales.ts';
import fs from 'fs'; import fs from 'fs';
import { caches } from '../../src/cache.js'; 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; export default router;

View File

@ -258,7 +258,7 @@ export const randomItemWeighted = <T extends WeightedItem>(array: T[]): T => {
export const randomNumber = (min: number, max: number): number => Math.floor(Math.random() * (max - min + 1)) + min; 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<User, 'roles'>, locale: string | null, area: string = ''): boolean => { export const isGranted = (user: Pick<User, 'roles'>, locale: string | null, area: string = ''): boolean => {
if (area === '*') { if (area === '*') {
@ -502,3 +502,12 @@ export const parseUserJwt = (token: string): string | JwtPayload | null => {
return null; return null;
} }
}; };
export const filterObjectKeys = <T extends Record<string, any>, K extends keyof T>(obj: T, keysToKeep: K[]): Pick<T, K> => {
return keysToKeep.reduce((filteredObj, key) => {
if (key in obj) {
filteredObj[key] = obj[key];
}
return filteredObj;
}, {} as Pick<T, K>);
};