diff --git a/backend/src/controllers/user.controller.ts b/backend/src/controllers/user.controller.ts index 43fd6b0..bc72742 100644 --- a/backend/src/controllers/user.controller.ts +++ b/backend/src/controllers/user.controller.ts @@ -1,194 +1,205 @@ 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 { - TokenServiceBindings, - UserServiceBindings + TokenServiceBindings, + 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'; export class UserController { - constructor( - @inject(TokenServiceBindings.TOKEN_SERVICE) - public jwtService: TokenService, - @inject(UserServiceBindings.USER_SERVICE) - public userService: SzakdolgozatUserService, - @inject(SecurityBindings.USER, {optional: true}) - public user: UserProfile, - @repository(UserRepository) - public userRepository : UserRepository, - ) { } + constructor( + @inject(TokenServiceBindings.TOKEN_SERVICE) + public jwtService: TokenService, + @inject(UserServiceBindings.USER_SERVICE) + public userService: SzakdolgozatUserService, + @inject(SecurityBindings.USER, {optional: true}) + public user: UserProfile, + @repository(UserRepository) + public userRepository: UserRepository, + ) { + } - @post('/users') - @response(200, { - description: 'User model instance', - content: {'application/json': {schema: getModelSchemaRef(User, {exclude: ['password']})}}, - }) - async register( - @requestBody({ - content: { - 'application/json': { - schema: getModelSchemaRef(User, { - title: 'Registration request', - exclude: ['id', 'role'] - }), - }, - }, + @post('/users') + @response(200, { + description: 'User model instance', + content: {'application/json': {schema: getModelSchemaRef(User, {exclude: ['password']})}}, }) - request: Pick, - ): Promise { - const password = await hash(request.password, await genSalt()); - const user = { - email: request.email, - name: request.name, - password: password, - 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', + async register( + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(User, { + title: 'Registration request', + exclude: ['id', 'isAdmin'] + }), }, - }, }, - }, - }, - }, - }, - }) - async login( - @requestBody({ - description: 'The input of login function', - required: true, - content: { - 'application/json': { - schema: getModelSchemaRef(User, {exclude: ['id', 'role', 'name']}) - }, - }}) credentials: Pick, - ): 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); + }) + request: Pick, + ): Promise { + const password = await hash(request.password, await genSalt()); + const user = { + email: request.email, + name: request.name, + password: password, + isAdmin: false + } as User; + return this.userRepository.create(user); + } - // create a JSON Web Token based on the user profile - const token = await this.jwtService.generateToken(userProfile); - return {token}; - } - - @get('/users/count') - @response(200, { - description: 'User model count', - content: {'application/json': {schema: CountSchema}}, - }) - async count( - @param.where(User) where?: Where, - ): Promise { - 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}), + @post('/users/login', { + responses: { + '200': { + description: 'Token', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + token: { + type: 'string', + }, + user: getModelSchemaRef(User, {exclude: ['id', 'password']}) + }, + }, + }, + }, + }, }, - }, - }, - }) - async find( - @param.filter(User) filter?: Filter, - ): Promise { - 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, - ): Promise { - return this.userRepository.updateAll(user, where); - } + async login( + @requestBody({ + description: 'The input of login function', + required: true, + content: { + 'application/json': { + schema: getModelSchemaRef(User, {exclude: ['id', 'isAdmin', 'name']}) + }, + } + }) credentials: Pick, + ): Promise<{ token: string, user: Omit }> { + // 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}') - @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 - ): Promise { - return this.userRepository.findById(id, filter); - } + // create a JSON Web Token based on the user profile + const token = await this.jwtService.generateToken(userProfile); + return {token, user}; + } - @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}), + @post('/users/logout', { + responses: { + '204': { + description: 'Logged out', + }, }, - }, }) - user: User, - ): Promise { - await this.userRepository.updateById(id, user); - } + @authenticate('jwt') + async logout(@inject(RestBindings.Http.REQUEST) request: Request): Promise { + 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}') - @response(204, { - description: 'User PUT success', - }) - async replaceById( - @param.path.number('id') id: number, - @requestBody() user: User, - ): Promise { - await this.userRepository.replaceById(id, user); - } + @get('/users/count') + @response(200, { + description: 'User model count', + content: {'application/json': {schema: CountSchema}}, + }) + async count( + @param.where(User) where?: Where, + ): Promise { + return this.userRepository.count(where); + } - @del('/users/{id}') - @response(204, { - description: 'User DELETE success', - }) - async deleteById(@param.path.number('id') id: number): Promise { - await this.userRepository.deleteById(id); - } + @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, + ): Promise { + 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, + ): Promise { + 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 + ): Promise { + 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 { + await this.userRepository.updateById(id, user); + } + + @del('/users/{id}') + @response(204, { + description: 'User DELETE success', + }) + async deleteById(@param.path.number('id') id: number): Promise { + await this.userRepository.deleteById(id); + } } diff --git a/backend/src/models/user.model.ts b/backend/src/models/user.model.ts index a208338..a36ac0c 100644 --- a/backend/src/models/user.model.ts +++ b/backend/src/models/user.model.ts @@ -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) { diff --git a/frontend/src/app/api.service.spec.ts b/frontend/src/app/api.service.spec.ts new file mode 100644 index 0000000..c0310ae --- /dev/null +++ b/frontend/src/app/api.service.spec.ts @@ -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(); + }); +}); diff --git a/frontend/src/app/api.service.ts b/frontend/src/app/api.service.ts new file mode 100644 index 0000000..44aeb83 --- /dev/null +++ b/frontend/src/app/api.service.ts @@ -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 { + return this.http.request(method, environment.backendUrl + url, { + body, + headers: {Authorization: 'Bearer ' + this.loginService.token} + }).toPromise(); + } + + async logout(): Promise { + await this.request('post', '/users/logout', ''); + this.loginService.token = null; + this.loginService.user = null; + } +} diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 97b8050..5569e18 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -6,8 +6,7 @@ Menü Főoldal - Link 2 - Link 3 + {{ item.title }} @@ -22,10 +21,11 @@ Szakdolgozat - + + {{ loginService.user.name }} Kijelentkezés - + Regisztráció Bejelentkezés diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 13b1c9c..b2f47fe 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -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 { - 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' }; diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index f08823c..c9c9aa5 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -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 diff --git a/frontend/src/app/login/login.component.html b/frontend/src/app/login/login.component.html index 9fff068..df59dd0 100644 --- a/frontend/src/app/login/login.component.html +++ b/frontend/src/app/login/login.component.html @@ -3,13 +3,16 @@ Email + placeholder="h123456@stud.u-szeged.hu" [formControl]="email" [errorStateMatcher]="matcher"/> Egyetemi email cim Jelszó + minlength="8" [formControl]="pass" [errorStateMatcher]="matcher"/> + + A megadott email cim vagy jelszó nem megfelelő + diff --git a/frontend/src/app/login/login.component.ts b/frontend/src/app/login/login.component.ts index 1a1e8ad..5a5b15f 100644 --- a/frontend/src/app/login/login.component.ts +++ b/frontend/src/app/login/login.component.ts @@ -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 { - await this.loginService.login(this.email, this.pass); - await this.router.navigate(['/']); + 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 + }); + } } } diff --git a/frontend/src/app/model/user.model.ts b/frontend/src/app/model/user.model.ts new file mode 100644 index 0000000..161b589 --- /dev/null +++ b/frontend/src/app/model/user.model.ts @@ -0,0 +1,7 @@ +export class User { + name: string; + isAdmin: boolean; +} + +export type UserRole = 'teacher' | 'student'; + diff --git a/frontend/src/app/register/register.component.html b/frontend/src/app/register/register.component.html index d12284f..a52a598 100644 --- a/frontend/src/app/register/register.component.html +++ b/frontend/src/app/register/register.component.html @@ -16,6 +16,18 @@ Egyetemi email megadása szükséges + + Teljes név + + + A név formátuma nem megfelelő. + + + A teljes név megadása kötelező. + + Jelszó { + async createUser(email: string, password: string, name: string): Promise { + await this.http.post(environment.backendUrl + '/users', {email, password, name}).toPromise(); } - async logout(): Promise { - } - - async login(email: string, pass: string): Promise { + async login(email: string, password: string): Promise { + 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; + } } } diff --git a/frontend/src/app/utility/form-error-state-matcher.ts b/frontend/src/app/utility/form-error-state-matcher.ts new file mode 100644 index 0000000..c908b0a --- /dev/null +++ b/frontend/src/app/utility/form-error-state-matcher.ts @@ -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)); + } +} diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts index 7b4f817..cb5a97f 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -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' }; /*