-เพิ่ม pkg jose ไว้ encrypt secret key
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 6m5s
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:
10
ng-ttc-frontend/package-lock.json
generated
10
ng-ttc-frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<div *ngIf="countdown$ | async as countdown" class="token-timer">
|
||||
<p>หมดอายุใน: {{ countdown }}</p>
|
||||
</div>
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
<!-- Sidebar (เฉพาะ main) -->
|
||||
<app-sidebar></app-sidebar>
|
||||
<app-token-timer></app-token-timer>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-y-auto bg-gray-50 text-gray-900">
|
||||
|
||||
96
ng-ttc-frontend/src/app/services/jwt.service.ts
Normal file
96
ng-ttc-frontend/src/app/services/jwt.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiBaseUrl: 'http://localhost:8000'
|
||||
apiBaseUrl: 'http://localhost:8000',
|
||||
jwt_secret: '5b8273b2f79602e6b3987d3a9b018c66fd15e14848ff73ab1d332942c11eac80'
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
apiBaseUrl: 'https://api.nuttakit.work'
|
||||
apiBaseUrl: 'https://api.nuttakit.work',
|
||||
jwt_secret: '5b8273b2f79602e6b3987d3a9b018c66fd15e14848ff73ab1d332942c11eac80'
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user