Implement signup approval in server

pull/5548/head
Chocobozzz 2023-01-19 09:27:16 +01:00 committed by Chocobozzz
parent bc48e33b80
commit e364e31e25
59 changed files with 1561 additions and 448 deletions

View File

@ -382,9 +382,15 @@ contact_form:
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: []

View File

@ -392,9 +392,15 @@ contact_form:
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: []

View File

@ -74,6 +74,7 @@ cache:
signup:
enabled: true
requires_approval: false
requires_email_verification: false
transcoding:

View File

@ -193,6 +193,7 @@ function customConfig (): CustomConfig {
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
},

View File

@ -0,0 +1,72 @@
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()
}

View File

@ -4,26 +4,21 @@ import { Hooks } from '@server/lib/plugins/hooks'
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,
@ -31,19 +26,17 @@ import {
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'
@ -51,22 +44,14 @@ import { myVideosHistoryRouter } from './my-history'
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)
@ -122,14 +107,6 @@ usersRouter.post('/',
asyncRetryTransactionMiddleware(createUser)
)
usersRouter.post('/register',
signupRateLimiter,
asyncMiddleware(ensureUserRegistrationAllowed),
ensureUserRegistrationAllowedForIP,
asyncMiddleware(usersRegisterValidator),
asyncRetryTransactionMiddleware(registerUser)
)
usersRouter.put('/:id',
authenticate,
ensureUserHasRight(UserRight.MANAGE_USERS),
@ -156,17 +133,6 @@ usersRouter.post('/:id/reset-password',
asyncMiddleware(resetUserPassword)
)
usersRouter.post('/ask-send-verify-email',
askSendEmailLimiter,
asyncMiddleware(usersAskSendVerifyEmailValidator),
asyncMiddleware(reSendVerifyUserEmail)
)
usersRouter.post('/:id/verify-email',
asyncMiddleware(usersVerifyEmailValidator),
asyncMiddleware(verifyUserEmail)
)
// ---------------------------------------------------------------------------
export {
@ -218,35 +184,6 @@ async function createUser (req: express.Request, res: express.Response) {
})
}
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
@ -360,28 +297,6 @@ async function resetUserPassword (req: express.Request, res: express.Response) {
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())

View File

@ -0,0 +1,236 @@
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)
}

View File

@ -0,0 +1,25 @@
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
}

View File

@ -116,6 +116,11 @@ function checkEmailConfig () {
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.')
}

View File

@ -28,7 +28,7 @@ function checkMissedConfig () {
'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',

View File

@ -305,6 +305,7 @@ const CONFIG = {
},
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') },

View File

@ -6,6 +6,7 @@ import { randomInt, root } from '@shared/core-utils'
import {
AbuseState,
JobType,
UserRegistrationState,
VideoChannelSyncState,
VideoImportState,
VideoPrivacy,
@ -25,7 +26,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 745
const LAST_MIGRATION_VERSION = 750
// ---------------------------------------------------------------------------
@ -78,6 +79,8 @@ const SORTABLE_COLUMNS = {
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
@ -290,6 +293,10 @@ const CONSTRAINTS_FIELDS = {
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
},
@ -516,6 +523,12 @@ const ABUSE_STATES: { [ id in AbuseState ]: string } = {
[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',
@ -660,7 +673,7 @@ const USER_PASSWORD_CREATE_LIFETIME = 60000 * 60 * 24 * 7 // 7 days
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',
@ -1069,13 +1082,14 @@ export {
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,

View File

@ -5,7 +5,9 @@ import { TrackerModel } from '@server/models/server/tracker'
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'
@ -50,7 +52,6 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla
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
@ -155,7 +156,8 @@ async function initDatabaseModels (silent: boolean) {
PluginModel,
ActorCustomPageModel,
VideoJobInfoModel,
VideoChannelSyncModel
VideoChannelSyncModel,
UserRegistrationModel
])
// Check extensions exist in the database

View File

@ -0,0 +1,58 @@
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
}

View File

@ -11,20 +11,31 @@ import OAuth2Server, {
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
}
/**
@ -128,7 +139,17 @@ async function handlePasswordGrant (options: {
}
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]) {

View File

@ -3,13 +3,13 @@ import { merge } from 'lodash'
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')
@ -62,7 +62,9 @@ class Emailer {
subject: 'Reset your account password',
locals: {
username,
resetPasswordUrl
resetPasswordUrl,
hideNotificationPreferencesLink: true
}
}
@ -76,21 +78,33 @@ class Emailer {
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
}
}
@ -123,7 +137,33 @@ class Emailer {
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'
}
}

View File

@ -222,19 +222,9 @@ body(width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule:
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;')

View File

@ -0,0 +1,10 @@
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}]

View File

@ -0,0 +1,9 @@
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}

View File

@ -0,0 +1,9 @@
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].

View File

@ -1,17 +1,19 @@
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.

View File

@ -1,4 +1,4 @@
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'
@ -13,6 +13,7 @@ import {
AbuseStateChangeForReporter,
AutoFollowForInstance,
CommentMention,
DirectRegistrationForModerators,
FollowForInstance,
FollowForUser,
ImportFinishedForOwner,
@ -30,7 +31,7 @@ import {
OwnedPublicationAfterAutoUnblacklist,
OwnedPublicationAfterScheduleUpdate,
OwnedPublicationAfterTranscoding,
RegistrationForModerators,
RegistrationRequestForModerators,
StudioEditionFinishedForOwner,
UnblacklistForOwner
} from './shared'
@ -47,7 +48,8 @@ class Notifier {
newBlacklist: [ NewBlacklistForOwner ],
unblacklist: [ UnblacklistForOwner ],
importFinished: [ ImportFinishedForOwner ],
userRegistration: [ RegistrationForModerators ],
directRegistration: [ DirectRegistrationForModerators ],
registrationRequest: [ RegistrationRequestForModerators ],
userFollow: [ FollowForUser ],
instanceFollow: [ FollowForInstance ],
autoInstanceFollow: [ AutoFollowForInstance ],
@ -138,13 +140,20 @@ class Notifier {
})
}
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

View File

@ -6,7 +6,7 @@ import { MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi
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 () {
@ -40,7 +40,7 @@ export class RegistrationForModerators extends AbstractNotification <MUserDefaul
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
}

View File

@ -1,3 +1,4 @@
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'

View File

@ -0,0 +1,48 @@
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
}
}
}
}

View File

@ -9,7 +9,7 @@ import {
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,
@ -124,16 +124,28 @@ class Redis {
/* ************ 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 ************ */
@ -346,8 +358,12 @@ class Redis {
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) {

View File

@ -261,10 +261,17 @@ class ServerConfigManager {
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)
@ -273,6 +280,7 @@ class ServerConfigManager {
allowed,
allowedForCurrentIP,
minimumAge: CONFIG.SIGNUP.MINIMUM_AGE,
requiresApproval: CONFIG.SIGNUP.REQUIRES_APPROVAL,
requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
}

View File

@ -4,11 +4,24 @@ import { UserModel } from '../models/user/user'
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 }

View File

@ -10,7 +10,7 @@ import { sequelizeTypescript } from '../initializers/database'
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'
@ -97,7 +97,7 @@ async function createUserAccountAndChannelAndPlaylist (parameters: {
})
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)
@ -160,15 +160,28 @@ async function createApplicationActor (applicationId: number) {
// ---------------------------------------------------------------------------
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 })
}
// ---------------------------------------------------------------------------
@ -232,7 +245,10 @@ export {
createApplicationActor,
createUserAccountAndChannelAndPlaylist,
createLocalAccountWithoutKeys,
sendVerifyUserEmail,
sendVerifyRegistrationEmail,
isAbleToUploadVideo,
buildUser
}
@ -264,7 +280,13 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction |
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)

View File

@ -29,6 +29,7 @@ const customConfigUpdateValidator = [
body('signup.enabled').isBoolean(),
body('signup.limit').isInt(),
body('signup.requiresEmailVerification').isBoolean(),
body('signup.requiresApproval').isBoolean(),
body('signup.minimumAge').isInt(),
body('admin.email').isEmail(),

View File

@ -21,8 +21,10 @@ export * from './server'
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'

View File

@ -0,0 +1,60 @@
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
}

View File

@ -14,7 +14,7 @@ function checkUserEmailExist (email: string, res: express.Response, abortRespons
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) {
@ -58,6 +58,6 @@ async function checkUserExist (finder: () => Promise<MUserDefault>, res: express
export {
checkUserIdExist,
checkUserEmailExist,
checkUserNameOrEmailDoesNotAlreadyExist,
checkUserNameOrEmailDoNotAlreadyExist,
checkUserExist
}

View File

@ -1,9 +1,41 @@
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)
}
@ -27,64 +59,3 @@ function createSortableColumns (sortableColumns: string[]) {
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
}

View File

@ -0,0 +1,94 @@
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
}

View File

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

View File

@ -1,8 +1,7 @@
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 {
@ -24,17 +23,16 @@ 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
@ -81,7 +79,7 @@ const usersAddValidator = [
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) {
@ -109,51 +107,6 @@ const usersAddValidator = [
}
]
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),
@ -365,45 +318,6 @@ const usersVideosValidator = [
}
]
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(),
@ -455,58 +369,6 @@ const usersResetPasswordValidator = [
}
]
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),
@ -603,21 +465,16 @@ export {
usersListValidator,
usersAddValidator,
deleteMeValidator,
usersRegisterValidator,
usersBlockingValidator,
usersRemoveValidator,
usersUpdateValidator,
usersUpdateMeValidator,
usersVideoRatingValidator,
usersCheckCurrentPasswordFactory,
ensureUserRegistrationAllowed,
ensureUserRegistrationAllowedForIP,
usersGetValidator,
usersVideosValidator,
usersAskResetPasswordValidator,
usersResetPasswordValidator,
usersAskSendVerifyEmailValidator,
usersVerifyEmailValidator,
userAutocompleteValidator,
ensureAuthUserOwnsAccountValidator,
ensureCanModerateUser,

View File

@ -180,7 +180,9 @@ export class UserNotificationListQueryBuilder extends AbstractRunQuery {
"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 () {
@ -196,74 +198,76 @@ export class UserNotificationListQueryBuilder extends AbstractRunQuery {
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"`
}
}

View File

@ -20,6 +20,7 @@ import { VideoCommentModel } from '../video/video-comment'
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',
@ -98,6 +99,14 @@ import { UserModel } from './user'
[Op.ne]: null
}
}
},
{
fields: [ 'userRegistrationId' ],
where: {
userRegistrationId: {
[Op.ne]: null
}
}
}
] as (ModelIndexesOptions & { where?: WhereOptions })[]
})
@ -241,6 +250,18 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
})
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 }
@ -416,6 +437,10 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
? { 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,
@ -429,6 +454,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
actorFollow,
plugin,
peertube,
registration,
createdAt: this.createdAt.toISOString(),
updatedAt: this.updatedAt.toISOString()
}

View File

@ -0,0 +1,259 @@
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
}
}
}

View File

@ -441,16 +441,17 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
})
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

View File

@ -8,6 +8,7 @@ import {
MActorUrl,
MChannelBannerAccountDefault,
MChannelSyncChannel,
MRegistration,
MStreamingPlaylist,
MUserAccountUrl,
MVideoChangeOwnershipFull,
@ -171,6 +172,7 @@ declare module 'express' {
actorFull?: MActorFull
user?: MUserDefault
userRegistration?: MRegistration
server?: MServer

View File

@ -1,4 +1,5 @@
export * from './user'
export * from './user-notification'
export * from './user-notification-setting'
export * from './user-registration'
export * from './user-video-history'

View File

@ -3,6 +3,7 @@ import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse
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'
@ -94,13 +95,16 @@ export module UserNotificationIncludes {
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'>
// ############################################################################
@ -114,4 +118,5 @@ export type UserNotificationModelForApi =
Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> &
Use<'Plugin', UserNotificationIncludes.PluginInclude> &
Use<'Application', UserNotificationIncludes.ApplicationInclude> &
Use<'Account', UserNotificationIncludes.AccountIncludeActor>
Use<'Account', UserNotificationIncludes.AccountIncludeActor> &
Use<'UserRegistration', UserNotificationIncludes.UserRegistrationInclude>

View File

@ -0,0 +1,15 @@
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>

View File

@ -23,7 +23,8 @@ const userRoleRights: { [ id in UserRole ]: UserRight[] } = {
UserRight.MANAGE_ACCOUNTS_BLOCKLIST,
UserRight.MANAGE_SERVERS_BLOCKLIST,
UserRight.MANAGE_USERS,
UserRight.SEE_ALL_COMMENTS
UserRight.SEE_ALL_COMMENTS,
UserRight.MANAGE_REGISTRATIONS
],
[UserRole.USER]: []

View File

@ -91,6 +91,10 @@ export const serverFilterHookObject = {
// 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,
@ -156,6 +160,9 @@ export const serverActionHookObject = {
'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

View File

@ -83,6 +83,7 @@ export interface CustomConfig {
signup: {
enabled: boolean
limit: number
requiresApproval: boolean
requiresEmailVerification: boolean
minimumAge: number
}

View File

@ -131,6 +131,7 @@ export interface ServerConfig {
allowed: boolean
allowedForCurrentIP: boolean
requiresEmailVerification: boolean
requiresApproval: boolean
minimumAge: number
}

View File

@ -39,7 +39,13 @@ export const enum ServerErrorCode {
*/
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'
}
/**
@ -70,5 +76,5 @@ export const enum OAuth2ErrorCode {
*
* @see https://github.com/oauthjs/node-oauth2-server/blob/master/lib/errors/invalid-token-error.js
*/
INVALID_TOKEN = 'invalid_token',
INVALID_TOKEN = 'invalid_token'
}

View File

@ -1,3 +1,4 @@
export * from './registration'
export * from './two-factor-enable-result.model'
export * from './user-create-result.model'
export * from './user-create.model'
@ -6,7 +7,6 @@ export * from './user-login.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'

View File

@ -0,0 +1,5 @@
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'

View File

@ -0,0 +1,5 @@
import { UserRegister } from './user-register.model'
export interface UserRegistrationRequest extends UserRegister {
registrationReason: string
}

View File

@ -0,0 +1,5 @@
export const enum UserRegistrationState {
PENDING = 1,
REJECTED = 2,
ACCEPTED = 3
}

View File

@ -0,0 +1,3 @@
export interface UserRegistrationUpdateState {
moderationResponse: string
}

View File

@ -0,0 +1,29 @@
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
}
}

View File

@ -32,7 +32,9 @@ export const enum UserNotificationType {
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 {
@ -126,6 +128,11 @@ export interface UserNotification {
latestVersion: string
}
registration?: {
id: number
username: string
}
createdAt: string
updatedAt: string
}

View File

@ -43,5 +43,7 @@ export const enum UserRight {
MANAGE_VIDEO_FILES = 25,
RUN_VIDEO_TRANSCODING = 26,
MANAGE_VIDEO_IMPORTS = 27
MANAGE_VIDEO_IMPORTS = 27,
MANAGE_REGISTRATIONS = 28
}