mirror of https://github.com/Chocobozzz/PeerTube
Handle email update on server
parent
fff77ba231
commit
d1ab89deb7
|
@ -0,0 +1 @@
|
|||
custom: https://framasoft.org/en/#soutenir
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LAST_MIGRATION_VERSION = 385
|
||||
const LAST_MIGRATION_VERSION = 390
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ export interface User {
|
|||
id: number
|
||||
username: string
|
||||
email: string
|
||||
pendingEmail: string | null
|
||||
emailVerified: boolean
|
||||
nsfwPolicy: NSFWPolicyType
|
||||
|
||||
|
|
Loading…
Reference in New Issue