From 2f2d88dcfc76bac6815d60fa9915b8e797853292 Mon Sep 17 00:00:00 2001 From: Mar0xy Date: Wed, 18 Oct 2023 02:41:36 +0200 Subject: add: Require Approval for Signup --- packages/backend/src/server/api/EndpointsModule.ts | 4 ++ .../backend/src/server/api/SigninApiService.ts | 20 +++++++ .../backend/src/server/api/SignupApiService.ts | 32 ++++++++++++ packages/backend/src/server/api/endpoints.ts | 2 + .../src/server/api/endpoints/admin/approve-user.ts | 61 ++++++++++++++++++++++ .../backend/src/server/api/endpoints/admin/meta.ts | 5 ++ .../src/server/api/endpoints/admin/show-user.ts | 2 + .../src/server/api/endpoints/admin/update-meta.ts | 5 ++ packages/backend/src/server/api/endpoints/meta.ts | 5 ++ 9 files changed, 136 insertions(+) create mode 100644 packages/backend/src/server/api/endpoints/admin/approve-user.ts (limited to 'packages/backend/src/server') diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 9aa75b0af8..079040b85f 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -62,6 +62,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'; @@ -415,6 +416,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 }; @@ -772,6 +774,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $admin_showUser, $admin_showUsers, $admin_suspendUser, + $admin_approveUser, $admin_unsuspendUser, $admin_updateMeta, $admin_deleteAccount, @@ -1123,6 +1126,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $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 2d4de605ea..fd247df22a 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -21,6 +21,7 @@ import { IdService } from '@/core/IdService.js'; import { bindThis } from '@/decorators.js'; import { WebAuthnService } from '@/core/WebAuthnService.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import { MetaService } from '@/core/MetaService.js'; import { RateLimiterService } from './RateLimiterService.js'; import { SigninService } from './SigninService.js'; import type { AuthenticationResponseJSON } from '@simplewebauthn/typescript-types'; @@ -46,6 +47,7 @@ export class SigninApiService { private signinService: SigninService, private userAuthService: UserAuthService, private webAuthnService: WebAuthnService, + private metaService: MetaService, ) { } @@ -64,6 +66,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']; @@ -123,6 +127,17 @@ export class SigninApiService { const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + 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 argon2.verify(profile.password!, password) || bcrypt.compareSync(password, profile.password!); @@ -147,6 +162,8 @@ export class SigninApiService { password: newHash }); } + if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true }); + return this.signinService.signin(request, reply, user); } else { return await fail(403, { @@ -176,6 +193,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) { @@ -187,6 +206,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 02e5cd4fdf..53f770e172 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -22,6 +22,7 @@ import { bindThis } from '@/decorators.js'; import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; import { SigninService } from './SigninService.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; +import instance from './endpoints/charts/instance.js'; @Injectable() export class SignupApiService { @@ -63,6 +64,7 @@ export class SignupApiService { host?: string; invitationCode?: string; emailAddress?: string; + reason?: string; 'hcaptcha-response'?: string; 'g-recaptcha-response'?: string; 'turnstile-response'?: string; @@ -100,6 +102,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) { @@ -115,6 +118,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) { @@ -170,6 +180,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}`; @@ -185,6 +196,19 @@ 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.'); + } + reply.code(204); return; } else { @@ -222,12 +246,15 @@ export class SignupApiService { const code = body['code']; + const instance = await this.metaService.fetch(true); + try { const pendingUser = await this.userPendingsRepository.findOneByOrFail({ code }); const { account, secret } = await this.signupService.signup({ username: pendingUser.username, passwordHash: pendingUser.password, + reason: pendingUser.reason, }); this.userPendingsRepository.delete({ @@ -250,6 +277,11 @@ export class SignupApiService { pendingUserId: null, }); } + + if (instance.approvalRequiredForSignup) { + reply.code(204); + return; + } 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 d7f5611f55..e978093364 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -62,6 +62,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'; @@ -413,6 +414,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 0000000000..0ea656ddaf --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/approve-user.ts @@ -0,0 +1,61 @@ +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, +} 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 88725ddbbf..763c4ea807 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -32,6 +32,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + approvalRequiredForSignup: { + type: 'boolean', + optional: false, nullable: false, + }, enableHcaptcha: { type: 'boolean', optional: false, nullable: false, @@ -353,6 +357,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, enableRecaptcha: instance.enableRecaptcha, 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 f550c4fd28..5ad90f48b4 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -73,6 +73,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/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 40cc50fe7c..be14037a76 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -59,6 +59,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 }, @@ -249,6 +250,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/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index d37919b474..dbd72763bf 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -100,6 +100,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + approvalRequiredForSignup: { + type: 'boolean', + optional: false, nullable: false, + }, enableHcaptcha: { type: 'boolean', optional: false, nullable: false, @@ -308,6 +312,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, enableRecaptcha: instance.enableRecaptcha, -- cgit v1.2.3-freya