diff --git a/client/src/app/+accounts/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts index 16a1c3e33..f13646451 100644 --- a/client/src/app/+accounts/account-videos/account-videos.component.ts +++ b/client/src/app/+accounts/account-videos/account-videos.component.ts @@ -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({ diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html index 47c9281a3..28f70e72d 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html +++ b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html @@ -1,8 +1,16 @@ {{ error }} {{ success }} -
- {{ user.pendingEmail }} is awaiting email verification +
+
+ {{ user.pendingEmail }} is awaiting email verification. +
+ + @if (verificationEmailSent) { +
Email verification sent!
+ } @else { + + }
diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts index 956c910c2..22e4e1292 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts @@ -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 + } + }) + } } diff --git a/client/src/app/+signup/+verify-account/routes.ts b/client/src/app/+signup/+verify-account/routes.ts index 21889a533..de75ffebe 100644 --- a/client/src/app/+signup/+verify-account/routes.ts +++ b/client/src/app/+signup/+verify-account/routes.ts @@ -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` diff --git a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html deleted file mode 100644 index c4d8688d2..000000000 --- a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html +++ /dev/null @@ -1,22 +0,0 @@ -
-

Request email for account verification

- - -
- - - - - -
- - - - - -
This instance does not require email verification.
-
-
diff --git a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts deleted file mode 100644 index 127f61284..000000000 --- a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts +++ /dev/null @@ -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) - }) - } -} diff --git a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html index ec8d79291..683809272 100644 --- a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html +++ b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html @@ -1,19 +1,20 @@

Verify email

- - + @if (success) { + @if (this.isRegistration() || this.isRegistrationRequest()) { + + } @else { + Email updated. + } - Email updated. + } @else if (failed) { + + An error occurred. - - An error occurred. - - - Request a new verification email - - + + Request a new verification email + + + }
diff --git a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts index feb8db713..b055b8c86 100644 --- a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts +++ b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts @@ -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()) { diff --git a/client/src/app/+signup/+verify-account/verify-new-account-ask-send-email/verify-new-account-ask-send-email.component.html b/client/src/app/+signup/+verify-account/verify-new-account-ask-send-email/verify-new-account-ask-send-email.component.html new file mode 100644 index 000000000..aaae8d55a --- /dev/null +++ b/client/src/app/+signup/+verify-account/verify-new-account-ask-send-email/verify-new-account-ask-send-email.component.html @@ -0,0 +1,22 @@ +
+

Request email for account verification

+ + @if (requiresEmailVerification) { +
+
+ + + + + +
+ + +
+ } @else { +
{{ instanceName }} does not require email verification.
+ } +
diff --git a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss b/client/src/app/+signup/+verify-account/verify-new-account-ask-send-email/verify-new-account-ask-send-email.component.scss similarity index 100% rename from client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss rename to client/src/app/+signup/+verify-account/verify-new-account-ask-send-email/verify-new-account-ask-send-email.component.scss diff --git a/client/src/app/+signup/+verify-account/verify-new-account-ask-send-email/verify-new-account-ask-send-email.component.ts b/client/src/app/+signup/+verify-account/verify-new-account-ask-send-email/verify-new-account-ask-send-email.component.ts new file mode 100644 index 000000000..7c6843b74 --- /dev/null +++ b/client/src/app/+signup/+verify-account/verify-new-account-ask-send-email/verify-new-account-ask-send-email.component.ts @@ -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) + }) + } +} diff --git a/client/src/app/+signup/shared/signup-success-after-email.component.html b/client/src/app/+signup/shared/signup-success-after-email.component.html index ef6010740..a00f0ffc2 100644 --- a/client/src/app/+signup/shared/signup-success-after-email.component.html +++ b/client/src/app/+signup/shared/signup-success-after-email.component.html @@ -3,19 +3,17 @@ - + @if (requiresApproval()) {

Your email has been verified and your account request has been sent!

A moderator will check your registration request soon and you'll receive an email when it is accepted or rejected.

-
- - + } @else {

Your email has been verified and your account has been created!

If you need help using PeerTube, you can have a look at the documentation.

-
+ }
diff --git a/client/src/app/+signup/shared/signup-success-after-email.component.ts b/client/src/app/+signup/shared/signup-success-after-email.component.ts index 2f82a51dd..d94a2f13c 100644 --- a/client/src/app/+signup/shared/signup-success-after-email.component.ts +++ b/client/src/app/+signup/shared/signup-success-after-email.component.ts @@ -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(undefined) diff --git a/client/src/app/+signup/shared/signup.service.ts b/client/src/app/+signup/shared/signup.service.ts index 48b01fee9..fab503776 100644 --- a/client/src/app/+signup/shared/signup.service.ts +++ b/client/src/app/+signup/shared/signup.service.ts @@ -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) { diff --git a/client/src/app/core/users/user.service.ts b/client/src/app/core/users/user.service.ts index 39caca40a..81cd37c03 100644 --- a/client/src/app/core/users/user.service.ts +++ b/client/src/app/core/users/user.service.ts @@ -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' diff --git a/client/src/app/shared/shared-forms/input-text.component.scss b/client/src/app/shared/shared-forms/input-text.component.scss index e22077a05..ab0bccf2c 100644 --- a/client/src/app/shared/shared-forms/input-text.component.scss +++ b/client/src/app/shared/shared-forms/input-text.component.scss @@ -5,8 +5,6 @@ input { @include peertube-input-text(auto); - @include padding-left(15px !important); - @include padding-right(15px !important); } .btn, diff --git a/server/core/assets/email-templates/verify-email/html.pug b/server/core/assets/email-templates/verify-registration-email/html.pug similarity index 74% rename from server/core/assets/email-templates/verify-email/html.pug rename to server/core/assets/email-templates/verify-registration-email/html.pug index 19ef65f75..c5c2fa91f 100644 --- a/server/core/assets/email-templates/verify-email/html.pug +++ b/server/core/assets/email-templates/verify-registration-email/html.pug @@ -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. diff --git a/server/core/assets/email-templates/verify-user-change-email/html.pug b/server/core/assets/email-templates/verify-user-change-email/html.pug new file mode 100644 index 000000000..c5b18662d --- /dev/null +++ b/server/core/assets/email-templates/verify-user-change-email/html.pug @@ -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. diff --git a/server/core/controllers/api/users/email-verification.ts b/server/core/controllers/api/users/email-verification.ts index ef9dc30b6..c9856a661 100644 --- a/server/core/controllers/api/users/email-verification.ts +++ b/server/core/controllers/api/users/email-verification.ts @@ -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) } diff --git a/server/core/controllers/api/users/me.ts b/server/core/controllers/api/users/me.ts index c8f58ff88..5f85915bd 100644 --- a/server/core/controllers/api/users/me.ts +++ b/server/core/controllers/api/users/me.ts @@ -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() diff --git a/server/core/controllers/api/users/registrations.ts b/server/core/controllers/api/users/registrations.ts index e38b215a7..a3ef130ab 100644 --- a/server/core/controllers/api/users/registrations.ts +++ b/server/core/controllers/api/users/registrations.ts @@ -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) diff --git a/server/core/lib/auth/oauth-model.ts b/server/core/lib/auth/oauth-model.ts index a1236764c..cbb2718ee 100644 --- a/server/core/lib/auth/oauth-model.ts +++ b/server/core/lib/auth/oauth-model.ts @@ -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] } @@ -166,7 +168,7 @@ async function revokeToken ( TokensCache.Instance.clearCacheByToken(token.accessToken) token.destroy() - .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) + .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) return { success: true, redirectUrl } } @@ -261,7 +263,7 @@ async function updateUserFromExternal ( { type UserAttributeKeys = keyof AttributesOnly - 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> - const mappingKeys: { [ id in AccountAttributeKeys ]?: AuthenticatedResultUpdaterFieldName } = { + const mappingKeys: { [id in AccountAttributeKeys]?: AuthenticatedResultUpdaterFieldName } = { name: 'displayName' } diff --git a/server/core/lib/emailer.ts b/server/core/lib/emailer.ts index 0c208bd20..de1424a74 100644 --- a/server/core/lib/emailer.ts +++ b/server/core/lib/emailer.ts @@ -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) ] diff --git a/server/core/lib/user.ts b/server/core/lib/user.ts index 79762b196..cb03b69de 100644 --- a/server/core/lib/user.ts +++ b/server/core/lib/user.ts @@ -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 (users: T[], email: string): T { +export function getByEmailPermissive (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) { diff --git a/server/core/middlewares/validators/shared/users.ts b/server/core/middlewares/validators/shared/users.ts index 98e65881f..54a156230 100644 --- a/server/core/middlewares/validators/shared/users.ts +++ b/server/core/middlewares/validators/shared/users.ts @@ -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 diff --git a/server/core/middlewares/validators/users/shared/user-registrations.ts b/server/core/middlewares/validators/users/shared/user-registrations.ts index 5d9284ece..03a493b3c 100644 --- a/server/core/middlewares/validators/users/shared/user-registrations.ts +++ b/server/core/middlewares/validators/users/shared/user-registrations.ts @@ -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 () => { - const registrations = await UserRegistrationModel.listByEmailCaseInsensitive(email) +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, res: express.Response, abortResponse = true) { +export async function checkRegistrationExist (finder: () => Promise, res: express.Response, abortResponse = true) { const registration = await finder() if (!registration) { @@ -58,10 +62,3 @@ async function checkRegistrationExist (finder: () => Promise, res res.locals.userRegistration = registration return true } - -export { - checkRegistrationIdExist, - checkRegistrationEmailExistPermissive, - checkRegistrationHandlesDoNotAlreadyExist, - checkRegistrationExist -} diff --git a/server/core/middlewares/validators/users/user-email-verification.ts b/server/core/middlewares/validators/users/user-email-verification.ts index 1c4c10b2e..a19928bb6 100644 --- a/server/core/middlewares/validators/users/user-email-verification.ts +++ b/server/core/middlewares/validators/users/user-email-verification.ts @@ -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'), diff --git a/server/core/models/user/user.ts b/server/core/models/user/user.ts index 40e668662..77937932f 100644 --- a/server/core/models/user/user.ts +++ b/server/core/models/user/user.ts @@ -37,7 +37,8 @@ import { HasOne, Is, IsEmail, - IsUUID, Scopes, + IsUUID, + Scopes, Table, UpdatedAt } from 'sequelize-typescript' @@ -173,7 +174,7 @@ type WhereUserIdScopeOptions = { whereUserId?: '$userId' | '"UserModel"."id"' } daily: false, onlyMaxResolution: true }) + - ')' + ')' ), 'videoQuotaUsed' ], @@ -185,7 +186,7 @@ type WhereUserIdScopeOptions = { whereUserId?: '$userId' | '"UserModel"."id"' } daily: true, onlyMaxResolution: true }) + - ')' + ')' ), 'videoQuotaUsedDaily' ] @@ -205,7 +206,7 @@ type WhereUserIdScopeOptions = { whereUserId?: '$userId' | '"UserModel"."id"' } daily: false, onlyMaxResolution: false }) + - ')' + ')' ), 'totalVideoFileSize' ] @@ -225,7 +226,7 @@ type WhereUserIdScopeOptions = { whereUserId?: '$userId' | '"UserModel"."id"' } 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + `WHERE "account"."userId" = ${options.whereUserId}` + - ')' + ')' ), 'videosCount' ], @@ -234,13 +235,13 @@ type WhereUserIdScopeOptions = { whereUserId?: '$userId' | '"UserModel"."id"' } '(' + `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` + 'FROM (' + - 'SELECT COUNT("abuse"."id") AS "abuses", ' + - `COUNT("abuse"."id") FILTER (WHERE "abuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` + - 'FROM "abuse" ' + - 'INNER JOIN "account" ON "account"."id" = "abuse"."flaggedAccountId" ' + - `WHERE "account"."userId" = ${options.whereUserId}` + + 'SELECT COUNT("abuse"."id") AS "abuses", ' + + `COUNT("abuse"."id") FILTER (WHERE "abuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` + + 'FROM "abuse" ' + + 'INNER JOIN "account" ON "account"."id" = "abuse"."flaggedAccountId" ' + + `WHERE "account"."userId" = ${options.whereUserId}` + ') t' + - ')' + ')' ), 'abusesCount' ], @@ -251,7 +252,7 @@ type WhereUserIdScopeOptions = { whereUserId?: '$userId' | '"UserModel"."id"' } 'FROM "abuse" ' + 'INNER JOIN "account" ON "account"."id" = "abuse"."reporterAccountId" ' + `WHERE "account"."userId" = ${options.whereUserId}` + - ')' + ')' ), 'abusesCreatedCount' ], @@ -262,7 +263,7 @@ type WhereUserIdScopeOptions = { whereUserId?: '$userId' | '"UserModel"."id"' } 'FROM "videoComment" ' + 'INNER JOIN "account" ON "account"."id" = "videoComment"."accountId" ' + `WHERE "account"."userId" = ${options.whereUserId}` + - ')' + ')' ), 'videoCommentsCount' ] @@ -285,7 +286,6 @@ type WhereUserIdScopeOptions = { whereUserId?: '$userId' | '"UserModel"."id"' } ] }) export class UserModel extends SequelizeModel { - @AllowNull(true) @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true)) @Column @@ -554,8 +554,8 @@ export class UserModel extends SequelizeModel { static listWithRight (right: UserRightType): Promise { const roles = Object.keys(USER_ROLE_LABELS) - .map(k => parseInt(k, 10) as UserRoleType) - .filter(role => hasUserRight(role, right)) + .map(k => parseInt(k, 10) as UserRoleType) + .filter(role => hasUserRight(role, right)) const query = { where: { @@ -675,6 +675,18 @@ export class UserModel extends SequelizeModel { return UserModel.findAll(query) } + static loadByPendingEmailCaseInsensitive (pendingEmail: string): Promise { + const query = { + where: where( + fn('LOWER', col('pendingEmail')), + '=', + pendingEmail.toLowerCase() + ) + } + + return UserModel.findAll(query) + } + static loadByUsernameOrEmailCaseInsensitive (usernameOrEmail: string): Promise { const query = { where: { @@ -863,8 +875,8 @@ export class UserModel extends SequelizeModel { return 'SELECT COALESCE(SUM("size"), 0) AS "total" ' + 'FROM (' + - `SELECT ${sizeSelect} AS "size" FROM (${webVideoFiles} UNION ${hlsFiles}) t1 ` + - 'GROUP BY "t1"."videoId"' + + `SELECT ${sizeSelect} AS "size" FROM (${webVideoFiles} UNION ${hlsFiles}) t1 ` + + 'GROUP BY "t1"."videoId"' + ') t2' } @@ -925,7 +937,7 @@ export class UserModel extends SequelizeModel { } return UserModel.findAll(query) - .then(u => u.map(u => u.username)) + .then(u => u.map(u => u.username)) } hasRight (right: UserRightType) { @@ -1037,13 +1049,13 @@ export class UserModel extends SequelizeModel { if (Array.isArray(this.Account.VideoChannels) === true) { json.videoChannels = this.Account.VideoChannels - .map(c => c.toFormattedJSON()) - .sort((v1, v2) => { - if (v1.createdAt < v2.createdAt) return -1 - if (v1.createdAt === v2.createdAt) return 0 + .map(c => c.toFormattedJSON()) + .sort((v1, v2) => { + if (v1.createdAt < v2.createdAt) return -1 + if (v1.createdAt === v2.createdAt) return 0 - return 1 - }) + return 1 + }) } return json @@ -1053,7 +1065,7 @@ export class UserModel extends SequelizeModel { const formatted = this.toFormattedJSON({ withAdminFlags: true }) const specialPlaylists = this.Account.VideoPlaylists - .map(p => ({ id: p.id, name: p.name, type: p.type })) + .map(p => ({ id: p.id, name: p.name, type: p.type })) return Object.assign(formatted, { specialPlaylists }) } diff --git a/server/core/types/express.d.ts b/server/core/types/express.d.ts index 098c2fef5..c881b134b 100644 --- a/server/core/types/express.d.ts +++ b/server/core/types/express.d.ts @@ -197,6 +197,9 @@ declare module 'express' { user?: MUserDefault userRegistration?: MRegistration + // For verification links + userEmail?: MUserDefault + userPendingEmail?: MUserDefault server?: MServer