mirror of
https://github.com/CaramelFur/Picsur.git
synced 2025-11-15 07:55:49 +01:00
Refactor user validation
This commit is contained in:
61
frontend/src/app/models/forms/default-validators.ts
Normal file
61
frontend/src/app/models/forms/default-validators.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { ValidationErrors, Validators } from '@angular/forms';
|
||||||
|
|
||||||
|
// Match this with user entity in shared lib
|
||||||
|
// (Security is not handled here, this is only for the user)
|
||||||
|
|
||||||
|
function errorsToError(errors: ValidationErrors | null): string {
|
||||||
|
if (errors) {
|
||||||
|
const error = Object.keys(errors)[0];
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
return 'unkown';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UsernameValidators = [
|
||||||
|
Validators.required,
|
||||||
|
Validators.minLength(4),
|
||||||
|
Validators.maxLength(32),
|
||||||
|
Validators.pattern('^[a-zA-Z0-9]+$'),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CreateUsernameError = (
|
||||||
|
errors: ValidationErrors | null
|
||||||
|
): string => {
|
||||||
|
const error = errorsToError(errors);
|
||||||
|
switch (error) {
|
||||||
|
case 'required':
|
||||||
|
return 'Username is required';
|
||||||
|
case 'minlength':
|
||||||
|
return 'Username is too short';
|
||||||
|
case 'maxlength':
|
||||||
|
return 'Username is too long';
|
||||||
|
case 'pattern':
|
||||||
|
return 'Username can only contain letters and numbers';
|
||||||
|
default:
|
||||||
|
return 'Invalid username';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PasswordValidators = [
|
||||||
|
Validators.required,
|
||||||
|
Validators.minLength(4),
|
||||||
|
Validators.maxLength(1024),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CreatePasswordError = (
|
||||||
|
errors: ValidationErrors | null
|
||||||
|
): string => {
|
||||||
|
const error = errorsToError(errors);
|
||||||
|
switch (error) {
|
||||||
|
case 'required':
|
||||||
|
return 'Password is required';
|
||||||
|
case 'minlength':
|
||||||
|
return 'Password is too short';
|
||||||
|
case 'maxlength':
|
||||||
|
return 'Password is too long';
|
||||||
|
case 'compare':
|
||||||
|
return 'Password does not match';
|
||||||
|
default:
|
||||||
|
return 'Invalid password';
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,43 +1,29 @@
|
|||||||
import { FormControl, Validators } from '@angular/forms';
|
import { FormControl } from '@angular/forms';
|
||||||
import { Fail, Failable } from 'picsur-shared/dist/types';
|
import { Fail, Failable } from 'picsur-shared/dist/types';
|
||||||
|
import {
|
||||||
|
CreatePasswordError,
|
||||||
|
CreateUsernameError,
|
||||||
|
PasswordValidators,
|
||||||
|
UsernameValidators
|
||||||
|
} from './default-validators';
|
||||||
import { UserPassModel } from './userpass';
|
import { UserPassModel } from './userpass';
|
||||||
|
|
||||||
export class LoginControl {
|
export class LoginControl {
|
||||||
public username = new FormControl('', [
|
public username = new FormControl('', UsernameValidators);
|
||||||
Validators.required,
|
public password = new FormControl('', PasswordValidators);
|
||||||
Validators.minLength(3),
|
|
||||||
]);
|
|
||||||
|
|
||||||
public password = new FormControl('', [
|
|
||||||
Validators.required,
|
|
||||||
Validators.minLength(3),
|
|
||||||
]);
|
|
||||||
|
|
||||||
public get usernameError() {
|
public get usernameError() {
|
||||||
return this.username.hasError('required')
|
return CreateUsernameError(this.username.errors);
|
||||||
? 'Username is required'
|
|
||||||
: this.username.hasError('minlength')
|
|
||||||
? 'Username is too short'
|
|
||||||
: '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get passwordError() {
|
public get passwordError() {
|
||||||
return this.password.hasError('required')
|
return CreatePasswordError(this.password.errors);
|
||||||
? 'Password is required'
|
|
||||||
: this.password.hasError('minlength')
|
|
||||||
? 'Password is too short'
|
|
||||||
: '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getData(): Failable<UserPassModel> {
|
public getData(): Failable<UserPassModel> {
|
||||||
if (this.username.errors || this.password.errors) {
|
if (this.username.errors || this.password.errors)
|
||||||
return Fail('Invalid username or password');
|
return Fail('Invalid username or password');
|
||||||
} else {
|
else return this.getRawData();
|
||||||
return {
|
|
||||||
username: this.username.value,
|
|
||||||
password: this.password.value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRawData(): UserPassModel {
|
public getRawData(): UserPassModel {
|
||||||
|
|||||||
@@ -1,49 +1,32 @@
|
|||||||
import { FormControl, Validators } from '@angular/forms';
|
import { FormControl } from '@angular/forms';
|
||||||
import { Fail, Failable } from 'picsur-shared/dist/types';
|
import { Fail, Failable } from 'picsur-shared/dist/types';
|
||||||
import { Compare } from './compare.validator';
|
import { Compare } from './compare.validator';
|
||||||
|
import {
|
||||||
|
CreatePasswordError,
|
||||||
|
CreateUsernameError,
|
||||||
|
PasswordValidators,
|
||||||
|
UsernameValidators
|
||||||
|
} from './default-validators';
|
||||||
import { UserPassModel } from './userpass';
|
import { UserPassModel } from './userpass';
|
||||||
|
|
||||||
export class RegisterControl {
|
export class RegisterControl {
|
||||||
public username = new FormControl('', [
|
public username = new FormControl('', UsernameValidators);
|
||||||
Validators.required,
|
public password = new FormControl('', PasswordValidators);
|
||||||
Validators.minLength(3),
|
|
||||||
]);
|
|
||||||
|
|
||||||
public password = new FormControl('', [
|
|
||||||
Validators.required,
|
|
||||||
Validators.minLength(3),
|
|
||||||
]);
|
|
||||||
|
|
||||||
public passwordConfirm = new FormControl('', [
|
public passwordConfirm = new FormControl('', [
|
||||||
Validators.required,
|
...PasswordValidators,
|
||||||
Validators.minLength(3),
|
|
||||||
Compare(this.password),
|
Compare(this.password),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
public get usernameError() {
|
public get usernameError() {
|
||||||
return this.username.hasError('required')
|
return CreateUsernameError(this.username.errors);
|
||||||
? 'Username is required'
|
|
||||||
: this.username.hasError('minlength')
|
|
||||||
? 'Username is too short'
|
|
||||||
: '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get passwordError() {
|
public get passwordError() {
|
||||||
return this.password.hasError('required')
|
return CreatePasswordError(this.password.errors);
|
||||||
? 'Password is required'
|
|
||||||
: this.password.hasError('minlength')
|
|
||||||
? 'Password is too short'
|
|
||||||
: '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get passwordConfirmError() {
|
public get passwordConfirmError() {
|
||||||
return this.passwordConfirm.hasError('required')
|
return CreatePasswordError(this.passwordConfirm.errors);
|
||||||
? 'Password confirmation is required'
|
|
||||||
: this.passwordConfirm.hasError('minlength')
|
|
||||||
? 'Password confirmation is too short'
|
|
||||||
: this.passwordConfirm.hasError('compare')
|
|
||||||
? 'Password confirmation does not match'
|
|
||||||
: '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getData(): Failable<UserPassModel> {
|
public getData(): Failable<UserPassModel> {
|
||||||
@@ -51,14 +34,9 @@ export class RegisterControl {
|
|||||||
this.username.errors ||
|
this.username.errors ||
|
||||||
this.password.errors ||
|
this.password.errors ||
|
||||||
this.passwordConfirm.errors
|
this.passwordConfirm.errors
|
||||||
) {
|
)
|
||||||
return Fail('Invalid username or password');
|
return Fail('Invalid username or password');
|
||||||
} else {
|
else return this.getRawData();
|
||||||
return {
|
|
||||||
username: this.username.value,
|
|
||||||
password: this.password.value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRawData(): UserPassModel {
|
public getRawData(): UserPassModel {
|
||||||
|
|||||||
@@ -1,19 +1,30 @@
|
|||||||
<h1>Users</h1>
|
<h1>Users</h1>
|
||||||
|
|
||||||
<table mat-table [dataSource]="dataSubject">
|
<mat-table [dataSource]="dataSubject" class="mat-elevation-z8">
|
||||||
<ng-container matColumnDef="id">
|
<ng-container matColumnDef="id">
|
||||||
<th mat-header-cell *matHeaderCellDef>ID</th>
|
<mat-header-cell *matHeaderCellDef>ID</mat-header-cell>
|
||||||
<td mat-cell *matCellDef="let user">{{ user.id }}</td>
|
<mat-cell *matCellDef="let user">{{ user.id }}</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="username">
|
<ng-container matColumnDef="username">
|
||||||
<th mat-header-cell *matHeaderCellDef>Username</th>
|
<mat-header-cell *matHeaderCellDef>Username</mat-header-cell>
|
||||||
<td mat-cell *matCellDef="let user">{{ user.username }}</td>
|
<mat-cell *matCellDef="let user">{{ user.username }}</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
<ng-container matColumnDef="actions">
|
||||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
<mat-header-cell *matHeaderCellDef>Actions</mat-header-cell>
|
||||||
</table>
|
<mat-cell *matCellDef="let user">
|
||||||
|
<button mat-icon-button>
|
||||||
|
<mat-icon aria-label="Edit">edit</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||||
|
<mat-row *matRowDef="let row; columns: displayedColumns">
|
||||||
|
pog
|
||||||
|
</mat-row>
|
||||||
|
</mat-table>
|
||||||
|
|
||||||
<mat-paginator
|
<mat-paginator
|
||||||
color="accent"
|
color="accent"
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
table {
|
mat-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mat-column-actions {
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { UserManageService } from 'src/app/services/api/usermanage.service';
|
|||||||
styleUrls: ['./settings-users.component.scss'],
|
styleUrls: ['./settings-users.component.scss'],
|
||||||
})
|
})
|
||||||
export class SettingsUsersComponent implements OnInit {
|
export class SettingsUsersComponent implements OnInit {
|
||||||
public readonly displayedColumns: string[] = ['id', 'username'];
|
public readonly displayedColumns: string[] = ['id', 'username', 'actions'];
|
||||||
public readonly pageSizeOptions: number[] = [5, 10, 25, 100];
|
public readonly pageSizeOptions: number[] = [5, 10, 25, 100];
|
||||||
public readonly startingPageSize = this.pageSizeOptions[2];
|
public readonly startingPageSize = this.pageSizeOptions[2];
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { SettingsUsersComponent } from './settings-users.component';
|
import { SettingsUsersComponent } from './settings-users.component';
|
||||||
@@ -9,6 +11,8 @@ import { SettingsUsersRoutingModule } from './settings-users.routing.module';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SettingsUsersRoutingModule,
|
SettingsUsersRoutingModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
MatTableModule,
|
MatTableModule,
|
||||||
MatPaginatorModule,
|
MatPaginatorModule,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,24 +1,16 @@
|
|||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
IsArray, IsDefined,
|
IsArray, IsDefined,
|
||||||
IsEnum, IsNotEmpty, IsString,
|
IsEnum, IsString,
|
||||||
ValidateNested
|
ValidateNested
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { EUser } from '../../entities/user.entity';
|
import { EUser, SimpleUser } from '../../entities/user.entity';
|
||||||
import { Permissions, PermissionsList } from '../permissions';
|
import { Permissions, PermissionsList } from '../permissions';
|
||||||
|
|
||||||
// Api
|
// Api
|
||||||
|
|
||||||
// UserLogin
|
// UserLogin
|
||||||
export class UserLoginRequest {
|
export class UserLoginRequest extends SimpleUser {}
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserLoginResponse {
|
export class UserLoginResponse {
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -27,15 +19,7 @@ export class UserLoginResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UserRegister
|
// UserRegister
|
||||||
export class UserRegisterRequest {
|
export class UserRegisterRequest extends SimpleUser {}
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserRegisterResponse extends EUser {}
|
export class UserRegisterResponse extends EUser {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsArray, IsDefined, IsInt, IsNotEmpty, IsString, Min, ValidateNested } from 'class-validator';
|
import {
|
||||||
import { EUser } from '../../entities/user.entity';
|
IsArray,
|
||||||
|
IsDefined,
|
||||||
|
IsInt, IsString,
|
||||||
|
Min,
|
||||||
|
ValidateNested
|
||||||
|
} from 'class-validator';
|
||||||
|
import { EUser, SimpleUser, SimpleUsername } from '../../entities/user.entity';
|
||||||
import { Roles } from '../roles.dto';
|
import { Roles } from '../roles.dto';
|
||||||
|
|
||||||
// UserList
|
// UserList
|
||||||
@@ -35,43 +41,19 @@ export class UserListResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UserCreate
|
// UserCreate
|
||||||
export class UserCreateRequest {
|
export class UserCreateRequest extends SimpleUser {}
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserCreateResponse extends EUser {}
|
export class UserCreateResponse extends EUser {}
|
||||||
|
|
||||||
|
|
||||||
// UserDelete
|
// UserDelete
|
||||||
export class UserDeleteRequest {
|
export class UserDeleteRequest extends SimpleUsername {}
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
username: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserDeleteResponse extends EUser {}
|
export class UserDeleteResponse extends EUser {}
|
||||||
|
|
||||||
// UserInfo
|
// UserInfo
|
||||||
export class UserInfoRequest {
|
export class UserInfoRequest extends SimpleUsername {}
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
username: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserInfoResponse extends EUser {}
|
export class UserInfoResponse extends EUser {}
|
||||||
|
|
||||||
// UserUpdateRoles
|
// UserUpdateRoles
|
||||||
export class UserUpdateRolesRequest {
|
export class UserUpdateRolesRequest extends SimpleUsername {
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsDefined()
|
@IsDefined()
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
|
|||||||
@@ -1,20 +1,40 @@
|
|||||||
import { Exclude } from 'class-transformer';
|
import { Exclude } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
IsArray, IsInt, IsNotEmpty,
|
IsAlphanumeric,
|
||||||
|
IsArray,
|
||||||
|
IsInt,
|
||||||
|
IsNotEmpty,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString
|
IsString,
|
||||||
|
Length
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { Roles } from '../dto/roles.dto';
|
import { Roles } from '../dto/roles.dto';
|
||||||
|
|
||||||
export class EUser {
|
// Match this with user validators in frontend
|
||||||
|
// (Not security focused, but it tells the user what is wrong)
|
||||||
|
|
||||||
|
export class SimpleUsername {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@Length(4, 32)
|
||||||
|
@IsAlphanumeric()
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a simple user object with just the username and unhashed password
|
||||||
|
export class SimpleUser extends SimpleUsername {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@Length(4, 1024)
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actual entity that goes in the db
|
||||||
|
export class EUser extends SimpleUsername {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
roles: Roles;
|
roles: Roles;
|
||||||
|
|||||||
Reference in New Issue
Block a user