diff --git a/locales/en-US.yml b/locales/en-US.yml index 4ec8aa8d5f..f591cbed48 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -471,6 +471,7 @@ unregister: "Unregister" passwordLessLogin: "Password-less login" passwordLessLoginDescription: "Allows password-less login using a security- or passkey only" resetPassword: "Reset password" +promptIdCheck: "Prompt for ID Verification" newPasswordIs: "The new password is \"{password}\"" reduceUiAnimation: "Reduce UI animations" share: "Share" @@ -1111,6 +1112,7 @@ nonSensitiveOnly: "Non-sensitive only" nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non-sensitive only (Only likes from remote)" rolesAssignedToMe: "Roles assigned to me" resetPasswordConfirm: "Really reset your password?" +promptIdCheckConfirm: "Are you sure you want to prompt id verification on this user?" sensitiveWords: "Sensitive words" sensitiveWordsDescription: "The visibility of all notes containing any of the configured words will be set to \"Home\" automatically. You can list multiple by separating them via line breaks." sensitiveWordsDescription2: "Using spaces will create AND expressions and surrounding keywords with slashes will turn them into a regular expression." diff --git a/locales/index.d.ts b/locales/index.d.ts index e71b7994f5..4bee227f0d 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1900,6 +1900,10 @@ export interface Locale extends ILocale { * パスワードをリセット */ "resetPassword": string; + /** + * 身分証の確認を求める + */ + "promptIdCheck": string; /** * 新しいパスワードは「{password}」です */ @@ -4461,6 +4465,10 @@ export interface Locale extends ILocale { * パスワードリセットしますか? */ "resetPasswordConfirm": string; + /** + * 本当にこのユーザーのID確認を促しますか? + */ + "promptIdCheckConfirm": string; /** * センシティブワード */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index a747b405ee..668d94c531 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -471,6 +471,7 @@ unregister: "登録を解除" passwordLessLogin: "パスワードレスログイン" passwordLessLoginDescription: "パスワードを使用せず、セキュリティキーやパスキーなどのみでログインします" resetPassword: "パスワードをリセット" +promptIdCheck: "身分証の確認を求める" newPasswordIs: "新しいパスワードは「{password}」です" reduceUiAnimation: "UIのアニメーションを減らす" share: "共有" @@ -1111,6 +1112,7 @@ nonSensitiveOnly: "非センシティブのみ" nonSensitiveOnlyForLocalLikeOnlyForRemote: "非センシティブのみ (リモートはいいねのみ)" rolesAssignedToMe: "自分に割り当てられたロール" resetPasswordConfirm: "パスワードリセットしますか?" +promptIdCheckConfirm: "本当にこのユーザーのID確認を促しますか?" sensitiveWords: "センシティブワード" sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。" sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。" diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 78ac8a521f..d191ae63fa 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -605,6 +605,8 @@ export class UserEntityService implements OnModuleInit { }))), memo: memo, moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined, + idCheckRequired: user.idCheckRequired, + idVerified: user.idVerified, } : {}), ...(isDetailed && isMe ? { @@ -667,8 +669,6 @@ export class UserEntityService implements OnModuleInit { }, }) : [], - idCheckRequired: user.idCheckRequired, - idVerified: user.idVerified, } : {}), ...(relation ? { diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 22c68b1fb4..f27deee8a3 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -453,6 +453,14 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'boolean', nullable: false, optional: true, }, + idCheckRequired: { + type: 'boolean', + nullable: true, optional: true + }, + idVerified: { + type: 'boolean', + nullable: true, optional: true + }, //#endregion }, } as const; @@ -693,15 +701,7 @@ export const packedMeDetailedOnlySchema = { }, }, }, - }, - idCheckRequired: { - type: 'boolean', - nullable: true, optional: true - }, - idVerified: { - type: 'boolean', - nullable: true, optional: true - }, + } //#endregion }, } as const; diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index cb82b13abe..3e673a13f4 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -97,6 +97,7 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js'; import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js'; import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js'; +import * as ep___admin_prompt_id_check from './endpoints/admin/prompt-id-check.js'; import * as ep___announcements from './endpoints/announcements.js'; import * as ep___announcements_show from './endpoints/announcements/show.js'; import * as ep___antennas_create from './endpoints/antennas/create.js'; @@ -494,6 +495,7 @@ const $admin_systemWebhook_delete: Provider = { provide: 'ep:admin/system-webhoo const $admin_systemWebhook_list: Provider = { provide: 'ep:admin/system-webhook/list', useClass: ep___admin_systemWebhook_list.default }; const $admin_systemWebhook_show: Provider = { provide: 'ep:admin/system-webhook/show', useClass: ep___admin_systemWebhook_show.default }; const $admin_systemWebhook_update: Provider = { provide: 'ep:admin/system-webhook/update', useClass: ep___admin_systemWebhook_update.default }; +const $admin_prompt_id_check: Provider = { provide: 'ep:admin/prompt-id-check', useClass: ep___admin_prompt_id_check.default }; const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default }; const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default }; const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default }; @@ -895,6 +897,7 @@ const $stripe_createVerifySession: Provider = { provide: 'ep:stripe/create-verif $admin_systemWebhook_list, $admin_systemWebhook_show, $admin_systemWebhook_update, + $admin_prompt_id_check, $announcements, $announcements_show, $antennas_create, @@ -1290,6 +1293,7 @@ const $stripe_createVerifySession: Provider = { provide: 'ep:stripe/create-verif $admin_systemWebhook_list, $admin_systemWebhook_show, $admin_systemWebhook_update, + $admin_prompt_id_check, $announcements, $announcements_show, $antennas_create, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index fd6dd980e8..0daf317200 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -103,6 +103,7 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js'; import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js'; import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js'; +import * as ep___admin_prompt_id_check from './endpoints/admin/prompt-id-check.js'; import * as ep___announcements from './endpoints/announcements.js'; import * as ep___announcements_show from './endpoints/announcements/show.js'; import * as ep___antennas_create from './endpoints/antennas/create.js'; @@ -484,6 +485,7 @@ const eps = [ ['admin/update-meta', ep___admin_updateMeta], ['admin/delete-account', ep___admin_deleteAccount], ['admin/update-user-note', ep___admin_updateUserNote], + ['admin/prompt-id-check', ep___admin_prompt_id_check], ['admin/roles/create', ep___admin_roles_create], ['admin/roles/delete', ep___admin_roles_delete], ['admin/roles/list', ep___admin_roles_list], diff --git a/packages/backend/src/server/api/endpoints/admin/prompt-id-check.ts b/packages/backend/src/server/api/endpoints/admin/prompt-id-check.ts new file mode 100644 index 0000000000..324fd61ce4 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/prompt-id-check.ts @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: marie and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UsersRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'write:admin:prompt-id-check-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 { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private roleService: RoleService, + ) { + 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 (await this.roleService.isModerator(user)) { + throw new Error('cannot prompt on moderator account'); + } + + await this.usersRepository.update(user.id, { + idCheckRequired: true, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index a7ca7f9547..b3946e5088 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -172,6 +172,14 @@ export const meta = { }, }, }, + idVerified: { + type: 'boolean', + optional: false, nullable: false, + }, + idCheckRequired: { + type: 'boolean', + optional: false, nullable: false, + }, }, }, } as const; @@ -253,6 +261,8 @@ export default class extends Endpoint { // eslint- expiresAt: a.expiresAt ? a.expiresAt.toISOString() : null, roleId: a.roleId, })), + idVerified: user.idVerified, + idCheckRequired: user.idCheckRequired, }; }); } diff --git a/packages/backend/src/server/api/endpoints/stripe/create-verify-session.ts b/packages/backend/src/server/api/endpoints/stripe/create-verify-session.ts index 4aab372b78..d5f3d4449b 100644 --- a/packages/backend/src/server/api/endpoints/stripe/create-verify-session.ts +++ b/packages/backend/src/server/api/endpoints/stripe/create-verify-session.ts @@ -10,6 +10,7 @@ import type { UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import Stripe from 'stripe'; import type { Config } from '@/config.js'; +import ms from 'ms'; export const meta = { tags: ['account'], @@ -22,6 +23,11 @@ export const meta = { optional: false, nullable: false, }, + limit: { + duration: ms('1hour'), + max: 5, + }, + errors: { userIsDeleted: { message: 'User is deleted.', diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 187ec66b42..88a378a6be 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -84,6 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.resetPassword }} + {{ i18n.ts.promptIdCheck }}
@@ -304,6 +305,21 @@ async function resetPassword() { } } +async function promptIdCheck() { + const confirm = await os.confirm({ + type: 'warning', + text: i18n.ts.promptIdCheckConfirm, + }); + if (confirm.canceled) { + return; + } else { + await misskeyApi('admin/prompt-id-check', { + userId: user.value.id, + }); + await refreshUser(); + } +} + async function toggleNSFW(v) { const confirm = await os.confirm({ type: 'warning', diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 306ef25307..bcf430f02c 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -247,6 +247,9 @@ type AdminNsfwUserRequest = operations['admin___nsfw-user']['requestBody']['cont // @public (undocumented) type AdminPromoCreateRequest = operations['admin___promo___create']['requestBody']['content']['application/json']; +// @public (undocumented) +type AdminPromptIdCheckRequest = operations['admin___prompt-id-check']['requestBody']['content']['application/json']; + // @public (undocumented) type AdminQueueDeliverDelayedResponse = operations['admin___queue___deliver-delayed']['responses']['200']['content']['application/json']; @@ -1323,6 +1326,7 @@ declare namespace entities { AdminUpdateMetaRequest, AdminDeleteAccountRequest, AdminUpdateUserNoteRequest, + AdminPromptIdCheckRequest, AdminRolesCreateRequest, AdminRolesCreateResponse, AdminRolesDeleteRequest, @@ -2842,7 +2846,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:approve-user", "write:admin:nsfw-user", "write:admin:unnsfw-user", "write:admin:silence-user", "write:admin:unsilence-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"]; +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:approve-user", "write:admin:nsfw-user", "write:admin:unnsfw-user", "write:admin:silence-user", "write:admin:unsilence-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:admin:prompt-id-check-user", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"]; // @public (undocumented) type PingResponse = operations['ping']['responses']['200']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index e44f9ca4e0..a29a9aa37c 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -856,6 +856,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:prompt-id-check-user* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 1dca5bcf75..fca6a701f1 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -101,6 +101,7 @@ import type { AdminUpdateMetaRequest, AdminDeleteAccountRequest, AdminUpdateUserNoteRequest, + AdminPromptIdCheckRequest, AdminRolesCreateRequest, AdminRolesCreateResponse, AdminRolesDeleteRequest, @@ -667,6 +668,7 @@ export type Endpoints = { 'admin/update-meta': { req: AdminUpdateMetaRequest; res: EmptyResponse }; 'admin/delete-account': { req: AdminDeleteAccountRequest; res: EmptyResponse }; 'admin/update-user-note': { req: AdminUpdateUserNoteRequest; res: EmptyResponse }; + 'admin/prompt-id-check': { req: AdminPromptIdCheckRequest; res: EmptyResponse }; 'admin/roles/create': { req: AdminRolesCreateRequest; res: AdminRolesCreateResponse }; 'admin/roles/delete': { req: AdminRolesDeleteRequest; res: EmptyResponse }; 'admin/roles/list': { req: EmptyRequest; res: AdminRolesListResponse }; @@ -1063,6 +1065,7 @@ export const endpointReqTypes: Record