-เพิ่ม pkg jose ไว้ encrypt secret key
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 6m5s

-jwt services/jwt.service.ts
-เพิ่ม  เวลา expire jwt token
This commit is contained in:
x2Skyz
2025-11-23 19:30:54 +07:00
parent 0d43286d84
commit 6669399b7e
12 changed files with 166 additions and 14 deletions

View File

@@ -28,6 +28,7 @@
"@tailwindcss/postcss": "^4.1.17", "@tailwindcss/postcss": "^4.1.17",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"jose": "^6.1.2",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"ng2-charts": "^6.0.1", "ng2-charts": "^6.0.1",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
@@ -13786,6 +13787,15 @@
"jiti": "bin/jiti.js" "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": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

View File

@@ -60,6 +60,7 @@
"@tailwindcss/postcss": "^4.1.17", "@tailwindcss/postcss": "^4.1.17",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"jose": "^6.1.2",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"ng2-charts": "^6.0.1", "ng2-charts": "^6.0.1",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",

View File

@@ -15,6 +15,7 @@ import { SidebarComponent } from './component/sidebar/sidebar.component';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { LicensePrivacyTermsComponent } from './component/license-privacy-terms/license-privacy-terms.component'; 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 { MainDashboardContentComponent } from './content/main-dashboard-content/main-dashboard-content.component';
// import { MainDashboardComponent } from './component/main-dashboard/main-dashboard.component'; // import { MainDashboardComponent } from './component/main-dashboard/main-dashboard.component';
// import { LoginForgotComponent } from './component/login-forgot/login-forgot.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, SidebarContentComponent,
SidebarComponent, SidebarComponent,
LicensePrivacyTermsComponent, LicensePrivacyTermsComponent,
TokenTimerComponent
// MainProjectAdd, // MainProjectAdd,
// MainProject, // MainProject,
// MainProjectContent, // MainProjectContent,

View File

@@ -1,6 +1,7 @@
import { Component, HostListener, OnInit } from '@angular/core'; import { Component, HostListener, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { trigger, state, style, transition, animate } from '@angular/animations'; import { trigger, state, style, transition, animate } from '@angular/animations';
import { JwtService } from '../../services/jwt.service';
@Component({ @Component({
selector: 'app-sidebar', selector: 'app-sidebar',
@@ -28,7 +29,10 @@ export class SidebarComponent implements OnInit {
isMobile = false; // ตรวจอุปกรณ์ isMobile = false; // ตรวจอุปกรณ์
showOverlay = false; // สำหรับ mobile overlay showOverlay = false; // สำหรับ mobile overlay
constructor(private router: Router) {} constructor(
private router: Router,
private jwtService: JwtService
) {}
ngOnInit() { ngOnInit() {
this.checkDevice(); this.checkDevice();
@@ -60,7 +64,6 @@ export class SidebarComponent implements OnInit {
} }
logout() { logout() {
localStorage.removeItem('access_token'); this.jwtService.logout();
this.router.navigate(['/login']);
} }
} }

View File

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

View File

@@ -0,0 +1,3 @@
<div *ngIf="countdown$ | async as countdown" class="token-timer">
<p>หมดอายุใน: {{ countdown }}</p>
</div>

View File

@@ -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<string | null>;
constructor(private jwtService: JwtService) {
this.countdown$ = this.jwtService.countdown$;
}
ngOnInit(): void {}
}

View File

@@ -4,6 +4,7 @@ import { GeneralService } from '../../services/generalservice';
import { LoginForgotComponent } from '../../../app/component/login-forgot/login-forgot.component'; import { LoginForgotComponent } from '../../../app/component/login-forgot/login-forgot.component';
import { LoginPageComponent } from '../../../app/component/login-page/login-page.component'; import { LoginPageComponent } from '../../../app/component/login-page/login-page.component';
import { finalize } from 'rxjs/operators'; import { finalize } from 'rxjs/operators';
import { JwtService } from '../../services/jwt.service';
@Component({ @Component({
selector: 'app-login-content', selector: 'app-login-content',
@@ -19,7 +20,8 @@ export class LoginContentComponent implements OnInit {
constructor( constructor(
private generalService: GeneralService, private generalService: GeneralService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router private router: Router,
private jwtService: JwtService
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@@ -57,6 +59,7 @@ export class LoginContentComponent implements OnInit {
if (result.code === '200' && result.data?.token) { if (result.code === '200' && result.data?.token) {
this.generalService.trowApi(result); this.generalService.trowApi(result);
localStorage.setItem('access_token', result.data.token); localStorage.setItem('access_token', result.data.token);
this.jwtService.restartCountdown();
this.router.navigate(['main']); this.router.navigate(['main']);
} else { } else {
const errorMessage = result.message_th || result.message || 'Sign-in failed.'; const errorMessage = result.message_th || result.message || 'Sign-in failed.';
@@ -95,21 +98,21 @@ export class LoginContentComponent implements OnInit {
next: (result: any) => { next: (result: any) => {
if (result.code === '200') { if (result.code === '200') {
this.generalService.trowApi(result); this.generalService.trowApi(result);
console.log(`✅ OTP ส่งไปที่ ${value.email}`); // console.log(`✅ OTP ส่งไปที่ ${value.email}`);
} else { } else {
console.warn('⚠️ ไม่สามารถส่ง OTP ได้:', result.message_th); // console.warn('⚠️ ไม่สามารถส่ง OTP ได้:', result.message_th);
} }
}, },
error: (error: any) => { error: (error: any) => {
this.loginForgotComponent.isSendOtp = false; this.loginForgotComponent.isSendOtp = false;
this.loginForgotComponent.isLoading = false; this.loginForgotComponent.isLoading = false;
this.generalService.trowApi(error); this.generalService.trowApi(error);
console.error('❌ Error sending OTP:', error); // console.error('❌ Error sending OTP:', error);
}, },
complete: () => { complete: () => {
this.loginForgotComponent.isLoading = false; this.loginForgotComponent.isLoading = false;
this.loginForgotComponent.isSendOtp = true; 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({ this.generalService.postRequest(uri, request).subscribe({
next: (result: any) => { next: (result: any) => {
if (result.code === '200') { if (result.code === '200') {
console.log(`OTP ส่งไปยืนยันสำเร็จ`); // console.log(`OTP ส่งไปยืนยันสำเร็จ`);
} else { } else {
console.warn('⚠️ ไม่สามารถส่ง OTP ได้:', result.message_th); // console.warn('⚠️ ไม่สามารถส่ง OTP ได้:', result.message_th);
} }
}, },
error: (error: any) => { error: (error: any) => {
console.error('❌ Error sending OTP:', error); // console.error('❌ Error sending OTP:', error);
}, },
complete: () => { complete: () => {
this.router.navigate(['/login']); this.router.navigate(['/login']);
console.log('📨 OTP send request completed'); // console.log('📨 OTP send request completed');
} }
}); });
} }

View File

@@ -1,6 +1,7 @@
<div class="flex h-screen overflow-hidden"> <div class="flex h-screen overflow-hidden">
<!-- Sidebar (เฉพาะ main) --> <!-- Sidebar (เฉพาะ main) -->
<app-sidebar></app-sidebar> <app-sidebar></app-sidebar>
<app-token-timer></app-token-timer>
<!-- Content --> <!-- Content -->
<div class="flex-1 overflow-y-auto bg-gray-50 text-gray-900"> <div class="flex-1 overflow-y-auto bg-gray-50 text-gray-900">

View File

@@ -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<string | null>(null);
public countdown$: Observable<string | null> = this.countdownSub.asObservable();
private timerSubscription: Subscription | null = null;
constructor(
private router: Router,
private toastr: ToastrService
) {
this.startTokenCountdown();
}
async decodeToken(token: string): Promise<any> {
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();
}
}

View File

@@ -1,4 +1,5 @@
export const environment = { export const environment = {
production: false, production: false,
apiBaseUrl: 'http://localhost:8000' apiBaseUrl: 'http://localhost:8000',
jwt_secret: '5b8273b2f79602e6b3987d3a9b018c66fd15e14848ff73ab1d332942c11eac80'
}; };

View File

@@ -1,4 +1,5 @@
export const environment = { export const environment = {
production: true, production: true,
apiBaseUrl: 'https://api.nuttakit.work' apiBaseUrl: 'https://api.nuttakit.work',
jwt_secret: '5b8273b2f79602e6b3987d3a9b018c66fd15e14848ff73ab1d332942c11eac80'
}; };