Refactor user validation

This commit is contained in:
rubikscraft 2022-03-21 17:21:03 +01:00
parent 8c88c5f24e
commit 581be5921b
No known key found for this signature in database
GPG key ID: 1463EBE9200A5CD4
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 {
CreatePasswordError,
CreateUsernameError,
PasswordValidators,
UsernameValidators
} from './default-validators';
import { UserPassModel } from './userpass';
export class LoginControl {
public username = new FormControl('', [
Validators.required,
Validators.minLength(3),
]);
public password = new FormControl('', [
Validators.required,
Validators.minLength(3),
]);
public username = new FormControl('', UsernameValidators);
public password = new FormControl('', PasswordValidators);
public get usernameError() {
return this.username.hasError('required')
? 'Username is required'
: this.username.hasError('minlength')
? 'Username is too short'
: '';
return CreateUsernameError(this.username.errors);
}
public get passwordError() {
return this.password.hasError('required')
? 'Password is required'
: this.password.hasError('minlength')
? 'Password is too short'
: '';
return CreatePasswordError(this.password.errors);
}
public getData(): Failable<UserPassModel> {
if (this.username.errors || this.password.errors) {
if (this.username.errors || this.password.errors)
return Fail('Invalid username or password');
} else {
return {
username: this.username.value,
password: this.password.value,
};
}
else return this.getRawData();
}
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 { Compare } from './compare.validator';
import {
CreatePasswordError,
CreateUsernameError,
PasswordValidators,
UsernameValidators
} from './default-validators';
import { UserPassModel } from './userpass';
export class RegisterControl {
public username = new FormControl('', [
Validators.required,
Validators.minLength(3),
]);
public password = new FormControl('', [
Validators.required,
Validators.minLength(3),
]);
public username = new FormControl('', UsernameValidators);
public password = new FormControl('', PasswordValidators);
public passwordConfirm = new FormControl('', [
Validators.required,
Validators.minLength(3),
...PasswordValidators,
Compare(this.password),
]);
public get usernameError() {
return this.username.hasError('required')
? 'Username is required'
: this.username.hasError('minlength')
? 'Username is too short'
: '';
return CreateUsernameError(this.username.errors);
}
public get passwordError() {
return this.password.hasError('required')
? 'Password is required'
: this.password.hasError('minlength')
? 'Password is too short'
: '';
return CreatePasswordError(this.password.errors);
}
public get passwordConfirmError() {
return this.passwordConfirm.hasError('required')
? 'Password confirmation is required'
: this.passwordConfirm.hasError('minlength')
? 'Password confirmation is too short'
: this.passwordConfirm.hasError('compare')
? 'Password confirmation does not match'
: '';
return CreatePasswordError(this.passwordConfirm.errors);
}
public getData(): Failable<UserPassModel> {
@ -51,14 +34,9 @@ export class RegisterControl {
this.username.errors ||
this.password.errors ||
this.passwordConfirm.errors
) {
)
return Fail('Invalid username or password');
} else {
return {
username: this.username.value,
password: this.password.value,
};
}
else return this.getRawData();
}
public getRawData(): UserPassModel {

View file

@ -1,19 +1,30 @@
<h1>Users</h1>
<table mat-table [dataSource]="dataSubject">
<mat-table [dataSource]="dataSubject" class="mat-elevation-z8">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>ID</th>
<td mat-cell *matCellDef="let user">{{ user.id }}</td>
<mat-header-cell *matHeaderCellDef>ID</mat-header-cell>
<mat-cell *matCellDef="let user">{{ user.id }}</mat-cell>
</ng-container>
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef>Username</th>
<td mat-cell *matCellDef="let user">{{ user.username }}</td>
<mat-header-cell *matHeaderCellDef>Username</mat-header-cell>
<mat-cell *matCellDef="let user">{{ user.username }}</mat-cell>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef>Actions</mat-header-cell>
<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
color="accent"

View file

@ -1,3 +1,7 @@
table {
mat-table {
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'],
})
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 startingPageSize = this.pageSizeOptions[2];

View file

@ -1,5 +1,7 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatTableModule } from '@angular/material/table';
import { SettingsUsersComponent } from './settings-users.component';
@ -9,6 +11,8 @@ import { SettingsUsersRoutingModule } from './settings-users.routing.module';
imports: [
CommonModule,
SettingsUsersRoutingModule,
MatButtonModule,
MatIconModule,
MatTableModule,
MatPaginatorModule,
],

View file

@ -1,24 +1,16 @@
import { Type } from 'class-transformer';
import {
IsArray, IsDefined,
IsEnum, IsNotEmpty, IsString,
IsEnum, IsString,
ValidateNested
} from 'class-validator';
import { EUser } from '../../entities/user.entity';
import { EUser, SimpleUser } from '../../entities/user.entity';
import { Permissions, PermissionsList } from '../permissions';
// Api
// UserLogin
export class UserLoginRequest {
@IsNotEmpty()
@IsString()
username: string;
@IsNotEmpty()
@IsString()
password: string;
}
export class UserLoginRequest extends SimpleUser {}
export class UserLoginResponse {
@IsString()
@ -27,15 +19,7 @@ export class UserLoginResponse {
}
// UserRegister
export class UserRegisterRequest {
@IsString()
@IsNotEmpty()
username: string;
@IsString()
@IsNotEmpty()
password: string;
}
export class UserRegisterRequest extends SimpleUser {}
export class UserRegisterResponse extends EUser {}

View file

@ -1,6 +1,12 @@
import { Type } from 'class-transformer';
import { IsArray, IsDefined, IsInt, IsNotEmpty, IsString, Min, ValidateNested } from 'class-validator';
import { EUser } from '../../entities/user.entity';
import {
IsArray,
IsDefined,
IsInt, IsString,
Min,
ValidateNested
} from 'class-validator';
import { EUser, SimpleUser, SimpleUsername } from '../../entities/user.entity';
import { Roles } from '../roles.dto';
// UserList
@ -35,43 +41,19 @@ export class UserListResponse {
}
// UserCreate
export class UserCreateRequest {
@IsString()
@IsNotEmpty()
username: string;
@IsString()
@IsNotEmpty()
password: string;
}
export class UserCreateRequest extends SimpleUser {}
export class UserCreateResponse extends EUser {}
// UserDelete
export class UserDeleteRequest {
@IsString()
@IsNotEmpty()
username: string;
}
export class UserDeleteRequest extends SimpleUsername {}
export class UserDeleteResponse extends EUser {}
// UserInfo
export class UserInfoRequest {
@IsString()
@IsNotEmpty()
username: string;
}
export class UserInfoRequest extends SimpleUsername {}
export class UserInfoResponse extends EUser {}
// UserUpdateRoles
export class UserUpdateRolesRequest {
@IsString()
@IsNotEmpty()
username: string;
export class UserUpdateRolesRequest extends SimpleUsername {
@IsArray()
@IsDefined()
@IsString({ each: true })

View file

@ -1,20 +1,40 @@
import { Exclude } from 'class-transformer';
import {
IsArray, IsInt, IsNotEmpty,
IsAlphanumeric,
IsArray,
IsInt,
IsNotEmpty,
IsOptional,
IsString
IsString,
Length
} from 'class-validator';
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()
@IsInt()
id?: number;
@IsNotEmpty()
@IsString()
username: string;
@IsArray()
@IsString({ each: true })
roles: Roles;