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
|
videoQuotaDaily?: number
|
||||||
role?: UserRoleType
|
role?: UserRoleType
|
||||||
adminFlags?: UserAdminFlagType
|
adminFlags?: UserAdminFlagType
|
||||||
|
email?: string
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
username,
|
username,
|
||||||
|
@ -168,7 +169,8 @@ export class UsersCommand extends AbstractCommand {
|
||||||
password = 'password',
|
password = 'password',
|
||||||
videoQuota,
|
videoQuota,
|
||||||
videoQuotaDaily,
|
videoQuotaDaily,
|
||||||
role = UserRole.USER
|
role = UserRole.USER,
|
||||||
|
email = username + '@example.com'
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
const path = '/api/v1/users'
|
const path = '/api/v1/users'
|
||||||
|
@ -182,7 +184,7 @@ export class UsersCommand extends AbstractCommand {
|
||||||
password,
|
password,
|
||||||
role,
|
role,
|
||||||
adminFlags,
|
adminFlags,
|
||||||
email: username + '@example.com',
|
email,
|
||||||
videoQuota,
|
videoQuota,
|
||||||
videoQuotaDaily
|
videoQuotaDaily
|
||||||
},
|
},
|
||||||
|
|
|
@ -60,10 +60,30 @@ describe('Test my user API validators', function () {
|
||||||
|
|
||||||
it('Should fail with an invalid email attribute', async function () {
|
it('Should fail with an invalid email attribute', async function () {
|
||||||
const fields = {
|
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 () {
|
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 () {
|
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'
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const email of emails) {
|
||||||
|
const fields = { ...baseCorrectParams, email }
|
||||||
|
|
||||||
await check(fields, HttpStatusCode.CONFLICT_409)
|
await check(fields, HttpStatusCode.CONFLICT_409)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with a bad display name', async function () {
|
it('Should fail with a bad display name', async function () {
|
||||||
|
@ -267,9 +274,10 @@ describe('Test registrations API validators', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail if the email is already awaiting registration approval', async function () {
|
it('Should fail if the email is already awaiting registration approval', async function () {
|
||||||
|
for (const email of [ 'user_request_2@example.com', 'user_requesT_2@example.com' ]) {
|
||||||
await server.registrations.requestRegistration({
|
await server.registrations.requestRegistration({
|
||||||
username: 'user42',
|
username: 'user42',
|
||||||
email: 'user_request_2@example.com',
|
email,
|
||||||
registrationReason: 'tt',
|
registrationReason: 'tt',
|
||||||
channel: {
|
channel: {
|
||||||
displayName: 'my user request 42 channel',
|
displayName: 'my user request 42 channel',
|
||||||
|
@ -277,6 +285,7 @@ describe('Test registrations API validators', function () {
|
||||||
},
|
},
|
||||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail if the channel is already awaiting registration approval', async function () {
|
it('Should fail if the channel is already awaiting registration approval', async function () {
|
||||||
|
|
|
@ -206,7 +206,13 @@ describe('Test users admin API validators', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail if we add a user with the same email', async 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'
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const email of emails) {
|
||||||
|
const fields = { ...baseCorrectParams, email }
|
||||||
|
|
||||||
await makePostBodyRequest({
|
await makePostBodyRequest({
|
||||||
url: server.url,
|
url: server.url,
|
||||||
|
@ -215,6 +221,7 @@ describe('Test users admin API validators', function () {
|
||||||
fields,
|
fields,
|
||||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with an invalid videoQuota', async function () {
|
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 })
|
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 () {
|
it('Should fail with an invalid emailVerified attribute', async function () {
|
||||||
const fields = {
|
const fields = {
|
||||||
emailVerified: 'yes'
|
emailVerified: 'yes'
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* 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 { HttpStatusCode } from '@peertube/peertube-models'
|
||||||
import {
|
import {
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
|
@ -11,6 +9,9 @@ import {
|
||||||
setAccessTokensToServers,
|
setAccessTokensToServers,
|
||||||
waitJobs
|
waitJobs
|
||||||
} from '@peertube/peertube-server-commands'
|
} 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 () {
|
describe('Test emails', function () {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
|
@ -32,6 +33,8 @@ describe('Test emails', function () {
|
||||||
password: 'super_password'
|
password: 'super_password'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const similarUsers = [ { username: 'lowercase_user_1' }, { username: 'lowercase_user__1' } ]
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(120000)
|
this.timeout(120000)
|
||||||
|
|
||||||
|
@ -41,6 +44,17 @@ describe('Test emails', function () {
|
||||||
await setAccessTokensToServers([ server ])
|
await setAccessTokensToServers([ server ])
|
||||||
await server.config.enableSignup(true)
|
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 })
|
const created = await server.users.create({ username: user.username, password: user.password })
|
||||||
userId = created.id
|
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 () {
|
it('Should reset the password', async function () {
|
||||||
await server.users.resetPassword({ userId, verificationString, password: 'super_password2' })
|
await server.users.resetPassword({ userId, verificationString, password: 'super_password2' })
|
||||||
})
|
})
|
||||||
|
@ -269,6 +287,13 @@ describe('Test emails', function () {
|
||||||
|
|
||||||
describe('When verifying a user email', 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 () {
|
it('Should ask to send the verification email', async function () {
|
||||||
await server.users.askSendVerifyEmail({ email: 'user_1@example.com' })
|
await server.users.askSendVerifyEmail({ email: 'user_1@example.com' })
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,11 @@ describe('Test oauth', function () {
|
||||||
await setAccessTokensToServers([ server ])
|
await setAccessTokensToServers([ server ])
|
||||||
|
|
||||||
sqlCommand = new SQLCommand(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 () {
|
describe('OAuth client', function () {
|
||||||
|
@ -87,6 +92,9 @@ describe('Test oauth', function () {
|
||||||
|
|
||||||
it('Should be able to login', async function () {
|
it('Should be able to login', async function () {
|
||||||
await server.login.login({ expectedStatus: HttpStatusCode.OK_200 })
|
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 () {
|
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 }
|
const user3 = { username: 'ROOt', password: server.store.user.password }
|
||||||
await server.login.login({ user: user3, expectedStatus: HttpStatusCode.OK_200 })
|
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 () {
|
describe('Logout', function () {
|
||||||
|
|
|
@ -81,6 +81,10 @@ export class SQLCommand {
|
||||||
await this.updateQuery(`UPDATE "userExport" SET storage = :storage WHERE "userId" = :userId`, { storage, userId })
|
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) {
|
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 { OAuthTokenModel } from '../../models/oauth/oauth-token.js'
|
||||||
import { UserModel } from '../../models/user/user.js'
|
import { UserModel } from '../../models/user/user.js'
|
||||||
import { findAvailableLocalActorName } from '../local-actor.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 { ExternalUser } from './external-auth.js'
|
||||||
import { TokensCache } from './tokens-cache.js'
|
import { TokensCache } from './tokens-cache.js'
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ async function getUser (usernameOrEmail?: string, password?: string, bypassLogin
|
||||||
if (bypassLogin && bypassLogin.bypass === true) {
|
if (bypassLogin && bypassLogin.bypass === true) {
|
||||||
logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName)
|
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) {
|
if (!user) {
|
||||||
user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.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: ******).')
|
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 we don't find the user, or if the user belongs to a plugin
|
||||||
if (!user || user.pluginAuth !== null || !password) return null
|
if (!user || user.pluginAuth !== null || !password) return null
|
||||||
|
|
|
@ -154,13 +154,15 @@ async function handlePasswordGrant (options: {
|
||||||
|
|
||||||
const user = await getUser(usernameOrEmail, password, bypassLogin)
|
const user = await getUser(usernameOrEmail, password, bypassLogin)
|
||||||
if (!user) {
|
if (!user) {
|
||||||
const registration = await UserRegistrationModel.loadByEmailOrUsername(usernameOrEmail)
|
const registrations = await UserRegistrationModel.listByEmailCaseInsensitiveOrUsername(usernameOrEmail)
|
||||||
|
|
||||||
if (registration?.state === UserRegistrationState.REJECTED) {
|
if (registrations.length === 1) {
|
||||||
|
if (registrations[0].state === UserRegistrationState.REJECTED) {
|
||||||
throw new RegistrationApprovalRejected('Registration approval for this account has been rejected')
|
throw new RegistrationApprovalRejected('Registration approval for this account has been rejected')
|
||||||
} else if (registration?.state === UserRegistrationState.PENDING) {
|
} else if (registrations[0].state === UserRegistrationState.PENDING) {
|
||||||
throw new RegistrationWaitingForApproval('Registration for this account is awaiting approval')
|
throw new RegistrationWaitingForApproval('Registration for this account is awaiting approval')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw new InvalidGrantError('Invalid grant: user credentials are invalid')
|
throw new InvalidGrantError('Invalid grant: user credentials are invalid')
|
||||||
}
|
}
|
||||||
|
|
|
@ -237,6 +237,12 @@ async function isUserQuotaValid (options: {
|
||||||
return true
|
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 {
|
export {
|
||||||
|
@ -250,7 +256,8 @@ export {
|
||||||
sendVerifyRegistrationEmail,
|
sendVerifyRegistrationEmail,
|
||||||
|
|
||||||
isUserQuotaValid,
|
isUserQuotaValid,
|
||||||
buildUser
|
buildUser,
|
||||||
|
getUserByEmailPermissive
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||||
import { HttpStatusCode, UserRightType } from '@peertube/peertube-models'
|
import { HttpStatusCode, UserRightType } from '@peertube/peertube-models'
|
||||||
|
import { getUserByEmailPermissive } from '@server/lib/user.js'
|
||||||
import { ActorModel } from '@server/models/actor/actor.js'
|
import { ActorModel } from '@server/models/actor/actor.js'
|
||||||
import { UserModel } from '@server/models/user/user.js'
|
import { UserModel } from '@server/models/user/user.js'
|
||||||
import { MAccountId, MUserAccountId, MUserDefault } from '@server/types/models/index.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)
|
return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
|
export function checkUserEmailExistPermissive (email: string, res: express.Response, abortResponse = true) {
|
||||||
return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
|
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) {
|
export async function checkUsernameOrEmailDoNotAlreadyExist (username: string, email: string, res: express.Response) {
|
||||||
const user = await UserModel.loadByUsernameOrEmail(username, email)
|
const existingUser = await UserModel.loadByUsernameOrEmailCaseInsensitive(username)
|
||||||
|
const existingEmail = await UserModel.loadByUsernameOrEmailCaseInsensitive(email)
|
||||||
|
|
||||||
if (user) {
|
if (existingUser.length > 0 || existingEmail.length > 0) {
|
||||||
res.fail({
|
res.fail({
|
||||||
status: HttpStatusCode.CONFLICT_409,
|
status: HttpStatusCode.CONFLICT_409,
|
||||||
message: 'User with this username or email already exists.'
|
message: 'User with this username or email already exists.'
|
||||||
|
@ -37,6 +43,20 @@ export async function checkUserNameOrEmailDoNotAlreadyExist (username: string, e
|
||||||
return true
|
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) {
|
export async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) {
|
||||||
const user = await finder()
|
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 { MRegistration } from '@server/types/models/index.js'
|
||||||
import { forceNumber, pick } from '@peertube/peertube-core-utils'
|
import { forceNumber, pick } from '@peertube/peertube-core-utils'
|
||||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||||
|
import { getUserByEmailPermissive } from '@server/lib/user.js'
|
||||||
|
|
||||||
function checkRegistrationIdExist (idArg: number | string, res: express.Response) {
|
function checkRegistrationIdExist (idArg: number | string, res: express.Response) {
|
||||||
const id = forceNumber(idArg)
|
const id = forceNumber(idArg)
|
||||||
return checkRegistrationExist(() => UserRegistrationModel.load(id), res)
|
return checkRegistrationExist(() => UserRegistrationModel.load(id), res)
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkRegistrationEmailExist (email: string, res: express.Response, abortResponse = true) {
|
function checkRegistrationEmailExistPermissive (email: string, res: express.Response, abortResponse = true) {
|
||||||
return checkRegistrationExist(() => UserRegistrationModel.loadByEmail(email), res, abortResponse)
|
return checkRegistrationExist(async () => {
|
||||||
|
const registrations = await UserRegistrationModel.listByEmailCaseInsensitive(email)
|
||||||
|
|
||||||
|
return getUserByEmailPermissive(registrations, email)
|
||||||
|
}, res, abortResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkRegistrationHandlesDoNotAlreadyExist (options: {
|
async function checkRegistrationHandlesDoNotAlreadyExist (options: {
|
||||||
|
@ -21,9 +26,11 @@ async function checkRegistrationHandlesDoNotAlreadyExist (options: {
|
||||||
}) {
|
}) {
|
||||||
const { res } = 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({
|
res.fail({
|
||||||
status: HttpStatusCode.CONFLICT_409,
|
status: HttpStatusCode.CONFLICT_409,
|
||||||
message: 'Registration with this username, channel name or email already exists.'
|
message: 'Registration with this username, channel name or email already exists.'
|
||||||
|
@ -54,7 +61,7 @@ async function checkRegistrationExist (finder: () => Promise<MRegistration>, res
|
||||||
|
|
||||||
export {
|
export {
|
||||||
checkRegistrationIdExist,
|
checkRegistrationIdExist,
|
||||||
checkRegistrationEmailExist,
|
checkRegistrationEmailExistPermissive,
|
||||||
checkRegistrationHandlesDoNotAlreadyExist,
|
checkRegistrationHandlesDoNotAlreadyExist,
|
||||||
checkRegistrationExist
|
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 express from 'express'
|
||||||
import { body, param } from 'express-validator'
|
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 { logger } from '../../../helpers/logger.js'
|
||||||
import { Redis } from '../../../lib/redis.js'
|
import { Redis } from '../../../lib/redis.js'
|
||||||
import { areValidationErrors, checkUserEmailExist, checkUserIdExist } from '../shared/index.js'
|
import { areValidationErrors, checkUserEmailExistPermissive, checkUserIdExist } from '../shared/index.js'
|
||||||
import { checkRegistrationEmailExist, checkRegistrationIdExist } from './shared/user-registrations.js'
|
import { checkRegistrationEmailExistPermissive, checkRegistrationIdExist } from './shared/user-registrations.js'
|
||||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
|
||||||
|
|
||||||
const usersAskSendVerifyEmailValidator = [
|
export const usersAskSendVerifyEmailValidator = [
|
||||||
body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
|
body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
|
||||||
|
|
||||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
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')
|
}, 'filter:api.email-verification.ask-send-verify-email.body')
|
||||||
|
|
||||||
const [ userExists, registrationExists ] = await Promise.all([
|
const [ userExists, registrationExists ] = await Promise.all([
|
||||||
checkUserEmailExist(email, res, false),
|
checkUserEmailExistPermissive(email, res, false),
|
||||||
checkRegistrationEmailExist(email, res, false)
|
checkRegistrationEmailExistPermissive(email, res, false)
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!userExists && !registrationExists) {
|
if (!userExists && !registrationExists) {
|
||||||
|
@ -40,7 +40,7 @@ const usersAskSendVerifyEmailValidator = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const usersVerifyEmailValidator = [
|
export const usersVerifyEmailValidator = [
|
||||||
param('id')
|
param('id')
|
||||||
.isInt().not().isEmpty().withMessage('Should have a valid id'),
|
.isInt().not().isEmpty().withMessage('Should have a valid id'),
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ const usersVerifyEmailValidator = [
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const registrationVerifyEmailValidator = [
|
export const registrationVerifyEmailValidator = [
|
||||||
param('registrationId')
|
param('registrationId')
|
||||||
.isInt().not().isEmpty().withMessage('Should have a valid registrationId'),
|
.isInt().not().isEmpty().withMessage('Should have a valid registrationId'),
|
||||||
|
|
||||||
|
@ -88,12 +88,3 @@ const registrationVerifyEmailValidator = [
|
||||||
return next()
|
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 { isVideoChannelDisplayNameValid, isVideoChannelUsernameValid } from '../../../helpers/custom-validators/video-channels.js'
|
||||||
import { isSignupAllowed, isSignupAllowedForCurrentIP, SignupMode } from '../../../lib/signup.js'
|
import { isSignupAllowed, isSignupAllowedForCurrentIP, SignupMode } from '../../../lib/signup.js'
|
||||||
import { ActorModel } from '../../../models/actor/actor.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'
|
import { checkRegistrationHandlesDoNotAlreadyExist, checkRegistrationIdExist } from './shared/user-registrations.js'
|
||||||
|
|
||||||
const usersDirectRegistrationValidator = usersCommonRegistrationValidatorFactory()
|
const usersDirectRegistrationValidator = usersCommonRegistrationValidatorFactory()
|
||||||
|
@ -182,7 +182,7 @@ function usersCommonRegistrationValidatorFactory (additionalValidationChain: Val
|
||||||
|
|
||||||
const body: UserRegister | UserRegistrationRequest = req.body
|
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) {
|
||||||
if (!body.channel.name || !body.channel.displayName) {
|
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 { ActorModel } from '../../../models/actor/actor.js'
|
||||||
import {
|
import {
|
||||||
areValidationErrors,
|
areValidationErrors,
|
||||||
|
checkEmailDoesNotAlreadyExist,
|
||||||
checkUserCanManageAccount,
|
checkUserCanManageAccount,
|
||||||
checkUserEmailExist,
|
checkUserEmailExistPermissive,
|
||||||
checkUserIdExist,
|
checkUserIdExist,
|
||||||
checkUserNameOrEmailDoNotAlreadyExist,
|
checkUsernameOrEmailDoNotAlreadyExist,
|
||||||
doesVideoChannelIdExist,
|
doesVideoChannelIdExist,
|
||||||
doesVideoExist,
|
doesVideoExist,
|
||||||
isValidVideoIdParam
|
isValidVideoIdParam
|
||||||
|
@ -85,7 +86,7 @@ export const usersAddValidator = [
|
||||||
|
|
||||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
if (areValidationErrors(req, res, { omitBodyLog: true })) return
|
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
|
const authUser = res.locals.oauth.token.User
|
||||||
if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.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.' })
|
return res.fail({ message: 'Cannot change root role.' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.body.email && !await checkEmailDoesNotAlreadyExist(req.body.email, res)) return
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -278,6 +281,8 @@ export const usersUpdateMeValidator = [
|
||||||
|
|
||||||
if (areValidationErrors(req, res, { omitBodyLog: true })) return
|
if (areValidationErrors(req, res, { omitBodyLog: true })) return
|
||||||
|
|
||||||
|
if (req.body.email && !await checkEmailDoesNotAlreadyExist(req.body.email, res)) return
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -339,7 +344,7 @@ export const usersAskResetPasswordValidator = [
|
||||||
email: req.body.email
|
email: req.body.email
|
||||||
}, 'filter:api.users.ask-reset-password.body')
|
}, 'filter:api.users.ask-reset-password.body')
|
||||||
|
|
||||||
const exists = await checkUserEmailExist(email, res, false)
|
const exists = await checkUserEmailExistPermissive(email, res, false)
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
logger.debug('User with email %s does not exist (asking reset password).', email)
|
logger.debug('User with email %s does not exist (asking reset password).', email)
|
||||||
// Do not leak our emails
|
// 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 { cryptPassword } from '@server/helpers/peertube-crypto.js'
|
||||||
import { USER_REGISTRATION_STATES } from '@server/initializers/constants.js'
|
import { USER_REGISTRATION_STATES } from '@server/initializers/constants.js'
|
||||||
import { MRegistration, MRegistrationFormattable } from '@server/types/models/index.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 {
|
import {
|
||||||
AllowNull,
|
AllowNull,
|
||||||
BeforeCreate,
|
BeforeCreate,
|
||||||
|
@ -129,36 +129,49 @@ export class UserRegistrationModel extends SequelizeModel<UserRegistrationModel>
|
||||||
return UserRegistrationModel.findByPk(id)
|
return UserRegistrationModel.findByPk(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadByEmail (email: string): Promise<MRegistration> {
|
static listByEmailCaseInsensitive (email: string): Promise<MRegistration[]> {
|
||||||
const query = {
|
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 = {
|
const query = {
|
||||||
where: {
|
where: {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ email: emailOrUsername },
|
where(
|
||||||
|
fn('LOWER', col('email')),
|
||||||
|
'=',
|
||||||
|
emailOrUsername.toLowerCase()
|
||||||
|
),
|
||||||
|
|
||||||
{ username: emailOrUsername }
|
{ username: emailOrUsername }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return UserRegistrationModel.findOne(query)
|
return UserRegistrationModel.findAll(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadByEmailOrHandle (options: {
|
static listByEmailCaseInsensitiveOrHandle (options: {
|
||||||
email: string
|
email: string
|
||||||
username: string
|
username: string
|
||||||
channelHandle?: string
|
channelHandle?: string
|
||||||
}): Promise<MRegistration> {
|
}): Promise<MRegistration[]> {
|
||||||
const { email, username, channelHandle } = options
|
const { email, username, channelHandle } = options
|
||||||
|
|
||||||
let or: WhereOptions = [
|
let or: WhereOptions = [
|
||||||
{ email },
|
where(
|
||||||
|
fn('LOWER', col('email')),
|
||||||
|
'=',
|
||||||
|
email.toLowerCase()
|
||||||
|
),
|
||||||
{ channelHandle: username },
|
{ channelHandle: username },
|
||||||
{ 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)
|
return UserModel.scope(ScopeNames.FOR_ME_API).findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadByEmail (email: string): Promise<MUserDefault> {
|
static loadByEmailCaseInsensitive (email: string): Promise<MUserDefault[]> {
|
||||||
const query = {
|
const query = {
|
||||||
where: {
|
where: where(
|
||||||
email
|
fn('LOWER', col('email')),
|
||||||
}
|
'=',
|
||||||
|
email.toLowerCase()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return UserModel.findOne(query)
|
return UserModel.findAll(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadByUsernameOrEmail (username: string, email?: string): Promise<MUserDefault> {
|
static loadByUsernameOrEmailCaseInsensitive (usernameOrEmail: string): Promise<MUserDefault[]> {
|
||||||
if (!email) email = username
|
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
where: {
|
where: {
|
||||||
[Op.or]: [
|
[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> {
|
static loadByVideoId (videoId: number): Promise<MUserDefault> {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue