feat(policies): account removal policy
This commit is contained in:
parent
fdb49bc2e4
commit
0ec6a0fbfb
8
locales/index.d.ts
vendored
8
locales/index.d.ts
vendored
@ -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": {
|
||||||
/**
|
/**
|
||||||
|
@ -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: "戻る"
|
||||||
|
@ -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: "이 광고의 표시 빈도 낮추기"
|
||||||
|
@ -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)),
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -107,6 +107,7 @@ export const ROLE_POLICIES = [
|
|||||||
'userEachUserListsLimit',
|
'userEachUserListsLimit',
|
||||||
'rateLimitFactor',
|
'rateLimitFactor',
|
||||||
'avatarDecorationLimit',
|
'avatarDecorationLimit',
|
||||||
|
'canUseAccountRemoval',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// なんか動かない
|
// なんか動かない
|
||||||
|
@ -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'));
|
||||||
|
@ -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 */
|
||||||
|
Loading…
Reference in New Issue
Block a user