add: approval section in control panel

This commit is contained in:
Mar0xy 2023-10-20 00:02:01 +02:00
parent fc5d75f8d4
commit 142f500f4b
No known key found for this signature in database
GPG key ID: 56569BBE47D2C828
7 changed files with 199 additions and 1 deletions

View file

@ -34,6 +34,7 @@ signup: "Sign Up"
uploading: "Uploading..." uploading: "Uploading..."
save: "Save" save: "Save"
users: "Users" users: "Users"
approvals: "Approvals"
addUser: "Add a user" addUser: "Add a user"
favorite: "Add to favorites" favorite: "Add to favorites"
favorites: "Favorites" favorites: "Favorites"

1
locales/index.d.ts vendored
View file

@ -37,6 +37,7 @@ export interface Locale {
"uploading": string; "uploading": string;
"save": string; "save": string;
"users": string; "users": string;
"approvals": string;
"addUser": string; "addUser": string;
"favorite": string; "favorite": string;
"favorites": string; "favorites": string;

View file

@ -34,6 +34,7 @@ signup: "新規登録"
uploading: "アップロード中" uploading: "アップロード中"
save: "保存" save: "保存"
users: "ユーザー" users: "ユーザー"
approvals: "承認"
addUser: "ユーザーを追加" addUser: "ユーザーを追加"
favorite: "お気に入り" favorite: "お気に入り"
favorites: "お気に入り" favorites: "お気に入り"

View file

@ -0,0 +1,114 @@
<template>
<MkFolder :expanded="false">
<template #icon><i class="ph-user ph-bold ph-lg"></i></template>
<template #label>{{ i18n.ts.user }}: {{ user.username }}</template>
<div class="_gaps_s" :class="$style.root">
<div :class="$style.items">
<div>
<div :class="$style.label">{{ i18n.ts.createdAt }}</div>
<div><MkTime :time="user.createdAt" mode="absolute"/></div>
</div>
<div v-if="email">
<div :class="$style.label">{{ i18n.ts.emailAddress }}</div>
<div>{{ email }}</div>
</div>
<div>
<div :class="$style.label">Reason</div>
<div>{{ reason }}</div>
</div>
</div>
<div :class="$style.buttons">
<MkButton inline success @click="approveAccount(user)">{{ i18n.ts.approveAccount }}</MkButton>
<MkButton inline danger @click="deleteAccount(user)">{{ i18n.ts.denyAccount }}</MkButton>
</div>
</div>
</MkFolder>
</template>
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
const props = defineProps<{
user: Misskey.entities.User;
}>();
let reason = $ref('');
let email = $ref('');
function getReason() {
return os.api('admin/show-user', {
userId: props.user.id,
}).then(info => {
reason = info?.signupReason;
email = info?.email;
});
}
getReason();
const emits = defineEmits<{
(event: 'deleted', value: string): void;
}>();
async function deleteAccount(user) {
const confirm = await os.confirm({
type: 'warning',
text: i18n.ts.deleteAccountConfirm,
});
if (confirm.canceled) return;
const typed = await os.inputText({
text: i18n.t('typeToConfirm', { x: user?.username }),
});
if (typed.canceled) return;
if (typed.result === user?.username) {
await os.apiWithDialog('admin/delete-account', {
userId: user.id,
});
emits('deleted', user.id);
} else {
os.alert({
type: 'error',
text: 'input not match',
});
}
}
async function approveAccount(user) {
const confirm = await os.confirm({
type: 'warning',
text: i18n.ts.approveConfirm,
});
if (confirm.canceled) return;
await os.api('admin/approve-user', { userId: user.id });
emits('deleted', user.id);
}
</script>
<style lang="scss" module>
.root {
text-align: left;
}
.items {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
grid-gap: 12px;
}
.label {
font-size: 0.85em;
padding: 0 0 8px 0;
user-select: none;
opacity: 0.7;
}
.buttons {
display: flex;
gap: 8px;
}
</style>

View file

@ -0,0 +1,72 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="900">
<div class="_gaps_m">
<MkPagination ref="paginationComponent" :pagination="pagination">
<template #default="{ items }">
<div class="_gaps_s">
<SkApprovalUser v-for="item in items" :key="item.id" :user="(item as any)" :onDeleted="deleted"/>
</div>
</template>
</MkPagination>
</div>
</MkSpacer>
</MkStickyContainer>
</div>
</template>
<script lang="ts" setup>
import { computed, shallowRef } from 'vue';
import XHeader from './_header_.vue';
import MkPagination from '@/components/MkPagination.vue';
import SkApprovalUser from '@/components/SkApprovalUser.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
let paginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
const pagination = {
endpoint: 'admin/show-users' as const,
limit: 10,
params: computed(() => ({
sort: '+createdAt',
state: 'approved',
origin: 'local',
})),
offsetMode: true,
};
function deleted(id: string) {
if (paginationComponent.value) {
paginationComponent.value.items.delete(id);
}
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata(computed(() => ({
title: i18n.ts.approvals,
icon: 'ph-chalkboard-teacher ph-bold pg-lg',
})));
</script>
<style lang="scss" module>
.inputs {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.input {
flex: 1;
}
</style>

View file

@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo v-if="noMaintainerInformation" warn class="info">{{ i18n.ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo> <MkInfo v-if="noMaintainerInformation" warn class="info">{{ i18n.ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
<MkInfo v-if="noBotProtection" warn class="info">{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo> <MkInfo v-if="noBotProtection" warn class="info">{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
<MkInfo v-if="noEmailServer" warn class="info">{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo> <MkInfo v-if="noEmailServer" warn class="info">{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
<MkInfo v-if="pendingUserApprovals" warn class="info">{{ i18n.ts.pendingUserApprovals }} <MkA to="/admin/users" class="_link">{{ i18n.ts.check }}</MkA></MkInfo> <MkInfo v-if="pendingUserApprovals" warn class="info">{{ i18n.ts.pendingUserApprovals }} <MkA to="/admin/approvals" class="_link">{{ i18n.ts.check }}</MkA></MkInfo>
<MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu> <MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu>
</div> </div>
@ -114,6 +114,11 @@ const menuDef = $computed(() => [{
text: i18n.ts.invite, text: i18n.ts.invite,
to: '/admin/invites', to: '/admin/invites',
active: currentPage?.route.name === 'invites', active: currentPage?.route.name === 'invites',
}, {
icon: 'ph-chalkboard-teacher ph-bold ph-lg',
text: i18n.ts.approvals,
to: '/admin/approvals',
active: currentPage?.route.name === 'approvals',
}, { }, {
icon: 'ph-seal-check ph-bold pg-lg', icon: 'ph-seal-check ph-bold pg-lg',
text: i18n.ts.roles, text: i18n.ts.roles,

View file

@ -443,6 +443,10 @@ export const routes = [{
path: '/invites', path: '/invites',
name: 'invites', name: 'invites',
component: page(() => import('./pages/admin/invites.vue')), component: page(() => import('./pages/admin/invites.vue')),
}, {
path: '/approvals',
name: 'approvals',
component: page(() => import('./pages/admin/approvals.vue')),
}, { }, {
path: '/', path: '/',
component: page(() => import('./pages/_empty_.vue')), component: page(() => import('./pages/_empty_.vue')),