feat(policies): account removal policy

This commit is contained in:
オスカー、 2024-05-11 21:55:40 +09:00
parent fdb49bc2e4
commit 0ec6a0fbfb
Signed by: SWREI
GPG Key ID: 139D6573F92DA9F7
9 changed files with 45 additions and 10 deletions

8
locales/index.d.ts vendored
View File

@ -6744,6 +6744,10 @@ export interface Locale extends ILocale {
* *
*/ */
"canDeleteContent": string; "canDeleteContent": string;
/**
*
*/
"canUseAccountRemoval": string;
/** /**
* *
*/ */
@ -7049,6 +7053,10 @@ export interface Locale extends ILocale {
* *
*/ */
"inProgress": string; "inProgress": string;
/**
*
*/
"youCantUseThisTime": string;
}; };
"_ad": { "_ad": {
/** /**

View File

@ -1741,6 +1741,7 @@ _role:
canCreateContent: "コンテンツの作成" canCreateContent: "コンテンツの作成"
canUpdateContent: "コンテンツの編集" canUpdateContent: "コンテンツの編集"
canDeleteContent: "コンテンツの削除" canDeleteContent: "コンテンツの削除"
canUseAccountRemoval: "アカウントの削除"
canPurgeAccount: "完全なアカウントの削除" canPurgeAccount: "完全なアカウントの削除"
canUpdateAvatar: "アイコンの変更" canUpdateAvatar: "アイコンの変更"
canUpdateBanner: "バナーの変更" canUpdateBanner: "バナーの変更"
@ -1825,6 +1826,7 @@ _accountDelete:
requestAccountDelete: "アカウント削除をリクエスト" requestAccountDelete: "アカウント削除をリクエスト"
started: "削除処理が開始されました。" started: "削除処理が開始されました。"
inProgress: "削除が進行中" inProgress: "削除が進行中"
youCantUseThisTime: "現在、アカウントの削除はできません。"
_ad: _ad:
back: "戻る" back: "戻る"

View File

@ -1719,6 +1719,7 @@ _role:
canCreateContent: "컨텐츠 생성 허용" canCreateContent: "컨텐츠 생성 허용"
canUpdateContent: "컨텐츠 수정 허용" canUpdateContent: "컨텐츠 수정 허용"
canDeleteContent: "컨텐츠 삭제 허용" canDeleteContent: "컨텐츠 삭제 허용"
canUseAccountRemoval: "계정 삭제 허용"
canPurgeAccount: "완전한 계정 삭제 허용" canPurgeAccount: "완전한 계정 삭제 허용"
canUpdateAvatar: "아바타 변경 허용" canUpdateAvatar: "아바타 변경 허용"
canUpdateBanner: "배너 변경 허용" canUpdateBanner: "배너 변경 허용"
@ -1790,6 +1791,7 @@ _accountDelete:
requestAccountDelete: "계정 삭제 요청" requestAccountDelete: "계정 삭제 요청"
started: "삭제 작업이 시작되었습니다." started: "삭제 작업이 시작되었습니다."
inProgress: "삭제 진행 중" inProgress: "삭제 진행 중"
youCantUseThisTime: "지금은 계정 삭제를 진행할 수 없습니다."
_ad: _ad:
back: "뒤로" back: "뒤로"
reduceFrequencyOfThisAd: "이 광고의 표시 빈도 낮추기" reduceFrequencyOfThisAd: "이 광고의 표시 빈도 낮추기"

View File

@ -68,6 +68,7 @@ export type RolePolicies = {
userEachUserListsLimit: number; userEachUserListsLimit: number;
rateLimitFactor: number; rateLimitFactor: number;
avatarDecorationLimit: number; avatarDecorationLimit: number;
canUseAccountRemoval: boolean;
}; };
export const DEFAULT_POLICIES: RolePolicies = { export const DEFAULT_POLICIES: RolePolicies = {
@ -106,6 +107,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
userEachUserListsLimit: 50, userEachUserListsLimit: 50,
rateLimitFactor: 1, rateLimitFactor: 1,
avatarDecorationLimit: 1, avatarDecorationLimit: 1,
canUseAccountRemoval: true,
}; };
@Injectable() @Injectable()
@ -389,6 +391,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)), canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)),
canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)), canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)),
canDeleteContent: calc('canDeleteContent', vs => vs.some(v => v === true)), canDeleteContent: calc('canDeleteContent', vs => vs.some(v => v === true)),
canUseAccountRemoval: calc('canUseAccountRemoval', vs => vs.some(v => v === true)),
canPurgeAccount: calc('canPurgeAccount', vs => vs.some(v => v === true)), canPurgeAccount: calc('canPurgeAccount', vs => vs.some(v => v === true)),
canUpdateAvatar: calc('canUpdateAvatar', vs => vs.some(v => v === true)), canUpdateAvatar: calc('canUpdateAvatar', vs => vs.some(v => v === true)),
canUpdateBanner: calc('canUpdateBanner', vs => vs.some(v => v === true)), canUpdateBanner: calc('canUpdateBanner', vs => vs.some(v => v === true)),

View File

@ -308,6 +308,10 @@ export const packedRolePoliciesSchema = {
type: 'integer', type: 'integer',
optional: false, nullable: false, optional: false, nullable: false,
}, },
canUseAccountRemoval: {
type: 'boolean',
optional: false, nullable: false,
},
}, },
} as const; } as const;

View File

@ -8,6 +8,7 @@ import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { DeleteAccountService } from '@/core/DeleteAccountService.js'; import { DeleteAccountService } from '@/core/DeleteAccountService.js';
import { RoleService } from '@/core/RoleService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UserAuthService } from '@/core/UserAuthService.js'; import { UserAuthService } from '@/core/UserAuthService.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
@ -18,17 +19,21 @@ export const meta = {
secure: true, secure: true,
errors: { errors: {
incorrectPassword: { removalDisabled: {
message: 'Incorrect password.', message: 'Account removal is disabled by your role.',
code: 'INCORRECT_PASSWORD', code: 'REMOVAL_DISABLED',
id: '44326b04-08ea-4525-b01c-98cc117bdd2a', id: '453d954b-3d8b-4df0-a261-b26ec6660ea3',
}, },
authenticationFailed: { authenticationFailed: {
message: 'Authentication failed.', message: 'Your password or 2FA token is invalid.',
code: 'AUTHENTICATION_FAILED', code: 'AUTHENTICATION_FAILED',
id: 'ea791cff-63e7-4b2a-92fc-646ab641794e', id: 'ea791cff-63e7-4b2a-92fc-646ab641794e',
}, },
alreadyRemoved: {
message: 'Your account is already removed.',
code: 'ACCOUNT_REMOVED',
id: '59b8f0e6-6eb2-4dc1-a080-1de3108416d0',
},
}, },
} as const; } as const;
@ -52,18 +57,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userAuthService: UserAuthService, private userAuthService: UserAuthService,
private deleteAccountService: DeleteAccountService, private deleteAccountService: DeleteAccountService,
private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
const policies = await this.roleService.getUserPolicies(me.id);
if (!policies.canUseAccountRemoval) {
throw new ApiError(meta.errors.removalDisabled);
}
const userDetailed = await this.usersRepository.findOneByOrFail({ id: me.id }); const userDetailed = await this.usersRepository.findOneByOrFail({ id: me.id });
if (userDetailed.isDeleted) { if (userDetailed.isDeleted) {
return; throw new ApiError(meta.errors.alreadyRemoved);
} }
const passwordMatched = await bcrypt.compare(ps.password, profile.password!); const passwordMatched = await bcrypt.compare(ps.password, profile.password!);
if (!passwordMatched) { if (!passwordMatched) {
throw new ApiError(meta.errors.incorrectPassword); throw new ApiError(meta.errors.authenticationFailed);
} }
if (profile.twoFactorEnabled) { if (profile.twoFactorEnabled) {

View File

@ -107,6 +107,7 @@ export const ROLE_POLICIES = [
'userEachUserListsLimit', 'userEachUserListsLimit',
'rateLimitFactor', 'rateLimitFactor',
'avatarDecorationLimit', 'avatarDecorationLimit',
'canUseAccountRemoval',
] as const; ] as const;
// なんか動かない // なんか動かない

View File

@ -40,12 +40,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-alert-triangle"></i></template> <template #icon><i class="ti ti-alert-triangle"></i></template>
<template #label>{{ i18n.ts.closeAccount }}</template> <template #label>{{ i18n.ts.closeAccount }}</template>
<div class="_gaps_m"> <div v-if="$i.policies.canUseAccountRemoval" class="_gaps_m">
<FormInfo warn>{{ i18n.ts._accountDelete.mayTakeTime }}</FormInfo> <FormInfo warn>{{ i18n.ts._accountDelete.mayTakeTime }}</FormInfo>
<FormInfo>{{ i18n.ts._accountDelete.sendEmail }}</FormInfo> <FormInfo>{{ i18n.ts._accountDelete.sendEmail }}</FormInfo>
<MkButton v-if="!$i.isDeleted" danger @click="deleteAccount">{{ i18n.ts._accountDelete.requestAccountDelete }}</MkButton> <MkButton v-if="!$i.isDeleted" danger @click="deleteAccount">{{ i18n.ts._accountDelete.requestAccountDelete }}</MkButton>
<MkButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</MkButton> <MkButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</MkButton>
</div> </div>
<div v-else class="_gaps_m">
<FormInfo warn>{{ i18n.ts._accountDelete.youCantUseThisTime }}</FormInfo>
</div>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
@ -105,7 +108,7 @@ import FormSection from '@/components/form/section.vue';
const $i = signinRequired(); const $i = signinRequired();
const reportError = computed(defaultStore.makeGetterSetter('reportError')); // const reportError = computed(defaultStore.makeGetterSetter('reportError'));
const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct')); const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct'));
const devMode = computed(defaultStore.makeGetterSetter('devMode')); const devMode = computed(defaultStore.makeGetterSetter('devMode'));
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies')); const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));

View File

@ -4925,6 +4925,7 @@ export type components = {
userEachUserListsLimit: number; userEachUserListsLimit: number;
rateLimitFactor: number; rateLimitFactor: number;
avatarDecorationLimit: number; avatarDecorationLimit: number;
canUseAccountRemoval: boolean;
}; };
ReversiGameLite: { ReversiGameLite: {
/** Format: id */ /** Format: id */