From 879151ea6ceee782d6d94f9e69ca67ede186b934 Mon Sep 17 00:00:00 2001 From: Desy Ayurianti Date: Wed, 16 Oct 2024 17:13:13 +0700 Subject: [PATCH] feat(login+logout+register): integrate login logout register with REST API --- agrilink_vocpro/angular.json | 6 +- agrilink_vocpro/package-lock.json | 53 +++++----- agrilink_vocpro/package.json | 4 +- agrilink_vocpro/src/app/app.config.ts | 9 +- agrilink_vocpro/src/app/app.routes.ts | 22 +++-- .../app/cores/guard/guards/auth.guard.spec.ts | 17 ++++ .../src/app/cores/guard/guards/auth.guard.ts | 20 ++++ .../src/app/cores/interface/auth.ts | 14 +++ .../src/app/cores/interface/sensor-data.ts | 9 +- .../src/app/cores/services/api.service.ts | 24 +---- .../app/cores/services/auth.service.spec.ts | 16 +++ .../src/app/cores/services/auth.service.ts | 87 ++++++++++++++++ .../app/cores/services/sensor.service.spec.ts | 16 +++ .../src/app/cores/services/sensor.service.ts | 55 +++++++++++ .../src/app/pages/auth/auth.component.html | 67 ++++++------- .../src/app/pages/auth/auth.component.ts | 36 ++++++- .../pages/dashboard/dashboard.component.ts | 11 +-- .../layouts/sidebar/sidebar.component.html | 18 ++-- .../layouts/sidebar/sidebar.component.ts | 25 ++++- .../gauge-chart/gauge-chart.component.html | 10 -- .../gauge-chart/gauge-chart.component.scss | 11 --- .../page/gauge-chart/gauge-chart.component.ts | 99 ------------------- .../dashboard/page/graph/graph.component.ts | 8 +- .../pages/register/register.component.html | 41 ++++++++ .../pages/register/register.component.scss | 29 ++++++ .../register.component.spec.ts} | 12 +-- .../app/pages/register/register.component.ts | 44 +++++++++ 27 files changed, 515 insertions(+), 248 deletions(-) create mode 100644 agrilink_vocpro/src/app/cores/guard/guards/auth.guard.spec.ts create mode 100644 agrilink_vocpro/src/app/cores/guard/guards/auth.guard.ts create mode 100644 agrilink_vocpro/src/app/cores/interface/auth.ts create mode 100644 agrilink_vocpro/src/app/cores/services/auth.service.spec.ts create mode 100644 agrilink_vocpro/src/app/cores/services/auth.service.ts create mode 100644 agrilink_vocpro/src/app/cores/services/sensor.service.spec.ts create mode 100644 agrilink_vocpro/src/app/cores/services/sensor.service.ts delete mode 100644 agrilink_vocpro/src/app/pages/dashboard/page/gauge-chart/gauge-chart.component.html delete mode 100644 agrilink_vocpro/src/app/pages/dashboard/page/gauge-chart/gauge-chart.component.scss delete mode 100644 agrilink_vocpro/src/app/pages/dashboard/page/gauge-chart/gauge-chart.component.ts create mode 100644 agrilink_vocpro/src/app/pages/register/register.component.html create mode 100644 agrilink_vocpro/src/app/pages/register/register.component.scss rename agrilink_vocpro/src/app/pages/{dashboard/page/gauge-chart/gauge-chart.component.spec.ts => register/register.component.spec.ts} (52%) create mode 100644 agrilink_vocpro/src/app/pages/register/register.component.ts diff --git a/agrilink_vocpro/angular.json b/agrilink_vocpro/angular.json index 752110a..e79b4ff 100644 --- a/agrilink_vocpro/angular.json +++ b/agrilink_vocpro/angular.json @@ -34,7 +34,8 @@ "styles": [ "src/styles.scss", "node_modules/bootstrap/dist/css/bootstrap.min.css", - "node_modules/bootstrap-icons/font/bootstrap-icons.css" + "node_modules/bootstrap-icons/font/bootstrap-icons.css", + "node_modules/ngx-toastr/toastr.css" ], "scripts": [ "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" @@ -97,7 +98,8 @@ "styles": [ "src/styles.scss", "node_modules/bootstrap/dist/css/bootstrap.min.css", - "node_modules/bootstrap-icons/font/bootstrap-icons.css" + "node_modules/bootstrap-icons/font/bootstrap-icons.css", + "node_modules/ngx-toastr/toastr.css" ], "scripts": [ "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" diff --git a/agrilink_vocpro/package-lock.json b/agrilink_vocpro/package-lock.json index 7c42a2f..7c65fad 100644 --- a/agrilink_vocpro/package-lock.json +++ b/agrilink_vocpro/package-lock.json @@ -8,7 +8,7 @@ "name": "agrilink-vocpro", "version": "0.0.0", "dependencies": { - "@angular/animations": "^18.2.0", + "@angular/animations": "^18.2.8", "@angular/common": "^18.2.0", "@angular/compiler": "^18.2.0", "@angular/core": "^18.2.0", @@ -20,6 +20,8 @@ "bootstrap-icons": "^1.11.3", "chart.js": "^4.4.4", "highcharts-angular": "^4.0.1", + "jwt-decode": "^4.0.0", + "ngx-toastr": "^19.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.10" @@ -272,9 +274,9 @@ } }, "node_modules/@angular/animations": { - "version": "18.2.4", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.4.tgz", - "integrity": "sha512-ajjXpLD+SyxbHnmhj2ZvYpXneviOjcBgU9L2UX4OVS0jVWxCNHLhJrcMqtqFHA6U5fPnhPNR9cmnt6tmqri0rA==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.8.tgz", + "integrity": "sha512-dMSn2hg70siv3lhP+vqhMbgc923xw6XBUvnpCPEzhZqFHvPXfh/LubmsD5RtqHmjWebXtgVcgS+zg3Gq3jB2lg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -283,7 +285,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.4" + "@angular/core": "18.2.8" } }, "node_modules/@angular/build": { @@ -3862,17 +3864,6 @@ "node": ">=14" } }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.20.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz", @@ -7632,13 +7623,6 @@ "node": ">= 0.4" } }, - "node_modules/highcharts": { - "version": "11.4.8", - "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-11.4.8.tgz", - "integrity": "sha512-5Tke9LuzZszC4osaFisxLIcw7xgNGz4Sy3Jc9pRMV+ydm6sYqsPYdU8ELOgpzGNrbrRNDRBtveoR5xS3SzneEA==", - "license": "https://www.highcharts.com/license", - "peer": true - }, "node_modules/highcharts-angular": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/highcharts-angular/-/highcharts-angular-4.0.1.tgz", @@ -8633,6 +8617,15 @@ ], "license": "MIT" }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/karma": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", @@ -10063,6 +10056,20 @@ "dev": true, "license": "MIT" }, + "node_modules/ngx-toastr": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-19.0.0.tgz", + "integrity": "sha512-6pTnktwwWD+kx342wuMOWB4+bkyX9221pAgGz3SHOJH0/MI9erLucS8PeeJDFwbUYyh75nQ6AzVtolgHxi52dQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0-0", + "@angular/core": ">=16.0.0-0", + "@angular/platform-browser": ">=16.0.0-0" + } + }, "node_modules/nice-napi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", diff --git a/agrilink_vocpro/package.json b/agrilink_vocpro/package.json index 0b57c3c..d8d8a65 100644 --- a/agrilink_vocpro/package.json +++ b/agrilink_vocpro/package.json @@ -10,7 +10,7 @@ }, "private": true, "dependencies": { - "@angular/animations": "^18.2.0", + "@angular/animations": "^18.2.8", "@angular/common": "^18.2.0", "@angular/compiler": "^18.2.0", "@angular/core": "^18.2.0", @@ -22,6 +22,8 @@ "bootstrap-icons": "^1.11.3", "chart.js": "^4.4.4", "highcharts-angular": "^4.0.1", + "jwt-decode": "^4.0.0", + "ngx-toastr": "^19.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.10" diff --git a/agrilink_vocpro/src/app/app.config.ts b/agrilink_vocpro/src/app/app.config.ts index 74f4c3b..7e40e44 100644 --- a/agrilink_vocpro/src/app/app.config.ts +++ b/agrilink_vocpro/src/app/app.config.ts @@ -1,13 +1,16 @@ -import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; +import { ApplicationConfig, provideZoneChangeDetection, importProvidersFrom } from '@angular/core'; import { provideRouter } from '@angular/router'; import { provideHttpClient } from '@angular/common/http'; - +import { provideAnimations } from '@angular/platform-browser/animations'; +import { ToastrModule } from 'ngx-toastr'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), - provideHttpClient() + provideHttpClient(), + provideAnimations(), + importProvidersFrom(ToastrModule.forRoot()), ] }; diff --git a/agrilink_vocpro/src/app/app.routes.ts b/agrilink_vocpro/src/app/app.routes.ts index 0899796..d797d16 100644 --- a/agrilink_vocpro/src/app/app.routes.ts +++ b/agrilink_vocpro/src/app/app.routes.ts @@ -3,28 +3,36 @@ import { DashboardComponent } from './pages/dashboard/dashboard.component'; import { LayoutsComponent } from './pages/dashboard/layouts/layouts.component'; import { GraphComponent } from './pages/dashboard/page/graph/graph.component'; import { AuthComponent } from './pages/auth/auth.component'; +import { AuthGuard } from './cores/guard/guards/auth.guard'; +import { RegisterComponent } from './pages/register/register.component'; export const routes: Routes = [ { path: '', - redirectTo:'dashboard', - pathMatch:'full' + redirectTo: 'auth', + pathMatch: 'full' }, { - path: 'auth', + path: 'auth', component: AuthComponent }, { - path: '', + path: 'register', + component: RegisterComponent + }, + { + path: '', component: LayoutsComponent, - children:[ + children: [ { path: 'dashboard', component: DashboardComponent, - }, + canActivate: [AuthGuard] + }, { path: 'graph', - component: GraphComponent + component: GraphComponent, + canActivate: [AuthGuard] }, ] } diff --git a/agrilink_vocpro/src/app/cores/guard/guards/auth.guard.spec.ts b/agrilink_vocpro/src/app/cores/guard/guards/auth.guard.spec.ts new file mode 100644 index 0000000..4ae275e --- /dev/null +++ b/agrilink_vocpro/src/app/cores/guard/guards/auth.guard.spec.ts @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing'; +import { CanActivateFn } from '@angular/router'; + +import { authGuard } from './auth.guard'; + +describe('authGuard', () => { + const executeGuard: CanActivateFn = (...guardParameters) => + TestBed.runInInjectionContext(() => authGuard(...guardParameters)); + + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + + it('should be created', () => { + expect(executeGuard).toBeTruthy(); + }); +}); diff --git a/agrilink_vocpro/src/app/cores/guard/guards/auth.guard.ts b/agrilink_vocpro/src/app/cores/guard/guards/auth.guard.ts new file mode 100644 index 0000000..489e713 --- /dev/null +++ b/agrilink_vocpro/src/app/cores/guard/guards/auth.guard.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { CanActivate, Router } from '@angular/router'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthGuard implements CanActivate { + + constructor(private router: Router) {} + + canActivate(): boolean { + const token = localStorage.getItem('accessToken'); + if (token) { + return true; + } else { + this.router.navigate(['auth']); + return false; + } + } +} diff --git a/agrilink_vocpro/src/app/cores/interface/auth.ts b/agrilink_vocpro/src/app/cores/interface/auth.ts new file mode 100644 index 0000000..6be4345 --- /dev/null +++ b/agrilink_vocpro/src/app/cores/interface/auth.ts @@ -0,0 +1,14 @@ +export interface LoginData { + email: string; + password: string; + rememberMe?: boolean; +} + +export interface RegistrationData { + username: string; + pwd: string; + email: string; + google_id: string; + fullname: string; + avatar?: string; +} diff --git a/agrilink_vocpro/src/app/cores/interface/sensor-data.ts b/agrilink_vocpro/src/app/cores/interface/sensor-data.ts index f8256cd..f087e6c 100644 --- a/agrilink_vocpro/src/app/cores/interface/sensor-data.ts +++ b/agrilink_vocpro/src/app/cores/interface/sensor-data.ts @@ -18,13 +18,14 @@ export interface ParameterSensor { export interface ApiResponse { data: { - dht?: ParameterSensor[]; - npk1?: ParameterSensor[]; - npk2?: ParameterSensor[]; + dht: Array; + npk1: Array; + npk2: Array; }; statusCode: number; message: string; -} + } + export interface DHTSensor { lightIntensity: number; diff --git a/agrilink_vocpro/src/app/cores/services/api.service.ts b/agrilink_vocpro/src/app/cores/services/api.service.ts index 4ae3657..b3c8267 100644 --- a/agrilink_vocpro/src/app/cores/services/api.service.ts +++ b/agrilink_vocpro/src/app/cores/services/api.service.ts @@ -1,29 +1,11 @@ import { Injectable } from '@angular/core'; -import { HttpClient, HttpParams } from '@angular/common/http'; -import { Observable } from 'rxjs'; -import { ApiResponse } from '../interface/sensor-data'; +import { HttpClient } from '@angular/common/http'; @Injectable({ providedIn: 'root' }) export class ApiService { - private baseUrl = 'https://jx027dj4-3333.asse.devtunnels.ms/api/sensor/'; + protected baseUrl = 'https://jx027dj4-3333.asse.devtunnels.ms/'; - constructor(private http: HttpClient) {} - - getSensorData(sensor: string, metric: string, startDate: string, timeRange: string): Observable { - const url = `${this.baseUrl}getData`; - const params = new HttpParams() - .set('range[start]', startDate) - .set('range[time_range]', timeRange) - .set('sensor', sensor) - .set('metric', metric); - - return this.http.get(url, { params }); - } - - getLatestData(): Observable { - const url = `${this.baseUrl}getLatest`; - return this.http.get(url); - } + constructor(protected http: HttpClient) {} } diff --git a/agrilink_vocpro/src/app/cores/services/auth.service.spec.ts b/agrilink_vocpro/src/app/cores/services/auth.service.spec.ts new file mode 100644 index 0000000..f1251ca --- /dev/null +++ b/agrilink_vocpro/src/app/cores/services/auth.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/agrilink_vocpro/src/app/cores/services/auth.service.ts b/agrilink_vocpro/src/app/cores/services/auth.service.ts new file mode 100644 index 0000000..a8d8855 --- /dev/null +++ b/agrilink_vocpro/src/app/cores/services/auth.service.ts @@ -0,0 +1,87 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { ApiService } from './api.service'; +import { LoginData } from '../interface/auth'; +import {jwtDecode}from 'jwt-decode'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthService extends ApiService { + private authUrl = `${this.baseUrl}auth/login`; + private logoutUrl = `${this.baseUrl}auth/logout`; + private registerUrl = `${this.baseUrl}auth/register`; + + constructor(http: HttpClient) { + super(http); + } + + + login(data: LoginData): Observable { + const headers = new HttpHeaders({ + Authorization: 'Basic ' + btoa(`${data.email}:${data.password}`) + }); + + const formData = new FormData(); + formData.append('remember_me', data.rememberMe ? 'true' : 'false'); + + return this.http.post(this.authUrl, formData, { headers }).pipe( + tap(response => { + const accessToken = response.data.token; + this.saveTokens(accessToken); + + const jwtToken = response.data.jwtToken; + const decodedToken: any = jwtDecode(jwtToken); + + this.saveUserDataToStorage(decodedToken.user.fullname, decodedToken.user.avatar); + }) + ); + } + + saveTokens(token: string) { + localStorage.setItem('accessToken', token); + } + + logout(): Observable { + const token = localStorage.getItem('accessToken'); + const headers = new HttpHeaders({ + Authorization: `Bearer ${token}` + }); + + return this.http.post(this.logoutUrl, {}, { headers }).pipe( + tap(() => { + localStorage.removeItem('accessToken'); + this.clearUserDataFromStorage(); + }) + ); + } + + register(data: any): Observable { + const headers = new HttpHeaders({}); + return this.http.post(this.registerUrl, data, { headers }).pipe( + // tap(response => { + // console.log('Registration response:', response); + // }) + ); + } + + getUserFullName(): string | null { + return localStorage.getItem('userFullName'); + } + + getAvatar(): string | null { + return localStorage.getItem('avatar'); + } + + private saveUserDataToStorage(fullName: string | null, avatar: string | null) { + localStorage.setItem('userFullName', fullName || ''); + localStorage.setItem('avatar', avatar || ''); + } + + private clearUserDataFromStorage() { + localStorage.removeItem('userFullName'); + localStorage.removeItem('avatar'); + } +} diff --git a/agrilink_vocpro/src/app/cores/services/sensor.service.spec.ts b/agrilink_vocpro/src/app/cores/services/sensor.service.spec.ts new file mode 100644 index 0000000..1a2cdc0 --- /dev/null +++ b/agrilink_vocpro/src/app/cores/services/sensor.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { SensorService } from './sensor.service'; + +describe('SensorService', () => { + let service: SensorService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SensorService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/agrilink_vocpro/src/app/cores/services/sensor.service.ts b/agrilink_vocpro/src/app/cores/services/sensor.service.ts new file mode 100644 index 0000000..4a39720 --- /dev/null +++ b/agrilink_vocpro/src/app/cores/services/sensor.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@angular/core'; +import { HttpParams, HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; +import { catchError, Observable, throwError, tap } from 'rxjs'; +import { ApiService } from './api.service'; +import { ApiResponse } from '../interface/sensor-data'; + +@Injectable({ + providedIn: 'root' +}) +export class SensorService extends ApiService { + constructor(http: HttpClient) { + super(http); + } + + getSensorData(sensor: string, metric: string, startEnd: string, timeRange: string): Observable { + const url = `${this.baseUrl}api/sensor/getData`; + const params = new HttpParams() + .set('range[end]', startEnd) + .set('range[time_range]', timeRange) + .set('sensor', sensor) + .set('metric', metric); + + const token = localStorage.getItem('accessToken'); + const headers = new HttpHeaders({ + Authorization: 'Bearer ' + token, + }); + + return this.http.get(url, { params, headers }).pipe( + catchError((error: HttpErrorResponse) => { + if (error.error instanceof ErrorEvent) { + console.error('An error occurred:', error.error.message); + } else { + console.error(`Backend returned code ${error.status}, body was: ${error.error}`); + } + return throwError('Something went wrong; please try again later.'); + }) + ); + } + + getLatestData(): Observable { + const url = `${this.baseUrl}api/sensor/getLatest`; + const headers = new HttpHeaders({ + Authorization: 'Bearer ' + localStorage.getItem('accessToken'), + + }); + + return this.http.get(url, { headers }).pipe( + catchError(error => { + console.error('API Error:', error); + return throwError(error); + }) + ); + + } +} diff --git a/agrilink_vocpro/src/app/pages/auth/auth.component.html b/agrilink_vocpro/src/app/pages/auth/auth.component.html index 91ad5d2..7a76fec 100644 --- a/agrilink_vocpro/src/app/pages/auth/auth.component.html +++ b/agrilink_vocpro/src/app/pages/auth/auth.component.html @@ -1,43 +1,40 @@
-
-
-
-