From 30b7fea07155cafe3905e3deed22c4095b08afa5 Mon Sep 17 00:00:00 2001 From: rubikscraft Date: Sun, 13 Mar 2022 21:14:11 +0100 Subject: [PATCH] add register view --- .../src/routes/api/auth/user.controller.ts | 8 -- frontend/src/app/api/user.service.ts | 18 +++- frontend/src/app/guards/permission.guard.ts | 9 +- .../src/app/models/forms/compare.validator.ts | 17 ++++ .../login => models/forms}/login.model.ts | 16 +++- .../src/app/models/forms/register.model.ts | 76 +++++++++++++++++ .../models/{login.ts => forms/userpass.ts} | 2 +- frontend/src/app/router/router.module.ts | 26 +----- frontend/src/app/router/routes.ts | 37 ++++++++ .../src/app/routes/login/login.component.html | 5 +- .../src/app/routes/login/login.component.scss | 3 - .../src/app/routes/login/login.component.ts | 16 ++-- .../routes/processing/processing.component.ts | 1 - .../routes/register/register.component.html | 72 ++++++++++++++++ .../register.component.scss} | 0 .../app/routes/register/register.component.ts | 85 +++++++++++++++++++ .../src/app/routes/view/view.component.html | 2 +- frontend/src/scss/fixes.scss | 5 ++ shared/src/dto/api/user.dto.ts | 12 +-- 19 files changed, 352 insertions(+), 58 deletions(-) create mode 100644 frontend/src/app/models/forms/compare.validator.ts rename frontend/src/app/{routes/login => models/forms}/login.model.ts (73%) create mode 100644 frontend/src/app/models/forms/register.model.ts rename frontend/src/app/models/{login.ts => forms/userpass.ts} (56%) create mode 100644 frontend/src/app/router/routes.ts create mode 100644 frontend/src/app/routes/register/register.component.html rename frontend/src/app/routes/{processing/processing.component.scss => register/register.component.scss} (100%) create mode 100644 frontend/src/app/routes/register/register.component.ts diff --git a/backend/src/routes/api/auth/user.controller.ts b/backend/src/routes/api/auth/user.controller.ts index e185d15..bf2e1c3 100644 --- a/backend/src/routes/api/auth/user.controller.ts +++ b/backend/src/routes/api/auth/user.controller.ts @@ -63,14 +63,6 @@ export class UserController { throw new InternalServerErrorException('Could not register user'); } - if (register.isAdmin) { - const result = await this.usersService.addRoles(user, ['admin']); - if (HasFailed(result)) { - this.logger.warn(result.getReason()); - throw new InternalServerErrorException('Could not add admin role'); - } - } - return user; } diff --git a/frontend/src/app/api/user.service.ts b/frontend/src/app/api/user.service.ts index fa393de..c78bd2e 100644 --- a/frontend/src/app/api/user.service.ts +++ b/frontend/src/app/api/user.service.ts @@ -4,7 +4,7 @@ import { validate } from 'class-validator'; import jwt_decode from 'jwt-decode'; import { UserLoginRequest, - UserLoginResponse, UserMeResponse + UserLoginResponse, UserMeResponse, UserRegisterRequest, UserRegisterResponse } from 'picsur-shared/dist/dto/api/user.dto'; import { JwtDataDto } from 'picsur-shared/dist/dto/jwt.dto'; import { EUser } from 'picsur-shared/dist/entities/user.entity'; @@ -60,6 +60,22 @@ export class UserService { return user; } + public async register(username: string, password: string): AsyncFailable { + const request: UserRegisterRequest = { + username, + password, + }; + + const response = await this.api.post( + UserRegisterRequest, + UserRegisterResponse, + '/api/user/register', + request + ); + + return response; + } + public async logout(): AsyncFailable { const value = this.userSubject.getValue(); this.key.clear(); diff --git a/frontend/src/app/guards/permission.guard.ts b/frontend/src/app/guards/permission.guard.ts index d21f79d..bdb2360 100644 --- a/frontend/src/app/guards/permission.guard.ts +++ b/frontend/src/app/guards/permission.guard.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, + Router, RouterStateSnapshot } from '@angular/router'; import { Permissions } from 'picsur-shared/dist/dto/permissions'; @@ -12,7 +13,10 @@ import { PermissionService } from '../api/permission.service'; providedIn: 'root', }) export class PermissionGuard implements CanActivate { - constructor(private permissionService: PermissionService) {} + constructor( + private permissionService: PermissionService, + private router: Router + ) {} async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { const requiredPermissions: Permissions = route.data['permissions']; @@ -28,6 +32,9 @@ export class PermissionGuard implements CanActivate { ourPermissions.includes(permission) ); + if (!isOk) { + this.router.navigate(['/']); + } return isOk; } } diff --git a/frontend/src/app/models/forms/compare.validator.ts b/frontend/src/app/models/forms/compare.validator.ts new file mode 100644 index 0000000..b1f20eb --- /dev/null +++ b/frontend/src/app/models/forms/compare.validator.ts @@ -0,0 +1,17 @@ +import { + AbstractControl, + FormControl, + ValidationErrors, + ValidatorFn +} from '@angular/forms'; + +export function Compare(compareTo: FormControl): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (control.value !== compareTo.value) { + return { + compare: true, + }; + } + return null; + }; +} diff --git a/frontend/src/app/routes/login/login.model.ts b/frontend/src/app/models/forms/login.model.ts similarity index 73% rename from frontend/src/app/routes/login/login.model.ts rename to frontend/src/app/models/forms/login.model.ts index e76b090..dbdb1fd 100644 --- a/frontend/src/app/routes/login/login.model.ts +++ b/frontend/src/app/models/forms/login.model.ts @@ -1,6 +1,6 @@ import { FormControl, Validators } from '@angular/forms'; import { Fail, Failable } from 'picsur-shared/dist/types'; -import { LoginModel } from '../../models/login'; +import { UserPassModel } from './userpass'; export class LoginControl { public username = new FormControl('', [ @@ -29,7 +29,7 @@ export class LoginControl { : ''; } - public getData(): Failable { + public getData(): Failable { if (this.username.errors || this.password.errors) { return Fail('Invalid username or password'); } else { @@ -39,4 +39,16 @@ export class LoginControl { }; } } + + public getRawData(): UserPassModel { + return { + username: this.username.value, + password: this.password.value, + }; + } + + public putData(data: UserPassModel) { + this.username.setValue(data.username); + this.password.setValue(data.password); + } } diff --git a/frontend/src/app/models/forms/register.model.ts b/frontend/src/app/models/forms/register.model.ts new file mode 100644 index 0000000..a02c5c5 --- /dev/null +++ b/frontend/src/app/models/forms/register.model.ts @@ -0,0 +1,76 @@ +import { FormControl, Validators } from '@angular/forms'; +import { Fail, Failable } from 'picsur-shared/dist/types'; +import { Compare } from './compare.validator'; +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 passwordConfirm = new FormControl('', [ + Validators.required, + Validators.minLength(3), + Compare(this.password), + ]); + + public get usernameError() { + return this.username.hasError('required') + ? 'Username is required' + : this.username.hasError('minlength') + ? 'Username is too short' + : ''; + } + + public get passwordError() { + return this.password.hasError('required') + ? 'Password is required' + : this.password.hasError('minlength') + ? 'Password is too short' + : ''; + } + + 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' + : ''; + } + + public getData(): Failable { + if ( + 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, + }; + } + } + + public getRawData(): UserPassModel { + return { + username: this.username.value, + password: this.password.value, + }; + } + + public putData(data: UserPassModel) { + this.username.setValue(data.username); + this.password.setValue(data.password); + this.passwordConfirm.setValue(data.password); + } +} diff --git a/frontend/src/app/models/login.ts b/frontend/src/app/models/forms/userpass.ts similarity index 56% rename from frontend/src/app/models/login.ts rename to frontend/src/app/models/forms/userpass.ts index 3ac5aec..73cf823 100644 --- a/frontend/src/app/models/login.ts +++ b/frontend/src/app/models/forms/userpass.ts @@ -1,4 +1,4 @@ -export interface LoginModel { +export interface UserPassModel { username: string; password: string; } diff --git a/frontend/src/app/router/router.module.ts b/frontend/src/app/router/router.module.ts index 241640b..25e9c42 100644 --- a/frontend/src/app/router/router.module.ts +++ b/frontend/src/app/router/router.module.ts @@ -4,38 +4,19 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { RouterModule, Routes } from '@angular/router'; +import { RouterModule } from '@angular/router'; import { NgxDropzoneModule } from 'ngx-dropzone'; -import { Permission } from 'picsur-shared/dist/dto/permissions'; import { ApiModule } from '../api/api.module'; import { CopyFieldModule } from '../components/copyfield/copyfield.module'; -import { PageNotFoundComponent } from '../components/pagenotfound/pagenotfound.component'; import { PageNotFoundModule } from '../components/pagenotfound/pagenotfound.module'; import { GuardsModule } from '../guards/guards.module'; -import { PermissionGuard } from '../guards/permission.guard'; import { LoginComponent } from '../routes/login/login.component'; import { ProcessingComponent } from '../routes/processing/processing.component'; +import { RegisterComponent } from '../routes/register/register.component'; import { UploadComponent } from '../routes/upload/upload.component'; import { ViewComponent } from '../routes/view/view.component'; import { UtilModule } from '../util/util.module'; - -// TODO: split up router - -const routes: Routes = [ - { path: '', component: UploadComponent }, - { - path: 'processing', - component: ProcessingComponent, - }, - { path: 'view/:hash', component: ViewComponent }, - { - path: 'login', - component: LoginComponent, - canActivate: [PermissionGuard], - data: { permissions: [Permission.UserLogin] }, - }, - { path: '**', component: PageNotFoundComponent }, -]; +import { routes } from './routes'; @NgModule({ imports: [ @@ -59,6 +40,7 @@ const routes: Routes = [ ProcessingComponent, ViewComponent, LoginComponent, + RegisterComponent, ], exports: [RouterModule], }) diff --git a/frontend/src/app/router/routes.ts b/frontend/src/app/router/routes.ts new file mode 100644 index 0000000..e93d5ae --- /dev/null +++ b/frontend/src/app/router/routes.ts @@ -0,0 +1,37 @@ +import { Routes } from '@angular/router'; +import { Permission } from 'picsur-shared/dist/dto/permissions'; +import { PageNotFoundComponent } from '../components/pagenotfound/pagenotfound.component'; +import { PermissionGuard } from '../guards/permission.guard'; +import { LoginComponent } from '../routes/login/login.component'; +import { ProcessingComponent } from '../routes/processing/processing.component'; +import { RegisterComponent } from '../routes/register/register.component'; +import { UploadComponent } from '../routes/upload/upload.component'; +import { ViewComponent } from '../routes/view/view.component'; + +// TODO: split up router +export const routes: Routes = [ + { path: '', component: UploadComponent }, + { + path: 'processing', + component: ProcessingComponent, + }, + { + path: 'view/:hash', + component: ViewComponent, + canActivate: [PermissionGuard], + data: { permissions: [Permission.ImageView] }, + }, + { + path: 'login', + component: LoginComponent, + canActivate: [PermissionGuard], + data: { permissions: [Permission.UserLogin] }, + }, + { + path: 'register', + component: RegisterComponent, + canActivate: [PermissionGuard], + data: { permissions: [Permission.UserRegister] }, + }, + { path: '**', component: PageNotFoundComponent }, +]; diff --git a/frontend/src/app/routes/login/login.component.html b/frontend/src/app/routes/login/login.component.html index 8a7660a..bbc3583 100644 --- a/frontend/src/app/routes/login/login.component.html +++ b/frontend/src/app/routes/login/login.component.html @@ -1,6 +1,9 @@ -
+
+
+

Login

+
Failed to login. Please check your username and password. diff --git a/frontend/src/app/routes/login/login.component.scss b/frontend/src/app/routes/login/login.component.scss index c7acb4b..e69de29 100644 --- a/frontend/src/app/routes/login/login.component.scss +++ b/frontend/src/app/routes/login/login.component.scss @@ -1,3 +0,0 @@ -mat-form-field { - width: 100%; -} diff --git a/frontend/src/app/routes/login/login.component.ts b/frontend/src/app/routes/login/login.component.ts index 7de28e1..756413c 100644 --- a/frontend/src/app/routes/login/login.component.ts +++ b/frontend/src/app/routes/login/login.component.ts @@ -7,7 +7,8 @@ import { PermissionService } from 'src/app/api/permission.service'; import { UserService } from 'src/app/api/user.service'; import { SnackBarType } from 'src/app/models/snack-bar-type'; import { UtilService } from 'src/app/util/util.service'; -import { LoginControl } from './login.model'; +import { LoginControl } from '../../models/forms/login.model'; +import { UserPassModel } from '../../models/forms/userpass'; @Component({ selector: 'app-login', @@ -34,9 +35,10 @@ export class LoginComponent implements OnInit { ) {} ngOnInit(): void { - if (this.userService.isLoggedIn) { - this.router.navigate(['/'], { replaceUrl: true }); - return; + const state = history.state as UserPassModel; + if (state) { + this.model.putData(state); + history.replaceState(null, ''); } this.onPermissions(); @@ -67,8 +69,8 @@ export class LoginComponent implements OnInit { } async onRegister() { - //prevent default - - console.log('click'); + this.router.navigate(['/register'], { + state: this.model.getRawData(), + }); } } diff --git a/frontend/src/app/routes/processing/processing.component.ts b/frontend/src/app/routes/processing/processing.component.ts index cb36d80..baee7ec 100644 --- a/frontend/src/app/routes/processing/processing.component.ts +++ b/frontend/src/app/routes/processing/processing.component.ts @@ -7,7 +7,6 @@ import { UtilService } from 'src/app/util/util.service'; @Component({ templateUrl: './processing.component.html', - styleUrls: ['./processing.component.scss'], }) export class ProcessingComponent implements OnInit { constructor( diff --git a/frontend/src/app/routes/register/register.component.html b/frontend/src/app/routes/register/register.component.html new file mode 100644 index 0000000..eccda54 --- /dev/null +++ b/frontend/src/app/routes/register/register.component.html @@ -0,0 +1,72 @@ +
+
+ +
+

Register

+
+
+ Failed to register. +
+
+ + Username + + {{ + model.usernameError + }} + +
+
+ + Password + + {{ + model.passwordError + }} + +
+
+ + Confirm Password + + {{ + model.passwordConfirmError + }} + +
+
+ + +
+ +
+
diff --git a/frontend/src/app/routes/processing/processing.component.scss b/frontend/src/app/routes/register/register.component.scss similarity index 100% rename from frontend/src/app/routes/processing/processing.component.scss rename to frontend/src/app/routes/register/register.component.scss diff --git a/frontend/src/app/routes/register/register.component.ts b/frontend/src/app/routes/register/register.component.ts new file mode 100644 index 0000000..77259b1 --- /dev/null +++ b/frontend/src/app/routes/register/register.component.ts @@ -0,0 +1,85 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; +import { Permission, Permissions } from 'picsur-shared/dist/dto/permissions'; +import { HasFailed } from 'picsur-shared/dist/types'; +import { PermissionService } from 'src/app/api/permission.service'; +import { UserService } from 'src/app/api/user.service'; +import { UserPassModel } from 'src/app/models/forms/userpass'; +import { SnackBarType } from 'src/app/models/snack-bar-type'; +import { UtilService } from 'src/app/util/util.service'; +import { RegisterControl } from '../../models/forms/register.model'; + +@Component({ + selector: 'app-register', + templateUrl: './register.component.html', + styleUrls: ['./register.component.scss'], +}) +export class RegisterComponent implements OnInit { + private readonly logger = console; + + private permissions: Permissions = []; + + public get showLogin() { + return this.permissions.includes(Permission.UserLogin); + } + + model = new RegisterControl(); + registerFail = false; + + constructor( + private userService: UserService, + private permissionService: PermissionService, + private router: Router, + private utilService: UtilService + ) {} + + ngOnInit(): void { + const state = history.state as UserPassModel; + if (state) { + this.model.putData(state); + history.replaceState(null, ''); + } + + this.onPermissions(); + } + + @AutoUnsubscribe() + onPermissions() { + return this.permissionService.live.subscribe((permissions) => { + this.permissions = permissions; + }); + } + + async onSubmit() { + const data = this.model.getData(); + if (HasFailed(data)) { + return; + } + + const user = await this.userService.register(data.username, data.password); + if (HasFailed(user)) { + this.logger.warn(user); + this.registerFail = true; + return; + } + + this.utilService.showSnackBar('Register successful', SnackBarType.Success); + + if (!this.userService.isLoggedIn) { + const loginResult = this.userService.login(data.username, data.password); + if (HasFailed(loginResult)) { + this.logger.warn(loginResult); + this.utilService.showSnackBar('Failed to login', SnackBarType.Error); + } + } + + this.router.navigate(['/']); + } + + async onLogin() { + this.router.navigate(['/login'], { + state: this.model.getRawData(), + }); + } +} diff --git a/frontend/src/app/routes/view/view.component.html b/frontend/src/app/routes/view/view.component.html index a6cbc1e..56f09f8 100644 --- a/frontend/src/app/routes/view/view.component.html +++ b/frontend/src/app/routes/view/view.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/frontend/src/scss/fixes.scss b/frontend/src/scss/fixes.scss index f6f5fc5..18d490c 100644 --- a/frontend/src/scss/fixes.scss +++ b/frontend/src/scss/fixes.scss @@ -4,3 +4,8 @@ height: initial !important; width: initial !important; } + +form mat-form-field { + width: inherit; + max-width: 40rem; +} diff --git a/shared/src/dto/api/user.dto.ts b/shared/src/dto/api/user.dto.ts index eae7af6..07aa825 100644 --- a/shared/src/dto/api/user.dto.ts +++ b/shared/src/dto/api/user.dto.ts @@ -1,13 +1,9 @@ import { Type } from 'class-transformer'; import { - IsArray, - IsBoolean, - IsDefined, + IsArray, IsDefined, IsEnum, IsInt, - IsNotEmpty, - IsOptional, - IsPositive, + IsNotEmpty, IsPositive, IsString, ValidateNested } from 'class-validator'; @@ -43,10 +39,6 @@ export class UserRegisterRequest { @IsString() @IsNotEmpty() password: string; - - @IsBoolean() - @IsOptional() - isAdmin?: boolean; } export class UserRegisterResponse extends EUser {}