Implement signup approval in server
This commit is contained in:
parent
bc48e33b80
commit
e364e31e25
|
@ -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: []
|
||||
|
|
|
@ -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: []
|
||||
|
|
|
@ -74,6 +74,7 @@ cache:
|
|||
|
||||
signup:
|
||||
enabled: true
|
||||
requires_approval: false
|
||||
requires_email_verification: false
|
||||
|
||||
transcoding:
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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())
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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.')
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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') },
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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]) {
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;')
|
||||
|
|
|
@ -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}]
|
|
@ -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}
|
|
@ -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].
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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]: []
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -83,6 +83,7 @@ export interface CustomConfig {
|
|||
signup: {
|
||||
enabled: boolean
|
||||
limit: number
|
||||
requiresApproval: boolean
|
||||
requiresEmailVerification: boolean
|
||||
minimumAge: number
|
||||
}
|
||||
|
|
|
@ -131,6 +131,7 @@ export interface ServerConfig {
|
|||
allowed: boolean
|
||||
allowedForCurrentIP: boolean
|
||||
requiresEmailVerification: boolean
|
||||
requiresApproval: boolean
|
||||
minimumAge: number
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
|
@ -0,0 +1,5 @@
|
|||
import { UserRegister } from './user-register.model'
|
||||
|
||||
export interface UserRegistrationRequest extends UserRegister {
|
||||
registrationReason: string
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export const enum UserRegistrationState {
|
||||
PENDING = 1,
|
||||
REJECTED = 2,
|
||||
ACCEPTED = 3
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface UserRegistrationUpdateState {
|
||||
moderationResponse: string
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue