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,194 +1,205 @@
|
||||||
import {Count, CountSchema, Filter, FilterExcludingWhere, repository, Where,} from '@loopback/repository';
|
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 {User} from '../models';
|
||||||
import {UserRepository} from '../repositories';
|
import {UserRepository} from '../repositories';
|
||||||
import {
|
import {
|
||||||
TokenServiceBindings,
|
TokenServiceBindings,
|
||||||
UserServiceBindings
|
UserServiceBindings
|
||||||
} from '@loopback/authentication-jwt';
|
} from '@loopback/authentication-jwt';
|
||||||
import {inject} from '@loopback/core';
|
import {inject} from '@loopback/core';
|
||||||
import {TokenService} from '@loopback/authentication';
|
import {authenticate, TokenService} from '@loopback/authentication';
|
||||||
import {SecurityBindings, UserProfile} from '@loopback/security';
|
import {SecurityBindings, UserProfile} from '@loopback/security';
|
||||||
import {genSalt, hash} from 'bcryptjs';
|
import {genSalt, hash} from 'bcryptjs';
|
||||||
import {SzakdolgozatUserService} from '../services';
|
import {SzakdolgozatUserService} from '../services';
|
||||||
|
|
||||||
export class UserController {
|
export class UserController {
|
||||||
constructor(
|
constructor(
|
||||||
@inject(TokenServiceBindings.TOKEN_SERVICE)
|
@inject(TokenServiceBindings.TOKEN_SERVICE)
|
||||||
public jwtService: TokenService,
|
public jwtService: TokenService,
|
||||||
@inject(UserServiceBindings.USER_SERVICE)
|
@inject(UserServiceBindings.USER_SERVICE)
|
||||||
public userService: SzakdolgozatUserService,
|
public userService: SzakdolgozatUserService,
|
||||||
@inject(SecurityBindings.USER, {optional: true})
|
@inject(SecurityBindings.USER, {optional: true})
|
||||||
public user: UserProfile,
|
public user: UserProfile,
|
||||||
@repository(UserRepository)
|
@repository(UserRepository)
|
||||||
public userRepository : UserRepository,
|
public userRepository: UserRepository,
|
||||||
) { }
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
@post('/users')
|
@post('/users')
|
||||||
@response(200, {
|
@response(200, {
|
||||||
description: 'User model instance',
|
description: 'User model instance',
|
||||||
content: {'application/json': {schema: getModelSchemaRef(User, {exclude: ['password']})}},
|
content: {'application/json': {schema: getModelSchemaRef(User, {exclude: ['password']})}},
|
||||||
})
|
|
||||||
async register(
|
|
||||||
@requestBody({
|
|
||||||
content: {
|
|
||||||
'application/json': {
|
|
||||||
schema: getModelSchemaRef(User, {
|
|
||||||
title: 'Registration request',
|
|
||||||
exclude: ['id', 'role']
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
request: Pick<User, 'email' | 'name' | 'password'>,
|
async register(
|
||||||
): Promise<User> {
|
@requestBody({
|
||||||
const password = await hash(request.password, await genSalt());
|
content: {
|
||||||
const user = {
|
'application/json': {
|
||||||
email: request.email,
|
schema: getModelSchemaRef(User, {
|
||||||
name: request.name,
|
title: 'Registration request',
|
||||||
password: password,
|
exclude: ['id', 'isAdmin']
|
||||||
role: 'student'
|
}),
|
||||||
} as User;
|
|
||||||
return this.userRepository.create(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
@post('/users/login', {
|
|
||||||
responses: {
|
|
||||||
'200': {
|
|
||||||
description: 'Token',
|
|
||||||
content: {
|
|
||||||
'application/json': {
|
|
||||||
schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
token: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
},
|
request: Pick<User, 'email' | 'name' | 'password'>,
|
||||||
},
|
): Promise<User> {
|
||||||
},
|
const password = await hash(request.password, await genSalt());
|
||||||
})
|
const user = {
|
||||||
async login(
|
email: request.email,
|
||||||
@requestBody({
|
name: request.name,
|
||||||
description: 'The input of login function',
|
password: password,
|
||||||
required: true,
|
isAdmin: false
|
||||||
content: {
|
} as User;
|
||||||
'application/json': {
|
return this.userRepository.create(user);
|
||||||
schema: getModelSchemaRef(User, {exclude: ['id', 'role', 'name']})
|
}
|
||||||
},
|
|
||||||
}}) credentials: Pick<User, 'email' | 'password'>,
|
|
||||||
): Promise<{token: string}> {
|
|
||||||
// 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)
|
|
||||||
const userProfile = this.userService.convertToUserProfile(user);
|
|
||||||
|
|
||||||
// create a JSON Web Token based on the user profile
|
@post('/users/login', {
|
||||||
const token = await this.jwtService.generateToken(userProfile);
|
responses: {
|
||||||
return {token};
|
'200': {
|
||||||
}
|
description: 'Token',
|
||||||
|
content: {
|
||||||
@get('/users/count')
|
'application/json': {
|
||||||
@response(200, {
|
schema: {
|
||||||
description: 'User model count',
|
type: 'object',
|
||||||
content: {'application/json': {schema: CountSchema}},
|
properties: {
|
||||||
})
|
token: {
|
||||||
async count(
|
type: 'string',
|
||||||
@param.where(User) where?: Where<User>,
|
},
|
||||||
): Promise<Count> {
|
user: getModelSchemaRef(User, {exclude: ['id', 'password']})
|
||||||
return this.userRepository.count(where);
|
},
|
||||||
}
|
},
|
||||||
|
},
|
||||||
@get('/users')
|
},
|
||||||
@response(200, {
|
},
|
||||||
description: 'Array of User model instances',
|
|
||||||
content: {
|
|
||||||
'application/json': {
|
|
||||||
schema: {
|
|
||||||
type: 'array',
|
|
||||||
items: getModelSchemaRef(User, {includeRelations: true}),
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
async find(
|
|
||||||
@param.filter(User) filter?: Filter<User>,
|
|
||||||
): Promise<User[]> {
|
|
||||||
return this.userRepository.find(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@patch('/users')
|
|
||||||
@response(200, {
|
|
||||||
description: 'User PATCH success count',
|
|
||||||
content: {'application/json': {schema: CountSchema}},
|
|
||||||
})
|
|
||||||
async updateAll(
|
|
||||||
@requestBody({
|
|
||||||
content: {
|
|
||||||
'application/json': {
|
|
||||||
schema: getModelSchemaRef(User, {partial: true}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
user: User,
|
async login(
|
||||||
@param.where(User) where?: Where<User>,
|
@requestBody({
|
||||||
): Promise<Count> {
|
description: 'The input of login function',
|
||||||
return this.userRepository.updateAll(user, where);
|
required: true,
|
||||||
}
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: getModelSchemaRef(User, {exclude: ['id', 'isAdmin', 'name']})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}) 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)
|
||||||
|
const userProfile = this.userService.convertToUserProfile(user);
|
||||||
|
|
||||||
@get('/users/{id}')
|
// create a JSON Web Token based on the user profile
|
||||||
@response(200, {
|
const token = await this.jwtService.generateToken(userProfile);
|
||||||
description: 'User model instance',
|
return {token, user};
|
||||||
content: {
|
}
|
||||||
'application/json': {
|
|
||||||
schema: getModelSchemaRef(User, {includeRelations: true}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
async findById(
|
|
||||||
@param.path.number('id') id: number,
|
|
||||||
@param.filter(User, {exclude: 'where'}) filter?: FilterExcludingWhere<User>
|
|
||||||
): Promise<User> {
|
|
||||||
return this.userRepository.findById(id, filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@patch('/users/{id}')
|
@post('/users/logout', {
|
||||||
@response(204, {
|
responses: {
|
||||||
description: 'User PATCH success',
|
'204': {
|
||||||
})
|
description: 'Logged out',
|
||||||
async updateById(
|
},
|
||||||
@param.path.number('id') id: number,
|
|
||||||
@requestBody({
|
|
||||||
content: {
|
|
||||||
'application/json': {
|
|
||||||
schema: getModelSchemaRef(User, {partial: true}),
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
})
|
})
|
||||||
user: User,
|
@authenticate('jwt')
|
||||||
): Promise<void> {
|
async logout(@inject(RestBindings.Http.REQUEST) request: Request): Promise<void> {
|
||||||
await this.userRepository.updateById(id, user);
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@put('/users/{id}')
|
@get('/users/count')
|
||||||
@response(204, {
|
@response(200, {
|
||||||
description: 'User PUT success',
|
description: 'User model count',
|
||||||
})
|
content: {'application/json': {schema: CountSchema}},
|
||||||
async replaceById(
|
})
|
||||||
@param.path.number('id') id: number,
|
async count(
|
||||||
@requestBody() user: User,
|
@param.where(User) where?: Where<User>,
|
||||||
): Promise<void> {
|
): Promise<Count> {
|
||||||
await this.userRepository.replaceById(id, user);
|
return this.userRepository.count(where);
|
||||||
}
|
}
|
||||||
|
|
||||||
@del('/users/{id}')
|
@get('/users')
|
||||||
@response(204, {
|
@response(200, {
|
||||||
description: 'User DELETE success',
|
description: 'Array of User model instances',
|
||||||
})
|
content: {
|
||||||
async deleteById(@param.path.number('id') id: number): Promise<void> {
|
'application/json': {
|
||||||
await this.userRepository.deleteById(id);
|
schema: {
|
||||||
}
|
type: 'array',
|
||||||
|
items: getModelSchemaRef(User, {includeRelations: true}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async find(
|
||||||
|
@param.filter(User) filter?: Filter<User>,
|
||||||
|
): Promise<User[]> {
|
||||||
|
return this.userRepository.find(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@patch('/users')
|
||||||
|
@response(200, {
|
||||||
|
description: 'User PATCH success count',
|
||||||
|
content: {'application/json': {schema: CountSchema}},
|
||||||
|
})
|
||||||
|
async updateAll(
|
||||||
|
@requestBody({
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: getModelSchemaRef(User, {partial: true}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
user: User,
|
||||||
|
@param.where(User) where?: Where<User>,
|
||||||
|
): Promise<Count> {
|
||||||
|
return this.userRepository.updateAll(user, where);
|
||||||
|
}
|
||||||
|
|
||||||
|
@get('/users/{id}')
|
||||||
|
@response(200, {
|
||||||
|
description: 'User model instance',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: getModelSchemaRef(User, {includeRelations: true}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async findById(
|
||||||
|
@param.path.number('id') id: number,
|
||||||
|
@param.filter(User, {exclude: 'where'}) filter?: FilterExcludingWhere<User>
|
||||||
|
): Promise<User> {
|
||||||
|
return this.userRepository.findById(id, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@patch('/users/{id}')
|
||||||
|
@response(204, {
|
||||||
|
description: 'User PATCH success',
|
||||||
|
})
|
||||||
|
async updateById(
|
||||||
|
@param.path.number('id') id: number,
|
||||||
|
@requestBody({
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: getModelSchemaRef(User, {partial: true}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
user: User,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.userRepository.updateById(id, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@del('/users/{id}')
|
||||||
|
@response(204, {
|
||||||
|
description: 'User DELETE success',
|
||||||
|
})
|
||||||
|
async deleteById(@param.path.number('id') id: number): Promise<void> {
|
||||||
|
await this.userRepository.deleteById(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,10 +39,10 @@ export class User extends Entity {
|
||||||
password: string;
|
password: string;
|
||||||
|
|
||||||
@property({
|
@property({
|
||||||
type: 'string',
|
type: 'boolean',
|
||||||
required: true,
|
required: true,
|
||||||
})
|
})
|
||||||
role: 'admin' | 'teacher' | 'student';
|
isAdmin: boolean;
|
||||||
|
|
||||||
|
|
||||||
constructor(data?: Partial<User>) {
|
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-toolbar>Menü</mat-toolbar>
|
||||||
<mat-nav-list>
|
<mat-nav-list>
|
||||||
<a mat-list-item routerLink="/">Főoldal</a>
|
<a mat-list-item routerLink="/">Főoldal</a>
|
||||||
<a mat-list-item href="#">Link 2</a>
|
<a mat-list-item *ngFor="let item of getMenuItems()" [routerLink]="'/' + item.path">{{ item.title }}</a>
|
||||||
<a mat-list-item href="#">Link 3</a>
|
|
||||||
</mat-nav-list>
|
</mat-nav-list>
|
||||||
</mat-sidenav>
|
</mat-sidenav>
|
||||||
<mat-sidenav-content>
|
<mat-sidenav-content>
|
||||||
|
@ -22,10 +21,11 @@
|
||||||
</button>
|
</button>
|
||||||
<span>Szakdolgozat</span>
|
<span>Szakdolgozat</span>
|
||||||
<span class="toolbar-spacer"></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>
|
<a mat-button (click)="logout()">Kijelentkezés</a>
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="!loginService.loggedInUser">
|
<span *ngIf="!loginService.token">
|
||||||
<a mat-button routerLink="/register">Regisztráció</a>
|
<a mat-button routerLink="/register">Regisztráció</a>
|
||||||
<a mat-button routerLink="/login">
|
<a mat-button routerLink="/login">
|
||||||
Bejelentkezés
|
Bejelentkezés
|
||||||
|
|
|
@ -4,6 +4,8 @@ import {Observable} from 'rxjs';
|
||||||
import {map, shareReplay} from 'rxjs/operators';
|
import {map, shareReplay} from 'rxjs/operators';
|
||||||
import {LoginService} from './shared/login.service';
|
import {LoginService} from './shared/login.service';
|
||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
|
import {ApiService} from './api.service';
|
||||||
|
import {UserRole} from './model/user.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
|
@ -18,15 +20,25 @@ export class AppComponent implements OnInit {
|
||||||
shareReplay()
|
shareReplay()
|
||||||
);
|
);
|
||||||
|
|
||||||
constructor(private breakpointObserver: BreakpointObserver, public loginService: LoginService,
|
menu: MenuItem[] = [
|
||||||
private router: Router) {
|
{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 {
|
ngOnInit(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout(): Promise<void> {
|
async logout(): Promise<void> {
|
||||||
await this.loginService.logout();
|
await this.api.logout();
|
||||||
await this.router.navigate(['/']);
|
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 {MatInputModule} from '@angular/material/input';
|
||||||
import {RegisterComponent} from './register/register.component';
|
import {RegisterComponent} from './register/register.component';
|
||||||
import {LoginService} from './shared/login.service';
|
import {LoginService} from './shared/login.service';
|
||||||
import {AngularFireModule, FirebaseApp} from '@angular/fire';
|
import {HttpClientModule} from '@angular/common/http';
|
||||||
import {AngularFireAuthModule} from '@angular/fire/auth';
|
|
||||||
import {AngularFirestoreModule} from '@angular/fire/firestore';
|
|
||||||
import {AngularFireDatabaseModule} from '@angular/fire/database';
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -43,10 +40,7 @@ import {AngularFireDatabaseModule} from '@angular/fire/database';
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
AngularFireModule.initializeApp((window as any).firebaseCredentials),
|
HttpClientModule
|
||||||
AngularFirestoreModule,
|
|
||||||
AngularFireDatabaseModule,
|
|
||||||
AngularFireAuthModule
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
LoginService
|
LoginService
|
||||||
|
|
|
@ -3,13 +3,16 @@
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>Email</mat-label>
|
<mat-label>Email</mat-label>
|
||||||
<input matInput class="mat-input-element" type="email" name="email" required="required"
|
<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-hint>Egyetemi email cim</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>Jelszó</mat-label>
|
<mat-label>Jelszó</mat-label>
|
||||||
<input matInput class="mat-input-element" type="password" name="password" required="required"
|
<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-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>
|
<button mat-raised-button color="primary" (click)="doLogin()">Bejelentkezés</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import {Component, OnInit} from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
import {LoginService} from '../shared/login.service';
|
import {LoginService} from '../shared/login.service';
|
||||||
|
import {FormErrorStateMatcher} from '../utility/form-error-state-matcher';
|
||||||
|
import {FormControl} from '@angular/forms';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
|
@ -8,8 +10,9 @@ import {LoginService} from '../shared/login.service';
|
||||||
styleUrls: ['./login.component.css']
|
styleUrls: ['./login.component.css']
|
||||||
})
|
})
|
||||||
export class LoginComponent implements OnInit {
|
export class LoginComponent implements OnInit {
|
||||||
email: string;
|
email = new FormControl('');
|
||||||
pass: string;
|
pass = new FormControl('');
|
||||||
|
matcher = new FormErrorStateMatcher();
|
||||||
|
|
||||||
constructor(private router: Router, private loginService: LoginService) {
|
constructor(private router: Router, private loginService: LoginService) {
|
||||||
}
|
}
|
||||||
|
@ -19,7 +22,15 @@ export class LoginComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
async doLogin(): Promise<void> {
|
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(['/']);
|
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
|
Egyetemi email megadása szükséges
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</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-form-field>
|
||||||
<mat-label>Jelszó</mat-label>
|
<mat-label>Jelszó</mat-label>
|
||||||
<input matInput [formControl]="passFormControl" [errorStateMatcher]="matcher"
|
<input matInput [formControl]="passFormControl" [errorStateMatcher]="matcher"
|
||||||
|
|
|
@ -1,15 +1,8 @@
|
||||||
import {Component, OnInit} from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {ErrorStateMatcher} from '@angular/material/core';
|
import {AbstractControl, FormControl, ValidationErrors, Validators} from '@angular/forms';
|
||||||
import {AbstractControl, FormControl, FormGroupDirective, NgForm, ValidationErrors, Validators} from '@angular/forms';
|
|
||||||
import {LoginService} from '../shared/login.service';
|
import {LoginService} from '../shared/login.service';
|
||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
|
import {FormErrorStateMatcher} from '../utility/form-error-state-matcher';
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-register',
|
selector: 'app-register',
|
||||||
|
@ -27,6 +20,10 @@ export class RegisterComponent implements OnInit {
|
||||||
RegisterComponent.validateUniEmail
|
RegisterComponent.validateUniEmail
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
nameFormControl = new FormControl('', [
|
||||||
|
Validators.pattern(/([A-Za-z-.]+ )+[A-Za-z-.]+/)
|
||||||
|
]);
|
||||||
|
|
||||||
passFormControl = new FormControl('', [
|
passFormControl = new FormControl('', [
|
||||||
Validators.required,
|
Validators.required,
|
||||||
Validators.minLength(8)
|
Validators.minLength(8)
|
||||||
|
@ -75,7 +72,7 @@ export class RegisterComponent implements OnInit {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
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(['/']);
|
await this.router.navigate(['/']);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e);
|
alert(e);
|
||||||
|
|
|
@ -1,21 +1,34 @@
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {environment} from '../../environments/environment';
|
||||||
|
import {User} from '../model/user.model';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class LoginService {
|
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();
|
||||||
async login(email: string, pass: string): Promise<void> {
|
this.token = resp.token;
|
||||||
|
this.user = resp.user;
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.status === 401 || e.status === 422) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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`.
|
// The list of file replacements can be found in `angular.json`.
|
||||||
|
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: false
|
production: false,
|
||||||
|
backendUrl: 'http://localhost:8019'
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
Loading…
Reference in a new issue