1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-03 09:49:20 +02:00

feat: add user password constraints (#6945)

* feat: add user password length constraints

* add password length changes in locale files

* revert maximum password length changes

* add tests

* fix lint

* fix lint and test

* fix tests

* Revert "add password length changes in locale files"

This reverts commit eaaf63ba7c.

* Update PR

---------

Co-authored-by: Chocobozzz <me@florianbigard.com>
This commit is contained in:
Shalabh Agarwal 2025-09-10 20:21:04 +05:30 committed by GitHub
parent eedfb8b0a2
commit efa32646ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 195 additions and 76 deletions

View file

@ -6,12 +6,12 @@ import { AuthService, Notifier, ScreenService, ServerService } from '@app/core'
import { import {
USER_CHANNEL_NAME_VALIDATOR, USER_CHANNEL_NAME_VALIDATOR,
USER_EMAIL_VALIDATOR, USER_EMAIL_VALIDATOR,
USER_PASSWORD_OPTIONAL_VALIDATOR,
USER_PASSWORD_VALIDATOR,
USER_ROLE_VALIDATOR, USER_ROLE_VALIDATOR,
USER_USERNAME_VALIDATOR, USER_USERNAME_VALIDATOR,
USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_DAILY_VALIDATOR,
USER_VIDEO_QUOTA_VALIDATOR USER_VIDEO_QUOTA_VALIDATOR,
getUserNewPasswordOptionalValidator,
getUserNewPasswordValidator
} from '@app/shared/form-validators/user-validators' } from '@app/shared/form-validators/user-validators'
import { AdminConfigService } from '@app/shared/shared-admin/admin-config.service' import { AdminConfigService } from '@app/shared/shared-admin/admin-config.service'
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service' import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
@ -76,11 +76,17 @@ export class UserCreateComponent extends UserEdit implements OnInit {
videoQuotaDaily: -1 videoQuotaDaily: -1
} }
const passwordConstraints = this.serverService.getHTMLConfig().fieldsConstraints.users.password
this.buildForm({ this.buildForm({
username: USER_USERNAME_VALIDATOR, username: USER_USERNAME_VALIDATOR,
channelName: USER_CHANNEL_NAME_VALIDATOR, channelName: USER_CHANNEL_NAME_VALIDATOR,
email: USER_EMAIL_VALIDATOR, email: USER_EMAIL_VALIDATOR,
password: this.isPasswordOptional() ? USER_PASSWORD_OPTIONAL_VALIDATOR : USER_PASSWORD_VALIDATOR,
password: this.isPasswordOptional()
? getUserNewPasswordOptionalValidator(passwordConstraints.minLength, passwordConstraints.maxLength)
: getUserNewPasswordValidator(passwordConstraints.minLength, passwordConstraints.maxLength),
role: USER_ROLE_VALIDATOR, role: USER_ROLE_VALIDATOR,
videoQuota: USER_VIDEO_QUOTA_VALIDATOR, videoQuota: USER_VIDEO_QUOTA_VALIDATOR,
videoQuotaDaily: USER_VIDEO_QUOTA_DAILY_VALIDATOR, videoQuotaDaily: USER_VIDEO_QUOTA_DAILY_VALIDATOR,

View file

@ -1,12 +1,12 @@
import { NgClass, NgIf } from '@angular/common'
import { Component, OnInit, inject, input } from '@angular/core' import { Component, OnInit, inject, input } from '@angular/core'
import { Notifier } from '@app/core' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators' import { Notifier, ServerService } from '@app/core'
import { getUserNewPasswordValidator } from '@app/shared/form-validators/user-validators'
import { FormReactive } from '@app/shared/shared-forms/form-reactive' import { FormReactive } from '@app/shared/shared-forms/form-reactive'
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service' import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
import { UserUpdate } from '@peertube/peertube-models'
import { NgClass, NgIf } from '@angular/common'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { UserAdminService } from '@app/shared/shared-users/user-admin.service' import { UserAdminService } from '@app/shared/shared-users/user-admin.service'
import { UserUpdate } from '@peertube/peertube-models'
@Component({ @Component({
selector: 'my-user-password', selector: 'my-user-password',
@ -18,6 +18,7 @@ export class UserPasswordComponent extends FormReactive implements OnInit {
protected formReactiveService = inject(FormReactiveService) protected formReactiveService = inject(FormReactiveService)
private notifier = inject(Notifier) private notifier = inject(Notifier)
private userAdminService = inject(UserAdminService) private userAdminService = inject(UserAdminService)
private serverService = inject(ServerService)
readonly userId = input<number>(undefined) readonly userId = input<number>(undefined)
readonly username = input<string>(undefined) readonly username = input<string>(undefined)
@ -26,9 +27,9 @@ export class UserPasswordComponent extends FormReactive implements OnInit {
showPassword = false showPassword = false
ngOnInit () { ngOnInit () {
this.buildForm({ const { minLength, maxLength } = this.serverService.getHTMLConfig().fieldsConstraints.users.password
password: USER_PASSWORD_VALIDATOR
}) this.buildForm({ password: getUserNewPasswordValidator(minLength, maxLength) })
} }
formValidated () { formValidated () {

View file

@ -1,11 +1,11 @@
import { NgIf } from '@angular/common' import { NgIf } from '@angular/common'
import { Component, OnInit, inject } from '@angular/core' import { Component, OnInit, inject } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { AuthService, Notifier, UserService } from '@app/core' import { AuthService, Notifier, ServerService, UserService } from '@app/core'
import { import {
USER_CONFIRM_PASSWORD_VALIDATOR, USER_CONFIRM_PASSWORD_VALIDATOR,
USER_EXISTING_PASSWORD_VALIDATOR, USER_EXISTING_PASSWORD_VALIDATOR,
USER_PASSWORD_VALIDATOR getUserNewPasswordValidator
} from '@app/shared/form-validators/user-validators' } from '@app/shared/form-validators/user-validators'
import { FormReactive } from '@app/shared/shared-forms/form-reactive' import { FormReactive } from '@app/shared/shared-forms/form-reactive'
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service' import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
@ -25,14 +25,17 @@ export class MyAccountChangePasswordComponent extends FormReactive implements On
private notifier = inject(Notifier) private notifier = inject(Notifier)
private authService = inject(AuthService) private authService = inject(AuthService)
private userService = inject(UserService) private userService = inject(UserService)
private serverService = inject(ServerService)
error: string error: string
user: User user: User
ngOnInit () { ngOnInit () {
const { minLength, maxLength } = this.serverService.getHTMLConfig().fieldsConstraints.users.password
this.buildForm({ this.buildForm({
'current-password': USER_EXISTING_PASSWORD_VALIDATOR, 'current-password': USER_EXISTING_PASSWORD_VALIDATOR,
'new-password': USER_PASSWORD_VALIDATOR, 'new-password': getUserNewPasswordValidator(minLength, maxLength),
'new-confirmed-password': USER_CONFIRM_PASSWORD_VALIDATOR 'new-confirmed-password': USER_CONFIRM_PASSWORD_VALIDATOR
}) })

View file

@ -1,12 +1,12 @@
import { Component, OnInit, inject } from '@angular/core' import { Component, OnInit, inject } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { Notifier, UserService } from '@app/core' import { Notifier, ServerService, UserService } from '@app/core'
import { RESET_PASSWORD_CONFIRM_VALIDATOR } from '@app/shared/form-validators/reset-password-validators' import { RESET_PASSWORD_CONFIRM_VALIDATOR } from '@app/shared/form-validators/reset-password-validators'
import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators' import { getUserNewPasswordValidator } from '@app/shared/form-validators/user-validators'
import { FormReactive } from '@app/shared/shared-forms/form-reactive' import { FormReactive } from '@app/shared/shared-forms/form-reactive'
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service' import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
import { InputTextComponent } from '../shared/shared-forms/input-text.component' import { InputTextComponent } from '../shared/shared-forms/input-text.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
@Component({ @Component({
templateUrl: './reset-password.component.html', templateUrl: './reset-password.component.html',
@ -16,6 +16,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
export class ResetPasswordComponent extends FormReactive implements OnInit { export class ResetPasswordComponent extends FormReactive implements OnInit {
protected formReactiveService = inject(FormReactiveService) protected formReactiveService = inject(FormReactiveService)
private userService = inject(UserService) private userService = inject(UserService)
private serverService = inject(ServerService)
private notifier = inject(Notifier) private notifier = inject(Notifier)
private router = inject(Router) private router = inject(Router)
private route = inject(ActivatedRoute) private route = inject(ActivatedRoute)
@ -24,8 +25,10 @@ export class ResetPasswordComponent extends FormReactive implements OnInit {
private verificationString: string private verificationString: string
ngOnInit () { ngOnInit () {
const { minLength, maxLength } = this.serverService.getHTMLConfig().fieldsConstraints.users.password
this.buildForm({ this.buildForm({
'password': USER_PASSWORD_VALIDATOR, 'password': getUserNewPasswordValidator(minLength, maxLength),
'password-confirm': RESET_PASSWORD_CONFIRM_VALIDATOR 'password-confirm': RESET_PASSWORD_CONFIRM_VALIDATOR
}) })

View file

@ -62,7 +62,7 @@
<div class="col-md-12 col-xl-6 form-group"> <div class="col-md-12 col-xl-6 form-group">
<label for="password" i18n>Password</label> <label for="password" i18n>Password</label>
<div class="form-group-description">{{ getMinPasswordLengthMessage() }}</div> <div class="form-group-description">{{ minPasswordLengthMessage }}</div>
<my-input-text <my-input-text
formControlName="password" inputId="password" formControlName="password" inputId="password"

View file

@ -2,11 +2,12 @@ import { NgClass, NgIf } from '@angular/common'
import { Component, OnInit, inject, input, output } from '@angular/core' import { Component, OnInit, inject, input, output } from '@angular/core'
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
import { SignupService } from '@app/+signup/shared/signup.service' import { SignupService } from '@app/+signup/shared/signup.service'
import { ServerService } from '@app/core'
import { import {
USER_DISPLAY_NAME_REQUIRED_VALIDATOR, USER_DISPLAY_NAME_REQUIRED_VALIDATOR,
USER_EMAIL_VALIDATOR, USER_EMAIL_VALIDATOR,
USER_PASSWORD_VALIDATOR, USER_USERNAME_VALIDATOR,
USER_USERNAME_VALIDATOR getUserNewPasswordValidator
} from '@app/shared/form-validators/user-validators' } from '@app/shared/form-validators/user-validators'
import { FormReactive } from '@app/shared/shared-forms/form-reactive' import { FormReactive } from '@app/shared/shared-forms/form-reactive'
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service' import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
@ -24,21 +25,29 @@ import { InputTextComponent } from '../../../shared/shared-forms/input-text.comp
export class RegisterStepUserComponent extends FormReactive implements OnInit { export class RegisterStepUserComponent extends FormReactive implements OnInit {
protected formReactiveService = inject(FormReactiveService) protected formReactiveService = inject(FormReactiveService)
private signupService = inject(SignupService) private signupService = inject(SignupService)
private serverService = inject(ServerService)
readonly videoUploadDisabled = input(false) readonly videoUploadDisabled = input(false)
readonly requiresEmailVerification = input(false) readonly requiresEmailVerification = input(false)
readonly formBuilt = output<FormGroup>() readonly formBuilt = output<FormGroup>()
minPasswordLengthMessage: string
get instanceHost () { get instanceHost () {
return window.location.host return window.location.host
} }
ngOnInit () { ngOnInit () {
const passwordConstraints = this.serverService.getHTMLConfig().fieldsConstraints.users.password
const passwordValidator = getUserNewPasswordValidator(passwordConstraints.minLength, passwordConstraints.maxLength)
this.minPasswordLengthMessage = passwordValidator.MESSAGES.minlength
this.buildForm({ this.buildForm({
displayName: USER_DISPLAY_NAME_REQUIRED_VALIDATOR, displayName: USER_DISPLAY_NAME_REQUIRED_VALIDATOR,
username: USER_USERNAME_VALIDATOR, username: USER_USERNAME_VALIDATOR,
password: USER_PASSWORD_VALIDATOR, password: passwordValidator,
email: USER_EMAIL_VALIDATOR email: USER_EMAIL_VALIDATOR
}) })
@ -51,10 +60,6 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
.subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue)) .subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue))
} }
getMinPasswordLengthMessage () {
return USER_PASSWORD_VALIDATOR.MESSAGES.minlength
}
private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
const username = this.form.value['username'] || '' const username = this.form.value['username'] || ''

View file

@ -1,4 +1,4 @@
import { Validators } from '@angular/forms' import { ValidatorFn, Validators } from '@angular/forms'
import { BuildFormValidator } from './form-validator.model' import { BuildFormValidator } from './form-validator.model'
export const USER_USERNAME_REGEX_CHARACTERS = '[a-z0-9][a-z0-9._]' export const USER_USERNAME_REGEX_CHARACTERS = '[a-z0-9][a-z0-9._]'
@ -70,27 +70,33 @@ export const USER_OTP_TOKEN_VALIDATOR: BuildFormValidator = {
} }
} }
export const USER_PASSWORD_VALIDATOR = { export function getUserNewPasswordValidator (minLength: number, maxLength: number) {
VALIDATORS: [ const base = getUserNewPasswordOptionalValidator(minLength, maxLength)
Validators.required,
Validators.minLength(6), return {
Validators.maxLength(255) VALIDATORS: [
], Validators.required,
MESSAGES: {
required: $localize`Password is required.`, ...base.VALIDATORS
minlength: $localize`Password must be at least 6 characters long.`, ] as ValidatorFn[],
maxlength: $localize`Password cannot be more than 255 characters long.` MESSAGES: {
required: $localize`Password is required.`,
...base.MESSAGES
}
} }
} }
export const USER_PASSWORD_OPTIONAL_VALIDATOR: BuildFormValidator = { export function getUserNewPasswordOptionalValidator (minLength: number, maxLength: number) {
VALIDATORS: [ return {
Validators.minLength(6), VALIDATORS: [
Validators.maxLength(255) Validators.minLength(minLength),
], Validators.maxLength(maxLength)
MESSAGES: { ] as ValidatorFn[],
minlength: $localize`Password must be at least 6 characters long.`, MESSAGES: {
maxlength: $localize`Password cannot be more than 255 characters long.` minlength: $localize`Password must be at least ${minLength} characters long.`,
maxlength: $localize`Password cannot be more than ${maxLength} characters long.`
}
} }
} }

View file

@ -567,6 +567,9 @@ user:
default_channel_name: 'Main $1 channel' # The placeholder $1 is used to represent the user's username default_channel_name: 'Main $1 channel' # The placeholder $1 is used to represent the user's username
password_constraints:
min_length: 8
video_channels: video_channels:
max_per_user: 20 # Allows each user to create up to 20 video channels. max_per_user: 20 # Allows each user to create up to 20 video channels.

View file

@ -141,3 +141,7 @@ video_studio:
transcoding: transcoding:
keep_original_file: false keep_original_file: false
user:
password_constraints:
min_length: 6

View file

@ -577,6 +577,9 @@ user:
default_channel_name: 'Main $1 channel' # The placeholder $1 is used to represent the user's username default_channel_name: 'Main $1 channel' # The placeholder $1 is used to represent the user's username
password_constraints:
min_length: 8
video_channels: video_channels:
max_per_user: 20 # Allows each user to create up to 20 video channels. max_per_user: 20 # Allows each user to create up to 20 video channels.
@ -1196,5 +1199,5 @@ email:
video_comments: video_comments:
# Accept or not comments from remote instances # Accept or not comments from remote instances
# This setting is not retroactive: current remote comments of your instance will not be affected # This setting is not retroactive: current comments from remote platforms will not be deleted
accept_remote_comments: true accept_remote_comments: true

View file

@ -172,3 +172,7 @@ search:
video_transcription: video_transcription:
model: 'tiny' model: 'tiny'
user:
password_constraints:
min_length: 6

View file

@ -439,6 +439,15 @@ export interface ServerConfig {
nsfwFlagsSettings: { nsfwFlagsSettings: {
enabled: boolean enabled: boolean
} }
fieldsConstraints: {
users: {
password: {
minLength: number
maxLength: number
}
}
}
} }
export type HTMLServerConfig = Omit<ServerConfig, 'signup'> export type HTMLServerConfig = Omit<ServerConfig, 'signup'>

View file

@ -27,10 +27,10 @@ describe('Test registrations API validators', function () {
await setDefaultAccountAvatar([ server ]) await setDefaultAccountAvatar([ server ])
await setDefaultChannelAvatar([ server ]) await setDefaultChannelAvatar([ server ])
await server.config.enableSignup(false); await server.config.enableSignup(false)
({ token: moderatorToken } = await server.users.generate('moderator', UserRole.MODERATOR)); ;({ token: moderatorToken } = await server.users.generate('moderator', UserRole.MODERATOR))
({ token: userToken } = await server.users.generate('user', UserRole.USER)) ;({ token: userToken } = await server.users.generate('user', UserRole.USER))
}) })
describe('Register', function () { describe('Register', function () {
@ -46,7 +46,6 @@ describe('Test registrations API validators', function () {
} }
describe('When registering a new user or requesting user registration', function () { describe('When registering a new user or requesting user registration', function () {
async function check (fields: any, expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400) { async function check (fields: any, expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400) {
await server.config.enableSignup(false) await server.config.enableSignup(false)
await makePostBodyRequest({ url: server.url, path: registrationPath, fields, expectedStatus }) await makePostBodyRequest({ url: server.url, path: registrationPath, fields, expectedStatus })
@ -209,7 +208,6 @@ describe('Test registrations API validators', function () {
}) })
describe('On direct registration', function () { describe('On direct registration', function () {
it('Should succeed with the correct params', async function () { it('Should succeed with the correct params', async function () {
await server.config.enableSignup(false) await server.config.enableSignup(false)
@ -233,7 +231,6 @@ describe('Test registrations API validators', function () {
}) })
describe('On registration request', function () { describe('On registration request', function () {
before(async function () { before(async function () {
this.timeout(60000) this.timeout(60000)
@ -321,10 +318,10 @@ describe('Test registrations API validators', function () {
before(async function () { before(async function () {
this.timeout(60000) this.timeout(60000)
await server.config.enableSignup(true); await server.config.enableSignup(true)
({ id: id1 } = await server.registrations.requestRegistration({ username: 'request_2', registrationReason: 'toto' })); ;({ id: id1 } = await server.registrations.requestRegistration({ username: 'request_2', registrationReason: 'toto' }))
({ id: id2 } = await server.registrations.requestRegistration({ username: 'request_3', registrationReason: 'toto' })) ;({ id: id2 } = await server.registrations.requestRegistration({ username: 'request_3', registrationReason: 'toto' }))
}) })
it('Should fail to accept/reject registration without token', async function () { it('Should fail to accept/reject registration without token', async function () {
@ -384,9 +381,9 @@ describe('Test registrations API validators', function () {
let id3: number let id3: number
before(async function () { before(async function () {
({ id: id1 } = await server.registrations.requestRegistration({ username: 'request_4', registrationReason: 'toto' })); ;({ id: id1 } = await server.registrations.requestRegistration({ username: 'request_4', registrationReason: 'toto' }))
({ id: id2 } = await server.registrations.requestRegistration({ username: 'request_5', registrationReason: 'toto' })); ;({ id: id2 } = await server.registrations.requestRegistration({ username: 'request_5', registrationReason: 'toto' }))
({ id: id3 } = await server.registrations.requestRegistration({ username: 'request_6', registrationReason: 'toto' })) ;({ id: id3 } = await server.registrations.requestRegistration({ username: 'request_6', registrationReason: 'toto' }))
await server.registrations.accept({ id: id2, moderationResponse: 'tt' }) await server.registrations.accept({ id: id2, moderationResponse: 'tt' })
await server.registrations.reject({ id: id3, moderationResponse: 'tt' }) await server.registrations.reject({ id: id3, moderationResponse: 'tt' })

View file

@ -37,7 +37,6 @@ describe('Test abuses', function () {
}) })
describe('Video abuses', function () { describe('Video abuses', function () {
before(async function () { before(async function () {
this.timeout(50000) this.timeout(50000)
@ -315,8 +314,8 @@ describe('Test abuses', function () {
const abuse = body.data.find(a => a.id === createRes.abuse.id) const abuse = body.data.find(a => a.id === createRes.abuse.id)
expect(abuse.reason).to.equals(reason5) expect(abuse.reason).to.equals(reason5)
expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, 'predefined reasons do not match the one reported') expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, 'predefined reasons do not match the one reported')
expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported") expect(abuse.video.startAt).to.equal(1, 'starting timestamp doesn\'t match the one reported')
expect(abuse.video.endAt).to.equal(5, "ending timestamp doesn't match the one reported") expect(abuse.video.endAt).to.equal(5, 'ending timestamp doesn\'t match the one reported')
} }
}) })
@ -374,7 +373,6 @@ describe('Test abuses', function () {
}) })
describe('Comment abuses', function () { describe('Comment abuses', function () {
async function getComment (server: PeerTubeServer, videoIdArg: number | string) { async function getComment (server: PeerTubeServer, videoIdArg: number | string) {
const videoId = typeof videoIdArg === 'string' const videoId = typeof videoIdArg === 'string'
? await server.videos.getId({ uuid: videoIdArg }) ? await server.videos.getId({ uuid: videoIdArg })
@ -570,7 +568,6 @@ describe('Test abuses', function () {
}) })
describe('Account abuses', function () { describe('Account abuses', function () {
function getAccountFromServer (server: PeerTubeServer, targetName: string, targetServer: PeerTubeServer) { function getAccountFromServer (server: PeerTubeServer, targetName: string, targetServer: PeerTubeServer) {
return server.accounts.get({ accountName: targetName + '@' + targetServer.host }) return server.accounts.get({ accountName: targetName + '@' + targetServer.host })
} }
@ -711,7 +708,6 @@ describe('Test abuses', function () {
}) })
describe('Common actions on abuses', function () { describe('Common actions on abuses', function () {
it('Should update the state of an abuse', async function () { it('Should update the state of an abuse', async function () {
await commands[0].update({ abuseId: abuseServer1.id, body: { state: AbuseState.REJECTED } }) await commands[0].update({ abuseId: abuseServer1.id, body: { state: AbuseState.REJECTED } })

View file

@ -1022,3 +1022,66 @@ describe('Test config', function () {
await cleanupTests([ server ]) await cleanupTests([ server ])
}) })
}) })
describe('YAML config', function () {
let server: PeerTubeServer
let userToken: string
before(async function () {
this.timeout(30000)
server = await createSingleServer(1, {
user: {
password_constraints: {
min_length: 10
}
}
})
await setAccessTokensToServers([ server ])
})
it('Should update the minimum length of a password', async function () {
await server.users.create({ username: 'user10', password: 'short', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
await server.users.create({ username: 'user10', password: 's'.repeat(10) })
})
it('Should still be able to login with an old password', async function () {
})
it('Should update the minimum length of a password but still allow to login', async function () {
await server.kill()
await server.run({
user: {
password_constraints: {
min_length: 12
}
}
})
const res = await server.login.login({ user: { username: 'user10', password: 's'.repeat(10) } })
userToken = res.access_token
})
it('Should be able to change the password', async function () {
await server.users.updateMe({
token: userToken,
currentPassword: 's'.repeat(10),
password: 'password',
expectedStatus: HttpStatusCode.BAD_REQUEST_400
})
await server.users.updateMe({
token: userToken,
currentPassword: 's'.repeat(10),
password: 's'.repeat(12)
})
await server.login.login({ user: { username: 'user10', password: 's'.repeat(12) } })
})
after(async function () {
await cleanupTests([ server ])
})
})

View file

@ -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 { UserRegistrationState, UserRole } from '@peertube/peertube-models' import { UserRegistrationState, UserRole } from '@peertube/peertube-models'
import { import {
cleanupTests, cleanupTests,
@ -11,6 +9,8 @@ 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 { expect } from 'chai'
describe('Test registrations', function () { describe('Test registrations', function () {
let server: PeerTubeServer let server: PeerTubeServer

View file

@ -20,6 +20,10 @@ function createPrivateAndPublicKeys () {
async function comparePassword (plainPassword: string, hashPassword: string) { async function comparePassword (plainPassword: string, hashPassword: string) {
if (!plainPassword) return false if (!plainPassword) return false
if (Buffer.byteLength(plainPassword, 'utf8') > 72) {
throw new Error('Cannot compare more than 72 bytes with bcrypt')
}
const { compare } = await import('bcrypt') const { compare } = await import('bcrypt')
return compare(plainPassword, hashPassword) return compare(plainPassword, hashPassword)
@ -110,7 +114,6 @@ export {
comparePassword, comparePassword,
createPrivateAndPublicKeys, createPrivateAndPublicKeys,
cryptPassword, cryptPassword,
encrypt, encrypt,
decrypt decrypt
} }

View file

@ -63,6 +63,7 @@ export function checkMissedConfig () {
'user.history.videos.enabled', 'user.history.videos.enabled',
'user.video_quota', 'user.video_quota',
'user.video_quota_daily', 'user.video_quota_daily',
'user.password_constraints.min_length',
'video_channels.max_per_user', 'video_channels.max_per_user',
'csp.enabled', 'csp.enabled',
'csp.report_only', 'csp.report_only',

View file

@ -507,6 +507,7 @@ const CONFIG = {
get MINIMUM_AGE () { get MINIMUM_AGE () {
return config.get<number>('signup.minimum_age') return config.get<number>('signup.minimum_age')
}, },
FILTERS: { FILTERS: {
CIDR: { CIDR: {
get WHITELIST () { get WHITELIST () {
@ -534,6 +535,11 @@ const CONFIG = {
}, },
get DEFAULT_CHANNEL_NAME () { get DEFAULT_CHANNEL_NAME () {
return config.get<string>('user.default_channel_name') return config.get<string>('user.default_channel_name')
},
PASSWORD_CONSTRAINTS: {
get MIN_LENGTH () {
return config.get<number>('user.password_constraints.min_length')
}
} }
}, },
VIDEO_CHANNELS: { VIDEO_CHANNELS: {

View file

@ -366,7 +366,7 @@ export const CONSTRAINTS_FIELDS = {
NAME: { min: 1, max: 120 }, // Length NAME: { min: 1, max: 120 }, // Length
DESCRIPTION: { min: 3, max: 1000 }, // Length DESCRIPTION: { min: 3, max: 1000 }, // Length
USERNAME: { min: 1, max: 50 }, // Length USERNAME: { min: 1, max: 50 }, // Length
PASSWORD: { min: 6, max: 255 }, // Length PASSWORD: { min: CONFIG.USER.PASSWORD_CONSTRAINTS.MIN_LENGTH, max: 50 }, // Length
VIDEO_QUOTA: { min: -1 }, VIDEO_QUOTA: { min: -1 },
VIDEO_QUOTA_DAILY: { min: -1 }, VIDEO_QUOTA_DAILY: { min: -1 },
VIDEO_LANGUAGES: { max: 500 }, // Array length VIDEO_LANGUAGES: { max: 500 }, // Array length

View file

@ -397,6 +397,15 @@ class ServerConfigManager {
nsfwFlagsSettings: { nsfwFlagsSettings: {
enabled: CONFIG.NSFW_FLAGS_SETTINGS.ENABLED enabled: CONFIG.NSFW_FLAGS_SETTINGS.ENABLED
},
fieldsConstraints: {
users: {
password: {
minLength: CONSTRAINTS_FIELDS.USERS.PASSWORD.min,
maxLength: CONSTRAINTS_FIELDS.USERS.PASSWORD.max
}
}
} }
} }
} }
@ -423,7 +432,7 @@ class ServerConfigManager {
minimumAge: CONFIG.SIGNUP.MINIMUM_AGE, minimumAge: CONFIG.SIGNUP.MINIMUM_AGE,
requiresApproval: CONFIG.SIGNUP.REQUIRES_APPROVAL, requiresApproval: CONFIG.SIGNUP.REQUIRES_APPROVAL,
requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
} } satisfies ServerConfig['signup']
const htmlConfig = await this.getHTMLServerConfig() const htmlConfig = await this.getHTMLServerConfig()

View file

@ -1,3 +1,4 @@
import { forceNumber } from '@peertube/peertube-core-utils'
import { UserRegistration, UserRegistrationState, type UserRegistrationStateType } from '@peertube/peertube-models' import { UserRegistration, UserRegistrationState, type UserRegistrationStateType } from '@peertube/peertube-models'
import { import {
isRegistrationModerationResponseValid, isRegistrationModerationResponseValid,
@ -22,10 +23,9 @@ import {
Table, Table,
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { isUserDisplayNameValid, isUserEmailVerifiedValid, isUserPasswordValid } from '../../helpers/custom-validators/users.js' import { isUserDisplayNameValid, isUserEmailVerifiedValid } from '../../helpers/custom-validators/users.js'
import { SequelizeModel, getSort, parseAggregateResult, throwIfNotValid } from '../shared/index.js' import { getSort, parseAggregateResult, SequelizeModel, throwIfNotValid } from '../shared/index.js'
import { UserModel } from './user.js' import { UserModel } from './user.js'
import { forceNumber } from '@peertube/peertube-core-utils'
@Table({ @Table({
tableName: 'userRegistration', tableName: 'userRegistration',
@ -65,7 +65,6 @@ export class UserRegistrationModel extends SequelizeModel<UserRegistrationModel>
declare moderationResponse: string declare moderationResponse: string
@AllowNull(true) @AllowNull(true)
@Is('RegistrationPassword', value => throwIfNotValid(value, isUserPasswordValid, 'registration password', true))
@Column @Column
declare password: string declare password: string

View file

@ -57,7 +57,6 @@ import {
isUserNoModal, isUserNoModal,
isUserNSFWPolicyValid, isUserNSFWPolicyValid,
isUserP2PEnabledValid, isUserP2PEnabledValid,
isUserPasswordValid,
isUserRoleValid, isUserRoleValid,
isUserVideoLanguages, isUserVideoLanguages,
isUserVideoQuotaDailyValid, isUserVideoQuotaDailyValid,
@ -290,7 +289,6 @@ type WhereUserIdScopeOptions = { whereUserId?: '$userId' | '"UserModel"."id"' }
}) })
export class UserModel extends SequelizeModel<UserModel> { export class UserModel extends SequelizeModel<UserModel> {
@AllowNull(true) @AllowNull(true)
@Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true))
@Column @Column
declare password: string declare password: string