mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 17:59:37 +02:00
feat(API): permissive email check in login, reset & verification (#6648)
* feat(API): permissive email check in reset & verification In order to not force users to be case sensitive when asking for password reset or resend email verification. When there's multiple emails where the only difference in the local is the capitalized letters, in those cases the users has to be case sensitive. closes #6570 * feat(API/login): permissive email handling Allow case insensitive email when there's no other candidate. closes #6570 * code review changes * Fix tests * Add more duplicate email checks --------- Co-authored-by: Chocobozzz <me@florianbigard.com>
This commit is contained in:
parent
9f64909fc7
commit
a51fb3f35e
17 changed files with 247 additions and 92 deletions
|
@ -161,6 +161,7 @@ export class UsersCommand extends AbstractCommand {
|
|||
videoQuotaDaily?: number
|
||||
role?: UserRoleType
|
||||
adminFlags?: UserAdminFlagType
|
||||
email?: string
|
||||
}) {
|
||||
const {
|
||||
username,
|
||||
|
@ -168,7 +169,8 @@ export class UsersCommand extends AbstractCommand {
|
|||
password = 'password',
|
||||
videoQuota,
|
||||
videoQuotaDaily,
|
||||
role = UserRole.USER
|
||||
role = UserRole.USER,
|
||||
email = username + '@example.com'
|
||||
} = options
|
||||
|
||||
const path = '/api/v1/users'
|
||||
|
@ -182,7 +184,7 @@ export class UsersCommand extends AbstractCommand {
|
|||
password,
|
||||
role,
|
||||
adminFlags,
|
||||
email: username + '@example.com',
|
||||
email,
|
||||
videoQuota,
|
||||
videoQuotaDaily
|
||||
},
|
||||
|
|
|
@ -60,10 +60,30 @@ describe('Test my user API validators', function () {
|
|||
|
||||
it('Should fail with an invalid email attribute', async function () {
|
||||
const fields = {
|
||||
email: 'blabla'
|
||||
email: 'blabla',
|
||||
currentPassword: 'password'
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: server.accessToken, fields })
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an already existing email attribute', async function () {
|
||||
const emails = [ 'moderator1@example.com', 'moderatoR1@example.com' ]
|
||||
|
||||
for (const email of emails) {
|
||||
const fields = {
|
||||
email,
|
||||
currentPassword: 'password'
|
||||
}
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + 'me',
|
||||
token: userToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with a too small password', async function () {
|
||||
|
|
|
@ -110,9 +110,16 @@ describe('Test registrations API validators', function () {
|
|||
})
|
||||
|
||||
it('Should fail if we register a user with the same email', async function () {
|
||||
const fields = { ...baseCorrectParams, email: 'admin' + server.internalServerNumber + '@example.com' }
|
||||
const emails = [
|
||||
'admin' + server.internalServerNumber + '@example.com',
|
||||
'Admin' + server.internalServerNumber + '@example.com'
|
||||
]
|
||||
|
||||
await check(fields, HttpStatusCode.CONFLICT_409)
|
||||
for (const email of emails) {
|
||||
const fields = { ...baseCorrectParams, email }
|
||||
|
||||
await check(fields, HttpStatusCode.CONFLICT_409)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with a bad display name', async function () {
|
||||
|
@ -267,16 +274,18 @@ describe('Test registrations API validators', function () {
|
|||
})
|
||||
|
||||
it('Should fail if the email is already awaiting registration approval', async function () {
|
||||
await server.registrations.requestRegistration({
|
||||
username: 'user42',
|
||||
email: 'user_request_2@example.com',
|
||||
registrationReason: 'tt',
|
||||
channel: {
|
||||
displayName: 'my user request 42 channel',
|
||||
name: 'user_request_42_channel'
|
||||
},
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
for (const email of [ 'user_request_2@example.com', 'user_requesT_2@example.com' ]) {
|
||||
await server.registrations.requestRegistration({
|
||||
username: 'user42',
|
||||
email,
|
||||
registrationReason: 'tt',
|
||||
channel: {
|
||||
displayName: 'my user request 42 channel',
|
||||
name: 'user_request_42_channel'
|
||||
},
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail if the channel is already awaiting registration approval', async function () {
|
||||
|
|
|
@ -206,15 +206,22 @@ describe('Test users admin API validators', function () {
|
|||
})
|
||||
|
||||
it('Should fail if we add a user with the same email', async function () {
|
||||
const fields = { ...baseCorrectParams, email: 'user1@example.com' }
|
||||
const emails = [
|
||||
'user1@example.com',
|
||||
'uSer1@example.com'
|
||||
]
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
for (const email of emails) {
|
||||
const fields = { ...baseCorrectParams, email }
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an invalid videoQuota', async function () {
|
||||
|
@ -333,6 +340,18 @@ describe('Test users admin API validators', function () {
|
|||
await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an existing email attribute', async function () {
|
||||
const fields = { email: 'modeRator1@example.com' }
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + userId,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid emailVerified attribute', async function () {
|
||||
const fields = {
|
||||
emailVerified: 'yes'
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
|
@ -11,6 +9,9 @@ import {
|
|||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
|
||||
import { SQLCommand } from '@tests/shared/sql-command.js'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Test emails', function () {
|
||||
let server: PeerTubeServer
|
||||
|
@ -32,6 +33,8 @@ describe('Test emails', function () {
|
|||
password: 'super_password'
|
||||
}
|
||||
|
||||
const similarUsers = [ { username: 'lowercase_user_1' }, { username: 'lowercase_user__1' } ]
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
|
@ -41,6 +44,17 @@ describe('Test emails', function () {
|
|||
await setAccessTokensToServers([ server ])
|
||||
await server.config.enableSignup(true)
|
||||
|
||||
{
|
||||
const sqlCommand = new SQLCommand(server)
|
||||
|
||||
for (const user of similarUsers) {
|
||||
await server.users.create(user)
|
||||
}
|
||||
|
||||
await sqlCommand.setUserEmail('lowercase_user__1', 'Lowercase_user_1@example.com')
|
||||
await sqlCommand.cleanup()
|
||||
}
|
||||
|
||||
{
|
||||
const created = await server.users.create({ username: user.username, password: user.password })
|
||||
userId = created.id
|
||||
|
@ -101,6 +115,10 @@ describe('Test emails', function () {
|
|||
})
|
||||
})
|
||||
|
||||
it('Should fail with wrong capitalization when multiple users with similar email exists', async function () {
|
||||
await server.users.askResetPassword({ email: similarUsers[0].username.toUpperCase(), expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should reset the password', async function () {
|
||||
await server.users.resetPassword({ userId, verificationString, password: 'super_password2' })
|
||||
})
|
||||
|
@ -269,6 +287,13 @@ describe('Test emails', function () {
|
|||
|
||||
describe('When verifying a user email', function () {
|
||||
|
||||
it('Should fail with wrong capitalization when multiple users with similar email exists', async function () {
|
||||
await server.users.askSendVerifyEmail({
|
||||
email: similarUsers[0].username.toUpperCase(),
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should ask to send the verification email', async function () {
|
||||
await server.users.askSendVerifyEmail({ email: 'user_1@example.com' })
|
||||
|
||||
|
|
|
@ -30,6 +30,11 @@ describe('Test oauth', function () {
|
|||
await setAccessTokensToServers([ server ])
|
||||
|
||||
sqlCommand = new SQLCommand(server)
|
||||
|
||||
await server.users.create({ username: 'user1', email: 'user@example.com' })
|
||||
await server.users.create({ username: 'user2', password: 'AdvancedPassword' })
|
||||
|
||||
await sqlCommand.setUserEmail('user2', 'User@example.com')
|
||||
})
|
||||
|
||||
describe('OAuth client', function () {
|
||||
|
@ -87,6 +92,9 @@ describe('Test oauth', function () {
|
|||
|
||||
it('Should be able to login', async function () {
|
||||
await server.login.login({ expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
const user = { username: 'User@example.com', password: 'AdvancedPassword' }
|
||||
await server.login.login({ user, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
|
||||
it('Should be able to login with an insensitive username', async function () {
|
||||
|
@ -99,6 +107,22 @@ describe('Test oauth', function () {
|
|||
const user3 = { username: 'ROOt', password: server.store.user.password }
|
||||
await server.login.login({ user: user3, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
|
||||
it('Should be able to login with an insensitive email when no similar emails exist', async function () {
|
||||
const user = { username: 'ADMIN' + server.internalServerNumber + '@example.com', password: server.store.user.password }
|
||||
await server.login.login({ user, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
const user2 = { username: 'admin' + server.internalServerNumber + '@example.com', password: server.store.user.password }
|
||||
await server.login.login({ user: user2, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
|
||||
it('Should not be able to login with an insensitive email when similar emails exist', async function () {
|
||||
const user = { username: 'uSer@example.com', password: 'AdvancedPassword' }
|
||||
await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
const user2 = { username: 'User@example.com', password: 'AdvancedPassword' }
|
||||
await server.login.login({ user: user2, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Logout', function () {
|
||||
|
|
|
@ -81,6 +81,10 @@ export class SQLCommand {
|
|||
await this.updateQuery(`UPDATE "userExport" SET storage = :storage WHERE "userId" = :userId`, { storage, userId })
|
||||
}
|
||||
|
||||
async setUserEmail (username: string, email: string) {
|
||||
await this.updateQuery(`UPDATE "user" SET email = :email WHERE "username" = :username`, { email, username })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
setPluginVersion (pluginName: string, newVersion: string) {
|
||||
|
|
|
@ -14,7 +14,7 @@ import { OAuthClientModel } from '../../models/oauth/oauth-client.js'
|
|||
import { OAuthTokenModel } from '../../models/oauth/oauth-token.js'
|
||||
import { UserModel } from '../../models/user/user.js'
|
||||
import { findAvailableLocalActorName } from '../local-actor.js'
|
||||
import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user.js'
|
||||
import { buildUser, createUserAccountAndChannelAndPlaylist, getUserByEmailPermissive } from '../user.js'
|
||||
import { ExternalUser } from './external-auth.js'
|
||||
import { TokensCache } from './tokens-cache.js'
|
||||
|
||||
|
@ -87,7 +87,7 @@ async function getUser (usernameOrEmail?: string, password?: string, bypassLogin
|
|||
if (bypassLogin && bypassLogin.bypass === true) {
|
||||
logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName)
|
||||
|
||||
let user = await UserModel.loadByEmail(bypassLogin.user.email)
|
||||
let user = getUserByEmailPermissive(await UserModel.loadByEmailCaseInsensitive(bypassLogin.user.email), bypassLogin.user.email)
|
||||
|
||||
if (!user) {
|
||||
user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user)
|
||||
|
@ -119,7 +119,14 @@ async function getUser (usernameOrEmail?: string, password?: string, bypassLogin
|
|||
|
||||
logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).')
|
||||
|
||||
const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail)
|
||||
const users = await UserModel.loadByUsernameOrEmailCaseInsensitive(usernameOrEmail)
|
||||
let user: MUserDefault
|
||||
|
||||
if (usernameOrEmail.includes('@')) {
|
||||
user = getUserByEmailPermissive(users, usernameOrEmail)
|
||||
} else if (users.length === 1) {
|
||||
user = users[0]
|
||||
}
|
||||
|
||||
// If we don't find the user, or if the user belongs to a plugin
|
||||
if (!user || user.pluginAuth !== null || !password) return null
|
||||
|
|
|
@ -154,12 +154,14 @@ async function handlePasswordGrant (options: {
|
|||
|
||||
const user = await getUser(usernameOrEmail, password, bypassLogin)
|
||||
if (!user) {
|
||||
const registration = await UserRegistrationModel.loadByEmailOrUsername(usernameOrEmail)
|
||||
const registrations = await UserRegistrationModel.listByEmailCaseInsensitiveOrUsername(usernameOrEmail)
|
||||
|
||||
if (registration?.state === UserRegistrationState.REJECTED) {
|
||||
throw new RegistrationApprovalRejected('Registration approval for this account has been rejected')
|
||||
} else if (registration?.state === UserRegistrationState.PENDING) {
|
||||
throw new RegistrationWaitingForApproval('Registration for this account is awaiting approval')
|
||||
if (registrations.length === 1) {
|
||||
if (registrations[0].state === UserRegistrationState.REJECTED) {
|
||||
throw new RegistrationApprovalRejected('Registration approval for this account has been rejected')
|
||||
} else if (registrations[0].state === UserRegistrationState.PENDING) {
|
||||
throw new RegistrationWaitingForApproval('Registration for this account is awaiting approval')
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidGrantError('Invalid grant: user credentials are invalid')
|
||||
|
|
|
@ -237,6 +237,12 @@ async function isUserQuotaValid (options: {
|
|||
return true
|
||||
}
|
||||
|
||||
function getUserByEmailPermissive <T extends { email: string }> (users: T[], email: string): T {
|
||||
if (users.length === 1) return users[0]
|
||||
|
||||
return users.find(r => r.email === email)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
|
@ -250,7 +256,8 @@ export {
|
|||
sendVerifyRegistrationEmail,
|
||||
|
||||
isUserQuotaValid,
|
||||
buildUser
|
||||
buildUser,
|
||||
getUserByEmailPermissive
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserRightType } from '@peertube/peertube-models'
|
||||
import { getUserByEmailPermissive } from '@server/lib/user.js'
|
||||
import { ActorModel } from '@server/models/actor/actor.js'
|
||||
import { UserModel } from '@server/models/user/user.js'
|
||||
import { MAccountId, MUserAccountId, MUserDefault } from '@server/types/models/index.js'
|
||||
|
@ -10,14 +11,19 @@ export function checkUserIdExist (idArg: number | string, res: express.Response,
|
|||
return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
|
||||
}
|
||||
|
||||
export function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
|
||||
return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
|
||||
export function checkUserEmailExistPermissive (email: string, res: express.Response, abortResponse = true) {
|
||||
return checkUserExist(async () => {
|
||||
const users = await UserModel.loadByEmailCaseInsensitive(email)
|
||||
|
||||
return getUserByEmailPermissive(users, email)
|
||||
}, res, abortResponse)
|
||||
}
|
||||
|
||||
export async function checkUserNameOrEmailDoNotAlreadyExist (username: string, email: string, res: express.Response) {
|
||||
const user = await UserModel.loadByUsernameOrEmail(username, email)
|
||||
export async function checkUsernameOrEmailDoNotAlreadyExist (username: string, email: string, res: express.Response) {
|
||||
const existingUser = await UserModel.loadByUsernameOrEmailCaseInsensitive(username)
|
||||
const existingEmail = await UserModel.loadByUsernameOrEmailCaseInsensitive(email)
|
||||
|
||||
if (user) {
|
||||
if (existingUser.length > 0 || existingEmail.length > 0) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'User with this username or email already exists.'
|
||||
|
@ -37,6 +43,20 @@ export async function checkUserNameOrEmailDoNotAlreadyExist (username: string, e
|
|||
return true
|
||||
}
|
||||
|
||||
export async function checkEmailDoesNotAlreadyExist (email: string, res: express.Response) {
|
||||
const user = await UserModel.loadByEmailCaseInsensitive(email)
|
||||
|
||||
if (user.length !== 0) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'User with this email already exists.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) {
|
||||
const user = await finder()
|
||||
|
||||
|
|
|
@ -3,14 +3,19 @@ import { UserRegistrationModel } from '@server/models/user/user-registration.js'
|
|||
import { MRegistration } from '@server/types/models/index.js'
|
||||
import { forceNumber, pick } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { getUserByEmailPermissive } from '@server/lib/user.js'
|
||||
|
||||
function checkRegistrationIdExist (idArg: number | string, res: express.Response) {
|
||||
const id = forceNumber(idArg)
|
||||
return checkRegistrationExist(() => UserRegistrationModel.load(id), res)
|
||||
}
|
||||
|
||||
function checkRegistrationEmailExist (email: string, res: express.Response, abortResponse = true) {
|
||||
return checkRegistrationExist(() => UserRegistrationModel.loadByEmail(email), res, abortResponse)
|
||||
function checkRegistrationEmailExistPermissive (email: string, res: express.Response, abortResponse = true) {
|
||||
return checkRegistrationExist(async () => {
|
||||
const registrations = await UserRegistrationModel.listByEmailCaseInsensitive(email)
|
||||
|
||||
return getUserByEmailPermissive(registrations, email)
|
||||
}, res, abortResponse)
|
||||
}
|
||||
|
||||
async function checkRegistrationHandlesDoNotAlreadyExist (options: {
|
||||
|
@ -21,9 +26,11 @@ async function checkRegistrationHandlesDoNotAlreadyExist (options: {
|
|||
}) {
|
||||
const { res } = options
|
||||
|
||||
const registration = await UserRegistrationModel.loadByEmailOrHandle(pick(options, [ 'username', 'email', 'channelHandle' ]))
|
||||
const registrations = await UserRegistrationModel.listByEmailCaseInsensitiveOrHandle(
|
||||
pick(options, [ 'username', 'email', 'channelHandle' ])
|
||||
)
|
||||
|
||||
if (registration) {
|
||||
if (registrations.length !== 0) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Registration with this username, channel name or email already exists.'
|
||||
|
@ -54,7 +61,7 @@ async function checkRegistrationExist (finder: () => Promise<MRegistration>, res
|
|||
|
||||
export {
|
||||
checkRegistrationIdExist,
|
||||
checkRegistrationEmailExist,
|
||||
checkRegistrationEmailExistPermissive,
|
||||
checkRegistrationHandlesDoNotAlreadyExist,
|
||||
checkRegistrationExist
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { Redis } from '../../../lib/redis.js'
|
||||
import { areValidationErrors, checkUserEmailExist, checkUserIdExist } from '../shared/index.js'
|
||||
import { checkRegistrationEmailExist, checkRegistrationIdExist } from './shared/user-registrations.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { areValidationErrors, checkUserEmailExistPermissive, checkUserIdExist } from '../shared/index.js'
|
||||
import { checkRegistrationEmailExistPermissive, checkRegistrationIdExist } from './shared/user-registrations.js'
|
||||
|
||||
const usersAskSendVerifyEmailValidator = [
|
||||
export const usersAskSendVerifyEmailValidator = [
|
||||
body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
|
@ -19,8 +19,8 @@ const usersAskSendVerifyEmailValidator = [
|
|||
}, 'filter:api.email-verification.ask-send-verify-email.body')
|
||||
|
||||
const [ userExists, registrationExists ] = await Promise.all([
|
||||
checkUserEmailExist(email, res, false),
|
||||
checkRegistrationEmailExist(email, res, false)
|
||||
checkUserEmailExistPermissive(email, res, false),
|
||||
checkRegistrationEmailExistPermissive(email, res, false)
|
||||
])
|
||||
|
||||
if (!userExists && !registrationExists) {
|
||||
|
@ -40,7 +40,7 @@ const usersAskSendVerifyEmailValidator = [
|
|||
}
|
||||
]
|
||||
|
||||
const usersVerifyEmailValidator = [
|
||||
export const usersVerifyEmailValidator = [
|
||||
param('id')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid id'),
|
||||
|
||||
|
@ -67,7 +67,7 @@ const usersVerifyEmailValidator = [
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const registrationVerifyEmailValidator = [
|
||||
export const registrationVerifyEmailValidator = [
|
||||
param('registrationId')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid registrationId'),
|
||||
|
||||
|
@ -88,12 +88,3 @@ const registrationVerifyEmailValidator = [
|
|||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
usersAskSendVerifyEmailValidator,
|
||||
usersVerifyEmailValidator,
|
||||
|
||||
registrationVerifyEmailValidator
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { isUserDisplayNameValid, isUserPasswordValid, isUserUsernameValid } from
|
|||
import { isVideoChannelDisplayNameValid, isVideoChannelUsernameValid } from '../../../helpers/custom-validators/video-channels.js'
|
||||
import { isSignupAllowed, isSignupAllowedForCurrentIP, SignupMode } from '../../../lib/signup.js'
|
||||
import { ActorModel } from '../../../models/actor/actor.js'
|
||||
import { areValidationErrors, checkUserNameOrEmailDoNotAlreadyExist } from '../shared/index.js'
|
||||
import { areValidationErrors, checkUsernameOrEmailDoNotAlreadyExist } from '../shared/index.js'
|
||||
import { checkRegistrationHandlesDoNotAlreadyExist, checkRegistrationIdExist } from './shared/user-registrations.js'
|
||||
|
||||
const usersDirectRegistrationValidator = usersCommonRegistrationValidatorFactory()
|
||||
|
@ -182,7 +182,7 @@ function usersCommonRegistrationValidatorFactory (additionalValidationChain: Val
|
|||
|
||||
const body: UserRegister | UserRegistrationRequest = req.body
|
||||
|
||||
if (!await checkUserNameOrEmailDoNotAlreadyExist(body.username, body.email, res)) return
|
||||
if (!await checkUsernameOrEmailDoNotAlreadyExist(body.username, body.email, res)) return
|
||||
|
||||
if (body.channel) {
|
||||
if (!body.channel.name || !body.channel.displayName) {
|
||||
|
|
|
@ -31,10 +31,11 @@ import { Redis } from '../../../lib/redis.js'
|
|||
import { ActorModel } from '../../../models/actor/actor.js'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkEmailDoesNotAlreadyExist,
|
||||
checkUserCanManageAccount,
|
||||
checkUserEmailExist,
|
||||
checkUserEmailExistPermissive,
|
||||
checkUserIdExist,
|
||||
checkUserNameOrEmailDoNotAlreadyExist,
|
||||
checkUsernameOrEmailDoNotAlreadyExist,
|
||||
doesVideoChannelIdExist,
|
||||
doesVideoExist,
|
||||
isValidVideoIdParam
|
||||
|
@ -85,7 +86,7 @@ export const usersAddValidator = [
|
|||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { omitBodyLog: true })) return
|
||||
if (!await checkUserNameOrEmailDoNotAlreadyExist(req.body.username, req.body.email, res)) return
|
||||
if (!await checkUsernameOrEmailDoNotAlreadyExist(req.body.username, req.body.email, res)) return
|
||||
|
||||
const authUser = res.locals.oauth.token.User
|
||||
if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
|
||||
|
@ -199,6 +200,8 @@ export const usersUpdateValidator = [
|
|||
return res.fail({ message: 'Cannot change root role.' })
|
||||
}
|
||||
|
||||
if (req.body.email && !await checkEmailDoesNotAlreadyExist(req.body.email, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
@ -278,6 +281,8 @@ export const usersUpdateMeValidator = [
|
|||
|
||||
if (areValidationErrors(req, res, { omitBodyLog: true })) return
|
||||
|
||||
if (req.body.email && !await checkEmailDoesNotAlreadyExist(req.body.email, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
@ -339,7 +344,7 @@ export const usersAskResetPasswordValidator = [
|
|||
email: req.body.email
|
||||
}, 'filter:api.users.ask-reset-password.body')
|
||||
|
||||
const exists = await checkUserEmailExist(email, res, false)
|
||||
const exists = await checkUserEmailExistPermissive(email, res, false)
|
||||
if (!exists) {
|
||||
logger.debug('User with email %s does not exist (asking reset password).', email)
|
||||
// Do not leak our emails
|
||||
|
|
|
@ -8,7 +8,7 @@ import { isVideoChannelDisplayNameValid } from '@server/helpers/custom-validator
|
|||
import { cryptPassword } from '@server/helpers/peertube-crypto.js'
|
||||
import { USER_REGISTRATION_STATES } from '@server/initializers/constants.js'
|
||||
import { MRegistration, MRegistrationFormattable } from '@server/types/models/index.js'
|
||||
import { FindOptions, Op, QueryTypes, WhereOptions } from 'sequelize'
|
||||
import { col, FindOptions, fn, Op, QueryTypes, where, WhereOptions } from 'sequelize'
|
||||
import {
|
||||
AllowNull,
|
||||
BeforeCreate,
|
||||
|
@ -129,36 +129,49 @@ export class UserRegistrationModel extends SequelizeModel<UserRegistrationModel>
|
|||
return UserRegistrationModel.findByPk(id)
|
||||
}
|
||||
|
||||
static loadByEmail (email: string): Promise<MRegistration> {
|
||||
static listByEmailCaseInsensitive (email: string): Promise<MRegistration[]> {
|
||||
const query = {
|
||||
where: { email }
|
||||
where: where(
|
||||
fn('LOWER', col('email')),
|
||||
'=',
|
||||
email.toLowerCase()
|
||||
)
|
||||
}
|
||||
|
||||
return UserRegistrationModel.findOne(query)
|
||||
return UserRegistrationModel.findAll(query)
|
||||
}
|
||||
|
||||
static loadByEmailOrUsername (emailOrUsername: string): Promise<MRegistration> {
|
||||
static listByEmailCaseInsensitiveOrUsername (emailOrUsername: string): Promise<MRegistration[]> {
|
||||
const query = {
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ email: emailOrUsername },
|
||||
where(
|
||||
fn('LOWER', col('email')),
|
||||
'=',
|
||||
emailOrUsername.toLowerCase()
|
||||
),
|
||||
|
||||
{ username: emailOrUsername }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return UserRegistrationModel.findOne(query)
|
||||
return UserRegistrationModel.findAll(query)
|
||||
}
|
||||
|
||||
static loadByEmailOrHandle (options: {
|
||||
static listByEmailCaseInsensitiveOrHandle (options: {
|
||||
email: string
|
||||
username: string
|
||||
channelHandle?: string
|
||||
}): Promise<MRegistration> {
|
||||
}): Promise<MRegistration[]> {
|
||||
const { email, username, channelHandle } = options
|
||||
|
||||
let or: WhereOptions = [
|
||||
{ email },
|
||||
where(
|
||||
fn('LOWER', col('email')),
|
||||
'=',
|
||||
email.toLowerCase()
|
||||
),
|
||||
{ channelHandle: username },
|
||||
{ username }
|
||||
]
|
||||
|
@ -176,7 +189,7 @@ export class UserRegistrationModel extends SequelizeModel<UserRegistrationModel>
|
|||
}
|
||||
}
|
||||
|
||||
return UserRegistrationModel.findOne(query)
|
||||
return UserRegistrationModel.findAll(query)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -663,30 +663,30 @@ export class UserModel extends SequelizeModel<UserModel> {
|
|||
return UserModel.scope(ScopeNames.FOR_ME_API).findOne(query)
|
||||
}
|
||||
|
||||
static loadByEmail (email: string): Promise<MUserDefault> {
|
||||
static loadByEmailCaseInsensitive (email: string): Promise<MUserDefault[]> {
|
||||
const query = {
|
||||
where: {
|
||||
email
|
||||
}
|
||||
where: where(
|
||||
fn('LOWER', col('email')),
|
||||
'=',
|
||||
email.toLowerCase()
|
||||
)
|
||||
}
|
||||
|
||||
return UserModel.findOne(query)
|
||||
return UserModel.findAll(query)
|
||||
}
|
||||
|
||||
static loadByUsernameOrEmail (username: string, email?: string): Promise<MUserDefault> {
|
||||
if (!email) email = username
|
||||
|
||||
static loadByUsernameOrEmailCaseInsensitive (usernameOrEmail: string): Promise<MUserDefault[]> {
|
||||
const query = {
|
||||
where: {
|
||||
[Op.or]: [
|
||||
where(fn('lower', col('username')), fn('lower', username) as any),
|
||||
where(fn('lower', col('username')), fn('lower', usernameOrEmail) as any),
|
||||
|
||||
{ email }
|
||||
where(fn('lower', col('email')), fn('lower', usernameOrEmail) as any)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return UserModel.findOne(query)
|
||||
return UserModel.findAll(query)
|
||||
}
|
||||
|
||||
static loadByVideoId (videoId: number): Promise<MUserDefault> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue