diff options
author | Chocobozzz <me@florianbigard.com> | 2019-06-11 11:54:33 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-06-11 14:31:11 +0200 |
commit | d1ab89deb79f70c439b58750d044d9cadf1194e5 (patch) | |
tree | 3cd18a0a8a3bf7497aec9d803c759bf54656115c | |
parent | fff77ba23191f9bca959d5989a1f1df331dbac0b (diff) | |
download | PeerTube-d1ab89deb79f70c439b58750d044d9cadf1194e5.tar.gz PeerTube-d1ab89deb79f70c439b58750d044d9cadf1194e5.tar.zst PeerTube-d1ab89deb79f70c439b58750d044d9cadf1194e5.zip |
Handle email update on server
-rw-r--r-- | .github/FUNDING.yml | 1 | ||||
-rw-r--r-- | server/controllers/api/users/index.ts | 18 | ||||
-rw-r--r-- | server/controllers/api/users/me.ts | 16 | ||||
-rw-r--r-- | server/initializers/constants.ts | 2 | ||||
-rw-r--r-- | server/initializers/migrations/0390-user-pending-email.ts | 25 | ||||
-rw-r--r-- | server/lib/user.ts | 18 | ||||
-rw-r--r-- | server/middlewares/validators/users.ts | 39 | ||||
-rw-r--r-- | server/models/account/user.ts | 6 | ||||
-rw-r--r-- | server/tests/api/server/email.ts | 2 | ||||
-rw-r--r-- | server/tests/api/users/users-verification.ts | 59 | ||||
-rw-r--r-- | shared/extra-utils/users/users.ts | 7 | ||||
-rw-r--r-- | shared/models/users/user.model.ts | 1 |
12 files changed, 164 insertions, 30 deletions
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..cece65761 --- /dev/null +++ b/.github/FUNDING.yml | |||
@@ -0,0 +1 @@ | |||
custom: https://framasoft.org/en/#soutenir | |||
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index 99f51a648..c1d72087c 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -6,7 +6,7 @@ import { getFormattedObjects } from '../../../helpers/utils' | |||
6 | import { RATES_LIMIT, WEBSERVER } from '../../../initializers/constants' | 6 | import { RATES_LIMIT, WEBSERVER } from '../../../initializers/constants' |
7 | import { Emailer } from '../../../lib/emailer' | 7 | import { Emailer } from '../../../lib/emailer' |
8 | import { Redis } from '../../../lib/redis' | 8 | import { Redis } from '../../../lib/redis' |
9 | import { createUserAccountAndChannelAndPlaylist } from '../../../lib/user' | 9 | import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' |
10 | import { | 10 | import { |
11 | asyncMiddleware, | 11 | asyncMiddleware, |
12 | asyncRetryTransactionMiddleware, | 12 | asyncRetryTransactionMiddleware, |
@@ -147,7 +147,7 @@ usersRouter.post('/:id/reset-password', | |||
147 | usersRouter.post('/ask-send-verify-email', | 147 | usersRouter.post('/ask-send-verify-email', |
148 | askSendEmailLimiter, | 148 | askSendEmailLimiter, |
149 | asyncMiddleware(usersAskSendVerifyEmailValidator), | 149 | asyncMiddleware(usersAskSendVerifyEmailValidator), |
150 | asyncMiddleware(askSendVerifyUserEmail) | 150 | asyncMiddleware(reSendVerifyUserEmail) |
151 | ) | 151 | ) |
152 | 152 | ||
153 | usersRouter.post('/:id/verify-email', | 153 | usersRouter.post('/:id/verify-email', |
@@ -320,14 +320,7 @@ async function resetUserPassword (req: express.Request, res: express.Response) { | |||
320 | return res.status(204).end() | 320 | return res.status(204).end() |
321 | } | 321 | } |
322 | 322 | ||
323 | async function sendVerifyUserEmail (user: UserModel) { | 323 | async function reSendVerifyUserEmail (req: express.Request, res: express.Response) { |
324 | const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id) | ||
325 | const url = WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString | ||
326 | await Emailer.Instance.addVerifyEmailJob(user.email, url) | ||
327 | return | ||
328 | } | ||
329 | |||
330 | async function askSendVerifyUserEmail (req: express.Request, res: express.Response) { | ||
331 | const user = res.locals.user | 324 | const user = res.locals.user |
332 | 325 | ||
333 | await sendVerifyUserEmail(user) | 326 | await sendVerifyUserEmail(user) |
@@ -339,6 +332,11 @@ async function verifyUserEmail (req: express.Request, res: express.Response) { | |||
339 | const user = res.locals.user | 332 | const user = res.locals.user |
340 | user.emailVerified = true | 333 | user.emailVerified = true |
341 | 334 | ||
335 | if (req.body.isPendingEmail === true) { | ||
336 | user.email = user.pendingEmail | ||
337 | user.pendingEmail = null | ||
338 | } | ||
339 | |||
342 | await user.save() | 340 | await user.save() |
343 | 341 | ||
344 | return res.status(204).end() | 342 | return res.status(204).end() |
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index ddb239e7b..1750a02e9 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts | |||
@@ -28,6 +28,7 @@ import { VideoImportModel } from '../../../models/video/video-import' | |||
28 | import { AccountModel } from '../../../models/account/account' | 28 | import { AccountModel } from '../../../models/account/account' |
29 | import { CONFIG } from '../../../initializers/config' | 29 | import { CONFIG } from '../../../initializers/config' |
30 | import { sequelizeTypescript } from '../../../initializers/database' | 30 | import { sequelizeTypescript } from '../../../initializers/database' |
31 | import { sendVerifyUserEmail } from '../../../lib/user' | ||
31 | 32 | ||
32 | const auditLogger = auditLoggerFactory('users-me') | 33 | const auditLogger = auditLoggerFactory('users-me') |
33 | 34 | ||
@@ -171,17 +172,26 @@ async function deleteMe (req: express.Request, res: express.Response) { | |||
171 | 172 | ||
172 | async function updateMe (req: express.Request, res: express.Response) { | 173 | async function updateMe (req: express.Request, res: express.Response) { |
173 | const body: UserUpdateMe = req.body | 174 | const body: UserUpdateMe = req.body |
175 | let sendVerificationEmail = false | ||
174 | 176 | ||
175 | const user = res.locals.oauth.token.user | 177 | const user = res.locals.oauth.token.user |
176 | const oldUserAuditView = new UserAuditView(user.toFormattedJSON({})) | 178 | const oldUserAuditView = new UserAuditView(user.toFormattedJSON({})) |
177 | 179 | ||
178 | if (body.password !== undefined) user.password = body.password | 180 | if (body.password !== undefined) user.password = body.password |
179 | if (body.email !== undefined) user.email = body.email | ||
180 | if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy | 181 | if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy |
181 | if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled | 182 | if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled |
182 | if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo | 183 | if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo |
183 | if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled | 184 | if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled |
184 | 185 | ||
186 | if (body.email !== undefined) { | ||
187 | if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { | ||
188 | user.pendingEmail = body.email | ||
189 | sendVerificationEmail = true | ||
190 | } else { | ||
191 | user.email = body.email | ||
192 | } | ||
193 | } | ||
194 | |||
185 | await sequelizeTypescript.transaction(async t => { | 195 | await sequelizeTypescript.transaction(async t => { |
186 | const userAccount = await AccountModel.load(user.Account.id) | 196 | const userAccount = await AccountModel.load(user.Account.id) |
187 | 197 | ||
@@ -196,6 +206,10 @@ async function updateMe (req: express.Request, res: express.Response) { | |||
196 | auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON({})), oldUserAuditView) | 206 | auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON({})), oldUserAuditView) |
197 | }) | 207 | }) |
198 | 208 | ||
209 | if (sendVerificationEmail === true) { | ||
210 | await sendVerifyUserEmail(user, true) | ||
211 | } | ||
212 | |||
199 | return res.sendStatus(204) | 213 | return res.sendStatus(204) |
200 | } | 214 | } |
201 | 215 | ||
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index be30be463..c2b8eff95 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' | |||
14 | 14 | ||
15 | // --------------------------------------------------------------------------- | 15 | // --------------------------------------------------------------------------- |
16 | 16 | ||
17 | const LAST_MIGRATION_VERSION = 385 | 17 | const LAST_MIGRATION_VERSION = 390 |
18 | 18 | ||
19 | // --------------------------------------------------------------------------- | 19 | // --------------------------------------------------------------------------- |
20 | 20 | ||
diff --git a/server/initializers/migrations/0390-user-pending-email.ts b/server/initializers/migrations/0390-user-pending-email.ts new file mode 100644 index 000000000..5ca871746 --- /dev/null +++ b/server/initializers/migrations/0390-user-pending-email.ts | |||
@@ -0,0 +1,25 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize, | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | const data = { | ||
10 | type: Sequelize.STRING(400), | ||
11 | allowNull: true, | ||
12 | defaultValue: null | ||
13 | } | ||
14 | |||
15 | await utils.queryInterface.addColumn('user', 'pendingEmail', data) | ||
16 | } | ||
17 | |||
18 | function down (options) { | ||
19 | throw new Error('Not implemented.') | ||
20 | } | ||
21 | |||
22 | export { | ||
23 | up, | ||
24 | down | ||
25 | } | ||
diff --git a/server/lib/user.ts b/server/lib/user.ts index b50b09d72..0e4007770 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as uuidv4 from 'uuid/v4' | 1 | import * as uuidv4 from 'uuid/v4' |
2 | import { ActivityPubActorType } from '../../shared/models/activitypub' | 2 | import { ActivityPubActorType } from '../../shared/models/activitypub' |
3 | import { SERVER_ACTOR_NAME } from '../initializers/constants' | 3 | import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants' |
4 | import { AccountModel } from '../models/account/account' | 4 | import { AccountModel } from '../models/account/account' |
5 | import { UserModel } from '../models/account/user' | 5 | import { UserModel } from '../models/account/user' |
6 | import { buildActorInstance, getAccountActivityPubUrl, setAsyncActorKeys } from './activitypub' | 6 | import { buildActorInstance, getAccountActivityPubUrl, setAsyncActorKeys } from './activitypub' |
@@ -12,6 +12,8 @@ import { UserNotificationSetting, UserNotificationSettingValue } from '../../sha | |||
12 | import { createWatchLaterPlaylist } from './video-playlist' | 12 | import { createWatchLaterPlaylist } from './video-playlist' |
13 | import { sequelizeTypescript } from '../initializers/database' | 13 | import { sequelizeTypescript } from '../initializers/database' |
14 | import { Transaction } from 'sequelize/types' | 14 | import { Transaction } from 'sequelize/types' |
15 | import { Redis } from './redis' | ||
16 | import { Emailer } from './emailer' | ||
15 | 17 | ||
16 | type ChannelNames = { name: string, displayName: string } | 18 | type ChannelNames = { name: string, displayName: string } |
17 | async function createUserAccountAndChannelAndPlaylist (parameters: { | 19 | async function createUserAccountAndChannelAndPlaylist (parameters: { |
@@ -100,12 +102,24 @@ async function createApplicationActor (applicationId: number) { | |||
100 | return accountCreated | 102 | return accountCreated |
101 | } | 103 | } |
102 | 104 | ||
105 | async function sendVerifyUserEmail (user: UserModel, isPendingEmail = false) { | ||
106 | const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id) | ||
107 | let url = WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString | ||
108 | |||
109 | if (isPendingEmail) url += '&isPendingEmail=true' | ||
110 | |||
111 | const email = isPendingEmail ? user.pendingEmail : user.email | ||
112 | |||
113 | await Emailer.Instance.addVerifyEmailJob(email, url) | ||
114 | } | ||
115 | |||
103 | // --------------------------------------------------------------------------- | 116 | // --------------------------------------------------------------------------- |
104 | 117 | ||
105 | export { | 118 | export { |
106 | createApplicationActor, | 119 | createApplicationActor, |
107 | createUserAccountAndChannelAndPlaylist, | 120 | createUserAccountAndChannelAndPlaylist, |
108 | createLocalAccountWithoutKeys | 121 | createLocalAccountWithoutKeys, |
122 | sendVerifyUserEmail | ||
109 | } | 123 | } |
110 | 124 | ||
111 | // --------------------------------------------------------------------------- | 125 | // --------------------------------------------------------------------------- |
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index b4e09c9b7..a4d4ae46d 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -27,7 +27,6 @@ import { areValidationErrors } from './utils' | |||
27 | import { ActorModel } from '../../models/activitypub/actor' | 27 | import { ActorModel } from '../../models/activitypub/actor' |
28 | import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor' | 28 | import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor' |
29 | import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels' | 29 | import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels' |
30 | import { UserCreate } from '../../../shared/models/users' | ||
31 | import { UserRegister } from '../../../shared/models/users/user-register.model' | 30 | import { UserRegister } from '../../../shared/models/users/user-register.model' |
32 | 31 | ||
33 | const usersAddValidator = [ | 32 | const usersAddValidator = [ |
@@ -178,13 +177,27 @@ const usersUpdateValidator = [ | |||
178 | ] | 177 | ] |
179 | 178 | ||
180 | const usersUpdateMeValidator = [ | 179 | const usersUpdateMeValidator = [ |
181 | body('displayName').optional().custom(isUserDisplayNameValid).withMessage('Should have a valid display name'), | 180 | body('displayName') |
182 | body('description').optional().custom(isUserDescriptionValid).withMessage('Should have a valid description'), | 181 | .optional() |
183 | body('currentPassword').optional().custom(isUserPasswordValid).withMessage('Should have a valid current password'), | 182 | .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'), |
184 | body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'), | 183 | body('description') |
185 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), | 184 | .optional() |
186 | body('nsfwPolicy').optional().custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'), | 185 | .custom(isUserDescriptionValid).withMessage('Should have a valid description'), |
187 | body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'), | 186 | body('currentPassword') |
187 | .optional() | ||
188 | .custom(isUserPasswordValid).withMessage('Should have a valid current password'), | ||
189 | body('password') | ||
190 | .optional() | ||
191 | .custom(isUserPasswordValid).withMessage('Should have a valid password'), | ||
192 | body('email') | ||
193 | .optional() | ||
194 | .isEmail().withMessage('Should have a valid email attribute'), | ||
195 | body('nsfwPolicy') | ||
196 | .optional() | ||
197 | .custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'), | ||
198 | body('autoPlayVideo') | ||
199 | .optional() | ||
200 | .custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'), | ||
188 | body('videosHistoryEnabled') | 201 | body('videosHistoryEnabled') |
189 | .optional() | 202 | .optional() |
190 | .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'), | 203 | .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'), |
@@ -329,8 +342,14 @@ const usersAskSendVerifyEmailValidator = [ | |||
329 | ] | 342 | ] |
330 | 343 | ||
331 | const usersVerifyEmailValidator = [ | 344 | const usersVerifyEmailValidator = [ |
332 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), | 345 | param('id') |
333 | body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'), | 346 | .isInt().not().isEmpty().withMessage('Should have a valid id'), |
347 | |||
348 | body('verificationString') | ||
349 | .not().isEmpty().withMessage('Should have a valid verification string'), | ||
350 | body('isPendingEmail') | ||
351 | .optional() | ||
352 | .toBoolean(), | ||
334 | 353 | ||
335 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 354 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
336 | logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params }) | 355 | logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params }) |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 4a9acd703..e75039521 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -114,6 +114,11 @@ export class UserModel extends Model<UserModel> { | |||
114 | email: string | 114 | email: string |
115 | 115 | ||
116 | @AllowNull(true) | 116 | @AllowNull(true) |
117 | @IsEmail | ||
118 | @Column(DataType.STRING(400)) | ||
119 | pendingEmail: string | ||
120 | |||
121 | @AllowNull(true) | ||
117 | @Default(null) | 122 | @Default(null) |
118 | @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true)) | 123 | @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true)) |
119 | @Column | 124 | @Column |
@@ -540,6 +545,7 @@ export class UserModel extends Model<UserModel> { | |||
540 | id: this.id, | 545 | id: this.id, |
541 | username: this.username, | 546 | username: this.username, |
542 | email: this.email, | 547 | email: this.email, |
548 | pendingEmail: this.pendingEmail, | ||
543 | emailVerified: this.emailVerified, | 549 | emailVerified: this.emailVerified, |
544 | nsfwPolicy: this.nsfwPolicy, | 550 | nsfwPolicy: this.nsfwPolicy, |
545 | webTorrentEnabled: this.webTorrentEnabled, | 551 | webTorrentEnabled: this.webTorrentEnabled, |
diff --git a/server/tests/api/server/email.ts b/server/tests/api/server/email.ts index 5929a3adb..7b7acfd12 100644 --- a/server/tests/api/server/email.ts +++ b/server/tests/api/server/email.ts | |||
@@ -250,7 +250,7 @@ describe('Test emails', function () { | |||
250 | }) | 250 | }) |
251 | 251 | ||
252 | it('Should not verify the email with an invalid verification string', async function () { | 252 | it('Should not verify the email with an invalid verification string', async function () { |
253 | await verifyEmail(server.url, userId, verificationString + 'b', 403) | 253 | await verifyEmail(server.url, userId, verificationString + 'b', false, 403) |
254 | }) | 254 | }) |
255 | 255 | ||
256 | it('Should verify the email', async function () { | 256 | it('Should verify the email', async function () { |
diff --git a/server/tests/api/users/users-verification.ts b/server/tests/api/users/users-verification.ts index 3b37a26cf..b8fa1430b 100644 --- a/server/tests/api/users/users-verification.ts +++ b/server/tests/api/users/users-verification.ts | |||
@@ -3,18 +3,29 @@ | |||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { | 5 | import { |
6 | registerUser, flushTests, getUserInformation, getMyUserInformation, killallServers, | 6 | cleanupTests, |
7 | userLogin, login, flushAndRunServer, ServerInfo, verifyEmail, updateCustomSubConfig, wait, cleanupTests | 7 | flushAndRunServer, |
8 | getMyUserInformation, | ||
9 | getUserInformation, | ||
10 | login, | ||
11 | registerUser, | ||
12 | ServerInfo, | ||
13 | updateCustomSubConfig, | ||
14 | updateMyUser, | ||
15 | userLogin, | ||
16 | verifyEmail | ||
8 | } from '../../../../shared/extra-utils' | 17 | } from '../../../../shared/extra-utils' |
9 | import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' | 18 | import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' |
10 | import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' | 19 | import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' |
11 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 20 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' |
21 | import { User } from '../../../../shared/models/users' | ||
12 | 22 | ||
13 | const expect = chai.expect | 23 | const expect = chai.expect |
14 | 24 | ||
15 | describe('Test users account verification', function () { | 25 | describe('Test users account verification', function () { |
16 | let server: ServerInfo | 26 | let server: ServerInfo |
17 | let userId: number | 27 | let userId: number |
28 | let userAccessToken: string | ||
18 | let verificationString: string | 29 | let verificationString: string |
19 | let expectedEmailsLength = 0 | 30 | let expectedEmailsLength = 0 |
20 | const user1 = { | 31 | const user1 = { |
@@ -83,11 +94,53 @@ describe('Test users account verification', function () { | |||
83 | 94 | ||
84 | it('Should verify the user via email and allow login', async function () { | 95 | it('Should verify the user via email and allow login', async function () { |
85 | await verifyEmail(server.url, userId, verificationString) | 96 | await verifyEmail(server.url, userId, verificationString) |
86 | await login(server.url, server.client, user1) | 97 | |
98 | const res = await login(server.url, server.client, user1) | ||
99 | userAccessToken = res.body.access_token | ||
100 | |||
87 | const resUserVerified = await getUserInformation(server.url, server.accessToken, userId) | 101 | const resUserVerified = await getUserInformation(server.url, server.accessToken, userId) |
88 | expect(resUserVerified.body.emailVerified).to.be.true | 102 | expect(resUserVerified.body.emailVerified).to.be.true |
89 | }) | 103 | }) |
90 | 104 | ||
105 | it('Should be able to change the user email', async function () { | ||
106 | let updateVerificationString: string | ||
107 | |||
108 | { | ||
109 | await updateMyUser({ | ||
110 | url: server.url, | ||
111 | accessToken: userAccessToken, | ||
112 | email: 'updated@example.com' | ||
113 | }) | ||
114 | |||
115 | await waitJobs(server) | ||
116 | expectedEmailsLength++ | ||
117 | expect(emails).to.have.lengthOf(expectedEmailsLength) | ||
118 | |||
119 | const email = emails[expectedEmailsLength - 1] | ||
120 | |||
121 | const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text']) | ||
122 | updateVerificationString = verificationStringMatches[1] | ||
123 | } | ||
124 | |||
125 | { | ||
126 | const res = await getMyUserInformation(server.url, userAccessToken) | ||
127 | const me: User = res.body | ||
128 | |||
129 | expect(me.email).to.equal('user_1@example.com') | ||
130 | expect(me.pendingEmail).to.equal('updated@example.com') | ||
131 | } | ||
132 | |||
133 | { | ||
134 | await verifyEmail(server.url, userId, updateVerificationString, true) | ||
135 | |||
136 | const res = await getMyUserInformation(server.url, userAccessToken) | ||
137 | const me: User = res.body | ||
138 | |||
139 | expect(me.email).to.equal('updated@example.com') | ||
140 | expect(me.pendingEmail).to.be.null | ||
141 | } | ||
142 | }) | ||
143 | |||
91 | it('Should register user not requiring email verification if setting not enabled', async function () { | 144 | it('Should register user not requiring email verification if setting not enabled', async function () { |
92 | this.timeout(5000) | 145 | this.timeout(5000) |
93 | await updateCustomSubConfig(server.url, server.accessToken, { | 146 | await updateCustomSubConfig(server.url, server.accessToken, { |
diff --git a/shared/extra-utils/users/users.ts b/shared/extra-utils/users/users.ts index c09211b71..1c39881d6 100644 --- a/shared/extra-utils/users/users.ts +++ b/shared/extra-utils/users/users.ts | |||
@@ -323,13 +323,16 @@ function askSendVerifyEmail (url: string, email: string) { | |||
323 | }) | 323 | }) |
324 | } | 324 | } |
325 | 325 | ||
326 | function verifyEmail (url: string, userId: number, verificationString: string, statusCodeExpected = 204) { | 326 | function verifyEmail (url: string, userId: number, verificationString: string, isPendingEmail = false, statusCodeExpected = 204) { |
327 | const path = '/api/v1/users/' + userId + '/verify-email' | 327 | const path = '/api/v1/users/' + userId + '/verify-email' |
328 | 328 | ||
329 | return makePostBodyRequest({ | 329 | return makePostBodyRequest({ |
330 | url, | 330 | url, |
331 | path, | 331 | path, |
332 | fields: { verificationString }, | 332 | fields: { |
333 | verificationString, | ||
334 | isPendingEmail | ||
335 | }, | ||
333 | statusCodeExpected | 336 | statusCodeExpected |
334 | }) | 337 | }) |
335 | } | 338 | } |
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index 2f6a3c719..b5823b47a 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts | |||
@@ -9,6 +9,7 @@ export interface User { | |||
9 | id: number | 9 | id: number |
10 | username: string | 10 | username: string |
11 | email: string | 11 | email: string |
12 | pendingEmail: string | null | ||
12 | emailVerified: boolean | 13 | emailVerified: boolean |
13 | nsfwPolicy: NSFWPolicyType | 14 | nsfwPolicy: NSFWPolicyType |
14 | 15 | ||