add: approval section in control panel
This commit is contained in:
parent
fc5d75f8d4
commit
142f500f4b
@ -34,6 +34,7 @@ signup: "Sign Up"
|
||||
uploading: "Uploading..."
|
||||
save: "Save"
|
||||
users: "Users"
|
||||
approvals: "Approvals"
|
||||
addUser: "Add a user"
|
||||
favorite: "Add to favorites"
|
||||
favorites: "Favorites"
|
||||
|
1
locales/index.d.ts
vendored
1
locales/index.d.ts
vendored
@ -37,6 +37,7 @@ export interface Locale {
|
||||
"uploading": string;
|
||||
"save": string;
|
||||
"users": string;
|
||||
"approvals": string;
|
||||
"addUser": string;
|
||||
"favorite": string;
|
||||
"favorites": string;
|
||||
|
@ -34,6 +34,7 @@ signup: "新規登録"
|
||||
uploading: "アップロード中"
|
||||
save: "保存"
|
||||
users: "ユーザー"
|
||||
approvals: "承認"
|
||||
addUser: "ユーザーを追加"
|
||||
favorite: "お気に入り"
|
||||
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="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="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>
|
||||
</div>
|
||||
@ -114,6 +114,11 @@ const menuDef = $computed(() => [{
|
||||
text: i18n.ts.invite,
|
||||
to: '/admin/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',
|
||||
text: i18n.ts.roles,
|
||||
|
@ -443,6 +443,10 @@ export const routes = [{
|
||||
path: '/invites',
|
||||
name: 'invites',
|
||||
component: page(() => import('./pages/admin/invites.vue')),
|
||||
}, {
|
||||
path: '/approvals',
|
||||
name: 'approvals',
|
||||
component: page(() => import('./pages/admin/approvals.vue')),
|
||||
}, {
|
||||
path: '/',
|
||||
component: page(() => import('./pages/_empty_.vue')),
|
||||
|
Loading…
Reference in New Issue
Block a user