Handle email update on server
This commit is contained in:
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 { RATES_LIMIT, WEBSERVER } from '../../../initializers/constants'
|
||||||
import { Emailer } from '../../../lib/emailer'
|
import { Emailer } from '../../../lib/emailer'
|
||||||
import { Redis } from '../../../lib/redis'
|
import { Redis } from '../../../lib/redis'
|
||||||
import { createUserAccountAndChannelAndPlaylist } from '../../../lib/user'
|
import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
|
||||||
import {
|
import {
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
asyncRetryTransactionMiddleware,
|
asyncRetryTransactionMiddleware,
|
||||||
|
@ -147,7 +147,7 @@ usersRouter.post('/:id/reset-password',
|
||||||
usersRouter.post('/ask-send-verify-email',
|
usersRouter.post('/ask-send-verify-email',
|
||||||
askSendEmailLimiter,
|
askSendEmailLimiter,
|
||||||
asyncMiddleware(usersAskSendVerifyEmailValidator),
|
asyncMiddleware(usersAskSendVerifyEmailValidator),
|
||||||
asyncMiddleware(askSendVerifyUserEmail)
|
asyncMiddleware(reSendVerifyUserEmail)
|
||||||
)
|
)
|
||||||
|
|
||||||
usersRouter.post('/:id/verify-email',
|
usersRouter.post('/:id/verify-email',
|
||||||
|
@ -320,14 +320,7 @@ async function resetUserPassword (req: express.Request, res: express.Response) {
|
||||||
return res.status(204).end()
|
return res.status(204).end()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendVerifyUserEmail (user: UserModel) {
|
async function reSendVerifyUserEmail (req: express.Request, res: express.Response) {
|
||||||
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) {
|
|
||||||
const user = res.locals.user
|
const user = res.locals.user
|
||||||
|
|
||||||
await sendVerifyUserEmail(user)
|
await sendVerifyUserEmail(user)
|
||||||
|
@ -339,6 +332,11 @@ async function verifyUserEmail (req: express.Request, res: express.Response) {
|
||||||
const user = res.locals.user
|
const user = res.locals.user
|
||||||
user.emailVerified = true
|
user.emailVerified = true
|
||||||
|
|
||||||
|
if (req.body.isPendingEmail === true) {
|
||||||
|
user.email = user.pendingEmail
|
||||||
|
user.pendingEmail = null
|
||||||
|
}
|
||||||
|
|
||||||
await user.save()
|
await user.save()
|
||||||
|
|
||||||
return res.status(204).end()
|
return res.status(204).end()
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { VideoImportModel } from '../../../models/video/video-import'
|
||||||
import { AccountModel } from '../../../models/account/account'
|
import { AccountModel } from '../../../models/account/account'
|
||||||
import { CONFIG } from '../../../initializers/config'
|
import { CONFIG } from '../../../initializers/config'
|
||||||
import { sequelizeTypescript } from '../../../initializers/database'
|
import { sequelizeTypescript } from '../../../initializers/database'
|
||||||
|
import { sendVerifyUserEmail } from '../../../lib/user'
|
||||||
|
|
||||||
const auditLogger = auditLoggerFactory('users-me')
|
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) {
|
async function updateMe (req: express.Request, res: express.Response) {
|
||||||
const body: UserUpdateMe = req.body
|
const body: UserUpdateMe = req.body
|
||||||
|
let sendVerificationEmail = false
|
||||||
|
|
||||||
const user = res.locals.oauth.token.user
|
const user = res.locals.oauth.token.user
|
||||||
const oldUserAuditView = new UserAuditView(user.toFormattedJSON({}))
|
const oldUserAuditView = new UserAuditView(user.toFormattedJSON({}))
|
||||||
|
|
||||||
if (body.password !== undefined) user.password = body.password
|
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.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy
|
||||||
if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled
|
if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled
|
||||||
if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
|
if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
|
||||||
if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled
|
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 => {
|
await sequelizeTypescript.transaction(async t => {
|
||||||
const userAccount = await AccountModel.load(user.Account.id)
|
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)
|
auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON({})), oldUserAuditView)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (sendVerificationEmail === true) {
|
||||||
|
await sendVerifyUserEmail(user, true)
|
||||||
|
}
|
||||||
|
|
||||||
return res.sendStatus(204)
|
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 * as uuidv4 from 'uuid/v4'
|
||||||
import { ActivityPubActorType } from '../../shared/models/activitypub'
|
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 { AccountModel } from '../models/account/account'
|
||||||
import { UserModel } from '../models/account/user'
|
import { UserModel } from '../models/account/user'
|
||||||
import { buildActorInstance, getAccountActivityPubUrl, setAsyncActorKeys } from './activitypub'
|
import { buildActorInstance, getAccountActivityPubUrl, setAsyncActorKeys } from './activitypub'
|
||||||
|
@ -12,6 +12,8 @@ import { UserNotificationSetting, UserNotificationSettingValue } from '../../sha
|
||||||
import { createWatchLaterPlaylist } from './video-playlist'
|
import { createWatchLaterPlaylist } from './video-playlist'
|
||||||
import { sequelizeTypescript } from '../initializers/database'
|
import { sequelizeTypescript } from '../initializers/database'
|
||||||
import { Transaction } from 'sequelize/types'
|
import { Transaction } from 'sequelize/types'
|
||||||
|
import { Redis } from './redis'
|
||||||
|
import { Emailer } from './emailer'
|
||||||
|
|
||||||
type ChannelNames = { name: string, displayName: string }
|
type ChannelNames = { name: string, displayName: string }
|
||||||
async function createUserAccountAndChannelAndPlaylist (parameters: {
|
async function createUserAccountAndChannelAndPlaylist (parameters: {
|
||||||
|
@ -100,12 +102,24 @@ async function createApplicationActor (applicationId: number) {
|
||||||
return accountCreated
|
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 {
|
export {
|
||||||
createApplicationActor,
|
createApplicationActor,
|
||||||
createUserAccountAndChannelAndPlaylist,
|
createUserAccountAndChannelAndPlaylist,
|
||||||
createLocalAccountWithoutKeys
|
createLocalAccountWithoutKeys,
|
||||||
|
sendVerifyUserEmail
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -27,7 +27,6 @@ import { areValidationErrors } from './utils'
|
||||||
import { ActorModel } from '../../models/activitypub/actor'
|
import { ActorModel } from '../../models/activitypub/actor'
|
||||||
import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor'
|
import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor'
|
||||||
import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels'
|
import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels'
|
||||||
import { UserCreate } from '../../../shared/models/users'
|
|
||||||
import { UserRegister } from '../../../shared/models/users/user-register.model'
|
import { UserRegister } from '../../../shared/models/users/user-register.model'
|
||||||
|
|
||||||
const usersAddValidator = [
|
const usersAddValidator = [
|
||||||
|
@ -178,13 +177,27 @@ const usersUpdateValidator = [
|
||||||
]
|
]
|
||||||
|
|
||||||
const usersUpdateMeValidator = [
|
const usersUpdateMeValidator = [
|
||||||
body('displayName').optional().custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
|
body('displayName')
|
||||||
body('description').optional().custom(isUserDescriptionValid).withMessage('Should have a valid description'),
|
.optional()
|
||||||
body('currentPassword').optional().custom(isUserPasswordValid).withMessage('Should have a valid current password'),
|
.custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
|
||||||
body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
|
body('description')
|
||||||
body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
|
.optional()
|
||||||
body('nsfwPolicy').optional().custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'),
|
.custom(isUserDescriptionValid).withMessage('Should have a valid description'),
|
||||||
body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'),
|
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')
|
body('videosHistoryEnabled')
|
||||||
.optional()
|
.optional()
|
||||||
.custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'),
|
.custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'),
|
||||||
|
@ -329,8 +342,14 @@ const usersAskSendVerifyEmailValidator = [
|
||||||
]
|
]
|
||||||
|
|
||||||
const usersVerifyEmailValidator = [
|
const usersVerifyEmailValidator = [
|
||||||
param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
|
param('id')
|
||||||
body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'),
|
.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) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
|
logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
|
||||||
|
|
|
@ -113,6 +113,11 @@ export class UserModel extends Model<UserModel> {
|
||||||
@Column(DataType.STRING(400))
|
@Column(DataType.STRING(400))
|
||||||
email: string
|
email: string
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@IsEmail
|
||||||
|
@Column(DataType.STRING(400))
|
||||||
|
pendingEmail: string
|
||||||
|
|
||||||
@AllowNull(true)
|
@AllowNull(true)
|
||||||
@Default(null)
|
@Default(null)
|
||||||
@Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true))
|
@Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true))
|
||||||
|
@ -540,6 +545,7 @@ export class UserModel extends Model<UserModel> {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
username: this.username,
|
username: this.username,
|
||||||
email: this.email,
|
email: this.email,
|
||||||
|
pendingEmail: this.pendingEmail,
|
||||||
emailVerified: this.emailVerified,
|
emailVerified: this.emailVerified,
|
||||||
nsfwPolicy: this.nsfwPolicy,
|
nsfwPolicy: this.nsfwPolicy,
|
||||||
webTorrentEnabled: this.webTorrentEnabled,
|
webTorrentEnabled: this.webTorrentEnabled,
|
||||||
|
|
|
@ -250,7 +250,7 @@ describe('Test emails', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should not verify the email with an invalid verification string', async 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 () {
|
it('Should verify the email', async function () {
|
||||||
|
|
|
@ -3,18 +3,29 @@
|
||||||
import * as chai from 'chai'
|
import * as chai from 'chai'
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
import {
|
import {
|
||||||
registerUser, flushTests, getUserInformation, getMyUserInformation, killallServers,
|
cleanupTests,
|
||||||
userLogin, login, flushAndRunServer, ServerInfo, verifyEmail, updateCustomSubConfig, wait, cleanupTests
|
flushAndRunServer,
|
||||||
|
getMyUserInformation,
|
||||||
|
getUserInformation,
|
||||||
|
login,
|
||||||
|
registerUser,
|
||||||
|
ServerInfo,
|
||||||
|
updateCustomSubConfig,
|
||||||
|
updateMyUser,
|
||||||
|
userLogin,
|
||||||
|
verifyEmail
|
||||||
} from '../../../../shared/extra-utils'
|
} from '../../../../shared/extra-utils'
|
||||||
import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
|
import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
|
||||||
import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
|
import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
|
||||||
import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
|
import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
|
||||||
|
import { User } from '../../../../shared/models/users'
|
||||||
|
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
describe('Test users account verification', function () {
|
describe('Test users account verification', function () {
|
||||||
let server: ServerInfo
|
let server: ServerInfo
|
||||||
let userId: number
|
let userId: number
|
||||||
|
let userAccessToken: string
|
||||||
let verificationString: string
|
let verificationString: string
|
||||||
let expectedEmailsLength = 0
|
let expectedEmailsLength = 0
|
||||||
const user1 = {
|
const user1 = {
|
||||||
|
@ -83,11 +94,53 @@ describe('Test users account verification', function () {
|
||||||
|
|
||||||
it('Should verify the user via email and allow login', async function () {
|
it('Should verify the user via email and allow login', async function () {
|
||||||
await verifyEmail(server.url, userId, verificationString)
|
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)
|
const resUserVerified = await getUserInformation(server.url, server.accessToken, userId)
|
||||||
expect(resUserVerified.body.emailVerified).to.be.true
|
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 () {
|
it('Should register user not requiring email verification if setting not enabled', async function () {
|
||||||
this.timeout(5000)
|
this.timeout(5000)
|
||||||
await updateCustomSubConfig(server.url, server.accessToken, {
|
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'
|
const path = '/api/v1/users/' + userId + '/verify-email'
|
||||||
|
|
||||||
return makePostBodyRequest({
|
return makePostBodyRequest({
|
||||||
url,
|
url,
|
||||||
path,
|
path,
|
||||||
fields: { verificationString },
|
fields: {
|
||||||
|
verificationString,
|
||||||
|
isPendingEmail
|
||||||
|
},
|
||||||
statusCodeExpected
|
statusCodeExpected
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ export interface User {
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
email: string
|
email: string
|
||||||
|
pendingEmail: string | null
|
||||||
emailVerified: boolean
|
emailVerified: boolean
|
||||||
nsfwPolicy: NSFWPolicyType
|
nsfwPolicy: NSFWPolicyType
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue