Refactor user validation

This commit is contained in:
rubikscraft
2022-03-21 17:21:03 +01:00
parent 8c88c5f24e
commit 581be5921b
10 changed files with 161 additions and 131 deletions

View 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';
}
};

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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"

View File

@@ -1,3 +1,7 @@
table { mat-table {
width: 100%; width: 100%;
} }
.mat-column-actions {
justify-content: end;
}

View File

@@ -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];

View File

@@ -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,
], ],

View File

@@ -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 {}

View File

@@ -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 })

View File

@@ -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;