Implement authentication on frontend
- Added error handling on login - Added login support, fixed register - Added logout support on backend and frontend - Changed role to isAdmin - Added permission checking to menu
This commit is contained in:
parent
cfc3d48a7b
commit
dbf093e72e
15 changed files with 322 additions and 210 deletions
|
@ -1,5 +1,5 @@
|
|||
import {Count, CountSchema, Filter, FilterExcludingWhere, repository, Where,} from '@loopback/repository';
|
||||
import {del, get, getModelSchemaRef, param, patch, post, put, requestBody, response,} from '@loopback/rest';
|
||||
import {del, get, getModelSchemaRef, param, patch, post, Request, requestBody, response, RestBindings,} from '@loopback/rest';
|
||||
import {User} from '../models';
|
||||
import {UserRepository} from '../repositories';
|
||||
import {
|
||||
|
@ -7,7 +7,7 @@ import {
|
|||
UserServiceBindings
|
||||
} from '@loopback/authentication-jwt';
|
||||
import {inject} from '@loopback/core';
|
||||
import {TokenService} from '@loopback/authentication';
|
||||
import {authenticate, TokenService} from '@loopback/authentication';
|
||||
import {SecurityBindings, UserProfile} from '@loopback/security';
|
||||
import {genSalt, hash} from 'bcryptjs';
|
||||
import {SzakdolgozatUserService} from '../services';
|
||||
|
@ -21,8 +21,9 @@ export class UserController {
|
|||
@inject(SecurityBindings.USER, {optional: true})
|
||||
public user: UserProfile,
|
||||
@repository(UserRepository)
|
||||
public userRepository : UserRepository,
|
||||
) { }
|
||||
public userRepository: UserRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
@post('/users')
|
||||
@response(200, {
|
||||
|
@ -35,7 +36,7 @@ export class UserController {
|
|||
'application/json': {
|
||||
schema: getModelSchemaRef(User, {
|
||||
title: 'Registration request',
|
||||
exclude: ['id', 'role']
|
||||
exclude: ['id', 'isAdmin']
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
@ -47,7 +48,7 @@ export class UserController {
|
|||
email: request.email,
|
||||
name: request.name,
|
||||
password: password,
|
||||
role: 'student'
|
||||
isAdmin: false
|
||||
} as User;
|
||||
return this.userRepository.create(user);
|
||||
}
|
||||
|
@ -64,6 +65,7 @@ export class UserController {
|
|||
token: {
|
||||
type: 'string',
|
||||
},
|
||||
user: getModelSchemaRef(User, {exclude: ['id', 'password']})
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -77,10 +79,11 @@ export class UserController {
|
|||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: getModelSchemaRef(User, {exclude: ['id', 'role', 'name']})
|
||||
schema: getModelSchemaRef(User, {exclude: ['id', 'isAdmin', 'name']})
|
||||
},
|
||||
}}) credentials: Pick<User, 'email' | 'password'>,
|
||||
): Promise<{token: string}> {
|
||||
}
|
||||
}) credentials: Pick<User, 'email' | 'password'>,
|
||||
): Promise<{ token: string, user: Omit<User, 'id' | 'password'> }> {
|
||||
// ensure the user exists, and the password is correct
|
||||
const user = await this.userService.verifyCredentials(credentials);
|
||||
// convert a User object into a UserProfile object (reduced set of properties)
|
||||
|
@ -88,7 +91,26 @@ export class UserController {
|
|||
|
||||
// create a JSON Web Token based on the user profile
|
||||
const token = await this.jwtService.generateToken(userProfile);
|
||||
return {token};
|
||||
return {token, user};
|
||||
}
|
||||
|
||||
@post('/users/logout', {
|
||||
responses: {
|
||||
'204': {
|
||||
description: 'Logged out',
|
||||
},
|
||||
},
|
||||
})
|
||||
@authenticate('jwt')
|
||||
async logout(@inject(RestBindings.Http.REQUEST) request: Request): Promise<void> {
|
||||
const split = request.headers.authorization?.split(' ');
|
||||
if (split && split.length > 1) {
|
||||
if (this.jwtService.revokeToken) {
|
||||
await this.jwtService.revokeToken(split[1]);
|
||||
} else {
|
||||
console.error('Cannot revoke token');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@get('/users/count')
|
||||
|
@ -173,17 +195,6 @@ export class UserController {
|
|||
await this.userRepository.updateById(id, user);
|
||||
}
|
||||
|
||||
@put('/users/{id}')
|
||||
@response(204, {
|
||||
description: 'User PUT success',
|
||||
})
|
||||
async replaceById(
|
||||
@param.path.number('id') id: number,
|
||||
@requestBody() user: User,
|
||||
): Promise<void> {
|
||||
await this.userRepository.replaceById(id, user);
|
||||
}
|
||||
|
||||
@del('/users/{id}')
|
||||
@response(204, {
|
||||
description: 'User DELETE success',
|
||||
|
|
|
@ -39,10 +39,10 @@ export class User extends Entity {
|
|||
password: string;
|
||||
|
||||
@property({
|
||||
type: 'string',
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
})
|
||||
role: 'admin' | 'teacher' | 'student';
|
||||
isAdmin: boolean;
|
||||
|
||||
|
||||
constructor(data?: Partial<User>) {
|
||||
|
|
16
frontend/src/app/api.service.spec.ts
Normal file
16
frontend/src/app/api.service.spec.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ApiService } from './api.service';
|
||||
|
||||
describe('ApiService', () => {
|
||||
let service: ApiService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(ApiService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
26
frontend/src/app/api.service.ts
Normal file
26
frontend/src/app/api.service.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {environment} from '../environments/environment';
|
||||
import {LoginService} from './shared/login.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApiService {
|
||||
|
||||
constructor(private http: HttpClient, private loginService: LoginService) {
|
||||
}
|
||||
|
||||
request(method: 'post' | 'get' | 'delete', url: string, body: any): Promise<any> {
|
||||
return this.http.request(method, environment.backendUrl + url, {
|
||||
body,
|
||||
headers: {Authorization: 'Bearer ' + this.loginService.token}
|
||||
}).toPromise();
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
await this.request('post', '/users/logout', '');
|
||||
this.loginService.token = null;
|
||||
this.loginService.user = null;
|
||||
}
|
||||
}
|
|
@ -6,8 +6,7 @@
|
|||
<mat-toolbar>Menü</mat-toolbar>
|
||||
<mat-nav-list>
|
||||
<a mat-list-item routerLink="/">Főoldal</a>
|
||||
<a mat-list-item href="#">Link 2</a>
|
||||
<a mat-list-item href="#">Link 3</a>
|
||||
<a mat-list-item *ngFor="let item of getMenuItems()" [routerLink]="'/' + item.path">{{ item.title }}</a>
|
||||
</mat-nav-list>
|
||||
</mat-sidenav>
|
||||
<mat-sidenav-content>
|
||||
|
@ -22,10 +21,11 @@
|
|||
</button>
|
||||
<span>Szakdolgozat</span>
|
||||
<span class="toolbar-spacer"></span>
|
||||
<span *ngIf="loginService.loggedInUser">
|
||||
<span *ngIf="loginService.token">
|
||||
<span>{{ loginService.user.name }}</span>
|
||||
<a mat-button (click)="logout()">Kijelentkezés</a>
|
||||
</span>
|
||||
<span *ngIf="!loginService.loggedInUser">
|
||||
<span *ngIf="!loginService.token">
|
||||
<a mat-button routerLink="/register">Regisztráció</a>
|
||||
<a mat-button routerLink="/login">
|
||||
Bejelentkezés
|
||||
|
|
|
@ -4,6 +4,8 @@ import {Observable} from 'rxjs';
|
|||
import {map, shareReplay} from 'rxjs/operators';
|
||||
import {LoginService} from './shared/login.service';
|
||||
import {Router} from '@angular/router';
|
||||
import {ApiService} from './api.service';
|
||||
import {UserRole} from './model/user.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
@ -18,15 +20,25 @@ export class AppComponent implements OnInit {
|
|||
shareReplay()
|
||||
);
|
||||
|
||||
constructor(private breakpointObserver: BreakpointObserver, public loginService: LoginService,
|
||||
private router: Router) {
|
||||
menu: MenuItem[] = [
|
||||
{path: 'subjects', title: 'Tárgyak', requiredRole: 'admin'}
|
||||
];
|
||||
|
||||
constructor(private breakpointObserver: BreakpointObserver, public loginService: LoginService, private api: ApiService,
|
||||
private router: Router, private login: LoginService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
await this.loginService.logout();
|
||||
await this.api.logout();
|
||||
await this.router.navigate(['/']);
|
||||
}
|
||||
|
||||
getMenuItems(): MenuItem[] {
|
||||
return this.menu.filter(item => item.requiredRole === 'admin' ? this.login.user?.isAdmin : true); // TODO: Roles
|
||||
}
|
||||
}
|
||||
|
||||
type MenuItem = { path: string, title: string, requiredRole: UserRole | 'admin' };
|
||||
|
|
|
@ -17,10 +17,7 @@ import {MatFormFieldModule} from '@angular/material/form-field';
|
|||
import {MatInputModule} from '@angular/material/input';
|
||||
import {RegisterComponent} from './register/register.component';
|
||||
import {LoginService} from './shared/login.service';
|
||||
import {AngularFireModule, FirebaseApp} from '@angular/fire';
|
||||
import {AngularFireAuthModule} from '@angular/fire/auth';
|
||||
import {AngularFirestoreModule} from '@angular/fire/firestore';
|
||||
import {AngularFireDatabaseModule} from '@angular/fire/database';
|
||||
import {HttpClientModule} from '@angular/common/http';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -43,10 +40,7 @@ import {AngularFireDatabaseModule} from '@angular/fire/database';
|
|||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule,
|
||||
AngularFireModule.initializeApp((window as any).firebaseCredentials),
|
||||
AngularFirestoreModule,
|
||||
AngularFireDatabaseModule,
|
||||
AngularFireAuthModule
|
||||
HttpClientModule
|
||||
],
|
||||
providers: [
|
||||
LoginService
|
||||
|
|
|
@ -3,13 +3,16 @@
|
|||
<mat-form-field>
|
||||
<mat-label>Email</mat-label>
|
||||
<input matInput class="mat-input-element" type="email" name="email" required="required"
|
||||
placeholder="h123456@stud.u-szeged.hu" [(ngModel)]="email"/>
|
||||
placeholder="h123456@stud.u-szeged.hu" [formControl]="email" [errorStateMatcher]="matcher"/>
|
||||
<mat-hint>Egyetemi email cim</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Jelszó</mat-label>
|
||||
<input matInput class="mat-input-element" type="password" name="password" required="required"
|
||||
minlength="8" [(ngModel)]="pass"/>
|
||||
minlength="8" [formControl]="pass" [errorStateMatcher]="matcher"/>
|
||||
</mat-form-field>
|
||||
<mat-error *ngIf="matcher.isErrorState(email, null)">
|
||||
A megadott email cim vagy jelszó nem megfelelő
|
||||
</mat-error>
|
||||
<button mat-raised-button color="primary" (click)="doLogin()">Bejelentkezés</button>
|
||||
</form>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {Router} from '@angular/router';
|
||||
import {LoginService} from '../shared/login.service';
|
||||
import {FormErrorStateMatcher} from '../utility/form-error-state-matcher';
|
||||
import {FormControl} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
|
@ -8,8 +10,9 @@ import {LoginService} from '../shared/login.service';
|
|||
styleUrls: ['./login.component.css']
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
email: string;
|
||||
pass: string;
|
||||
email = new FormControl('');
|
||||
pass = new FormControl('');
|
||||
matcher = new FormErrorStateMatcher();
|
||||
|
||||
constructor(private router: Router, private loginService: LoginService) {
|
||||
}
|
||||
|
@ -19,7 +22,15 @@ export class LoginComponent implements OnInit {
|
|||
}
|
||||
|
||||
async doLogin(): Promise<void> {
|
||||
await this.loginService.login(this.email, this.pass);
|
||||
if (await this.loginService.login(this.email.value, this.pass.value)) {
|
||||
await this.router.navigate(['/']);
|
||||
} else {
|
||||
this.email.setErrors({
|
||||
login: true
|
||||
});
|
||||
this.pass.setErrors({
|
||||
login: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
7
frontend/src/app/model/user.model.ts
Normal file
7
frontend/src/app/model/user.model.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export class User {
|
||||
name: string;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
export type UserRole = 'teacher' | 'student';
|
||||
|
|
@ -16,6 +16,18 @@
|
|||
Egyetemi email megadása szükséges
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Teljes név</mat-label>
|
||||
<input matInput [formControl]="nameFormControl" [errorStateMatcher]="matcher"
|
||||
class="mat-input-element" type="text" name="name" required="required"
|
||||
minlength="4"/>
|
||||
<mat-error *ngIf="nameFormControl.hasError('pattern') && !nameFormControl.hasError('required')">
|
||||
A név formátuma nem megfelelő.
|
||||
</mat-error>
|
||||
<mat-error *ngIf="nameFormControl.hasError('required')">
|
||||
A teljes név megadása kötelező.
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Jelszó</mat-label>
|
||||
<input matInput [formControl]="passFormControl" [errorStateMatcher]="matcher"
|
||||
|
|
|
@ -1,15 +1,8 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {ErrorStateMatcher} from '@angular/material/core';
|
||||
import {AbstractControl, FormControl, FormGroupDirective, NgForm, ValidationErrors, Validators} from '@angular/forms';
|
||||
import {AbstractControl, FormControl, ValidationErrors, Validators} from '@angular/forms';
|
||||
import {LoginService} from '../shared/login.service';
|
||||
import {Router} from '@angular/router';
|
||||
|
||||
export class FormErrorStateMatcher implements ErrorStateMatcher {
|
||||
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
|
||||
const isSubmitted = form && form.submitted;
|
||||
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
|
||||
}
|
||||
}
|
||||
import {FormErrorStateMatcher} from '../utility/form-error-state-matcher';
|
||||
|
||||
@Component({
|
||||
selector: 'app-register',
|
||||
|
@ -27,6 +20,10 @@ export class RegisterComponent implements OnInit {
|
|||
RegisterComponent.validateUniEmail
|
||||
]);
|
||||
|
||||
nameFormControl = new FormControl('', [
|
||||
Validators.pattern(/([A-Za-z-.]+ )+[A-Za-z-.]+/)
|
||||
]);
|
||||
|
||||
passFormControl = new FormControl('', [
|
||||
Validators.required,
|
||||
Validators.minLength(8)
|
||||
|
@ -75,7 +72,7 @@ export class RegisterComponent implements OnInit {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
await this.loginService.createUser(this.emailFormControl.value, this.passFormControl.value);
|
||||
await this.loginService.createUser(this.emailFormControl.value, this.passFormControl.value, this.nameFormControl.value);
|
||||
await this.router.navigate(['/']);
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
|
|
|
@ -1,21 +1,34 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {environment} from '../../environments/environment';
|
||||
import {User} from '../model/user.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class LoginService {
|
||||
|
||||
loggedInUser: string; // TODO
|
||||
token: string;
|
||||
user: User;
|
||||
|
||||
constructor() {
|
||||
constructor(private http: HttpClient) {
|
||||
}
|
||||
|
||||
async createUser(email: string, pass: string): Promise<void> {
|
||||
async createUser(email: string, password: string, name: string): Promise<void> {
|
||||
await this.http.post(environment.backendUrl + '/users', {email, password, name}).toPromise();
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
async login(email: string, password: string): Promise<boolean> {
|
||||
try {
|
||||
const resp: any = await this.http.post(environment.backendUrl + '/users/login', {email, password}).toPromise();
|
||||
this.token = resp.token;
|
||||
this.user = resp.user;
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e.status === 401 || e.status === 422) {
|
||||
return false;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
async login(email: string, pass: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
|
9
frontend/src/app/utility/form-error-state-matcher.ts
Normal file
9
frontend/src/app/utility/form-error-state-matcher.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import {ErrorStateMatcher} from '@angular/material/core';
|
||||
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
|
||||
|
||||
export class FormErrorStateMatcher implements ErrorStateMatcher {
|
||||
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
|
||||
const isSubmitted = form && form.submitted;
|
||||
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
|
||||
}
|
||||
}
|
|
@ -3,7 +3,8 @@
|
|||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false
|
||||
production: false,
|
||||
backendUrl: 'http://localhost:8019'
|
||||
};
|
||||
|
||||
/*
|
||||
|
|
Loading…
Reference in a new issue