mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 09:49:20 +02:00
Better ask email verification flow
Allow user to resend the email verification link when changing the current email Fix success messages when validating a new email
This commit is contained in:
parent
e19ee1ebc9
commit
986e71a1f7
29 changed files with 426 additions and 271 deletions
|
@ -1,12 +1,12 @@
|
|||
import { NgIf } from '@angular/common'
|
||||
import { Component, OnDestroy, OnInit, inject, viewChild } from '@angular/core'
|
||||
import { Component, inject, OnDestroy, OnInit, viewChild } from '@angular/core'
|
||||
import { ComponentPaginationLight, DisableForReuseHook, ScreenService } from '@app/core'
|
||||
import { Account } from '@app/shared/shared-main/account/account.model'
|
||||
import { AccountService } from '@app/shared/shared-main/account/account.service'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { VideoFilters } from '@app/shared/shared-video-miniature/video-filters.model'
|
||||
import { VideoSortField } from '@peertube/peertube-models'
|
||||
import { first, Subscription } from 'rxjs'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { VideosListComponent } from '../../shared/shared-video-miniature/videos-list.component'
|
||||
|
||||
@Component({
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
<my-alert *ngIf="error" type="danger">{{ error }}</my-alert>
|
||||
<my-alert *ngIf="success" type="success">{{ success }}</my-alert>
|
||||
|
||||
<div i18n class="pending-email" *ngIf="user.pendingEmail">
|
||||
<strong>{{ user.pendingEmail }}</strong> is awaiting email verification
|
||||
<div class="pending-email" *ngIf="user.pendingEmail">
|
||||
<div i18n>
|
||||
<strong>{{ user.pendingEmail }}</strong> is awaiting email verification.
|
||||
</div>
|
||||
|
||||
@if (verificationEmailSent) {
|
||||
<div i18n>Email verification sent!</div>
|
||||
} @else {
|
||||
<button type="button" class="peertube-button-like-link" i18n (click)="resendVerificationEmail()">Resend your verification email</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<form class="change-email" (ngSubmit)="changeEmail()" [formGroup]="form">
|
||||
|
|
|
@ -23,6 +23,7 @@ export class MyAccountChangeEmailComponent extends FormReactive implements OnIni
|
|||
private userService = inject(UserService)
|
||||
private serverService = inject(ServerService)
|
||||
|
||||
verificationEmailSent = false
|
||||
error: string
|
||||
success: string
|
||||
user: User
|
||||
|
@ -68,4 +69,16 @@ export class MyAccountChangeEmailComponent extends FormReactive implements OnIni
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
resendVerificationEmail () {
|
||||
this.userService.askSendVerifyEmail(this.user.pendingEmail).subscribe({
|
||||
next: () => {
|
||||
this.verificationEmailSent = true
|
||||
},
|
||||
|
||||
error: err => {
|
||||
this.error = err.message
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Routes } from '@angular/router'
|
||||
import { VerifyAccountAskSendEmailComponent } from './verify-account-ask-send-email/verify-account-ask-send-email.component'
|
||||
import { VerifyNewAccountAskSendEmailComponent } from './verify-new-account-ask-send-email/verify-new-account-ask-send-email.component'
|
||||
import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component'
|
||||
import { SignupService } from '../shared/signup.service'
|
||||
|
||||
|
@ -19,7 +19,7 @@ export default [
|
|||
},
|
||||
{
|
||||
path: 'ask-send-email',
|
||||
component: VerifyAccountAskSendEmailComponent,
|
||||
component: VerifyNewAccountAskSendEmailComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Ask to send an email to verify your account`
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
<div class="margin-content">
|
||||
<h1 i18n class="title-page">Request email for account verification</h1>
|
||||
|
||||
<form *ngIf="requiresEmailVerification; else emailVerificationNotRequired" (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" class="form-control" [ngClass]="{ 'input-error': formErrors['verify-email-email'] }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors['verify-email-email']" class="form-error" role="alert">{{ formErrors['verify-email-email'] }}</div>
|
||||
</div>
|
||||
|
||||
<input class="peertube-button primary-button" 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>
|
|
@ -1,46 +0,0 @@
|
|||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { SignupService } from '@app/+signup/shared/signup.service'
|
||||
import { Notifier, RedirectService, ServerService } from '@app/core'
|
||||
import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
||||
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgIf, NgClass } from '@angular/common'
|
||||
|
||||
@Component({
|
||||
selector: 'my-verify-account-ask-send-email',
|
||||
templateUrl: './verify-account-ask-send-email.component.html',
|
||||
styleUrls: [ './verify-account-ask-send-email.component.scss' ],
|
||||
imports: [ NgIf, FormsModule, ReactiveFormsModule, NgClass ]
|
||||
})
|
||||
export class VerifyAccountAskSendEmailComponent extends FormReactive implements OnInit {
|
||||
protected formReactiveService = inject(FormReactiveService)
|
||||
private signupService = inject(SignupService)
|
||||
private serverService = inject(ServerService)
|
||||
private notifier = inject(Notifier)
|
||||
private redirectService = inject(RedirectService)
|
||||
|
||||
requiresEmailVerification = false
|
||||
|
||||
ngOnInit () {
|
||||
this.serverService.getConfig()
|
||||
.subscribe(config => this.requiresEmailVerification = config.signup.requiresEmailVerification)
|
||||
|
||||
this.buildForm({
|
||||
'verify-email-email': USER_EMAIL_VALIDATOR
|
||||
})
|
||||
}
|
||||
|
||||
askSendVerifyEmail () {
|
||||
const email = this.form.value['verify-email-email']
|
||||
this.signupService.askSendVerifyEmail(email)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`An email with verification link will be sent to ${email}.`)
|
||||
this.redirectService.redirectToHomepage()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,19 +1,20 @@
|
|||
<div *ngIf="loaded" class="margin-content">
|
||||
<h1 i18n class="title-page">Verify email</h1>
|
||||
|
||||
<my-signup-success-after-email
|
||||
*ngIf="displaySignupSuccess()"
|
||||
[requiresApproval]="isRegistrationRequest() && requiresApproval"
|
||||
>
|
||||
</my-signup-success-after-email>
|
||||
@if (success) {
|
||||
@if (this.isRegistration() || this.isRegistrationRequest()) {
|
||||
<my-signup-success-after-email [requiresApproval]="requiresApproval"></my-signup-success-after-email>
|
||||
} @else {
|
||||
<my-alert type="success" i18n>Email updated.</my-alert>
|
||||
}
|
||||
|
||||
<my-alert type="success" i18n *ngIf="!isRegistrationRequest() && isPendingEmail && success">Email updated.</my-alert>
|
||||
|
||||
<my-alert type="danger" *ngIf="failed">
|
||||
} @else if (failed) {
|
||||
<my-alert type="danger">
|
||||
<span i18n>An error occurred.</span>
|
||||
|
||||
<a i18n class="ms-1 link-primary" routerLink="/verify-account/ask-send-email">
|
||||
Request a new verification email
|
||||
</a>
|
||||
</my-alert>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { NgIf } from '@angular/common'
|
|||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router'
|
||||
import { SignupService } from '@app/+signup/shared/signup.service'
|
||||
import { AuthService, Notifier, ServerService } from '@app/core'
|
||||
import { AuthService, Notifier, ServerService, UserService } from '@app/core'
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { SignupSuccessAfterEmailComponent } from '../../shared/signup-success-after-email.component'
|
||||
|
||||
|
@ -13,6 +13,7 @@ import { SignupSuccessAfterEmailComponent } from '../../shared/signup-success-af
|
|||
})
|
||||
export class VerifyAccountEmailComponent implements OnInit {
|
||||
private signupService = inject(SignupService)
|
||||
private userService = inject(UserService)
|
||||
private server = inject(ServerService)
|
||||
private authService = inject(AuthService)
|
||||
private notifier = inject(Notifier)
|
||||
|
@ -44,9 +45,7 @@ export class VerifyAccountEmailComponent implements OnInit {
|
|||
|
||||
this.userId = queryParams['userId']
|
||||
this.registrationId = queryParams['registrationId']
|
||||
|
||||
this.verificationString = queryParams['verificationString']
|
||||
|
||||
this.isPendingEmail = queryParams['isPendingEmail'] === 'true'
|
||||
|
||||
if (!this.verificationString) {
|
||||
|
@ -62,15 +61,12 @@ export class VerifyAccountEmailComponent implements OnInit {
|
|||
this.verifyEmail()
|
||||
}
|
||||
|
||||
isRegistrationRequest () {
|
||||
return !!this.registrationId
|
||||
isRegistration () {
|
||||
return !this.isPendingEmail
|
||||
}
|
||||
|
||||
displaySignupSuccess () {
|
||||
if (!this.success) return false
|
||||
if (!this.isRegistrationRequest() && this.isPendingEmail) return false
|
||||
|
||||
return true
|
||||
isRegistrationRequest () {
|
||||
return !!this.registrationId
|
||||
}
|
||||
|
||||
verifyEmail () {
|
||||
|
@ -88,7 +84,7 @@ export class VerifyAccountEmailComponent implements OnInit {
|
|||
isPendingEmail: this.isPendingEmail
|
||||
}
|
||||
|
||||
this.signupService.verifyUserEmail(options)
|
||||
this.userService.verifyUserEmail(options)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
if (this.authService.isLoggedIn()) {
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<div class="margin-content">
|
||||
<h1 i18n class="title-page">Request email for account verification</h1>
|
||||
|
||||
@if (requiresEmailVerification) {
|
||||
<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" class="form-control" [ngClass]="{ 'input-error': formErrors['verify-email-email'] }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors['verify-email-email']" class="form-error" role="alert">{{ formErrors['verify-email-email'] }}</div>
|
||||
</div>
|
||||
|
||||
<input class="peertube-button primary-button" type="submit" i18n-value value="Send verification email" [disabled]="!form.valid">
|
||||
</form>
|
||||
} @else {
|
||||
<div i18n>{{ instanceName }} does not require email verification.</div>
|
||||
}
|
||||
</div>
|
|
@ -0,0 +1,57 @@
|
|||
import { NgClass, NgIf } from '@angular/common'
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { SignupService } from '@app/+signup/shared/signup.service'
|
||||
import { Notifier, RedirectService, ServerService, UserService } from '@app/core'
|
||||
import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
||||
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { forkJoin } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'my-verify-new-account-ask-send-email',
|
||||
templateUrl: './verify-new-account-ask-send-email.component.html',
|
||||
styleUrls: [ './verify-new-account-ask-send-email.component.scss' ],
|
||||
imports: [ NgIf, FormsModule, ReactiveFormsModule, NgClass ]
|
||||
})
|
||||
export class VerifyNewAccountAskSendEmailComponent extends FormReactive implements OnInit {
|
||||
protected formReactiveService = inject(FormReactiveService)
|
||||
private userService = inject(UserService)
|
||||
private signupService = inject(SignupService)
|
||||
private serverService = inject(ServerService)
|
||||
private notifier = inject(Notifier)
|
||||
private redirectService = inject(RedirectService)
|
||||
|
||||
requiresEmailVerification = false
|
||||
|
||||
get instanceName () {
|
||||
return this.serverService.getHTMLConfig().instance.name
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.serverService.getConfig()
|
||||
.subscribe(config => {
|
||||
this.requiresEmailVerification = config.signup.requiresEmailVerification
|
||||
})
|
||||
|
||||
this.buildForm({
|
||||
'verify-email-email': USER_EMAIL_VALIDATOR
|
||||
})
|
||||
}
|
||||
|
||||
askSendVerifyEmail () {
|
||||
const email = this.form.value['verify-email-email']
|
||||
|
||||
forkJoin([
|
||||
this.userService.askSendVerifyEmail(email),
|
||||
this.signupService.askSendVerifyEmail(email)
|
||||
]).subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`An email with verification link will be sent to ${email}.`)
|
||||
this.redirectService.redirectToHomepage()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -3,19 +3,17 @@
|
|||
</my-signup-step-title>
|
||||
|
||||
<my-alert type="primary">
|
||||
<ng-container *ngIf="requiresApproval()">
|
||||
@if (requiresApproval()) {
|
||||
<p i18n>Your email has been verified and your account request has been sent!</p>
|
||||
|
||||
<p i18n>
|
||||
A moderator will check your registration request soon and you'll receive an email when it is accepted or rejected.
|
||||
</p>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!requiresApproval()">
|
||||
} @else {
|
||||
<p i18n>Your email has been verified and your account has been created!</p>
|
||||
|
||||
<p i18n>
|
||||
If you need help using PeerTube, you can have a look at the <a class="link-primary" href="https://docs.joinpeertube.org/use/setup-account" target="_blank" rel="noopener noreferrer">documentation</a>.
|
||||
</p>
|
||||
</ng-container>
|
||||
}
|
||||
</my-alert>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { NgIf } from '@angular/common'
|
||||
import { Component, input } from '@angular/core'
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { SignupStepTitleComponent } from './signup-step-title.component'
|
||||
|
@ -7,7 +6,7 @@ import { SignupStepTitleComponent } from './signup-step-title.component'
|
|||
selector: 'my-signup-success-after-email',
|
||||
templateUrl: './signup-success-after-email.component.html',
|
||||
styleUrls: [ './signup-success.component.scss' ],
|
||||
imports: [ SignupStepTitleComponent, NgIf, AlertComponent ]
|
||||
imports: [ SignupStepTitleComponent, AlertComponent ]
|
||||
})
|
||||
export class SignupSuccessAfterEmailComponent {
|
||||
readonly requiresApproval = input<boolean>(undefined)
|
||||
|
|
|
@ -25,21 +25,11 @@ export class SignupService {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
verifyUserEmail (options: {
|
||||
userId: number
|
||||
verificationString: string
|
||||
isPendingEmail: boolean
|
||||
}) {
|
||||
const { userId, verificationString, isPendingEmail } = options
|
||||
askSendVerifyEmail (email: string) {
|
||||
const url = `${UserService.BASE_USERS_URL}registrations/ask-send-verify-email`
|
||||
|
||||
const url = `${UserService.BASE_USERS_URL}${userId}/verify-email`
|
||||
const body = {
|
||||
verificationString,
|
||||
isPendingEmail
|
||||
}
|
||||
|
||||
return this.authHttp.post(url, body)
|
||||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||
return this.authHttp.post(url, { email })
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
verifyRegistrationEmail (options: {
|
||||
|
@ -55,13 +45,6 @@ export class SignupService {
|
|||
.pipe(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(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) {
|
||||
|
|
|
@ -88,6 +88,8 @@ export class UserService {
|
|||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
changeEmail (password: string, newEmail: string) {
|
||||
const url = UserService.BASE_USERS_URL + 'me'
|
||||
const body: UserUpdateMe = {
|
||||
|
@ -99,6 +101,32 @@ export class UserService {
|
|||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
askSendVerifyEmail (email: string) {
|
||||
const url = UserService.BASE_USERS_URL + 'ask-send-verify-email'
|
||||
|
||||
return this.authHttp.post(url, { email })
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
verifyUserEmail (options: {
|
||||
userId: number
|
||||
verificationString: string
|
||||
isPendingEmail: boolean
|
||||
}) {
|
||||
const { userId, verificationString, isPendingEmail } = options
|
||||
|
||||
const url = `${UserService.BASE_USERS_URL}${userId}/verify-email`
|
||||
const body = {
|
||||
verificationString,
|
||||
isPendingEmail
|
||||
}
|
||||
|
||||
return this.authHttp.post(url, body)
|
||||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
updateMyProfile (profile: UserUpdateMe) {
|
||||
const url = UserService.BASE_USERS_URL + 'me'
|
||||
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
|
||||
input {
|
||||
@include peertube-input-text(auto);
|
||||
@include padding-left(15px !important);
|
||||
@include padding-right(15px !important);
|
||||
}
|
||||
|
||||
.btn,
|
||||
|
|
|
@ -5,13 +5,10 @@ block title
|
|||
|
||||
block content
|
||||
if isRegistrationRequest
|
||||
p You just requested an account on #[a(href=WEBSERVER.URL) #{instanceName}].
|
||||
else
|
||||
p You just created an account on #[a(href=WEBSERVER.URL) #{instanceName}].
|
||||
|
||||
if isRegistrationRequest
|
||||
p You requested an account on #[a(href=WEBSERVER.URL) #{instanceName}].
|
||||
p To complete your registration request you must verify your email first!
|
||||
else
|
||||
p You created an account on #[a(href=WEBSERVER.URL) #{instanceName}].
|
||||
p To start using your account you must verify your email first!
|
||||
|
||||
p Please follow #[a(href=verifyEmailUrl) this link] to verify this email belongs to you.
|
|
@ -0,0 +1,11 @@
|
|||
extends ../common/greetings
|
||||
|
||||
block title
|
||||
| Email verification
|
||||
|
||||
block content
|
||||
p You requested to change your 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 contact your administrator.
|
|
@ -1,11 +1,12 @@
|
|||
import express from 'express'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import express from 'express'
|
||||
import { CONFIG } from '../../../initializers/config.js'
|
||||
import { sendVerifyRegistrationEmail, sendVerifyUserEmail } from '../../../lib/user.js'
|
||||
import { sendVerifyRegistrationEmail, sendVerifyRegistrationRequestEmail, sendVerifyUserChangeEmail } from '../../../lib/user.js'
|
||||
import { asyncMiddleware, buildRateLimiter } from '../../../middlewares/index.js'
|
||||
import {
|
||||
registrationVerifyEmailValidator,
|
||||
usersAskSendVerifyEmailValidator,
|
||||
usersAskSendRegistrationVerifyEmailValidator,
|
||||
usersAskSendUserVerifyEmailValidator,
|
||||
usersVerifyEmailValidator
|
||||
} from '../../../middlewares/validators/index.js'
|
||||
|
||||
|
@ -16,18 +17,24 @@ const askSendEmailLimiter = buildRateLimiter({
|
|||
|
||||
const emailVerificationRouter = express.Router()
|
||||
|
||||
emailVerificationRouter.post([ '/ask-send-verify-email', '/registrations/ask-send-verify-email' ],
|
||||
emailVerificationRouter.post(
|
||||
'/ask-send-verify-email',
|
||||
askSendEmailLimiter,
|
||||
asyncMiddleware(usersAskSendVerifyEmailValidator),
|
||||
asyncMiddleware(reSendVerifyUserEmail)
|
||||
asyncMiddleware(usersAskSendUserVerifyEmailValidator),
|
||||
asyncMiddleware(reSendUserVerifyUserEmail)
|
||||
)
|
||||
|
||||
emailVerificationRouter.post('/:id/verify-email',
|
||||
asyncMiddleware(usersVerifyEmailValidator),
|
||||
asyncMiddleware(verifyUserEmail)
|
||||
emailVerificationRouter.post(
|
||||
'/registrations/ask-send-verify-email',
|
||||
askSendEmailLimiter,
|
||||
asyncMiddleware(usersAskSendRegistrationVerifyEmailValidator),
|
||||
asyncMiddleware(reSendRegistrationVerifyUserEmail)
|
||||
)
|
||||
|
||||
emailVerificationRouter.post('/registrations/:registrationId/verify-email',
|
||||
emailVerificationRouter.post('/:id/verify-email', asyncMiddleware(usersVerifyEmailValidator), asyncMiddleware(verifyUserEmail))
|
||||
|
||||
emailVerificationRouter.post(
|
||||
'/registrations/:registrationId/verify-email',
|
||||
asyncMiddleware(registrationVerifyEmailValidator),
|
||||
asyncMiddleware(verifyRegistrationEmail)
|
||||
)
|
||||
|
@ -38,14 +45,20 @@ export {
|
|||
emailVerificationRouter
|
||||
}
|
||||
|
||||
async function reSendVerifyUserEmail (req: express.Request, res: express.Response) {
|
||||
const user = res.locals.user
|
||||
const registration = res.locals.userRegistration
|
||||
async function reSendUserVerifyUserEmail (req: express.Request, res: express.Response) {
|
||||
if (res.locals.userPendingEmail) { // User wants to change its current email
|
||||
await sendVerifyUserChangeEmail(res.locals.userPendingEmail)
|
||||
} else { // After an account creation
|
||||
await sendVerifyRegistrationEmail(res.locals.userEmail)
|
||||
}
|
||||
|
||||
if (user) await sendVerifyUserEmail(user)
|
||||
else if (registration) await sendVerifyRegistrationEmail(registration)
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
async function reSendRegistrationVerifyUserEmail (req: express.Request, res: express.Response) {
|
||||
await sendVerifyRegistrationRequestEmail(res.locals.userRegistration)
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
async function verifyUserEmail (req: express.Request, res: express.Response) {
|
||||
|
@ -59,7 +72,7 @@ async function verifyUserEmail (req: express.Request, res: express.Response) {
|
|||
|
||||
await user.save()
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
async function verifyRegistrationEmail (req: express.Request, res: express.Response) {
|
||||
|
@ -68,5 +81,5 @@ async function verifyRegistrationEmail (req: express.Request, res: express.Respo
|
|||
|
||||
await registration.save()
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import { MIMETYPES } from '../../../initializers/constants.js'
|
|||
import { sequelizeTypescript } from '../../../initializers/database.js'
|
||||
import { sendUpdateActor } from '../../../lib/activitypub/send/index.js'
|
||||
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../../lib/local-actor.js'
|
||||
import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user.js'
|
||||
import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserChangeEmail } from '../../../lib/user.js'
|
||||
import {
|
||||
asyncMiddleware,
|
||||
asyncRetryTransactionMiddleware,
|
||||
|
@ -290,7 +290,7 @@ async function updateMe (req: express.Request, res: express.Response) {
|
|||
})
|
||||
|
||||
if (sendVerificationEmail === true) {
|
||||
await sendVerifyUserEmail(user, true)
|
||||
await sendVerifyUserChangeEmail(user)
|
||||
}
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
import express from 'express'
|
||||
import { Emailer } from '@server/lib/emailer.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { UserRegistrationModel } from '@server/models/user/user-registration.js'
|
||||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
|
@ -11,11 +7,20 @@ import {
|
|||
UserRegistrationUpdateState,
|
||||
UserRight
|
||||
} from '@peertube/peertube-models'
|
||||
import { Emailer } from '@server/lib/emailer.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { UserRegistrationModel } from '@server/models/user/user-registration.js'
|
||||
import express from 'express'
|
||||
import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { CONFIG } from '../../../initializers/config.js'
|
||||
import { Notifier } from '../../../lib/notifier/index.js'
|
||||
import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyRegistrationEmail, sendVerifyUserEmail } from '../../../lib/user.js'
|
||||
import {
|
||||
buildUser,
|
||||
createUserAccountAndChannelAndPlaylist,
|
||||
sendVerifyRegistrationEmail,
|
||||
sendVerifyRegistrationRequestEmail
|
||||
} from '../../../lib/user.js'
|
||||
import {
|
||||
acceptOrRejectRegistrationValidator,
|
||||
asyncMiddleware,
|
||||
|
@ -45,7 +50,8 @@ const registrationRateLimiter = buildRateLimiter({
|
|||
|
||||
const registrationsRouter = express.Router()
|
||||
|
||||
registrationsRouter.post('/registrations/request',
|
||||
registrationsRouter.post(
|
||||
'/registrations/request',
|
||||
registrationRateLimiter,
|
||||
asyncMiddleware(ensureUserRegistrationAllowedFactory('request-registration')),
|
||||
ensureUserRegistrationAllowedForIP,
|
||||
|
@ -53,27 +59,31 @@ registrationsRouter.post('/registrations/request',
|
|||
asyncRetryTransactionMiddleware(requestRegistration)
|
||||
)
|
||||
|
||||
registrationsRouter.post('/registrations/:registrationId/accept',
|
||||
registrationsRouter.post(
|
||||
'/registrations/:registrationId/accept',
|
||||
authenticate,
|
||||
ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS),
|
||||
asyncMiddleware(acceptOrRejectRegistrationValidator),
|
||||
asyncRetryTransactionMiddleware(acceptRegistration)
|
||||
)
|
||||
registrationsRouter.post('/registrations/:registrationId/reject',
|
||||
registrationsRouter.post(
|
||||
'/registrations/:registrationId/reject',
|
||||
authenticate,
|
||||
ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS),
|
||||
asyncMiddleware(acceptOrRejectRegistrationValidator),
|
||||
asyncRetryTransactionMiddleware(rejectRegistration)
|
||||
)
|
||||
|
||||
registrationsRouter.delete('/registrations/:registrationId',
|
||||
registrationsRouter.delete(
|
||||
'/registrations/:registrationId',
|
||||
authenticate,
|
||||
ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS),
|
||||
asyncMiddleware(getRegistrationValidator),
|
||||
asyncRetryTransactionMiddleware(deleteRegistration)
|
||||
)
|
||||
|
||||
registrationsRouter.get('/registrations',
|
||||
registrationsRouter.get(
|
||||
'/registrations',
|
||||
authenticate,
|
||||
ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS),
|
||||
paginationValidator,
|
||||
|
@ -84,7 +94,8 @@ registrationsRouter.get('/registrations',
|
|||
asyncMiddleware(listRegistrations)
|
||||
)
|
||||
|
||||
registrationsRouter.post('/register',
|
||||
registrationsRouter.post(
|
||||
'/register',
|
||||
registrationRateLimiter,
|
||||
asyncMiddleware(ensureUserRegistrationAllowedFactory('direct-registration')),
|
||||
ensureUserRegistrationAllowedForIP,
|
||||
|
@ -118,7 +129,7 @@ async function requestRegistration (req: express.Request, res: express.Response)
|
|||
await registration.save()
|
||||
|
||||
if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
|
||||
await sendVerifyRegistrationEmail(registration)
|
||||
await sendVerifyRegistrationRequestEmail(registration)
|
||||
}
|
||||
|
||||
Notifier.Instance.notifyOnNewRegistrationRequest(registration)
|
||||
|
@ -242,7 +253,7 @@ async function registerUser (req: express.Request, res: express.Response) {
|
|||
logger.info('User %s with its channel and account registered.', body.username)
|
||||
|
||||
if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
|
||||
await sendVerifyUserEmail(user)
|
||||
await sendVerifyRegistrationEmail(user)
|
||||
}
|
||||
|
||||
Notifier.Instance.notifyOnNewDirectRegistration(user)
|
||||
|
|
|
@ -14,7 +14,7 @@ import { OAuthClientModel } from '../../models/oauth/oauth-client.js'
|
|||
import { OAuthTokenModel } from '../../models/oauth/oauth-token.js'
|
||||
import { UserModel } from '../../models/user/user.js'
|
||||
import { findAvailableLocalActorName } from '../local-actor.js'
|
||||
import { buildUser, createUserAccountAndChannelAndPlaylist, getUserByEmailPermissive } from '../user.js'
|
||||
import { buildUser, createUserAccountAndChannelAndPlaylist, getByEmailPermissive } from '../user.js'
|
||||
import { ExternalUser } from './external-auth.js'
|
||||
import { TokensCache } from './tokens-cache.js'
|
||||
|
||||
|
@ -87,7 +87,7 @@ async function getUser (usernameOrEmail?: string, password?: string, bypassLogin
|
|||
if (bypassLogin && bypassLogin.bypass === true) {
|
||||
logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName)
|
||||
|
||||
let user = getUserByEmailPermissive(await UserModel.loadByEmailCaseInsensitive(bypassLogin.user.email), bypassLogin.user.email)
|
||||
let user = getByEmailPermissive(await UserModel.loadByEmailCaseInsensitive(bypassLogin.user.email), bypassLogin.user.email)
|
||||
|
||||
if (!user) {
|
||||
user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user)
|
||||
|
@ -105,7 +105,9 @@ async function getUser (usernameOrEmail?: string, password?: string, bypassLogin
|
|||
if (user.pluginAuth !== bypassLogin.pluginName) {
|
||||
logger.info(
|
||||
'Cannot bypass oauth login by plugin %s because %s has another plugin auth method (%s).',
|
||||
bypassLogin.pluginName, bypassLogin.user.email, user.pluginAuth
|
||||
bypassLogin.pluginName,
|
||||
bypassLogin.user.email,
|
||||
user.pluginAuth
|
||||
)
|
||||
|
||||
return null
|
||||
|
@ -123,7 +125,7 @@ async function getUser (usernameOrEmail?: string, password?: string, bypassLogin
|
|||
let user: MUserDefault
|
||||
|
||||
if (usernameOrEmail.includes('@')) {
|
||||
user = getUserByEmailPermissive(users, usernameOrEmail)
|
||||
user = getByEmailPermissive(users, usernameOrEmail)
|
||||
} else if (users.length === 1) {
|
||||
user = users[0]
|
||||
}
|
||||
|
@ -261,7 +263,7 @@ async function updateUserFromExternal (
|
|||
|
||||
{
|
||||
type UserAttributeKeys = keyof AttributesOnly<UserModel>
|
||||
const mappingKeys: { [ id in UserAttributeKeys ]?: AuthenticatedResultUpdaterFieldName } = {
|
||||
const mappingKeys: { [id in UserAttributeKeys]?: AuthenticatedResultUpdaterFieldName } = {
|
||||
role: 'role',
|
||||
adminFlags: 'adminFlags',
|
||||
videoQuota: 'videoQuota',
|
||||
|
@ -278,7 +280,7 @@ async function updateUserFromExternal (
|
|||
|
||||
{
|
||||
type AccountAttributeKeys = keyof Partial<AttributesOnly<AccountModel>>
|
||||
const mappingKeys: { [ id in AccountAttributeKeys ]?: AuthenticatedResultUpdaterFieldName } = {
|
||||
const mappingKeys: { [id in AccountAttributeKeys]?: AuthenticatedResultUpdaterFieldName } = {
|
||||
name: 'displayName'
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ import { JobQueue } from './job-queue/index.js'
|
|||
import { Hooks } from './plugins/hooks.js'
|
||||
|
||||
class Emailer {
|
||||
|
||||
private static instance: Emailer
|
||||
private initialized = false
|
||||
private transporter: Transporter
|
||||
|
@ -89,7 +88,29 @@ class Emailer {
|
|||
return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
|
||||
}
|
||||
|
||||
addVerifyEmailJob (options: {
|
||||
addUserVerifyChangeEmailJob (options: {
|
||||
username: string
|
||||
to: string
|
||||
verifyEmailUrl: string
|
||||
}) {
|
||||
const { username, to, verifyEmailUrl } = options
|
||||
|
||||
const emailPayload: EmailPayload = {
|
||||
template: 'verify-user-change-email',
|
||||
to: [ to ],
|
||||
subject: `Verify your email on ${CONFIG.INSTANCE.NAME}`,
|
||||
locals: {
|
||||
username,
|
||||
verifyEmailUrl,
|
||||
|
||||
hideNotificationPreferencesLink: true
|
||||
}
|
||||
}
|
||||
|
||||
return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
|
||||
}
|
||||
|
||||
addRegistrationVerifyEmailJob (options: {
|
||||
username: string
|
||||
isRegistrationRequest: boolean
|
||||
to: string
|
||||
|
@ -98,7 +119,7 @@ class Emailer {
|
|||
const { username, isRegistrationRequest, to, verifyEmailUrl } = options
|
||||
|
||||
const emailPayload: EmailPayload = {
|
||||
template: 'verify-email',
|
||||
template: 'verify-registration-email',
|
||||
to: [ to ],
|
||||
subject: `Verify your email on ${CONFIG.INSTANCE.NAME}`,
|
||||
locals: {
|
||||
|
@ -337,7 +358,7 @@ class Emailer {
|
|||
private initSMTPTransport () {
|
||||
logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT)
|
||||
|
||||
let tls: { ca: [ Buffer ] }
|
||||
let tls: { ca: [Buffer] }
|
||||
if (CONFIG.SMTP.CA_FILE) {
|
||||
tls = {
|
||||
ca: [ readFileSync(CONFIG.SMTP.CA_FILE) ]
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Transaction } from 'sequelize'
|
||||
import {
|
||||
ActivityPubActorType,
|
||||
UserAdminFlag,
|
||||
|
@ -12,6 +11,7 @@ import { logger } from '@server/helpers/logger.js'
|
|||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { UserModel } from '@server/models/user/user.js'
|
||||
import { MActorDefault } from '@server/types/models/actor/index.js'
|
||||
import { Transaction } from 'sequelize'
|
||||
import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants.js'
|
||||
import { sequelizeTypescript } from '../initializers/database.js'
|
||||
import { AccountModel } from '../models/account/account.js'
|
||||
|
@ -29,7 +29,7 @@ import { createWatchLaterPlaylist } from './video-playlist.js'
|
|||
|
||||
type ChannelNames = { name: string, displayName: string }
|
||||
|
||||
function buildUser (options: {
|
||||
export function buildUser (options: {
|
||||
username: string
|
||||
password: string
|
||||
email: string
|
||||
|
@ -80,7 +80,7 @@ function buildUser (options: {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function createUserAccountAndChannelAndPlaylist (parameters: {
|
||||
export async function createUserAccountAndChannelAndPlaylist (parameters: {
|
||||
userToCreate: MUser
|
||||
userDisplayName?: string
|
||||
channelNames?: ChannelNames
|
||||
|
@ -125,7 +125,7 @@ async function createUserAccountAndChannelAndPlaylist (parameters: {
|
|||
return { user, account, videoChannel }
|
||||
}
|
||||
|
||||
async function createLocalAccountWithoutKeys (parameters: {
|
||||
export async function createLocalAccountWithoutKeys (parameters: {
|
||||
name: string
|
||||
displayName?: string
|
||||
userId: number | null
|
||||
|
@ -152,7 +152,7 @@ async function createLocalAccountWithoutKeys (parameters: {
|
|||
return accountInstanceCreated
|
||||
}
|
||||
|
||||
async function createApplicationActor (applicationId: number) {
|
||||
export async function createApplicationActor (applicationId: number) {
|
||||
const accountCreated = await createLocalAccountWithoutKeys({
|
||||
name: SERVER_ACTOR_NAME,
|
||||
userId: null,
|
||||
|
@ -168,47 +168,64 @@ async function createApplicationActor (applicationId: number) {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) {
|
||||
export async function buildUserVerifyEmail (user: MUser, isPendingEmail: boolean) {
|
||||
const verificationString = await Redis.Instance.setUserVerifyEmailVerificationString(user.id)
|
||||
let verifyEmailUrl = `${WEBSERVER.URL}/verify-account/email?userId=${user.id}&verificationString=${verificationString}`
|
||||
|
||||
if (isPendingEmail) verifyEmailUrl += '&isPendingEmail=true'
|
||||
const verifyEmailUrl = `${WEBSERVER.URL}/verify-account/email?userId=${user.id}&verificationString=${verificationString}`
|
||||
|
||||
const to = isPendingEmail
|
||||
? user.pendingEmail
|
||||
: user.email
|
||||
if (isPendingEmail) return verifyEmailUrl + '&isPendingEmail=true'
|
||||
|
||||
const username = user.username
|
||||
|
||||
Emailer.Instance.addVerifyEmailJob({ username, to, verifyEmailUrl, isRegistrationRequest: false })
|
||||
return verifyEmailUrl
|
||||
}
|
||||
|
||||
async function sendVerifyRegistrationEmail (registration: MRegistration) {
|
||||
export async function buildRegistrationRequestVerifyEmail (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
|
||||
return `${WEBSERVER.URL}/verify-account/email?registrationId=${registration.id}&verificationString=${verificationString}`
|
||||
}
|
||||
|
||||
Emailer.Instance.addVerifyEmailJob({ username, to, verifyEmailUrl, isRegistrationRequest: true })
|
||||
export async function sendVerifyUserChangeEmail (user: MUser) {
|
||||
Emailer.Instance.addUserVerifyChangeEmailJob({
|
||||
username: user.username,
|
||||
to: user.pendingEmail,
|
||||
verifyEmailUrl: await buildUserVerifyEmail(user, true)
|
||||
})
|
||||
}
|
||||
|
||||
export async function sendVerifyRegistrationRequestEmail (registration: MRegistration) {
|
||||
Emailer.Instance.addRegistrationVerifyEmailJob({
|
||||
username: registration.username,
|
||||
to: registration.email,
|
||||
verifyEmailUrl: await buildRegistrationRequestVerifyEmail(registration),
|
||||
isRegistrationRequest: true
|
||||
})
|
||||
}
|
||||
|
||||
export async function sendVerifyRegistrationEmail (user: MUser) {
|
||||
Emailer.Instance.addRegistrationVerifyEmailJob({
|
||||
username: user.username,
|
||||
to: user.email,
|
||||
verifyEmailUrl: await buildUserVerifyEmail(user, false),
|
||||
isRegistrationRequest: true
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getOriginalVideoFileTotalFromUser (user: MUserId) {
|
||||
export async function getOriginalVideoFileTotalFromUser (user: MUserId) {
|
||||
const base = await UserModel.getUserQuota({ userId: user.id, daily: false })
|
||||
|
||||
return base + LiveQuotaStore.Instance.getLiveQuotaOfUser(user.id)
|
||||
}
|
||||
|
||||
// Returns cumulative size of all video files uploaded in the last 24 hours.
|
||||
async function getOriginalVideoFileTotalDailyFromUser (user: MUserId) {
|
||||
export async function getOriginalVideoFileTotalDailyFromUser (user: MUserId) {
|
||||
const base = await UserModel.getUserQuota({ userId: user.id, daily: true })
|
||||
|
||||
return base + LiveQuotaStore.Instance.getLiveQuotaOfUser(user.id)
|
||||
}
|
||||
|
||||
async function isUserQuotaValid (options: {
|
||||
export async function isUserQuotaValid (options: {
|
||||
userId: number
|
||||
uploadSize: number
|
||||
checkDaily?: boolean // default true
|
||||
|
@ -227,7 +244,8 @@ async function isUserQuotaValid (options: {
|
|||
const uploadedDaily = uploadSize + totalBytesDaily
|
||||
|
||||
logger.debug(
|
||||
'Check user %d quota to upload content.', userId,
|
||||
'Check user %d quota to upload content.',
|
||||
userId,
|
||||
{ totalBytes, totalBytesDaily, videoQuota: user.videoQuota, videoQuotaDaily: user.videoQuotaDaily, uploadSize }
|
||||
)
|
||||
|
||||
|
@ -237,29 +255,14 @@ async function isUserQuotaValid (options: {
|
|||
return true
|
||||
}
|
||||
|
||||
function getUserByEmailPermissive <T extends { email: string }> (users: T[], email: string): T {
|
||||
export function getByEmailPermissive<T extends { email: string }> (users: T[], email: string, field: keyof T = 'email'): T {
|
||||
if (users.length === 1) return users[0]
|
||||
|
||||
return users.find(r => r.email === email)
|
||||
return users.find(r => r[field] === email)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
getOriginalVideoFileTotalFromUser,
|
||||
getOriginalVideoFileTotalDailyFromUser,
|
||||
createApplicationActor,
|
||||
createUserAccountAndChannelAndPlaylist,
|
||||
createLocalAccountWithoutKeys,
|
||||
|
||||
sendVerifyUserEmail,
|
||||
sendVerifyRegistrationEmail,
|
||||
|
||||
isUserQuotaValid,
|
||||
buildUser,
|
||||
getUserByEmailPermissive
|
||||
}
|
||||
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createDefaultUserNotificationSettings (user: MUserId, t: Transaction | undefined) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserRightType } from '@peertube/peertube-models'
|
||||
import { getUserByEmailPermissive } from '@server/lib/user.js'
|
||||
import { getByEmailPermissive } from '@server/lib/user.js'
|
||||
import { ActorModel } from '@server/models/actor/actor.js'
|
||||
import { UserModel } from '@server/models/user/user.js'
|
||||
import { MAccountId, MUserAccountId, MUserDefault } from '@server/types/models/index.js'
|
||||
|
@ -16,7 +16,19 @@ export function checkUserEmailExistPermissive (email: string, res: express.Respo
|
|||
async () => {
|
||||
const users = await UserModel.loadByEmailCaseInsensitive(email)
|
||||
|
||||
return getUserByEmailPermissive(users, email)
|
||||
return getByEmailPermissive(users, email)
|
||||
},
|
||||
res,
|
||||
abortResponse
|
||||
)
|
||||
}
|
||||
|
||||
export function checkUserPendingEmailExistPermissive (email: string, res: express.Response, abortResponse = true) {
|
||||
return checkUserExist(
|
||||
async () => {
|
||||
const users = await UserModel.loadByPendingEmailCaseInsensitive(email)
|
||||
|
||||
return getByEmailPermissive(users, email)
|
||||
},
|
||||
res,
|
||||
abortResponse
|
||||
|
|
|
@ -3,22 +3,26 @@ import { UserRegistrationModel } from '@server/models/user/user-registration.js'
|
|||
import { MRegistration } from '@server/types/models/index.js'
|
||||
import { forceNumber, pick } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { getUserByEmailPermissive } from '@server/lib/user.js'
|
||||
import { getByEmailPermissive } from '@server/lib/user.js'
|
||||
|
||||
function checkRegistrationIdExist (idArg: number | string, res: express.Response) {
|
||||
export function checkRegistrationIdExist (idArg: number | string, res: express.Response) {
|
||||
const id = forceNumber(idArg)
|
||||
return checkRegistrationExist(() => UserRegistrationModel.load(id), res)
|
||||
}
|
||||
|
||||
function checkRegistrationEmailExistPermissive (email: string, res: express.Response, abortResponse = true) {
|
||||
return checkRegistrationExist(async () => {
|
||||
export function checkRegistrationEmailExistPermissive (email: string, res: express.Response, abortResponse = true) {
|
||||
return checkRegistrationExist(
|
||||
async () => {
|
||||
const registrations = await UserRegistrationModel.listByEmailCaseInsensitive(email)
|
||||
|
||||
return getUserByEmailPermissive(registrations, email)
|
||||
}, res, abortResponse)
|
||||
return getByEmailPermissive(registrations, email)
|
||||
},
|
||||
res,
|
||||
abortResponse
|
||||
)
|
||||
}
|
||||
|
||||
async function checkRegistrationHandlesDoNotAlreadyExist (options: {
|
||||
export async function checkRegistrationHandlesDoNotAlreadyExist (options: {
|
||||
username: string
|
||||
channelHandle: string
|
||||
email: string
|
||||
|
@ -41,7 +45,7 @@ async function checkRegistrationHandlesDoNotAlreadyExist (options: {
|
|||
return true
|
||||
}
|
||||
|
||||
async function checkRegistrationExist (finder: () => Promise<MRegistration>, res: express.Response, abortResponse = true) {
|
||||
export async function checkRegistrationExist (finder: () => Promise<MRegistration>, res: express.Response, abortResponse = true) {
|
||||
const registration = await finder()
|
||||
|
||||
if (!registration) {
|
||||
|
@ -58,10 +62,3 @@ async function checkRegistrationExist (finder: () => Promise<MRegistration>, res
|
|||
res.locals.userRegistration = registration
|
||||
return true
|
||||
}
|
||||
|
||||
export {
|
||||
checkRegistrationIdExist,
|
||||
checkRegistrationEmailExistPermissive,
|
||||
checkRegistrationHandlesDoNotAlreadyExist,
|
||||
checkRegistrationExist
|
||||
}
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { getByEmailPermissive } from '@server/lib/user.js'
|
||||
import { UserModel } from '@server/models/user/user.js'
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { Redis } from '../../../lib/redis.js'
|
||||
import { areValidationErrors, checkUserEmailExistPermissive, checkUserIdExist } from '../shared/index.js'
|
||||
import { areValidationErrors, checkUserIdExist } from '../shared/index.js'
|
||||
import { checkRegistrationEmailExistPermissive, checkRegistrationIdExist } from './shared/user-registrations.js'
|
||||
|
||||
export const usersAskSendVerifyEmailValidator = [
|
||||
export const usersAskSendUserVerifyEmailValidator = [
|
||||
body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
|
@ -18,18 +20,31 @@ export const usersAskSendVerifyEmailValidator = [
|
|||
email: req.body.email
|
||||
}, 'filter:api.email-verification.ask-send-verify-email.body')
|
||||
|
||||
const [ userExists, registrationExists ] = await Promise.all([
|
||||
checkUserEmailExistPermissive(email, res, false),
|
||||
checkRegistrationEmailExistPermissive(email, res, false)
|
||||
const [ userEmail, userPendingEmail ] = await Promise.all([
|
||||
UserModel.loadByEmailCaseInsensitive(email).then(users => getByEmailPermissive(users, email)),
|
||||
UserModel.loadByPendingEmailCaseInsensitive(email).then(users => getByEmailPermissive(users, email))
|
||||
])
|
||||
|
||||
if (!userExists && !registrationExists) {
|
||||
logger.debug('User or registration with email %s does not exist (asking verify email).', email)
|
||||
if (userEmail && userPendingEmail) {
|
||||
logger.error(`Found 2 users with email ${email} to send verification link.`)
|
||||
|
||||
// Do not leak our emails
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
if (res.locals.user?.pluginAuth) {
|
||||
if (!userEmail && !userPendingEmail) {
|
||||
logger.debug(`User with email ${email} does not exist (asking verify email).`)
|
||||
|
||||
// Do not leak our emails
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
res.locals.userEmail = userEmail
|
||||
res.locals.userPendingEmail = userPendingEmail
|
||||
|
||||
const user = userEmail || userPendingEmail
|
||||
|
||||
if (user.pluginAuth) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Cannot ask verification email of a user that uses a plugin authentication.'
|
||||
|
@ -40,6 +55,29 @@ export const usersAskSendVerifyEmailValidator = [
|
|||
}
|
||||
]
|
||||
|
||||
export const usersAskSendRegistrationVerifyEmailValidator = [
|
||||
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 { email } = await Hooks.wrapObject({
|
||||
email: req.body.email
|
||||
}, 'filter:api.email-verification.ask-send-verify-email.body')
|
||||
|
||||
const registrationExists = await checkRegistrationEmailExistPermissive(email, res, false)
|
||||
|
||||
if (!registrationExists) {
|
||||
logger.debug(`Registration with email ${email} does not exist (asking verify email).`)
|
||||
|
||||
// Do not leak our emails
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersVerifyEmailValidator = [
|
||||
param('id')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid id'),
|
||||
|
|
|
@ -37,7 +37,8 @@ import {
|
|||
HasOne,
|
||||
Is,
|
||||
IsEmail,
|
||||
IsUUID, Scopes,
|
||||
IsUUID,
|
||||
Scopes,
|
||||
Table,
|
||||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
|
@ -285,7 +286,6 @@ type WhereUserIdScopeOptions = { whereUserId?: '$userId' | '"UserModel"."id"' }
|
|||
]
|
||||
})
|
||||
export class UserModel extends SequelizeModel<UserModel> {
|
||||
|
||||
@AllowNull(true)
|
||||
@Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true))
|
||||
@Column
|
||||
|
@ -675,6 +675,18 @@ export class UserModel extends SequelizeModel<UserModel> {
|
|||
return UserModel.findAll(query)
|
||||
}
|
||||
|
||||
static loadByPendingEmailCaseInsensitive (pendingEmail: string): Promise<MUserDefault[]> {
|
||||
const query = {
|
||||
where: where(
|
||||
fn('LOWER', col('pendingEmail')),
|
||||
'=',
|
||||
pendingEmail.toLowerCase()
|
||||
)
|
||||
}
|
||||
|
||||
return UserModel.findAll(query)
|
||||
}
|
||||
|
||||
static loadByUsernameOrEmailCaseInsensitive (usernameOrEmail: string): Promise<MUserDefault[]> {
|
||||
const query = {
|
||||
where: {
|
||||
|
|
3
server/core/types/express.d.ts
vendored
3
server/core/types/express.d.ts
vendored
|
@ -197,6 +197,9 @@ declare module 'express' {
|
|||
|
||||
user?: MUserDefault
|
||||
userRegistration?: MRegistration
|
||||
// For verification links
|
||||
userEmail?: MUserDefault
|
||||
userPendingEmail?: MUserDefault
|
||||
|
||||
server?: MServer
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue