From e364e31e25bd1d4b8d801c845a96d6be708f0a18 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 19 Jan 2023 09:27:16 +0100 Subject: Implement signup approval in server --- .../middlewares/validators/user-registrations.ts | 203 +++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 server/middlewares/validators/user-registrations.ts (limited to 'server/middlewares/validators/user-registrations.ts') diff --git a/server/middlewares/validators/user-registrations.ts b/server/middlewares/validators/user-registrations.ts new file mode 100644 index 000000000..e263c27c5 --- /dev/null +++ b/server/middlewares/validators/user-registrations.ts @@ -0,0 +1,203 @@ +import express from 'express' +import { body, param, query, ValidationChain } from 'express-validator' +import { exists, isIdValid } from '@server/helpers/custom-validators/misc' +import { isRegistrationModerationResponseValid, isRegistrationReasonValid } from '@server/helpers/custom-validators/user-registration' +import { CONFIG } from '@server/initializers/config' +import { Hooks } from '@server/lib/plugins/hooks' +import { HttpStatusCode, UserRegister, UserRegistrationRequest, UserRegistrationState } from '@shared/models' +import { isUserDisplayNameValid, isUserPasswordValid, isUserUsernameValid } from '../../helpers/custom-validators/users' +import { isVideoChannelDisplayNameValid, isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels' +import { isSignupAllowed, isSignupAllowedForCurrentIP, SignupMode } from '../../lib/signup' +import { ActorModel } from '../../models/actor/actor' +import { areValidationErrors, checkUserNameOrEmailDoNotAlreadyExist } from './shared' +import { checkRegistrationHandlesDoNotAlreadyExist, checkRegistrationIdExist } from './shared/user-registrations' + +const usersDirectRegistrationValidator = usersCommonRegistrationValidatorFactory() + +const usersRequestRegistrationValidator = [ + ...usersCommonRegistrationValidatorFactory([ + body('registrationReason') + .custom(isRegistrationReasonValid) + ]), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + const body: UserRegistrationRequest = req.body + + if (CONFIG.SIGNUP.REQUIRES_APPROVAL !== true) { + return res.fail({ + status: HttpStatusCode.BAD_REQUEST_400, + message: 'Signup approval is not enabled on this instance' + }) + } + + const options = { username: body.username, email: body.email, channelHandle: body.channel?.name, res } + if (!await checkRegistrationHandlesDoNotAlreadyExist(options)) return + + return next() + } +] + +// --------------------------------------------------------------------------- + +function ensureUserRegistrationAllowedFactory (signupMode: SignupMode) { + return async (req: express.Request, res: express.Response, next: express.NextFunction) => { + const allowedParams = { + body: req.body, + ip: req.ip, + signupMode + } + + const allowedResult = await Hooks.wrapPromiseFun( + isSignupAllowed, + allowedParams, + + signupMode === 'direct-registration' + ? 'filter:api.user.signup.allowed.result' + : 'filter:api.user.request-signup.allowed.result' + ) + + if (allowedResult.allowed === false) { + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: allowedResult.errorMessage || 'User registration is not enabled, user limit is reached or registration requires approval.' + }) + } + + return next() + } +} + +const ensureUserRegistrationAllowedForIP = [ + (req: express.Request, res: express.Response, next: express.NextFunction) => { + const allowed = isSignupAllowedForCurrentIP(req.ip) + + if (allowed === false) { + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'You are not on a network authorized for registration.' + }) + } + + return next() + } +] + +// --------------------------------------------------------------------------- + +const acceptOrRejectRegistrationValidator = [ + param('registrationId') + .custom(isIdValid), + + body('moderationResponse') + .custom(isRegistrationModerationResponseValid), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + if (areValidationErrors(req, res)) return + if (!await checkRegistrationIdExist(req.params.registrationId, res)) return + + if (res.locals.userRegistration.state !== UserRegistrationState.PENDING) { + return res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'This registration is already accepted or rejected.' + }) + } + + return next() + } +] + +// --------------------------------------------------------------------------- + +const getRegistrationValidator = [ + param('registrationId') + .custom(isIdValid), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + if (areValidationErrors(req, res)) return + if (!await checkRegistrationIdExist(req.params.registrationId, res)) return + + return next() + } +] + +// --------------------------------------------------------------------------- + +const listRegistrationsValidator = [ + query('search') + .optional() + .custom(exists), + + (req: express.Request, res: express.Response, next: express.NextFunction) => { + if (areValidationErrors(req, res)) return + + return next() + } +] + +// --------------------------------------------------------------------------- + +export { + usersDirectRegistrationValidator, + usersRequestRegistrationValidator, + + ensureUserRegistrationAllowedFactory, + ensureUserRegistrationAllowedForIP, + + getRegistrationValidator, + listRegistrationsValidator, + + acceptOrRejectRegistrationValidator +} + +// --------------------------------------------------------------------------- + +function usersCommonRegistrationValidatorFactory (additionalValidationChain: ValidationChain[] = []) { + return [ + body('username') + .custom(isUserUsernameValid), + body('password') + .custom(isUserPasswordValid), + body('email') + .isEmail(), + body('displayName') + .optional() + .custom(isUserDisplayNameValid), + + body('channel.name') + .optional() + .custom(isVideoChannelUsernameValid), + body('channel.displayName') + .optional() + .custom(isVideoChannelDisplayNameValid), + + ...additionalValidationChain, + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + if (areValidationErrors(req, res, { omitBodyLog: true })) return + + const body: UserRegister | UserRegistrationRequest = req.body + + if (!await checkUserNameOrEmailDoNotAlreadyExist(body.username, body.email, res)) return + + if (body.channel) { + if (!body.channel.name || !body.channel.displayName) { + return res.fail({ message: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' }) + } + + if (body.channel.name === body.username) { + return res.fail({ message: 'Channel name cannot be the same as user username.' }) + } + + const existing = await ActorModel.loadLocalByName(body.channel.name) + if (existing) { + return res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: `Channel with name ${body.channel.name} already exists.` + }) + } + } + + return next() + } + ] +} -- cgit v1.2.3