aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-01-20 15:34:01 +0100
committerChocobozzz <me@florianbigard.com>2023-01-20 15:34:01 +0100
commit4115f20084f302f497be9cb12237564679ca54ec (patch)
tree74cf8f5b3884edeb0b47b529cf04b306cd12b23d
parente854d57bed56bcbba4d191af54125ae6dd569a88 (diff)
downloadPeerTube-4115f20084f302f497be9cb12237564679ca54ec.tar.gz
PeerTube-4115f20084f302f497be9cb12237564679ca54ec.tar.zst
PeerTube-4115f20084f302f497be9cb12237564679ca54ec.zip
Add ability to not send an email for registration
-rw-r--r--client/src/app/+admin/moderation/registration-list/admin-registration.service.ts22
-rw-r--r--client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html13
-rw-r--r--client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts61
-rw-r--r--client/src/app/shared/form-validators/form-validator.model.ts2
-rw-r--r--server/controllers/api/users/registrations.ts23
-rw-r--r--server/middlewares/validators/user-registrations.ts7
-rw-r--r--server/tests/api/users/registrations.ts36
-rw-r--r--shared/models/users/registration/user-registration-update-state.model.ts1
-rw-r--r--shared/server-commands/users/registrations-command.ts20
-rw-r--r--support/doc/api/openapi.yaml3
10 files changed, 137 insertions, 51 deletions
diff --git a/client/src/app/+admin/moderation/registration-list/admin-registration.service.ts b/client/src/app/+admin/moderation/registration-list/admin-registration.service.ts
index 10e2938c7..a9f13cf2f 100644
--- a/client/src/app/+admin/moderation/registration-list/admin-registration.service.ts
+++ b/client/src/app/+admin/moderation/registration-list/admin-registration.service.ts
@@ -5,7 +5,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'
5import { Injectable } from '@angular/core' 5import { Injectable } from '@angular/core'
6import { RestExtractor, RestPagination, RestService } from '@app/core' 6import { RestExtractor, RestPagination, RestService } from '@app/core'
7import { arrayify } from '@shared/core-utils' 7import { arrayify } from '@shared/core-utils'
8import { ResultList, UserRegistration } from '@shared/models' 8import { ResultList, UserRegistration, UserRegistrationUpdateState } from '@shared/models'
9import { environment } from '../../../../environments/environment' 9import { environment } from '../../../../environments/environment'
10 10
11@Injectable() 11@Injectable()
@@ -40,17 +40,29 @@ export class AdminRegistrationService {
40 ) 40 )
41 } 41 }
42 42
43 acceptRegistration (registration: UserRegistration, moderationResponse: string) { 43 acceptRegistration (options: {
44 registration: UserRegistration
45 moderationResponse: string
46 preventEmailDelivery: boolean
47 }) {
48 const { registration, moderationResponse, preventEmailDelivery } = options
49
44 const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/accept' 50 const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/accept'
45 const body = { moderationResponse } 51 const body: UserRegistrationUpdateState = { moderationResponse, preventEmailDelivery }
46 52
47 return this.authHttp.post(url, body) 53 return this.authHttp.post(url, body)
48 .pipe(catchError(res => this.restExtractor.handleError(res))) 54 .pipe(catchError(res => this.restExtractor.handleError(res)))
49 } 55 }
50 56
51 rejectRegistration (registration: UserRegistration, moderationResponse: string) { 57 rejectRegistration (options: {
58 registration: UserRegistration
59 moderationResponse: string
60 preventEmailDelivery: boolean
61 }) {
62 const { registration, moderationResponse, preventEmailDelivery } = options
63
52 const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/reject' 64 const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/reject'
53 const body = { moderationResponse } 65 const body: UserRegistrationUpdateState = { moderationResponse, preventEmailDelivery }
54 66
55 return this.authHttp.post(url, body) 67 return this.authHttp.post(url, body)
56 .pipe(catchError(res => this.restExtractor.handleError(res))) 68 .pipe(catchError(res => this.restExtractor.handleError(res)))
diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html
index 7a33bb94b..8e46b0cf9 100644
--- a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html
+++ b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html
@@ -12,7 +12,7 @@
12 <div class="modal-body mb-3"> 12 <div class="modal-body mb-3">
13 13
14 <div i18n *ngIf="!registration.emailVerified" class="alert alert-warning"> 14 <div i18n *ngIf="!registration.emailVerified" class="alert alert-warning">
15 Registration email has not been verified. 15 Registration email has not been verified. Email delivery has been disabled by default.
16 </div> 16 </div>
17 17
18 <div class="description"> 18 <div class="description">
@@ -21,7 +21,7 @@
21 <strong>Accepting</strong>&nbsp;<em>{{ registration.username }}</em> registration will create the account and channel. 21 <strong>Accepting</strong>&nbsp;<em>{{ registration.username }}</em> registration will create the account and channel.
22 </p> 22 </p>
23 23
24 <p *ngIf="isEmailEnabled()" i18n> 24 <p *ngIf="isEmailEnabled()" i18n [ngClass]="{ 'text-decoration-line-through': isPreventEmailDeliveryChecked() }">
25 An email will be sent to <em>{{ registration.email }}</em> explaining its account has been created with the moderation response you'll write below. 25 An email will be sent to <em>{{ registration.email }}</em> explaining its account has been created with the moderation response you'll write below.
26 </p> 26 </p>
27 27
@@ -31,7 +31,7 @@
31 </ng-container> 31 </ng-container>
32 32
33 <ng-container *ngIf="isReject()"> 33 <ng-container *ngIf="isReject()">
34 <p i18n> 34 <p i18n [ngClass]="{ 'text-decoration-line-through': isPreventEmailDeliveryChecked() }">
35 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. 35 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.
36 </p> 36 </p>
37 37
@@ -53,6 +53,13 @@
53 {{ formErrors.moderationResponse }} 53 {{ formErrors.moderationResponse }}
54 </div> 54 </div>
55 </div> 55 </div>
56
57 <div class="form-group">
58 <my-peertube-checkbox
59 inputName="preventEmailDelivery" formControlName="preventEmailDelivery" [disabled]="!isEmailEnabled()"
60 i18n-labelText labelText="Prevent email from being sent to the user"
61 ></my-peertube-checkbox>
62 </div>
56 </div> 63 </div>
57 64
58 <div class="modal-footer inputs"> 65 <div class="modal-footer inputs">
diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts
index fbe8deb41..3a7e5dea1 100644
--- a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts
+++ b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts
@@ -34,7 +34,8 @@ export class ProcessRegistrationModalComponent extends FormReactive implements O
34 34
35 ngOnInit () { 35 ngOnInit () {
36 this.buildForm({ 36 this.buildForm({
37 moderationResponse: REGISTRATION_MODERATION_RESPONSE_VALIDATOR 37 moderationResponse: REGISTRATION_MODERATION_RESPONSE_VALIDATOR,
38 preventEmailDelivery: null
38 }) 39 })
39 } 40 }
40 41
@@ -50,6 +51,10 @@ export class ProcessRegistrationModalComponent extends FormReactive implements O
50 this.processMode = mode 51 this.processMode = mode
51 this.registration = registration 52 this.registration = registration
52 53
54 this.form.patchValue({
55 preventEmailDelivery: !this.isEmailEnabled() || registration.emailVerified !== true
56 })
57
53 this.openedModal = this.modalService.open(this.modal, { centered: true }) 58 this.openedModal = this.modalService.open(this.modal, { centered: true })
54 } 59 }
55 60
@@ -77,31 +82,41 @@ export class ProcessRegistrationModalComponent extends FormReactive implements O
77 return this.server.getHTMLConfig().email.enabled 82 return this.server.getHTMLConfig().email.enabled
78 } 83 }
79 84
80 private acceptRegistration () { 85 isPreventEmailDeliveryChecked () {
81 this.registrationService.acceptRegistration(this.registration, this.form.value.moderationResponse) 86 return this.form.value.preventEmailDelivery
82 .subscribe({ 87 }
83 next: () => {
84 this.notifier.success($localize`${this.registration.username} account created`)
85
86 this.registrationProcessed.emit()
87 this.hide()
88 },
89 88
90 error: err => this.notifier.error(err.message) 89 private acceptRegistration () {
91 }) 90 this.registrationService.acceptRegistration({
91 registration: this.registration,
92 moderationResponse: this.form.value.moderationResponse,
93 preventEmailDelivery: this.form.value.preventEmailDelivery
94 }).subscribe({
95 next: () => {
96 this.notifier.success($localize`${this.registration.username} account created`)
97
98 this.registrationProcessed.emit()
99 this.hide()
100 },
101
102 error: err => this.notifier.error(err.message)
103 })
92 } 104 }
93 105
94 private rejectRegistration () { 106 private rejectRegistration () {
95 this.registrationService.rejectRegistration(this.registration, this.form.value.moderationResponse) 107 this.registrationService.rejectRegistration({
96 .subscribe({ 108 registration: this.registration,
97 next: () => { 109 moderationResponse: this.form.value.moderationResponse,
98 this.notifier.success($localize`${this.registration.username} registration rejected`) 110 preventEmailDelivery: this.form.value.preventEmailDelivery
99 111 }).subscribe({
100 this.registrationProcessed.emit() 112 next: () => {
101 this.hide() 113 this.notifier.success($localize`${this.registration.username} registration rejected`)
102 }, 114
103 115 this.registrationProcessed.emit()
104 error: err => this.notifier.error(err.message) 116 this.hide()
105 }) 117 },
118
119 error: err => this.notifier.error(err.message)
120 })
106 } 121 }
107} 122}
diff --git a/client/src/app/shared/form-validators/form-validator.model.ts b/client/src/app/shared/form-validators/form-validator.model.ts
index 31c253b9b..1e4bba86b 100644
--- a/client/src/app/shared/form-validators/form-validator.model.ts
+++ b/client/src/app/shared/form-validators/form-validator.model.ts
@@ -12,5 +12,5 @@ export type BuildFormArgument = {
12} 12}
13 13
14export type BuildFormDefaultValues = { 14export type BuildFormDefaultValues = {
15 [ name: string ]: number | string | string[] | BuildFormDefaultValues 15 [ name: string ]: boolean | number | string | string[] | BuildFormDefaultValues
16} 16}
diff --git a/server/controllers/api/users/registrations.ts b/server/controllers/api/users/registrations.ts
index 3d4e0aa18..5e213d6cc 100644
--- a/server/controllers/api/users/registrations.ts
+++ b/server/controllers/api/users/registrations.ts
@@ -3,7 +3,14 @@ import { Emailer } from '@server/lib/emailer'
3import { Hooks } from '@server/lib/plugins/hooks' 3import { Hooks } from '@server/lib/plugins/hooks'
4import { UserRegistrationModel } from '@server/models/user/user-registration' 4import { UserRegistrationModel } from '@server/models/user/user-registration'
5import { pick } from '@shared/core-utils' 5import { pick } from '@shared/core-utils'
6import { HttpStatusCode, UserRegister, UserRegistrationRequest, UserRegistrationState, UserRight } from '@shared/models' 6import {
7 HttpStatusCode,
8 UserRegister,
9 UserRegistrationRequest,
10 UserRegistrationState,
11 UserRegistrationUpdateState,
12 UserRight
13} from '@shared/models'
7import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger' 14import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger'
8import { logger } from '../../../helpers/logger' 15import { logger } from '../../../helpers/logger'
9import { CONFIG } from '../../../initializers/config' 16import { CONFIG } from '../../../initializers/config'
@@ -125,6 +132,7 @@ async function requestRegistration (req: express.Request, res: express.Response)
125 132
126async function acceptRegistration (req: express.Request, res: express.Response) { 133async function acceptRegistration (req: express.Request, res: express.Response) {
127 const registration = res.locals.userRegistration 134 const registration = res.locals.userRegistration
135 const body: UserRegistrationUpdateState = req.body
128 136
129 const userToCreate = buildUser({ 137 const userToCreate = buildUser({
130 username: registration.username, 138 username: registration.username,
@@ -150,26 +158,31 @@ async function acceptRegistration (req: express.Request, res: express.Response)
150 158
151 registration.userId = user.id 159 registration.userId = user.id
152 registration.state = UserRegistrationState.ACCEPTED 160 registration.state = UserRegistrationState.ACCEPTED
153 registration.moderationResponse = req.body.moderationResponse 161 registration.moderationResponse = body.moderationResponse
154 162
155 await registration.save() 163 await registration.save()
156 164
157 logger.info('Registration of %s accepted', registration.username) 165 logger.info('Registration of %s accepted', registration.username)
158 166
159 Emailer.Instance.addUserRegistrationRequestProcessedJob(registration) 167 if (body.preventEmailDelivery !== true) {
168 Emailer.Instance.addUserRegistrationRequestProcessedJob(registration)
169 }
160 170
161 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 171 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
162} 172}
163 173
164async function rejectRegistration (req: express.Request, res: express.Response) { 174async function rejectRegistration (req: express.Request, res: express.Response) {
165 const registration = res.locals.userRegistration 175 const registration = res.locals.userRegistration
176 const body: UserRegistrationUpdateState = req.body
166 177
167 registration.state = UserRegistrationState.REJECTED 178 registration.state = UserRegistrationState.REJECTED
168 registration.moderationResponse = req.body.moderationResponse 179 registration.moderationResponse = body.moderationResponse
169 180
170 await registration.save() 181 await registration.save()
171 182
172 Emailer.Instance.addUserRegistrationRequestProcessedJob(registration) 183 if (body.preventEmailDelivery !== true) {
184 Emailer.Instance.addUserRegistrationRequestProcessedJob(registration)
185 }
173 186
174 logger.info('Registration of %s rejected', registration.username) 187 logger.info('Registration of %s rejected', registration.username)
175 188
diff --git a/server/middlewares/validators/user-registrations.ts b/server/middlewares/validators/user-registrations.ts
index e263c27c5..fcf655a2c 100644
--- a/server/middlewares/validators/user-registrations.ts
+++ b/server/middlewares/validators/user-registrations.ts
@@ -1,6 +1,6 @@
1import express from 'express' 1import express from 'express'
2import { body, param, query, ValidationChain } from 'express-validator' 2import { body, param, query, ValidationChain } from 'express-validator'
3import { exists, isIdValid } from '@server/helpers/custom-validators/misc' 3import { exists, isBooleanValid, isIdValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc'
4import { isRegistrationModerationResponseValid, isRegistrationReasonValid } from '@server/helpers/custom-validators/user-registration' 4import { isRegistrationModerationResponseValid, isRegistrationReasonValid } from '@server/helpers/custom-validators/user-registration'
5import { CONFIG } from '@server/initializers/config' 5import { CONFIG } from '@server/initializers/config'
6import { Hooks } from '@server/lib/plugins/hooks' 6import { Hooks } from '@server/lib/plugins/hooks'
@@ -91,6 +91,11 @@ const acceptOrRejectRegistrationValidator = [
91 body('moderationResponse') 91 body('moderationResponse')
92 .custom(isRegistrationModerationResponseValid), 92 .custom(isRegistrationModerationResponseValid),
93 93
94 body('preventEmailDelivery')
95 .optional()
96 .customSanitizer(toBooleanOrNull)
97 .custom(isBooleanValid).withMessage('Should have preventEmailDelivery boolean'),
98
94 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 99 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
95 if (areValidationErrors(req, res)) return 100 if (areValidationErrors(req, res)) return
96 if (!await checkRegistrationIdExist(req.params.registrationId, res)) return 101 if (!await checkRegistrationIdExist(req.params.registrationId, res)) return
diff --git a/server/tests/api/users/registrations.ts b/server/tests/api/users/registrations.ts
index a9e1114e8..e6524f07d 100644
--- a/server/tests/api/users/registrations.ts
+++ b/server/tests/api/users/registrations.ts
@@ -329,6 +329,42 @@ describe('Test registrations', function () {
329 } 329 }
330 }) 330 })
331 331
332 it('Should be able to prevent email delivery on accept/reject', async function () {
333 this.timeout(50000)
334
335 let id1: number
336 let id2: number
337
338 {
339 const { id } = await server.registrations.requestRegistration({
340 username: 'user7',
341 email: 'user7@example.com',
342 registrationReason: 'tt'
343 })
344 id1 = id
345 }
346 {
347 const { id } = await server.registrations.requestRegistration({
348 username: 'user8',
349 email: 'user8@example.com',
350 registrationReason: 'tt'
351 })
352 id2 = id
353 }
354
355 await server.registrations.accept({ id: id1, moderationResponse: 'tt', preventEmailDelivery: true })
356 await server.registrations.reject({ id: id2, moderationResponse: 'tt', preventEmailDelivery: true })
357
358 await waitJobs([ server ])
359
360 const filtered = emails.filter(e => {
361 const address = e['to'][0]['address']
362 return address === 'user7@example.com' || address === 'user8@example.com'
363 })
364
365 expect(filtered).to.have.lengthOf(0)
366 })
367
332 it('Should request a registration without a channel, that will conflict with an already existing channel', async function () { 368 it('Should request a registration without a channel, that will conflict with an already existing channel', async function () {
333 let id1: number 369 let id1: number
334 let id2: number 370 let id2: number
diff --git a/shared/models/users/registration/user-registration-update-state.model.ts b/shared/models/users/registration/user-registration-update-state.model.ts
index 636e22c32..a1740dcca 100644
--- a/shared/models/users/registration/user-registration-update-state.model.ts
+++ b/shared/models/users/registration/user-registration-update-state.model.ts
@@ -1,3 +1,4 @@
1export interface UserRegistrationUpdateState { 1export interface UserRegistrationUpdateState {
2 moderationResponse: string 2 moderationResponse: string
3 preventEmailDelivery?: boolean
3} 4}
diff --git a/shared/server-commands/users/registrations-command.ts b/shared/server-commands/users/registrations-command.ts
index 4e97571f4..f57f54b34 100644
--- a/shared/server-commands/users/registrations-command.ts
+++ b/shared/server-commands/users/registrations-command.ts
@@ -1,5 +1,5 @@
1import { pick } from '@shared/core-utils' 1import { pick } from '@shared/core-utils'
2import { HttpStatusCode, ResultList, UserRegistration, UserRegistrationRequest } from '@shared/models' 2import { HttpStatusCode, ResultList, UserRegistration, UserRegistrationRequest, UserRegistrationUpdateState } from '@shared/models'
3import { unwrapBody } from '../requests' 3import { unwrapBody } from '../requests'
4import { AbstractCommand, OverrideCommandOptions } from '../shared' 4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5 5
@@ -47,35 +47,29 @@ export class RegistrationsCommand extends AbstractCommand {
47 47
48 // --------------------------------------------------------------------------- 48 // ---------------------------------------------------------------------------
49 49
50 accept (options: OverrideCommandOptions & { 50 accept (options: OverrideCommandOptions & { id: number } & UserRegistrationUpdateState) {
51 id: number 51 const { id } = options
52 moderationResponse: string
53 }) {
54 const { id, moderationResponse } = options
55 const path = '/api/v1/users/registrations/' + id + '/accept' 52 const path = '/api/v1/users/registrations/' + id + '/accept'
56 53
57 return this.postBodyRequest({ 54 return this.postBodyRequest({
58 ...options, 55 ...options,
59 56
60 path, 57 path,
61 fields: { moderationResponse }, 58 fields: pick(options, [ 'moderationResponse', 'preventEmailDelivery' ]),
62 implicitToken: true, 59 implicitToken: true,
63 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 60 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
64 }) 61 })
65 } 62 }
66 63
67 reject (options: OverrideCommandOptions & { 64 reject (options: OverrideCommandOptions & { id: number } & UserRegistrationUpdateState) {
68 id: number 65 const { id } = options
69 moderationResponse: string
70 }) {
71 const { id, moderationResponse } = options
72 const path = '/api/v1/users/registrations/' + id + '/reject' 66 const path = '/api/v1/users/registrations/' + id + '/reject'
73 67
74 return this.postBodyRequest({ 68 return this.postBodyRequest({
75 ...options, 69 ...options,
76 70
77 path, 71 path,
78 fields: { moderationResponse }, 72 fields: pick(options, [ 'moderationResponse', 'preventEmailDelivery' ]),
79 implicitToken: true, 73 implicitToken: true,
80 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 74 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
81 }) 75 })
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index 61cd6a651..f90b7f575 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -7961,6 +7961,9 @@ components:
7961 moderationResponse: 7961 moderationResponse:
7962 type: string 7962 type: string
7963 description: Moderation response to send to the user 7963 description: Moderation response to send to the user
7964 preventEmailDelivery:
7965 type: boolean
7966 description: Set it to true if you don't want PeerTube to send an email to the user
7964 required: 7967 required:
7965 - moderationResponse 7968 - moderationResponse
7966 7969