diff --git a/packages/backend/migration/1736372582304-approval.js b/packages/backend/migration/1736372582304-approval.js new file mode 100644 index 000000000..d107a1970 --- /dev/null +++ b/packages/backend/migration/1736372582304-approval.js @@ -0,0 +1,22 @@ +/* + * 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`); + 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/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 514b0d2b2..3e99beeb7 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -58,9 +58,10 @@ export class SignupService { password?: string | null; passwordHash?: MiUserProfile['password'] | null; host?: string | null; + reason?: string | null; ignorePreservedUsernames?: boolean; }) { - const { username, password, passwordHash, host } = opts; + const { username, password, passwordHash, host, reason } = opts; let hash = passwordHash; // Validate username @@ -93,9 +94,9 @@ export class SignupService { } const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent(); + const instance = await this.metaService.fetch(true); if (!opts.ignorePreservedUsernames && !isTheFirstUser) { - const instance = await this.metaService.fetch(true); const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); if (isPreserved) { throw new Error('USED_USERNAME'); @@ -128,6 +129,10 @@ export class SignupService { try { let account!: MiUser; + let defaultApproval = false; + + if (!instance.approvalRequiredForSignup) defaultApproval = true; + // Start transaction await this.db.transaction(async transactionalEntityManager => { const exist = await transactionalEntityManager.findOneBy(MiUser, { @@ -144,6 +149,8 @@ export class SignupService { host: host ? this.utilityService.normalizeHost(host) : null, token: secret, isRoot: isTheFirstUser, + approved: defaultApproval, + signupReason: reason, })); await transactionalEntityManager.save(new MiUserKeypair({ diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 7016dfa03..a4d0ef60d 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -328,8 +328,8 @@ export class ApPersonService implements OnModuleInit { this.logger.error('error occurred while fetching following/followers collection', { error: err }); } return 'private'; - }) - ) + }), + ), ); const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); @@ -378,6 +378,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, @@ -526,8 +527,8 @@ export class ApPersonService implements OnModuleInit { return undefined; } return 'private'; - }) - ) + }), + ), ); const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index 28780b8d4..1a77e279f 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -68,6 +68,7 @@ export class MetaEntityService { privacyPolicyUrl: instance.privacyPolicyUrl, disableRegistration: instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, + approvalRequiredForSignup: instance.approvalRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, enableMcaptcha: instance.enableMcaptcha, @@ -140,6 +141,7 @@ export class MetaEntityService { globalTimeline: instance.policies.gtlAvailable, registration: !instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, + approvalRequiredForSignup: instance.approvalRequiredForSignup, hCaptcha: instance.enableHcaptcha, hcaptcha: instance.enableHcaptcha, mCaptcha: instance.enableMcaptcha, diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index e9bf5a397..11bab48c9 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -614,6 +614,8 @@ export class UserEntityService implements OnModuleInit { ...(opts.includeSecrets ? { email: profile!.email, emailVerified: profile!.emailVerified, + approved: user.approved, + signupReason: user.signupReason, securityKeysList: profile!.twoFactorEnabled ? this.userSecurityKeysRepository.find({ where: { @@ -704,7 +706,7 @@ export class UserEntityService implements OnModuleInit { } } - return (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap?.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes })))) + return (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes })))) .filter(result => result.status === 'fulfilled') .map(result => (result as PromiseFulfilledResult>).value); } diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 649b2c2f9..e639312e6 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -184,6 +184,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 4a16d0397..7db66f1f8 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -261,6 +261,16 @@ export class MiUser { }) public token: string | null; + @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 86c671caf..8f0be4625 100644 --- a/packages/backend/src/models/UserPending.ts +++ b/packages/backend/src/models/UserPending.ts @@ -38,4 +38,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 7314a224d..91dd6fd8b 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -75,6 +75,10 @@ export const packedMetaLiteSchema = { type: 'boolean', optional: false, nullable: false, }, + approvalRequiredForSignup: { + type: 'boolean', + optional: false, nullable: false, + }, enableHcaptcha: { type: 'boolean', optional: false, nullable: false, @@ -275,6 +279,10 @@ export const packedMetaDetailedOnlySchema = { type: 'boolean', optional: false, nullable: false, }, + approvalRequiredForSignup: { + type: 'boolean', + optional: false, nullable: false, + }, localTimeline: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 924eac7a1..407af2d1c 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -82,6 +82,7 @@ import * as ep___admin_showUserAccountMoveLogs from './endpoints/admin/show-user 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_updateUserName from './endpoints/admin/update-user-name.js'; @@ -477,6 +478,7 @@ const $admin_showUserAccountMoveLogs: Provider = { provide: 'ep:admin/show-user- 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_updateUserName: Provider = { provide: 'ep:admin/update-user-name', useClass: ep___admin_updateUserName.default }; @@ -876,6 +878,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_showUser, $admin_showUsers, $admin_suspendUser, + $admin_approveUser, $admin_unsuspendUser, $admin_updateMeta, $admin_updateUserName, @@ -1269,6 +1272,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_showUser, $admin_showUsers, $admin_suspendUser, + $admin_approveUser, $admin_unsuspendUser, $admin_updateMeta, $admin_updateUserName, diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index a56a2d85b..4c945bd4d 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -79,6 +79,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']; @@ -132,13 +134,13 @@ export class SigninApiService { emailVerified: true, user: { host: IsNull(), - } + }, } : { user: { usernameLower: username.toLowerCase(), host: IsNull(), - } - } + }, + }, }); const user = (profile?.user as MiLocalUser) ?? null; @@ -163,6 +165,17 @@ export class SigninApiService { }); } + if (!user.approved && instance.approvalRequiredForSignup) { + reply.code(403); + return { + error: { + message: 'The account has not been approved by an admin yet. Try again later.', + code: 'NOT_APPROVED', + id: '22d05606-fbcf-421a-a2db-b32241faft1b', + }, + }; + } + // Compare password const same = await bcrypt.compare(password, profile.password!); @@ -207,6 +220,7 @@ export class SigninApiService { } if (same) { + if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true }); logger.info('Successfully signed in with password.'); return this.signinService.signin(request, reply, user); } else { @@ -234,6 +248,7 @@ export class SigninApiService { }); } + if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true }); logger.info('Successfully signed in with password and two-factor token.'); return this.signinService.signin(request, reply, user); } else if (body.credential) { @@ -247,6 +262,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 }); logger.info('Successfully signed in with WebAuthn authentication.'); return this.signinService.signin(request, reply, user); } else { diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index a0f909d93..bc77a6817 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { IsNull } from 'typeorm'; @@ -21,8 +22,8 @@ import { bindThis } from '@/decorators.js'; import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; import { LoggerService } from '@/core/LoggerService.js'; import { SigninService } from './SigninService.js'; +import instance from './endpoints/charts/instance.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; -import { randomUUID } from 'node:crypto'; @Injectable() export class SignupApiService { @@ -65,6 +66,7 @@ export class SignupApiService { host?: string; invitationCode?: string; emailAddress?: string; + reason?: string; 'hcaptcha-response'?: string; 'g-recaptcha-response'?: string; 'turnstile-response'?: string; @@ -117,6 +119,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 (instance.emailRequiredForSignup) { @@ -134,6 +137,13 @@ export class SignupApiService { } } + if (instance.approvalRequiredForSignup) { + if (reason == null || typeof reason !== 'string') { + reply.code(400); + return; + } + } + let ticket: MiRegistrationTicket | null = null; if (instance.disableRegistration) { @@ -211,6 +221,7 @@ export class SignupApiService { email: emailAddress!, username: username, password: hash, + reason: reason, }).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0])); const link = `${this.config.url}/signup-complete/${code}`; @@ -231,6 +242,17 @@ export class SignupApiService { reply.code(204); return; } else { + if (instance.approvalRequiredForSignup) { + await this.signupService.signup({ + username, password, host, reason, + }); + + if (emailAddress) { + this.emailService.sendEmail(emailAddress, 'Approval pending', + 'Congratulations! Your account is now pending approval. You will get notified when you have been accepted.', + 'Congratulations! Your account is now pending approval. You will get notified when you have been accepted.'); + } + } try { const { account, secret } = await this.signupService.signup({ username, password, host, @@ -272,6 +294,8 @@ export class SignupApiService { const code = body['code']; + const instance = await this.metaService.fetch(true); + try { const pendingUser = await this.userPendingsRepository.findOneByOrFail({ code }); @@ -283,6 +307,7 @@ export class SignupApiService { const { account, secret } = await this.signupService.signup({ username: pendingUser.username, passwordHash: pendingUser.password, + reason: pendingUser.reason, }); this.userPendingsRepository.delete({ @@ -306,6 +331,15 @@ export class SignupApiService { }); } + if (instance.approvalRequiredForSignup) { + if (pendingUser.email) { + this.emailService.sendEmail(pendingUser.email, 'Approval pending', + 'Congratulations! Your account is now pending approval. You will get notified when you have been accepted.', + 'Congratulations! Your account is now pending approval. You will get notified when you have been accepted.'); + } + return { pendingApproval: true }; + } + logger.info('Successfully created user.', { userId: account.id }); return this.signinService.signin(request, reply, account as MiLocalUser); } catch (err) { diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 987228be6..afc052261 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_showUserAccountMoveLogs from './endpoints/admin/show-user 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_updateUserName from './endpoints/admin/update-user-name.js'; @@ -475,6 +476,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/update-user-name', ep___admin_updateUserName], 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 000000000..53002a71f --- /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-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, + + @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, + userHost: user.host, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 2b9f5c6e3..8c7709796 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, @@ -552,6 +556,7 @@ export default class extends Endpoint { // eslint- privacyPolicyUrl: instance.privacyPolicyUrl, 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 f2c40541f..930bccef4 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -180,7 +180,7 @@ export const meta = { memo: { type: 'string', optional: false, nullable: true, - } + }, }, }, }, @@ -240,6 +240,8 @@ export default class extends Endpoint { // eslint- return { email: profile.email, emailVerified: profile.emailVerified, + 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 424212ba2..685da928e 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', 'approved'], default: 'all' }, origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, username: { type: 'string', nullable: true, default: null }, hostname: { @@ -64,6 +64,7 @@ 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 = 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 73b20bf42..6b4cb8521 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -65,6 +65,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 }, @@ -323,6 +324,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 e125b074f..ed9e29663 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -54,6 +54,7 @@ export const followersVisibilities = ['public', 'followers', 'private'] as const export const moderationLogTypes = [ 'updateServerSettings', 'suspend', + 'approve', 'unsuspend', 'updateUserName', 'updateUserNote', @@ -111,6 +112,11 @@ export type ModerationLogPayloads = { userUsername: string; userHost: string | null; }; + approve: { + userId: string; + userUsername: string; + userHost: string | null; + }; unsuspend: { userId: string; userUsername: string;