Implement signup approval in server
This commit is contained in:
parent
bc48e33b80
commit
e364e31e25
|
@ -382,9 +382,15 @@ contact_form:
|
||||||
|
|
||||||
signup:
|
signup:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
|
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
|
||||||
|
|
||||||
minimum_age: 16 # Used to configure the signup form
|
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
|
requires_email_verification: false
|
||||||
|
|
||||||
filters:
|
filters:
|
||||||
cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist
|
cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist
|
||||||
whitelist: []
|
whitelist: []
|
||||||
|
|
|
@ -392,9 +392,15 @@ contact_form:
|
||||||
|
|
||||||
signup:
|
signup:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
|
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
|
||||||
|
|
||||||
minimum_age: 16 # Used to configure the signup form
|
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
|
requires_email_verification: false
|
||||||
|
|
||||||
filters:
|
filters:
|
||||||
cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist
|
cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist
|
||||||
whitelist: []
|
whitelist: []
|
||||||
|
|
|
@ -74,6 +74,7 @@ cache:
|
||||||
|
|
||||||
signup:
|
signup:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
requires_approval: false
|
||||||
requires_email_verification: false
|
requires_email_verification: false
|
||||||
|
|
||||||
transcoding:
|
transcoding:
|
||||||
|
|
|
@ -193,6 +193,7 @@ function customConfig (): CustomConfig {
|
||||||
signup: {
|
signup: {
|
||||||
enabled: CONFIG.SIGNUP.ENABLED,
|
enabled: CONFIG.SIGNUP.ENABLED,
|
||||||
limit: CONFIG.SIGNUP.LIMIT,
|
limit: CONFIG.SIGNUP.LIMIT,
|
||||||
|
requiresApproval: CONFIG.SIGNUP.REQUIRES_APPROVAL,
|
||||||
requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION,
|
requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION,
|
||||||
minimumAge: CONFIG.SIGNUP.MINIMUM_AGE
|
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 { OAuthTokenModel } from '@server/models/oauth/oauth-token'
|
||||||
import { MUserAccountDefault } from '@server/types/models'
|
import { MUserAccountDefault } from '@server/types/models'
|
||||||
import { pick } from '@shared/core-utils'
|
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 { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { generateRandomString, getFormattedObjects } from '../../../helpers/utils'
|
import { generateRandomString, getFormattedObjects } from '../../../helpers/utils'
|
||||||
import { CONFIG } from '../../../initializers/config'
|
|
||||||
import { WEBSERVER } from '../../../initializers/constants'
|
import { WEBSERVER } from '../../../initializers/constants'
|
||||||
import { sequelizeTypescript } from '../../../initializers/database'
|
import { sequelizeTypescript } from '../../../initializers/database'
|
||||||
import { Emailer } from '../../../lib/emailer'
|
import { Emailer } from '../../../lib/emailer'
|
||||||
import { Notifier } from '../../../lib/notifier'
|
|
||||||
import { Redis } from '../../../lib/redis'
|
import { Redis } from '../../../lib/redis'
|
||||||
import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
|
import { buildUser, createUserAccountAndChannelAndPlaylist } from '../../../lib/user'
|
||||||
import {
|
import {
|
||||||
adminUsersSortValidator,
|
adminUsersSortValidator,
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
asyncRetryTransactionMiddleware,
|
asyncRetryTransactionMiddleware,
|
||||||
authenticate,
|
authenticate,
|
||||||
buildRateLimiter,
|
|
||||||
ensureUserHasRight,
|
ensureUserHasRight,
|
||||||
ensureUserRegistrationAllowed,
|
|
||||||
ensureUserRegistrationAllowedForIP,
|
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
setDefaultSort,
|
setDefaultSort,
|
||||||
|
@ -31,19 +26,17 @@ import {
|
||||||
usersAddValidator,
|
usersAddValidator,
|
||||||
usersGetValidator,
|
usersGetValidator,
|
||||||
usersListValidator,
|
usersListValidator,
|
||||||
usersRegisterValidator,
|
|
||||||
usersRemoveValidator,
|
usersRemoveValidator,
|
||||||
usersUpdateValidator
|
usersUpdateValidator
|
||||||
} from '../../../middlewares'
|
} from '../../../middlewares'
|
||||||
import {
|
import {
|
||||||
ensureCanModerateUser,
|
ensureCanModerateUser,
|
||||||
usersAskResetPasswordValidator,
|
usersAskResetPasswordValidator,
|
||||||
usersAskSendVerifyEmailValidator,
|
|
||||||
usersBlockingValidator,
|
usersBlockingValidator,
|
||||||
usersResetPasswordValidator,
|
usersResetPasswordValidator
|
||||||
usersVerifyEmailValidator
|
|
||||||
} from '../../../middlewares/validators'
|
} from '../../../middlewares/validators'
|
||||||
import { UserModel } from '../../../models/user/user'
|
import { UserModel } from '../../../models/user/user'
|
||||||
|
import { emailVerificationRouter } from './email-verification'
|
||||||
import { meRouter } from './me'
|
import { meRouter } from './me'
|
||||||
import { myAbusesRouter } from './my-abuses'
|
import { myAbusesRouter } from './my-abuses'
|
||||||
import { myBlocklistRouter } from './my-blocklist'
|
import { myBlocklistRouter } from './my-blocklist'
|
||||||
|
@ -51,22 +44,14 @@ import { myVideosHistoryRouter } from './my-history'
|
||||||
import { myNotificationsRouter } from './my-notifications'
|
import { myNotificationsRouter } from './my-notifications'
|
||||||
import { mySubscriptionsRouter } from './my-subscriptions'
|
import { mySubscriptionsRouter } from './my-subscriptions'
|
||||||
import { myVideoPlaylistsRouter } from './my-video-playlists'
|
import { myVideoPlaylistsRouter } from './my-video-playlists'
|
||||||
|
import { registrationsRouter } from './registrations'
|
||||||
import { twoFactorRouter } from './two-factor'
|
import { twoFactorRouter } from './two-factor'
|
||||||
|
|
||||||
const auditLogger = auditLoggerFactory('users')
|
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()
|
const usersRouter = express.Router()
|
||||||
|
usersRouter.use('/', emailVerificationRouter)
|
||||||
|
usersRouter.use('/', registrationsRouter)
|
||||||
usersRouter.use('/', twoFactorRouter)
|
usersRouter.use('/', twoFactorRouter)
|
||||||
usersRouter.use('/', tokensRouter)
|
usersRouter.use('/', tokensRouter)
|
||||||
usersRouter.use('/', myNotificationsRouter)
|
usersRouter.use('/', myNotificationsRouter)
|
||||||
|
@ -122,14 +107,6 @@ usersRouter.post('/',
|
||||||
asyncRetryTransactionMiddleware(createUser)
|
asyncRetryTransactionMiddleware(createUser)
|
||||||
)
|
)
|
||||||
|
|
||||||
usersRouter.post('/register',
|
|
||||||
signupRateLimiter,
|
|
||||||
asyncMiddleware(ensureUserRegistrationAllowed),
|
|
||||||
ensureUserRegistrationAllowedForIP,
|
|
||||||
asyncMiddleware(usersRegisterValidator),
|
|
||||||
asyncRetryTransactionMiddleware(registerUser)
|
|
||||||
)
|
|
||||||
|
|
||||||
usersRouter.put('/:id',
|
usersRouter.put('/:id',
|
||||||
authenticate,
|
authenticate,
|
||||||
ensureUserHasRight(UserRight.MANAGE_USERS),
|
ensureUserHasRight(UserRight.MANAGE_USERS),
|
||||||
|
@ -156,17 +133,6 @@ usersRouter.post('/:id/reset-password',
|
||||||
asyncMiddleware(resetUserPassword)
|
asyncMiddleware(resetUserPassword)
|
||||||
)
|
)
|
||||||
|
|
||||||
usersRouter.post('/ask-send-verify-email',
|
|
||||||
askSendEmailLimiter,
|
|
||||||
asyncMiddleware(usersAskSendVerifyEmailValidator),
|
|
||||||
asyncMiddleware(reSendVerifyUserEmail)
|
|
||||||
)
|
|
||||||
|
|
||||||
usersRouter.post('/:id/verify-email',
|
|
||||||
asyncMiddleware(usersVerifyEmailValidator),
|
|
||||||
asyncMiddleware(verifyUserEmail)
|
|
||||||
)
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
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) {
|
async function unblockUser (req: express.Request, res: express.Response) {
|
||||||
const user = res.locals.user
|
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()
|
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) {
|
async function changeUserBlock (res: express.Response, user: MUserAccountDefault, block: boolean, reason?: string) {
|
||||||
const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
|
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.')
|
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) {
|
if (CONFIG.CONTACT_FORM.ENABLED) {
|
||||||
logger.warn('Emailer is disabled so the contact form will not work.')
|
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',
|
'csp.enabled', 'csp.report_only', 'csp.report_uri',
|
||||||
'security.frameguard.enabled',
|
'security.frameguard.enabled',
|
||||||
'cache.previews.size', 'cache.captions.size', 'cache.torrents.size', 'admin.email', 'contact_form.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',
|
'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
|
||||||
'redundancy.videos.strategies', 'redundancy.videos.check_interval',
|
'redundancy.videos.strategies', 'redundancy.videos.check_interval',
|
||||||
'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 'transcoding.hls.enabled',
|
'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 'transcoding.hls.enabled',
|
||||||
|
|
|
@ -305,6 +305,7 @@ const CONFIG = {
|
||||||
},
|
},
|
||||||
SIGNUP: {
|
SIGNUP: {
|
||||||
get ENABLED () { return config.get<boolean>('signup.enabled') },
|
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 LIMIT () { return config.get<number>('signup.limit') },
|
||||||
get REQUIRES_EMAIL_VERIFICATION () { return config.get<boolean>('signup.requires_email_verification') },
|
get REQUIRES_EMAIL_VERIFICATION () { return config.get<boolean>('signup.requires_email_verification') },
|
||||||
get MINIMUM_AGE () { return config.get<number>('signup.minimum_age') },
|
get MINIMUM_AGE () { return config.get<number>('signup.minimum_age') },
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { randomInt, root } from '@shared/core-utils'
|
||||||
import {
|
import {
|
||||||
AbuseState,
|
AbuseState,
|
||||||
JobType,
|
JobType,
|
||||||
|
UserRegistrationState,
|
||||||
VideoChannelSyncState,
|
VideoChannelSyncState,
|
||||||
VideoImportState,
|
VideoImportState,
|
||||||
VideoPrivacy,
|
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' ],
|
ACCOUNT_FOLLOWERS: [ 'createdAt' ],
|
||||||
CHANNEL_FOLLOWERS: [ 'createdAt' ],
|
CHANNEL_FOLLOWERS: [ 'createdAt' ],
|
||||||
|
|
||||||
|
USER_REGISTRATIONS: [ 'createdAt', 'state' ],
|
||||||
|
|
||||||
VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot', 'best' ],
|
VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot', 'best' ],
|
||||||
|
|
||||||
// Don't forget to update peertube-search-index with the same values
|
// Don't forget to update peertube-search-index with the same values
|
||||||
|
@ -290,6 +293,10 @@ const CONSTRAINTS_FIELDS = {
|
||||||
ABUSE_MESSAGES: {
|
ABUSE_MESSAGES: {
|
||||||
MESSAGE: { min: 2, max: 3000 } // Length
|
MESSAGE: { min: 2, max: 3000 } // Length
|
||||||
},
|
},
|
||||||
|
USER_REGISTRATIONS: {
|
||||||
|
REASON_MESSAGE: { min: 2, max: 3000 }, // Length
|
||||||
|
MODERATOR_MESSAGE: { min: 2, max: 3000 } // Length
|
||||||
|
},
|
||||||
VIDEO_BLACKLIST: {
|
VIDEO_BLACKLIST: {
|
||||||
REASON: { min: 2, max: 300 } // Length
|
REASON: { min: 2, max: 300 } // Length
|
||||||
},
|
},
|
||||||
|
@ -516,6 +523,12 @@ const ABUSE_STATES: { [ id in AbuseState ]: string } = {
|
||||||
[AbuseState.ACCEPTED]: 'Accepted'
|
[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 } = {
|
const VIDEO_PLAYLIST_PRIVACIES: { [ id in VideoPlaylistPrivacy ]: string } = {
|
||||||
[VideoPlaylistPrivacy.PUBLIC]: 'Public',
|
[VideoPlaylistPrivacy.PUBLIC]: 'Public',
|
||||||
[VideoPlaylistPrivacy.UNLISTED]: 'Unlisted',
|
[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 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 } = {
|
const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = {
|
||||||
DO_NOT_LIST: 'do_not_list',
|
DO_NOT_LIST: 'do_not_list',
|
||||||
|
@ -1069,13 +1082,14 @@ export {
|
||||||
VIDEO_TRANSCODING_FPS,
|
VIDEO_TRANSCODING_FPS,
|
||||||
FFMPEG_NICE,
|
FFMPEG_NICE,
|
||||||
ABUSE_STATES,
|
ABUSE_STATES,
|
||||||
|
USER_REGISTRATION_STATES,
|
||||||
LRU_CACHE,
|
LRU_CACHE,
|
||||||
REQUEST_TIMEOUTS,
|
REQUEST_TIMEOUTS,
|
||||||
MAX_LOCAL_VIEWER_WATCH_SECTIONS,
|
MAX_LOCAL_VIEWER_WATCH_SECTIONS,
|
||||||
USER_PASSWORD_RESET_LIFETIME,
|
USER_PASSWORD_RESET_LIFETIME,
|
||||||
USER_PASSWORD_CREATE_LIFETIME,
|
USER_PASSWORD_CREATE_LIFETIME,
|
||||||
MEMOIZE_TTL,
|
MEMOIZE_TTL,
|
||||||
USER_EMAIL_VERIFY_LIFETIME,
|
EMAIL_VERIFY_LIFETIME,
|
||||||
OVERVIEWS,
|
OVERVIEWS,
|
||||||
SCHEDULER_INTERVALS_MS,
|
SCHEDULER_INTERVALS_MS,
|
||||||
REPEAT_JOBS,
|
REPEAT_JOBS,
|
||||||
|
|
|
@ -5,7 +5,9 @@ import { TrackerModel } from '@server/models/server/tracker'
|
||||||
import { VideoTrackerModel } from '@server/models/server/video-tracker'
|
import { VideoTrackerModel } from '@server/models/server/video-tracker'
|
||||||
import { UserModel } from '@server/models/user/user'
|
import { UserModel } from '@server/models/user/user'
|
||||||
import { UserNotificationModel } from '@server/models/user/user-notification'
|
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 { 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 { VideoJobInfoModel } from '@server/models/video/video-job-info'
|
||||||
import { VideoLiveSessionModel } from '@server/models/video/video-live-session'
|
import { VideoLiveSessionModel } from '@server/models/video/video-live-session'
|
||||||
import { VideoSourceModel } from '@server/models/video/video-source'
|
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 { VideoTagModel } from '../models/video/video-tag'
|
||||||
import { VideoViewModel } from '../models/view/video-view'
|
import { VideoViewModel } from '../models/view/video-view'
|
||||||
import { CONFIG } from './config'
|
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
|
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
||||||
|
|
||||||
|
@ -155,7 +156,8 @@ async function initDatabaseModels (silent: boolean) {
|
||||||
PluginModel,
|
PluginModel,
|
||||||
ActorCustomPageModel,
|
ActorCustomPageModel,
|
||||||
VideoJobInfoModel,
|
VideoJobInfoModel,
|
||||||
VideoChannelSyncModel
|
VideoChannelSyncModel,
|
||||||
|
UserRegistrationModel
|
||||||
])
|
])
|
||||||
|
|
||||||
// Check extensions exist in the database
|
// 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 { randomBytesPromise } from '@server/helpers/core-utils'
|
||||||
import { isOTPValid } from '@server/helpers/otp'
|
import { isOTPValid } from '@server/helpers/otp'
|
||||||
import { CONFIG } from '@server/initializers/config'
|
import { CONFIG } from '@server/initializers/config'
|
||||||
|
import { UserRegistrationModel } from '@server/models/user/user-registration'
|
||||||
import { MOAuthClient } from '@server/types/models'
|
import { MOAuthClient } from '@server/types/models'
|
||||||
import { sha1 } from '@shared/extra-utils'
|
import { sha1 } from '@shared/extra-utils'
|
||||||
import { HttpStatusCode } from '@shared/models'
|
import { HttpStatusCode, ServerErrorCode, UserRegistrationState } from '@shared/models'
|
||||||
import { OTP } from '../../initializers/constants'
|
import { OTP } from '../../initializers/constants'
|
||||||
import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model'
|
import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model'
|
||||||
|
|
||||||
class MissingTwoFactorError extends Error {
|
class MissingTwoFactorError extends Error {
|
||||||
code = HttpStatusCode.UNAUTHORIZED_401
|
code = HttpStatusCode.UNAUTHORIZED_401
|
||||||
name = 'missing_two_factor'
|
name = ServerErrorCode.MISSING_TWO_FACTOR
|
||||||
}
|
}
|
||||||
|
|
||||||
class InvalidTwoFactorError extends Error {
|
class InvalidTwoFactorError extends Error {
|
||||||
code = HttpStatusCode.BAD_REQUEST_400
|
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)
|
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 (user.otpSecret) {
|
||||||
if (!request.headers[OTP.HEADER_NAME]) {
|
if (!request.headers[OTP.HEADER_NAME]) {
|
||||||
|
|
|
@ -3,13 +3,13 @@ import { merge } from 'lodash'
|
||||||
import { createTransport, Transporter } from 'nodemailer'
|
import { createTransport, Transporter } from 'nodemailer'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { arrayify, root } from '@shared/core-utils'
|
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 { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model'
|
||||||
import { isTestOrDevInstance } from '../helpers/core-utils'
|
import { isTestOrDevInstance } from '../helpers/core-utils'
|
||||||
import { bunyanLogger, logger } from '../helpers/logger'
|
import { bunyanLogger, logger } from '../helpers/logger'
|
||||||
import { CONFIG, isEmailEnabled } from '../initializers/config'
|
import { CONFIG, isEmailEnabled } from '../initializers/config'
|
||||||
import { WEBSERVER } from '../initializers/constants'
|
import { WEBSERVER } from '../initializers/constants'
|
||||||
import { MUser } from '../types/models'
|
import { MRegistration, MUser } from '../types/models'
|
||||||
import { JobQueue } from './job-queue'
|
import { JobQueue } from './job-queue'
|
||||||
|
|
||||||
const Email = require('email-templates')
|
const Email = require('email-templates')
|
||||||
|
@ -62,7 +62,9 @@ class Emailer {
|
||||||
subject: 'Reset your account password',
|
subject: 'Reset your account password',
|
||||||
locals: {
|
locals: {
|
||||||
username,
|
username,
|
||||||
resetPasswordUrl
|
resetPasswordUrl,
|
||||||
|
|
||||||
|
hideNotificationPreferencesLink: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,21 +78,33 @@ class Emailer {
|
||||||
subject: 'Create your account password',
|
subject: 'Create your account password',
|
||||||
locals: {
|
locals: {
|
||||||
username,
|
username,
|
||||||
createPasswordUrl
|
createPasswordUrl,
|
||||||
|
|
||||||
|
hideNotificationPreferencesLink: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
|
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 = {
|
const emailPayload: EmailPayload = {
|
||||||
template: 'verify-email',
|
template: 'verify-email',
|
||||||
to: [ to ],
|
to: [ to ],
|
||||||
subject: `Verify your email on ${CONFIG.INSTANCE.NAME}`,
|
subject: `Verify your email on ${CONFIG.INSTANCE.NAME}`,
|
||||||
locals: {
|
locals: {
|
||||||
username,
|
username,
|
||||||
verifyEmailUrl
|
verifyEmailUrl,
|
||||||
|
isRegistrationRequest,
|
||||||
|
|
||||||
|
hideNotificationPreferencesLink: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,7 +137,33 @@ class Emailer {
|
||||||
body,
|
body,
|
||||||
|
|
||||||
// There are not notification preferences for the contact form
|
// 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;')
|
td(aria-hidden='true' height='20' style='font-size: 0px; line-height: 0px;')
|
||||||
br
|
br
|
||||||
//- Clear Spacer : END
|
//- 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 Body : END
|
||||||
//- Email Footer : BEGIN
|
//- Email Footer : BEGIN
|
||||||
unless hideNotificationPreferences
|
unless hideNotificationPreferencesLink
|
||||||
table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;')
|
table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;')
|
||||||
tr
|
tr
|
||||||
td(style='padding: 20px; padding-bottom: 0px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;')
|
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
|
extends ../common/greetings
|
||||||
|
|
||||||
block title
|
block title
|
||||||
| Account verification
|
| Email verification
|
||||||
|
|
||||||
block content
|
block content
|
||||||
p Welcome to #{instanceName}!
|
if isRegistrationRequest
|
||||||
p.
|
p You just requested an account on #[a(href=WEBSERVER.URL) #{instanceName}].
|
||||||
You just created an account at #[a(href=WEBSERVER.URL) #{instanceName}].
|
else
|
||||||
Your username there is: #{username}.
|
p You just created an account on #[a(href=WEBSERVER.URL) #{instanceName}].
|
||||||
p.
|
|
||||||
To start using your account you must verify your email first!
|
if isRegistrationRequest
|
||||||
Please follow #[a(href=verifyEmailUrl) this link] to verify this email belongs to you.
|
p To complete your registration request you must verify your email first!
|
||||||
p.
|
else
|
||||||
If you can't see the verification link above you can use the following link #[a(href=verifyEmailUrl) #{verifyEmailUrl}]
|
p To start using your account you must verify your email first!
|
||||||
p.
|
|
||||||
If you are not the person who initiated this request, please ignore this email.
|
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 { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
|
||||||
import { UserNotificationSettingValue } from '../../../shared/models/users'
|
import { UserNotificationSettingValue } from '../../../shared/models/users'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
|
@ -13,6 +13,7 @@ import {
|
||||||
AbuseStateChangeForReporter,
|
AbuseStateChangeForReporter,
|
||||||
AutoFollowForInstance,
|
AutoFollowForInstance,
|
||||||
CommentMention,
|
CommentMention,
|
||||||
|
DirectRegistrationForModerators,
|
||||||
FollowForInstance,
|
FollowForInstance,
|
||||||
FollowForUser,
|
FollowForUser,
|
||||||
ImportFinishedForOwner,
|
ImportFinishedForOwner,
|
||||||
|
@ -30,7 +31,7 @@ import {
|
||||||
OwnedPublicationAfterAutoUnblacklist,
|
OwnedPublicationAfterAutoUnblacklist,
|
||||||
OwnedPublicationAfterScheduleUpdate,
|
OwnedPublicationAfterScheduleUpdate,
|
||||||
OwnedPublicationAfterTranscoding,
|
OwnedPublicationAfterTranscoding,
|
||||||
RegistrationForModerators,
|
RegistrationRequestForModerators,
|
||||||
StudioEditionFinishedForOwner,
|
StudioEditionFinishedForOwner,
|
||||||
UnblacklistForOwner
|
UnblacklistForOwner
|
||||||
} from './shared'
|
} from './shared'
|
||||||
|
@ -47,7 +48,8 @@ class Notifier {
|
||||||
newBlacklist: [ NewBlacklistForOwner ],
|
newBlacklist: [ NewBlacklistForOwner ],
|
||||||
unblacklist: [ UnblacklistForOwner ],
|
unblacklist: [ UnblacklistForOwner ],
|
||||||
importFinished: [ ImportFinishedForOwner ],
|
importFinished: [ ImportFinishedForOwner ],
|
||||||
userRegistration: [ RegistrationForModerators ],
|
directRegistration: [ DirectRegistrationForModerators ],
|
||||||
|
registrationRequest: [ RegistrationRequestForModerators ],
|
||||||
userFollow: [ FollowForUser ],
|
userFollow: [ FollowForUser ],
|
||||||
instanceFollow: [ FollowForInstance ],
|
instanceFollow: [ FollowForInstance ],
|
||||||
autoInstanceFollow: [ AutoFollowForInstance ],
|
autoInstanceFollow: [ AutoFollowForInstance ],
|
||||||
|
@ -138,13 +140,20 @@ class Notifier {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyOnNewUserRegistration (user: MUserDefault): void {
|
notifyOnNewDirectRegistration (user: MUserDefault): void {
|
||||||
const models = this.notificationModels.userRegistration
|
const models = this.notificationModels.directRegistration
|
||||||
|
|
||||||
this.sendNotifications(models, user)
|
this.sendNotifications(models, user)
|
||||||
.catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err }))
|
.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 {
|
notifyOfNewUserFollow (actorFollow: MActorFollowFull): void {
|
||||||
const models = this.notificationModels.userFollow
|
const models = this.notificationModels.userFollow
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi
|
||||||
import { UserNotificationType, UserRight } from '@shared/models'
|
import { UserNotificationType, UserRight } from '@shared/models'
|
||||||
import { AbstractNotification } from '../common/abstract-notification'
|
import { AbstractNotification } from '../common/abstract-notification'
|
||||||
|
|
||||||
export class RegistrationForModerators extends AbstractNotification <MUserDefault> {
|
export class DirectRegistrationForModerators extends AbstractNotification <MUserDefault> {
|
||||||
private moderators: MUserDefault[]
|
private moderators: MUserDefault[]
|
||||||
|
|
||||||
async prepare () {
|
async prepare () {
|
||||||
|
@ -40,7 +40,7 @@ export class RegistrationForModerators extends AbstractNotification <MUserDefaul
|
||||||
return {
|
return {
|
||||||
template: 'user-registered',
|
template: 'user-registered',
|
||||||
to,
|
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: {
|
locals: {
|
||||||
user: this.payload
|
user: this.payload
|
||||||
}
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
export * from './new-peertube-version-for-admins'
|
export * from './new-peertube-version-for-admins'
|
||||||
export * from './new-plugin-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,
|
CONTACT_FORM_LIFETIME,
|
||||||
RESUMABLE_UPLOAD_SESSION_LIFETIME,
|
RESUMABLE_UPLOAD_SESSION_LIFETIME,
|
||||||
TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME,
|
TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME,
|
||||||
USER_EMAIL_VERIFY_LIFETIME,
|
EMAIL_VERIFY_LIFETIME,
|
||||||
USER_PASSWORD_CREATE_LIFETIME,
|
USER_PASSWORD_CREATE_LIFETIME,
|
||||||
USER_PASSWORD_RESET_LIFETIME,
|
USER_PASSWORD_RESET_LIFETIME,
|
||||||
VIEW_LIFETIME,
|
VIEW_LIFETIME,
|
||||||
|
@ -124,16 +124,28 @@ class Redis {
|
||||||
|
|
||||||
/* ************ Email verification ************ */
|
/* ************ Email verification ************ */
|
||||||
|
|
||||||
async setVerifyEmailVerificationString (userId: number) {
|
async setUserVerifyEmailVerificationString (userId: number) {
|
||||||
const generatedString = await generateRandomString(32)
|
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
|
return generatedString
|
||||||
}
|
}
|
||||||
|
|
||||||
async getVerifyEmailLink (userId: number) {
|
async getUserVerifyEmailLink (userId: number) {
|
||||||
return this.getValue(this.generateVerifyEmailKey(userId))
|
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 ************ */
|
/* ************ Contact form per IP ************ */
|
||||||
|
@ -346,8 +358,12 @@ class Redis {
|
||||||
return 'two-factor-request-' + userId + '-' + token
|
return 'two-factor-request-' + userId + '-' + token
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateVerifyEmailKey (userId: number) {
|
private generateUserVerifyEmailKey (userId: number) {
|
||||||
return 'verify-email-' + userId
|
return 'verify-email-user-' + userId
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateRegistrationVerifyEmailKey (registrationId: number) {
|
||||||
|
return 'verify-email-registration-' + registrationId
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateIPViewKey (ip: string, videoUUID: string) {
|
private generateIPViewKey (ip: string, videoUUID: string) {
|
||||||
|
|
|
@ -261,10 +261,17 @@ class ServerConfigManager {
|
||||||
async getServerConfig (ip?: string): Promise<ServerConfig> {
|
async getServerConfig (ip?: string): Promise<ServerConfig> {
|
||||||
const { allowed } = await Hooks.wrapPromiseFun(
|
const { allowed } = await Hooks.wrapPromiseFun(
|
||||||
isSignupAllowed,
|
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)
|
const allowedForCurrentIP = isSignupAllowedForCurrentIP(ip)
|
||||||
|
@ -273,6 +280,7 @@ class ServerConfigManager {
|
||||||
allowed,
|
allowed,
|
||||||
allowedForCurrentIP,
|
allowedForCurrentIP,
|
||||||
minimumAge: CONFIG.SIGNUP.MINIMUM_AGE,
|
minimumAge: CONFIG.SIGNUP.MINIMUM_AGE,
|
||||||
|
requiresApproval: CONFIG.SIGNUP.REQUIRES_APPROVAL,
|
||||||
requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
|
requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,24 @@ import { UserModel } from '../models/user/user'
|
||||||
|
|
||||||
const isCidr = require('is-cidr')
|
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) {
|
if (CONFIG.SIGNUP.ENABLED === false) {
|
||||||
return { allowed: false }
|
return { allowed: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (signupMode === 'direct-registration' && CONFIG.SIGNUP.REQUIRES_APPROVAL === true) {
|
||||||
|
return { allowed: false }
|
||||||
|
}
|
||||||
|
|
||||||
// No limit and signup is enabled
|
// No limit and signup is enabled
|
||||||
if (CONFIG.SIGNUP.LIMIT === -1) {
|
if (CONFIG.SIGNUP.LIMIT === -1) {
|
||||||
return { allowed: true }
|
return { allowed: true }
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { sequelizeTypescript } from '../initializers/database'
|
||||||
import { AccountModel } from '../models/account/account'
|
import { AccountModel } from '../models/account/account'
|
||||||
import { UserNotificationSettingModel } from '../models/user/user-notification-setting'
|
import { UserNotificationSettingModel } from '../models/user/user-notification-setting'
|
||||||
import { MAccountDefault, MChannelActor } from '../types/models'
|
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 { generateAndSaveActorKeys } from './activitypub/actors'
|
||||||
import { getLocalAccountActivityPubUrl } from './activitypub/url'
|
import { getLocalAccountActivityPubUrl } from './activitypub/url'
|
||||||
import { Emailer } from './emailer'
|
import { Emailer } from './emailer'
|
||||||
|
@ -97,7 +97,7 @@ async function createUserAccountAndChannelAndPlaylist (parameters: {
|
||||||
})
|
})
|
||||||
userCreated.Account = accountCreated
|
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 videoChannel = await createLocalVideoChannel(channelAttributes, accountCreated, t)
|
||||||
|
|
||||||
const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t)
|
const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t)
|
||||||
|
@ -160,15 +160,28 @@ async function createApplicationActor (applicationId: number) {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) {
|
async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) {
|
||||||
const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id)
|
const verificationString = await Redis.Instance.setUserVerifyEmailVerificationString(user.id)
|
||||||
let url = WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString
|
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
|
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,
|
createApplicationActor,
|
||||||
createUserAccountAndChannelAndPlaylist,
|
createUserAccountAndChannelAndPlaylist,
|
||||||
createLocalAccountWithoutKeys,
|
createLocalAccountWithoutKeys,
|
||||||
|
|
||||||
sendVerifyUserEmail,
|
sendVerifyUserEmail,
|
||||||
|
sendVerifyRegistrationEmail,
|
||||||
|
|
||||||
isAbleToUploadVideo,
|
isAbleToUploadVideo,
|
||||||
buildUser
|
buildUser
|
||||||
}
|
}
|
||||||
|
@ -264,7 +280,13 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction |
|
||||||
return UserNotificationSettingModel.create(values, { transaction: t })
|
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
|
if (channelNames) return channelNames
|
||||||
|
|
||||||
const channelName = await findAvailableLocalActorName(user.username + '_channel', transaction)
|
const channelName = await findAvailableLocalActorName(user.username + '_channel', transaction)
|
||||||
|
|
|
@ -29,6 +29,7 @@ const customConfigUpdateValidator = [
|
||||||
body('signup.enabled').isBoolean(),
|
body('signup.enabled').isBoolean(),
|
||||||
body('signup.limit').isInt(),
|
body('signup.limit').isInt(),
|
||||||
body('signup.requiresEmailVerification').isBoolean(),
|
body('signup.requiresEmailVerification').isBoolean(),
|
||||||
|
body('signup.requiresApproval').isBoolean(),
|
||||||
body('signup.minimumAge').isInt(),
|
body('signup.minimumAge').isInt(),
|
||||||
|
|
||||||
body('admin.email').isEmail(),
|
body('admin.email').isEmail(),
|
||||||
|
|
|
@ -21,8 +21,10 @@ export * from './server'
|
||||||
export * from './sort'
|
export * from './sort'
|
||||||
export * from './static'
|
export * from './static'
|
||||||
export * from './themes'
|
export * from './themes'
|
||||||
|
export * from './user-email-verification'
|
||||||
export * from './user-history'
|
export * from './user-history'
|
||||||
export * from './user-notifications'
|
export * from './user-notifications'
|
||||||
|
export * from './user-registrations'
|
||||||
export * from './user-subscriptions'
|
export * from './user-subscriptions'
|
||||||
export * from './users'
|
export * from './users'
|
||||||
export * from './videos'
|
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)
|
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)
|
const user = await UserModel.loadByUsernameOrEmail(username, email)
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
|
@ -58,6 +58,6 @@ async function checkUserExist (finder: () => Promise<MUserDefault>, res: express
|
||||||
export {
|
export {
|
||||||
checkUserIdExist,
|
checkUserIdExist,
|
||||||
checkUserEmailExist,
|
checkUserEmailExist,
|
||||||
checkUserNameOrEmailDoesNotAlreadyExist,
|
checkUserNameOrEmailDoNotAlreadyExist,
|
||||||
checkUserExist
|
checkUserExist
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,41 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { query } from 'express-validator'
|
import { query } from 'express-validator'
|
||||||
|
|
||||||
import { SORTABLE_COLUMNS } from '../../initializers/constants'
|
import { SORTABLE_COLUMNS } from '../../initializers/constants'
|
||||||
import { areValidationErrors } from './shared'
|
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[] = []) {
|
function checkSortFactory (columns: string[], tags: string[] = []) {
|
||||||
return checkSort(createSortableColumns(columns), tags)
|
return checkSort(createSortableColumns(columns), tags)
|
||||||
}
|
}
|
||||||
|
@ -27,64 +59,3 @@ function createSortableColumns (sortableColumns: string[]) {
|
||||||
|
|
||||||
return sortableColumns.concat(sortableColumnDesc)
|
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 express from 'express'
|
||||||
import { body, param, query } from 'express-validator'
|
import { body, param, query } from 'express-validator'
|
||||||
import { Hooks } from '@server/lib/plugins/hooks'
|
|
||||||
import { forceNumber } from '@shared/core-utils'
|
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 { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
|
||||||
import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
|
import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
|
||||||
import {
|
import {
|
||||||
|
@ -24,17 +23,16 @@ import {
|
||||||
isUserVideoQuotaValid,
|
isUserVideoQuotaValid,
|
||||||
isUserVideosHistoryEnabledValid
|
isUserVideosHistoryEnabledValid
|
||||||
} from '../../helpers/custom-validators/users'
|
} 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 { logger } from '../../helpers/logger'
|
||||||
import { isThemeRegistered } from '../../lib/plugins/theme-utils'
|
import { isThemeRegistered } from '../../lib/plugins/theme-utils'
|
||||||
import { Redis } from '../../lib/redis'
|
import { Redis } from '../../lib/redis'
|
||||||
import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup'
|
|
||||||
import { ActorModel } from '../../models/actor/actor'
|
import { ActorModel } from '../../models/actor/actor'
|
||||||
import {
|
import {
|
||||||
areValidationErrors,
|
areValidationErrors,
|
||||||
checkUserEmailExist,
|
checkUserEmailExist,
|
||||||
checkUserIdExist,
|
checkUserIdExist,
|
||||||
checkUserNameOrEmailDoesNotAlreadyExist,
|
checkUserNameOrEmailDoNotAlreadyExist,
|
||||||
doesVideoChannelIdExist,
|
doesVideoChannelIdExist,
|
||||||
doesVideoExist,
|
doesVideoExist,
|
||||||
isValidVideoIdParam
|
isValidVideoIdParam
|
||||||
|
@ -81,7 +79,7 @@ const usersAddValidator = [
|
||||||
|
|
||||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
if (areValidationErrors(req, res, { omitBodyLog: true })) return
|
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
|
const authUser = res.locals.oauth.token.User
|
||||||
if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.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 = [
|
const usersRemoveValidator = [
|
||||||
param('id')
|
param('id')
|
||||||
.custom(isIdValid),
|
.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 = [
|
const usersAskResetPasswordValidator = [
|
||||||
body('email')
|
body('email')
|
||||||
.isEmail(),
|
.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) => {
|
const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => {
|
||||||
return [
|
return [
|
||||||
body('currentPassword').optional().custom(exists),
|
body('currentPassword').optional().custom(exists),
|
||||||
|
@ -603,21 +465,16 @@ export {
|
||||||
usersListValidator,
|
usersListValidator,
|
||||||
usersAddValidator,
|
usersAddValidator,
|
||||||
deleteMeValidator,
|
deleteMeValidator,
|
||||||
usersRegisterValidator,
|
|
||||||
usersBlockingValidator,
|
usersBlockingValidator,
|
||||||
usersRemoveValidator,
|
usersRemoveValidator,
|
||||||
usersUpdateValidator,
|
usersUpdateValidator,
|
||||||
usersUpdateMeValidator,
|
usersUpdateMeValidator,
|
||||||
usersVideoRatingValidator,
|
usersVideoRatingValidator,
|
||||||
usersCheckCurrentPasswordFactory,
|
usersCheckCurrentPasswordFactory,
|
||||||
ensureUserRegistrationAllowed,
|
|
||||||
ensureUserRegistrationAllowedForIP,
|
|
||||||
usersGetValidator,
|
usersGetValidator,
|
||||||
usersVideosValidator,
|
usersVideosValidator,
|
||||||
usersAskResetPasswordValidator,
|
usersAskResetPasswordValidator,
|
||||||
usersResetPasswordValidator,
|
usersResetPasswordValidator,
|
||||||
usersAskSendVerifyEmailValidator,
|
|
||||||
usersVerifyEmailValidator,
|
|
||||||
userAutocompleteValidator,
|
userAutocompleteValidator,
|
||||||
ensureAuthUserOwnsAccountValidator,
|
ensureAuthUserOwnsAccountValidator,
|
||||||
ensureCanModerateUser,
|
ensureCanModerateUser,
|
||||||
|
|
|
@ -180,7 +180,9 @@ export class UserNotificationListQueryBuilder extends AbstractRunQuery {
|
||||||
"Account->Actor->Avatars"."type" AS "Account.Actor.Avatars.type",
|
"Account->Actor->Avatars"."type" AS "Account.Actor.Avatars.type",
|
||||||
"Account->Actor->Avatars"."filename" AS "Account.Actor.Avatars.filename",
|
"Account->Actor->Avatars"."filename" AS "Account.Actor.Avatars.filename",
|
||||||
"Account->Actor->Server"."id" AS "Account.Actor.Server.id",
|
"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 () {
|
private getJoins () {
|
||||||
|
@ -196,74 +198,76 @@ export class UserNotificationListQueryBuilder extends AbstractRunQuery {
|
||||||
ON "Video->VideoChannel->Actor"."serverId" = "Video->VideoChannel->Actor->Server"."id"
|
ON "Video->VideoChannel->Actor"."serverId" = "Video->VideoChannel->Actor->Server"."id"
|
||||||
) ON "UserNotificationModel"."videoId" = "Video"."id"
|
) ON "UserNotificationModel"."videoId" = "Video"."id"
|
||||||
|
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
"videoComment" AS "VideoComment"
|
"videoComment" AS "VideoComment"
|
||||||
INNER JOIN "account" AS "VideoComment->Account" ON "VideoComment"."accountId" = "VideoComment->Account"."id"
|
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"
|
INNER JOIN "actor" AS "VideoComment->Account->Actor" ON "VideoComment->Account"."actorId" = "VideoComment->Account->Actor"."id"
|
||||||
LEFT JOIN "actorImage" AS "VideoComment->Account->Actor->Avatars"
|
LEFT JOIN "actorImage" AS "VideoComment->Account->Actor->Avatars"
|
||||||
ON "VideoComment->Account->Actor"."id" = "VideoComment->Account->Actor->Avatars"."actorId"
|
ON "VideoComment->Account->Actor"."id" = "VideoComment->Account->Actor->Avatars"."actorId"
|
||||||
AND "VideoComment->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
|
AND "VideoComment->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
|
||||||
LEFT JOIN "server" AS "VideoComment->Account->Actor->Server"
|
LEFT JOIN "server" AS "VideoComment->Account->Actor->Server"
|
||||||
ON "VideoComment->Account->Actor"."serverId" = "VideoComment->Account->Actor->Server"."id"
|
ON "VideoComment->Account->Actor"."serverId" = "VideoComment->Account->Actor->Server"."id"
|
||||||
INNER JOIN "video" AS "VideoComment->Video" ON "VideoComment"."videoId" = "VideoComment->Video"."id"
|
INNER JOIN "video" AS "VideoComment->Video" ON "VideoComment"."videoId" = "VideoComment->Video"."id"
|
||||||
) ON "UserNotificationModel"."commentId" = "VideoComment"."id"
|
) ON "UserNotificationModel"."commentId" = "VideoComment"."id"
|
||||||
|
|
||||||
LEFT JOIN "abuse" AS "Abuse" ON "UserNotificationModel"."abuseId" = "Abuse"."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 "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 "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 "commentAbuse" AS "Abuse->VideoCommentAbuse" ON "Abuse"."id" = "Abuse->VideoCommentAbuse"."abuseId"
|
||||||
LEFT JOIN "videoComment" AS "Abuse->VideoCommentAbuse->VideoComment"
|
LEFT JOIN "videoComment" AS "Abuse->VideoCommentAbuse->VideoComment"
|
||||||
ON "Abuse->VideoCommentAbuse"."videoCommentId" = "Abuse->VideoCommentAbuse->VideoComment"."id"
|
ON "Abuse->VideoCommentAbuse"."videoCommentId" = "Abuse->VideoCommentAbuse->VideoComment"."id"
|
||||||
LEFT JOIN "video" AS "Abuse->VideoCommentAbuse->VideoComment->Video"
|
LEFT JOIN "video" AS "Abuse->VideoCommentAbuse->VideoComment->Video"
|
||||||
ON "Abuse->VideoCommentAbuse->VideoComment"."videoId" = "Abuse->VideoCommentAbuse->VideoComment->Video"."id"
|
ON "Abuse->VideoCommentAbuse->VideoComment"."videoId" = "Abuse->VideoCommentAbuse->VideoComment->Video"."id"
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
"account" AS "Abuse->FlaggedAccount"
|
"account" AS "Abuse->FlaggedAccount"
|
||||||
INNER JOIN "actor" AS "Abuse->FlaggedAccount->Actor" ON "Abuse->FlaggedAccount"."actorId" = "Abuse->FlaggedAccount->Actor"."id"
|
INNER JOIN "actor" AS "Abuse->FlaggedAccount->Actor" ON "Abuse->FlaggedAccount"."actorId" = "Abuse->FlaggedAccount->Actor"."id"
|
||||||
LEFT JOIN "actorImage" AS "Abuse->FlaggedAccount->Actor->Avatars"
|
LEFT JOIN "actorImage" AS "Abuse->FlaggedAccount->Actor->Avatars"
|
||||||
ON "Abuse->FlaggedAccount->Actor"."id" = "Abuse->FlaggedAccount->Actor->Avatars"."actorId"
|
ON "Abuse->FlaggedAccount->Actor"."id" = "Abuse->FlaggedAccount->Actor->Avatars"."actorId"
|
||||||
AND "Abuse->FlaggedAccount->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
|
AND "Abuse->FlaggedAccount->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
|
||||||
LEFT JOIN "server" AS "Abuse->FlaggedAccount->Actor->Server"
|
LEFT JOIN "server" AS "Abuse->FlaggedAccount->Actor->Server"
|
||||||
ON "Abuse->FlaggedAccount->Actor"."serverId" = "Abuse->FlaggedAccount->Actor->Server"."id"
|
ON "Abuse->FlaggedAccount->Actor"."serverId" = "Abuse->FlaggedAccount->Actor->Server"."id"
|
||||||
) ON "Abuse"."flaggedAccountId" = "Abuse->FlaggedAccount"."id"
|
) ON "Abuse"."flaggedAccountId" = "Abuse->FlaggedAccount"."id"
|
||||||
|
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
"videoBlacklist" AS "VideoBlacklist"
|
"videoBlacklist" AS "VideoBlacklist"
|
||||||
INNER JOIN "video" AS "VideoBlacklist->Video" ON "VideoBlacklist"."videoId" = "VideoBlacklist->Video"."id"
|
INNER JOIN "video" AS "VideoBlacklist->Video" ON "VideoBlacklist"."videoId" = "VideoBlacklist->Video"."id"
|
||||||
) ON "UserNotificationModel"."videoBlacklistId" = "VideoBlacklist"."id"
|
) ON "UserNotificationModel"."videoBlacklistId" = "VideoBlacklist"."id"
|
||||||
|
|
||||||
LEFT JOIN "videoImport" AS "VideoImport" ON "UserNotificationModel"."videoImportId" = "VideoImport"."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 "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 (
|
LEFT JOIN (
|
||||||
"actorFollow" AS "ActorFollow"
|
"actorFollow" AS "ActorFollow"
|
||||||
INNER JOIN "actor" AS "ActorFollow->ActorFollower" ON "ActorFollow"."actorId" = "ActorFollow->ActorFollower"."id"
|
INNER JOIN "actor" AS "ActorFollow->ActorFollower" ON "ActorFollow"."actorId" = "ActorFollow->ActorFollower"."id"
|
||||||
INNER JOIN "account" AS "ActorFollow->ActorFollower->Account"
|
INNER JOIN "account" AS "ActorFollow->ActorFollower->Account"
|
||||||
ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Account"."actorId"
|
ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Account"."actorId"
|
||||||
LEFT JOIN "actorImage" AS "ActorFollow->ActorFollower->Avatars"
|
LEFT JOIN "actorImage" AS "ActorFollow->ActorFollower->Avatars"
|
||||||
ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Avatars"."actorId"
|
ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Avatars"."actorId"
|
||||||
AND "ActorFollow->ActorFollower->Avatars"."type" = ${ActorImageType.AVATAR}
|
AND "ActorFollow->ActorFollower->Avatars"."type" = ${ActorImageType.AVATAR}
|
||||||
LEFT JOIN "server" AS "ActorFollow->ActorFollower->Server"
|
LEFT JOIN "server" AS "ActorFollow->ActorFollower->Server"
|
||||||
ON "ActorFollow->ActorFollower"."serverId" = "ActorFollow->ActorFollower->Server"."id"
|
ON "ActorFollow->ActorFollower"."serverId" = "ActorFollow->ActorFollower->Server"."id"
|
||||||
INNER JOIN "actor" AS "ActorFollow->ActorFollowing" ON "ActorFollow"."targetActorId" = "ActorFollow->ActorFollowing"."id"
|
INNER JOIN "actor" AS "ActorFollow->ActorFollowing" ON "ActorFollow"."targetActorId" = "ActorFollow->ActorFollowing"."id"
|
||||||
LEFT JOIN "videoChannel" AS "ActorFollow->ActorFollowing->VideoChannel"
|
LEFT JOIN "videoChannel" AS "ActorFollow->ActorFollowing->VideoChannel"
|
||||||
ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->VideoChannel"."actorId"
|
ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->VideoChannel"."actorId"
|
||||||
LEFT JOIN "account" AS "ActorFollow->ActorFollowing->Account"
|
LEFT JOIN "account" AS "ActorFollow->ActorFollowing->Account"
|
||||||
ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->Account"."actorId"
|
ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->Account"."actorId"
|
||||||
LEFT JOIN "server" AS "ActorFollow->ActorFollowing->Server"
|
LEFT JOIN "server" AS "ActorFollow->ActorFollowing->Server"
|
||||||
ON "ActorFollow->ActorFollowing"."serverId" = "ActorFollow->ActorFollowing->Server"."id"
|
ON "ActorFollow->ActorFollowing"."serverId" = "ActorFollow->ActorFollowing->Server"."id"
|
||||||
) ON "UserNotificationModel"."actorFollowId" = "ActorFollow"."id"
|
) ON "UserNotificationModel"."actorFollowId" = "ActorFollow"."id"
|
||||||
|
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
"account" AS "Account"
|
"account" AS "Account"
|
||||||
INNER JOIN "actor" AS "Account->Actor" ON "Account"."actorId" = "Account->Actor"."id"
|
INNER JOIN "actor" AS "Account->Actor" ON "Account"."actorId" = "Account->Actor"."id"
|
||||||
LEFT JOIN "actorImage" AS "Account->Actor->Avatars"
|
LEFT JOIN "actorImage" AS "Account->Actor->Avatars"
|
||||||
ON "Account->Actor"."id" = "Account->Actor->Avatars"."actorId"
|
ON "Account->Actor"."id" = "Account->Actor->Avatars"."actorId"
|
||||||
AND "Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
|
AND "Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
|
||||||
LEFT JOIN "server" AS "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"
|
LEFT JOIN "server" AS "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"
|
||||||
) ON "UserNotificationModel"."accountId" = "Account"."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 { VideoImportModel } from '../video/video-import'
|
||||||
import { UserNotificationListQueryBuilder } from './sql/user-notitication-list-query-builder'
|
import { UserNotificationListQueryBuilder } from './sql/user-notitication-list-query-builder'
|
||||||
import { UserModel } from './user'
|
import { UserModel } from './user'
|
||||||
|
import { UserRegistrationModel } from './user-registration'
|
||||||
|
|
||||||
@Table({
|
@Table({
|
||||||
tableName: 'userNotification',
|
tableName: 'userNotification',
|
||||||
|
@ -98,6 +99,14 @@ import { UserModel } from './user'
|
||||||
[Op.ne]: null
|
[Op.ne]: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: [ 'userRegistrationId' ],
|
||||||
|
where: {
|
||||||
|
userRegistrationId: {
|
||||||
|
[Op.ne]: null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
] as (ModelIndexesOptions & { where?: WhereOptions })[]
|
] as (ModelIndexesOptions & { where?: WhereOptions })[]
|
||||||
})
|
})
|
||||||
|
@ -241,6 +250,18 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
|
||||||
})
|
})
|
||||||
Application: ApplicationModel
|
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) {
|
static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
|
||||||
const where = { userId }
|
const where = { userId }
|
||||||
|
|
||||||
|
@ -416,6 +437,10 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
|
||||||
? { latestVersion: this.Application.latestPeerTubeVersion }
|
? { latestVersion: this.Application.latestPeerTubeVersion }
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
|
const registration = this.UserRegistration
|
||||||
|
? { id: this.UserRegistration.id, username: this.UserRegistration.username }
|
||||||
|
: undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
|
@ -429,6 +454,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
|
||||||
actorFollow,
|
actorFollow,
|
||||||
plugin,
|
plugin,
|
||||||
peertube,
|
peertube,
|
||||||
|
registration,
|
||||||
createdAt: this.createdAt.toISOString(),
|
createdAt: this.createdAt.toISOString(),
|
||||||
updatedAt: this.updatedAt.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[]
|
OAuthTokens: OAuthTokenModel[]
|
||||||
|
|
||||||
|
// Used if we already set an encrypted password in user model
|
||||||
|
skipPasswordEncryption = false
|
||||||
|
|
||||||
@BeforeCreate
|
@BeforeCreate
|
||||||
@BeforeUpdate
|
@BeforeUpdate
|
||||||
static cryptPasswordIfNeeded (instance: UserModel) {
|
static async cryptPasswordIfNeeded (instance: UserModel) {
|
||||||
if (instance.changed('password') && instance.password) {
|
if (instance.skipPasswordEncryption) return
|
||||||
return cryptPassword(instance.password)
|
if (!instance.changed('password')) return
|
||||||
.then(hash => {
|
if (!instance.password) return
|
||||||
instance.password = hash
|
|
||||||
return undefined
|
instance.password = await cryptPassword(instance.password)
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterUpdate
|
@AfterUpdate
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
MActorUrl,
|
MActorUrl,
|
||||||
MChannelBannerAccountDefault,
|
MChannelBannerAccountDefault,
|
||||||
MChannelSyncChannel,
|
MChannelSyncChannel,
|
||||||
|
MRegistration,
|
||||||
MStreamingPlaylist,
|
MStreamingPlaylist,
|
||||||
MUserAccountUrl,
|
MUserAccountUrl,
|
||||||
MVideoChangeOwnershipFull,
|
MVideoChangeOwnershipFull,
|
||||||
|
@ -171,6 +172,7 @@ declare module 'express' {
|
||||||
actorFull?: MActorFull
|
actorFull?: MActorFull
|
||||||
|
|
||||||
user?: MUserDefault
|
user?: MUserDefault
|
||||||
|
userRegistration?: MRegistration
|
||||||
|
|
||||||
server?: MServer
|
server?: MServer
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './user'
|
export * from './user'
|
||||||
export * from './user-notification'
|
export * from './user-notification'
|
||||||
export * from './user-notification-setting'
|
export * from './user-notification-setting'
|
||||||
|
export * from './user-registration'
|
||||||
export * from './user-video-history'
|
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 { ApplicationModel } from '@server/models/application/application'
|
||||||
import { PluginModel } from '@server/models/server/plugin'
|
import { PluginModel } from '@server/models/server/plugin'
|
||||||
import { UserNotificationModel } from '@server/models/user/user-notification'
|
import { UserNotificationModel } from '@server/models/user/user-notification'
|
||||||
|
import { UserRegistrationModel } from '@server/models/user/user-registration'
|
||||||
import { PickWith, PickWithOpt } from '@shared/typescript-utils'
|
import { PickWith, PickWithOpt } from '@shared/typescript-utils'
|
||||||
import { AbuseModel } from '../../../models/abuse/abuse'
|
import { AbuseModel } from '../../../models/abuse/abuse'
|
||||||
import { AccountModel } from '../../../models/account/account'
|
import { AccountModel } from '../../../models/account/account'
|
||||||
|
@ -94,13 +95,16 @@ export module UserNotificationIncludes {
|
||||||
|
|
||||||
export type ApplicationInclude =
|
export type ApplicationInclude =
|
||||||
Pick<ApplicationModel, 'latestPeerTubeVersion'>
|
Pick<ApplicationModel, 'latestPeerTubeVersion'>
|
||||||
|
|
||||||
|
export type UserRegistrationInclude =
|
||||||
|
Pick<UserRegistrationModel, 'id' | 'username'>
|
||||||
}
|
}
|
||||||
|
|
||||||
// ############################################################################
|
// ############################################################################
|
||||||
|
|
||||||
export type MUserNotification =
|
export type MUserNotification =
|
||||||
Omit<UserNotificationModel, 'User' | 'Video' | 'VideoComment' | 'Abuse' | 'VideoBlacklist' |
|
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<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> &
|
||||||
Use<'Plugin', UserNotificationIncludes.PluginInclude> &
|
Use<'Plugin', UserNotificationIncludes.PluginInclude> &
|
||||||
Use<'Application', UserNotificationIncludes.ApplicationInclude> &
|
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_ACCOUNTS_BLOCKLIST,
|
||||||
UserRight.MANAGE_SERVERS_BLOCKLIST,
|
UserRight.MANAGE_SERVERS_BLOCKLIST,
|
||||||
UserRight.MANAGE_USERS,
|
UserRight.MANAGE_USERS,
|
||||||
UserRight.SEE_ALL_COMMENTS
|
UserRight.SEE_ALL_COMMENTS,
|
||||||
|
UserRight.MANAGE_REGISTRATIONS
|
||||||
],
|
],
|
||||||
|
|
||||||
[UserRole.USER]: []
|
[UserRole.USER]: []
|
||||||
|
|
|
@ -91,6 +91,10 @@ export const serverFilterHookObject = {
|
||||||
// Filter result used to check if a user can register on the instance
|
// Filter result used to check if a user can register on the instance
|
||||||
'filter:api.user.signup.allowed.result': true,
|
'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 result used to check if video/torrent download is allowed
|
||||||
'filter:api.download.video.allowed.result': true,
|
'filter:api.download.video.allowed.result': true,
|
||||||
'filter:api.download.torrent.allowed.result': true,
|
'filter:api.download.torrent.allowed.result': true,
|
||||||
|
@ -156,6 +160,9 @@ export const serverActionHookObject = {
|
||||||
'action:api.user.unblocked': true,
|
'action:api.user.unblocked': true,
|
||||||
// Fired when a user registered on the instance
|
// Fired when a user registered on the instance
|
||||||
'action:api.user.registered': true,
|
'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
|
// Fired when an admin/moderator created a user
|
||||||
'action:api.user.created': true,
|
'action:api.user.created': true,
|
||||||
// Fired when a user is removed by an admin/moderator
|
// Fired when a user is removed by an admin/moderator
|
||||||
|
|
|
@ -83,6 +83,7 @@ export interface CustomConfig {
|
||||||
signup: {
|
signup: {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
limit: number
|
limit: number
|
||||||
|
requiresApproval: boolean
|
||||||
requiresEmailVerification: boolean
|
requiresEmailVerification: boolean
|
||||||
minimumAge: number
|
minimumAge: number
|
||||||
}
|
}
|
||||||
|
|
|
@ -131,6 +131,7 @@ export interface ServerConfig {
|
||||||
allowed: boolean
|
allowed: boolean
|
||||||
allowedForCurrentIP: boolean
|
allowedForCurrentIP: boolean
|
||||||
requiresEmailVerification: boolean
|
requiresEmailVerification: boolean
|
||||||
|
requiresApproval: boolean
|
||||||
minimumAge: number
|
minimumAge: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,13 @@ export const enum ServerErrorCode {
|
||||||
*/
|
*/
|
||||||
INCORRECT_FILES_IN_TORRENT = 'incorrect_files_in_torrent',
|
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
|
* @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 './two-factor-enable-result.model'
|
||||||
export * from './user-create-result.model'
|
export * from './user-create-result.model'
|
||||||
export * from './user-create.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-setting.model'
|
||||||
export * from './user-notification.model'
|
export * from './user-notification.model'
|
||||||
export * from './user-refresh-token.model'
|
export * from './user-refresh-token.model'
|
||||||
export * from './user-register.model'
|
|
||||||
export * from './user-right.enum'
|
export * from './user-right.enum'
|
||||||
export * from './user-role'
|
export * from './user-role'
|
||||||
export * from './user-scoped-token'
|
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_PLUGIN_VERSION = 17,
|
||||||
NEW_PEERTUBE_VERSION = 18,
|
NEW_PEERTUBE_VERSION = 18,
|
||||||
|
|
||||||
MY_VIDEO_STUDIO_EDITION_FINISHED = 19
|
MY_VIDEO_STUDIO_EDITION_FINISHED = 19,
|
||||||
|
|
||||||
|
NEW_USER_REGISTRATION_REQUEST = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoInfo {
|
export interface VideoInfo {
|
||||||
|
@ -126,6 +128,11 @@ export interface UserNotification {
|
||||||
latestVersion: string
|
latestVersion: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registration?: {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,5 +43,7 @@ export const enum UserRight {
|
||||||
MANAGE_VIDEO_FILES = 25,
|
MANAGE_VIDEO_FILES = 25,
|
||||||
RUN_VIDEO_TRANSCODING = 26,
|
RUN_VIDEO_TRANSCODING = 26,
|
||||||
|
|
||||||
MANAGE_VIDEO_IMPORTS = 27
|
MANAGE_VIDEO_IMPORTS = 27,
|
||||||
|
|
||||||
|
MANAGE_REGISTRATIONS = 28
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue