add: approval section in control panel
This commit is contained in:
parent
fc5d75f8d4
commit
142f500f4b
7 changed files with 199 additions and 1 deletions
|
@ -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
1
locales/index.d.ts
vendored
|
@ -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;
|
||||||
|
|
|
@ -34,6 +34,7 @@ signup: "新規登録"
|
||||||
uploading: "アップロード中"
|
uploading: "アップロード中"
|
||||||
save: "保存"
|
save: "保存"
|
||||||
users: "ユーザー"
|
users: "ユーザー"
|
||||||
|
approvals: "承認"
|
||||||
addUser: "ユーザーを追加"
|
addUser: "ユーザーを追加"
|
||||||
favorite: "お気に入り"
|
favorite: "お気に入り"
|
||||||
favorites: "お気に入り"
|
favorites: "お気に入り"
|
||||||
|
|
114
packages/frontend/src/components/SkApprovalUser.vue
Normal file
114
packages/frontend/src/components/SkApprovalUser.vue
Normal 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>
|
72
packages/frontend/src/pages/admin/approvals.vue
Normal file
72
packages/frontend/src/pages/admin/approvals.vue
Normal 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>
|
|
@ -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,
|
||||||
|
|
|
@ -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')),
|
||||||
|
|
Loading…
Reference in a new issue