diff --git a/ng-ttc-frontend/package-lock.json b/ng-ttc-frontend/package-lock.json index d699337..afe88ea 100644 --- a/ng-ttc-frontend/package-lock.json +++ b/ng-ttc-frontend/package-lock.json @@ -28,6 +28,7 @@ "@tailwindcss/postcss": "^4.1.17", "chart.js": "^4.5.1", "dotenv": "^17.2.3", + "jose": "^6.1.2", "jwt-decode": "^4.0.0", "ng2-charts": "^6.0.1", "rxjs": "~7.8.0", @@ -13786,6 +13787,15 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jose": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.2.tgz", + "integrity": "sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/ng-ttc-frontend/package.json b/ng-ttc-frontend/package.json index c6a4308..c1012a0 100644 --- a/ng-ttc-frontend/package.json +++ b/ng-ttc-frontend/package.json @@ -60,6 +60,7 @@ "@tailwindcss/postcss": "^4.1.17", "chart.js": "^4.5.1", "dotenv": "^17.2.3", + "jose": "^6.1.2", "jwt-decode": "^4.0.0", "ng2-charts": "^6.0.1", "rxjs": "~7.8.0", diff --git a/ng-ttc-frontend/src/app/app.module.ts b/ng-ttc-frontend/src/app/app.module.ts index 388a21f..668afd3 100644 --- a/ng-ttc-frontend/src/app/app.module.ts +++ b/ng-ttc-frontend/src/app/app.module.ts @@ -15,6 +15,7 @@ import { SidebarComponent } from './component/sidebar/sidebar.component'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { LicensePrivacyTermsComponent } from './component/license-privacy-terms/license-privacy-terms.component'; +import { TokenTimerComponent } from './component/token-timer/token-timer.component'; // import { MainDashboardContentComponent } from './content/main-dashboard-content/main-dashboard-content.component'; // import { MainDashboardComponent } from './component/main-dashboard/main-dashboard.component'; // import { LoginForgotComponent } from './component/login-forgot/login-forgot.component'; @@ -38,6 +39,7 @@ import { MainProjectAdd } from './component/main-project-add/main-project-add'; SidebarContentComponent, SidebarComponent, LicensePrivacyTermsComponent, + TokenTimerComponent // MainProjectAdd, // MainProject, // MainProjectContent, diff --git a/ng-ttc-frontend/src/app/component/sidebar/sidebar.component.ts b/ng-ttc-frontend/src/app/component/sidebar/sidebar.component.ts index 893babd..aed7315 100644 --- a/ng-ttc-frontend/src/app/component/sidebar/sidebar.component.ts +++ b/ng-ttc-frontend/src/app/component/sidebar/sidebar.component.ts @@ -1,6 +1,7 @@ import { Component, HostListener, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { trigger, state, style, transition, animate } from '@angular/animations'; +import { JwtService } from '../../services/jwt.service'; @Component({ selector: 'app-sidebar', @@ -28,7 +29,10 @@ export class SidebarComponent implements OnInit { isMobile = false; // ตรวจอุปกรณ์ showOverlay = false; // สำหรับ mobile overlay - constructor(private router: Router) {} + constructor( + private router: Router, + private jwtService: JwtService + ) {} ngOnInit() { this.checkDevice(); @@ -60,7 +64,6 @@ export class SidebarComponent implements OnInit { } logout() { - localStorage.removeItem('access_token'); - this.router.navigate(['/login']); + this.jwtService.logout(); } } diff --git a/ng-ttc-frontend/src/app/component/token-timer/token-timer.component.css b/ng-ttc-frontend/src/app/component/token-timer/token-timer.component.css new file mode 100644 index 0000000..6e49340 --- /dev/null +++ b/ng-ttc-frontend/src/app/component/token-timer/token-timer.component.css @@ -0,0 +1,11 @@ +.token-timer { + position: fixed; + bottom: 10px; + right: 10px; + background-color: #f8d7da; + color: #721c24; + padding: 10px; + border: 1px solid #f5c6cb; + border-radius: 5px; + width: 10rem; +} diff --git a/ng-ttc-frontend/src/app/component/token-timer/token-timer.component.html b/ng-ttc-frontend/src/app/component/token-timer/token-timer.component.html new file mode 100644 index 0000000..64799eb --- /dev/null +++ b/ng-ttc-frontend/src/app/component/token-timer/token-timer.component.html @@ -0,0 +1,3 @@ +
+

หมดอายุใน: {{ countdown }}

+
diff --git a/ng-ttc-frontend/src/app/component/token-timer/token-timer.component.ts b/ng-ttc-frontend/src/app/component/token-timer/token-timer.component.ts new file mode 100644 index 0000000..da718d9 --- /dev/null +++ b/ng-ttc-frontend/src/app/component/token-timer/token-timer.component.ts @@ -0,0 +1,20 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { JwtService } from '../../services/jwt.service'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-token-timer', + standalone: false, + templateUrl: './token-timer.component.html', + styleUrls: ['./token-timer.component.css'] +}) +export class TokenTimerComponent implements OnInit { + countdown$: Observable; + + constructor(private jwtService: JwtService) { + this.countdown$ = this.jwtService.countdown$; + } + + ngOnInit(): void {} +} diff --git a/ng-ttc-frontend/src/app/content/login-content/login-content.component.ts b/ng-ttc-frontend/src/app/content/login-content/login-content.component.ts index f6bcf35..d3e1981 100644 --- a/ng-ttc-frontend/src/app/content/login-content/login-content.component.ts +++ b/ng-ttc-frontend/src/app/content/login-content/login-content.component.ts @@ -4,6 +4,7 @@ import { GeneralService } from '../../services/generalservice'; import { LoginForgotComponent } from '../../../app/component/login-forgot/login-forgot.component'; import { LoginPageComponent } from '../../../app/component/login-page/login-page.component'; import { finalize } from 'rxjs/operators'; +import { JwtService } from '../../services/jwt.service'; @Component({ selector: 'app-login-content', @@ -19,7 +20,8 @@ export class LoginContentComponent implements OnInit { constructor( private generalService: GeneralService, private route: ActivatedRoute, - private router: Router + private router: Router, + private jwtService: JwtService ) {} ngOnInit(): void { @@ -57,6 +59,7 @@ export class LoginContentComponent implements OnInit { if (result.code === '200' && result.data?.token) { this.generalService.trowApi(result); localStorage.setItem('access_token', result.data.token); + this.jwtService.restartCountdown(); this.router.navigate(['main']); } else { const errorMessage = result.message_th || result.message || 'Sign-in failed.'; @@ -95,21 +98,21 @@ export class LoginContentComponent implements OnInit { next: (result: any) => { if (result.code === '200') { this.generalService.trowApi(result); - console.log(`✅ OTP ส่งไปที่ ${value.email}`); + // console.log(`✅ OTP ส่งไปที่ ${value.email}`); } else { - console.warn('⚠️ ไม่สามารถส่ง OTP ได้:', result.message_th); + // console.warn('⚠️ ไม่สามารถส่ง OTP ได้:', result.message_th); } }, error: (error: any) => { this.loginForgotComponent.isSendOtp = false; this.loginForgotComponent.isLoading = false; this.generalService.trowApi(error); - console.error('❌ Error sending OTP:', error); + // console.error('❌ Error sending OTP:', error); }, complete: () => { this.loginForgotComponent.isLoading = false; this.loginForgotComponent.isSendOtp = true; - console.log('📨 OTP send request completed'); + // console.log('📨 OTP send request completed'); } }); } @@ -123,17 +126,17 @@ export class LoginContentComponent implements OnInit { this.generalService.postRequest(uri, request).subscribe({ next: (result: any) => { if (result.code === '200') { - console.log(`OTP ส่งไปยืนยันสำเร็จ`); + // console.log(`OTP ส่งไปยืนยันสำเร็จ`); } else { - console.warn('⚠️ ไม่สามารถส่ง OTP ได้:', result.message_th); + // console.warn('⚠️ ไม่สามารถส่ง OTP ได้:', result.message_th); } }, error: (error: any) => { - console.error('❌ Error sending OTP:', error); + // console.error('❌ Error sending OTP:', error); }, complete: () => { this.router.navigate(['/login']); - console.log('📨 OTP send request completed'); + // console.log('📨 OTP send request completed'); } }); } diff --git a/ng-ttc-frontend/src/app/content/sidebar-content/sidebar-content.component.html b/ng-ttc-frontend/src/app/content/sidebar-content/sidebar-content.component.html index caeb126..28c490f 100644 --- a/ng-ttc-frontend/src/app/content/sidebar-content/sidebar-content.component.html +++ b/ng-ttc-frontend/src/app/content/sidebar-content/sidebar-content.component.html @@ -1,6 +1,7 @@
+
diff --git a/ng-ttc-frontend/src/app/services/jwt.service.ts b/ng-ttc-frontend/src/app/services/jwt.service.ts new file mode 100644 index 0000000..2197439 --- /dev/null +++ b/ng-ttc-frontend/src/app/services/jwt.service.ts @@ -0,0 +1,96 @@ +import { Injectable } from '@angular/core'; +import { environment } from '../../environments/environment'; +import * as jose from 'jose'; +import { BehaviorSubject, Observable, timer, Subscription } from 'rxjs'; +import { map, takeWhile, finalize } from 'rxjs/operators'; +import { Router } from '@angular/router'; +import { ToastrService } from 'ngx-toastr'; + +@Injectable({ + providedIn: 'root' +}) +export class JwtService { + private secret = new TextEncoder().encode(environment.jwt_secret); + private countdownSub = new BehaviorSubject(null); + public countdown$: Observable = this.countdownSub.asObservable(); + private timerSubscription: Subscription | null = null; + + constructor( + private router: Router, + private toastr: ToastrService + ) { + this.startTokenCountdown(); + } + + async decodeToken(token: string): Promise { + try { + const { payload } = await jose.jwtVerify(token, this.secret); + return payload; + } catch (error) { + // console.error('Invalid token:', error); + return null; + } + } + + private startTokenCountdown(): void { + if (this.timerSubscription) { + this.timerSubscription.unsubscribe(); + } + + const token = localStorage.getItem('access_token'); + if (!token) { + this.countdownSub.next(null); + return; + } + + this.decodeToken(token).then(payload => { + if (payload && payload.exp) { + const expirationTime = payload.exp * 1000; + this.timerSubscription = timer(0, 1000).pipe( + map(() => { + const now = new Date().getTime(); + const distance = expirationTime - now; + + if (distance < 0) { + this.logout(); + return 'Expired'; + } + + const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((distance % (1000 * 60)) / 1000); + + return `${this.pad(hours)}:${this.pad(minutes)}:${this.pad(seconds)}`; + }), + takeWhile(val => val !== 'Expired', true), + finalize(() => { + if (this.countdownSub.value !== 'Expired') { + this.logout(); + } + this.countdownSub.next('Expired'); + }) + ).subscribe(val => this.countdownSub.next(val)); + } else { + this.countdownSub.next(null); + } + }); + } + + private pad(num: number): string { + return num < 10 ? '0' + num : num.toString(); + } + + public logout(): void { + localStorage.removeItem('access_token'); + this.countdownSub.next(null); + if (this.timerSubscription) { + this.timerSubscription.unsubscribe(); + } + // this.toastr.info('Your session has expired. Please log in again.', 'Session Expired'); + this.router.navigate(['/login']); // Assuming '/login' is your login route + } + + public restartCountdown(): void { + this.startTokenCountdown(); + } +} diff --git a/ng-ttc-frontend/src/environments/environment.development.ts b/ng-ttc-frontend/src/environments/environment.development.ts index d0503a7..ce21d32 100644 --- a/ng-ttc-frontend/src/environments/environment.development.ts +++ b/ng-ttc-frontend/src/environments/environment.development.ts @@ -1,4 +1,5 @@ export const environment = { production: false, - apiBaseUrl: 'http://localhost:8000' + apiBaseUrl: 'http://localhost:8000', + jwt_secret: '5b8273b2f79602e6b3987d3a9b018c66fd15e14848ff73ab1d332942c11eac80' }; diff --git a/ng-ttc-frontend/src/environments/environment.ts b/ng-ttc-frontend/src/environments/environment.ts index 6c1ed6a..828ab5a 100644 --- a/ng-ttc-frontend/src/environments/environment.ts +++ b/ng-ttc-frontend/src/environments/environment.ts @@ -1,4 +1,5 @@ export const environment = { production: true, - apiBaseUrl: 'https://api.nuttakit.work' + apiBaseUrl: 'https://api.nuttakit.work', + jwt_secret: '5b8273b2f79602e6b3987d3a9b018c66fd15e14848ff73ab1d332942c11eac80' };