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:
parent
eedfb8b0a2
commit
efa32646ed
23 changed files with 195 additions and 76 deletions
|
@ -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,
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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'] || ''
|
||||||
|
|
||||||
|
|
|
@ -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.`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -141,3 +141,7 @@ video_studio:
|
||||||
|
|
||||||
transcoding:
|
transcoding:
|
||||||
keep_original_file: false
|
keep_original_file: false
|
||||||
|
|
||||||
|
user:
|
||||||
|
password_constraints:
|
||||||
|
min_length: 6
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -172,3 +172,7 @@ search:
|
||||||
|
|
||||||
video_transcription:
|
video_transcription:
|
||||||
model: 'tiny'
|
model: 'tiny'
|
||||||
|
|
||||||
|
user:
|
||||||
|
password_constraints:
|
||||||
|
min_length: 6
|
||||||
|
|
|
@ -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'>
|
||||||
|
|
|
@ -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' })
|
||||||
|
|
|
@ -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 } })
|
||||||
|
|
||||||
|
|
|
@ -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 ])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue