From efa32646ed4bf891e58a24ae889e0a5bf032327a Mon Sep 17 00:00:00 2001 From: Shalabh Agarwal <34604329+serendipty01@users.noreply.github.com> Date: Wed, 10 Sep 2025 20:21:04 +0530 Subject: [PATCH 1/2] 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 eaaf63ba7c62c4bb0c303641d884a31af4b82f0b. * Update PR --------- Co-authored-by: Chocobozzz --- .../users/user-edit/user-create.component.ts | 14 +++-- .../user-edit/user-password.component.ts | 17 ++--- .../my-account-change-password.component.ts | 9 ++- .../reset-password.component.ts | 11 ++-- .../steps/register-step-user.component.html | 2 +- .../steps/register-step-user.component.ts | 19 +++--- .../shared/form-validators/user-validators.ts | 44 +++++++------ config/default.yaml | 3 + config/dev.yaml | 4 ++ config/production.yaml.example | 5 +- config/test.yaml | 4 ++ .../models/src/server/server-config.model.ts | 9 +++ .../src/api/check-params/registrations.ts | 21 +++---- packages/tests/src/api/moderation/abuses.ts | 8 +-- packages/tests/src/api/server/config.ts | 63 +++++++++++++++++++ packages/tests/src/api/users/registrations.ts | 4 +- server/core/helpers/peertube-crypto.ts | 5 +- .../core/initializers/checker-before-init.ts | 1 + server/core/initializers/config.ts | 6 ++ server/core/initializers/constants.ts | 2 +- server/core/lib/server-config-manager.ts | 11 +++- server/core/models/user/user-registration.ts | 7 +-- server/core/models/user/user.ts | 2 - 23 files changed, 195 insertions(+), 76 deletions(-) diff --git a/client/src/app/+admin/overview/users/user-edit/user-create.component.ts b/client/src/app/+admin/overview/users/user-edit/user-create.component.ts index 5a5f09733..5025a275c 100644 --- a/client/src/app/+admin/overview/users/user-edit/user-create.component.ts +++ b/client/src/app/+admin/overview/users/user-edit/user-create.component.ts @@ -6,12 +6,12 @@ import { AuthService, Notifier, ScreenService, ServerService } from '@app/core' import { USER_CHANNEL_NAME_VALIDATOR, USER_EMAIL_VALIDATOR, - USER_PASSWORD_OPTIONAL_VALIDATOR, - USER_PASSWORD_VALIDATOR, USER_ROLE_VALIDATOR, USER_USERNAME_VALIDATOR, USER_VIDEO_QUOTA_DAILY_VALIDATOR, - USER_VIDEO_QUOTA_VALIDATOR + USER_VIDEO_QUOTA_VALIDATOR, + getUserNewPasswordOptionalValidator, + getUserNewPasswordValidator } from '@app/shared/form-validators/user-validators' import { AdminConfigService } from '@app/shared/shared-admin/admin-config.service' import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service' @@ -76,11 +76,17 @@ export class UserCreateComponent extends UserEdit implements OnInit { videoQuotaDaily: -1 } + const passwordConstraints = this.serverService.getHTMLConfig().fieldsConstraints.users.password + this.buildForm({ username: USER_USERNAME_VALIDATOR, channelName: USER_CHANNEL_NAME_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, videoQuota: USER_VIDEO_QUOTA_VALIDATOR, videoQuotaDaily: USER_VIDEO_QUOTA_DAILY_VALIDATOR, diff --git a/client/src/app/+admin/overview/users/user-edit/user-password.component.ts b/client/src/app/+admin/overview/users/user-edit/user-password.component.ts index 92329211c..13a32a8a7 100644 --- a/client/src/app/+admin/overview/users/user-edit/user-password.component.ts +++ b/client/src/app/+admin/overview/users/user-edit/user-password.component.ts @@ -1,12 +1,12 @@ +import { NgClass, NgIf } from '@angular/common' import { Component, OnInit, inject, input } from '@angular/core' -import { Notifier } from '@app/core' -import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +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 { 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 { UserUpdate } from '@peertube/peertube-models' @Component({ selector: 'my-user-password', @@ -18,6 +18,7 @@ export class UserPasswordComponent extends FormReactive implements OnInit { protected formReactiveService = inject(FormReactiveService) private notifier = inject(Notifier) private userAdminService = inject(UserAdminService) + private serverService = inject(ServerService) readonly userId = input(undefined) readonly username = input(undefined) @@ -26,9 +27,9 @@ export class UserPasswordComponent extends FormReactive implements OnInit { showPassword = false ngOnInit () { - this.buildForm({ - password: USER_PASSWORD_VALIDATOR - }) + const { minLength, maxLength } = this.serverService.getHTMLConfig().fieldsConstraints.users.password + + this.buildForm({ password: getUserNewPasswordValidator(minLength, maxLength) }) } formValidated () { diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts b/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts index 69f0928ec..fcf3bea3f 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts @@ -1,11 +1,11 @@ import { NgIf } from '@angular/common' import { Component, OnInit, inject } from '@angular/core' import { FormsModule, ReactiveFormsModule } from '@angular/forms' -import { AuthService, Notifier, UserService } from '@app/core' +import { AuthService, Notifier, ServerService, UserService } from '@app/core' import { USER_CONFIRM_PASSWORD_VALIDATOR, USER_EXISTING_PASSWORD_VALIDATOR, - USER_PASSWORD_VALIDATOR + getUserNewPasswordValidator } from '@app/shared/form-validators/user-validators' import { FormReactive } from '@app/shared/shared-forms/form-reactive' 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 authService = inject(AuthService) private userService = inject(UserService) + private serverService = inject(ServerService) error: string user: User ngOnInit () { + const { minLength, maxLength } = this.serverService.getHTMLConfig().fieldsConstraints.users.password + this.buildForm({ 'current-password': USER_EXISTING_PASSWORD_VALIDATOR, - 'new-password': USER_PASSWORD_VALIDATOR, + 'new-password': getUserNewPasswordValidator(minLength, maxLength), 'new-confirmed-password': USER_CONFIRM_PASSWORD_VALIDATOR }) diff --git a/client/src/app/+reset-password/reset-password.component.ts b/client/src/app/+reset-password/reset-password.component.ts index 1f0a286f8..e7538dd92 100644 --- a/client/src/app/+reset-password/reset-password.component.ts +++ b/client/src/app/+reset-password/reset-password.component.ts @@ -1,12 +1,12 @@ import { Component, OnInit, inject } from '@angular/core' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' 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 { 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 { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service' import { InputTextComponent } from '../shared/shared-forms/input-text.component' -import { FormsModule, ReactiveFormsModule } from '@angular/forms' @Component({ templateUrl: './reset-password.component.html', @@ -16,6 +16,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms' export class ResetPasswordComponent extends FormReactive implements OnInit { protected formReactiveService = inject(FormReactiveService) private userService = inject(UserService) + private serverService = inject(ServerService) private notifier = inject(Notifier) private router = inject(Router) private route = inject(ActivatedRoute) @@ -24,8 +25,10 @@ export class ResetPasswordComponent extends FormReactive implements OnInit { private verificationString: string ngOnInit () { + const { minLength, maxLength } = this.serverService.getHTMLConfig().fieldsConstraints.users.password + this.buildForm({ - 'password': USER_PASSWORD_VALIDATOR, + 'password': getUserNewPasswordValidator(minLength, maxLength), 'password-confirm': RESET_PASSWORD_CONFIRM_VALIDATOR }) diff --git a/client/src/app/+signup/+register/steps/register-step-user.component.html b/client/src/app/+signup/+register/steps/register-step-user.component.html index 922eb2605..6f6f00583 100644 --- a/client/src/app/+signup/+register/steps/register-step-user.component.html +++ b/client/src/app/+signup/+register/steps/register-step-user.component.html @@ -62,7 +62,7 @@
-
{{ getMinPasswordLengthMessage() }}
+
{{ minPasswordLengthMessage }}
() + minPasswordLengthMessage: string + get instanceHost () { return window.location.host } ngOnInit () { + const passwordConstraints = this.serverService.getHTMLConfig().fieldsConstraints.users.password + const passwordValidator = getUserNewPasswordValidator(passwordConstraints.minLength, passwordConstraints.maxLength) + + this.minPasswordLengthMessage = passwordValidator.MESSAGES.minlength + this.buildForm({ displayName: USER_DISPLAY_NAME_REQUIRED_VALIDATOR, username: USER_USERNAME_VALIDATOR, - password: USER_PASSWORD_VALIDATOR, + password: passwordValidator, email: USER_EMAIL_VALIDATOR }) @@ -51,10 +60,6 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit { .subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue)) } - getMinPasswordLengthMessage () { - return USER_PASSWORD_VALIDATOR.MESSAGES.minlength - } - private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { const username = this.form.value['username'] || '' diff --git a/client/src/app/shared/form-validators/user-validators.ts b/client/src/app/shared/form-validators/user-validators.ts index ed6e0582e..64b165f56 100644 --- a/client/src/app/shared/form-validators/user-validators.ts +++ b/client/src/app/shared/form-validators/user-validators.ts @@ -1,4 +1,4 @@ -import { Validators } from '@angular/forms' +import { ValidatorFn, Validators } from '@angular/forms' import { BuildFormValidator } from './form-validator.model' 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 = { - VALIDATORS: [ - Validators.required, - Validators.minLength(6), - Validators.maxLength(255) - ], - MESSAGES: { - required: $localize`Password is required.`, - minlength: $localize`Password must be at least 6 characters long.`, - maxlength: $localize`Password cannot be more than 255 characters long.` +export function getUserNewPasswordValidator (minLength: number, maxLength: number) { + const base = getUserNewPasswordOptionalValidator(minLength, maxLength) + + return { + VALIDATORS: [ + Validators.required, + + ...base.VALIDATORS + ] as ValidatorFn[], + MESSAGES: { + required: $localize`Password is required.`, + + ...base.MESSAGES + } } } -export const USER_PASSWORD_OPTIONAL_VALIDATOR: BuildFormValidator = { - VALIDATORS: [ - Validators.minLength(6), - Validators.maxLength(255) - ], - MESSAGES: { - minlength: $localize`Password must be at least 6 characters long.`, - maxlength: $localize`Password cannot be more than 255 characters long.` +export function getUserNewPasswordOptionalValidator (minLength: number, maxLength: number) { + return { + VALIDATORS: [ + Validators.minLength(minLength), + Validators.maxLength(maxLength) + ] as ValidatorFn[], + MESSAGES: { + minlength: $localize`Password must be at least ${minLength} characters long.`, + maxlength: $localize`Password cannot be more than ${maxLength} characters long.` + } } } diff --git a/config/default.yaml b/config/default.yaml index 75d144f77..c8dedbcd0 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -567,6 +567,9 @@ user: default_channel_name: 'Main $1 channel' # The placeholder $1 is used to represent the user's username + password_constraints: + min_length: 8 + video_channels: max_per_user: 20 # Allows each user to create up to 20 video channels. diff --git a/config/dev.yaml b/config/dev.yaml index a75bf6860..85114f824 100644 --- a/config/dev.yaml +++ b/config/dev.yaml @@ -141,3 +141,7 @@ video_studio: transcoding: keep_original_file: false + +user: + password_constraints: + min_length: 6 diff --git a/config/production.yaml.example b/config/production.yaml.example index 1f7c310df..9e950c003 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -577,6 +577,9 @@ user: default_channel_name: 'Main $1 channel' # The placeholder $1 is used to represent the user's username + password_constraints: + min_length: 8 + video_channels: max_per_user: 20 # Allows each user to create up to 20 video channels. @@ -1196,5 +1199,5 @@ email: video_comments: # 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 diff --git a/config/test.yaml b/config/test.yaml index c5a6b0cf5..14a3dcb04 100644 --- a/config/test.yaml +++ b/config/test.yaml @@ -172,3 +172,7 @@ search: video_transcription: model: 'tiny' + +user: + password_constraints: + min_length: 6 diff --git a/packages/models/src/server/server-config.model.ts b/packages/models/src/server/server-config.model.ts index 1bd5a7e22..4bf483b25 100644 --- a/packages/models/src/server/server-config.model.ts +++ b/packages/models/src/server/server-config.model.ts @@ -439,6 +439,15 @@ export interface ServerConfig { nsfwFlagsSettings: { enabled: boolean } + + fieldsConstraints: { + users: { + password: { + minLength: number + maxLength: number + } + } + } } export type HTMLServerConfig = Omit diff --git a/packages/tests/src/api/check-params/registrations.ts b/packages/tests/src/api/check-params/registrations.ts index 5ba3a54da..4cf5cdc4a 100644 --- a/packages/tests/src/api/check-params/registrations.ts +++ b/packages/tests/src/api/check-params/registrations.ts @@ -27,10 +27,10 @@ describe('Test registrations API validators', function () { await setDefaultAccountAvatar([ server ]) await setDefaultChannelAvatar([ server ]) - await server.config.enableSignup(false); + await server.config.enableSignup(false) - ({ token: moderatorToken } = await server.users.generate('moderator', UserRole.MODERATOR)); - ({ token: userToken } = await server.users.generate('user', UserRole.USER)) + ;({ token: moderatorToken } = await server.users.generate('moderator', UserRole.MODERATOR)) + ;({ token: userToken } = await server.users.generate('user', UserRole.USER)) }) describe('Register', function () { @@ -46,7 +46,6 @@ describe('Test registrations API validators', function () { } describe('When registering a new user or requesting user registration', function () { - async function check (fields: any, expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400) { await server.config.enableSignup(false) await makePostBodyRequest({ url: server.url, path: registrationPath, fields, expectedStatus }) @@ -209,7 +208,6 @@ describe('Test registrations API validators', function () { }) describe('On direct registration', function () { - it('Should succeed with the correct params', async function () { await server.config.enableSignup(false) @@ -233,7 +231,6 @@ describe('Test registrations API validators', function () { }) describe('On registration request', function () { - before(async function () { this.timeout(60000) @@ -321,10 +318,10 @@ describe('Test registrations API validators', function () { before(async function () { 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: id2 } = await server.registrations.requestRegistration({ username: 'request_3', registrationReason: 'toto' })) + ;({ id: id1 } = await server.registrations.requestRegistration({ username: 'request_2', registrationReason: 'toto' })) + ;({ id: id2 } = await server.registrations.requestRegistration({ username: 'request_3', registrationReason: 'toto' })) }) it('Should fail to accept/reject registration without token', async function () { @@ -384,9 +381,9 @@ describe('Test registrations API validators', function () { let id3: number before(async function () { - ({ id: id1 } = await server.registrations.requestRegistration({ username: 'request_4', 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: id1 } = await server.registrations.requestRegistration({ username: 'request_4', registrationReason: 'toto' })) + ;({ id: id2 } = await server.registrations.requestRegistration({ username: 'request_5', registrationReason: 'toto' })) + ;({ id: id3 } = await server.registrations.requestRegistration({ username: 'request_6', registrationReason: 'toto' })) await server.registrations.accept({ id: id2, moderationResponse: 'tt' }) await server.registrations.reject({ id: id3, moderationResponse: 'tt' }) diff --git a/packages/tests/src/api/moderation/abuses.ts b/packages/tests/src/api/moderation/abuses.ts index 68f759435..5e08921f6 100644 --- a/packages/tests/src/api/moderation/abuses.ts +++ b/packages/tests/src/api/moderation/abuses.ts @@ -37,7 +37,6 @@ describe('Test abuses', function () { }) describe('Video abuses', function () { - before(async function () { this.timeout(50000) @@ -315,8 +314,8 @@ describe('Test abuses', function () { const abuse = body.data.find(a => a.id === createRes.abuse.id) expect(abuse.reason).to.equals(reason5) 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.endAt).to.equal(5, "ending 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') } }) @@ -374,7 +373,6 @@ describe('Test abuses', function () { }) describe('Comment abuses', function () { - async function getComment (server: PeerTubeServer, videoIdArg: number | string) { const videoId = typeof videoIdArg === 'string' ? await server.videos.getId({ uuid: videoIdArg }) @@ -570,7 +568,6 @@ describe('Test abuses', function () { }) describe('Account abuses', function () { - function getAccountFromServer (server: PeerTubeServer, targetName: string, targetServer: PeerTubeServer) { return server.accounts.get({ accountName: targetName + '@' + targetServer.host }) } @@ -711,7 +708,6 @@ describe('Test abuses', function () { }) describe('Common actions on abuses', function () { - it('Should update the state of an abuse', async function () { await commands[0].update({ abuseId: abuseServer1.id, body: { state: AbuseState.REJECTED } }) diff --git a/packages/tests/src/api/server/config.ts b/packages/tests/src/api/server/config.ts index 2f74ee911..ea2af9c6e 100644 --- a/packages/tests/src/api/server/config.ts +++ b/packages/tests/src/api/server/config.ts @@ -1022,3 +1022,66 @@ describe('Test config', function () { 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 ]) + }) +}) diff --git a/packages/tests/src/api/users/registrations.ts b/packages/tests/src/api/users/registrations.ts index d6fabb9b7..2031196d6 100644 --- a/packages/tests/src/api/users/registrations.ts +++ b/packages/tests/src/api/users/registrations.ts @@ -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 { UserRegistrationState, UserRole } from '@peertube/peertube-models' import { cleanupTests, @@ -11,6 +9,8 @@ import { setAccessTokensToServers, waitJobs } from '@peertube/peertube-server-commands' +import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' +import { expect } from 'chai' describe('Test registrations', function () { let server: PeerTubeServer diff --git a/server/core/helpers/peertube-crypto.ts b/server/core/helpers/peertube-crypto.ts index b8c0ae7f6..afe676bc6 100644 --- a/server/core/helpers/peertube-crypto.ts +++ b/server/core/helpers/peertube-crypto.ts @@ -20,6 +20,10 @@ function createPrivateAndPublicKeys () { async function comparePassword (plainPassword: string, hashPassword: string) { 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') return compare(plainPassword, hashPassword) @@ -110,7 +114,6 @@ export { comparePassword, createPrivateAndPublicKeys, cryptPassword, - encrypt, decrypt } diff --git a/server/core/initializers/checker-before-init.ts b/server/core/initializers/checker-before-init.ts index 5fe832006..b1f7bad7b 100644 --- a/server/core/initializers/checker-before-init.ts +++ b/server/core/initializers/checker-before-init.ts @@ -63,6 +63,7 @@ export function checkMissedConfig () { 'user.history.videos.enabled', 'user.video_quota', 'user.video_quota_daily', + 'user.password_constraints.min_length', 'video_channels.max_per_user', 'csp.enabled', 'csp.report_only', diff --git a/server/core/initializers/config.ts b/server/core/initializers/config.ts index d16e4ea73..d6306019f 100644 --- a/server/core/initializers/config.ts +++ b/server/core/initializers/config.ts @@ -507,6 +507,7 @@ const CONFIG = { get MINIMUM_AGE () { return config.get('signup.minimum_age') }, + FILTERS: { CIDR: { get WHITELIST () { @@ -534,6 +535,11 @@ const CONFIG = { }, get DEFAULT_CHANNEL_NAME () { return config.get('user.default_channel_name') + }, + PASSWORD_CONSTRAINTS: { + get MIN_LENGTH () { + return config.get('user.password_constraints.min_length') + } } }, VIDEO_CHANNELS: { diff --git a/server/core/initializers/constants.ts b/server/core/initializers/constants.ts index aaa353938..416cc285b 100644 --- a/server/core/initializers/constants.ts +++ b/server/core/initializers/constants.ts @@ -366,7 +366,7 @@ export const CONSTRAINTS_FIELDS = { NAME: { min: 1, max: 120 }, // Length DESCRIPTION: { min: 3, max: 1000 }, // 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_DAILY: { min: -1 }, VIDEO_LANGUAGES: { max: 500 }, // Array length diff --git a/server/core/lib/server-config-manager.ts b/server/core/lib/server-config-manager.ts index 1a9b78660..e15d5badf 100644 --- a/server/core/lib/server-config-manager.ts +++ b/server/core/lib/server-config-manager.ts @@ -397,6 +397,15 @@ class ServerConfigManager { nsfwFlagsSettings: { 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, requiresApproval: CONFIG.SIGNUP.REQUIRES_APPROVAL, requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION - } + } satisfies ServerConfig['signup'] const htmlConfig = await this.getHTMLServerConfig() diff --git a/server/core/models/user/user-registration.ts b/server/core/models/user/user-registration.ts index bd066506f..7647a7a7a 100644 --- a/server/core/models/user/user-registration.ts +++ b/server/core/models/user/user-registration.ts @@ -1,3 +1,4 @@ +import { forceNumber } from '@peertube/peertube-core-utils' import { UserRegistration, UserRegistrationState, type UserRegistrationStateType } from '@peertube/peertube-models' import { isRegistrationModerationResponseValid, @@ -22,10 +23,9 @@ import { Table, UpdatedAt } from 'sequelize-typescript' -import { isUserDisplayNameValid, isUserEmailVerifiedValid, isUserPasswordValid } from '../../helpers/custom-validators/users.js' -import { SequelizeModel, getSort, parseAggregateResult, throwIfNotValid } from '../shared/index.js' +import { isUserDisplayNameValid, isUserEmailVerifiedValid } from '../../helpers/custom-validators/users.js' +import { getSort, parseAggregateResult, SequelizeModel, throwIfNotValid } from '../shared/index.js' import { UserModel } from './user.js' -import { forceNumber } from '@peertube/peertube-core-utils' @Table({ tableName: 'userRegistration', @@ -65,7 +65,6 @@ export class UserRegistrationModel extends SequelizeModel declare moderationResponse: string @AllowNull(true) - @Is('RegistrationPassword', value => throwIfNotValid(value, isUserPasswordValid, 'registration password', true)) @Column declare password: string diff --git a/server/core/models/user/user.ts b/server/core/models/user/user.ts index 4afde1b32..4559dbc6e 100644 --- a/server/core/models/user/user.ts +++ b/server/core/models/user/user.ts @@ -57,7 +57,6 @@ import { isUserNoModal, isUserNSFWPolicyValid, isUserP2PEnabledValid, - isUserPasswordValid, isUserRoleValid, isUserVideoLanguages, isUserVideoQuotaDailyValid, @@ -290,7 +289,6 @@ type WhereUserIdScopeOptions = { whereUserId?: '$userId' | '"UserModel"."id"' } }) export class UserModel extends SequelizeModel { @AllowNull(true) - @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true)) @Column declare password: string From bbc1afada5638bdb3221aeb4f2203c9ebf8ff99c Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 10 Sep 2025 15:16:40 +0200 Subject: [PATCH 2/2] Optimize updating oauth tokens activity --- client/angular.json | 2 +- client/src/standalone/build-tools/vite-utils.ts | 2 +- .../update-token-session-scheduler.ts | 17 +++++++---------- server/core/middlewares/auth.ts | 3 +-- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/client/angular.json b/client/angular.json index a63a14e58..9e0795154 100644 --- a/client/angular.json +++ b/client/angular.json @@ -185,7 +185,7 @@ "." ], "sass": { - "silenceDeprecations": [ "import", "mixed-decls", "color-functions", "global-builtin" ] + "silenceDeprecations": [ "import", "color-functions", "global-builtin" ] } }, "assets": [ diff --git a/client/src/standalone/build-tools/vite-utils.ts b/client/src/standalone/build-tools/vite-utils.ts index bf366659c..60297ac9e 100644 --- a/client/src/standalone/build-tools/vite-utils.ts +++ b/client/src/standalone/build-tools/vite-utils.ts @@ -7,7 +7,7 @@ export function getCSSConfig (root: string) { api: 'modern-compiler', loadPaths: [ resolve(root, './src/sass/include') ], // FIXME: Wait for bootstrap upgrade that fixes deprecated sass utils - silenceDeprecations: [ 'import', 'mixed-decls', 'color-functions', 'global-builtin' ] + silenceDeprecations: [ 'import', 'color-functions', 'global-builtin' ] } } } diff --git a/server/core/lib/schedulers/update-token-session-scheduler.ts b/server/core/lib/schedulers/update-token-session-scheduler.ts index b6e5a2b8e..67826617f 100644 --- a/server/core/lib/schedulers/update-token-session-scheduler.ts +++ b/server/core/lib/schedulers/update-token-session-scheduler.ts @@ -3,7 +3,6 @@ import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants.js' import { AbstractScheduler } from './abstract-scheduler.js' type UpdatePayload = { - id: number lastActivityDate: Date lastActivityIP: string lastActivityDevice: string @@ -14,29 +13,27 @@ export class UpdateTokenSessionScheduler extends AbstractScheduler { protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.UPDATE_TOKEN_SESSION - private readonly toUpdate = new Set() + private toUpdate = new Map() private constructor () { super() } - addToUpdate (payload: UpdatePayload) { - this.toUpdate.add(payload) + addToUpdate (id: number, payload: UpdatePayload) { + this.toUpdate.set(id, payload) } protected async internalExecute () { - const toUpdate = Array.from(this.toUpdate) - this.toUpdate.clear() + const entriesToUpdate = this.toUpdate.entries() + this.toUpdate = new Map() - for (const payload of toUpdate) { + for (const [ id, payload ] of entriesToUpdate) { await OAuthTokenModel.update({ lastActivityDate: payload.lastActivityDate, lastActivityIP: payload.lastActivityIP, lastActivityDevice: payload.lastActivityDevice }, { - where: { - id: payload.id - }, + where: { id }, // Prevent tokens cache invalidation, we don't update fields that are meaningful for this cache hooks: false }) diff --git a/server/core/middlewares/auth.ts b/server/core/middlewares/auth.ts index fc5fe5a04..c0460b546 100644 --- a/server/core/middlewares/auth.ts +++ b/server/core/middlewares/auth.ts @@ -13,8 +13,7 @@ export function authenticate (req: express.Request, res: express.Response, next: res.locals.oauth = { token } res.locals.authenticated = true - UpdateTokenSessionScheduler.Instance.addToUpdate({ - id: token.id, + UpdateTokenSessionScheduler.Instance.addToUpdate(token.id, { lastActivityDate: new Date(), lastActivityIP: req.ip, lastActivityDevice: req.header('user-agent')