enhance: コントロールパネルから二要素認証を解除できるように

This commit is contained in:
kakkokari-gtyih 2026-06-24 18:04:14 +09:00
commit ce26f15f66
14 changed files with 228 additions and 2 deletions

View file

@ -619,6 +619,8 @@ output: "出力"
script: "スクリプト"
disablePagesScript: "Pagesのスクリプトを無効にする"
updateRemoteUser: "リモートユーザー情報の更新"
unsetMfa: "二要素認証を解除"
unsetMfaConfirm: "二要素認証を解除しますか?"
unsetUserAvatar: "アイコンを解除"
unsetUserAvatarConfirm: "アイコンを解除しますか?"
unsetUserBanner: "バナーを解除"
@ -2517,6 +2519,7 @@ _permissions:
"read:admin:show-moderation-log": "モデレーションログを見る"
"read:admin:show-user": "ユーザーのプライベートな情報を見る"
"write:admin:suspend-user": "ユーザーを凍結する"
"write:admin:unset-mfa": "ユーザーの二要素認証を解除する"
"write:admin:unset-user-avatar": "ユーザーのアバターを削除する"
"write:admin:unset-user-banner": "ユーザーのバーナーを削除する"
"write:admin:unsuspend-user": "ユーザーの凍結を解除する"
@ -3062,6 +3065,7 @@ _moderationLogTypes:
createAvatarDecoration: "アイコンデコレーションを作成"
updateAvatarDecoration: "アイコンデコレーションを更新"
deleteAvatarDecoration: "アイコンデコレーションを削除"
unsetMfa: "ユーザーの二要素認証を解除"
unsetUserAvatar: "ユーザーのアイコンを解除"
unsetUserBanner: "ユーザーのバナーを解除"
createSystemWebhook: "SystemWebhookを作成"

View file

@ -104,6 +104,7 @@ export * as 'admin/system-webhook/list' from './endpoints/admin/system-webhook/l
export * as 'admin/system-webhook/show' from './endpoints/admin/system-webhook/show.js';
export * as 'admin/system-webhook/test' from './endpoints/admin/system-webhook/test.js';
export * as 'admin/system-webhook/update' from './endpoints/admin/system-webhook/update.js';
export * as 'admin/unset-mfa' from './endpoints/admin/unset-mfa.js';
export * as 'admin/unset-user-avatar' from './endpoints/admin/unset-user-avatar.js';
export * as 'admin/unset-user-banner' from './endpoints/admin/unset-user-banner.js';
export * as 'admin/unsuspend-user' from './endpoints/admin/unsuspend-user.js';

View file

@ -0,0 +1,78 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ApiError } from '@/server/api/error.js';
import { MiUserProfile } from '@/models/UserProfile.js';
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
import type { UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'write:admin:unset-mfa',
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: 'ccafc7fe-5074-4edd-9dc0-8ef9ef6a701d',
},
},
} 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.db)
private db: DataSource,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId });
if (user == null) {
throw new ApiError(meta.errors.noSuchUser);
}
await this.db.transaction(async (transactionalEntityManager) => {
// パスキーを全て削除
await transactionalEntityManager.delete(MiUserSecurityKey, { userId: user.id });
// TOTP・パスワードレスログインを無効化
await transactionalEntityManager.update(MiUserProfile, { userId: user.id }, {
twoFactorSecret: null,
twoFactorBackupSecret: null,
twoFactorEnabled: false,
usePasswordLessLogin: false,
});
}).then(() => {
this.moderationLogService.log(me, 'unsetMfa', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
});
});
});
}
}

View file

@ -120,6 +120,7 @@ export const moderationLogTypes = [
'createAvatarDecoration',
'updateAvatarDecoration',
'deleteAvatarDecoration',
'unsetMfa',
'unsetUserAvatar',
'unsetUserBanner',
'createSystemWebhook',
@ -327,6 +328,11 @@ export type ModerationLogPayloads = {
avatarDecorationId: string;
avatarDecoration: any;
};
unsetMfa: {
userId: string;
userUsername: string;
userHost: string | null;
};
unsetUserAvatar: {
userId: string;
userUsername: string;

View file

@ -98,6 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<MkButton v-if="user.host == null" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
<MkButton v-if="user.host == null" inline @click="unsetMfa"><i class="ti ti-shield"></i> {{ i18n.ts.unsetMfa }}</MkButton>
</div>
<MkFolder>
@ -344,6 +345,20 @@ async function resetPassword() {
}
}
async function unsetMfa() {
const confirm = await os.confirm({
type: 'warning',
text: i18n.ts.unsetMfaConfirm,
});
if (confirm.canceled) {
return;
} else {
await os.apiWithDialog('admin/unset-mfa', {
userId: user.value.id,
});
}
}
async function toggleSuspend(v: boolean) {
const confirm = await os.confirm({
type: 'warning',

View file

@ -22,6 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.logYellow]: [
'markSensitiveDriveFile',
'resetPassword',
'unsetMfa',
'suspendRemoteInstance',
].includes(log.type),
[$style.logRed]: [
@ -68,6 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="log.type === 'deleteUserAnnouncement'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'deleteNote'">: @{{ log.info.noteUserUsername }}{{ log.info.noteUserHost ? '@' + log.info.noteUserHost : '' }}</span>
<span v-else-if="log.type === 'deleteDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
<span v-else-if="log.type === 'unsetMfa'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'createAvatarDecoration'">: {{ log.info.avatarDecoration.name }}</span>
<span v-else-if="log.type === 'updateAvatarDecoration'">: {{ log.info.before.name }}</span>
<span v-else-if="log.type === 'deleteAvatarDecoration'">: {{ log.info.avatarDecoration.name }}</span>
@ -112,6 +114,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="log.type === 'createAd'" class="ti ti-plus"></i>
<i v-else-if="log.type === 'updateAd'" class="ti ti-pencil"></i>
<i v-else-if="log.type === 'deleteAd'" class="ti ti-trash"></i>
<i v-else-if="log.type === 'unsetMfa'" class="ti ti-shield"></i>
<i v-else-if="log.type === 'createAvatarDecoration'" class="ti ti-plus"></i>
<i v-else-if="log.type === 'updateAvatarDecoration'" class="ti ti-pencil"></i>
<i v-else-if="log.type === 'deleteAvatarDecoration'" class="ti ti-trash"></i>

View file

@ -2488,6 +2488,14 @@ export interface Locale extends ILocale {
*
*/
"updateRemoteUser": string;
/**
*
*/
"unsetMfa": string;
/**
*
*/
"unsetMfaConfirm": string;
/**
*
*/
@ -9578,6 +9586,10 @@ export interface Locale extends ILocale {
*
*/
"write:admin:suspend-user": string;
/**
*
*/
"write:admin:unset-mfa": string;
/**
*
*/
@ -11548,6 +11560,10 @@ export interface Locale extends ILocale {
*
*/
"deleteAvatarDecoration": string;
/**
*
*/
"unsetMfa": string;
/**
*
*/

View file

@ -433,6 +433,9 @@ type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___upda
// @public (undocumented)
type AdminSystemWebhookUpdateResponse = operations['admin___system-webhook___update']['responses']['200']['content']['application/json'];
// @public (undocumented)
type AdminUnsetMfaRequest = operations['admin___unset-mfa']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminUnsetUserAvatarRequest = operations['admin___unset-user-avatar']['requestBody']['content']['application/json'];
@ -1664,6 +1667,7 @@ declare namespace entities {
AdminSystemWebhookTestRequest,
AdminSystemWebhookUpdateRequest,
AdminSystemWebhookUpdateResponse,
AdminUnsetMfaRequest,
AdminUnsetUserAvatarRequest,
AdminUnsetUserBannerRequest,
AdminUnsuspendUserRequest,
@ -3000,6 +3004,9 @@ type ModerationLog = {
} | {
type: 'updateAbuseReportNote';
info: ModerationLogPayloads['updateAbuseReportNote'];
} | {
type: 'unsetMfa';
info: ModerationLogPayloads['unsetMfa'];
} | {
type: 'unsetUserAvatar';
info: ModerationLogPayloads['unsetUserAvatar'];
@ -3045,7 +3052,7 @@ type ModerationLog = {
});
// @public (undocumented)
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost", "deleteChatRoom", "updateProxyAccountDescription"];
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetMfa", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost", "deleteChatRoom", "updateProxyAccountDescription"];
// @public (undocumented)
type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
@ -3349,7 +3356,7 @@ type PartialRolePolicyOverride = Partial<{
}>;
// @public (undocumented)
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse", "write:chat", "read:chat"];
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-mfa", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse", "write:chat", "read:chat"];
// @public (undocumented)
type PingResponse = operations['ping']['responses']['200']['content']['application/json'];

View file

@ -1049,6 +1049,17 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:unset-mfa*
*/
request<E extends 'admin/unset-mfa', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*

View file

@ -131,6 +131,7 @@ import type {
AdminSystemWebhookTestRequest,
AdminSystemWebhookUpdateRequest,
AdminSystemWebhookUpdateResponse,
AdminUnsetMfaRequest,
AdminUnsetUserAvatarRequest,
AdminUnsetUserBannerRequest,
AdminUnsuspendUserRequest,
@ -761,6 +762,7 @@ export type Endpoints = {
'admin/system-webhook/show': { req: AdminSystemWebhookShowRequest; res: AdminSystemWebhookShowResponse };
'admin/system-webhook/test': { req: AdminSystemWebhookTestRequest; res: EmptyResponse };
'admin/system-webhook/update': { req: AdminSystemWebhookUpdateRequest; res: AdminSystemWebhookUpdateResponse };
'admin/unset-mfa': { req: AdminUnsetMfaRequest; res: EmptyResponse };
'admin/unset-user-avatar': { req: AdminUnsetUserAvatarRequest; res: EmptyResponse };
'admin/unset-user-banner': { req: AdminUnsetUserBannerRequest; res: EmptyResponse };
'admin/unsuspend-user': { req: AdminUnsuspendUserRequest; res: EmptyResponse };

View file

@ -134,6 +134,7 @@ export type AdminSystemWebhookShowResponse = operations['admin___system-webhook_
export type AdminSystemWebhookTestRequest = operations['admin___system-webhook___test']['requestBody']['content']['application/json'];
export type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json'];
export type AdminSystemWebhookUpdateResponse = operations['admin___system-webhook___update']['responses']['200']['content']['application/json'];
export type AdminUnsetMfaRequest = operations['admin___unset-mfa']['requestBody']['content']['application/json'];
export type AdminUnsetUserAvatarRequest = operations['admin___unset-user-avatar']['requestBody']['content']['application/json'];
export type AdminUnsetUserBannerRequest = operations['admin___unset-user-banner']['requestBody']['content']['application/json'];
export type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json'];

View file

@ -860,6 +860,15 @@ export type paths = {
*/
post: operations['admin___system-webhook___update'];
};
'/admin/unset-mfa': {
/**
* admin/unset-mfa
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:unset-mfa*
*/
post: operations['admin___unset-mfa'];
};
'/admin/unset-user-avatar': {
/**
* admin/unset-user-avatar
@ -12617,6 +12626,69 @@ export interface operations {
};
};
};
'admin___unset-mfa': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
userId: string;
};
};
};
responses: {
/** @description OK (without any results) */
204: {
headers: {
[name: string]: unknown;
};
};
/** @description Client error */
400: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
'admin___unset-user-avatar': {
requestBody: {
content: {

View file

@ -98,6 +98,7 @@ export const permissions = [
'read:admin:show-moderation-log',
'read:admin:show-user',
'write:admin:suspend-user',
'write:admin:unset-mfa',
'write:admin:unset-user-avatar',
'write:admin:unset-user-banner',
'write:admin:unsuspend-user',
@ -174,6 +175,7 @@ export const moderationLogTypes = [
'createAvatarDecoration',
'updateAvatarDecoration',
'deleteAvatarDecoration',
'unsetMfa',
'unsetUserAvatar',
'unsetUserBanner',
'createSystemWebhook',
@ -462,6 +464,11 @@ export type ModerationLogPayloads = {
avatarDecorationId: string;
avatarDecoration: AvatarDecoration;
};
unsetMfa: {
userId: string;
userUsername: string;
userHost: string | null;
};
unsetUserAvatar: {
userId: string;
userUsername: string;

View file

@ -169,6 +169,9 @@ export type ModerationLog = {
} | {
type: 'updateAbuseReportNote';
info: ModerationLogPayloads['updateAbuseReportNote'];
} | {
type: 'unsetMfa';
info: ModerationLogPayloads['unsetMfa'];
} | {
type: 'unsetUserAvatar';
info: ModerationLogPayloads['unsetUserAvatar'];