Handle email update on server

pull/1902/head
Chocobozzz 2019-06-11 11:54:33 +02:00
parent fff77ba231
commit d1ab89deb7
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
12 changed files with 164 additions and 30 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
custom: https://framasoft.org/en/#soutenir

View File

@ -6,7 +6,7 @@ import { getFormattedObjects } from '../../../helpers/utils'
import { RATES_LIMIT, WEBSERVER } from '../../../initializers/constants'
import { Emailer } from '../../../lib/emailer'
import { Redis } from '../../../lib/redis'
import { createUserAccountAndChannelAndPlaylist } from '../../../lib/user'
import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
@ -147,7 +147,7 @@ usersRouter.post('/:id/reset-password',
usersRouter.post('/ask-send-verify-email',
askSendEmailLimiter,
asyncMiddleware(usersAskSendVerifyEmailValidator),
asyncMiddleware(askSendVerifyUserEmail)
asyncMiddleware(reSendVerifyUserEmail)
)
usersRouter.post('/:id/verify-email',
@ -320,14 +320,7 @@ async function resetUserPassword (req: express.Request, res: express.Response) {
return res.status(204).end()
}
async function sendVerifyUserEmail (user: UserModel) {
const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id)
const url = WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString
await Emailer.Instance.addVerifyEmailJob(user.email, url)
return
}
async function askSendVerifyUserEmail (req: express.Request, res: express.Response) {
async function reSendVerifyUserEmail (req: express.Request, res: express.Response) {
const user = res.locals.user
await sendVerifyUserEmail(user)
@ -339,6 +332,11 @@ async function verifyUserEmail (req: express.Request, res: express.Response) {
const user = res.locals.user
user.emailVerified = true
if (req.body.isPendingEmail === true) {
user.email = user.pendingEmail
user.pendingEmail = null
}
await user.save()
return res.status(204).end()

View File

@ -28,6 +28,7 @@ import { VideoImportModel } from '../../../models/video/video-import'
import { AccountModel } from '../../../models/account/account'
import { CONFIG } from '../../../initializers/config'
import { sequelizeTypescript } from '../../../initializers/database'
import { sendVerifyUserEmail } from '../../../lib/user'
const auditLogger = auditLoggerFactory('users-me')
@ -171,17 +172,26 @@ async function deleteMe (req: express.Request, res: express.Response) {
async function updateMe (req: express.Request, res: express.Response) {
const body: UserUpdateMe = req.body
let sendVerificationEmail = false
const user = res.locals.oauth.token.user
const oldUserAuditView = new UserAuditView(user.toFormattedJSON({}))
if (body.password !== undefined) user.password = body.password
if (body.email !== undefined) user.email = body.email
if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy
if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled
if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled
if (body.email !== undefined) {
if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
user.pendingEmail = body.email
sendVerificationEmail = true
} else {
user.email = body.email
}
}
await sequelizeTypescript.transaction(async t => {
const userAccount = await AccountModel.load(user.Account.id)
@ -196,6 +206,10 @@ async function updateMe (req: express.Request, res: express.Response) {
auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON({})), oldUserAuditView)
})
if (sendVerificationEmail === true) {
await sendVerifyUserEmail(user, true)
}
return res.sendStatus(204)
}

View File

@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 385
const LAST_MIGRATION_VERSION = 390
// ---------------------------------------------------------------------------

View File

@ -0,0 +1,25 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction,
queryInterface: Sequelize.QueryInterface,
sequelize: Sequelize.Sequelize,
db: any
}): Promise<void> {
const data = {
type: Sequelize.STRING(400),
allowNull: true,
defaultValue: null
}
await utils.queryInterface.addColumn('user', 'pendingEmail', data)
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -1,6 +1,6 @@
import * as uuidv4 from 'uuid/v4'
import { ActivityPubActorType } from '../../shared/models/activitypub'
import { SERVER_ACTOR_NAME } from '../initializers/constants'
import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants'
import { AccountModel } from '../models/account/account'
import { UserModel } from '../models/account/user'
import { buildActorInstance, getAccountActivityPubUrl, setAsyncActorKeys } from './activitypub'
@ -12,6 +12,8 @@ import { UserNotificationSetting, UserNotificationSettingValue } from '../../sha
import { createWatchLaterPlaylist } from './video-playlist'
import { sequelizeTypescript } from '../initializers/database'
import { Transaction } from 'sequelize/types'
import { Redis } from './redis'
import { Emailer } from './emailer'
type ChannelNames = { name: string, displayName: string }
async function createUserAccountAndChannelAndPlaylist (parameters: {
@ -100,12 +102,24 @@ async function createApplicationActor (applicationId: number) {
return accountCreated
}
async function sendVerifyUserEmail (user: UserModel, isPendingEmail = false) {
const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id)
let url = WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString
if (isPendingEmail) url += '&isPendingEmail=true'
const email = isPendingEmail ? user.pendingEmail : user.email
await Emailer.Instance.addVerifyEmailJob(email, url)
}
// ---------------------------------------------------------------------------
export {
createApplicationActor,
createUserAccountAndChannelAndPlaylist,
createLocalAccountWithoutKeys
createLocalAccountWithoutKeys,
sendVerifyUserEmail
}
// ---------------------------------------------------------------------------

View File

@ -27,7 +27,6 @@ import { areValidationErrors } from './utils'
import { ActorModel } from '../../models/activitypub/actor'
import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor'
import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels'
import { UserCreate } from '../../../shared/models/users'
import { UserRegister } from '../../../shared/models/users/user-register.model'
const usersAddValidator = [
@ -178,13 +177,27 @@ const usersUpdateValidator = [
]
const usersUpdateMeValidator = [
body('displayName').optional().custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
body('description').optional().custom(isUserDescriptionValid).withMessage('Should have a valid description'),
body('currentPassword').optional().custom(isUserPasswordValid).withMessage('Should have a valid current password'),
body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
body('nsfwPolicy').optional().custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'),
body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'),
body('displayName')
.optional()
.custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
body('description')
.optional()
.custom(isUserDescriptionValid).withMessage('Should have a valid description'),
body('currentPassword')
.optional()
.custom(isUserPasswordValid).withMessage('Should have a valid current password'),
body('password')
.optional()
.custom(isUserPasswordValid).withMessage('Should have a valid password'),
body('email')
.optional()
.isEmail().withMessage('Should have a valid email attribute'),
body('nsfwPolicy')
.optional()
.custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'),
body('autoPlayVideo')
.optional()
.custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'),
body('videosHistoryEnabled')
.optional()
.custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'),
@ -329,8 +342,14 @@ const usersAskSendVerifyEmailValidator = [
]
const usersVerifyEmailValidator = [
param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'),
param('id')
.isInt().not().isEmpty().withMessage('Should have a valid id'),
body('verificationString')
.not().isEmpty().withMessage('Should have a valid verification string'),
body('isPendingEmail')
.optional()
.toBoolean(),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })

View File

@ -113,6 +113,11 @@ export class UserModel extends Model<UserModel> {
@Column(DataType.STRING(400))
email: string
@AllowNull(true)
@IsEmail
@Column(DataType.STRING(400))
pendingEmail: string
@AllowNull(true)
@Default(null)
@Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true))
@ -540,6 +545,7 @@ export class UserModel extends Model<UserModel> {
id: this.id,
username: this.username,
email: this.email,
pendingEmail: this.pendingEmail,
emailVerified: this.emailVerified,
nsfwPolicy: this.nsfwPolicy,
webTorrentEnabled: this.webTorrentEnabled,

View File

@ -250,7 +250,7 @@ describe('Test emails', function () {
})
it('Should not verify the email with an invalid verification string', async function () {
await verifyEmail(server.url, userId, verificationString + 'b', 403)
await verifyEmail(server.url, userId, verificationString + 'b', false, 403)
})
it('Should verify the email', async function () {

View File

@ -3,18 +3,29 @@
import * as chai from 'chai'
import 'mocha'
import {
registerUser, flushTests, getUserInformation, getMyUserInformation, killallServers,
userLogin, login, flushAndRunServer, ServerInfo, verifyEmail, updateCustomSubConfig, wait, cleanupTests
cleanupTests,
flushAndRunServer,
getMyUserInformation,
getUserInformation,
login,
registerUser,
ServerInfo,
updateCustomSubConfig,
updateMyUser,
userLogin,
verifyEmail
} from '../../../../shared/extra-utils'
import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
import { User } from '../../../../shared/models/users'
const expect = chai.expect
describe('Test users account verification', function () {
let server: ServerInfo
let userId: number
let userAccessToken: string
let verificationString: string
let expectedEmailsLength = 0
const user1 = {
@ -83,11 +94,53 @@ describe('Test users account verification', function () {
it('Should verify the user via email and allow login', async function () {
await verifyEmail(server.url, userId, verificationString)
await login(server.url, server.client, user1)
const res = await login(server.url, server.client, user1)
userAccessToken = res.body.access_token
const resUserVerified = await getUserInformation(server.url, server.accessToken, userId)
expect(resUserVerified.body.emailVerified).to.be.true
})
it('Should be able to change the user email', async function () {
let updateVerificationString: string
{
await updateMyUser({
url: server.url,
accessToken: userAccessToken,
email: 'updated@example.com'
})
await waitJobs(server)
expectedEmailsLength++
expect(emails).to.have.lengthOf(expectedEmailsLength)
const email = emails[expectedEmailsLength - 1]
const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text'])
updateVerificationString = verificationStringMatches[1]
}
{
const res = await getMyUserInformation(server.url, userAccessToken)
const me: User = res.body
expect(me.email).to.equal('user_1@example.com')
expect(me.pendingEmail).to.equal('updated@example.com')
}
{
await verifyEmail(server.url, userId, updateVerificationString, true)
const res = await getMyUserInformation(server.url, userAccessToken)
const me: User = res.body
expect(me.email).to.equal('updated@example.com')
expect(me.pendingEmail).to.be.null
}
})
it('Should register user not requiring email verification if setting not enabled', async function () {
this.timeout(5000)
await updateCustomSubConfig(server.url, server.accessToken, {

View File

@ -323,13 +323,16 @@ function askSendVerifyEmail (url: string, email: string) {
})
}
function verifyEmail (url: string, userId: number, verificationString: string, statusCodeExpected = 204) {
function verifyEmail (url: string, userId: number, verificationString: string, isPendingEmail = false, statusCodeExpected = 204) {
const path = '/api/v1/users/' + userId + '/verify-email'
return makePostBodyRequest({
url,
path,
fields: { verificationString },
fields: {
verificationString,
isPendingEmail
},
statusCodeExpected
})
}

View File

@ -9,6 +9,7 @@ export interface User {
id: number
username: string
email: string
pendingEmail: string | null
emailVerified: boolean
nsfwPolicy: NSFWPolicyType