-เพิ่ม 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",
|
"@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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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']);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { 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');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
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 = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
apiBaseUrl: 'http://localhost:8000'
|
apiBaseUrl: 'http://localhost:8000',
|
||||||
|
jwt_secret: '5b8273b2f79602e6b3987d3a9b018c66fd15e14848ff73ab1d332942c11eac80'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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'
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user