mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-08-03 19:17:07 -04:00
224 lines
9.5 KiB
Vue
224 lines
9.5 KiB
Vue
<template>
|
|
<Page>
|
|
<NotFound v-if="!$isGranted('users') && !$isGranted('community')" />
|
|
<div v-else>
|
|
<p>
|
|
<nuxt-link to="/admin">
|
|
<Icon v="user-cog" />
|
|
<T>admin.header</T>
|
|
</nuxt-link>
|
|
</p>
|
|
<h2>
|
|
<Icon v="user-cog" />
|
|
Users
|
|
</h2>
|
|
|
|
<section>
|
|
<details class="border mb-3" @click="showUsers = true">
|
|
<summary class="bg-light p-3">
|
|
<Icon v="users" />
|
|
Users
|
|
({{ stats._.users }} overall, {{ stats._.admins }} admins)
|
|
</summary>
|
|
<div v-if="showUsers" class="border-top">
|
|
<div class="input-group mt-4">
|
|
<input v-model="userFilter" class="form-control" :placeholder="$t('crud.filterLong')">
|
|
<button
|
|
:class="['btn', adminsFilter ? 'btn-secondary' : 'btn-outline-secondary']"
|
|
@click="adminsFilter = !adminsFilter"
|
|
>
|
|
Only admins
|
|
</button>
|
|
<button
|
|
:class="['btn', localeFilter ? 'btn-secondary' : 'btn-outline-secondary']"
|
|
@click="localeFilter = !localeFilter"
|
|
>
|
|
Only this version
|
|
</button>
|
|
</div>
|
|
<ServerTable
|
|
endpoint="/api/admin/users"
|
|
:query="{ filter: userFilterDelayed || undefined, localeFilter: localeFilter || undefined, adminsFilter: adminsFilter || undefined }"
|
|
:columns="5"
|
|
count
|
|
>
|
|
<template #header>
|
|
<th class="text-nowrap">
|
|
<T>admin.user.user</T>
|
|
</th>
|
|
<th class="text-nowrap">
|
|
<T>admin.user.createdAt</T>
|
|
</th>
|
|
<th class="text-nowrap">
|
|
<T>admin.user.email</T>
|
|
</th>
|
|
<th class="text-nowrap">
|
|
<T>admin.user.roles</T>
|
|
</th>
|
|
<th class="text-nowrap">
|
|
<T>admin.user.profiles</T>
|
|
</th>
|
|
</template>
|
|
|
|
<template #row="s">
|
|
<td>
|
|
<a :href="`https://pronouns.page/@${s.el.username}`">@{{ s.el.username }}</a>
|
|
<a v-if="$isGranted('*') || $isGranted('impersonate')" href="#" class="badge bg-primary text-white" @click.prevent="impersonate(s.el.email)"><Icon v="user-secret" /></a>
|
|
<nuxt-link v-if="$isGranted('*')" :to="`/admin/audit-log/${s.el.username}/${s.el.id}`" class="badge bg-primary text-white">
|
|
<Icon v="file-search" />
|
|
</nuxt-link>
|
|
<a v-if="$isGranted('*') || $isGranted('community')" href="#" class="badge bg-danger text-white" @click.prevent="erasure(s.el.id, s.el.email)"><Icon v="truck-plow" /></a>
|
|
</td>
|
|
<td>
|
|
{{ $datetime($ulidTime(s.el.id)) }}
|
|
</td>
|
|
<td>
|
|
<p>
|
|
<a :href="`mailto:${s.el.email}`" target="_blank" rel="noopener">
|
|
{{ s.el.email }}
|
|
</a>
|
|
</p>
|
|
<!--
|
|
<ul v-if="s.el.socialConnections.length" class="list-inline">
|
|
<li v-for="conn in s.el.socialConnections" class="list-inline-item">
|
|
<Icon :v="socialProviders[conn].icon || conn" set="b"/>
|
|
</li>
|
|
</ul>
|
|
-->
|
|
</td>
|
|
<td>
|
|
<Roles :user="s.el" />
|
|
</td>
|
|
<td>
|
|
<ul class="list-unstyled">
|
|
<template v-for="locale in s.el.profiles">
|
|
<li v-if="$locales[locale]" :key="locale">
|
|
<LocaleLink :link="`/@${s.el.username}`" :locale="locale">
|
|
{{ $locales[locale].name }}
|
|
</LocaleLink>
|
|
</li>
|
|
</template>
|
|
</ul>
|
|
</td>
|
|
</template>
|
|
</ServerTable>
|
|
</div>
|
|
</details>
|
|
</section>
|
|
|
|
<section>
|
|
<ChartSet name="users" :data="chart" init="cumulative" />
|
|
</section>
|
|
|
|
<section>
|
|
<Chart
|
|
label="number of profiles by locale"
|
|
:data="profilesByLocale"
|
|
type="bar"
|
|
:options="{
|
|
indexAxis: 'y',
|
|
responsive: true,
|
|
interaction: {
|
|
intersect: false,
|
|
mode: 'y',
|
|
},
|
|
}"
|
|
/>
|
|
</section>
|
|
</div>
|
|
</Page>
|
|
</template>
|
|
|
|
<script>
|
|
import { longtimeCookieSetting } from '../src/cookieSettings.ts';
|
|
import { useNuxtApp } from 'nuxt/app';
|
|
import useSimpleHead from '../composables/useSimpleHead.ts';
|
|
import { socialProviders } from '../src/socialProviders.ts';
|
|
import useConfig from '../composables/useConfig.ts';
|
|
import useDialogue from '../composables/useDialogue.ts';
|
|
|
|
export default {
|
|
async setup() {
|
|
const { $translator: translator } = useNuxtApp();
|
|
const dialogue = useDialogue();
|
|
useSimpleHead({
|
|
title: `${translator.translate('admin.header')} • Users`,
|
|
}, translator);
|
|
|
|
const tokenCookie = useCookie('token', longtimeCookieSetting);
|
|
const impersonatorCookie = useCookie('impersonator', longtimeCookieSetting);
|
|
|
|
|
|
const stats = useFetch('/api/admin/stats');
|
|
const chart = useFetch('/api/admin/stats/users-chart/_');
|
|
await Promise.all([stats, chart]);
|
|
|
|
return {
|
|
config: useConfig(),
|
|
dialogue,
|
|
tokenCookie,
|
|
impersonatorCookie,
|
|
stats: stats.data.value,
|
|
chart: chart.data.value,
|
|
};
|
|
},
|
|
data() {
|
|
return {
|
|
socialProviders,
|
|
showUsers: false,
|
|
userFilter: '',
|
|
userFilterDelayed: '',
|
|
userFilterDelayHandle: undefined,
|
|
localeFilter: true,
|
|
adminsFilter: false,
|
|
};
|
|
},
|
|
computed: {
|
|
profilesByLocale() {
|
|
const r = {};
|
|
|
|
Object.entries(this.stats)
|
|
.filter(([locale, _localeStats]) => locale !== '_' && locale !== 'calculatedAt')
|
|
.sort(([_aLocale, aLocaleStats], [_bLocale, bLocaleStats]) => bLocaleStats.users - aLocaleStats.users)
|
|
.forEach(([locale, localeStats]) => {
|
|
r[locale] = localeStats.users;
|
|
});
|
|
|
|
return r;
|
|
},
|
|
},
|
|
watch: {
|
|
userFilter() {
|
|
if (this.userFilterDelayHandle !== undefined) {
|
|
clearInterval(this.userFilterDelayHandle);
|
|
}
|
|
|
|
this.userFilterDelayHandle = setTimeout(() => {
|
|
this.userFilterDelayed = this.userFilter;
|
|
}, 750);
|
|
},
|
|
},
|
|
methods: {
|
|
async impersonate(email) {
|
|
const { token } = await this.$fetch(`/api/admin/impersonate/${encodeURIComponent(email)}`);
|
|
this.impersonatorCookie = this.tokenCookie;
|
|
this.tokenCookie = token;
|
|
await this.$router.push(`/${this.config.user.route}`);
|
|
setTimeout(() => window.location.reload(), 500);
|
|
},
|
|
async erasure(id, email) {
|
|
await this.dialogue.confirm(`Are you sure you want to remove this account (${email})? ` +
|
|
'This should only be done in two cases: ' +
|
|
'an explicit GPDR request directly from the user, ' +
|
|
'or having proof that owner is not yet 13 years old.', 'danger');
|
|
|
|
if (await this.$csrfFetch(`/api/user/data-erasure/${id}`, { method: 'POST' })) {
|
|
await this.dialogue.alert(`Account ${email} removed successfully`, 'success');
|
|
} else {
|
|
await this.dialogue.alert(this.$t('error.generic', 'danger'));
|
|
}
|
|
},
|
|
},
|
|
};
|
|
</script>
|