signup:
enabled: false
+
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
+
minimum_age: 16 # Used to configure the signup form
+
+ # Users fill a form to register so moderators can accept/reject the registration
+ requires_approval: true
requires_email_verification: false
+
filters:
cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist
whitelist: []
signup:
enabled: false
+
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
+
minimum_age: 16 # Used to configure the signup form
+
+ # Users fill a form to register so moderators can accept/reject the registration
+ requires_approval: true
requires_email_verification: false
+
filters:
cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist
whitelist: []
signup:
enabled: true
+ requires_approval: false
requires_email_verification: false
transcoding:
signup: {
enabled: CONFIG.SIGNUP.ENABLED,
limit: CONFIG.SIGNUP.LIMIT,
+ requiresApproval: CONFIG.SIGNUP.REQUIRES_APPROVAL,
requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION,
minimumAge: CONFIG.SIGNUP.MINIMUM_AGE
},
--- /dev/null
+import express from 'express'
+import { HttpStatusCode } from '@shared/models'
+import { CONFIG } from '../../../initializers/config'
+import { sendVerifyRegistrationEmail, sendVerifyUserEmail } from '../../../lib/user'
+import { asyncMiddleware, buildRateLimiter } from '../../../middlewares'
+import {
+ registrationVerifyEmailValidator,
+ usersAskSendVerifyEmailValidator,
+ usersVerifyEmailValidator
+} from '../../../middlewares/validators'
+
+const askSendEmailLimiter = buildRateLimiter({
+ windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
+ max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX
+})
+
+const emailVerificationRouter = express.Router()
+
+emailVerificationRouter.post([ '/ask-send-verify-email', '/registrations/ask-send-verify-email' ],
+ askSendEmailLimiter,
+ asyncMiddleware(usersAskSendVerifyEmailValidator),
+ asyncMiddleware(reSendVerifyUserEmail)
+)
+
+emailVerificationRouter.post('/:id/verify-email',
+ asyncMiddleware(usersVerifyEmailValidator),
+ asyncMiddleware(verifyUserEmail)
+)
+
+emailVerificationRouter.post('/registrations/:registrationId/verify-email',
+ asyncMiddleware(registrationVerifyEmailValidator),
+ asyncMiddleware(verifyRegistrationEmail)
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+ emailVerificationRouter
+}
+
+async function reSendVerifyUserEmail (req: express.Request, res: express.Response) {
+ const user = res.locals.user
+ const registration = res.locals.userRegistration
+
+ if (user) await sendVerifyUserEmail(user)
+ else if (registration) await sendVerifyRegistrationEmail(registration)
+
+ return res.status(HttpStatusCode.NO_CONTENT_204).end()
+}
+
+async function verifyUserEmail (req: express.Request, res: express.Response) {
+ const user = res.locals.user
+ user.emailVerified = true
+
+ if (req.body.isPendingEmail === true) {
+ user.email = user.pendingEmail
+ user.pendingEmail = null
+ }
+
+ await user.save()
+
+ return res.status(HttpStatusCode.NO_CONTENT_204).end()
+}
+
+async function verifyRegistrationEmail (req: express.Request, res: express.Response) {
+ const registration = res.locals.userRegistration
+ registration.emailVerified = true
+
+ await registration.save()
+
+ return res.status(HttpStatusCode.NO_CONTENT_204).end()
+}
import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
import { MUserAccountDefault } from '@server/types/models'
import { pick } from '@shared/core-utils'
-import { HttpStatusCode, UserCreate, UserCreateResult, UserRegister, UserRight, UserUpdate } from '@shared/models'
+import { HttpStatusCode, UserCreate, UserCreateResult, UserRight, UserUpdate } from '@shared/models'
import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
import { logger } from '../../../helpers/logger'
import { generateRandomString, getFormattedObjects } from '../../../helpers/utils'
-import { CONFIG } from '../../../initializers/config'
import { WEBSERVER } from '../../../initializers/constants'
import { sequelizeTypescript } from '../../../initializers/database'
import { Emailer } from '../../../lib/emailer'
-import { Notifier } from '../../../lib/notifier'
import { Redis } from '../../../lib/redis'
-import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
+import { buildUser, createUserAccountAndChannelAndPlaylist } from '../../../lib/user'
import {
adminUsersSortValidator,
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
- buildRateLimiter,
ensureUserHasRight,
- ensureUserRegistrationAllowed,
- ensureUserRegistrationAllowedForIP,
paginationValidator,
setDefaultPagination,
setDefaultSort,
usersAddValidator,
usersGetValidator,
usersListValidator,
- usersRegisterValidator,
usersRemoveValidator,
usersUpdateValidator
} from '../../../middlewares'
import {
ensureCanModerateUser,
usersAskResetPasswordValidator,
- usersAskSendVerifyEmailValidator,
usersBlockingValidator,
- usersResetPasswordValidator,
- usersVerifyEmailValidator
+ usersResetPasswordValidator
} from '../../../middlewares/validators'
import { UserModel } from '../../../models/user/user'
+import { emailVerificationRouter } from './email-verification'
import { meRouter } from './me'
import { myAbusesRouter } from './my-abuses'
import { myBlocklistRouter } from './my-blocklist'
import { myNotificationsRouter } from './my-notifications'
import { mySubscriptionsRouter } from './my-subscriptions'
import { myVideoPlaylistsRouter } from './my-video-playlists'
+import { registrationsRouter } from './registrations'
import { twoFactorRouter } from './two-factor'
const auditLogger = auditLoggerFactory('users')
-const signupRateLimiter = buildRateLimiter({
- windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS,
- max: CONFIG.RATES_LIMIT.SIGNUP.MAX,
- skipFailedRequests: true
-})
-
-const askSendEmailLimiter = buildRateLimiter({
- windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
- max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX
-})
-
const usersRouter = express.Router()
+usersRouter.use('/', emailVerificationRouter)
+usersRouter.use('/', registrationsRouter)
usersRouter.use('/', twoFactorRouter)
usersRouter.use('/', tokensRouter)
usersRouter.use('/', myNotificationsRouter)
asyncRetryTransactionMiddleware(createUser)
)
-usersRouter.post('/register',
- signupRateLimiter,
- asyncMiddleware(ensureUserRegistrationAllowed),
- ensureUserRegistrationAllowedForIP,
- asyncMiddleware(usersRegisterValidator),
- asyncRetryTransactionMiddleware(registerUser)
-)
-
usersRouter.put('/:id',
authenticate,
ensureUserHasRight(UserRight.MANAGE_USERS),
asyncMiddleware(resetUserPassword)
)
-usersRouter.post('/ask-send-verify-email',
- askSendEmailLimiter,
- asyncMiddleware(usersAskSendVerifyEmailValidator),
- asyncMiddleware(reSendVerifyUserEmail)
-)
-
-usersRouter.post('/:id/verify-email',
- asyncMiddleware(usersVerifyEmailValidator),
- asyncMiddleware(verifyUserEmail)
-)
-
// ---------------------------------------------------------------------------
export {
})
}
-async function registerUser (req: express.Request, res: express.Response) {
- const body: UserRegister = req.body
-
- const userToCreate = buildUser({
- ...pick(body, [ 'username', 'password', 'email' ]),
-
- emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null
- })
-
- const { user, account, videoChannel } = await createUserAccountAndChannelAndPlaylist({
- userToCreate,
- userDisplayName: body.displayName || undefined,
- channelNames: body.channel
- })
-
- auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON()))
- logger.info('User %s with its channel and account registered.', body.username)
-
- if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
- await sendVerifyUserEmail(user)
- }
-
- Notifier.Instance.notifyOnNewUserRegistration(user)
-
- Hooks.runAction('action:api.user.registered', { body, user, account, videoChannel, req, res })
-
- return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
-}
-
async function unblockUser (req: express.Request, res: express.Response) {
const user = res.locals.user
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
-async function reSendVerifyUserEmail (req: express.Request, res: express.Response) {
- const user = res.locals.user
-
- await sendVerifyUserEmail(user)
-
- return res.status(HttpStatusCode.NO_CONTENT_204).end()
-}
-
-async function verifyUserEmail (req: express.Request, res: express.Response) {
- const user = res.locals.user
- user.emailVerified = true
-
- if (req.body.isPendingEmail === true) {
- user.email = user.pendingEmail
- user.pendingEmail = null
- }
-
- await user.save()
-
- return res.status(HttpStatusCode.NO_CONTENT_204).end()
-}
-
async function changeUserBlock (res: express.Response, user: MUserAccountDefault, block: boolean, reason?: string) {
const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
--- /dev/null
+import express from 'express'
+import { Emailer } from '@server/lib/emailer'
+import { Hooks } from '@server/lib/plugins/hooks'
+import { UserRegistrationModel } from '@server/models/user/user-registration'
+import { pick } from '@shared/core-utils'
+import { HttpStatusCode, UserRegister, UserRegistrationRequest, UserRegistrationState, UserRight } from '@shared/models'
+import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger'
+import { logger } from '../../../helpers/logger'
+import { CONFIG } from '../../../initializers/config'
+import { Notifier } from '../../../lib/notifier'
+import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyRegistrationEmail, sendVerifyUserEmail } from '../../../lib/user'
+import {
+ acceptOrRejectRegistrationValidator,
+ asyncMiddleware,
+ asyncRetryTransactionMiddleware,
+ authenticate,
+ buildRateLimiter,
+ ensureUserHasRight,
+ ensureUserRegistrationAllowedFactory,
+ ensureUserRegistrationAllowedForIP,
+ getRegistrationValidator,
+ listRegistrationsValidator,
+ paginationValidator,
+ setDefaultPagination,
+ setDefaultSort,
+ userRegistrationsSortValidator,
+ usersDirectRegistrationValidator,
+ usersRequestRegistrationValidator
+} from '../../../middlewares'
+
+const auditLogger = auditLoggerFactory('users')
+
+const registrationRateLimiter = buildRateLimiter({
+ windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS,
+ max: CONFIG.RATES_LIMIT.SIGNUP.MAX,
+ skipFailedRequests: true
+})
+
+const registrationsRouter = express.Router()
+
+registrationsRouter.post('/registrations/request',
+ registrationRateLimiter,
+ asyncMiddleware(ensureUserRegistrationAllowedFactory('request-registration')),
+ ensureUserRegistrationAllowedForIP,
+ asyncMiddleware(usersRequestRegistrationValidator),
+ asyncRetryTransactionMiddleware(requestRegistration)
+)
+
+registrationsRouter.post('/registrations/:registrationId/accept',
+ authenticate,
+ ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS),
+ asyncMiddleware(acceptOrRejectRegistrationValidator),
+ asyncRetryTransactionMiddleware(acceptRegistration)
+)
+registrationsRouter.post('/registrations/:registrationId/reject',
+ authenticate,
+ ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS),
+ asyncMiddleware(acceptOrRejectRegistrationValidator),
+ asyncRetryTransactionMiddleware(rejectRegistration)
+)
+
+registrationsRouter.delete('/registrations/:registrationId',
+ authenticate,
+ ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS),
+ asyncMiddleware(getRegistrationValidator),
+ asyncRetryTransactionMiddleware(deleteRegistration)
+)
+
+registrationsRouter.get('/registrations',
+ authenticate,
+ ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS),
+ paginationValidator,
+ userRegistrationsSortValidator,
+ setDefaultSort,
+ setDefaultPagination,
+ listRegistrationsValidator,
+ asyncMiddleware(listRegistrations)
+)
+
+registrationsRouter.post('/register',
+ registrationRateLimiter,
+ asyncMiddleware(ensureUserRegistrationAllowedFactory('direct-registration')),
+ ensureUserRegistrationAllowedForIP,
+ asyncMiddleware(usersDirectRegistrationValidator),
+ asyncRetryTransactionMiddleware(registerUser)
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+ registrationsRouter
+}
+
+// ---------------------------------------------------------------------------
+
+async function requestRegistration (req: express.Request, res: express.Response) {
+ const body: UserRegistrationRequest = req.body
+
+ const registration = new UserRegistrationModel({
+ ...pick(body, [ 'username', 'password', 'email', 'registrationReason' ]),
+
+ accountDisplayName: body.displayName,
+ channelDisplayName: body.channel?.displayName,
+ channelHandle: body.channel?.name,
+
+ state: UserRegistrationState.PENDING,
+
+ emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null
+ })
+
+ await registration.save()
+
+ if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
+ await sendVerifyRegistrationEmail(registration)
+ }
+
+ Notifier.Instance.notifyOnNewRegistrationRequest(registration)
+
+ Hooks.runAction('action:api.user.requested-registration', { body, registration, req, res })
+
+ return res.json(registration.toFormattedJSON())
+}
+
+// ---------------------------------------------------------------------------
+
+async function acceptRegistration (req: express.Request, res: express.Response) {
+ const registration = res.locals.userRegistration
+
+ const userToCreate = buildUser({
+ username: registration.username,
+ password: registration.password,
+ email: registration.email,
+ emailVerified: registration.emailVerified
+ })
+ // We already encrypted password in registration model
+ userToCreate.skipPasswordEncryption = true
+
+ // TODO: handle conflicts if someone else created a channel handle/user handle/user email between registration and approval
+
+ const { user } = await createUserAccountAndChannelAndPlaylist({
+ userToCreate,
+ userDisplayName: registration.accountDisplayName,
+ channelNames: registration.channelHandle && registration.channelDisplayName
+ ? {
+ name: registration.channelHandle,
+ displayName: registration.channelDisplayName
+ }
+ : undefined
+ })
+
+ registration.userId = user.id
+ registration.state = UserRegistrationState.ACCEPTED
+ registration.moderationResponse = req.body.moderationResponse
+
+ await registration.save()
+
+ logger.info('Registration of %s accepted', registration.username)
+
+ Emailer.Instance.addUserRegistrationRequestProcessedJob(registration)
+
+ return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
+}
+
+async function rejectRegistration (req: express.Request, res: express.Response) {
+ const registration = res.locals.userRegistration
+
+ registration.state = UserRegistrationState.REJECTED
+ registration.moderationResponse = req.body.moderationResponse
+
+ await registration.save()
+
+ Emailer.Instance.addUserRegistrationRequestProcessedJob(registration)
+
+ logger.info('Registration of %s rejected', registration.username)
+
+ return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
+}
+
+// ---------------------------------------------------------------------------
+
+async function deleteRegistration (req: express.Request, res: express.Response) {
+ const registration = res.locals.userRegistration
+
+ await registration.destroy()
+
+ logger.info('Registration of %s deleted', registration.username)
+
+ return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
+}
+
+// ---------------------------------------------------------------------------
+
+async function listRegistrations (req: express.Request, res: express.Response) {
+ const resultList = await UserRegistrationModel.listForApi({
+ start: req.query.start,
+ count: req.query.count,
+ sort: req.query.sort,
+ search: req.query.search
+ })
+
+ return res.json({
+ total: resultList.total,
+ data: resultList.data.map(d => d.toFormattedJSON())
+ })
+}
+
+// ---------------------------------------------------------------------------
+
+async function registerUser (req: express.Request, res: express.Response) {
+ const body: UserRegister = req.body
+
+ const userToCreate = buildUser({
+ ...pick(body, [ 'username', 'password', 'email' ]),
+
+ emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null
+ })
+
+ const { user, account, videoChannel } = await createUserAccountAndChannelAndPlaylist({
+ userToCreate,
+ userDisplayName: body.displayName || undefined,
+ channelNames: body.channel
+ })
+
+ auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON()))
+ logger.info('User %s with its channel and account registered.', body.username)
+
+ if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
+ await sendVerifyUserEmail(user)
+ }
+
+ Notifier.Instance.notifyOnNewDirectRegistration(user)
+
+ Hooks.runAction('action:api.user.registered', { body, user, account, videoChannel, req, res })
+
+ return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
+}
--- /dev/null
+import validator from 'validator'
+import { CONSTRAINTS_FIELDS, USER_REGISTRATION_STATES } from '../../initializers/constants'
+import { exists } from './misc'
+
+const USER_REGISTRATIONS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USER_REGISTRATIONS
+
+function isRegistrationStateValid (value: string) {
+ return exists(value) && USER_REGISTRATION_STATES[value] !== undefined
+}
+
+function isRegistrationModerationResponseValid (value: string) {
+ return exists(value) && validator.isLength(value, USER_REGISTRATIONS_CONSTRAINTS_FIELDS.MODERATOR_MESSAGE)
+}
+
+function isRegistrationReasonValid (value: string) {
+ return exists(value) && validator.isLength(value, USER_REGISTRATIONS_CONSTRAINTS_FIELDS.REASON_MESSAGE)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ isRegistrationStateValid,
+ isRegistrationModerationResponseValid,
+ isRegistrationReasonValid
+}
throw new Error('Emailer is disabled but you require signup email verification.')
}
+ if (CONFIG.SIGNUP.ENABLED && CONFIG.SIGNUP.REQUIRES_APPROVAL) {
+ // eslint-disable-next-line max-len
+ logger.warn('Emailer is disabled but signup approval is enabled: PeerTube will not be able to send an email to the user upon acceptance/rejection of the registration request')
+ }
+
if (CONFIG.CONTACT_FORM.ENABLED) {
logger.warn('Emailer is disabled so the contact form will not work.')
}
'csp.enabled', 'csp.report_only', 'csp.report_uri',
'security.frameguard.enabled',
'cache.previews.size', 'cache.captions.size', 'cache.torrents.size', 'admin.email', 'contact_form.enabled',
- 'signup.enabled', 'signup.limit', 'signup.requires_email_verification', 'signup.minimum_age',
+ 'signup.enabled', 'signup.limit', 'signup.requires_approval', 'signup.requires_email_verification', 'signup.minimum_age',
'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
'redundancy.videos.strategies', 'redundancy.videos.check_interval',
'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 'transcoding.hls.enabled',
},
SIGNUP: {
get ENABLED () { return config.get<boolean>('signup.enabled') },
+ get REQUIRES_APPROVAL () { return config.get<boolean>('signup.requires_approval') },
get LIMIT () { return config.get<number>('signup.limit') },
get REQUIRES_EMAIL_VERIFICATION () { return config.get<boolean>('signup.requires_email_verification') },
get MINIMUM_AGE () { return config.get<number>('signup.minimum_age') },
import {
AbuseState,
JobType,
+ UserRegistrationState,
VideoChannelSyncState,
VideoImportState,
VideoPrivacy,
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 745
+const LAST_MIGRATION_VERSION = 750
// ---------------------------------------------------------------------------
ACCOUNT_FOLLOWERS: [ 'createdAt' ],
CHANNEL_FOLLOWERS: [ 'createdAt' ],
+ USER_REGISTRATIONS: [ 'createdAt', 'state' ],
+
VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot', 'best' ],
// Don't forget to update peertube-search-index with the same values
ABUSE_MESSAGES: {
MESSAGE: { min: 2, max: 3000 } // Length
},
+ USER_REGISTRATIONS: {
+ REASON_MESSAGE: { min: 2, max: 3000 }, // Length
+ MODERATOR_MESSAGE: { min: 2, max: 3000 } // Length
+ },
VIDEO_BLACKLIST: {
REASON: { min: 2, max: 300 } // Length
},
[AbuseState.ACCEPTED]: 'Accepted'
}
+const USER_REGISTRATION_STATES: { [ id in UserRegistrationState ]: string } = {
+ [UserRegistrationState.PENDING]: 'Pending',
+ [UserRegistrationState.REJECTED]: 'Rejected',
+ [UserRegistrationState.ACCEPTED]: 'Accepted'
+}
+
const VIDEO_PLAYLIST_PRIVACIES: { [ id in VideoPlaylistPrivacy ]: string } = {
[VideoPlaylistPrivacy.PUBLIC]: 'Public',
[VideoPlaylistPrivacy.UNLISTED]: 'Unlisted',
const TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME = 60000 * 10 // 10 minutes
-const USER_EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes
+const EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes
const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = {
DO_NOT_LIST: 'do_not_list',
VIDEO_TRANSCODING_FPS,
FFMPEG_NICE,
ABUSE_STATES,
+ USER_REGISTRATION_STATES,
LRU_CACHE,
REQUEST_TIMEOUTS,
MAX_LOCAL_VIEWER_WATCH_SECTIONS,
USER_PASSWORD_RESET_LIFETIME,
USER_PASSWORD_CREATE_LIFETIME,
MEMOIZE_TTL,
- USER_EMAIL_VERIFY_LIFETIME,
+ EMAIL_VERIFY_LIFETIME,
OVERVIEWS,
SCHEDULER_INTERVALS_MS,
REPEAT_JOBS,
import { VideoTrackerModel } from '@server/models/server/video-tracker'
import { UserModel } from '@server/models/user/user'
import { UserNotificationModel } from '@server/models/user/user-notification'
+import { UserRegistrationModel } from '@server/models/user/user-registration'
import { UserVideoHistoryModel } from '@server/models/user/user-video-history'
+import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync'
import { VideoJobInfoModel } from '@server/models/video/video-job-info'
import { VideoLiveSessionModel } from '@server/models/video/video-live-session'
import { VideoSourceModel } from '@server/models/video/video-source'
import { VideoTagModel } from '../models/video/video-tag'
import { VideoViewModel } from '../models/view/video-view'
import { CONFIG } from './config'
-import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync'
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
PluginModel,
ActorCustomPageModel,
VideoJobInfoModel,
- VideoChannelSyncModel
+ VideoChannelSyncModel,
+ UserRegistrationModel
])
// Check extensions exist in the database
--- /dev/null
+
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction
+ queryInterface: Sequelize.QueryInterface
+ sequelize: Sequelize.Sequelize
+ db: any
+}): Promise<void> {
+ {
+ const query = `
+ CREATE TABLE IF NOT EXISTS "userRegistration" (
+ "id" serial,
+ "state" integer NOT NULL,
+ "registrationReason" text NOT NULL,
+ "moderationResponse" text,
+ "password" varchar(255),
+ "username" varchar(255) NOT NULL,
+ "email" varchar(400) NOT NULL,
+ "emailVerified" boolean,
+ "accountDisplayName" varchar(255),
+ "channelHandle" varchar(255),
+ "channelDisplayName" varchar(255),
+ "userId" integer REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
+ "createdAt" timestamp with time zone NOT NULL,
+ "updatedAt" timestamp with time zone NOT NULL,
+ PRIMARY KEY ("id")
+ );
+ `
+ await utils.sequelize.query(query, { transaction: utils.transaction })
+ }
+
+ {
+ await utils.queryInterface.addColumn('userNotification', 'userRegistrationId', {
+ type: Sequelize.INTEGER,
+ defaultValue: null,
+ allowNull: true,
+ references: {
+ model: 'userRegistration',
+ key: 'id'
+ },
+ onUpdate: 'CASCADE',
+ onDelete: 'SET NULL'
+ }, { transaction: utils.transaction })
+ }
+}
+
+async function down (utils: {
+ queryInterface: Sequelize.QueryInterface
+ transaction: Sequelize.Transaction
+}) {
+ await utils.queryInterface.dropTable('videoChannelSync', { transaction: utils.transaction })
+}
+
+export {
+ up,
+ down
+}
import { randomBytesPromise } from '@server/helpers/core-utils'
import { isOTPValid } from '@server/helpers/otp'
import { CONFIG } from '@server/initializers/config'
+import { UserRegistrationModel } from '@server/models/user/user-registration'
import { MOAuthClient } from '@server/types/models'
import { sha1 } from '@shared/extra-utils'
-import { HttpStatusCode } from '@shared/models'
+import { HttpStatusCode, ServerErrorCode, UserRegistrationState } from '@shared/models'
import { OTP } from '../../initializers/constants'
import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model'
class MissingTwoFactorError extends Error {
code = HttpStatusCode.UNAUTHORIZED_401
- name = 'missing_two_factor'
+ name = ServerErrorCode.MISSING_TWO_FACTOR
}
class InvalidTwoFactorError extends Error {
code = HttpStatusCode.BAD_REQUEST_400
- name = 'invalid_two_factor'
+ name = ServerErrorCode.INVALID_TWO_FACTOR
+}
+
+class RegistrationWaitingForApproval extends Error {
+ code = HttpStatusCode.BAD_REQUEST_400
+ name = ServerErrorCode.ACCOUNT_WAITING_FOR_APPROVAL
+}
+
+class RegistrationApprovalRejected extends Error {
+ code = HttpStatusCode.BAD_REQUEST_400
+ name = ServerErrorCode.ACCOUNT_APPROVAL_REJECTED
}
/**
}
const user = await getUser(request.body.username, request.body.password, bypassLogin)
- if (!user) throw new InvalidGrantError('Invalid grant: user credentials are invalid')
+ if (!user) {
+ const registration = await UserRegistrationModel.loadByEmailOrUsername(request.body.username)
+
+ if (registration?.state === UserRegistrationState.REJECTED) {
+ throw new RegistrationApprovalRejected('Registration approval for this account has been rejected')
+ } else if (registration?.state === UserRegistrationState.PENDING) {
+ throw new RegistrationWaitingForApproval('Registration for this account is awaiting approval')
+ }
+
+ throw new InvalidGrantError('Invalid grant: user credentials are invalid')
+ }
if (user.otpSecret) {
if (!request.headers[OTP.HEADER_NAME]) {
import { createTransport, Transporter } from 'nodemailer'
import { join } from 'path'
import { arrayify, root } from '@shared/core-utils'
-import { EmailPayload } from '@shared/models'
+import { EmailPayload, UserRegistrationState } from '@shared/models'
import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model'
import { isTestOrDevInstance } from '../helpers/core-utils'
import { bunyanLogger, logger } from '../helpers/logger'
import { CONFIG, isEmailEnabled } from '../initializers/config'
import { WEBSERVER } from '../initializers/constants'
-import { MUser } from '../types/models'
+import { MRegistration, MUser } from '../types/models'
import { JobQueue } from './job-queue'
const Email = require('email-templates')
subject: 'Reset your account password',
locals: {
username,
- resetPasswordUrl
+ resetPasswordUrl,
+
+ hideNotificationPreferencesLink: true
}
}
subject: 'Create your account password',
locals: {
username,
- createPasswordUrl
+ createPasswordUrl,
+
+ hideNotificationPreferencesLink: true
}
}
return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
}
- addVerifyEmailJob (username: string, to: string, verifyEmailUrl: string) {
+ addVerifyEmailJob (options: {
+ username: string
+ isRegistrationRequest: boolean
+ to: string
+ verifyEmailUrl: string
+ }) {
+ const { username, isRegistrationRequest, to, verifyEmailUrl } = options
+
const emailPayload: EmailPayload = {
template: 'verify-email',
to: [ to ],
subject: `Verify your email on ${CONFIG.INSTANCE.NAME}`,
locals: {
username,
- verifyEmailUrl
+ verifyEmailUrl,
+ isRegistrationRequest,
+
+ hideNotificationPreferencesLink: true
}
}
body,
// There are not notification preferences for the contact form
- hideNotificationPreferences: true
+ hideNotificationPreferencesLink: true
+ }
+ }
+
+ return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
+ }
+
+ addUserRegistrationRequestProcessedJob (registration: MRegistration) {
+ let template: string
+ let subject: string
+ if (registration.state === UserRegistrationState.ACCEPTED) {
+ template = 'user-registration-request-accepted'
+ subject = `Your registration request for ${registration.username} has been accepted`
+ } else {
+ template = 'user-registration-request-rejected'
+ subject = `Your registration request for ${registration.username} has been rejected`
+ }
+
+ const to = registration.email
+ const emailPayload: EmailPayload = {
+ to: [ to ],
+ template,
+ subject,
+ locals: {
+ username: registration.username,
+ moderationResponse: registration.moderationResponse,
+ loginLink: WEBSERVER.URL + '/login'
}
}
td(aria-hidden='true' height='20' style='font-size: 0px; line-height: 0px;')
br
//- Clear Spacer : END
- //- 1 Column Text : BEGIN
- if username
- tr
- td(style='background-color: #cccccc;')
- table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%')
- tr
- td(style='padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;')
- p(style='margin: 0;')
- | You are receiving this email as part of your notification settings on #{instanceName} for your account #{username}.
- //- 1 Column Text : END
//- Email Body : END
//- Email Footer : BEGIN
- unless hideNotificationPreferences
+ unless hideNotificationPreferencesLink
table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;')
tr
td(style='padding: 20px; padding-bottom: 0px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;')
--- /dev/null
+extends ../common/greetings
+
+block title
+ | Congratulation #{username}, your registration request has been accepted!
+
+block content
+ p Your registration request has been accepted.
+ p Moderators sent you the following message:
+ blockquote(style='white-space: pre-wrap') #{moderationResponse}
+ p Your account has been created and you can login on #[a(href=loginLink) #{loginLink}]
--- /dev/null
+extends ../common/greetings
+
+block title
+ | Registration request of your account #{username} has rejected
+
+block content
+ p Your registration request has been rejected.
+ p Moderators sent you the following message:
+ blockquote(style='white-space: pre-wrap') #{moderationResponse}
--- /dev/null
+extends ../common/greetings
+
+block title
+ | A new user wants to register
+
+block content
+ p User #{registration.username} wants to register on your PeerTube instance with the following reason:
+ blockquote(style='white-space: pre-wrap') #{registration.registrationReason}
+ p You can accept or reject the registration request in the #[a(href=`${WEBSERVER.URL}/admin/moderation/registrations/list`) administration].
extends ../common/greetings
block title
- | Account verification
+ | Email verification
block content
- p Welcome to #{instanceName}!
- p.
- You just created an account at #[a(href=WEBSERVER.URL) #{instanceName}].
- Your username there is: #{username}.
- p.
- To start using your account you must verify your email first!
- Please follow #[a(href=verifyEmailUrl) this link] to verify this email belongs to you.
- p.
- If you can't see the verification link above you can use the following link #[a(href=verifyEmailUrl) #{verifyEmailUrl}]
- p.
- If you are not the person who initiated this request, please ignore this email.
+ if isRegistrationRequest
+ p You just requested an account on #[a(href=WEBSERVER.URL) #{instanceName}].
+ else
+ p You just created an account on #[a(href=WEBSERVER.URL) #{instanceName}].
+
+ if isRegistrationRequest
+ p To complete your registration request you must verify your email first!
+ else
+ p To start using your account you must verify your email first!
+
+ p Please follow #[a(href=verifyEmailUrl) this link] to verify this email belongs to you.
+ p If you can't see the verification link above you can use the following link #[a(href=verifyEmailUrl) #{verifyEmailUrl}]
+ p If you are not the person who initiated this request, please ignore this email.
-import { MUser, MUserDefault } from '@server/types/models/user'
+import { MRegistration, MUser, MUserDefault } from '@server/types/models/user'
import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
import { UserNotificationSettingValue } from '../../../shared/models/users'
import { logger } from '../../helpers/logger'
AbuseStateChangeForReporter,
AutoFollowForInstance,
CommentMention,
+ DirectRegistrationForModerators,
FollowForInstance,
FollowForUser,
ImportFinishedForOwner,
OwnedPublicationAfterAutoUnblacklist,
OwnedPublicationAfterScheduleUpdate,
OwnedPublicationAfterTranscoding,
- RegistrationForModerators,
+ RegistrationRequestForModerators,
StudioEditionFinishedForOwner,
UnblacklistForOwner
} from './shared'
newBlacklist: [ NewBlacklistForOwner ],
unblacklist: [ UnblacklistForOwner ],
importFinished: [ ImportFinishedForOwner ],
- userRegistration: [ RegistrationForModerators ],
+ directRegistration: [ DirectRegistrationForModerators ],
+ registrationRequest: [ RegistrationRequestForModerators ],
userFollow: [ FollowForUser ],
instanceFollow: [ FollowForInstance ],
autoInstanceFollow: [ AutoFollowForInstance ],
})
}
- notifyOnNewUserRegistration (user: MUserDefault): void {
- const models = this.notificationModels.userRegistration
+ notifyOnNewDirectRegistration (user: MUserDefault): void {
+ const models = this.notificationModels.directRegistration
this.sendNotifications(models, user)
.catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err }))
}
+ notifyOnNewRegistrationRequest (registration: MRegistration): void {
+ const models = this.notificationModels.registrationRequest
+
+ this.sendNotifications(models, registration)
+ .catch(err => logger.error('Cannot notify moderators of new registration request (%s).', registration.username, { err }))
+ }
+
notifyOfNewUserFollow (actorFollow: MActorFollowFull): void {
const models = this.notificationModels.userFollow
import { UserNotificationType, UserRight } from '@shared/models'
import { AbstractNotification } from '../common/abstract-notification'
-export class RegistrationForModerators extends AbstractNotification <MUserDefault> {
+export class DirectRegistrationForModerators extends AbstractNotification <MUserDefault> {
private moderators: MUserDefault[]
async prepare () {
return {
template: 'user-registered',
to,
- subject: `a new user registered on ${CONFIG.INSTANCE.NAME}: ${this.payload.username}`,
+ subject: `A new user registered on ${CONFIG.INSTANCE.NAME}: ${this.payload.username}`,
locals: {
user: this.payload
}
export * from './new-peertube-version-for-admins'
export * from './new-plugin-version-for-admins'
-export * from './registration-for-moderators'
+export * from './direct-registration-for-moderators'
+export * from './registration-request-for-moderators'
--- /dev/null
+import { logger } from '@server/helpers/logger'
+import { UserModel } from '@server/models/user/user'
+import { UserNotificationModel } from '@server/models/user/user-notification'
+import { MRegistration, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models'
+import { UserNotificationType, UserRight } from '@shared/models'
+import { AbstractNotification } from '../common/abstract-notification'
+
+export class RegistrationRequestForModerators extends AbstractNotification <MRegistration> {
+ private moderators: MUserDefault[]
+
+ async prepare () {
+ this.moderators = await UserModel.listWithRight(UserRight.MANAGE_REGISTRATIONS)
+ }
+
+ log () {
+ logger.info('Notifying %s moderators of new user registration request of %s.', this.moderators.length, this.payload.username)
+ }
+
+ getSetting (user: MUserWithNotificationSetting) {
+ return user.NotificationSetting.newUserRegistration
+ }
+
+ getTargetUsers () {
+ return this.moderators
+ }
+
+ createNotification (user: MUserWithNotificationSetting) {
+ const notification = UserNotificationModel.build<UserNotificationModelForApi>({
+ type: UserNotificationType.NEW_USER_REGISTRATION_REQUEST,
+ userId: user.id,
+ userRegistrationId: this.payload.id
+ })
+ notification.UserRegistration = this.payload
+
+ return notification
+ }
+
+ createEmail (to: string) {
+ return {
+ template: 'user-registration-request',
+ to,
+ subject: `A new user wants to register: ${this.payload.username}`,
+ locals: {
+ registration: this.payload
+ }
+ }
+ }
+}
CONTACT_FORM_LIFETIME,
RESUMABLE_UPLOAD_SESSION_LIFETIME,
TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME,
- USER_EMAIL_VERIFY_LIFETIME,
+ EMAIL_VERIFY_LIFETIME,
USER_PASSWORD_CREATE_LIFETIME,
USER_PASSWORD_RESET_LIFETIME,
VIEW_LIFETIME,
/* ************ Email verification ************ */
- async setVerifyEmailVerificationString (userId: number) {
+ async setUserVerifyEmailVerificationString (userId: number) {
const generatedString = await generateRandomString(32)
- await this.setValue(this.generateVerifyEmailKey(userId), generatedString, USER_EMAIL_VERIFY_LIFETIME)
+ await this.setValue(this.generateUserVerifyEmailKey(userId), generatedString, EMAIL_VERIFY_LIFETIME)
return generatedString
}
- async getVerifyEmailLink (userId: number) {
- return this.getValue(this.generateVerifyEmailKey(userId))
+ async getUserVerifyEmailLink (userId: number) {
+ return this.getValue(this.generateUserVerifyEmailKey(userId))
+ }
+
+ async setRegistrationVerifyEmailVerificationString (registrationId: number) {
+ const generatedString = await generateRandomString(32)
+
+ await this.setValue(this.generateRegistrationVerifyEmailKey(registrationId), generatedString, EMAIL_VERIFY_LIFETIME)
+
+ return generatedString
+ }
+
+ async getRegistrationVerifyEmailLink (registrationId: number) {
+ return this.getValue(this.generateRegistrationVerifyEmailKey(registrationId))
}
/* ************ Contact form per IP ************ */
return 'two-factor-request-' + userId + '-' + token
}
- private generateVerifyEmailKey (userId: number) {
- return 'verify-email-' + userId
+ private generateUserVerifyEmailKey (userId: number) {
+ return 'verify-email-user-' + userId
+ }
+
+ private generateRegistrationVerifyEmailKey (registrationId: number) {
+ return 'verify-email-registration-' + registrationId
}
private generateIPViewKey (ip: string, videoUUID: string) {
async getServerConfig (ip?: string): Promise<ServerConfig> {
const { allowed } = await Hooks.wrapPromiseFun(
isSignupAllowed,
+
{
- ip
+ ip,
+ signupMode: CONFIG.SIGNUP.REQUIRES_APPROVAL
+ ? 'request-registration'
+ : 'direct-registration'
},
- 'filter:api.user.signup.allowed.result'
+
+ CONFIG.SIGNUP.REQUIRES_APPROVAL
+ ? 'filter:api.user.request-signup.allowed.result'
+ : 'filter:api.user.signup.allowed.result'
)
const allowedForCurrentIP = isSignupAllowedForCurrentIP(ip)
allowed,
allowedForCurrentIP,
minimumAge: CONFIG.SIGNUP.MINIMUM_AGE,
+ requiresApproval: CONFIG.SIGNUP.REQUIRES_APPROVAL,
requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
}
const isCidr = require('is-cidr')
-async function isSignupAllowed (): Promise<{ allowed: boolean, errorMessage?: string }> {
+export type SignupMode = 'direct-registration' | 'request-registration'
+
+async function isSignupAllowed (options: {
+ signupMode: SignupMode
+
+ ip: string // For plugins
+ body?: any
+}): Promise<{ allowed: boolean, errorMessage?: string }> {
+ const { signupMode } = options
+
if (CONFIG.SIGNUP.ENABLED === false) {
return { allowed: false }
}
+ if (signupMode === 'direct-registration' && CONFIG.SIGNUP.REQUIRES_APPROVAL === true) {
+ return { allowed: false }
+ }
+
// No limit and signup is enabled
if (CONFIG.SIGNUP.LIMIT === -1) {
return { allowed: true }
import { AccountModel } from '../models/account/account'
import { UserNotificationSettingModel } from '../models/user/user-notification-setting'
import { MAccountDefault, MChannelActor } from '../types/models'
-import { MUser, MUserDefault, MUserId } from '../types/models/user'
+import { MRegistration, MUser, MUserDefault, MUserId } from '../types/models/user'
import { generateAndSaveActorKeys } from './activitypub/actors'
import { getLocalAccountActivityPubUrl } from './activitypub/url'
import { Emailer } from './emailer'
})
userCreated.Account = accountCreated
- const channelAttributes = await buildChannelAttributes(userCreated, t, channelNames)
+ const channelAttributes = await buildChannelAttributes({ user: userCreated, transaction: t, channelNames })
const videoChannel = await createLocalVideoChannel(channelAttributes, accountCreated, t)
const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t)
// ---------------------------------------------------------------------------
async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) {
- const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id)
- let url = WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString
+ const verificationString = await Redis.Instance.setUserVerifyEmailVerificationString(user.id)
+ let verifyEmailUrl = `${WEBSERVER.URL}/verify-account/email?userId=${user.id}&verificationString=${verificationString}`
- if (isPendingEmail) url += '&isPendingEmail=true'
+ if (isPendingEmail) verifyEmailUrl += '&isPendingEmail=true'
+
+ const to = isPendingEmail
+ ? user.pendingEmail
+ : user.email
- const email = isPendingEmail ? user.pendingEmail : user.email
const username = user.username
- Emailer.Instance.addVerifyEmailJob(username, email, url)
+ Emailer.Instance.addVerifyEmailJob({ username, to, verifyEmailUrl, isRegistrationRequest: false })
+}
+
+async function sendVerifyRegistrationEmail (registration: MRegistration) {
+ const verificationString = await Redis.Instance.setRegistrationVerifyEmailVerificationString(registration.id)
+ const verifyEmailUrl = `${WEBSERVER.URL}/verify-account/email?registrationId=${registration.id}&verificationString=${verificationString}`
+
+ const to = registration.email
+ const username = registration.username
+
+ Emailer.Instance.addVerifyEmailJob({ username, to, verifyEmailUrl, isRegistrationRequest: true })
}
// ---------------------------------------------------------------------------
createApplicationActor,
createUserAccountAndChannelAndPlaylist,
createLocalAccountWithoutKeys,
+
sendVerifyUserEmail,
+ sendVerifyRegistrationEmail,
+
isAbleToUploadVideo,
buildUser
}
return UserNotificationSettingModel.create(values, { transaction: t })
}
-async function buildChannelAttributes (user: MUser, transaction?: Transaction, channelNames?: ChannelNames) {
+async function buildChannelAttributes (options: {
+ user: MUser
+ transaction?: Transaction
+ channelNames?: ChannelNames
+}) {
+ const { user, transaction, channelNames } = options
+
if (channelNames) return channelNames
const channelName = await findAvailableLocalActorName(user.username + '_channel', transaction)
body('signup.enabled').isBoolean(),
body('signup.limit').isInt(),
body('signup.requiresEmailVerification').isBoolean(),
+ body('signup.requiresApproval').isBoolean(),
body('signup.minimumAge').isInt(),
body('admin.email').isEmail(),
export * from './sort'
export * from './static'
export * from './themes'
+export * from './user-email-verification'
export * from './user-history'
export * from './user-notifications'
+export * from './user-registrations'
export * from './user-subscriptions'
export * from './users'
export * from './videos'
--- /dev/null
+import express from 'express'
+import { UserRegistrationModel } from '@server/models/user/user-registration'
+import { MRegistration } from '@server/types/models'
+import { forceNumber, pick } from '@shared/core-utils'
+import { HttpStatusCode } from '@shared/models'
+
+function checkRegistrationIdExist (idArg: number | string, res: express.Response) {
+ const id = forceNumber(idArg)
+ return checkRegistrationExist(() => UserRegistrationModel.load(id), res)
+}
+
+function checkRegistrationEmailExist (email: string, res: express.Response, abortResponse = true) {
+ return checkRegistrationExist(() => UserRegistrationModel.loadByEmail(email), res, abortResponse)
+}
+
+async function checkRegistrationHandlesDoNotAlreadyExist (options: {
+ username: string
+ channelHandle: string
+ email: string
+ res: express.Response
+}) {
+ const { res } = options
+
+ const registration = await UserRegistrationModel.loadByEmailOrHandle(pick(options, [ 'username', 'email', 'channelHandle' ]))
+
+ if (registration) {
+ res.fail({
+ status: HttpStatusCode.CONFLICT_409,
+ message: 'Registration with this username, channel name or email already exists.'
+ })
+ return false
+ }
+
+ return true
+}
+
+async function checkRegistrationExist (finder: () => Promise<MRegistration>, res: express.Response, abortResponse = true) {
+ const registration = await finder()
+
+ if (!registration) {
+ if (abortResponse === true) {
+ res.fail({
+ status: HttpStatusCode.NOT_FOUND_404,
+ message: 'User not found'
+ })
+ }
+
+ return false
+ }
+
+ res.locals.userRegistration = registration
+ return true
+}
+
+export {
+ checkRegistrationIdExist,
+ checkRegistrationEmailExist,
+ checkRegistrationHandlesDoNotAlreadyExist,
+ checkRegistrationExist
+}
return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
}
-async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
+async function checkUserNameOrEmailDoNotAlreadyExist (username: string, email: string, res: express.Response) {
const user = await UserModel.loadByUsernameOrEmail(username, email)
if (user) {
export {
checkUserIdExist,
checkUserEmailExist,
- checkUserNameOrEmailDoesNotAlreadyExist,
+ checkUserNameOrEmailDoNotAlreadyExist,
checkUserExist
}
import express from 'express'
import { query } from 'express-validator'
-
import { SORTABLE_COLUMNS } from '../../initializers/constants'
import { areValidationErrors } from './shared'
+export const adminUsersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ADMIN_USERS)
+export const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS)
+export const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ])
+export const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES)
+export const videosSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEOS)
+export const videoImportsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_IMPORTS)
+export const videosSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEOS_SEARCH)
+export const videoChannelsSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH)
+export const videoPlaylistsSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PLAYLISTS_SEARCH)
+export const videoCommentsValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_COMMENTS)
+export const videoCommentThreadsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS)
+export const videoRatesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_RATES)
+export const blacklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.BLACKLISTS)
+export const videoChannelsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNELS)
+export const instanceFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.INSTANCE_FOLLOWERS)
+export const instanceFollowingSortValidator = checkSortFactory(SORTABLE_COLUMNS.INSTANCE_FOLLOWING)
+export const userSubscriptionsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS)
+export const accountsBlocklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST)
+export const serversBlocklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.SERVERS_BLOCKLIST)
+export const userNotificationsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_NOTIFICATIONS)
+export const videoPlaylistsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PLAYLISTS)
+export const pluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.PLUGINS)
+export const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS)
+export const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES)
+export const videoChannelSyncsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNEL_SYNCS)
+
+export const accountsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNT_FOLLOWERS)
+export const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS)
+
+export const userRegistrationsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_REGISTRATIONS)
+
+// ---------------------------------------------------------------------------
+
function checkSortFactory (columns: string[], tags: string[] = []) {
return checkSort(createSortableColumns(columns), tags)
}
return sortableColumns.concat(sortableColumnDesc)
}
-
-const adminUsersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ADMIN_USERS)
-const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS)
-const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ])
-const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES)
-const videosSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEOS)
-const videoImportsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_IMPORTS)
-const videosSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEOS_SEARCH)
-const videoChannelsSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH)
-const videoPlaylistsSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PLAYLISTS_SEARCH)
-const videoCommentsValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_COMMENTS)
-const videoCommentThreadsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS)
-const videoRatesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_RATES)
-const blacklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.BLACKLISTS)
-const videoChannelsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNELS)
-const instanceFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.INSTANCE_FOLLOWERS)
-const instanceFollowingSortValidator = checkSortFactory(SORTABLE_COLUMNS.INSTANCE_FOLLOWING)
-const userSubscriptionsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS)
-const accountsBlocklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST)
-const serversBlocklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.SERVERS_BLOCKLIST)
-const userNotificationsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_NOTIFICATIONS)
-const videoPlaylistsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PLAYLISTS)
-const pluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.PLUGINS)
-const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS)
-const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES)
-const videoChannelSyncsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNEL_SYNCS)
-
-const accountsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNT_FOLLOWERS)
-const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS)
-
-// ---------------------------------------------------------------------------
-
-export {
- adminUsersSortValidator,
- abusesSortValidator,
- videoChannelsSortValidator,
- videoImportsSortValidator,
- videoCommentsValidator,
- videosSearchSortValidator,
- videosSortValidator,
- blacklistSortValidator,
- accountsSortValidator,
- instanceFollowersSortValidator,
- instanceFollowingSortValidator,
- jobsSortValidator,
- videoCommentThreadsSortValidator,
- videoRatesSortValidator,
- userSubscriptionsSortValidator,
- availablePluginsSortValidator,
- videoChannelsSearchSortValidator,
- accountsBlocklistSortValidator,
- serversBlocklistSortValidator,
- userNotificationsSortValidator,
- videoPlaylistsSortValidator,
- videoRedundanciesSortValidator,
- videoPlaylistsSearchSortValidator,
- accountsFollowersSortValidator,
- videoChannelsFollowersSortValidator,
- videoChannelSyncsSortValidator,
- pluginsSortValidator
-}
--- /dev/null
+import express from 'express'
+import { body, param } from 'express-validator'
+import { toBooleanOrNull } from '@server/helpers/custom-validators/misc'
+import { HttpStatusCode } from '@shared/models'
+import { logger } from '../../helpers/logger'
+import { Redis } from '../../lib/redis'
+import { areValidationErrors, checkUserEmailExist, checkUserIdExist } from './shared'
+import { checkRegistrationEmailExist, checkRegistrationIdExist } from './shared/user-registrations'
+
+const usersAskSendVerifyEmailValidator = [
+ body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ if (areValidationErrors(req, res)) return
+
+ const [ userExists, registrationExists ] = await Promise.all([
+ checkUserEmailExist(req.body.email, res, false),
+ checkRegistrationEmailExist(req.body.email, res, false)
+ ])
+
+ if (!userExists && !registrationExists) {
+ logger.debug('User or registration with email %s does not exist (asking verify email).', req.body.email)
+ // Do not leak our emails
+ return res.status(HttpStatusCode.NO_CONTENT_204).end()
+ }
+
+ if (res.locals.user?.pluginAuth) {
+ return res.fail({
+ status: HttpStatusCode.CONFLICT_409,
+ message: 'Cannot ask verification email of a user that uses a plugin authentication.'
+ })
+ }
+
+ return next()
+ }
+]
+
+const usersVerifyEmailValidator = [
+ param('id')
+ .isInt().not().isEmpty().withMessage('Should have a valid id'),
+
+ body('verificationString')
+ .not().isEmpty().withMessage('Should have a valid verification string'),
+ body('isPendingEmail')
+ .optional()
+ .customSanitizer(toBooleanOrNull),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ if (areValidationErrors(req, res)) return
+ if (!await checkUserIdExist(req.params.id, res)) return
+
+ const user = res.locals.user
+ const redisVerificationString = await Redis.Instance.getUserVerifyEmailLink(user.id)
+
+ if (redisVerificationString !== req.body.verificationString) {
+ return res.fail({ status: HttpStatusCode.FORBIDDEN_403, message: 'Invalid verification string.' })
+ }
+
+ return next()
+ }
+]
+
+// ---------------------------------------------------------------------------
+
+const registrationVerifyEmailValidator = [
+ param('registrationId')
+ .isInt().not().isEmpty().withMessage('Should have a valid registrationId'),
+
+ body('verificationString')
+ .not().isEmpty().withMessage('Should have a valid verification string'),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ if (areValidationErrors(req, res)) return
+ if (!await checkRegistrationIdExist(req.params.registrationId, res)) return
+
+ const registration = res.locals.userRegistration
+ const redisVerificationString = await Redis.Instance.getRegistrationVerifyEmailLink(registration.id)
+
+ if (redisVerificationString !== req.body.verificationString) {
+ return res.fail({ status: HttpStatusCode.FORBIDDEN_403, message: 'Invalid verification string.' })
+ }
+
+ return next()
+ }
+]
+
+// ---------------------------------------------------------------------------
+
+export {
+ usersAskSendVerifyEmailValidator,
+ usersVerifyEmailValidator,
+
+ registrationVerifyEmailValidator
+}
--- /dev/null
+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()
+ }
+ ]
+}
import express from 'express'
import { body, param, query } from 'express-validator'
-import { Hooks } from '@server/lib/plugins/hooks'
import { forceNumber } from '@shared/core-utils'
-import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models'
+import { HttpStatusCode, UserRight, UserRole } from '@shared/models'
import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
import {
isUserVideoQuotaValid,
isUserVideosHistoryEnabledValid
} from '../../helpers/custom-validators/users'
-import { isVideoChannelDisplayNameValid, isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels'
+import { isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels'
import { logger } from '../../helpers/logger'
import { isThemeRegistered } from '../../lib/plugins/theme-utils'
import { Redis } from '../../lib/redis'
-import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup'
import { ActorModel } from '../../models/actor/actor'
import {
areValidationErrors,
checkUserEmailExist,
checkUserIdExist,
- checkUserNameOrEmailDoesNotAlreadyExist,
+ checkUserNameOrEmailDoNotAlreadyExist,
doesVideoChannelIdExist,
doesVideoExist,
isValidVideoIdParam
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res, { omitBodyLog: true })) return
- if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
+ if (!await checkUserNameOrEmailDoNotAlreadyExist(req.body.username, req.body.email, res)) return
const authUser = res.locals.oauth.token.User
if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
}
]
-const usersRegisterValidator = [
- 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),
-
- async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- if (areValidationErrors(req, res, { omitBodyLog: true })) return
- if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
-
- const body: UserRegister = req.body
- 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()
- }
-]
-
const usersRemoveValidator = [
param('id')
.custom(isIdValid),
}
]
-const ensureUserRegistrationAllowed = [
- async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- const allowedParams = {
- body: req.body,
- ip: req.ip
- }
-
- const allowedResult = await Hooks.wrapPromiseFun(
- isSignupAllowed,
- allowedParams,
- 'filter:api.user.signup.allowed.result'
- )
-
- if (allowedResult.allowed === false) {
- return res.fail({
- status: HttpStatusCode.FORBIDDEN_403,
- message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.'
- })
- }
-
- 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 usersAskResetPasswordValidator = [
body('email')
.isEmail(),
}
]
-const usersAskSendVerifyEmailValidator = [
- body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
-
- async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- if (areValidationErrors(req, res)) return
-
- const exists = await checkUserEmailExist(req.body.email, res, false)
- if (!exists) {
- logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
- // Do not leak our emails
- return res.status(HttpStatusCode.NO_CONTENT_204).end()
- }
-
- if (res.locals.user.pluginAuth) {
- return res.fail({
- status: HttpStatusCode.CONFLICT_409,
- message: 'Cannot ask verification email of a user that uses a plugin authentication.'
- })
- }
-
- return next()
- }
-]
-
-const usersVerifyEmailValidator = [
- param('id')
- .isInt().not().isEmpty().withMessage('Should have a valid id'),
-
- body('verificationString')
- .not().isEmpty().withMessage('Should have a valid verification string'),
- body('isPendingEmail')
- .optional()
- .customSanitizer(toBooleanOrNull),
-
- async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- if (areValidationErrors(req, res)) return
- if (!await checkUserIdExist(req.params.id, res)) return
-
- const user = res.locals.user
- const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
-
- if (redisVerificationString !== req.body.verificationString) {
- return res.fail({
- status: HttpStatusCode.FORBIDDEN_403,
- message: 'Invalid verification string.'
- })
- }
-
- return next()
- }
-]
-
const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => {
return [
body('currentPassword').optional().custom(exists),
usersListValidator,
usersAddValidator,
deleteMeValidator,
- usersRegisterValidator,
usersBlockingValidator,
usersRemoveValidator,
usersUpdateValidator,
usersUpdateMeValidator,
usersVideoRatingValidator,
usersCheckCurrentPasswordFactory,
- ensureUserRegistrationAllowed,
- ensureUserRegistrationAllowedForIP,
usersGetValidator,
usersVideosValidator,
usersAskResetPasswordValidator,
usersResetPasswordValidator,
- usersAskSendVerifyEmailValidator,
- usersVerifyEmailValidator,
userAutocompleteValidator,
ensureAuthUserOwnsAccountValidator,
ensureCanModerateUser,
"Account->Actor->Avatars"."type" AS "Account.Actor.Avatars.type",
"Account->Actor->Avatars"."filename" AS "Account.Actor.Avatars.filename",
"Account->Actor->Server"."id" AS "Account.Actor.Server.id",
- "Account->Actor->Server"."host" AS "Account.Actor.Server.host"`
+ "Account->Actor->Server"."host" AS "Account.Actor.Server.host",
+ "UserRegistration"."id" AS "UserRegistration.id",
+ "UserRegistration"."username" AS "UserRegistration.username"`
}
private getJoins () {
ON "Video->VideoChannel->Actor"."serverId" = "Video->VideoChannel->Actor->Server"."id"
) ON "UserNotificationModel"."videoId" = "Video"."id"
- LEFT JOIN (
- "videoComment" AS "VideoComment"
- INNER JOIN "account" AS "VideoComment->Account" ON "VideoComment"."accountId" = "VideoComment->Account"."id"
- INNER JOIN "actor" AS "VideoComment->Account->Actor" ON "VideoComment->Account"."actorId" = "VideoComment->Account->Actor"."id"
- LEFT JOIN "actorImage" AS "VideoComment->Account->Actor->Avatars"
- ON "VideoComment->Account->Actor"."id" = "VideoComment->Account->Actor->Avatars"."actorId"
- AND "VideoComment->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
- LEFT JOIN "server" AS "VideoComment->Account->Actor->Server"
- ON "VideoComment->Account->Actor"."serverId" = "VideoComment->Account->Actor->Server"."id"
- INNER JOIN "video" AS "VideoComment->Video" ON "VideoComment"."videoId" = "VideoComment->Video"."id"
- ) ON "UserNotificationModel"."commentId" = "VideoComment"."id"
+ LEFT JOIN (
+ "videoComment" AS "VideoComment"
+ INNER JOIN "account" AS "VideoComment->Account" ON "VideoComment"."accountId" = "VideoComment->Account"."id"
+ INNER JOIN "actor" AS "VideoComment->Account->Actor" ON "VideoComment->Account"."actorId" = "VideoComment->Account->Actor"."id"
+ LEFT JOIN "actorImage" AS "VideoComment->Account->Actor->Avatars"
+ ON "VideoComment->Account->Actor"."id" = "VideoComment->Account->Actor->Avatars"."actorId"
+ AND "VideoComment->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
+ LEFT JOIN "server" AS "VideoComment->Account->Actor->Server"
+ ON "VideoComment->Account->Actor"."serverId" = "VideoComment->Account->Actor->Server"."id"
+ INNER JOIN "video" AS "VideoComment->Video" ON "VideoComment"."videoId" = "VideoComment->Video"."id"
+ ) ON "UserNotificationModel"."commentId" = "VideoComment"."id"
+
+ LEFT JOIN "abuse" AS "Abuse" ON "UserNotificationModel"."abuseId" = "Abuse"."id"
+ LEFT JOIN "videoAbuse" AS "Abuse->VideoAbuse" ON "Abuse"."id" = "Abuse->VideoAbuse"."abuseId"
+ LEFT JOIN "video" AS "Abuse->VideoAbuse->Video" ON "Abuse->VideoAbuse"."videoId" = "Abuse->VideoAbuse->Video"."id"
+ LEFT JOIN "commentAbuse" AS "Abuse->VideoCommentAbuse" ON "Abuse"."id" = "Abuse->VideoCommentAbuse"."abuseId"
+ LEFT JOIN "videoComment" AS "Abuse->VideoCommentAbuse->VideoComment"
+ ON "Abuse->VideoCommentAbuse"."videoCommentId" = "Abuse->VideoCommentAbuse->VideoComment"."id"
+ LEFT JOIN "video" AS "Abuse->VideoCommentAbuse->VideoComment->Video"
+ ON "Abuse->VideoCommentAbuse->VideoComment"."videoId" = "Abuse->VideoCommentAbuse->VideoComment->Video"."id"
+ LEFT JOIN (
+ "account" AS "Abuse->FlaggedAccount"
+ INNER JOIN "actor" AS "Abuse->FlaggedAccount->Actor" ON "Abuse->FlaggedAccount"."actorId" = "Abuse->FlaggedAccount->Actor"."id"
+ LEFT JOIN "actorImage" AS "Abuse->FlaggedAccount->Actor->Avatars"
+ ON "Abuse->FlaggedAccount->Actor"."id" = "Abuse->FlaggedAccount->Actor->Avatars"."actorId"
+ AND "Abuse->FlaggedAccount->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
+ LEFT JOIN "server" AS "Abuse->FlaggedAccount->Actor->Server"
+ ON "Abuse->FlaggedAccount->Actor"."serverId" = "Abuse->FlaggedAccount->Actor->Server"."id"
+ ) ON "Abuse"."flaggedAccountId" = "Abuse->FlaggedAccount"."id"
- LEFT JOIN "abuse" AS "Abuse" ON "UserNotificationModel"."abuseId" = "Abuse"."id"
- LEFT JOIN "videoAbuse" AS "Abuse->VideoAbuse" ON "Abuse"."id" = "Abuse->VideoAbuse"."abuseId"
- LEFT JOIN "video" AS "Abuse->VideoAbuse->Video" ON "Abuse->VideoAbuse"."videoId" = "Abuse->VideoAbuse->Video"."id"
- LEFT JOIN "commentAbuse" AS "Abuse->VideoCommentAbuse" ON "Abuse"."id" = "Abuse->VideoCommentAbuse"."abuseId"
- LEFT JOIN "videoComment" AS "Abuse->VideoCommentAbuse->VideoComment"
- ON "Abuse->VideoCommentAbuse"."videoCommentId" = "Abuse->VideoCommentAbuse->VideoComment"."id"
- LEFT JOIN "video" AS "Abuse->VideoCommentAbuse->VideoComment->Video"
- ON "Abuse->VideoCommentAbuse->VideoComment"."videoId" = "Abuse->VideoCommentAbuse->VideoComment->Video"."id"
- LEFT JOIN (
- "account" AS "Abuse->FlaggedAccount"
- INNER JOIN "actor" AS "Abuse->FlaggedAccount->Actor" ON "Abuse->FlaggedAccount"."actorId" = "Abuse->FlaggedAccount->Actor"."id"
- LEFT JOIN "actorImage" AS "Abuse->FlaggedAccount->Actor->Avatars"
- ON "Abuse->FlaggedAccount->Actor"."id" = "Abuse->FlaggedAccount->Actor->Avatars"."actorId"
- AND "Abuse->FlaggedAccount->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
- LEFT JOIN "server" AS "Abuse->FlaggedAccount->Actor->Server"
- ON "Abuse->FlaggedAccount->Actor"."serverId" = "Abuse->FlaggedAccount->Actor->Server"."id"
- ) ON "Abuse"."flaggedAccountId" = "Abuse->FlaggedAccount"."id"
+ LEFT JOIN (
+ "videoBlacklist" AS "VideoBlacklist"
+ INNER JOIN "video" AS "VideoBlacklist->Video" ON "VideoBlacklist"."videoId" = "VideoBlacklist->Video"."id"
+ ) ON "UserNotificationModel"."videoBlacklistId" = "VideoBlacklist"."id"
- LEFT JOIN (
- "videoBlacklist" AS "VideoBlacklist"
- INNER JOIN "video" AS "VideoBlacklist->Video" ON "VideoBlacklist"."videoId" = "VideoBlacklist->Video"."id"
- ) ON "UserNotificationModel"."videoBlacklistId" = "VideoBlacklist"."id"
+ LEFT JOIN "videoImport" AS "VideoImport" ON "UserNotificationModel"."videoImportId" = "VideoImport"."id"
+ LEFT JOIN "video" AS "VideoImport->Video" ON "VideoImport"."videoId" = "VideoImport->Video"."id"
- LEFT JOIN "videoImport" AS "VideoImport" ON "UserNotificationModel"."videoImportId" = "VideoImport"."id"
- LEFT JOIN "video" AS "VideoImport->Video" ON "VideoImport"."videoId" = "VideoImport->Video"."id"
+ LEFT JOIN "plugin" AS "Plugin" ON "UserNotificationModel"."pluginId" = "Plugin"."id"
- LEFT JOIN "plugin" AS "Plugin" ON "UserNotificationModel"."pluginId" = "Plugin"."id"
+ LEFT JOIN "application" AS "Application" ON "UserNotificationModel"."applicationId" = "Application"."id"
- LEFT JOIN "application" AS "Application" ON "UserNotificationModel"."applicationId" = "Application"."id"
+ LEFT JOIN (
+ "actorFollow" AS "ActorFollow"
+ INNER JOIN "actor" AS "ActorFollow->ActorFollower" ON "ActorFollow"."actorId" = "ActorFollow->ActorFollower"."id"
+ INNER JOIN "account" AS "ActorFollow->ActorFollower->Account"
+ ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Account"."actorId"
+ LEFT JOIN "actorImage" AS "ActorFollow->ActorFollower->Avatars"
+ ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Avatars"."actorId"
+ AND "ActorFollow->ActorFollower->Avatars"."type" = ${ActorImageType.AVATAR}
+ LEFT JOIN "server" AS "ActorFollow->ActorFollower->Server"
+ ON "ActorFollow->ActorFollower"."serverId" = "ActorFollow->ActorFollower->Server"."id"
+ INNER JOIN "actor" AS "ActorFollow->ActorFollowing" ON "ActorFollow"."targetActorId" = "ActorFollow->ActorFollowing"."id"
+ LEFT JOIN "videoChannel" AS "ActorFollow->ActorFollowing->VideoChannel"
+ ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->VideoChannel"."actorId"
+ LEFT JOIN "account" AS "ActorFollow->ActorFollowing->Account"
+ ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->Account"."actorId"
+ LEFT JOIN "server" AS "ActorFollow->ActorFollowing->Server"
+ ON "ActorFollow->ActorFollowing"."serverId" = "ActorFollow->ActorFollowing->Server"."id"
+ ) ON "UserNotificationModel"."actorFollowId" = "ActorFollow"."id"
- LEFT JOIN (
- "actorFollow" AS "ActorFollow"
- INNER JOIN "actor" AS "ActorFollow->ActorFollower" ON "ActorFollow"."actorId" = "ActorFollow->ActorFollower"."id"
- INNER JOIN "account" AS "ActorFollow->ActorFollower->Account"
- ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Account"."actorId"
- LEFT JOIN "actorImage" AS "ActorFollow->ActorFollower->Avatars"
- ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Avatars"."actorId"
- AND "ActorFollow->ActorFollower->Avatars"."type" = ${ActorImageType.AVATAR}
- LEFT JOIN "server" AS "ActorFollow->ActorFollower->Server"
- ON "ActorFollow->ActorFollower"."serverId" = "ActorFollow->ActorFollower->Server"."id"
- INNER JOIN "actor" AS "ActorFollow->ActorFollowing" ON "ActorFollow"."targetActorId" = "ActorFollow->ActorFollowing"."id"
- LEFT JOIN "videoChannel" AS "ActorFollow->ActorFollowing->VideoChannel"
- ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->VideoChannel"."actorId"
- LEFT JOIN "account" AS "ActorFollow->ActorFollowing->Account"
- ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->Account"."actorId"
- LEFT JOIN "server" AS "ActorFollow->ActorFollowing->Server"
- ON "ActorFollow->ActorFollowing"."serverId" = "ActorFollow->ActorFollowing->Server"."id"
- ) ON "UserNotificationModel"."actorFollowId" = "ActorFollow"."id"
+ LEFT JOIN (
+ "account" AS "Account"
+ INNER JOIN "actor" AS "Account->Actor" ON "Account"."actorId" = "Account->Actor"."id"
+ LEFT JOIN "actorImage" AS "Account->Actor->Avatars"
+ ON "Account->Actor"."id" = "Account->Actor->Avatars"."actorId"
+ AND "Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
+ LEFT JOIN "server" AS "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"
+ ) ON "UserNotificationModel"."accountId" = "Account"."id"
- LEFT JOIN (
- "account" AS "Account"
- INNER JOIN "actor" AS "Account->Actor" ON "Account"."actorId" = "Account->Actor"."id"
- LEFT JOIN "actorImage" AS "Account->Actor->Avatars"
- ON "Account->Actor"."id" = "Account->Actor->Avatars"."actorId"
- AND "Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
- LEFT JOIN "server" AS "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"
- ) ON "UserNotificationModel"."accountId" = "Account"."id"`
+ LEFT JOIN "userRegistration" as "UserRegistration" ON "UserNotificationModel"."userRegistrationId" = "UserRegistration"."id"`
}
}
import { VideoImportModel } from '../video/video-import'
import { UserNotificationListQueryBuilder } from './sql/user-notitication-list-query-builder'
import { UserModel } from './user'
+import { UserRegistrationModel } from './user-registration'
@Table({
tableName: 'userNotification',
[Op.ne]: null
}
}
+ },
+ {
+ fields: [ 'userRegistrationId' ],
+ where: {
+ userRegistrationId: {
+ [Op.ne]: null
+ }
+ }
}
] as (ModelIndexesOptions & { where?: WhereOptions })[]
})
})
Application: ApplicationModel
+ @ForeignKey(() => UserRegistrationModel)
+ @Column
+ userRegistrationId: number
+
+ @BelongsTo(() => UserRegistrationModel, {
+ foreignKey: {
+ allowNull: true
+ },
+ onDelete: 'cascade'
+ })
+ UserRegistration: UserRegistrationModel
+
static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
const where = { userId }
? { latestVersion: this.Application.latestPeerTubeVersion }
: undefined
+ const registration = this.UserRegistration
+ ? { id: this.UserRegistration.id, username: this.UserRegistration.username }
+ : undefined
+
return {
id: this.id,
type: this.type,
actorFollow,
plugin,
peertube,
+ registration,
createdAt: this.createdAt.toISOString(),
updatedAt: this.updatedAt.toISOString()
}
--- /dev/null
+import { FindOptions, Op, WhereOptions } from 'sequelize'
+import {
+ AllowNull,
+ BeforeCreate,
+ BelongsTo,
+ Column,
+ CreatedAt,
+ DataType,
+ ForeignKey,
+ Is,
+ IsEmail,
+ Model,
+ Table,
+ UpdatedAt
+} from 'sequelize-typescript'
+import {
+ isRegistrationModerationResponseValid,
+ isRegistrationReasonValid,
+ isRegistrationStateValid
+} from '@server/helpers/custom-validators/user-registration'
+import { isVideoChannelDisplayNameValid } from '@server/helpers/custom-validators/video-channels'
+import { cryptPassword } from '@server/helpers/peertube-crypto'
+import { USER_REGISTRATION_STATES } from '@server/initializers/constants'
+import { MRegistration, MRegistrationFormattable } from '@server/types/models'
+import { UserRegistration, UserRegistrationState } from '@shared/models'
+import { AttributesOnly } from '@shared/typescript-utils'
+import { isUserDisplayNameValid, isUserEmailVerifiedValid, isUserPasswordValid } from '../../helpers/custom-validators/users'
+import { getSort, throwIfNotValid } from '../shared'
+import { UserModel } from './user'
+
+@Table({
+ tableName: 'userRegistration',
+ indexes: [
+ {
+ fields: [ 'username' ],
+ unique: true
+ },
+ {
+ fields: [ 'email' ],
+ unique: true
+ },
+ {
+ fields: [ 'channelHandle' ],
+ unique: true
+ },
+ {
+ fields: [ 'userId' ],
+ unique: true
+ }
+ ]
+})
+export class UserRegistrationModel extends Model<Partial<AttributesOnly<UserRegistrationModel>>> {
+
+ @AllowNull(false)
+ @Is('RegistrationState', value => throwIfNotValid(value, isRegistrationStateValid, 'state'))
+ @Column
+ state: UserRegistrationState
+
+ @AllowNull(false)
+ @Is('RegistrationReason', value => throwIfNotValid(value, isRegistrationReasonValid, 'registration reason'))
+ @Column(DataType.TEXT)
+ registrationReason: string
+
+ @AllowNull(true)
+ @Is('RegistrationModerationResponse', value => throwIfNotValid(value, isRegistrationModerationResponseValid, 'moderation response', true))
+ @Column(DataType.TEXT)
+ moderationResponse: string
+
+ @AllowNull(true)
+ @Is('RegistrationPassword', value => throwIfNotValid(value, isUserPasswordValid, 'registration password', true))
+ @Column
+ password: string
+
+ @AllowNull(false)
+ @Column
+ username: string
+
+ @AllowNull(false)
+ @IsEmail
+ @Column(DataType.STRING(400))
+ email: string
+
+ @AllowNull(true)
+ @Is('RegistrationEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true))
+ @Column
+ emailVerified: boolean
+
+ @AllowNull(true)
+ @Is('RegistrationAccountDisplayName', value => throwIfNotValid(value, isUserDisplayNameValid, 'account display name', true))
+ @Column
+ accountDisplayName: string
+
+ @AllowNull(true)
+ @Is('ChannelHandle', value => throwIfNotValid(value, isVideoChannelDisplayNameValid, 'channel handle', true))
+ @Column
+ channelHandle: string
+
+ @AllowNull(true)
+ @Is('ChannelDisplayName', value => throwIfNotValid(value, isVideoChannelDisplayNameValid, 'channel display name', true))
+ @Column
+ channelDisplayName: string
+
+ @CreatedAt
+ createdAt: Date
+
+ @UpdatedAt
+ updatedAt: Date
+
+ @ForeignKey(() => UserModel)
+ @Column
+ userId: number
+
+ @BelongsTo(() => UserModel, {
+ foreignKey: {
+ allowNull: true
+ },
+ onDelete: 'SET NULL'
+ })
+ User: UserModel
+
+ @BeforeCreate
+ static async cryptPasswordIfNeeded (instance: UserRegistrationModel) {
+ instance.password = await cryptPassword(instance.password)
+ }
+
+ static load (id: number): Promise<MRegistration> {
+ return UserRegistrationModel.findByPk(id)
+ }
+
+ static loadByEmail (email: string): Promise<MRegistration> {
+ const query = {
+ where: { email }
+ }
+
+ return UserRegistrationModel.findOne(query)
+ }
+
+ static loadByEmailOrUsername (emailOrUsername: string): Promise<MRegistration> {
+ const query = {
+ where: {
+ [Op.or]: [
+ { email: emailOrUsername },
+ { username: emailOrUsername }
+ ]
+ }
+ }
+
+ return UserRegistrationModel.findOne(query)
+ }
+
+ static loadByEmailOrHandle (options: {
+ email: string
+ username: string
+ channelHandle?: string
+ }): Promise<MRegistration> {
+ const { email, username, channelHandle } = options
+
+ let or: WhereOptions = [
+ { email },
+ { channelHandle: username },
+ { username }
+ ]
+
+ if (channelHandle) {
+ or = or.concat([
+ { username: channelHandle },
+ { channelHandle }
+ ])
+ }
+
+ const query = {
+ where: {
+ [Op.or]: or
+ }
+ }
+
+ return UserRegistrationModel.findOne(query)
+ }
+
+ // ---------------------------------------------------------------------------
+
+ static listForApi (options: {
+ start: number
+ count: number
+ sort: string
+ search?: string
+ }) {
+ const { start, count, sort, search } = options
+
+ const where: WhereOptions = {}
+
+ if (search) {
+ Object.assign(where, {
+ [Op.or]: [
+ {
+ email: {
+ [Op.iLike]: '%' + search + '%'
+ }
+ },
+ {
+ username: {
+ [Op.iLike]: '%' + search + '%'
+ }
+ }
+ ]
+ })
+ }
+
+ const query: FindOptions = {
+ offset: start,
+ limit: count,
+ order: getSort(sort),
+ where,
+ include: [
+ {
+ model: UserModel.unscoped(),
+ required: false
+ }
+ ]
+ }
+
+ return Promise.all([
+ UserRegistrationModel.count(query),
+ UserRegistrationModel.findAll<MRegistrationFormattable>(query)
+ ]).then(([ total, data ]) => ({ total, data }))
+ }
+
+ // ---------------------------------------------------------------------------
+
+ toFormattedJSON (this: MRegistrationFormattable): UserRegistration {
+ return {
+ id: this.id,
+
+ state: {
+ id: this.state,
+ label: USER_REGISTRATION_STATES[this.state]
+ },
+
+ registrationReason: this.registrationReason,
+ moderationResponse: this.moderationResponse,
+
+ username: this.username,
+ email: this.email,
+ emailVerified: this.emailVerified,
+
+ accountDisplayName: this.accountDisplayName,
+
+ channelHandle: this.channelHandle,
+ channelDisplayName: this.channelDisplayName,
+
+ createdAt: this.createdAt,
+ updatedAt: this.updatedAt,
+
+ user: this.User
+ ? { id: this.User.id }
+ : null
+ }
+ }
+}
})
OAuthTokens: OAuthTokenModel[]
+ // Used if we already set an encrypted password in user model
+ skipPasswordEncryption = false
+
@BeforeCreate
@BeforeUpdate
- static cryptPasswordIfNeeded (instance: UserModel) {
- if (instance.changed('password') && instance.password) {
- return cryptPassword(instance.password)
- .then(hash => {
- instance.password = hash
- return undefined
- })
- }
+ static async cryptPasswordIfNeeded (instance: UserModel) {
+ if (instance.skipPasswordEncryption) return
+ if (!instance.changed('password')) return
+ if (!instance.password) return
+
+ instance.password = await cryptPassword(instance.password)
}
@AfterUpdate
MActorUrl,
MChannelBannerAccountDefault,
MChannelSyncChannel,
+ MRegistration,
MStreamingPlaylist,
MUserAccountUrl,
MVideoChangeOwnershipFull,
actorFull?: MActorFull
user?: MUserDefault
+ userRegistration?: MRegistration
server?: MServer
export * from './user'
export * from './user-notification'
export * from './user-notification-setting'
+export * from './user-registration'
export * from './user-video-history'
import { ApplicationModel } from '@server/models/application/application'
import { PluginModel } from '@server/models/server/plugin'
import { UserNotificationModel } from '@server/models/user/user-notification'
+import { UserRegistrationModel } from '@server/models/user/user-registration'
import { PickWith, PickWithOpt } from '@shared/typescript-utils'
import { AbuseModel } from '../../../models/abuse/abuse'
import { AccountModel } from '../../../models/account/account'
export type ApplicationInclude =
Pick<ApplicationModel, 'latestPeerTubeVersion'>
+
+ export type UserRegistrationInclude =
+ Pick<UserRegistrationModel, 'id' | 'username'>
}
// ############################################################################
export type MUserNotification =
Omit<UserNotificationModel, 'User' | 'Video' | 'VideoComment' | 'Abuse' | 'VideoBlacklist' |
- 'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application'>
+ 'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application' | 'UserRegistration'>
// ############################################################################
Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> &
Use<'Plugin', UserNotificationIncludes.PluginInclude> &
Use<'Application', UserNotificationIncludes.ApplicationInclude> &
- Use<'Account', UserNotificationIncludes.AccountIncludeActor>
+ Use<'Account', UserNotificationIncludes.AccountIncludeActor> &
+ Use<'UserRegistration', UserNotificationIncludes.UserRegistrationInclude>
--- /dev/null
+import { UserRegistrationModel } from '@server/models/user/user-registration'
+import { PickWith } from '@shared/typescript-utils'
+import { MUserId } from './user'
+
+type Use<K extends keyof UserRegistrationModel, M> = PickWith<UserRegistrationModel, K, M>
+
+// ############################################################################
+
+export type MRegistration = Omit<UserRegistrationModel, 'User'>
+
+// ############################################################################
+
+export type MRegistrationFormattable =
+ MRegistration &
+ Use<'User', MUserId>
UserRight.MANAGE_ACCOUNTS_BLOCKLIST,
UserRight.MANAGE_SERVERS_BLOCKLIST,
UserRight.MANAGE_USERS,
- UserRight.SEE_ALL_COMMENTS
+ UserRight.SEE_ALL_COMMENTS,
+ UserRight.MANAGE_REGISTRATIONS
],
[UserRole.USER]: []
// Filter result used to check if a user can register on the instance
'filter:api.user.signup.allowed.result': true,
+ // Filter result used to check if a user can send a registration request on the instance
+ // PeerTube >= 5.1
+ 'filter:api.user.request-signup.allowed.result': true,
+
// Filter result used to check if video/torrent download is allowed
'filter:api.download.video.allowed.result': true,
'filter:api.download.torrent.allowed.result': true,
'action:api.user.unblocked': true,
// Fired when a user registered on the instance
'action:api.user.registered': true,
+ // Fired when a user requested registration on the instance
+ // PeerTube >= 5.1
+ 'action:api.user.requested-registration': true,
// Fired when an admin/moderator created a user
'action:api.user.created': true,
// Fired when a user is removed by an admin/moderator
signup: {
enabled: boolean
limit: number
+ requiresApproval: boolean
requiresEmailVerification: boolean
minimumAge: number
}
allowed: boolean
allowedForCurrentIP: boolean
requiresEmailVerification: boolean
+ requiresApproval: boolean
minimumAge: number
}
*/
INCORRECT_FILES_IN_TORRENT = 'incorrect_files_in_torrent',
- COMMENT_NOT_ASSOCIATED_TO_VIDEO = 'comment_not_associated_to_video'
+ COMMENT_NOT_ASSOCIATED_TO_VIDEO = 'comment_not_associated_to_video',
+
+ MISSING_TWO_FACTOR = 'missing_two_factor',
+ INVALID_TWO_FACTOR = 'invalid_two_factor',
+
+ ACCOUNT_WAITING_FOR_APPROVAL = 'account_waiting_for_approval',
+ ACCOUNT_APPROVAL_REJECTED = 'account_approval_rejected'
}
/**
*
* @see https://github.com/oauthjs/node-oauth2-server/blob/master/lib/errors/invalid-token-error.js
*/
- INVALID_TOKEN = 'invalid_token',
+ INVALID_TOKEN = 'invalid_token'
}
+export * from './registration'
export * from './two-factor-enable-result.model'
export * from './user-create-result.model'
export * from './user-create.model'
export * from './user-notification-setting.model'
export * from './user-notification.model'
export * from './user-refresh-token.model'
-export * from './user-register.model'
export * from './user-right.enum'
export * from './user-role'
export * from './user-scoped-token'
--- /dev/null
+export * from './user-register.model'
+export * from './user-registration-request.model'
+export * from './user-registration-state.model'
+export * from './user-registration-update-state.model'
+export * from './user-registration.model'
--- /dev/null
+import { UserRegister } from './user-register.model'
+
+export interface UserRegistrationRequest extends UserRegister {
+ registrationReason: string
+}
--- /dev/null
+export const enum UserRegistrationState {
+ PENDING = 1,
+ REJECTED = 2,
+ ACCEPTED = 3
+}
--- /dev/null
+export interface UserRegistrationUpdateState {
+ moderationResponse: string
+}
--- /dev/null
+import { UserRegistrationState } from './user-registration-state.model'
+
+export interface UserRegistration {
+ id: number
+
+ state: {
+ id: UserRegistrationState
+ label: string
+ }
+
+ registrationReason: string
+ moderationResponse: string
+
+ username: string
+ email: string
+ emailVerified: boolean
+
+ accountDisplayName: string
+
+ channelHandle: string
+ channelDisplayName: string
+
+ createdAt: Date
+ updatedAt: Date
+
+ user?: {
+ id: number
+ }
+}
NEW_PLUGIN_VERSION = 17,
NEW_PEERTUBE_VERSION = 18,
- MY_VIDEO_STUDIO_EDITION_FINISHED = 19
+ MY_VIDEO_STUDIO_EDITION_FINISHED = 19,
+
+ NEW_USER_REGISTRATION_REQUEST = 20
}
export interface VideoInfo {
latestVersion: string
}
+ registration?: {
+ id: number
+ username: string
+ }
+
createdAt: string
updatedAt: string
}
MANAGE_VIDEO_FILES = 25,
RUN_VIDEO_TRANSCODING = 26,
- MANAGE_VIDEO_IMPORTS = 27
+ MANAGE_VIDEO_IMPORTS = 27,
+
+ MANAGE_REGISTRATIONS = 28
}