add register view

This commit is contained in:
rubikscraft
2022-03-13 21:14:11 +01:00
parent be5689f7fb
commit 30b7fea071
19 changed files with 352 additions and 58 deletions

View File

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

View File

@@ -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<EUser> {
const request: UserRegisterRequest = {
username,
password,
};
const response = await this.api.post(
UserRegisterRequest,
UserRegisterResponse,
'/api/user/register',
request
);
return response;
}
public async logout(): AsyncFailable<EUser> {
const value = this.userSubject.getValue();
this.key.clear();

View File

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

View File

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

View File

@@ -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<LoginModel> {
public getData(): Failable<UserPassModel> {
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);
}
}

View File

@@ -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<UserPassModel> {
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);
}
}

View File

@@ -1,4 +1,4 @@
export interface LoginModel {
export interface UserPassModel {
username: string;
password: string;
}

View File

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

View File

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

View File

@@ -1,6 +1,9 @@
<div class="content-border">
<div class="content-border padded">
<div class="container centered">
<form class="row" (ngSubmit)="onSubmit()">
<div class="col-12 py-2">
<h1>Login</h1>
</div>
<div class="col-12 py-2" *ngIf="loginFail">
<mat-error>
Failed to login. Please check your username and password.

View File

@@ -1,3 +0,0 @@
mat-form-field {
width: 100%;
}

View File

@@ -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(),
});
}
}

View File

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

View File

@@ -0,0 +1,72 @@
<div class="content-border padded">
<div class="container centered">
<form class="row" (ngSubmit)="onSubmit()">
<div class="col-12 py-2">
<h1>Register</h1>
</div>
<div class="col-12 py-2" *ngIf="registerFail">
<mat-error> Failed to register. </mat-error>
</div>
<div class="col-12 py-2">
<mat-form-field appearance="outline" color="accent">
<mat-label>Username</mat-label>
<input
matInput
type="text"
[formControl]="model.username"
name="username"
required
/>
<mat-error *ngIf="model.username.errors">{{
model.usernameError
}}</mat-error>
</mat-form-field>
</div>
<div class="col-12 py-2">
<mat-form-field appearance="outline" color="accent">
<mat-label>Password</mat-label>
<input
matInput
type="password"
[formControl]="model.password"
name="password"
required
/>
<mat-error *ngIf="model.password.errors">{{
model.passwordError
}}</mat-error>
</mat-form-field>
</div>
<div class="col-12 py-2">
<mat-form-field appearance="outline" color="accent">
<mat-label>Confirm Password</mat-label>
<input
matInput
type="password"
[formControl]="model.passwordConfirm"
name="confirmpassword"
required
/>
<mat-error *ngIf="model.passwordConfirm.errors">{{
model.passwordConfirmError
}}</mat-error>
</mat-form-field>
</div>
<div class="col-12 py-2">
<button mat-raised-button color="accent" class="mx-2" type="submit">
Register
</button>
<button
*ngIf="showLogin"
mat-raised-button
color="primary"
class="mx-2"
(click)="onLogin()"
type="button"
>
Login
</button>
</div>
</form>
</div>
</div>

View File

@@ -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(),
});
}
}

View File

@@ -1,4 +1,4 @@
<div class="content-border">
<div class="content-border padded">
<div class="container centered">
<div class="row">
<div class="col-12">

View File

@@ -4,3 +4,8 @@
height: initial !important;
width: initial !important;
}
form mat-form-field {
width: inherit;
max-width: 40rem;
}

View File

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