mirror of https://github.com/Chocobozzz/PeerTube
add user account email verificiation (#977)
* add user account email verificiation includes server and client code to: * enable verificationRequired via custom config * send verification email with registration * ask for verification email * verify via email * prevent login if not verified and required * conditional client links to ask for new verification email * allow login for verified=null these are users created when verification not required should still be able to login when verification is enabled * refactor email verifcation pr * change naming from verified to emailVerified * change naming from askVerifyEmail to askSendVerifyEmail * undo unrelated automatic prettier formatting on api/config * use redirectService for home * remove redundant success notification on email verified * revert test.yaml smpt hostpull/965/merge
parent
04291e1ba4
commit
d9eaee3939
|
@ -91,6 +91,11 @@
|
|||
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
|
||||
|
|
|
@ -90,6 +90,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
cacheCaptionsSize: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE,
|
||||
signupEnabled: null,
|
||||
signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT,
|
||||
signupRequiresEmailVerification: null,
|
||||
importVideosHttpEnabled: null,
|
||||
importVideosTorrentEnabled: null,
|
||||
adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL,
|
||||
|
@ -187,7 +188,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
},
|
||||
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']
|
||||
|
@ -250,6 +252,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
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,
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export * from '@app/+verify-account/verify-account-routing.module'
|
||||
export * from '@app/+verify-account/verify-account.module'
|
|
@ -0,0 +1,22 @@
|
|||
<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>
|
|
@ -0,0 +1,12 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
input:not([type=submit]) {
|
||||
@include peertube-input-text(340px);
|
||||
display: block;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
@include peertube-button;
|
||||
@include orange-button;
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<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>
|
|
@ -0,0 +1,54 @@
|
|||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
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 {}
|
|
@ -0,0 +1,27 @@
|
|||
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 { }
|
|
@ -13,6 +13,10 @@ const routes: Routes = [
|
|||
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'
|
||||
|
|
|
@ -40,7 +40,8 @@ export class ServerService {
|
|||
serverVersion: 'Unknown',
|
||||
signup: {
|
||||
allowed: false,
|
||||
allowedForCurrentIP: false
|
||||
allowedForCurrentIP: false,
|
||||
requiresEmailVerification: false
|
||||
},
|
||||
transcoding: {
|
||||
enabledResolutions: []
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
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">
|
||||
|
|
|
@ -94,4 +94,27 @@ export class UserService {
|
|||
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))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Router } from '@angular/router'
|
|||
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'
|
||||
|
||||
|
@ -21,6 +21,7 @@ export class SignupComponent extends FormReactive implements OnInit {
|
|||
private router: Router,
|
||||
private notificationsService: NotificationsService,
|
||||
private userService: UserService,
|
||||
private serverService: ServerService,
|
||||
private redirectService: RedirectService,
|
||||
private i18n: I18n
|
||||
) {
|
||||
|
@ -31,6 +32,10 @@ export class SignupComponent extends FormReactive implements OnInit {
|
|||
return window.location.host
|
||||
}
|
||||
|
||||
get requiresEmailVerification () {
|
||||
return this.serverService.getConfig().signup.requiresEmailVerification
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.buildForm({
|
||||
username: this.userValidatorsService.USER_USERNAME,
|
||||
|
@ -47,10 +52,17 @@ export class SignupComponent extends FormReactive implements OnInit {
|
|||
|
||||
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()
|
||||
},
|
||||
|
||||
|
|
|
@ -74,6 +74,7 @@ admin:
|
|||
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: []
|
||||
|
|
|
@ -87,6 +87,7 @@ admin:
|
|||
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: []
|
||||
|
|
|
@ -29,6 +29,7 @@ cache:
|
|||
|
||||
signup:
|
||||
enabled: true
|
||||
requires_email_verification: false
|
||||
|
||||
transcoding:
|
||||
enabled: true
|
||||
|
|
|
@ -60,7 +60,8 @@ async function getConfig (req: express.Request, res: express.Response, next: exp
|
|||
serverVersion: packageJSON.version,
|
||||
signup: {
|
||||
allowed,
|
||||
allowedForCurrentIP
|
||||
allowedForCurrentIP,
|
||||
requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
|
||||
},
|
||||
transcoding: {
|
||||
enabledResolutions
|
||||
|
@ -159,12 +160,20 @@ async function updateCustomConfig (req: express.Request, res: express.Response,
|
|||
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 })
|
||||
|
||||
|
@ -220,7 +229,8 @@ function customConfig (): CustomConfig {
|
|||
},
|
||||
signup: {
|
||||
enabled: CONFIG.SIGNUP.ENABLED,
|
||||
limit: CONFIG.SIGNUP.LIMIT
|
||||
limit: CONFIG.SIGNUP.LIMIT,
|
||||
requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
|
||||
},
|
||||
admin: {
|
||||
email: CONFIG.ADMIN.EMAIL
|
||||
|
|
|
@ -25,7 +25,10 @@ import {
|
|||
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'
|
||||
|
@ -110,6 +113,17 @@ usersRouter.post('/:id/reset-password',
|
|||
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,
|
||||
|
@ -165,7 +179,8 @@ async function registerUser (req: express.Request, res: express.Response) {
|
|||
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)
|
||||
|
@ -173,6 +188,10 @@ async function registerUser (req: express.Request, res: express.Response) {
|
|||
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()
|
||||
}
|
||||
|
||||
|
@ -261,6 +280,30 @@ async function resetUserPassword (req: express.Request, res: express.Response, n
|
|||
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()
|
||||
}
|
||||
|
|
|
@ -234,6 +234,7 @@ const customConfigKeysToKeep = [
|
|||
'cache-captions-size',
|
||||
'signup-enabled',
|
||||
'signup-limit',
|
||||
'signup-requiresEmailVerification',
|
||||
'admin-email',
|
||||
'user-videoQuota',
|
||||
'transcoding-enabled',
|
||||
|
|
|
@ -33,6 +33,10 @@ function isUserDescriptionValid (value: string) {
|
|||
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
|
||||
|
@ -72,6 +76,7 @@ export {
|
|||
isUserVideoQuotaValid,
|
||||
isUserVideoQuotaDailyValid,
|
||||
isUserUsernameValid,
|
||||
isUserEmailVerifiedValid,
|
||||
isUserNSFWPolicyValid,
|
||||
isUserAutoPlayVideoValid,
|
||||
isUserDisplayNameValid,
|
||||
|
|
|
@ -49,7 +49,8 @@ function checkMissedConfig () {
|
|||
'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',
|
||||
|
|
|
@ -15,7 +15,7 @@ let config: IConfig = require('config')
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LAST_MIGRATION_VERSION = 260
|
||||
const LAST_MIGRATION_VERSION = 265
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
@ -204,6 +204,7 @@ const CONFIG = {
|
|||
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') },
|
||||
|
@ -500,6 +501,8 @@ const BCRYPT_SALT_SIZE = 10
|
|||
|
||||
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',
|
||||
|
@ -661,6 +664,7 @@ export {
|
|||
VIDEO_ABUSE_STATES,
|
||||
JOB_REQUEST_TIMEOUT,
|
||||
USER_PASSWORD_RESET_LIFETIME,
|
||||
USER_EMAIL_VERIFY_LIFETIME,
|
||||
IMAGE_MIMETYPE_EXT,
|
||||
SCHEDULER_INTERVALS_MS,
|
||||
REPEAT_JOBS,
|
||||
|
|
|
@ -122,6 +122,7 @@ async function createOAuthAdminIfNotExist () {
|
|||
email,
|
||||
password,
|
||||
role,
|
||||
verified: true,
|
||||
nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
|
||||
videoQuota: -1,
|
||||
videoQuotaDaily: -1
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
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 }
|
|
@ -89,6 +89,23 @@ class Emailer {
|
|||
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.')
|
||||
|
|
|
@ -3,6 +3,7 @@ import { logger } from '../helpers/logger'
|
|||
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 }
|
||||
|
||||
|
@ -37,6 +38,10 @@ async function getUser (usernameOrEmail: string, password: string) {
|
|||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as express from 'express'
|
|||
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,
|
||||
|
@ -60,6 +60,18 @@ class Redis {
|
|||
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)
|
||||
}
|
||||
|
@ -135,6 +147,10 @@ class Redis {
|
|||
return 'reset-password-' + userId
|
||||
}
|
||||
|
||||
generateVerifyEmailKey (userId: number) {
|
||||
return 'verify-email-' + userId
|
||||
}
|
||||
|
||||
buildViewKey (ip: string, videoUUID: string) {
|
||||
return videoUUID + '-' + ip
|
||||
}
|
||||
|
|
|
@ -248,6 +248,48 @@ 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) => {
|
||||
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 {
|
||||
|
@ -263,7 +305,9 @@ export {
|
|||
ensureUserRegistrationAllowedForIP,
|
||||
usersGetValidator,
|
||||
usersAskResetPasswordValidator,
|
||||
usersResetPasswordValidator
|
||||
usersResetPasswordValidator,
|
||||
usersAskSendVerifyEmailValidator,
|
||||
usersVerifyEmailValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
isUserBlockedReasonValid,
|
||||
isUserBlockedValid,
|
||||
isUserNSFWPolicyValid,
|
||||
isUserEmailVerifiedValid,
|
||||
isUserPasswordValid,
|
||||
isUserRoleValid,
|
||||
isUserUsernameValid,
|
||||
|
@ -92,6 +93,12 @@ export class UserModel extends Model<UserModel> {
|
|||
@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)))
|
||||
|
@ -304,6 +311,7 @@ export class UserModel extends Model<UserModel> {
|
|||
id: this.id,
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
emailVerified: this.emailVerified,
|
||||
nsfwPolicy: this.nsfwPolicy,
|
||||
autoPlayVideo: this.autoPlayVideo,
|
||||
role: this.role,
|
||||
|
|
|
@ -42,7 +42,8 @@ describe('Test config API validators', function () {
|
|||
},
|
||||
signup: {
|
||||
enabled: false,
|
||||
limit: 5
|
||||
limit: 5,
|
||||
requiresEmailVerification: false
|
||||
},
|
||||
admin: {
|
||||
email: 'superadmin1@example.com'
|
||||
|
|
|
@ -737,6 +737,28 @@ describe('Test users API validators', function () {
|
|||
})
|
||||
})
|
||||
|
||||
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 ])
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ function checkInitialConfig (data: CustomConfig) {
|
|||
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)
|
||||
|
@ -64,6 +65,7 @@ function checkUpdatedConfig (data: CustomConfig) {
|
|||
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)
|
||||
|
@ -148,7 +150,8 @@ describe('Test config', function () {
|
|||
},
|
||||
signup: {
|
||||
enabled: false,
|
||||
limit: 5
|
||||
limit: 5,
|
||||
requiresEmailVerification: true
|
||||
},
|
||||
admin: {
|
||||
email: 'superadmin1@example.com'
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'mocha'
|
|||
import {
|
||||
addVideoToBlacklist,
|
||||
askResetPassword,
|
||||
askSendVerifyEmail,
|
||||
blockUser,
|
||||
createUser, removeVideoFromBlacklist,
|
||||
reportVideoAbuse,
|
||||
|
@ -12,7 +13,8 @@ import {
|
|||
runServer,
|
||||
unblockUser,
|
||||
uploadVideo,
|
||||
userLogin
|
||||
userLogin,
|
||||
verifyEmail
|
||||
} from '../../utils'
|
||||
import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index'
|
||||
import { mockSmtpServer } from '../../utils/miscs/email'
|
||||
|
@ -207,6 +209,44 @@ describe('Test emails', function () {
|
|||
})
|
||||
})
|
||||
|
||||
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 ])
|
||||
})
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import './user-subscriptions'
|
||||
import './users'
|
||||
import './users-verification'
|
||||
import './users-multiple-servers'
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
/* 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()
|
||||
}
|
||||
})
|
||||
})
|
|
@ -7,7 +7,7 @@ import {
|
|||
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'
|
||||
|
|
|
@ -74,7 +74,8 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) {
|
|||
},
|
||||
signup: {
|
||||
enabled: false,
|
||||
limit: 5
|
||||
limit: 5,
|
||||
requiresEmailVerification: false
|
||||
},
|
||||
admin: {
|
||||
email: 'superadmin1@example.com'
|
||||
|
|
|
@ -246,6 +246,28 @@ function resetPassword (url: string, userId: number, verificationString: string,
|
|||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -265,5 +287,7 @@ export {
|
|||
unblockUser,
|
||||
askResetPassword,
|
||||
resetPassword,
|
||||
updateMyAvatar
|
||||
updateMyAvatar,
|
||||
askSendVerifyEmail,
|
||||
verifyEmail
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ export interface CustomConfig {
|
|||
signup: {
|
||||
enabled: boolean
|
||||
limit: number
|
||||
requiresEmailVerification: boolean
|
||||
}
|
||||
|
||||
admin: {
|
||||
|
|
|
@ -16,7 +16,8 @@ export interface ServerConfig {
|
|||
|
||||
signup: {
|
||||
allowed: boolean,
|
||||
allowedForCurrentIP: boolean
|
||||
allowedForCurrentIP: boolean,
|
||||
requiresEmailVerification: boolean
|
||||
}
|
||||
|
||||
transcoding: {
|
||||
|
|
Loading…
Reference in New Issue