From 7bf8775c9972ab7a3333ca49b24f0b71227a4301 Mon Sep 17 00:00:00 2001 From: Yunochi Date: Sat, 19 Oct 2024 19:55:04 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EA=B0=80=EC=9E=85=20=EC=8A=B9=EC=9D=B8?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-pick commits from: https://github.com/nekoplanet/misskey/pull/20 - https://github.com/nekoplanet/misskey/pull/20/commits/736c8087164fa896dd68705130d438bc1e878378 - https://github.com/nekoplanet/misskey/pull/20/commits/dbd302860b32d6eeb704d26097cca56cc7d274b9 - https://github.com/nekoplanet/misskey/pull/20/commits/effcdaaebfb5cc1fda97f1160ae12f18bcdcb9df - https://github.com/nekoplanet/misskey/pull/20/commits/c5cd665d1e484c05200c50807e6c52ac1bff8295 - https://github.com/nekoplanet/misskey/pull/20/commits/db463c9946d35a14e13c1233d42685cb06ded936 Fix: signup-approve (serafuku patch) - state enum의 approved 를 pending 으로 변경 - metaEntityService 에서 approvalRequiredForSignup 넣어주도록 수정 - SignupRequest type에 reason 추가 - 모더레이션 페이지에서 승인 설정 바꾸면 update-meta 하도록 수정 - 기타 어쩌구 저쩌구 프론트&백앤드 버그수정 - Fix lint - fix E2E - 한글 번역 추가 (yozumina) - Fix: user Approved index 추가 - 가입화면 & 가입승인관리 UI 조정 - Fix: 가입 오픈 상태에서는 approved를 자동으로 true Co-authored-by: HotoRas Co-authored-by: Squarecat-meow --- locales/index.d.ts | 72 +++++++++++ locales/ja-JP.yml | 18 +++ locales/ko-KR.yml | 18 +++ .../migration/1697580470000-approvalSignup.js | 24 ++++ .../1729528715258-Index_user_Approved.js | 19 +++ .../src/core/CreateSystemUserService.ts | 1 + packages/backend/src/core/SignupService.ts | 2 + .../backend/src/core/WebhookTestService.ts | 3 + .../activitypub/models/ApPersonService.ts | 2 + .../src/core/entities/MetaEntityService.ts | 1 + .../src/core/entities/UserEntityService.ts | 2 + packages/backend/src/models/Meta.ts | 5 + packages/backend/src/models/User.ts | 11 ++ packages/backend/src/models/UserPending.ts | 5 + .../backend/src/models/json-schema/meta.ts | 4 + .../backend/src/models/json-schema/user.ts | 9 ++ .../backend/src/server/api/ApiCallService.ts | 8 ++ .../src/server/api/ApiServerService.ts | 1 + .../backend/src/server/api/EndpointsModule.ts | 4 + .../src/server/api/SigninApiService.ts | 20 +++ .../src/server/api/SignupApiService.ts | 77 ++++++++++- packages/backend/src/server/api/endpoints.ts | 2 + .../api/endpoints/admin/approve-user.ts | 62 +++++++++ .../src/server/api/endpoints/admin/meta.ts | 5 + .../server/api/endpoints/admin/show-user.ts | 6 + .../server/api/endpoints/admin/show-users.ts | 4 +- .../server/api/endpoints/admin/update-meta.ts | 5 + packages/backend/src/types.ts | 6 + packages/backend/test/e2e/users.ts | 1 + packages/frontend/.storybook/fakes.ts | 1 + .../src/components/MkApprovalUser.vue | 122 ++++++++++++++++++ packages/frontend/src/components/MkSignin.vue | 8 ++ .../src/components/MkSignupDialog.form.vue | 16 +++ .../src/components/MkSignupDialog.vue | 6 +- .../src/components/MkVisitorDashboard.vue | 3 + packages/frontend/src/pages/admin-user.vue | 36 ++++-- .../frontend/src/pages/admin/approvals.vue | 72 +++++++++++ packages/frontend/src/pages/admin/index.vue | 15 +++ .../frontend/src/pages/admin/moderation.vue | 15 +++ .../src/pages/admin/modlog.ModLog.vue | 5 + packages/frontend/src/pages/admin/users.vue | 1 + .../frontend/src/pages/signup-complete.vue | 7 + packages/frontend/src/router/definition.ts | 4 + packages/misskey-js/etc/misskey-js.api.md | 12 +- .../misskey-js/src/autogen/apiClientJSDoc.ts | 12 ++ packages/misskey-js/src/autogen/endpoint.ts | 2 + packages/misskey-js/src/autogen/entities.ts | 1 + packages/misskey-js/src/autogen/types.ts | 76 ++++++++++- packages/misskey-js/src/consts.ts | 7 + packages/misskey-js/src/entities.ts | 4 + 50 files changed, 805 insertions(+), 17 deletions(-) create mode 100644 packages/backend/migration/1697580470000-approvalSignup.js create mode 100644 packages/backend/migration/1729528715258-Index_user_Approved.js create mode 100644 packages/backend/src/server/api/endpoints/admin/approve-user.ts create mode 100644 packages/frontend/src/components/MkApprovalUser.vue create mode 100644 packages/frontend/src/pages/admin/approvals.vue diff --git a/locales/index.d.ts b/locales/index.d.ts index 6b9bb708db42..c8aae6037468 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -166,6 +166,10 @@ export interface Locale extends ILocale { * ユーザー */ "users": string; + /** + * 承認 + */ + "approvals": string; /** * ユーザーを追加 */ @@ -626,6 +630,14 @@ export interface Locale extends ILocale { * 凍結しますか? */ "suspendConfirm": string; + /** + * 登録を承認しますか? + */ + "registerApproveConfirm": string; + /** + * この操作は取り消せません。承認後、このユーザーに登録が承認された旨が記載されたメールが送信されます。 + */ + "registerApproveConfirmDescription": string; /** * 解凍しますか? */ @@ -3666,6 +3678,14 @@ export interface Locale extends ILocale { * アカウント登録にメールアドレスを必須にする */ "emailRequiredForSignup": string; + /** + * アカウント登録を承認制にする + */ + "approvalRequiredForSignup": string; + /** + * アカウント登録の承認 + */ + "signupPendingApprovals": string; /** * 未読 */ @@ -3870,6 +3890,10 @@ export interface Locale extends ILocale { * 未対応の通報があります。 */ "thereIsUnresolvedAbuseReportWarning": string; + /** + * 承認待ちのユーザーがいます。 + */ + "pendingUserApprovals": string; /** * 推奨 */ @@ -3902,6 +3926,26 @@ export interface Locale extends ILocale { * アカウント削除 */ "deleteAccount": string; + /** + * 承認する + */ + "approveAccount": string; + /** + * 拒否してアカウント削除 + */ + "denyAccount": string; + /** + * 承認済み + */ + "approved": string; + /** + * 未承認 + */ + "notApproved": string; + /** + * 承認状況 + */ + "approvalStatus": string; /** * ドキュメント */ @@ -4270,6 +4314,22 @@ export interface Locale extends ILocale { * 現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。 */ "invitationRequiredToRegister": string; + /** + * 現在このサーバーは承認制です。参加したい理由を記入し、承認された方のみ登録できます。 + */ + "approvalRequiredToRegister": string; + /** + * 登録理由 + */ + "registerReason": string; + /** + * サーバーへの登録はまだ承認されていません。しばらくしてから再度お試しください。登録時にメールアドレスを記入した場合は、登録が承認されたらメールでお知らせします。 + */ + "registerHasNotBeenApprovedYet": string; + /** + * サーバーへの登録が承認されたかどうかの通知を行うために、併せてアカウント登録にメールアドレスを必須にすることを強く推奨します。 + */ + "registerApprovalEmailRecommended": string; /** * このサーバーではメール配信はサポートされていません */ @@ -7106,6 +7166,14 @@ export interface Locale extends ILocale { * 入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。メールに記載されているリンクの有効期限は30分です。 */ "emailSent": ParameterizedString<"email">; + /** + * アカウントが作成され、承認待ちの状態です。 + */ + "approvalPending": string; + /** + * このサーバーに参加したい理由を入力してください。 + */ + "reasonInfo": string; }; "_accountDelete": { /** @@ -9780,6 +9848,10 @@ export interface Locale extends ILocale { * ロールのアサイン解除 */ "unassignRole": string; + /** + * 承認済み + */ + "approve": string; /** * 凍結 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index d243380ff68d..711e6277539c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -37,6 +37,7 @@ signup: "新規登録" uploading: "アップロード中" save: "保存" users: "ユーザー" +approvals: "承認" addUser: "ユーザーを追加" favorite: "お気に入り" favorites: "お気に入り" @@ -152,6 +153,8 @@ unsuspend: "解凍" blockConfirm: "ブロックしますか?" unblockConfirm: "ブロック解除しますか?" suspendConfirm: "凍結しますか?" +registerApproveConfirm: "登録を承認しますか?" +registerApproveConfirmDescription: "この操作は取り消せません。承認後、このユーザーに登録が承認された旨が記載されたメールが送信されます。" unsuspendConfirm: "解凍しますか?" selectList: "リストを選択" editList: "リストを編集" @@ -912,6 +915,8 @@ itsOff: "オフになっています" on: "オン" off: "オフ" emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする" +approvalRequiredForSignup: "アカウント登録を承認制にする" +signupPendingApprovals: "アカウント登録の承認" unread: "未読" filter: "フィルタ" controlPanel: "コントロールパネル" @@ -963,6 +968,7 @@ recentNHours: "直近{n}時間" recentNDays: "直近{n}日" noEmailServerWarning: "メールサーバーの設定がされていません。" thereIsUnresolvedAbuseReportWarning: "未対応の通報があります。" +pendingUserApprovals: "承認待ちのユーザーがいます。" recommended: "推奨" check: "チェック" driveCapOverrideLabel: "このユーザーのドライブ容量上限を変更" @@ -971,6 +977,11 @@ requireAdminForView: "閲覧するには管理者アカウントでログイン isSystemAccount: "システムにより自動で作成・管理されているアカウントです。" typeToConfirm: "この操作を行うには {x} と入力してください" deleteAccount: "アカウント削除" +approveAccount: "承認する" +denyAccount: "拒否してアカウント削除" +approved: "承認済み" +notApproved: "未承認" +approvalStatus: "承認状況" document: "ドキュメント" numberOfPageCache: "ページキャッシュ数" numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。" @@ -1063,6 +1074,10 @@ disableFederationConfirm: "連合なしにしますか?" disableFederationConfirmWarn: "連合なしにしても投稿は非公開になりません。ほとんどの場合、連合なしにする必要はありません。" disableFederationOk: "連合なしにする" invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。" +approvalRequiredToRegister: "現在このサーバーは承認制です。参加したい理由を記入し、承認された方のみ登録できます。" +registerReason: "登録理由" +registerHasNotBeenApprovedYet: "サーバーへの登録はまだ承認されていません。しばらくしてから再度お試しください。登録時にメールアドレスを記入した場合は、登録が承認されたらメールでお知らせします。" +registerApprovalEmailRecommended: "サーバーへの登録が承認されたかどうかの通知を行うために、併せてアカウント登録にメールアドレスを必須にすることを強く推奨します。" emailNotSupported: "このサーバーではメール配信はサポートされていません" postToTheChannel: "チャンネルに投稿" cannotBeChangedLater: "後から変更できません。" @@ -1840,6 +1855,8 @@ _signup: almostThere: "ほとんど完了です" emailAddressInfo: "あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。" emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。メールに記載されているリンクの有効期限は30分です。" + approvalPending: "アカウントが作成され、承認待ちの状態です。" + reasonInfo: "このサーバーに参加したい理由を入力してください。" _accountDelete: accountDelete: "アカウントの削除" @@ -2593,6 +2610,7 @@ _moderationLogTypes: updateRole: "ロールを更新" assignRole: "ロールへアサイン" unassignRole: "ロールのアサイン解除" + approve: "承認済み" suspend: "凍結" unsuspend: "凍結解除" addCustomEmoji: "カスタム絵文字追加" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 2a10c84953f2..8a060ac555fd 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -37,6 +37,7 @@ signup: "회원 가입" uploading: "업로드 중" save: "저장" users: "유저" +approvals: "승인" addUser: "유저 추가" favorite: "즐겨찾기" favorites: "즐겨찾기" @@ -152,6 +153,8 @@ unsuspend: "정지 해제" blockConfirm: "이 계정을 차단하시겠습니까?" unblockConfirm: "이 계정의 차단을 해제하시겠습니까?" suspendConfirm: "이 계정을 정지하시겠습니까?" +registerApproveConfirm: "가입을 승인하시겠습니까?" +registerApproveConfirmDescription: "이 동작은 되돌릴 수 없습니다. 승인 후, 이 유저에게 가입 승인 안내 메일이 송신됩니다." unsuspendConfirm: "이 계정의 정지를 해제하시겠습니까?" selectList: "리스트 선택" editList: "리스트 편집" @@ -912,6 +915,8 @@ itsOff: "꺼져 있습니다" on: "켜짐" off: "꺼짐" emailRequiredForSignup: "가입할 때 이메일 주소 입력을 필수로 하기" +approvalRequiredForSignup: "계정 가입을 승인제로 하기" +signupPendingApprovals: "계정 가입 승인" unread: "읽지 않음" filter: "필터" controlPanel: "제어판" @@ -963,6 +968,7 @@ recentNHours: "최근 {n}시간" recentNDays: "최근 {n}일" noEmailServerWarning: "메일 서버가 설정되어 있지 않습니다." thereIsUnresolvedAbuseReportWarning: "해결되지 않은 신고가 있습니다." +pendingUserApprovals: "승인 대기중인 유저가 있습니다." recommended: "추천" check: "체크" driveCapOverrideLabel: "이 유저의 드라이브 용량을 변경" @@ -971,6 +977,11 @@ requireAdminForView: "열람하려면 관리자 계정으로 로그인해야 합 isSystemAccount: "시스템에 의해 자동으로 생성되어 관리되는 계정입니다." typeToConfirm: "계속하시려면 {x} 을 입력하세요" deleteAccount: "계정 삭제" +approveAccount: "승인" +denyAccount: "거부하고 계정 삭제" +approved: "승인됨" +notApproved: "미승인" +approvalStatus: "승인상태" document: "문서" numberOfPageCache: "페이지 캐시 수" numberOfPageCacheDescription: "숫자가 클 수록 편리성이 높아지지만, 시스템 자원과 메모리를 더 많이 사용합니다." @@ -1063,6 +1074,10 @@ disableFederationConfirm: "정말로 연합을 끄시겠습니까?" disableFederationConfirmWarn: "연합을 끄더라도 게시물이 비공개로 전환되는 것은 아닙니다. 대부분의 경우 연합을 비활성화할 필요가 없습니다." disableFederationOk: "연합을 끄기" invitationRequiredToRegister: "현재 이 서버는 비공개입니다. 회원가입을 하시려면 초대 코드가 필요합니다." +approvalRequiredToRegister: "현재 이 서버는 승인제입니다. 가입하고 싶은 이유를 기입하고, 승인된 사람만이 가입할 수 있습니다." +registerReason: "가입 이유" +registerHasNotBeenApprovedYet: "서버 가입이 아직 승인되지 않았습니다. 잠시 뒤 다시 시도해주세요. 가입 시 이메일 주소를 입력한 경우, 가입이 승인된 후에 메일로 알려드립니다." +registerApprovalEmailRecommended: "서버 가입 승인이 이루어졌는지 알려주기 위해서 가입 시 이메일 입력 필수 기능을 켜는 것을 추천드립니다." emailNotSupported: "이 서버에서는 메일 전송을 지원하지 않습니다" postToTheChannel: "채널에 게시하기" cannotBeChangedLater: "나중에 변경할 수 없습니다." @@ -1825,6 +1840,8 @@ _signup: almostThere: "거의 다 끝났습니다" emailAddressInfo: "당신이 사용하고 있는 이메일 주소를 입력해 주세요. 이메일 주소는 다른 유저에게 공개되지 않습니다." emailSent: "입력하신 메일 주소({email})로 확인 메일을 보내드렸습니다. 가입을 완료하시려면 보내드린 메일에 있는 링크로 접속해 주세요." + approvalPending: "계정이 작성되어, 승인을 기다리고 있습니다." + reasonInfo: "이 서버에 가입하고 싶은 이유를 적어주세요." _accountDelete: accountDelete: "계정 삭제" mayTakeTime: "계정 삭제는 서버에 부하를 가하기 때문에, 작성한 콘텐츠나 업로드한 파일의 수가 많으면 완료까지 시간이 걸릴 수 있습니다." @@ -2525,6 +2542,7 @@ _moderationLogTypes: updateRole: "역할 수정" assignRole: "역할 할당" unassignRole: "역할 해제" + approve: "승인됨" suspend: "정지" unsuspend: "정지 해제" addCustomEmoji: "커스텀 이모지 추가" diff --git a/packages/backend/migration/1697580470000-approvalSignup.js b/packages/backend/migration/1697580470000-approvalSignup.js new file mode 100644 index 000000000000..61fc0ed64e58 --- /dev/null +++ b/packages/backend/migration/1697580470000-approvalSignup.js @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class ApprovalSignup1697580470000 { + name = 'ApprovalSignup1697580470000' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "approvalRequiredForSignup" boolean DEFAULT false NOT NULL`); + await queryRunner.query(`ALTER TABLE "user" ADD "approved" boolean DEFAULT false NOT NULL`); + //▼ 既存のユーザーについては全員Approveにする + await queryRunner.query(`UPDATE "user" SET "approved" = true`); + await queryRunner.query(`ALTER TABLE "user" ADD "signupReason" character varying(1000) NULL`); + await queryRunner.query(`ALTER TABLE "user_pending" ADD "reason" character varying(1000) NULL`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "approvalRequiredForSignup"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "approved"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "signupReason"`); + await queryRunner.query(`ALTER TABLE "user_pending" DROP COLUMN "reason"`); + } +} diff --git a/packages/backend/migration/1729528715258-Index_user_Approved.js b/packages/backend/migration/1729528715258-Index_user_Approved.js new file mode 100644 index 000000000000..25479b226e92 --- /dev/null +++ b/packages/backend/migration/1729528715258-Index_user_Approved.js @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class IndexUserApproved1729528715258 { + name = 'IndexUserApproved1729528715258'; + async up(queryRunner) { + await queryRunner.query(` + CREATE INDEX "IDX_USER_APPROVED" ON "user" ("approved") + `); + } + + async down(queryRunner) { + await queryRunner.query(` + DROP INDEX "public"."IDX_USER_APPROVED" + `); + } +} diff --git a/packages/backend/src/core/CreateSystemUserService.ts b/packages/backend/src/core/CreateSystemUserService.ts index 6c5b0f6a36ae..71c2db77aba6 100644 --- a/packages/backend/src/core/CreateSystemUserService.ts +++ b/packages/backend/src/core/CreateSystemUserService.ts @@ -60,6 +60,7 @@ export class CreateSystemUserService { isRoot: false, isLocked: true, isExplorable: false, + approved: true, isBot: true, }).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0])); diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 3865392b7f8b..7a1af361795b 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -53,6 +53,7 @@ export class SignupService { passwordHash?: MiUserProfile['password'] | null; host?: string | null; ignorePreservedUsernames?: boolean; + reason?: string | null; }) { const { username, password, passwordHash, host } = opts; let hash = passwordHash; @@ -130,6 +131,7 @@ export class SignupService { host: this.utilityService.toPunyNullable(host), token: secret, isRoot: isTheFirstUser, + signupReason: opts.reason, })); await transactionalEntityManager.save(new MiUserKeypair({ diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts index 1ae8d77bec8d..69ae0cdfdb82 100644 --- a/packages/backend/src/core/WebhookTestService.ts +++ b/packages/backend/src/core/WebhookTestService.ts @@ -83,6 +83,8 @@ function generateDummyUser(override?: Partial): MiUser { isExplorable: true, isHibernated: false, isDeleted: false, + approved: true, + signupReason: '', emojis: [], score: 0, host: null, @@ -199,6 +201,7 @@ function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<' })), isBot: user.isBot, isCat: user.isCat, + approved: user.approved, emojis: user.emojis, onlineStatus: 'active', badgeRoles: [], diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index a22fe14ff852..22d0ed85a2fc 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -349,6 +349,7 @@ export class ApPersonService implements OnModuleInit { alsoKnownAs: person.alsoKnownAs, isExplorable: person.discoverable, username: person.preferredUsername, + approved: true, usernameLower: person.preferredUsername?.toLowerCase(), host, inbox: person.inbox, @@ -524,6 +525,7 @@ export class ApPersonService implements OnModuleInit { emojis: emojiNames, name: truncate(person.name, nameLength), tags, + approved: true, isBot: getApType(object) === 'Service' || getApType(object) === 'Application', isCat: (person as any).isCat === true, isLocked: person.manuallyApprovesFollowers, diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index 409dca34263b..19be90887c73 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -87,6 +87,7 @@ export class MetaEntityService { inquiryUrl: instance.inquiryUrl, disableRegistration: instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, + approvalRequiredForSignup: instance.approvalRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, enableMcaptcha: instance.enableMcaptcha, diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 72a991946a01..cbd557c41afe 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -557,6 +557,7 @@ export class UserEntityService implements OnModuleInit { }))), memo: memo, moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined, + approved: iAmModerator ? user.approved : undefined, } : {}), ...(isDetailed && (isMe || iAmModerator) ? { @@ -614,6 +615,7 @@ export class UserEntityService implements OnModuleInit { ...(opts.includeSecrets ? { email: profile!.email, emailVerified: profile!.emailVerified, + signupReason: user.signupReason, securityKeysList: profile!.twoFactorEnabled ? this.userSecurityKeysRepository.find({ where: { diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 3af3ac605e22..bf44efc9e6d8 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -189,6 +189,11 @@ export class MiMeta { }) public emailRequiredForSignup: boolean; + @Column('boolean', { + default: false, + }) + public approvalRequiredForSignup: boolean; + @Column('boolean', { default: false, }) diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index 805a1e75aebd..7c65c17d5e0a 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -259,6 +259,17 @@ export class MiUser { }) public token: string | null; + @Index() + @Column('boolean', { + default: false, + }) + public approved: boolean; + + @Column('varchar', { + length: 1000, nullable: true, + }) + public signupReason: string | null; + constructor(data: Partial) { if (data == null) return; diff --git a/packages/backend/src/models/UserPending.ts b/packages/backend/src/models/UserPending.ts index 99f8a22a8481..961ae344f198 100644 --- a/packages/backend/src/models/UserPending.ts +++ b/packages/backend/src/models/UserPending.ts @@ -31,4 +31,9 @@ export class MiUserPending { length: 128, }) public password: string; + + @Column('varchar', { + length: 1000, + }) + public reason: string; } diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index e3fd63464a81..68c41e903ecc 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -79,6 +79,10 @@ export const packedMetaLiteSchema = { type: 'boolean', optional: false, nullable: false, }, + approvalRequiredForSignup: { + type: 'boolean', + optional: false, default: false, + }, enableHcaptcha: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 9cffd680f29b..e5622f860618 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -62,6 +62,11 @@ export const packedUserLiteSchema = { example: 'misskey.example.com', description: 'The local host is represented with `null`.', }, + approved: { + type: 'boolean', + nullable: false, default: false, + description: 'User whom registeration is approved or not', + }, avatarUrl: { type: 'string', format: 'url', @@ -379,6 +384,10 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'boolean', nullable: false, optional: true, }, + approved: { + type: 'boolean', + nullable: false, optional: true, + }, //#region relations isFollowing: { type: 'boolean', diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index aad833f1261e..2484b354c116 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -357,6 +357,14 @@ export class ApiCallService implements OnApplicationShutdown { kind: 'permission', id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370', }); + } else if (this.meta.approvalRequiredForSignup && user!.approved === false) { + throw new ApiError({ + message: 'Your account is not approved yet.', + code: 'YOUR_ACCOUNT_NOT_APPROVED', + kind: 'permission', + id: '2fe70810-0ed2-47db-a70b-dc3ecbf5f069', + httpStatusCode: 403, + }); } } diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index 3a8cb19f01b4..bc8610cd74c3 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -115,6 +115,7 @@ export class ApiServerService { host?: string; invitationCode?: string; emailAddress?: string; + reason?: string; 'hcaptcha-response'?: string; 'g-recaptcha-response'?: string; 'turnstile-response'?: string; diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 9a2da172ff13..fe8973173d8e 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -76,6 +76,7 @@ import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderatio import * as ep___admin_showUser from './endpoints/admin/show-user.js'; 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_approveUser from './endpoints/admin/approve-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'; @@ -465,6 +466,7 @@ const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation const $admin_showUser: Provider = { provide: 'ep:admin/show-user', useClass: ep___admin_showUser.default }; const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: ep___admin_showUsers.default }; const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default }; +const $admin_approveUser: Provider = { provide: 'ep:admin/approve-user', useClass: ep___admin_approveUser.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 }; @@ -858,6 +860,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_showUser, $admin_showUsers, $admin_suspendUser, + $admin_approveUser, $admin_unsuspendUser, $admin_updateMeta, $admin_deleteAccount, @@ -1245,6 +1248,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_showUser, $admin_showUsers, $admin_suspendUser, + $admin_approveUser, $admin_unsuspendUser, $admin_updateMeta, $admin_deleteAccount, diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 1d983ca4bc2a..da34f193d862 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -24,6 +24,7 @@ import { WebAuthnService } from '@/core/WebAuthnService.js'; import { UserAuthService } from '@/core/UserAuthService.js'; import { CaptchaService } from '@/core/CaptchaService.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; +import { MetaService } from '@/core/MetaService.js'; import { RateLimiterService } from './RateLimiterService.js'; import { SigninService } from './SigninService.js'; import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; @@ -56,6 +57,7 @@ export class SigninApiService { private userAuthService: UserAuthService, private webAuthnService: WebAuthnService, private captchaService: CaptchaService, + private metaService: MetaService, ) { } @@ -79,6 +81,8 @@ export class SigninApiService { reply.header('Access-Control-Allow-Origin', this.config.url); reply.header('Access-Control-Allow-Credentials', 'true'); + const instance = await this.metaService.fetch(true); + const body = request.body; const username = body['username']; const password = body['password']; @@ -154,6 +158,18 @@ export class SigninApiService { return; } + if (user.approved === false && instance.approvalRequiredForSignup) { + reply.code(403); + return { + error: { + message: 'Your account is not approved yet.', + code: 'YOUR_ACCOUNT_NOT_APPROVED', + kind: 'permission', + id: '2fe70810-0ed2-47db-a70b-dc3ecbf5f069', + }, + }; + } + // Compare password const same = await bcrypt.compare(password, profile.password!); @@ -204,6 +220,7 @@ export class SigninApiService { } if (same) { + if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true }); return this.signinService.signin(request, reply, user); } else { return await fail(403, { @@ -227,6 +244,8 @@ export class SigninApiService { }); } + if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true }); + return this.signinService.signin(request, reply, user); } else if (body.credential) { if (!same && !profile.usePasswordLessLogin) { @@ -238,6 +257,7 @@ export class SigninApiService { const authorized = await this.webAuthnService.verifyAuthentication(user.id, body.credential); if (authorized) { + if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true }); return this.signinService.signin(request, reply, user); } else { return await fail(403, { diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index 3ec5e5d3e6ce..a6d64bb58181 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -18,6 +18,7 @@ import { MiLocalUser } from '@/models/User.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { bindThis } from '@/decorators.js'; import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; +import { RoleService } from '@/core/RoleService.js'; import { SigninService } from './SigninService.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; @@ -51,6 +52,7 @@ export class SignupApiService { private signupService: SignupService, private signinService: SigninService, private emailService: EmailService, + private roleService: RoleService, ) { } @@ -63,6 +65,7 @@ export class SignupApiService { host?: string; invitationCode?: string; emailAddress?: string; + reason?: string; 'hcaptcha-response'?: string; 'g-recaptcha-response'?: string; 'turnstile-response'?: string; @@ -112,6 +115,7 @@ export class SignupApiService { const password = body['password']; const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] ?? null) : null; const invitationCode = body['invitationCode']; + const reason = body['reason']; const emailAddress = body['emailAddress']; if (this.meta.emailRequiredForSignup) { @@ -127,6 +131,13 @@ export class SignupApiService { } } + if (this.meta.approvalRequiredForSignup) { + if (reason == null || typeof reason !== 'string') { + reply.code(400); + return; + } + } + let ticket: MiRegistrationTicket | null = null; if (this.meta.disableRegistration) { @@ -169,6 +180,9 @@ export class SignupApiService { } if (this.meta.emailRequiredForSignup) { + if (!emailAddress) { + throw new FastifyReplyError(400, 'EMAIL_NOT_PROVIDED'); + } if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { throw new FastifyReplyError(400, 'DUPLICATED_USERNAME'); } @@ -192,14 +206,15 @@ export class SignupApiService { const pendingUser = await this.userPendingsRepository.insertOne({ id: this.idService.gen(), code, - email: emailAddress!, + email: emailAddress, username: username, password: hash, + reason: reason, }); const link = `${this.config.url}/signup-complete/${code}`; - this.emailService.sendEmail(emailAddress!, 'Signup', + this.emailService.sendEmail(emailAddress, 'Signup', `To complete signup, please click this link:
${link}`, `To complete signup, please click this link: ${link}`); @@ -210,6 +225,39 @@ export class SignupApiService { }); } + reply.code(204); + return; + } else if (this.meta.approvalRequiredForSignup) { + const { account } = await this.signupService.signup({ + username, password, host, reason, + }); + + if (emailAddress) { + this.emailService.sendEmail(emailAddress, 'Approval pending', + 'Your account is now pending approval.
You will get notified when you have been accepted.', + 'Your account is now pending approval. You will get notified when you have been accepted.'); + } + + if (ticket) { + await this.registrationTicketsRepository.update(ticket.id, { + usedAt: new Date(), + usedBy: account, + usedById: account.id, + }); + } + + const moderators = await this.roleService.getModerators(); + + for (const moderator of moderators) { + const profile = await this.userProfilesRepository.findOneBy({ userId: moderator.id }); + + if (profile?.email) { + this.emailService.sendEmail(profile.email, 'New user awaiting approval', + `A new user called ${account.username} is awaiting approval with the following reason: "${reason}"`, + `A new user called ${account.username} is awaiting approval with the following reason: "${reason}"`); + } + } + reply.code(204); return; } else { @@ -257,6 +305,7 @@ export class SignupApiService { const { account, secret } = await this.signupService.signup({ username: pendingUser.username, passwordHash: pendingUser.password, + reason: pendingUser.reason, }); this.userPendingsRepository.delete({ @@ -280,6 +329,30 @@ export class SignupApiService { }); } + if (this.meta.approvalRequiredForSignup) { + if (pendingUser.email) { + this.emailService.sendEmail(pendingUser.email, 'Approval pending', + 'Your account is now pending approval. You will get notified when you have been accepted.', + 'Your account is now pending approval. You will get notified when you have been accepted.'); + } + + const moderators = await this.roleService.getModerators(); + + for (const moderator of moderators) { + const profile = await this.userProfilesRepository.findOneBy({ userId: moderator.id }); + + if (profile?.email) { + this.emailService.sendEmail(profile.email, 'New user awaiting approval', + `A new user called ${pendingUser.username} is awaiting approval with the following reason: "${pendingUser.reason}"`, + `A new user called ${pendingUser.username} is awaiting approval with the following reason: "${pendingUser.reason}"`); + } + } + + return { pendingApproval: true }; + } else { + await this.usersRepository.update({ username: pendingUser.username }, { approved: true }); + } + return this.signinService.signin(request, reply, account as MiLocalUser); } catch (err) { throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString()); diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 164e958cd2fb..e9fe53308bf5 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -82,6 +82,7 @@ import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderatio import * as ep___admin_showUser from './endpoints/admin/show-user.js'; 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_approveUser from './endpoints/admin/approve-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'; @@ -469,6 +470,7 @@ const eps = [ ['admin/show-user', ep___admin_showUser], ['admin/show-users', ep___admin_showUsers], ['admin/suspend-user', ep___admin_suspendUser], + ['admin/approve-user', ep___admin_approveUser], ['admin/unsuspend-user', ep___admin_unsuspendUser], ['admin/update-meta', ep___admin_updateMeta], ['admin/delete-account', ep___admin_deleteAccount], diff --git a/packages/backend/src/server/api/endpoints/admin/approve-user.ts b/packages/backend/src/server/api/endpoints/admin/approve-user.ts new file mode 100644 index 000000000000..efe1a081c59c --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/approve-user.ts @@ -0,0 +1,62 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { DI } from '@/di-symbols.js'; +import { EmailService } from '@/core/EmailService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'write:admin:approve-account', + secure: true, +} 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, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private moderationLogService: ModerationLogService, + private emailService: EmailService, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + + if (user == null) { + throw new Error('user not found'); + } + + const profile = await this.userProfilesRepository.findOneBy({ userId: ps.userId }); + + await this.usersRepository.update(user.id, { + approved: true, + }); + + if (profile?.email) { + this.emailService.sendEmail(profile.email, 'Account Approved', + 'Your Account has been approved!
Have fun socializing!', + 'Your Account has been approved! Have fun socializing!'); + } + + this.moderationLogService.log(me, 'approve', { + userId: user.id, + userUsername: user.username, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 2b9b0079cf2e..dec1e3bd69a0 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -33,6 +33,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + approvalRequiredForSignup: { + type: 'boolean', + optional: false, nullable: false, + }, enableHcaptcha: { type: 'boolean', optional: false, nullable: false, @@ -569,6 +573,7 @@ export default class extends Endpoint { // eslint- inquiryUrl: instance.inquiryUrl, disableRegistration: instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, + approvalRequiredForSignup: instance.approvalRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, enableMcaptcha: instance.enableMcaptcha, 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 655bd32bce06..99bf6688e7d3 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -176,6 +176,10 @@ export const meta = { }, }, }, + signupReason: { + type: 'string', + optional: false, nullable: true, + }, }, }, } as const; @@ -231,6 +235,8 @@ export default class extends Endpoint { // eslint- email: profile.email, emailVerified: profile.emailVerified, followedMessage: profile.followedMessage, + approved: user.approved, + signupReason: user.signupReason, autoAcceptFollowed: profile.autoAcceptFollowed, noCrawle: profile.noCrawle, preventAiLearning: profile.preventAiLearning, diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index 2b2c8c60abbc..0140bc4e22d0 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -35,7 +35,7 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, offset: { type: 'integer', default: 0 }, sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt', '+lastActiveDate', '-lastActiveDate'] }, - state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'suspended'], default: 'all' }, + state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'suspended', 'pending', 'approved'], default: 'all' }, origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, username: { type: 'string', nullable: true, default: null }, hostname: { @@ -64,6 +64,8 @@ export default class extends Endpoint { // eslint- case 'available': query.where('user.isSuspended = FALSE'); break; case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break; case 'suspended': query.where('user.isSuspended = TRUE'); break; + case 'approved': query.where('user.approved = TRUE'); break; + case 'pending': query.where('user.approved = FALSE'); break; case 'admin': { const adminIds = await this.roleService.getAdministratorIds(); if (adminIds.length === 0) return []; diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 8f443f8bf0f3..387a97c832af 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -70,6 +70,7 @@ export const paramDef = { cacheRemoteFiles: { type: 'boolean' }, cacheRemoteSensitiveFiles: { type: 'boolean' }, emailRequiredForSignup: { type: 'boolean' }, + approvalRequiredForSignup: { type: 'boolean' }, enableHcaptcha: { type: 'boolean' }, hcaptchaSiteKey: { type: 'string', nullable: true }, hcaptchaSecretKey: { type: 'string', nullable: true }, @@ -317,6 +318,10 @@ export default class extends Endpoint { // eslint- set.emailRequiredForSignup = ps.emailRequiredForSignup; } + if (ps.approvalRequiredForSignup !== undefined) { + set.approvalRequiredForSignup = ps.approvalRequiredForSignup; + } + if (ps.enableHcaptcha !== undefined) { set.enableHcaptcha = ps.enableHcaptcha; } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index df3cfee17103..af1492d12f56 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -72,6 +72,7 @@ export const userImportableEntities = ['antenna', 'blocking', 'customEmoji', 'fo export const moderationLogTypes = [ 'updateServerSettings', 'suspend', + 'approve', 'unsuspend', 'updateUserNote', 'addCustomEmoji', @@ -132,6 +133,11 @@ export type ModerationLogPayloads = { userUsername: string; userHost: string | null; }; + approve: { + userId: string; + userUsername: string; + //userHost: string | null; // User approval is local action + }; unsuspend: { userId: string; userUsername: string; diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 822ca14ae6c3..ee87285c621c 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -151,6 +151,7 @@ describe('ユーザー', () => { securityKeys: user.securityKeys, ...(security ? { email: user.email, + signupReason: null, emailVerified: user.emailVerified, securityKeysList: user.securityKeysList, } : {}), diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index fc3b0334e470..3bea50bf00f6 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -213,6 +213,7 @@ export function userLite(id = 'someuserid', username = 'miskist', host: entities username, host, name, + approved: true, onlineStatus: 'unknown', avatarUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true', avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay', diff --git a/packages/frontend/src/components/MkApprovalUser.vue b/packages/frontend/src/components/MkApprovalUser.vue new file mode 100644 index 000000000000..2ea358fefc4a --- /dev/null +++ b/packages/frontend/src/components/MkApprovalUser.vue @@ -0,0 +1,122 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 5a27cd6de79d..87ecc2bb7997 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -307,6 +307,14 @@ function onSigninApiError(err?: any): void { showSuspendedDialog(); break; } + case '2fe70810-0ed2-47db-a70b-dc3ecbf5f069': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.registerHasNotBeenApprovedYet, + }); + break; + } case '22d05606-fbcf-421a-a2db-b32610dcfd1b': { os.alert({ type: 'error', diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index 3d1c44fc902d..bd8b8c9f4625 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -62,6 +62,10 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.passwordNotMatched }} + + + + @@ -85,6 +89,7 @@ import * as Misskey from 'misskey-js'; import * as config from '@@/js/config.js'; import MkButton from './MkButton.vue'; import MkInput from './MkInput.vue'; +import MkTextarea from './MkTextarea.vue'; import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; @@ -101,6 +106,7 @@ const props = withDefaults(defineProps<{ const emit = defineEmits<{ (ev: 'signup', user: Misskey.entities.SignupResponse): void; (ev: 'signupEmailPending'): void; + (ev: 'approvalPending'): void; }>(); const host = toUnicode(config.host); @@ -115,6 +121,7 @@ const username = ref(''); const password = ref(''); const retypedPassword = ref(''); const invitationCode = ref(''); +const reason = ref(''); const email = ref(''); const usernameState = ref(null); const emailState = ref(null); @@ -137,6 +144,7 @@ const shouldDisableSubmitting = computed((): boolean => { instance.enableTurnstile && !turnstileResponse.value || instance.enableTestcaptcha && !testcaptchaResponse.value || instance.emailRequiredForSignup && emailState.value !== 'ok' || + instance.approvalRequiredForSignup && (reason.value.length < 1 || reason.value.length > 1000) || usernameState.value !== 'ok' || passwordRetypeState.value !== 'match'; }); @@ -259,6 +267,7 @@ async function onSubmit(): Promise { password: password.value, emailAddress: email.value, invitationCode: invitationCode.value, + reason: reason.value, 'hcaptcha-response': hCaptchaResponse.value, 'm-captcha-response': mCaptchaResponse.value, 'g-recaptcha-response': reCaptchaResponse.value, @@ -285,6 +294,13 @@ async function onSubmit(): Promise { text: i18n.tsx._signup.emailSent({ email: email.value }), }); emit('signupEmailPending'); + } else if (instance.approvalRequiredForSignup) { + os.alert({ + type: 'success', + title: i18n.ts._signup.almostThere, + text: i18n.ts._signup.approvalPending, + }); + emit('approvalPending'); } else { const resJson = (await res.json()) as Misskey.entities.SignupResponse; if (_DEV_) console.log(resJson); diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue index f240e6dc4690..c3518db1f54c 100644 --- a/packages/frontend/src/components/MkSignupDialog.vue +++ b/packages/frontend/src/components/MkSignupDialog.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -63,6 +63,10 @@ function onSignup(res: Misskey.entities.SignupResponse) { function onSignupEmailPending() { dialog.value?.close(); } + +function onApprovalPending() { + dialog.value?.close(); +} diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 8a206a2f79a2..75852d8f561f 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -18,6 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.noInquiryUrlWarning }} {{ i18n.ts.configure }} {{ i18n.ts.noBotProtectionWarning }} {{ i18n.ts.configure }} {{ i18n.ts.noEmailServerWarning }} {{ i18n.ts.configure }} + {{ i18n.ts.pendingUserApprovals }} {{ i18n.ts.check }} @@ -66,6 +67,7 @@ const noBotProtection = computed(() => !instance.disableRegistration && !instanc const noEmailServer = computed(() => !instance.enableEmail); const noInquiryUrl = computed(() => isEmpty(instance.inquiryUrl)); const thereIsUnresolvedAbuseReport = ref(false); +const pendingUserApprovals = ref(false); const currentPage = computed(() => router.currentRef.value.child); misskeyApi('admin/abuse-user-reports', { @@ -75,6 +77,14 @@ misskeyApi('admin/abuse-user-reports', { if (reports.length > 0) thereIsUnresolvedAbuseReport.value = true; }); +misskeyApi('admin/show-users', { + state: 'pending', + origin: 'local', + limit: 1, +}).then(pendings => { + if (pendings.length > 0) pendingUserApprovals.value = true; +}); + const NARROW_THRESHOLD = 600; const ro = new ResizeObserver((entries, observer) => { if (entries.length === 0) return; @@ -111,6 +121,11 @@ const menuDef = computed(() => [{ text: i18n.ts.invite, to: '/admin/invites', active: currentPage.value?.route.name === 'invites', + }, { + icon: 'ti ti-user-check', + text: i18n.ts.signupPendingApprovals, + to: '/admin/approvals', + active: currentPage.value?.route.name === 'approvals', }, { icon: 'ti ti-badges', text: i18n.ts.roles, diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index 5d8a581b2e54..f705683d134a 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -19,6 +19,11 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + {{ i18n.ts.serverRules }} @@ -141,6 +146,7 @@ import MkFolder from '@/components/MkFolder.vue'; const enableRegistration = ref(false); const emailRequiredForSignup = ref(false); +const approvalRequiredForSignup = ref(false); const sensitiveWords = ref(''); const prohibitedWords = ref(''); const prohibitedWordsForNameOfUser = ref(''); @@ -154,6 +160,7 @@ async function init() { const meta = await misskeyApi('admin/meta'); enableRegistration.value = !meta.disableRegistration; emailRequiredForSignup.value = meta.emailRequiredForSignup; + approvalRequiredForSignup.value = meta.approvalRequiredForSignup; sensitiveWords.value = meta.sensitiveWords.join('\n'); prohibitedWords.value = meta.prohibitedWords.join('\n'); prohibitedWordsForNameOfUser.value = meta.prohibitedWordsForNameOfUser.join('\n'); @@ -180,6 +187,14 @@ function onChange_emailRequiredForSignup(value: boolean) { }); } +function onChange_approvalRequiredForSignup(value: boolean) { + os.apiWithDialog('admin/update-meta', { + approvalRequiredForSignup: value, + }).then(() => { + fetchInstance(true); + }); +} + function save_preservedUsernames() { os.apiWithDialog('admin/update-meta', { preservedUsernames: preservedUsernames.value.split('\n'), diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue index 1e144394fbaf..3ed779c16098 100644 --- a/packages/frontend/src/pages/admin/modlog.ModLog.vue +++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue @@ -26,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only ].includes(log.type), [$style.logRed]: [ 'suspend', + 'approve', 'deleteRole', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', @@ -45,6 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only >{{ i18n.ts._moderationLogTypes[log.type] }} : @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} : @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} + : @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} : @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} : @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} : @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} {{ log.info.roleName }} @@ -108,6 +110,9 @@ SPDX-License-Identifier: AGPL-3.0-only + diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue index d1bbb5b73418..db99b8db3498 100644 --- a/packages/frontend/src/pages/admin/users.vue +++ b/packages/frontend/src/pages/admin/users.vue @@ -21,6 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only + diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue index 14fb96d4f18a..503e6e0f73e0 100644 --- a/packages/frontend/src/pages/signup-complete.vue +++ b/packages/frontend/src/pages/signup-complete.vue @@ -46,6 +46,13 @@ function submit() { misskeyApi('signup-pending', { code: props.code, }).then(res => { + if (res.pendingApproval) { + return os.alert({ + type: 'success', + title: i18n.ts._signup.almostThere, + text: i18n.ts._signup.approvalPending, + }); + } return login(res.i, '/'); }).catch(() => { submitting.value = false; diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index 75f994b86517..86892e480a60 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -486,6 +486,10 @@ const routes: RouteDef[] = [{ path: '/system-webhook', name: 'system-webhook', component: page(() => import('@/pages/admin/system-webhook.vue')), + }, { + path: '/approvals', + name: 'approvals', + component: page(() => import('@/pages/admin/approvals.vue')), }, { path: '/', component: page(() => import('@/pages/_empty_.vue')), diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 5f0aa5ec373b..8b729abc06c9 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -117,6 +117,9 @@ type AdminAnnouncementsListResponse = operations['admin___announcements___list'] // @public (undocumented) type AdminAnnouncementsUpdateRequest = operations['admin___announcements___update']['requestBody']['content']['application/json']; +// @public (undocumented) +type AdminApproveUserRequest = operations['admin___approve-user']['requestBody']['content']['application/json']; + // @public (undocumented) type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json']; @@ -1315,6 +1318,7 @@ declare namespace entities { AdminShowUsersRequest, AdminShowUsersResponse, AdminSuspendUserRequest, + AdminApproveUserRequest, AdminUnsuspendUserRequest, AdminUpdateMetaRequest, AdminDeleteAccountRequest, @@ -2453,6 +2457,9 @@ type ModerationLog = { } & ({ type: 'updateServerSettings'; info: ModerationLogPayloads['updateServerSettings']; +} | { + type: 'approve'; + info: ModerationLogPayloads['approve']; } | { type: 'suspend'; info: ModerationLogPayloads['suspend']; @@ -2603,7 +2610,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"]; +export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "approve", "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"]; // @public (undocumented) type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json']; @@ -2883,7 +2890,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"]; +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", "write:admin:approve-account", "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"]; // @public (undocumented) type PingResponse = operations['ping']['responses']['200']['content']['application/json']; @@ -3134,6 +3141,7 @@ type SignupRequest = { host?: string; invitationCode?: string; emailAddress?: string; + reason?: string; 'hcaptcha-response'?: string | null; 'g-recaptcha-response'?: string | null; 'turnstile-response'?: string | null; diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index ffa0981ec28a..d91702cc2034 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -779,6 +779,18 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* / **Permission**: *write:admin:approve-account* + */ + 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 20ae1c458d3d..50a9029bffb9 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -94,6 +94,7 @@ import type { AdminShowUsersRequest, AdminShowUsersResponse, AdminSuspendUserRequest, + AdminApproveUserRequest, AdminUnsuspendUserRequest, AdminUpdateMetaRequest, AdminDeleteAccountRequest, @@ -652,6 +653,7 @@ export type Endpoints = { 'admin/show-user': { req: AdminShowUserRequest; res: AdminShowUserResponse }; 'admin/show-users': { req: AdminShowUsersRequest; res: AdminShowUsersResponse }; 'admin/suspend-user': { req: AdminSuspendUserRequest; res: EmptyResponse }; + 'admin/approve-user': { req: AdminApproveUserRequest; res: EmptyResponse }; 'admin/unsuspend-user': { req: AdminUnsuspendUserRequest; res: EmptyResponse }; 'admin/update-meta': { req: AdminUpdateMetaRequest; res: EmptyResponse }; 'admin/delete-account': { req: AdminDeleteAccountRequest; res: EmptyResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index f869e0f50a00..4af93298454d 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -97,6 +97,7 @@ export type AdminShowUserResponse = operations['admin___show-user']['responses'] export type AdminShowUsersRequest = operations['admin___show-users']['requestBody']['content']['application/json']; export type AdminShowUsersResponse = operations['admin___show-users']['responses']['200']['content']['application/json']; export type AdminSuspendUserRequest = operations['admin___suspend-user']['requestBody']['content']['application/json']; +export type AdminApproveUserRequest = operations['admin___approve-user']['requestBody']['content']['application/json']; export type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json']; export type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json']; export type AdminDeleteAccountRequest = operations['admin___delete-account']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 043fdb86b6a6..bb5f7b8dba76 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -648,6 +648,16 @@ export type paths = { */ post: operations['admin___suspend-user']; }; + '/admin/approve-user': { + /** + * admin/approve-user + * @description No description provided. + * + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* / **Permission**: *write:admin:approve-account* + */ + post: operations['admin___approve-user']; + }; '/admin/unsuspend-user': { /** * admin/unsuspend-user @@ -3738,6 +3748,11 @@ export type components = { * @example misskey.example.com */ host: string | null; + /** + * @description User whom registeration is approved or not + * @default false + */ + approved: boolean; /** Format: url */ avatarUrl: string | null; avatarBlurhash: string | null; @@ -3824,6 +3839,7 @@ export type components = { twoFactorEnabled?: boolean; usePasswordLessLogin?: boolean; securityKeys?: boolean; + approved?: boolean; isFollowing?: boolean; isFollowed?: boolean; hasPendingFollowRequestFromYou?: boolean; @@ -4983,6 +4999,8 @@ export type components = { defaultLightTheme: string | null; disableRegistration: boolean; emailRequiredForSignup: boolean; + /** @default false */ + approvalRequiredForSignup: boolean; enableHcaptcha: boolean; hcaptchaSiteKey: string | null; enableMcaptcha: boolean; @@ -5137,6 +5155,7 @@ export type operations = { cacheRemoteFiles: boolean; cacheRemoteSensitiveFiles: boolean; emailRequiredForSignup: boolean; + approvalRequiredForSignup: boolean; enableHcaptcha: boolean; hcaptchaSiteKey: string | null; enableMcaptcha: boolean; @@ -9278,6 +9297,7 @@ export type operations = { expiresAt: string | null; roleId: string; })[]; + signupReason: string | null; }; }; }; @@ -9333,7 +9353,7 @@ export type operations = { * @default all * @enum {string} */ - state?: 'all' | 'alive' | 'available' | 'admin' | 'moderator' | 'adminOrModerator' | 'suspended'; + state?: 'all' | 'alive' | 'available' | 'admin' | 'moderator' | 'adminOrModerator' | 'suspended' | 'pending' | 'approved'; /** * @default combined * @enum {string} @@ -9440,6 +9460,59 @@ export type operations = { }; }; }; + /** + * admin/approve-user + * @description No description provided. + * + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* / **Permission**: *write:admin:approve-account* + */ + 'admin___approve-user': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * admin/unsuspend-user * @description No description provided. @@ -9528,6 +9601,7 @@ export type operations = { cacheRemoteFiles?: boolean; cacheRemoteSensitiveFiles?: boolean; emailRequiredForSignup?: boolean; + approvalRequiredForSignup?: boolean; enableHcaptcha?: boolean; hcaptchaSiteKey?: string | null; hcaptchaSecretKey?: string | null; diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index c5911a70eb78..a1f896b4f1b5 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -66,6 +66,7 @@ export const permissions = [ 'read:admin:abuse-user-reports', 'write:admin:delete-account', 'write:admin:delete-all-files-of-a-user', + 'write:admin:approve-account', 'read:admin:index-stats', 'read:admin:table-stats', 'read:admin:user-ips', @@ -115,6 +116,7 @@ export const permissions = [ export const moderationLogTypes = [ 'updateServerSettings', 'suspend', + 'approve', 'unsuspend', 'updateUserNote', 'addCustomEmoji', @@ -195,6 +197,11 @@ export type ModerationLogPayloads = { userUsername: string; userHost: string | null; }; + approve: { + userId: string; + userUsername: string; + userHost: string | null; + }; unsuspend: { userId: string; userUsername: string; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index dd88791ed037..10312b1ccbe6 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -48,6 +48,9 @@ export type ModerationLog = { } & ({ type: 'updateServerSettings'; info: ModerationLogPayloads['updateServerSettings']; +} | { + type: 'approve'; + info: ModerationLogPayloads['approve']; } | { type: 'suspend'; info: ModerationLogPayloads['suspend']; @@ -254,6 +257,7 @@ export type SignupRequest = { host?: string; invitationCode?: string; emailAddress?: string; + reason?: string; 'hcaptcha-response'?: string | null; 'g-recaptcha-response'?: string | null; 'turnstile-response'?: string | null;