i18n-labelText labelText="Signup enabled"
></my-peertube-checkbox>
+ <my-peertube-checkbox *ngIf="isSignupEnabled()"
+ inputName="signupRequiresEmailVerification" formControlName="signupRequiresEmailVerification"
+ i18n-labelText labelText="Signup requires email verification"
+ ></my-peertube-checkbox>
+
<div *ngIf="isSignupEnabled()" class="form-group">
<label i18n for="signupLimit">Signup limit</label>
<input
cacheCaptionsSize: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE,
signupEnabled: null,
signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT,
+ signupRequiresEmailVerification: null,
importVideosHttpEnabled: null,
importVideosTorrentEnabled: null,
adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL,
},
signup: {
enabled: this.form.value['signupEnabled'],
- limit: this.form.value['signupLimit']
+ limit: this.form.value['signupLimit'],
+ requiresEmailVerification: this.form.value['signupRequiresEmailVerification']
},
admin: {
email: this.form.value['adminEmail']
cacheCaptionsSize: this.customConfig.cache.captions.size,
signupEnabled: this.customConfig.signup.enabled,
signupLimit: this.customConfig.signup.limit,
+ signupRequiresEmailVerification: this.customConfig.signup.requiresEmailVerification,
adminEmail: this.customConfig.admin.email,
userVideoQuota: this.customConfig.user.videoQuota,
userVideoQuotaDaily: this.customConfig.user.videoQuotaDaily,
--- /dev/null
+export * from '@app/+verify-account/verify-account-routing.module'
+export * from '@app/+verify-account/verify-account.module'
--- /dev/null
+<div class="margin-content">
+ <div i18n class="title-page title-page-single">
+ Request email for account verification
+ </div>
+
+ <form *ngIf="requiresEmailVerification; else emailVerificationNotRequired" role="form" (ngSubmit)="askSendVerifyEmail()" [formGroup]="form">
+ <div class="form-group">
+ <label i18n for="verify-email-email">Email</label>
+ <input
+ type="email" id="verify-email-email" i18n-placeholder placeholder="Email address" required
+ formControlName="verify-email-email" [ngClass]="{ 'input-error': formErrors['verify-email-email'] }"
+ >
+ <div *ngIf="formErrors['verify-email-email']" class="form-error">
+ {{ formErrors['verify-email-email'] }}
+ </div>
+ </div>
+ <input type="submit" i18n-value value="Send verification email" [disabled]="!form.valid">
+ </form>
+ <ng-template #emailVerificationNotRequired>
+ <div i18n>This instance does not require email verification.</div>
+ </ng-template>
+</div>
--- /dev/null
+@import '_variables';
+@import '_mixins';
+
+input:not([type=submit]) {
+ @include peertube-input-text(340px);
+ display: block;
+}
+
+input[type=submit] {
+ @include peertube-button;
+ @include orange-button;
+}
--- /dev/null
+import { Component, OnInit } from '@angular/core'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { NotificationsService } from 'angular2-notifications'
+import { ServerService } from '@app/core/server'
+import { RedirectService } from '@app/core'
+import { UserService, FormReactive } from '@app/shared'
+import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
+import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
+
+@Component({
+ selector: 'my-verify-account-ask-send-email',
+ templateUrl: './verify-account-ask-send-email.component.html',
+ styleUrls: [ './verify-account-ask-send-email.component.scss' ]
+})
+
+export class VerifyAccountAskSendEmailComponent extends FormReactive implements OnInit {
+
+ constructor (
+ protected formValidatorService: FormValidatorService,
+ private userValidatorsService: UserValidatorsService,
+ private userService: UserService,
+ private serverService: ServerService,
+ private notificationsService: NotificationsService,
+ private redirectService: RedirectService,
+ private i18n: I18n
+ ) {
+ super()
+ }
+
+ get requiresEmailVerification () {
+ return this.serverService.getConfig().signup.requiresEmailVerification
+ }
+
+ ngOnInit () {
+ this.buildForm({
+ 'verify-email-email': this.userValidatorsService.USER_EMAIL
+ })
+ }
+
+ askSendVerifyEmail () {
+ const email = this.form.value['verify-email-email']
+ this.userService.askSendVerifyEmail(email)
+ .subscribe(
+ () => {
+ const message = this.i18n(
+ 'An email with verification link will be sent to {{email}}.',
+ { email }
+ )
+ this.notificationsService.success(this.i18n('Success'), message)
+ this.redirectService.redirectToHomepage()
+ },
+
+ err => {
+ this.notificationsService.error(this.i18n('Error'), err.message)
+ }
+ )
+ }
+}
--- /dev/null
+<div class="margin-content">
+ <div i18n class="title-page title-page-single">
+ Verify account email confirmation
+ </div>
+
+ <div i18n *ngIf="success; else verificationError">
+ Your email has been verified and you may now login. Redirecting...
+ </div>
+ <ng-template #verificationError>
+ <div>
+ <span i18n>An error occurred. </span>
+ <a i18n routerLink="/verify-account/ask-email">Request new verification email.</a>
+ </div>
+ </ng-template>
+</div>
--- /dev/null
+import { Component, OnInit } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { NotificationsService } from 'angular2-notifications'
+import { UserService } from '@app/shared'
+
+@Component({
+ selector: 'my-verify-account-email',
+ templateUrl: './verify-account-email.component.html'
+})
+
+export class VerifyAccountEmailComponent implements OnInit {
+ success = false
+
+ private userId: number
+ private verificationString: string
+
+ constructor (
+ private userService: UserService,
+ private notificationsService: NotificationsService,
+ private router: Router,
+ private route: ActivatedRoute,
+ private i18n: I18n
+ ) {
+ }
+
+ ngOnInit () {
+
+ this.userId = this.route.snapshot.queryParams['userId']
+ this.verificationString = this.route.snapshot.queryParams['verificationString']
+
+ if (!this.userId || !this.verificationString) {
+ this.notificationsService.error(this.i18n('Error'), this.i18n('Unable to find user id or verification string.'))
+ } else {
+ this.verifyEmail()
+ }
+ }
+
+ verifyEmail () {
+ this.userService.verifyEmail(this.userId, this.verificationString)
+ .subscribe(
+ () => {
+ this.success = true
+ setTimeout(() => {
+ this.router.navigate([ '/login' ])
+ }, 2000)
+ },
+
+ err => {
+ this.notificationsService.error(this.i18n('Error'), err.message)
+ }
+ )
+ }
+}
--- /dev/null
+import { NgModule } from '@angular/core'
+import { RouterModule, Routes } from '@angular/router'
+
+import { MetaGuard } from '@ngx-meta/core'
+
+import { VerifyAccountEmailComponent } from '@app/+verify-account/verify-account-email/verify-account-email.component'
+import {
+ VerifyAccountAskSendEmailComponent
+} from '@app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component'
+
+const verifyAccountRoutes: Routes = [
+ {
+ path: '',
+ canActivateChild: [ MetaGuard ],
+ children: [
+ {
+ path: 'email',
+ component: VerifyAccountEmailComponent,
+ data: {
+ meta: {
+ title: 'Verify account email'
+ }
+ }
+ },
+ {
+ path: 'ask-send-email',
+ component: VerifyAccountAskSendEmailComponent,
+ data: {
+ meta: {
+ title: 'Verify account ask send email'
+ }
+ }
+ }
+ ]
+ }
+]
+
+@NgModule({
+ imports: [ RouterModule.forChild(verifyAccountRoutes) ],
+ exports: [ RouterModule ]
+})
+export class VerifyAccountRoutingModule {}
--- /dev/null
+import { NgModule } from '@angular/core'
+
+import { VerifyAccountRoutingModule } from '@app/+verify-account/verify-account-routing.module'
+import { VerifyAccountEmailComponent } from '@app/+verify-account/verify-account-email/verify-account-email.component'
+import {
+ VerifyAccountAskSendEmailComponent
+} from '@app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component'
+import { SharedModule } from '@app/shared'
+
+@NgModule({
+ imports: [
+ VerifyAccountRoutingModule,
+ SharedModule
+ ],
+
+ declarations: [
+ VerifyAccountEmailComponent,
+ VerifyAccountAskSendEmailComponent
+ ],
+
+ exports: [
+ ],
+
+ providers: [
+ ]
+})
+export class VerifyAccountModule { }
path: 'my-account',
loadChildren: './+my-account/my-account.module#MyAccountModule'
},
+ {
+ path: 'verify-account',
+ loadChildren: './+verify-account/verify-account.module#VerifyAccountModule'
+ },
{
path: 'accounts',
loadChildren: './+accounts/accounts.module#AccountsModule'
serverVersion: 'Unknown',
signup: {
allowed: false,
- allowedForCurrentIP: false
+ allowedForCurrentIP: false,
+ requiresEmailVerification: false
},
transcoding: {
enabledResolutions: []
Login
</div>
- <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
+ <div *ngIf="error" class="alert alert-danger">{{ error }}
+ <span *ngIf="error === 'User email is not verified.'"> <a i18n routerLink="/verify-account/ask-send-email">Request new verification email.</a></span>
+ </div>
<form role="form" (ngSubmit)="login()" [formGroup]="form">
<div class="form-group">
catchError(res => this.restExtractor.handleError(res))
)
}
+
+ verifyEmail (userId: number, verificationString: string) {
+ const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email`
+ const body = {
+ verificationString
+ }
+
+ return this.authHttp.post(url, body)
+ .pipe(
+ map(this.restExtractor.extractDataBool),
+ catchError(res => this.restExtractor.handleError(res))
+ )
+ }
+
+ askSendVerifyEmail (email: string) {
+ const url = UserService.BASE_USERS_URL + '/ask-send-verify-email'
+
+ return this.authHttp.post(url, { email })
+ .pipe(
+ map(this.restExtractor.extractDataBool),
+ catchError(err => this.restExtractor.handleError(err))
+ )
+ }
}
import { NotificationsService } from 'angular2-notifications'
import { UserCreate } from '../../../../shared'
import { FormReactive, UserService, UserValidatorsService } from '../shared'
-import { RedirectService } from '@app/core'
+import { RedirectService, ServerService } from '@app/core'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
private router: Router,
private notificationsService: NotificationsService,
private userService: UserService,
+ private serverService: ServerService,
private redirectService: RedirectService,
private i18n: I18n
) {
return window.location.host
}
+ get requiresEmailVerification () {
+ return this.serverService.getConfig().signup.requiresEmailVerification
+ }
+
ngOnInit () {
this.buildForm({
username: this.userValidatorsService.USER_USERNAME,
this.userService.signup(userCreate).subscribe(
() => {
- this.notificationsService.success(
- this.i18n('Success'),
- this.i18n('Registration for {{username}} complete.', { username: userCreate.username })
- )
+ if (this.requiresEmailVerification) {
+ this.notificationsService.alert(
+ this.i18n('Welcome'),
+ this.i18n('Please check your email to verify your account and complete signup.')
+ )
+ } else {
+ this.notificationsService.success(
+ this.i18n('Success'),
+ this.i18n('Registration for {{username}} complete.', { username: userCreate.username })
+ )
+ }
this.redirectService.redirectToHomepage()
},
signup:
enabled: false
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
+ requires_email_verification: false
filters:
cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist
whitelist: []
signup:
enabled: false
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
+ requires_email_verification: false
filters:
cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist
whitelist: []
signup:
enabled: true
+ requires_email_verification: false
transcoding:
enabled: true
serverVersion: packageJSON.version,
signup: {
allowed,
- allowedForCurrentIP
+ allowedForCurrentIP,
+ requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
},
transcoding: {
enabledResolutions
toUpdate.transcoding.threads = parseInt('' + toUpdate.transcoding.threads, 10)
// camelCase to snake_case key
- const toUpdateJSON = omit(toUpdate, 'user.videoQuota', 'instance.defaultClientRoute', 'instance.shortDescription', 'cache.videoCaptions')
+ const toUpdateJSON = omit(
+ toUpdate,
+ 'user.videoQuota',
+ 'instance.defaultClientRoute',
+ 'instance.shortDescription',
+ 'cache.videoCaptions',
+ 'signup.requiresEmailVerification'
+ )
toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota
toUpdateJSON.user['video_quota_daily'] = toUpdate.user.videoQuotaDaily
toUpdateJSON.instance['default_client_route'] = toUpdate.instance.defaultClientRoute
toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription
toUpdateJSON.instance['default_nsfw_policy'] = toUpdate.instance.defaultNSFWPolicy
+ toUpdateJSON.signup['requires_email_verification'] = toUpdate.signup.requiresEmailVerification
await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 })
},
signup: {
enabled: CONFIG.SIGNUP.ENABLED,
- limit: CONFIG.SIGNUP.LIMIT
+ limit: CONFIG.SIGNUP.LIMIT,
+ requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
},
admin: {
email: CONFIG.ADMIN.EMAIL
usersSortValidator,
usersUpdateValidator
} from '../../../middlewares'
-import { usersAskResetPasswordValidator, usersBlockingValidator, usersResetPasswordValidator } from '../../../middlewares/validators'
+import {
+ usersAskResetPasswordValidator, usersBlockingValidator, usersResetPasswordValidator,
+ usersAskSendVerifyEmailValidator, usersVerifyEmailValidator
+} from '../../../middlewares/validators'
import { UserModel } from '../../../models/account/user'
import { OAuthTokenModel } from '../../../models/oauth/oauth-token'
import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger'
asyncMiddleware(resetUserPassword)
)
+usersRouter.post('/ask-send-verify-email',
+ loginRateLimiter,
+ asyncMiddleware(usersAskSendVerifyEmailValidator),
+ asyncMiddleware(askSendVerifyUserEmail)
+)
+
+usersRouter.post('/:id/verify-email',
+ asyncMiddleware(usersVerifyEmailValidator),
+ asyncMiddleware(verifyUserEmail)
+)
+
usersRouter.post('/token',
loginRateLimiter,
token,
autoPlayVideo: true,
role: UserRole.USER,
videoQuota: CONFIG.USER.VIDEO_QUOTA,
- videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
+ videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY,
+ emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null
})
const { user } = await createUserAccountAndChannel(userToCreate)
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)
+ }
+
return res.type('json').status(204).end()
}
return res.status(204).end()
}
+async function sendVerifyUserEmail (user: UserModel) {
+ const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id)
+ const url = CONFIG.WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString
+ await Emailer.Instance.addVerifyEmailJob(user.email, url)
+ return
+}
+
+async function askSendVerifyUserEmail (req: express.Request, res: express.Response, next: express.NextFunction) {
+ const user = res.locals.user as UserModel
+
+ await sendVerifyUserEmail(user)
+
+ return res.status(204).end()
+}
+
+async function verifyUserEmail (req: express.Request, res: express.Response, next: express.NextFunction) {
+ const user = res.locals.user as UserModel
+ user.emailVerified = true
+
+ await user.save()
+
+ return res.status(204).end()
+}
+
function success (req: express.Request, res: express.Response, next: express.NextFunction) {
res.end()
}
'cache-captions-size',
'signup-enabled',
'signup-limit',
+ 'signup-requiresEmailVerification',
'admin-email',
'user-videoQuota',
'transcoding-enabled',
return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.DESCRIPTION))
}
+function isUserEmailVerifiedValid (value: any) {
+ return isBooleanValid(value)
+}
+
const nsfwPolicies = values(NSFW_POLICY_TYPES)
function isUserNSFWPolicyValid (value: any) {
return exists(value) && nsfwPolicies.indexOf(value) !== -1
isUserVideoQuotaValid,
isUserVideoQuotaDailyValid,
isUserUsernameValid,
+ isUserEmailVerifiedValid,
isUserNSFWPolicyValid,
isUserAutoPlayVideoValid,
isUserDisplayNameValid,
'log.level',
'user.video_quota', 'user.video_quota_daily',
'cache.previews.size', 'admin.email',
- 'signup.enabled', 'signup.limit', 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
+ 'signup.enabled', 'signup.limit', 'signup.requires_email_verification',
+ 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
'transcoding.enabled', 'transcoding.threads',
'import.videos.http.enabled',
'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 260
+const LAST_MIGRATION_VERSION = 265
// ---------------------------------------------------------------------------
SIGNUP: {
get ENABLED () { return config.get<boolean>('signup.enabled') },
get LIMIT () { return config.get<number>('signup.limit') },
+ get REQUIRES_EMAIL_VERIFICATION () { return config.get<boolean>('signup.requires_email_verification') },
FILTERS: {
CIDR: {
get WHITELIST () { return config.get<string[]>('signup.filters.cidr.whitelist') },
const USER_PASSWORD_RESET_LIFETIME = 60000 * 5 // 5 minutes
+const USER_EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes
+
const NSFW_POLICY_TYPES: { [ id: string]: NSFWPolicyType } = {
DO_NOT_LIST: 'do_not_list',
BLUR: 'blur',
VIDEO_ABUSE_STATES,
JOB_REQUEST_TIMEOUT,
USER_PASSWORD_RESET_LIFETIME,
+ USER_EMAIL_VERIFY_LIFETIME,
IMAGE_MIMETYPE_EXT,
SCHEDULER_INTERVALS_MS,
REPEAT_JOBS,
email,
password,
role,
+ verified: true,
nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
videoQuota: -1,
videoQuotaDaily: -1
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction
+ queryInterface: Sequelize.QueryInterface
+ sequelize: Sequelize.Sequelize
+}): Promise<any> {
+ {
+ const data = {
+ type: Sequelize.BOOLEAN,
+ allowNull: true,
+ defaultValue: null
+ }
+
+ await utils.queryInterface.addColumn('user', 'emailVerified', data)
+ }
+
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export { up, down }
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}
+ addVerifyEmailJob (to: string, verifyEmailUrl: string) {
+ const text = `Welcome to PeerTube,\n\n` +
+ `To start using PeerTube on ${CONFIG.WEBSERVER.HOST} you must verify your email! ` +
+ `Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` +
+ `If you are not the person who initiated this request, please ignore this email.\n\n` +
+ `Cheers,\n` +
+ `PeerTube.`
+
+ const emailPayload: EmailPayload = {
+ to: [ to ],
+ subject: 'Verify your PeerTube email',
+ text
+ }
+
+ return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
+ }
+
async addVideoAbuseReportJob (videoId: number) {
const video = await VideoModel.load(videoId)
if (!video) throw new Error('Unknown Video id during Abuse report.')
import { UserModel } from '../models/account/user'
import { OAuthClientModel } from '../models/oauth/oauth-client'
import { OAuthTokenModel } from '../models/oauth/oauth-token'
+import { CONFIG } from '../initializers/constants'
type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date }
if (user.blocked) throw new AccessDeniedError('User is blocked.')
+ if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION && user.emailVerified === false) {
+ throw new AccessDeniedError('User email is not verified.')
+ }
+
return user
}
import { createClient, RedisClient } from 'redis'
import { logger } from '../helpers/logger'
import { generateRandomString } from '../helpers/utils'
-import { CONFIG, USER_PASSWORD_RESET_LIFETIME, VIDEO_VIEW_LIFETIME } from '../initializers'
+import { CONFIG, USER_PASSWORD_RESET_LIFETIME, USER_EMAIL_VERIFY_LIFETIME, VIDEO_VIEW_LIFETIME } from '../initializers'
type CachedRoute = {
body: string,
return this.getValue(this.generateResetPasswordKey(userId))
}
+ async setVerifyEmailVerificationString (userId: number) {
+ const generatedString = await generateRandomString(32)
+
+ await this.setValue(this.generateVerifyEmailKey(userId), generatedString, USER_EMAIL_VERIFY_LIFETIME)
+
+ return generatedString
+ }
+
+ async getVerifyEmailLink (userId: number) {
+ return this.getValue(this.generateVerifyEmailKey(userId))
+ }
+
setIPVideoView (ip: string, videoUUID: string) {
return this.setValue(this.buildViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME)
}
return 'reset-password-' + userId
}
+ generateVerifyEmailKey (userId: number) {
+ return 'verify-email-' + userId
+ }
+
buildViewKey (ip: string, videoUUID: string) {
return videoUUID + '-' + ip
}
}
]
+const usersAskSendVerifyEmailValidator = [
+ body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking askUsersSendVerifyEmail parameters', { parameters: req.body })
+
+ 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(204).end()
+ }
+
+ 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'),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
+
+ if (areValidationErrors(req, res)) return
+ if (!await checkUserIdExist(req.params.id, res)) return
+
+ const user = res.locals.user as UserModel
+ const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
+
+ if (redisVerificationString !== req.body.verificationString) {
+ return res
+ .status(403)
+ .send({ error: 'Invalid verification string.' })
+ .end()
+ }
+
+ return next()
+ }
+]
+
// ---------------------------------------------------------------------------
export {
ensureUserRegistrationAllowedForIP,
usersGetValidator,
usersAskResetPasswordValidator,
- usersResetPasswordValidator
+ usersResetPasswordValidator,
+ usersAskSendVerifyEmailValidator,
+ usersVerifyEmailValidator
}
// ---------------------------------------------------------------------------
isUserBlockedReasonValid,
isUserBlockedValid,
isUserNSFWPolicyValid,
+ isUserEmailVerifiedValid,
isUserPasswordValid,
isUserRoleValid,
isUserUsernameValid,
@Column(DataType.STRING(400))
email: string
+ @AllowNull(true)
+ @Default(null)
+ @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean'))
+ @Column
+ emailVerified: boolean
+
@AllowNull(false)
@Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
@Column(DataType.ENUM(values(NSFW_POLICY_TYPES)))
id: this.id,
username: this.username,
email: this.email,
+ emailVerified: this.emailVerified,
nsfwPolicy: this.nsfwPolicy,
autoPlayVideo: this.autoPlayVideo,
role: this.role,
},
signup: {
enabled: false,
- limit: 5
+ limit: 5,
+ requiresEmailVerification: false
},
admin: {
email: 'superadmin1@example.com'
})
})
+ describe('When asking for an account verification email', function () {
+ const path = '/api/v1/users/ask-send-verify-email'
+
+ it('Should fail with a missing email', async function () {
+ const fields = {}
+
+ await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+ })
+
+ it('Should fail with an invalid email', async function () {
+ const fields = { email: 'hello' }
+
+ await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+ })
+
+ it('Should succeed with the correct params', async function () {
+ const fields = { email: 'admin@example.com' }
+
+ await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 204 })
+ })
+ })
+
after(async function () {
killallServers([ server, serverWithRegistrationDisabled ])
expect(data.cache.captions.size).to.equal(1)
expect(data.signup.enabled).to.be.true
expect(data.signup.limit).to.equal(4)
+ expect(data.signup.requiresEmailVerification).to.be.false
expect(data.admin.email).to.equal('admin1@example.com')
expect(data.user.videoQuota).to.equal(5242880)
expect(data.user.videoQuotaDaily).to.equal(-1)
expect(data.cache.captions.size).to.equal(3)
expect(data.signup.enabled).to.be.false
expect(data.signup.limit).to.equal(5)
+ expect(data.signup.requiresEmailVerification).to.be.true
expect(data.admin.email).to.equal('superadmin1@example.com')
expect(data.user.videoQuota).to.equal(5242881)
expect(data.user.videoQuotaDaily).to.equal(318742)
},
signup: {
enabled: false,
- limit: 5
+ limit: 5,
+ requiresEmailVerification: true
},
admin: {
email: 'superadmin1@example.com'
import {
addVideoToBlacklist,
askResetPassword,
+ askSendVerifyEmail,
blockUser,
createUser, removeVideoFromBlacklist,
reportVideoAbuse,
runServer,
unblockUser,
uploadVideo,
- userLogin
+ userLogin,
+ verifyEmail
} from '../../utils'
import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index'
import { mockSmtpServer } from '../../utils/miscs/email'
})
})
+ describe('When verifying a user email', function () {
+
+ it('Should ask to send the verification email', async function () {
+ this.timeout(10000)
+
+ await askSendVerifyEmail(server.url, 'user_1@example.com')
+
+ await waitJobs(server)
+ expect(emails).to.have.lengthOf(7)
+
+ const email = emails[6]
+
+ expect(email['from'][0]['address']).equal('test-admin@localhost')
+ expect(email['to'][0]['address']).equal('user_1@example.com')
+ expect(email['subject']).contains('Verify')
+
+ const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text'])
+ expect(verificationStringMatches).not.to.be.null
+
+ verificationString = verificationStringMatches[1]
+ expect(verificationString).to.not.be.undefined
+ expect(verificationString).to.have.length.above(2)
+
+ const userIdMatches = /userId=([0-9]+)/.exec(email['text'])
+ expect(userIdMatches).not.to.be.null
+
+ userId = parseInt(userIdMatches[1], 10)
+ })
+
+ it('Should not verify the email with an invalid verification string', async function () {
+ await verifyEmail(server.url, userId, verificationString + 'b', 403)
+ })
+
+ it('Should verify the email', async function () {
+ await verifyEmail(server.url, userId, verificationString)
+ })
+ })
+
after(async function () {
killallServers([ server ])
})
import './user-subscriptions'
import './users'
+import './users-verification'
import './users-multiple-servers'
--- /dev/null
+/* tslint:disable:no-unused-expression */
+
+import * as chai from 'chai'
+import 'mocha'
+import {
+ registerUser, flushTests, getUserInformation, getMyUserInformation, killallServers,
+ userLogin, login, runServer, ServerInfo, verifyEmail, updateCustomSubConfig
+} from '../../utils'
+import { setAccessTokensToServers } from '../../utils/users/login'
+import { mockSmtpServer } from '../../utils/miscs/email'
+import { waitJobs } from '../../utils/server/jobs'
+
+const expect = chai.expect
+
+describe('Test users account verification', function () {
+ let server: ServerInfo
+ let userId: number
+ let verificationString: string
+ let expectedEmailsLength = 0
+ const user1 = {
+ username: 'user_1',
+ password: 'super password'
+ }
+ const user2 = {
+ username: 'user_2',
+ password: 'super password'
+ }
+ const emails: object[] = []
+
+ before(async function () {
+ this.timeout(30000)
+
+ await mockSmtpServer(emails)
+
+ await flushTests()
+
+ const overrideConfig = {
+ smtp: {
+ hostname: 'localhost'
+ }
+ }
+ server = await runServer(1, overrideConfig)
+
+ await setAccessTokensToServers([ server ])
+ })
+
+ it('Should register user and send verification email if verification required', async function () {
+ this.timeout(5000)
+ await updateCustomSubConfig(server.url, server.accessToken, {
+ signup: {
+ enabled: true,
+ requiresEmailVerification: true,
+ limit: 10
+ }
+ })
+
+ await registerUser(server.url, user1.username, user1.password)
+
+ await waitJobs(server)
+ expectedEmailsLength++
+ expect(emails).to.have.lengthOf(expectedEmailsLength)
+
+ const email = emails[expectedEmailsLength - 1]
+
+ const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text'])
+ expect(verificationStringMatches).not.to.be.null
+
+ verificationString = verificationStringMatches[1]
+ expect(verificationString).to.have.length.above(2)
+
+ const userIdMatches = /userId=([0-9]+)/.exec(email['text'])
+ expect(userIdMatches).not.to.be.null
+
+ userId = parseInt(userIdMatches[1], 10)
+
+ const resUserInfo = await getUserInformation(server.url, server.accessToken, userId)
+ expect(resUserInfo.body.emailVerified).to.be.false
+ })
+
+ it('Should not allow login for user with unverified email', async function () {
+ const resLogin = await login(server.url, server.client, user1, 400)
+ expect(resLogin.body.error).to.contain('User email is not verified.')
+ })
+
+ it('Should verify the user via email and allow login', async function () {
+ await verifyEmail(server.url, userId, verificationString)
+ await login(server.url, server.client, user1)
+ const resUserVerified = await getUserInformation(server.url, server.accessToken, userId)
+ expect(resUserVerified.body.emailVerified).to.be.true
+ })
+
+ it('Should register user not requiring email verification if setting not enabled', async function () {
+ this.timeout(5000)
+ await updateCustomSubConfig(server.url, server.accessToken, {
+ signup: {
+ enabled: true,
+ requiresEmailVerification: false,
+ limit: 10
+ }
+ })
+
+ await registerUser(server.url, user2.username, user2.password)
+
+ await waitJobs(server)
+ expect(emails).to.have.lengthOf(expectedEmailsLength)
+
+ const accessToken = await userLogin(server, user2)
+
+ const resMyUserInfo = await getMyUserInformation(server.url, accessToken)
+ expect(resMyUserInfo.body.emailVerified).to.be.null
+ })
+
+ it('Should allow login for user with unverified email when setting later enabled', async function () {
+ await updateCustomSubConfig(server.url, server.accessToken, {
+ signup: {
+ enabled: true,
+ requiresEmailVerification: true,
+ limit: 10
+ }
+ })
+
+ await userLogin(server, user2)
+ })
+
+ after(async function () {
+ killallServers([ server ])
+
+ // Keep the logs if the test failed
+ if (this[ 'ok' ]) {
+ await flushTests()
+ }
+ })
+})
createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoQuotaUsed, getMyUserVideoRating,
getUserInformation, getUsersList, getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo,
registerUser, removeUser, removeVideo, runServer, ServerInfo, testImage, updateMyAvatar, updateMyUser, updateUser, uploadVideo, userLogin,
- deleteMe, blockUser, unblockUser
+ deleteMe, blockUser, unblockUser, updateCustomSubConfig
} from '../../utils/index'
import { follow } from '../../utils/server/follows'
import { setAccessTokensToServers } from '../../utils/users/login'
},
signup: {
enabled: false,
- limit: 5
+ limit: 5,
+ requiresEmailVerification: false
},
admin: {
email: 'superadmin1@example.com'
})
}
+function askSendVerifyEmail (url: string, email: string) {
+ const path = '/api/v1/users/ask-send-verify-email'
+
+ return makePostBodyRequest({
+ url,
+ path,
+ fields: { email },
+ statusCodeExpected: 204
+ })
+}
+
+function verifyEmail (url: string, userId: number, verificationString: string, statusCodeExpected = 204) {
+ const path = '/api/v1/users/' + userId + '/verify-email'
+
+ return makePostBodyRequest({
+ url,
+ path,
+ fields: { verificationString },
+ statusCodeExpected
+ })
+}
+
// ---------------------------------------------------------------------------
export {
unblockUser,
askResetPassword,
resetPassword,
- updateMyAvatar
+ updateMyAvatar,
+ askSendVerifyEmail,
+ verifyEmail
}
signup: {
enabled: boolean
limit: number
+ requiresEmailVerification: boolean
}
admin: {
signup: {
allowed: boolean,
- allowedForCurrentIP: boolean
+ allowedForCurrentIP: boolean,
+ requiresEmailVerification: boolean
}
transcoding: {