aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-01-19 09:27:16 +0100
committerChocobozzz <chocobozzz@cpy.re>2023-01-19 13:53:40 +0100
commite364e31e25bd1d4b8d801c845a96d6be708f0a18 (patch)
tree220785a42af361706eb8243960c5da9cddf4d2be /server/lib
parentbc48e33b80f357767b98c1d310b04bdda24c6d46 (diff)
downloadPeerTube-e364e31e25bd1d4b8d801c845a96d6be708f0a18.tar.gz
PeerTube-e364e31e25bd1d4b8d801c845a96d6be708f0a18.tar.zst
PeerTube-e364e31e25bd1d4b8d801c845a96d6be708f0a18.zip
Implement signup approval in server
Diffstat (limited to 'server/lib')
-rw-r--r--server/lib/auth/oauth.ts29
-rw-r--r--server/lib/emailer.ts54
-rw-r--r--server/lib/emails/common/base.pug12
-rw-r--r--server/lib/emails/user-registration-request-accepted/html.pug10
-rw-r--r--server/lib/emails/user-registration-request-rejected/html.pug9
-rw-r--r--server/lib/emails/user-registration-request/html.pug9
-rw-r--r--server/lib/emails/verify-email/html.pug26
-rw-r--r--server/lib/notifier/notifier.ts19
-rw-r--r--server/lib/notifier/shared/instance/direct-registration-for-moderators.ts (renamed from server/lib/notifier/shared/instance/registration-for-moderators.ts)4
-rw-r--r--server/lib/notifier/shared/instance/index.ts3
-rw-r--r--server/lib/notifier/shared/instance/registration-request-for-moderators.ts48
-rw-r--r--server/lib/redis.ts30
-rw-r--r--server/lib/server-config-manager.ts12
-rw-r--r--server/lib/signup.ts15
-rw-r--r--server/lib/user.ts38
15 files changed, 258 insertions, 60 deletions
diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts
index 2905c79a2..887c4f7c9 100644
--- a/server/lib/auth/oauth.ts
+++ b/server/lib/auth/oauth.ts
@@ -11,20 +11,31 @@ import OAuth2Server, {
11import { randomBytesPromise } from '@server/helpers/core-utils' 11import { randomBytesPromise } from '@server/helpers/core-utils'
12import { isOTPValid } from '@server/helpers/otp' 12import { isOTPValid } from '@server/helpers/otp'
13import { CONFIG } from '@server/initializers/config' 13import { CONFIG } from '@server/initializers/config'
14import { UserRegistrationModel } from '@server/models/user/user-registration'
14import { MOAuthClient } from '@server/types/models' 15import { MOAuthClient } from '@server/types/models'
15import { sha1 } from '@shared/extra-utils' 16import { sha1 } from '@shared/extra-utils'
16import { HttpStatusCode } from '@shared/models' 17import { HttpStatusCode, ServerErrorCode, UserRegistrationState } from '@shared/models'
17import { OTP } from '../../initializers/constants' 18import { OTP } from '../../initializers/constants'
18import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' 19import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model'
19 20
20class MissingTwoFactorError extends Error { 21class MissingTwoFactorError extends Error {
21 code = HttpStatusCode.UNAUTHORIZED_401 22 code = HttpStatusCode.UNAUTHORIZED_401
22 name = 'missing_two_factor' 23 name = ServerErrorCode.MISSING_TWO_FACTOR
23} 24}
24 25
25class InvalidTwoFactorError extends Error { 26class InvalidTwoFactorError extends Error {
26 code = HttpStatusCode.BAD_REQUEST_400 27 code = HttpStatusCode.BAD_REQUEST_400
27 name = 'invalid_two_factor' 28 name = ServerErrorCode.INVALID_TWO_FACTOR
29}
30
31class RegistrationWaitingForApproval extends Error {
32 code = HttpStatusCode.BAD_REQUEST_400
33 name = ServerErrorCode.ACCOUNT_WAITING_FOR_APPROVAL
34}
35
36class RegistrationApprovalRejected extends Error {
37 code = HttpStatusCode.BAD_REQUEST_400
38 name = ServerErrorCode.ACCOUNT_APPROVAL_REJECTED
28} 39}
29 40
30/** 41/**
@@ -128,7 +139,17 @@ async function handlePasswordGrant (options: {
128 } 139 }
129 140
130 const user = await getUser(request.body.username, request.body.password, bypassLogin) 141 const user = await getUser(request.body.username, request.body.password, bypassLogin)
131 if (!user) throw new InvalidGrantError('Invalid grant: user credentials are invalid') 142 if (!user) {
143 const registration = await UserRegistrationModel.loadByEmailOrUsername(request.body.username)
144
145 if (registration?.state === UserRegistrationState.REJECTED) {
146 throw new RegistrationApprovalRejected('Registration approval for this account has been rejected')
147 } else if (registration?.state === UserRegistrationState.PENDING) {
148 throw new RegistrationWaitingForApproval('Registration for this account is awaiting approval')
149 }
150
151 throw new InvalidGrantError('Invalid grant: user credentials are invalid')
152 }
132 153
133 if (user.otpSecret) { 154 if (user.otpSecret) {
134 if (!request.headers[OTP.HEADER_NAME]) { 155 if (!request.headers[OTP.HEADER_NAME]) {
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 39b662eb2..f5c3e4745 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -3,13 +3,13 @@ import { merge } from 'lodash'
3import { createTransport, Transporter } from 'nodemailer' 3import { createTransport, Transporter } from 'nodemailer'
4import { join } from 'path' 4import { join } from 'path'
5import { arrayify, root } from '@shared/core-utils' 5import { arrayify, root } from '@shared/core-utils'
6import { EmailPayload } from '@shared/models' 6import { EmailPayload, UserRegistrationState } from '@shared/models'
7import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model' 7import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model'
8import { isTestOrDevInstance } from '../helpers/core-utils' 8import { isTestOrDevInstance } from '../helpers/core-utils'
9import { bunyanLogger, logger } from '../helpers/logger' 9import { bunyanLogger, logger } from '../helpers/logger'
10import { CONFIG, isEmailEnabled } from '../initializers/config' 10import { CONFIG, isEmailEnabled } from '../initializers/config'
11import { WEBSERVER } from '../initializers/constants' 11import { WEBSERVER } from '../initializers/constants'
12import { MUser } from '../types/models' 12import { MRegistration, MUser } from '../types/models'
13import { JobQueue } from './job-queue' 13import { JobQueue } from './job-queue'
14 14
15const Email = require('email-templates') 15const Email = require('email-templates')
@@ -62,7 +62,9 @@ class Emailer {
62 subject: 'Reset your account password', 62 subject: 'Reset your account password',
63 locals: { 63 locals: {
64 username, 64 username,
65 resetPasswordUrl 65 resetPasswordUrl,
66
67 hideNotificationPreferencesLink: true
66 } 68 }
67 } 69 }
68 70
@@ -76,21 +78,33 @@ class Emailer {
76 subject: 'Create your account password', 78 subject: 'Create your account password',
77 locals: { 79 locals: {
78 username, 80 username,
79 createPasswordUrl 81 createPasswordUrl,
82
83 hideNotificationPreferencesLink: true
80 } 84 }
81 } 85 }
82 86
83 return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload }) 87 return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
84 } 88 }
85 89
86 addVerifyEmailJob (username: string, to: string, verifyEmailUrl: string) { 90 addVerifyEmailJob (options: {
91 username: string
92 isRegistrationRequest: boolean
93 to: string
94 verifyEmailUrl: string
95 }) {
96 const { username, isRegistrationRequest, to, verifyEmailUrl } = options
97
87 const emailPayload: EmailPayload = { 98 const emailPayload: EmailPayload = {
88 template: 'verify-email', 99 template: 'verify-email',
89 to: [ to ], 100 to: [ to ],
90 subject: `Verify your email on ${CONFIG.INSTANCE.NAME}`, 101 subject: `Verify your email on ${CONFIG.INSTANCE.NAME}`,
91 locals: { 102 locals: {
92 username, 103 username,
93 verifyEmailUrl 104 verifyEmailUrl,
105 isRegistrationRequest,
106
107 hideNotificationPreferencesLink: true
94 } 108 }
95 } 109 }
96 110
@@ -123,7 +137,33 @@ class Emailer {
123 body, 137 body,
124 138
125 // There are not notification preferences for the contact form 139 // There are not notification preferences for the contact form
126 hideNotificationPreferences: true 140 hideNotificationPreferencesLink: true
141 }
142 }
143
144 return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
145 }
146
147 addUserRegistrationRequestProcessedJob (registration: MRegistration) {
148 let template: string
149 let subject: string
150 if (registration.state === UserRegistrationState.ACCEPTED) {
151 template = 'user-registration-request-accepted'
152 subject = `Your registration request for ${registration.username} has been accepted`
153 } else {
154 template = 'user-registration-request-rejected'
155 subject = `Your registration request for ${registration.username} has been rejected`
156 }
157
158 const to = registration.email
159 const emailPayload: EmailPayload = {
160 to: [ to ],
161 template,
162 subject,
163 locals: {
164 username: registration.username,
165 moderationResponse: registration.moderationResponse,
166 loginLink: WEBSERVER.URL + '/login'
127 } 167 }
128 } 168 }
129 169
diff --git a/server/lib/emails/common/base.pug b/server/lib/emails/common/base.pug
index 6da5648e4..41e94564d 100644
--- a/server/lib/emails/common/base.pug
+++ b/server/lib/emails/common/base.pug
@@ -222,19 +222,9 @@ body(width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule:
222 td(aria-hidden='true' height='20' style='font-size: 0px; line-height: 0px;') 222 td(aria-hidden='true' height='20' style='font-size: 0px; line-height: 0px;')
223 br 223 br
224 //- Clear Spacer : END 224 //- Clear Spacer : END
225 //- 1 Column Text : BEGIN
226 if username
227 tr
228 td(style='background-color: #cccccc;')
229 table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%')
230 tr
231 td(style='padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;')
232 p(style='margin: 0;')
233 | You are receiving this email as part of your notification settings on #{instanceName} for your account #{username}.
234 //- 1 Column Text : END
235 //- Email Body : END 225 //- Email Body : END
236 //- Email Footer : BEGIN 226 //- Email Footer : BEGIN
237 unless hideNotificationPreferences 227 unless hideNotificationPreferencesLink
238 table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;') 228 table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;')
239 tr 229 tr
240 td(style='padding: 20px; padding-bottom: 0px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;') 230 td(style='padding: 20px; padding-bottom: 0px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;')
diff --git a/server/lib/emails/user-registration-request-accepted/html.pug b/server/lib/emails/user-registration-request-accepted/html.pug
new file mode 100644
index 000000000..7a52c3fe1
--- /dev/null
+++ b/server/lib/emails/user-registration-request-accepted/html.pug
@@ -0,0 +1,10 @@
1extends ../common/greetings
2
3block title
4 | Congratulation #{username}, your registration request has been accepted!
5
6block content
7 p Your registration request has been accepted.
8 p Moderators sent you the following message:
9 blockquote(style='white-space: pre-wrap') #{moderationResponse}
10 p Your account has been created and you can login on #[a(href=loginLink) #{loginLink}]
diff --git a/server/lib/emails/user-registration-request-rejected/html.pug b/server/lib/emails/user-registration-request-rejected/html.pug
new file mode 100644
index 000000000..ec0aa8dfe
--- /dev/null
+++ b/server/lib/emails/user-registration-request-rejected/html.pug
@@ -0,0 +1,9 @@
1extends ../common/greetings
2
3block title
4 | Registration request of your account #{username} has rejected
5
6block content
7 p Your registration request has been rejected.
8 p Moderators sent you the following message:
9 blockquote(style='white-space: pre-wrap') #{moderationResponse}
diff --git a/server/lib/emails/user-registration-request/html.pug b/server/lib/emails/user-registration-request/html.pug
new file mode 100644
index 000000000..64898f3f2
--- /dev/null
+++ b/server/lib/emails/user-registration-request/html.pug
@@ -0,0 +1,9 @@
1extends ../common/greetings
2
3block title
4 | A new user wants to register
5
6block content
7 p User #{registration.username} wants to register on your PeerTube instance with the following reason:
8 blockquote(style='white-space: pre-wrap') #{registration.registrationReason}
9 p You can accept or reject the registration request in the #[a(href=`${WEBSERVER.URL}/admin/moderation/registrations/list`) administration].
diff --git a/server/lib/emails/verify-email/html.pug b/server/lib/emails/verify-email/html.pug
index be9dde21b..19ef65f75 100644
--- a/server/lib/emails/verify-email/html.pug
+++ b/server/lib/emails/verify-email/html.pug
@@ -1,17 +1,19 @@
1extends ../common/greetings 1extends ../common/greetings
2 2
3block title 3block title
4 | Account verification 4 | Email verification
5 5
6block content 6block content
7 p Welcome to #{instanceName}! 7 if isRegistrationRequest
8 p. 8 p You just requested an account on #[a(href=WEBSERVER.URL) #{instanceName}].
9 You just created an account at #[a(href=WEBSERVER.URL) #{instanceName}]. 9 else
10 Your username there is: #{username}. 10 p You just created an account on #[a(href=WEBSERVER.URL) #{instanceName}].
11 p. 11
12 To start using your account you must verify your email first! 12 if isRegistrationRequest
13 Please follow #[a(href=verifyEmailUrl) this link] to verify this email belongs to you. 13 p To complete your registration request you must verify your email first!
14 p. 14 else
15 If you can't see the verification link above you can use the following link #[a(href=verifyEmailUrl) #{verifyEmailUrl}] 15 p To start using your account you must verify your email first!
16 p. 16
17 If you are not the person who initiated this request, please ignore this email. 17 p Please follow #[a(href=verifyEmailUrl) this link] to verify this email belongs to you.
18 p If you can't see the verification link above you can use the following link #[a(href=verifyEmailUrl) #{verifyEmailUrl}]
19 p If you are not the person who initiated this request, please ignore this email.
diff --git a/server/lib/notifier/notifier.ts b/server/lib/notifier/notifier.ts
index 66cfc31c4..920c55df0 100644
--- a/server/lib/notifier/notifier.ts
+++ b/server/lib/notifier/notifier.ts
@@ -1,4 +1,4 @@
1import { MUser, MUserDefault } from '@server/types/models/user' 1import { MRegistration, MUser, MUserDefault } from '@server/types/models/user'
2import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' 2import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
3import { UserNotificationSettingValue } from '../../../shared/models/users' 3import { UserNotificationSettingValue } from '../../../shared/models/users'
4import { logger } from '../../helpers/logger' 4import { logger } from '../../helpers/logger'
@@ -13,6 +13,7 @@ import {
13 AbuseStateChangeForReporter, 13 AbuseStateChangeForReporter,
14 AutoFollowForInstance, 14 AutoFollowForInstance,
15 CommentMention, 15 CommentMention,
16 DirectRegistrationForModerators,
16 FollowForInstance, 17 FollowForInstance,
17 FollowForUser, 18 FollowForUser,
18 ImportFinishedForOwner, 19 ImportFinishedForOwner,
@@ -30,7 +31,7 @@ import {
30 OwnedPublicationAfterAutoUnblacklist, 31 OwnedPublicationAfterAutoUnblacklist,
31 OwnedPublicationAfterScheduleUpdate, 32 OwnedPublicationAfterScheduleUpdate,
32 OwnedPublicationAfterTranscoding, 33 OwnedPublicationAfterTranscoding,
33 RegistrationForModerators, 34 RegistrationRequestForModerators,
34 StudioEditionFinishedForOwner, 35 StudioEditionFinishedForOwner,
35 UnblacklistForOwner 36 UnblacklistForOwner
36} from './shared' 37} from './shared'
@@ -47,7 +48,8 @@ class Notifier {
47 newBlacklist: [ NewBlacklistForOwner ], 48 newBlacklist: [ NewBlacklistForOwner ],
48 unblacklist: [ UnblacklistForOwner ], 49 unblacklist: [ UnblacklistForOwner ],
49 importFinished: [ ImportFinishedForOwner ], 50 importFinished: [ ImportFinishedForOwner ],
50 userRegistration: [ RegistrationForModerators ], 51 directRegistration: [ DirectRegistrationForModerators ],
52 registrationRequest: [ RegistrationRequestForModerators ],
51 userFollow: [ FollowForUser ], 53 userFollow: [ FollowForUser ],
52 instanceFollow: [ FollowForInstance ], 54 instanceFollow: [ FollowForInstance ],
53 autoInstanceFollow: [ AutoFollowForInstance ], 55 autoInstanceFollow: [ AutoFollowForInstance ],
@@ -138,13 +140,20 @@ class Notifier {
138 }) 140 })
139 } 141 }
140 142
141 notifyOnNewUserRegistration (user: MUserDefault): void { 143 notifyOnNewDirectRegistration (user: MUserDefault): void {
142 const models = this.notificationModels.userRegistration 144 const models = this.notificationModels.directRegistration
143 145
144 this.sendNotifications(models, user) 146 this.sendNotifications(models, user)
145 .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err })) 147 .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err }))
146 } 148 }
147 149
150 notifyOnNewRegistrationRequest (registration: MRegistration): void {
151 const models = this.notificationModels.registrationRequest
152
153 this.sendNotifications(models, registration)
154 .catch(err => logger.error('Cannot notify moderators of new registration request (%s).', registration.username, { err }))
155 }
156
148 notifyOfNewUserFollow (actorFollow: MActorFollowFull): void { 157 notifyOfNewUserFollow (actorFollow: MActorFollowFull): void {
149 const models = this.notificationModels.userFollow 158 const models = this.notificationModels.userFollow
150 159
diff --git a/server/lib/notifier/shared/instance/registration-for-moderators.ts b/server/lib/notifier/shared/instance/direct-registration-for-moderators.ts
index e92467424..5044f2068 100644
--- a/server/lib/notifier/shared/instance/registration-for-moderators.ts
+++ b/server/lib/notifier/shared/instance/direct-registration-for-moderators.ts
@@ -6,7 +6,7 @@ import { MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi
6import { UserNotificationType, UserRight } from '@shared/models' 6import { UserNotificationType, UserRight } from '@shared/models'
7import { AbstractNotification } from '../common/abstract-notification' 7import { AbstractNotification } from '../common/abstract-notification'
8 8
9export class RegistrationForModerators extends AbstractNotification <MUserDefault> { 9export class DirectRegistrationForModerators extends AbstractNotification <MUserDefault> {
10 private moderators: MUserDefault[] 10 private moderators: MUserDefault[]
11 11
12 async prepare () { 12 async prepare () {
@@ -40,7 +40,7 @@ export class RegistrationForModerators extends AbstractNotification <MUserDefaul
40 return { 40 return {
41 template: 'user-registered', 41 template: 'user-registered',
42 to, 42 to,
43 subject: `a new user registered on ${CONFIG.INSTANCE.NAME}: ${this.payload.username}`, 43 subject: `A new user registered on ${CONFIG.INSTANCE.NAME}: ${this.payload.username}`,
44 locals: { 44 locals: {
45 user: this.payload 45 user: this.payload
46 } 46 }
diff --git a/server/lib/notifier/shared/instance/index.ts b/server/lib/notifier/shared/instance/index.ts
index c3bb22aec..8c75a8ee9 100644
--- a/server/lib/notifier/shared/instance/index.ts
+++ b/server/lib/notifier/shared/instance/index.ts
@@ -1,3 +1,4 @@
1export * from './new-peertube-version-for-admins' 1export * from './new-peertube-version-for-admins'
2export * from './new-plugin-version-for-admins' 2export * from './new-plugin-version-for-admins'
3export * from './registration-for-moderators' 3export * from './direct-registration-for-moderators'
4export * from './registration-request-for-moderators'
diff --git a/server/lib/notifier/shared/instance/registration-request-for-moderators.ts b/server/lib/notifier/shared/instance/registration-request-for-moderators.ts
new file mode 100644
index 000000000..79920245a
--- /dev/null
+++ b/server/lib/notifier/shared/instance/registration-request-for-moderators.ts
@@ -0,0 +1,48 @@
1import { logger } from '@server/helpers/logger'
2import { UserModel } from '@server/models/user/user'
3import { UserNotificationModel } from '@server/models/user/user-notification'
4import { MRegistration, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models'
5import { UserNotificationType, UserRight } from '@shared/models'
6import { AbstractNotification } from '../common/abstract-notification'
7
8export class RegistrationRequestForModerators extends AbstractNotification <MRegistration> {
9 private moderators: MUserDefault[]
10
11 async prepare () {
12 this.moderators = await UserModel.listWithRight(UserRight.MANAGE_REGISTRATIONS)
13 }
14
15 log () {
16 logger.info('Notifying %s moderators of new user registration request of %s.', this.moderators.length, this.payload.username)
17 }
18
19 getSetting (user: MUserWithNotificationSetting) {
20 return user.NotificationSetting.newUserRegistration
21 }
22
23 getTargetUsers () {
24 return this.moderators
25 }
26
27 createNotification (user: MUserWithNotificationSetting) {
28 const notification = UserNotificationModel.build<UserNotificationModelForApi>({
29 type: UserNotificationType.NEW_USER_REGISTRATION_REQUEST,
30 userId: user.id,
31 userRegistrationId: this.payload.id
32 })
33 notification.UserRegistration = this.payload
34
35 return notification
36 }
37
38 createEmail (to: string) {
39 return {
40 template: 'user-registration-request',
41 to,
42 subject: `A new user wants to register: ${this.payload.username}`,
43 locals: {
44 registration: this.payload
45 }
46 }
47 }
48}
diff --git a/server/lib/redis.ts b/server/lib/redis.ts
index 451ddd0b6..3706d2228 100644
--- a/server/lib/redis.ts
+++ b/server/lib/redis.ts
@@ -9,7 +9,7 @@ import {
9 CONTACT_FORM_LIFETIME, 9 CONTACT_FORM_LIFETIME,
10 RESUMABLE_UPLOAD_SESSION_LIFETIME, 10 RESUMABLE_UPLOAD_SESSION_LIFETIME,
11 TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME, 11 TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME,
12 USER_EMAIL_VERIFY_LIFETIME, 12 EMAIL_VERIFY_LIFETIME,
13 USER_PASSWORD_CREATE_LIFETIME, 13 USER_PASSWORD_CREATE_LIFETIME,
14 USER_PASSWORD_RESET_LIFETIME, 14 USER_PASSWORD_RESET_LIFETIME,
15 VIEW_LIFETIME, 15 VIEW_LIFETIME,
@@ -124,16 +124,28 @@ class Redis {
124 124
125 /* ************ Email verification ************ */ 125 /* ************ Email verification ************ */
126 126
127 async setVerifyEmailVerificationString (userId: number) { 127 async setUserVerifyEmailVerificationString (userId: number) {
128 const generatedString = await generateRandomString(32) 128 const generatedString = await generateRandomString(32)
129 129
130 await this.setValue(this.generateVerifyEmailKey(userId), generatedString, USER_EMAIL_VERIFY_LIFETIME) 130 await this.setValue(this.generateUserVerifyEmailKey(userId), generatedString, EMAIL_VERIFY_LIFETIME)
131 131
132 return generatedString 132 return generatedString
133 } 133 }
134 134
135 async getVerifyEmailLink (userId: number) { 135 async getUserVerifyEmailLink (userId: number) {
136 return this.getValue(this.generateVerifyEmailKey(userId)) 136 return this.getValue(this.generateUserVerifyEmailKey(userId))
137 }
138
139 async setRegistrationVerifyEmailVerificationString (registrationId: number) {
140 const generatedString = await generateRandomString(32)
141
142 await this.setValue(this.generateRegistrationVerifyEmailKey(registrationId), generatedString, EMAIL_VERIFY_LIFETIME)
143
144 return generatedString
145 }
146
147 async getRegistrationVerifyEmailLink (registrationId: number) {
148 return this.getValue(this.generateRegistrationVerifyEmailKey(registrationId))
137 } 149 }
138 150
139 /* ************ Contact form per IP ************ */ 151 /* ************ Contact form per IP ************ */
@@ -346,8 +358,12 @@ class Redis {
346 return 'two-factor-request-' + userId + '-' + token 358 return 'two-factor-request-' + userId + '-' + token
347 } 359 }
348 360
349 private generateVerifyEmailKey (userId: number) { 361 private generateUserVerifyEmailKey (userId: number) {
350 return 'verify-email-' + userId 362 return 'verify-email-user-' + userId
363 }
364
365 private generateRegistrationVerifyEmailKey (registrationId: number) {
366 return 'verify-email-registration-' + registrationId
351 } 367 }
352 368
353 private generateIPViewKey (ip: string, videoUUID: string) { 369 private generateIPViewKey (ip: string, videoUUID: string) {
diff --git a/server/lib/server-config-manager.ts b/server/lib/server-config-manager.ts
index 78a9546ae..e87e2854f 100644
--- a/server/lib/server-config-manager.ts
+++ b/server/lib/server-config-manager.ts
@@ -261,10 +261,17 @@ class ServerConfigManager {
261 async getServerConfig (ip?: string): Promise<ServerConfig> { 261 async getServerConfig (ip?: string): Promise<ServerConfig> {
262 const { allowed } = await Hooks.wrapPromiseFun( 262 const { allowed } = await Hooks.wrapPromiseFun(
263 isSignupAllowed, 263 isSignupAllowed,
264
264 { 265 {
265 ip 266 ip,
267 signupMode: CONFIG.SIGNUP.REQUIRES_APPROVAL
268 ? 'request-registration'
269 : 'direct-registration'
266 }, 270 },
267 'filter:api.user.signup.allowed.result' 271
272 CONFIG.SIGNUP.REQUIRES_APPROVAL
273 ? 'filter:api.user.request-signup.allowed.result'
274 : 'filter:api.user.signup.allowed.result'
268 ) 275 )
269 276
270 const allowedForCurrentIP = isSignupAllowedForCurrentIP(ip) 277 const allowedForCurrentIP = isSignupAllowedForCurrentIP(ip)
@@ -273,6 +280,7 @@ class ServerConfigManager {
273 allowed, 280 allowed,
274 allowedForCurrentIP, 281 allowedForCurrentIP,
275 minimumAge: CONFIG.SIGNUP.MINIMUM_AGE, 282 minimumAge: CONFIG.SIGNUP.MINIMUM_AGE,
283 requiresApproval: CONFIG.SIGNUP.REQUIRES_APPROVAL,
276 requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION 284 requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
277 } 285 }
278 286
diff --git a/server/lib/signup.ts b/server/lib/signup.ts
index f094531eb..f19232621 100644
--- a/server/lib/signup.ts
+++ b/server/lib/signup.ts
@@ -4,11 +4,24 @@ import { UserModel } from '../models/user/user'
4 4
5const isCidr = require('is-cidr') 5const isCidr = require('is-cidr')
6 6
7async function isSignupAllowed (): Promise<{ allowed: boolean, errorMessage?: string }> { 7export type SignupMode = 'direct-registration' | 'request-registration'
8
9async function isSignupAllowed (options: {
10 signupMode: SignupMode
11
12 ip: string // For plugins
13 body?: any
14}): Promise<{ allowed: boolean, errorMessage?: string }> {
15 const { signupMode } = options
16
8 if (CONFIG.SIGNUP.ENABLED === false) { 17 if (CONFIG.SIGNUP.ENABLED === false) {
9 return { allowed: false } 18 return { allowed: false }
10 } 19 }
11 20
21 if (signupMode === 'direct-registration' && CONFIG.SIGNUP.REQUIRES_APPROVAL === true) {
22 return { allowed: false }
23 }
24
12 // No limit and signup is enabled 25 // No limit and signup is enabled
13 if (CONFIG.SIGNUP.LIMIT === -1) { 26 if (CONFIG.SIGNUP.LIMIT === -1) {
14 return { allowed: true } 27 return { allowed: true }
diff --git a/server/lib/user.ts b/server/lib/user.ts
index 2e433da04..ffb57944a 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -10,7 +10,7 @@ import { sequelizeTypescript } from '../initializers/database'
10import { AccountModel } from '../models/account/account' 10import { AccountModel } from '../models/account/account'
11import { UserNotificationSettingModel } from '../models/user/user-notification-setting' 11import { UserNotificationSettingModel } from '../models/user/user-notification-setting'
12import { MAccountDefault, MChannelActor } from '../types/models' 12import { MAccountDefault, MChannelActor } from '../types/models'
13import { MUser, MUserDefault, MUserId } from '../types/models/user' 13import { MRegistration, MUser, MUserDefault, MUserId } from '../types/models/user'
14import { generateAndSaveActorKeys } from './activitypub/actors' 14import { generateAndSaveActorKeys } from './activitypub/actors'
15import { getLocalAccountActivityPubUrl } from './activitypub/url' 15import { getLocalAccountActivityPubUrl } from './activitypub/url'
16import { Emailer } from './emailer' 16import { Emailer } from './emailer'
@@ -97,7 +97,7 @@ async function createUserAccountAndChannelAndPlaylist (parameters: {
97 }) 97 })
98 userCreated.Account = accountCreated 98 userCreated.Account = accountCreated
99 99
100 const channelAttributes = await buildChannelAttributes(userCreated, t, channelNames) 100 const channelAttributes = await buildChannelAttributes({ user: userCreated, transaction: t, channelNames })
101 const videoChannel = await createLocalVideoChannel(channelAttributes, accountCreated, t) 101 const videoChannel = await createLocalVideoChannel(channelAttributes, accountCreated, t)
102 102
103 const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t) 103 const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t)
@@ -160,15 +160,28 @@ async function createApplicationActor (applicationId: number) {
160// --------------------------------------------------------------------------- 160// ---------------------------------------------------------------------------
161 161
162async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) { 162async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) {
163 const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id) 163 const verificationString = await Redis.Instance.setUserVerifyEmailVerificationString(user.id)
164 let url = WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString 164 let verifyEmailUrl = `${WEBSERVER.URL}/verify-account/email?userId=${user.id}&verificationString=${verificationString}`
165 165
166 if (isPendingEmail) url += '&isPendingEmail=true' 166 if (isPendingEmail) verifyEmailUrl += '&isPendingEmail=true'
167
168 const to = isPendingEmail
169 ? user.pendingEmail
170 : user.email
167 171
168 const email = isPendingEmail ? user.pendingEmail : user.email
169 const username = user.username 172 const username = user.username
170 173
171 Emailer.Instance.addVerifyEmailJob(username, email, url) 174 Emailer.Instance.addVerifyEmailJob({ username, to, verifyEmailUrl, isRegistrationRequest: false })
175}
176
177async function sendVerifyRegistrationEmail (registration: MRegistration) {
178 const verificationString = await Redis.Instance.setRegistrationVerifyEmailVerificationString(registration.id)
179 const verifyEmailUrl = `${WEBSERVER.URL}/verify-account/email?registrationId=${registration.id}&verificationString=${verificationString}`
180
181 const to = registration.email
182 const username = registration.username
183
184 Emailer.Instance.addVerifyEmailJob({ username, to, verifyEmailUrl, isRegistrationRequest: true })
172} 185}
173 186
174// --------------------------------------------------------------------------- 187// ---------------------------------------------------------------------------
@@ -232,7 +245,10 @@ export {
232 createApplicationActor, 245 createApplicationActor,
233 createUserAccountAndChannelAndPlaylist, 246 createUserAccountAndChannelAndPlaylist,
234 createLocalAccountWithoutKeys, 247 createLocalAccountWithoutKeys,
248
235 sendVerifyUserEmail, 249 sendVerifyUserEmail,
250 sendVerifyRegistrationEmail,
251
236 isAbleToUploadVideo, 252 isAbleToUploadVideo,
237 buildUser 253 buildUser
238} 254}
@@ -264,7 +280,13 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction |
264 return UserNotificationSettingModel.create(values, { transaction: t }) 280 return UserNotificationSettingModel.create(values, { transaction: t })
265} 281}
266 282
267async function buildChannelAttributes (user: MUser, transaction?: Transaction, channelNames?: ChannelNames) { 283async function buildChannelAttributes (options: {
284 user: MUser
285 transaction?: Transaction
286 channelNames?: ChannelNames
287}) {
288 const { user, transaction, channelNames } = options
289
268 if (channelNames) return channelNames 290 if (channelNames) return channelNames
269 291
270 const channelName = await findAvailableLocalActorName(user.username + '_channel', transaction) 292 const channelName = await findAvailableLocalActorName(user.username + '_channel', transaction)