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'
};