enhance: コンテンツ削除を制限されていてもアカウントの閉鎖ができるように (MisskeyIO#532)
This commit is contained in:
parent
2564fc7346
commit
075ec2d7df
33 changed files with 390 additions and 403 deletions
|
|
@ -4,38 +4,47 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { UserSuspendService } from '@/core/UserSuspendService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteAccountService {
|
||||
public logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private userSuspendService: UserSuspendService,
|
||||
private roleService: RoleService,
|
||||
private queueService: QueueService,
|
||||
private userSuspendService: UserSuspendService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('delete-account');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async deleteAccount(user: {
|
||||
id: string;
|
||||
host: string | null;
|
||||
}): Promise<void> {
|
||||
public async deleteAccount(user: MiUser, soft: boolean, me: MiUser | null): Promise<void> {
|
||||
this.logger.warn(`Delete account requested by ${me ? me.id : 'remote'} for ${user.id} (soft: ${soft})`);
|
||||
|
||||
const _user = await this.usersRepository.findOneByOrFail({ id: user.id });
|
||||
if (_user.isRoot) throw new Error('cannot delete a root account');
|
||||
|
||||
// 物理削除する前にDelete activityを送信する
|
||||
await this.userSuspendService.doPostSuspend(user).catch(e => {});
|
||||
await this.userSuspendService.doPostSuspend(user).catch(err => this.logger.error(err));
|
||||
|
||||
this.queueService.createDeleteAccountJob(user, {
|
||||
soft: false,
|
||||
force: me ? await this.roleService.isModerator(me) : false,
|
||||
soft: soft,
|
||||
});
|
||||
|
||||
await this.usersRepository.update(user.id, {
|
||||
|
|
@ -44,4 +53,16 @@ export class DeleteAccountService {
|
|||
|
||||
this.globalEventService.publishInternalEvent('userChangeDeletedState', { id: user.id, isDeleted: true });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async deleteAllDriveFiles(user: MiUser, me: MiUser | null): Promise<void> {
|
||||
this.logger.warn(`Delete all drive files requested by ${me ? me.id : 'remote'} for ${user.id}`);
|
||||
|
||||
await this.usersRepository.findOneByOrFail({ id: user.id });
|
||||
|
||||
this.queueService.createDeleteAccountJob(user, {
|
||||
force: me ? await this.roleService.isModerator(me) : false,
|
||||
onlyFiles: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -352,10 +352,12 @@ export class QueueService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) {
|
||||
public createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean, force?: boolean, onlyFiles?: boolean } = {}) {
|
||||
return this.dbQueue.add('deleteAccount', {
|
||||
user: { id: user.id },
|
||||
soft: opts.soft,
|
||||
force: opts.force,
|
||||
onlyFiles: opts.onlyFiles,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export type RolePolicies = {
|
|||
canCreateContent: boolean;
|
||||
canUpdateContent: boolean;
|
||||
canDeleteContent: boolean;
|
||||
canPurgeAccount: boolean;
|
||||
canUpdateAvatar: boolean;
|
||||
canUpdateBanner: boolean;
|
||||
mentionLimit: number;
|
||||
|
|
@ -77,6 +78,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||
canCreateContent: true,
|
||||
canUpdateContent: true,
|
||||
canDeleteContent: true,
|
||||
canPurgeAccount: true,
|
||||
canUpdateAvatar: true,
|
||||
canUpdateBanner: true,
|
||||
mentionLimit: 20,
|
||||
|
|
@ -353,6 +355,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)),
|
||||
canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)),
|
||||
canDeleteContent: calc('canDeleteContent', vs => vs.some(v => v === true)),
|
||||
canPurgeAccount: calc('canPurgeAccount', vs => vs.some(v => v === true)),
|
||||
canUpdateAvatar: calc('canUpdateAvatar', vs => vs.some(v => v === true)),
|
||||
canUpdateBanner: calc('canUpdateBanner', vs => vs.some(v => v === true)),
|
||||
mentionLimit: calc('mentionLimit', vs => Math.max(...vs)),
|
||||
|
|
|
|||
|
|
@ -487,7 +487,8 @@ export class ApInboxService {
|
|||
return 'skip: already deleted';
|
||||
}
|
||||
|
||||
const job = await this.queueService.createDeleteAccountJob(actor);
|
||||
// リモートから消されたということなので、物理削除する
|
||||
const job = await this.queueService.createDeleteAccountJob(actor, { force: true, soft: false });
|
||||
|
||||
await this.usersRepository.update(actor.id, {
|
||||
isDeleted: true,
|
||||
|
|
|
|||
|
|
@ -179,6 +179,10 @@ export const packedRolePoliciesSchema = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
canPurgeAccount: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
canUpdateAvatar: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
|
|
|||
|
|
@ -4,14 +4,15 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { EmailService } from '@/core/EmailService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import type { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { EmailService } from '@/core/EmailService.js';
|
||||
import { SearchService } from '@/core/SearchService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbUserDeleteJobData } from '../types.js';
|
||||
|
|
@ -35,8 +36,9 @@ export class DeleteAccountProcessorService {
|
|||
|
||||
private driveService: DriveService,
|
||||
private emailService: EmailService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private searchService: SearchService,
|
||||
private roleService: RoleService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('delete-account');
|
||||
}
|
||||
|
|
@ -87,19 +89,32 @@ export class DeleteAccountProcessorService {
|
|||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbUserDeleteJobData>): Promise<string | void> {
|
||||
this.logger.info(`Deleting account of ${job.data.user.id} ...`);
|
||||
this.logger.info(`Deleting account of ${job.data.user.id} ...`, { userDeleteJobData: job.data });
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
this.deleteNotes(user),
|
||||
this.deleteFiles(user),
|
||||
]);
|
||||
const { canDeleteContent, canPurgeAccount } = !job.data.force
|
||||
? await this.roleService.getUserPolicies(user.id)
|
||||
: { canDeleteContent: true, canPurgeAccount: true };
|
||||
|
||||
{ // Send email notification
|
||||
if (job.data.onlyFiles) {
|
||||
if (!canDeleteContent) return 'Permission denied';
|
||||
|
||||
await this.deleteFiles(user);
|
||||
return 'Files deleted';
|
||||
}
|
||||
|
||||
if (canDeleteContent) {
|
||||
await Promise.all([
|
||||
this.deleteNotes(user),
|
||||
this.deleteFiles(user),
|
||||
]);
|
||||
}
|
||||
|
||||
if (user.token) { // Send email notification
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||
if (profile.email && profile.emailVerified) {
|
||||
await this.emailService.sendEmail(profile.email, 'Account deleted',
|
||||
|
|
@ -108,9 +123,13 @@ export class DeleteAccountProcessorService {
|
|||
}
|
||||
}
|
||||
|
||||
// soft指定されている場合は物理削除しない
|
||||
if (job.data.soft) {
|
||||
// nop
|
||||
// 制限されている もしくは soft指定されている場合は物理削除しない、代わりに凍結+削除フラグを立てる
|
||||
if (!(canDeleteContent && canPurgeAccount) || job.data.soft) {
|
||||
await this.usersRepository.update(user.id, {
|
||||
token: null,
|
||||
isSuspended: true,
|
||||
isDeleted: true,
|
||||
});
|
||||
} else {
|
||||
await this.usersRepository.delete(job.data.user.id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ export type DBExportAntennasData = {
|
|||
export type DbUserDeleteJobData = {
|
||||
user: ThinUser;
|
||||
soft?: boolean;
|
||||
force?: boolean;
|
||||
onlyFiles?: boolean;
|
||||
};
|
||||
|
||||
export type DbUserImportJobData = {
|
||||
|
|
|
|||
|
|
@ -27,11 +27,11 @@ import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-d
|
|||
import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js';
|
||||
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
|
||||
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
|
||||
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
|
||||
import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
|
||||
import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
|
||||
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
|
||||
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
|
||||
import * as ep___admin_drive_deleteAllFilesOfAUser from './endpoints/admin/drive/delete-all-files-of-a-user.js';
|
||||
import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
|
||||
import * as ep___admin_drive_showFile from './endpoints/admin/drive/show-file.js';
|
||||
import * as ep___admin_emoji_addAliasesBulk from './endpoints/admin/emoji/add-aliases-bulk.js';
|
||||
|
|
@ -79,7 +79,6 @@ import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
|
|||
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
|
||||
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
|
||||
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
|
||||
import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
|
||||
import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js';
|
||||
import * as ep___admin_roles_create from './endpoints/admin/roles/create.js';
|
||||
import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js';
|
||||
|
|
@ -413,11 +412,11 @@ const $admin_avatarDecorations_create: Provider = { provide: 'ep:admin/avatar-de
|
|||
const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-decorations/delete', useClass: ep___admin_avatarDecorations_delete.default };
|
||||
const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default };
|
||||
const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default };
|
||||
const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
|
||||
const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default };
|
||||
const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default };
|
||||
const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
|
||||
const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
|
||||
const $admin_drive_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/drive/delete-all-files-of-a-user', useClass: ep___admin_drive_deleteAllFilesOfAUser.default };
|
||||
const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default };
|
||||
const $admin_drive_showFile: Provider = { provide: 'ep:admin/drive/show-file', useClass: ep___admin_drive_showFile.default };
|
||||
const $admin_emoji_addAliasesBulk: Provider = { provide: 'ep:admin/emoji/add-aliases-bulk', useClass: ep___admin_emoji_addAliasesBulk.default };
|
||||
|
|
@ -465,7 +464,6 @@ const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: e
|
|||
const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default };
|
||||
const $admin_unsuspendUser: Provider = { provide: 'ep:admin/unsuspend-user', useClass: ep___admin_unsuspendUser.default };
|
||||
const $admin_updateMeta: Provider = { provide: 'ep:admin/update-meta', useClass: ep___admin_updateMeta.default };
|
||||
const $admin_deleteAccount: Provider = { provide: 'ep:admin/delete-account', useClass: ep___admin_deleteAccount.default };
|
||||
const $admin_updateUserNote: Provider = { provide: 'ep:admin/update-user-note', useClass: ep___admin_updateUserNote.default };
|
||||
const $admin_roles_create: Provider = { provide: 'ep:admin/roles/create', useClass: ep___admin_roles_create.default };
|
||||
const $admin_roles_delete: Provider = { provide: 'ep:admin/roles/delete', useClass: ep___admin_roles_delete.default };
|
||||
|
|
@ -803,11 +801,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$admin_avatarDecorations_delete,
|
||||
$admin_avatarDecorations_list,
|
||||
$admin_avatarDecorations_update,
|
||||
$admin_deleteAllFilesOfAUser,
|
||||
$admin_unsetUserAvatar,
|
||||
$admin_unsetUserBanner,
|
||||
$admin_drive_cleanRemoteFiles,
|
||||
$admin_drive_cleanup,
|
||||
$admin_drive_deleteAllFilesOfAUser,
|
||||
$admin_drive_files,
|
||||
$admin_drive_showFile,
|
||||
$admin_emoji_addAliasesBulk,
|
||||
|
|
@ -855,7 +853,6 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$admin_suspendUser,
|
||||
$admin_unsuspendUser,
|
||||
$admin_updateMeta,
|
||||
$admin_deleteAccount,
|
||||
$admin_updateUserNote,
|
||||
$admin_roles_create,
|
||||
$admin_roles_delete,
|
||||
|
|
@ -1187,11 +1184,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$admin_avatarDecorations_delete,
|
||||
$admin_avatarDecorations_list,
|
||||
$admin_avatarDecorations_update,
|
||||
$admin_deleteAllFilesOfAUser,
|
||||
$admin_unsetUserAvatar,
|
||||
$admin_unsetUserBanner,
|
||||
$admin_drive_cleanRemoteFiles,
|
||||
$admin_drive_cleanup,
|
||||
$admin_drive_deleteAllFilesOfAUser,
|
||||
$admin_drive_files,
|
||||
$admin_drive_showFile,
|
||||
$admin_emoji_addAliasesBulk,
|
||||
|
|
@ -1239,7 +1236,6 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$admin_suspendUser,
|
||||
$admin_unsuspendUser,
|
||||
$admin_updateMeta,
|
||||
$admin_deleteAccount,
|
||||
$admin_updateUserNote,
|
||||
$admin_roles_create,
|
||||
$admin_roles_delete,
|
||||
|
|
|
|||
|
|
@ -124,6 +124,13 @@ export class SigninApiService {
|
|||
});
|
||||
}
|
||||
|
||||
if (user.isDeleted && user.isSuspended) {
|
||||
logger.error('No such user. (logical deletion)');
|
||||
return error(404, {
|
||||
id: '6cc579cc-885d-43d8-95c2-b8c7fc963280',
|
||||
});
|
||||
}
|
||||
|
||||
if (user.isSuspended) {
|
||||
logger.error('User is suspended.');
|
||||
return error(403, {
|
||||
|
|
|
|||
|
|
@ -27,11 +27,11 @@ import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-d
|
|||
import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js';
|
||||
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
|
||||
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
|
||||
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
|
||||
import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
|
||||
import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
|
||||
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
|
||||
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
|
||||
import * as ep___admin_drive_deleteAllFilesOfAUser from './endpoints/admin/drive/delete-all-files-of-a-user.js';
|
||||
import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
|
||||
import * as ep___admin_drive_showFile from './endpoints/admin/drive/show-file.js';
|
||||
import * as ep___admin_emoji_addAliasesBulk from './endpoints/admin/emoji/add-aliases-bulk.js';
|
||||
|
|
@ -79,7 +79,6 @@ import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
|
|||
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
|
||||
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
|
||||
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
|
||||
import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
|
||||
import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js';
|
||||
import * as ep___admin_roles_create from './endpoints/admin/roles/create.js';
|
||||
import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js';
|
||||
|
|
@ -411,11 +410,11 @@ const eps = [
|
|||
['admin/avatar-decorations/delete', ep___admin_avatarDecorations_delete],
|
||||
['admin/avatar-decorations/list', ep___admin_avatarDecorations_list],
|
||||
['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
|
||||
['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
|
||||
['admin/unset-user-avatar', ep___admin_unsetUserAvatar],
|
||||
['admin/unset-user-banner', ep___admin_unsetUserBanner],
|
||||
['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
|
||||
['admin/drive/cleanup', ep___admin_drive_cleanup],
|
||||
['admin/drive/delete-all-files-of-a-user', ep___admin_drive_deleteAllFilesOfAUser],
|
||||
['admin/drive/files', ep___admin_drive_files],
|
||||
['admin/drive/show-file', ep___admin_drive_showFile],
|
||||
['admin/emoji/add-aliases-bulk', ep___admin_emoji_addAliasesBulk],
|
||||
|
|
@ -463,7 +462,6 @@ const eps = [
|
|||
['admin/suspend-user', ep___admin_suspendUser],
|
||||
['admin/unsuspend-user', ep___admin_unsuspendUser],
|
||||
['admin/update-meta', ep___admin_updateMeta],
|
||||
['admin/delete-account', ep___admin_deleteAccount],
|
||||
['admin/update-user-note', ep___admin_updateUserNote],
|
||||
['admin/roles/create', ep___admin_roles_create],
|
||||
['admin/roles/delete', ep___admin_roles_delete],
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { UserSuspendService } from '@/core/UserSuspendService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { DeleteAccountService } from '@/core/DeleteAccountService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
|
@ -17,6 +17,20 @@ export const meta = {
|
|||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
kind: 'write:admin:account',
|
||||
|
||||
errors: {
|
||||
userNotFound: {
|
||||
message: 'User not found.',
|
||||
code: 'USER_NOT_FOUND',
|
||||
id: '6c45276a-525e-46b0-892f-17a5036258bf',
|
||||
},
|
||||
|
||||
cannotDeleteModerator: {
|
||||
message: 'Cannot delete a moderator.',
|
||||
code: 'CANNOT_DELETE_MODERATOR',
|
||||
id: 'd195c621-f21a-4c2f-a634-484c2a616311',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
|
@ -33,37 +47,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private queueService: QueueService,
|
||||
private userSuspendService: UserSuspendService,
|
||||
private roleService: RoleService,
|
||||
private deleteAccountService: DeleteAccountService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||
|
||||
if (user == null) {
|
||||
throw new Error('user not found');
|
||||
}
|
||||
if (user == null) throw new ApiError(meta.errors.userNotFound);
|
||||
if (await this.roleService.isModerator(user)) throw new ApiError(meta.errors.cannotDeleteModerator);
|
||||
|
||||
if (user.isRoot) {
|
||||
throw new Error('cannot delete a root account');
|
||||
}
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
// 物理削除する前にDelete activityを送信する
|
||||
await this.userSuspendService.doPostSuspend(user).catch(err => {});
|
||||
|
||||
this.queueService.createDeleteAccountJob(user, {
|
||||
soft: false,
|
||||
});
|
||||
} else {
|
||||
this.queueService.createDeleteAccountJob(user, {
|
||||
soft: true, // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する
|
||||
});
|
||||
}
|
||||
|
||||
await this.usersRepository.update(user.id, {
|
||||
isDeleted: true,
|
||||
});
|
||||
// 管理者からの削除ということはモデレーション行為なので、soft delete にする
|
||||
await this.deleteAccountService.deleteAccount(user, true, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { DriveFilesRepository } from '@/models/_.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
kind: 'write:admin:delete-all-files-of-a-user',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private driveService: DriveService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const files = await this.driveFilesRepository.findBy({
|
||||
userId: ps.userId,
|
||||
});
|
||||
|
||||
for (const file of files) {
|
||||
this.driveService.deleteFile(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -4,17 +4,17 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DeleteAccountService } from '@/core/DeleteAccountService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import { DeleteAccountService } from '@/core/DeleteAccountService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
kind: 'write:admin:delete-account',
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:drive',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
|
@ -33,13 +33,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
private deleteAccountService: DeleteAccountService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps) => {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: ps.userId });
|
||||
if (user.isDeleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.deleteAccountService.deleteAccount(user);
|
||||
await this.deleteAccountService.deleteAllDriveFiles(user, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -115,6 +115,14 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isLimited: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isDeleted: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isSuspended: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
|
@ -242,6 +250,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
isModerator: isModerator,
|
||||
isSilenced: isSilenced,
|
||||
isLimited: isLimited,
|
||||
isDeleted: user.isDeleted,
|
||||
isSuspended: user.isSuspended,
|
||||
isHibernated: user.isHibernated,
|
||||
lastActiveDate: user.lastActiveDate ? user.lastActiveDate.toISOString() : null,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import { ApiError } from '@/server/api/error.js';
|
|||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
requireRolePolicy: 'canDeleteContent',
|
||||
|
||||
secure: true,
|
||||
|
||||
|
|
@ -76,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
await this.userAuthService.twoFactorAuthenticate(profile, token);
|
||||
}
|
||||
|
||||
await this.deleteAccountService.deleteAccount(me);
|
||||
await this.deleteAccountService.deleteAccount(me, false, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ describe('アンテナ', () => {
|
|||
await api('i/delete-account', { password: 'userDeletedBySelf' }, userDeletedBySelf);
|
||||
userDeletedByAdmin = await signup({ username: 'userDeletedByAdmin' });
|
||||
await post(userDeletedByAdmin, { text: 'test' });
|
||||
await api('admin/delete-account', { userId: userDeletedByAdmin.id }, root);
|
||||
await api('admin/accounts/delete', { userId: userDeletedByAdmin.id }, root);
|
||||
userFollowedByAlice = await signup({ username: 'userFollowedByAlice' });
|
||||
await post(userFollowedByAlice, { text: 'test' });
|
||||
await api('following/create', { userId: userFollowedByAlice.id }, alice);
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ describe('ユーザー', () => {
|
|||
await api('i/delete-account', { password: 'userDeletedBySelf' }, userDeletedBySelf);
|
||||
userDeletedByAdmin = await signup({ username: 'userDeletedByAdmin' });
|
||||
await post(userDeletedByAdmin, { text: 'test' });
|
||||
await api('admin/delete-account', { userId: userDeletedByAdmin.id }, root);
|
||||
await api('admin/accounts/delete', { userId: userDeletedByAdmin.id }, root);
|
||||
userFollowingAlice = await signup({ username: 'userFollowingAlice' });
|
||||
await post(userFollowingAlice, { text: 'test' });
|
||||
await api('following/create', { userId: alice.id }, userFollowingAlice);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue