Add ability to not send an email for registration

This commit is contained in:
Chocobozzz 2023-01-20 15:34:01 +01:00
parent e854d57bed
commit 4115f20084
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
10 changed files with 133 additions and 47 deletions

View File

@ -5,7 +5,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { RestExtractor, RestPagination, RestService } from '@app/core' import { RestExtractor, RestPagination, RestService } from '@app/core'
import { arrayify } from '@shared/core-utils' import { arrayify } from '@shared/core-utils'
import { ResultList, UserRegistration } from '@shared/models' import { ResultList, UserRegistration, UserRegistrationUpdateState } from '@shared/models'
import { environment } from '../../../../environments/environment' import { environment } from '../../../../environments/environment'
@Injectable() @Injectable()
@ -40,17 +40,29 @@ export class AdminRegistrationService {
) )
} }
acceptRegistration (registration: UserRegistration, moderationResponse: string) { acceptRegistration (options: {
registration: UserRegistration
moderationResponse: string
preventEmailDelivery: boolean
}) {
const { registration, moderationResponse, preventEmailDelivery } = options
const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/accept' const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/accept'
const body = { moderationResponse } const body: UserRegistrationUpdateState = { moderationResponse, preventEmailDelivery }
return this.authHttp.post(url, body) return this.authHttp.post(url, body)
.pipe(catchError(res => this.restExtractor.handleError(res))) .pipe(catchError(res => this.restExtractor.handleError(res)))
} }
rejectRegistration (registration: UserRegistration, moderationResponse: string) { rejectRegistration (options: {
registration: UserRegistration
moderationResponse: string
preventEmailDelivery: boolean
}) {
const { registration, moderationResponse, preventEmailDelivery } = options
const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/reject' const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/reject'
const body = { moderationResponse } const body: UserRegistrationUpdateState = { moderationResponse, preventEmailDelivery }
return this.authHttp.post(url, body) return this.authHttp.post(url, body)
.pipe(catchError(res => this.restExtractor.handleError(res))) .pipe(catchError(res => this.restExtractor.handleError(res)))

View File

@ -12,7 +12,7 @@
<div class="modal-body mb-3"> <div class="modal-body mb-3">
<div i18n *ngIf="!registration.emailVerified" class="alert alert-warning"> <div i18n *ngIf="!registration.emailVerified" class="alert alert-warning">
Registration email has not been verified. Registration email has not been verified. Email delivery has been disabled by default.
</div> </div>
<div class="description"> <div class="description">
@ -21,7 +21,7 @@
<strong>Accepting</strong>&nbsp;<em>{{ registration.username }}</em> registration will create the account and channel. <strong>Accepting</strong>&nbsp;<em>{{ registration.username }}</em> registration will create the account and channel.
</p> </p>
<p *ngIf="isEmailEnabled()" i18n> <p *ngIf="isEmailEnabled()" i18n [ngClass]="{ 'text-decoration-line-through': isPreventEmailDeliveryChecked() }">
An email will be sent to <em>{{ registration.email }}</em> explaining its account has been created with the moderation response you'll write below. An email will be sent to <em>{{ registration.email }}</em> explaining its account has been created with the moderation response you'll write below.
</p> </p>
@ -31,7 +31,7 @@
</ng-container> </ng-container>
<ng-container *ngIf="isReject()"> <ng-container *ngIf="isReject()">
<p i18n> <p i18n [ngClass]="{ 'text-decoration-line-through': isPreventEmailDeliveryChecked() }">
An email will be sent to <em>{{ registration.email }}</em> explaining its registration request has been <strong>rejected</strong> with the moderation response you'll write below. An email will be sent to <em>{{ registration.email }}</em> explaining its registration request has been <strong>rejected</strong> with the moderation response you'll write below.
</p> </p>
@ -53,6 +53,13 @@
{{ formErrors.moderationResponse }} {{ formErrors.moderationResponse }}
</div> </div>
</div> </div>
<div class="form-group">
<my-peertube-checkbox
inputName="preventEmailDelivery" formControlName="preventEmailDelivery" [disabled]="!isEmailEnabled()"
i18n-labelText labelText="Prevent email from being sent to the user"
></my-peertube-checkbox>
</div>
</div> </div>
<div class="modal-footer inputs"> <div class="modal-footer inputs">

View File

@ -34,7 +34,8 @@ export class ProcessRegistrationModalComponent extends FormReactive implements O
ngOnInit () { ngOnInit () {
this.buildForm({ this.buildForm({
moderationResponse: REGISTRATION_MODERATION_RESPONSE_VALIDATOR moderationResponse: REGISTRATION_MODERATION_RESPONSE_VALIDATOR,
preventEmailDelivery: null
}) })
} }
@ -50,6 +51,10 @@ export class ProcessRegistrationModalComponent extends FormReactive implements O
this.processMode = mode this.processMode = mode
this.registration = registration this.registration = registration
this.form.patchValue({
preventEmailDelivery: !this.isEmailEnabled() || registration.emailVerified !== true
})
this.openedModal = this.modalService.open(this.modal, { centered: true }) this.openedModal = this.modalService.open(this.modal, { centered: true })
} }
@ -77,31 +82,41 @@ export class ProcessRegistrationModalComponent extends FormReactive implements O
return this.server.getHTMLConfig().email.enabled return this.server.getHTMLConfig().email.enabled
} }
isPreventEmailDeliveryChecked () {
return this.form.value.preventEmailDelivery
}
private acceptRegistration () { private acceptRegistration () {
this.registrationService.acceptRegistration(this.registration, this.form.value.moderationResponse) this.registrationService.acceptRegistration({
.subscribe({ registration: this.registration,
next: () => { moderationResponse: this.form.value.moderationResponse,
this.notifier.success($localize`${this.registration.username} account created`) preventEmailDelivery: this.form.value.preventEmailDelivery
}).subscribe({
next: () => {
this.notifier.success($localize`${this.registration.username} account created`)
this.registrationProcessed.emit() this.registrationProcessed.emit()
this.hide() this.hide()
}, },
error: err => this.notifier.error(err.message) error: err => this.notifier.error(err.message)
}) })
} }
private rejectRegistration () { private rejectRegistration () {
this.registrationService.rejectRegistration(this.registration, this.form.value.moderationResponse) this.registrationService.rejectRegistration({
.subscribe({ registration: this.registration,
next: () => { moderationResponse: this.form.value.moderationResponse,
this.notifier.success($localize`${this.registration.username} registration rejected`) preventEmailDelivery: this.form.value.preventEmailDelivery
}).subscribe({
next: () => {
this.notifier.success($localize`${this.registration.username} registration rejected`)
this.registrationProcessed.emit() this.registrationProcessed.emit()
this.hide() this.hide()
}, },
error: err => this.notifier.error(err.message) error: err => this.notifier.error(err.message)
}) })
} }
} }

View File

@ -12,5 +12,5 @@ export type BuildFormArgument = {
} }
export type BuildFormDefaultValues = { export type BuildFormDefaultValues = {
[ name: string ]: number | string | string[] | BuildFormDefaultValues [ name: string ]: boolean | number | string | string[] | BuildFormDefaultValues
} }

View File

@ -3,7 +3,14 @@ import { Emailer } from '@server/lib/emailer'
import { Hooks } from '@server/lib/plugins/hooks' import { Hooks } from '@server/lib/plugins/hooks'
import { UserRegistrationModel } from '@server/models/user/user-registration' import { UserRegistrationModel } from '@server/models/user/user-registration'
import { pick } from '@shared/core-utils' import { pick } from '@shared/core-utils'
import { HttpStatusCode, UserRegister, UserRegistrationRequest, UserRegistrationState, UserRight } from '@shared/models' import {
HttpStatusCode,
UserRegister,
UserRegistrationRequest,
UserRegistrationState,
UserRegistrationUpdateState,
UserRight
} from '@shared/models'
import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger' import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { CONFIG } from '../../../initializers/config' import { CONFIG } from '../../../initializers/config'
@ -125,6 +132,7 @@ async function requestRegistration (req: express.Request, res: express.Response)
async function acceptRegistration (req: express.Request, res: express.Response) { async function acceptRegistration (req: express.Request, res: express.Response) {
const registration = res.locals.userRegistration const registration = res.locals.userRegistration
const body: UserRegistrationUpdateState = req.body
const userToCreate = buildUser({ const userToCreate = buildUser({
username: registration.username, username: registration.username,
@ -150,26 +158,31 @@ async function acceptRegistration (req: express.Request, res: express.Response)
registration.userId = user.id registration.userId = user.id
registration.state = UserRegistrationState.ACCEPTED registration.state = UserRegistrationState.ACCEPTED
registration.moderationResponse = req.body.moderationResponse registration.moderationResponse = body.moderationResponse
await registration.save() await registration.save()
logger.info('Registration of %s accepted', registration.username) logger.info('Registration of %s accepted', registration.username)
Emailer.Instance.addUserRegistrationRequestProcessedJob(registration) if (body.preventEmailDelivery !== true) {
Emailer.Instance.addUserRegistrationRequestProcessedJob(registration)
}
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
} }
async function rejectRegistration (req: express.Request, res: express.Response) { async function rejectRegistration (req: express.Request, res: express.Response) {
const registration = res.locals.userRegistration const registration = res.locals.userRegistration
const body: UserRegistrationUpdateState = req.body
registration.state = UserRegistrationState.REJECTED registration.state = UserRegistrationState.REJECTED
registration.moderationResponse = req.body.moderationResponse registration.moderationResponse = body.moderationResponse
await registration.save() await registration.save()
Emailer.Instance.addUserRegistrationRequestProcessedJob(registration) if (body.preventEmailDelivery !== true) {
Emailer.Instance.addUserRegistrationRequestProcessedJob(registration)
}
logger.info('Registration of %s rejected', registration.username) logger.info('Registration of %s rejected', registration.username)

View File

@ -1,6 +1,6 @@
import express from 'express' import express from 'express'
import { body, param, query, ValidationChain } from 'express-validator' import { body, param, query, ValidationChain } from 'express-validator'
import { exists, isIdValid } from '@server/helpers/custom-validators/misc' import { exists, isBooleanValid, isIdValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc'
import { isRegistrationModerationResponseValid, isRegistrationReasonValid } from '@server/helpers/custom-validators/user-registration' import { isRegistrationModerationResponseValid, isRegistrationReasonValid } from '@server/helpers/custom-validators/user-registration'
import { CONFIG } from '@server/initializers/config' import { CONFIG } from '@server/initializers/config'
import { Hooks } from '@server/lib/plugins/hooks' import { Hooks } from '@server/lib/plugins/hooks'
@ -91,6 +91,11 @@ const acceptOrRejectRegistrationValidator = [
body('moderationResponse') body('moderationResponse')
.custom(isRegistrationModerationResponseValid), .custom(isRegistrationModerationResponseValid),
body('preventEmailDelivery')
.optional()
.customSanitizer(toBooleanOrNull)
.custom(isBooleanValid).withMessage('Should have preventEmailDelivery boolean'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => { async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
if (!await checkRegistrationIdExist(req.params.registrationId, res)) return if (!await checkRegistrationIdExist(req.params.registrationId, res)) return

View File

@ -329,6 +329,42 @@ describe('Test registrations', function () {
} }
}) })
it('Should be able to prevent email delivery on accept/reject', async function () {
this.timeout(50000)
let id1: number
let id2: number
{
const { id } = await server.registrations.requestRegistration({
username: 'user7',
email: 'user7@example.com',
registrationReason: 'tt'
})
id1 = id
}
{
const { id } = await server.registrations.requestRegistration({
username: 'user8',
email: 'user8@example.com',
registrationReason: 'tt'
})
id2 = id
}
await server.registrations.accept({ id: id1, moderationResponse: 'tt', preventEmailDelivery: true })
await server.registrations.reject({ id: id2, moderationResponse: 'tt', preventEmailDelivery: true })
await waitJobs([ server ])
const filtered = emails.filter(e => {
const address = e['to'][0]['address']
return address === 'user7@example.com' || address === 'user8@example.com'
})
expect(filtered).to.have.lengthOf(0)
})
it('Should request a registration without a channel, that will conflict with an already existing channel', async function () { it('Should request a registration without a channel, that will conflict with an already existing channel', async function () {
let id1: number let id1: number
let id2: number let id2: number

View File

@ -1,3 +1,4 @@
export interface UserRegistrationUpdateState { export interface UserRegistrationUpdateState {
moderationResponse: string moderationResponse: string
preventEmailDelivery?: boolean
} }

View File

@ -1,5 +1,5 @@
import { pick } from '@shared/core-utils' import { pick } from '@shared/core-utils'
import { HttpStatusCode, ResultList, UserRegistration, UserRegistrationRequest } from '@shared/models' import { HttpStatusCode, ResultList, UserRegistration, UserRegistrationRequest, UserRegistrationUpdateState } from '@shared/models'
import { unwrapBody } from '../requests' import { unwrapBody } from '../requests'
import { AbstractCommand, OverrideCommandOptions } from '../shared' import { AbstractCommand, OverrideCommandOptions } from '../shared'
@ -47,35 +47,29 @@ export class RegistrationsCommand extends AbstractCommand {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
accept (options: OverrideCommandOptions & { accept (options: OverrideCommandOptions & { id: number } & UserRegistrationUpdateState) {
id: number const { id } = options
moderationResponse: string
}) {
const { id, moderationResponse } = options
const path = '/api/v1/users/registrations/' + id + '/accept' const path = '/api/v1/users/registrations/' + id + '/accept'
return this.postBodyRequest({ return this.postBodyRequest({
...options, ...options,
path, path,
fields: { moderationResponse }, fields: pick(options, [ 'moderationResponse', 'preventEmailDelivery' ]),
implicitToken: true, implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
}) })
} }
reject (options: OverrideCommandOptions & { reject (options: OverrideCommandOptions & { id: number } & UserRegistrationUpdateState) {
id: number const { id } = options
moderationResponse: string
}) {
const { id, moderationResponse } = options
const path = '/api/v1/users/registrations/' + id + '/reject' const path = '/api/v1/users/registrations/' + id + '/reject'
return this.postBodyRequest({ return this.postBodyRequest({
...options, ...options,
path, path,
fields: { moderationResponse }, fields: pick(options, [ 'moderationResponse', 'preventEmailDelivery' ]),
implicitToken: true, implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
}) })

View File

@ -7961,6 +7961,9 @@ components:
moderationResponse: moderationResponse:
type: string type: string
description: Moderation response to send to the user description: Moderation response to send to the user
preventEmailDelivery:
type: boolean
description: Set it to true if you don't want PeerTube to send an email to the user
required: required:
- moderationResponse - moderationResponse