refactor(login): modernize authentication system and UI components

parent 91fbf237
......@@ -42,6 +42,12 @@
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.production.ts"
}
],
"budgets": [
{
"type": "initial",
......@@ -57,10 +63,28 @@
"outputHashing": "all"
},
"development": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
],
"optimization": false,
"extractLicenses": false,
"sourceMap": true
},
"staging": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.staging.ts"
}
],
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
......
This diff is collapsed.
import { Injectable } from '@angular/core';
import { Injectable, inject } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { AuthService } from '../../../shared/data-access/service/auth.service';
@Injectable({
providedIn: 'root',
})
@Injectable({ providedIn: 'root' })
export class AdminGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
private readonly authService = inject(AuthService);
private readonly router = inject(Router);
canActivate(): Observable<boolean> {
return this.authService.authState$.pipe(
......
import { Injectable } from '@angular/core';
import { Injectable, inject } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { AuthService } from '../../../shared/data-access/service/auth.service';
@Injectable({
providedIn: 'root',
})
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
private readonly authService = inject(AuthService);
private readonly router = inject(Router);
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
return this.authService.authState$.pipe(
......
This diff is collapsed.
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
signal,
inject,
OnInit
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import {
FormBuilder,
FormGroup,
Validators,
ReactiveFormsModule
} from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from '../../shared/data-access/service/auth.service';
import { LoginRequest } from '../../shared/data-access/interface/auth.interface';
import { NzLayoutModule } from 'ng-zorro-antd/layout';
import { NzFormModule } from 'ng-zorro-antd/form';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
@Component({
selector: 'meu-login',
......@@ -12,41 +30,53 @@ import { LoginRequest } from '../../shared/data-access/interface/auth.interface'
styleUrls: ['./login.component.scss'],
imports: [
CommonModule,
ReactiveFormsModule
ReactiveFormsModule,
NzLayoutModule,
NzFormModule,
NzInputModule,
NzButtonModule,
NzIconModule,
NzToolTipModule,
NzCheckboxModule
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LoginComponent implements OnInit {
loginForm: FormGroup;
isLoading = false;
errorMessage = '';
private readonly fb = inject(FormBuilder); // tạo form login
private readonly authService = inject(AuthService); // xác thực
private readonly router = inject(Router); // điều hướng
constructor(
private fb: FormBuilder,
private authService: AuthService,
private router: Router,
private cdr: ChangeDetectorRef
) {
isVisible = signal<boolean>(false);
errorMessage = signal<string>('');
isLoading = signal<boolean>(false);
loginForm!: FormGroup;
ngOnInit(): void {
this.initForm();
this.checkAuthenticationStatus();
}
initForm(): void {
this.loginForm = this.fb.group({
username: ['', [Validators.required]],
password: ['', [Validators.required, Validators.minLength(6)]]
password: ['', [Validators.required, Validators.minLength(6)]],
});
}
ngOnInit() {
if (this.authService.isAuthenticated()) {
this.redirectBasedOnRole();
}
private checkAuthenticationStatus(): void {
this.authService.isAuthenticated()
? this.redirectBasedOnRole()
: null;
}
onSubmit(): void {
if (this.loginForm.invalid) {
this.markFormGroupTouched();
return;
}
onLogin(): void {
this.loginForm.invalid ? this.markFormGroupTouched() : this.performLogin();
}
this.isLoading = true;
this.errorMessage = '';
private performLogin(): void {
this.isLoading.set(true);
this.errorMessage.set('');
const credentials: LoginRequest = {
username: this.loginForm.value.username,
......@@ -55,26 +85,28 @@ export class LoginComponent implements OnInit {
this.authService.login(credentials).subscribe({
next: (response) => {
this.isLoading = false;
this.cdr.detectChanges();
this.isLoading.set(false);
this.redirectBasedOnRole();
},
error: (error) => {
this.errorMessage = error;
this.isLoading = false;
this.cdr.detectChanges();
this.errorMessage.set(error);
this.isLoading.set(false);
}
});
}
private redirectBasedOnRole(): void {
if (this.authService.isAdmin()) {
this.router.navigate(['/admin']);
} else if (this.authService.isCustomer()) {
this.router.navigate(['/']);
} else {
this.router.navigate(['/']);
}
this.authService.isAuthenticated()
? this.navigateByRole()
: this.router.navigate(['/login']);
}
private navigateByRole(): void {
this.authService.isAdmin()
? this.router.navigate(['/admin'])
: this.authService.isCustomer()
? this.router.navigate(['/'])
: this.router.navigate(['/']);
}
private markFormGroupTouched(): void {
......@@ -83,7 +115,6 @@ export class LoginComponent implements OnInit {
});
}
// Getter methods for template
get username() { return this.loginForm.get('username'); }
get password() { return this.loginForm.get('password'); }
}
......@@ -16,8 +16,17 @@ import { provideAnimationsAsync } from '@angular/platform-browser/animations/asy
import { interceptors } from '../../shared/utils/interceptor/interceptor';
import { shellRoutes } from './shell.routes';
// Import ng-zorro icons
import * as AllIcons from '@ant-design/icons-angular/icons';
import { IconDefinition } from '@ant-design/icons-angular';
registerLocaleData(en);
const antDesignIcons = AllIcons as {
[key: string]: IconDefinition;
};
const icons: IconDefinition[] = Object.keys(antDesignIcons).map(key => antDesignIcons[key]);
export const shellConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
......@@ -25,6 +34,7 @@ export const shellConfig: ApplicationConfig = {
provideAnimations(),
provideHttpClient(withInterceptors([interceptors])),
provideNzI18n(en_US),
provideNzIcons(icons),
importProvidersFrom(FormsModule),
provideAnimationsAsync(),
provideHttpClient(),
......
<header class="tw-bg-white tw-shadow-sm tw-border-b tw-border-gray-200">
<nz-header class="tw-bg-white/95 tw-backdrop-blur-sm tw-shadow-lg tw-border-b tw-border-gray-100 tw-px-0">
<div class="tw-max-w-7xl tw-mx-auto tw-px-4 sm:tw-px-6 lg:tw-px-8">
<div class="tw-flex tw-justify-between tw-items-center tw-h-16">
<div class="tw-flex tw-justify-between tw-items-center tw-h-18">
<!-- Logo/Brand -->
<div class="tw-flex tw-items-center">
<div class="tw-w-8 tw-h-8 tw-bg-gradient-to-r tw-from-blue-600 tw-to-indigo-600 tw-rounded-lg tw-flex tw-items-center tw-justify-center tw-mr-3">
<svg class="tw-w-5 tw-h-5 tw-text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M20 6h-2V4c0-1.11-.89-2-2-2H8c-1.11 0-2 .89-2 2v2H4c-1.11 0-2 .89-2 2v11c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zM8 4h8v2H8V4zm11 15H5V10h14v9zm-8-7.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5-.67 1.5-1.5 1.5-1.5-.67-1.5-1.5z"/>
</svg>
<div class="tw-w-12 tw-h-12 tw-bg-gradient-to-br tw-from-blue-600 tw-via-indigo-600 tw-to-purple-600 tw-rounded-2xl tw-flex tw-items-center tw-justify-center tw-shadow-lg tw-mr-4">
<span nz-icon nzType="star" nzTheme="fill" class="tw-text-white tw-text-xl"></span>
</div>
<div>
<h1 class="tw-text-2xl tw-font-bold tw-text-gray-900">JobMeU</h1>
<p class="tw-text-sm tw-text-gray-600">Management Suite</p>
</div>
<h1 class="tw-text-xl tw-font-bold tw-text-gray-900">Job Management</h1>
</div>
<!-- User Menu -->
<div *ngIf="authState$ | async as authState" class="tw-flex tw-items-center tw-space-x-4">
<div *ngIf="authState.isAuthenticated" class="tw-flex tw-items-center tw-space-x-4">
<!-- User Info -->
<div class="tw-flex tw-items-center tw-space-x-2">
<div class="tw-w-8 tw-h-8 tw-bg-gradient-to-r tw-from-blue-500 tw-to-purple-500 tw-rounded-full tw-flex tw-items-center tw-justify-center">
<svg class="tw-w-4 tw-h-4 tw-text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"/>
</svg>
</div>
<div class="tw-text-sm">
<div class="tw-font-medium tw-text-gray-900">{{ authState.user?.username || 'User' }}</div>
<div class="tw-text-gray-500 tw-capitalize">{{ authState.user?.role }}</div>
</div>
</div>
@if (authState$ | async; as authState) {
@if (authState.isAuthenticated) {
<div class="tw-flex tw-items-center tw-space-x-6">
<!-- User Info -->
<nz-card
class="tw-bg-gray-50 tw-rounded-2xl tw-shadow-none"
[nzBordered]="false"
[nzBodyStyle]="{ padding: '12px 16px' }">
<div class="tw-flex tw-items-center tw-space-x-3">
<nz-avatar
[nzSize]="40"
nzIcon="user"
class="tw-bg-gradient-to-br tw-from-blue-500 tw-to-purple-500">
</nz-avatar>
<div class="tw-text-sm">
<div class="tw-font-semibold tw-text-gray-900">{{ authState.user?.username || 'User' }}</div>
<nz-tag
[nzColor]="authState.user?.role === 'admin' ? 'blue' : 'green'"
class="tw-text-xs tw-font-medium tw-capitalize tw-mt-1">
{{ authState.user?.role }}
</nz-tag>
</div>
</div>
</nz-card>
<!-- Logout Button -->
<button
(click)="onLogout()"
[disabled]="isLoggingOut"
class="tw-inline-flex tw-items-center tw-px-3 tw-py-2 tw-border tw-border-transparent tw-text-sm tw-leading-4 tw-font-medium tw-rounded-md tw-text-white tw-bg-red-600 hover:tw-bg-red-700 focus:tw-outline-none focus:tw-ring-2 focus:tw-ring-offset-2 focus:tw-ring-red-500 disabled:tw-opacity-50 disabled:tw-cursor-not-allowed tw-transition-colors tw-duration-200"
>
<div *ngIf="!isLoggingOut" class="tw-flex tw-items-center">
<svg class="tw-w-4 tw-h-4 tw-mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
</svg>
<span>Logout</span>
</div>
<div *ngIf="isLoggingOut" class="tw-flex tw-items-center">
<svg class="tw-animate-spin tw-w-4 tw-h-4 tw-mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="tw-opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="tw-opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Logging out...</span>
</div>
</button>
</div>
</div>
<!-- Logout Button -->
<button
nz-button
nzType="primary"
nzDanger
(click)="onLogout()"
[nzLoading]="isLoggingOut"
[disabled]="isLoggingOut"
class="tw-h-10 tw-px-4 tw-rounded-lg tw-font-medium tw-shadow-sm hover:tw-shadow-md tw-transition-all tw-duration-200 tw-flex tw-items-center tw-justify-center tw-min-w-[100px] tw-text-sm">
@if (!isLoggingOut) {
<span nz-icon nzType="logout" nzTheme="outline" class="tw-mr-2 tw-text-sm"></span>
<span>Logout</span>
} @else {
<span nz-icon nzType="loading" nzSpin class="tw-mr-2 tw-text-sm"></span>
<span>Logging out...</span>
}
</button>
</div>
}
}
</div>
</div>
</header>
\ No newline at end of file
</nz-header>
\ No newline at end of file
......@@ -2,13 +2,27 @@ import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, OnInit, inject, ChangeDetectorRef } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../../../../../shared/data-access/service/auth.service';
import { Observable } from 'rxjs';
import { Observable, finalize } from 'rxjs';
import { AuthState } from '../../../../../shared/data-access/interface/auth.interface';
import { NzLayoutModule } from 'ng-zorro-antd/layout';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzCardModule } from 'ng-zorro-antd/card';
import { NzAvatarModule } from 'ng-zorro-antd/avatar';
import { NzTagModule } from 'ng-zorro-antd/tag';
import { NzButtonModule } from 'ng-zorro-antd/button';
@Component({
selector: 'meu-header',
standalone: true,
imports: [CommonModule],
imports: [
CommonModule,
NzLayoutModule,
NzIconModule,
NzCardModule,
NzAvatarModule,
NzTagModule,
NzButtonModule
],
templateUrl: './header.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
......@@ -28,18 +42,14 @@ export class HeaderComponent implements OnInit {
this.isLoggingOut = true;
this.cdr.markForCheck();
this.authService.logout().subscribe({
next: () => {
this.router.navigate(['/login']);
},
error: (error) => {
// Even if API fails, redirect to login
this.router.navigate(['/login']);
},
complete: () => {
this.isLoggingOut = false;
this.cdr.markForCheck();
}
});
this.authService.logout()
.pipe(
finalize(() => {
this.isLoggingOut = false;
this.cdr.markForCheck();
this.router.navigate(['/login']);
})
)
.subscribe();
}
}
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { LoginRequest, LoginResponse } from '../interface/auth.interface';
import { environment } from '../../../../environments/environment';
import { StorageService } from './storage.service';
@Injectable({ providedIn: 'root' })
export class AuthApiService {
private readonly http = inject(HttpClient);
private readonly storageService = inject(StorageService);
private readonly API_URL = environment.API_DOMAIN;
// HTTP Headers
private getHeaders = (withAuth = false): HttpHeaders => {
const headers: { [key: string]: string } = { 'Content-Type': 'application/json' };
if (withAuth) {
const token = this.storageService.getToken();
if (token) headers['Authorization'] = `Bearer ${token}`;
}
return new HttpHeaders(headers);
};
// API Calls
login = (credentials: LoginRequest): Observable<LoginResponse> =>
this.http.post<LoginResponse>(`${this.API_URL}/login`, credentials, {
headers: this.getHeaders()
}).pipe(catchError(this.handleError));
refreshToken = (): Observable<LoginResponse> => {
const refreshToken = this.storageService.getRefreshToken();
if (!refreshToken) {
return throwError('No refresh token available');
}
return this.http.post<LoginResponse>(`${this.API_URL}/refresh`, { refreshToken }, {
headers: this.getHeaders()
}).pipe(catchError(this.handleError));
};
// Error handling
private handleError = (error: any): Observable<never> => {
const message = error.error?.message || error.message || 'An error occurred';
const statusMessage = this.getStatusMessage(error.status, message);
return throwError(statusMessage);
};
private getStatusMessage = (status: number, defaultMessage: string): string => {
const statusMessages: Record<number, string> = {
401: 'Invalid credentials',
403: 'Access denied',
500: 'Server error. Please try again later.'
};
return statusMessages[status] || defaultMessage;
};
}
import { Injectable, inject } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { AuthState, UserInfo } from '../interface/auth.interface';
import { StorageService } from './storage.service';
@Injectable({ providedIn: 'root' })
export class AuthStateService {
private readonly storageService = inject(StorageService);
private authStateSubject = new BehaviorSubject<AuthState>({
isAuthenticated: false,
user: null,
token: null
});
authState$ = this.authStateSubject.asObservable();
// State getters
getCurrentState = (): AuthState => this.authStateSubject.value;
isAuthenticated = (): boolean => this.getCurrentState().isAuthenticated;
getCurrentUser = (): UserInfo | null => this.getCurrentState().user;
getCurrentToken = (): string | null => this.getCurrentState().token;
// Role checks
hasRole = (role: string): boolean => this.getCurrentUser()?.role === role;
isAdmin = (): boolean => this.hasRole('admin');
isCustomer = (): boolean => this.hasRole('customer');
// State updates
setAuthenticatedState = (user: UserInfo, token: string): void => {
this.authStateSubject.next({
isAuthenticated: true,
user,
token
});
};
setUnauthenticatedState = (): void => {
this.authStateSubject.next({
isAuthenticated: false,
user: null,
token: null
});
};
// Initialize state from storage
loadStateFromStorage = (): void => {
if (this.storageService.hasValidAuthData()) {
const user = this.storageService.getUser();
const token = this.storageService.getToken();
if (user && token) {
this.setAuthenticatedState(user, token);
return;
}
}
this.setUnauthenticatedState();
};
// Complete logout
logout = (): void => {
this.storageService.clearAllAuth();
this.setUnauthenticatedState();
};
}
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { BehaviorSubject, Observable, throwError, of } from 'rxjs';
import { map, catchError, tap, delay } from 'rxjs/operators';
import { Injectable, inject } from '@angular/core';
import { Observable, of, throwError } from 'rxjs';
import { tap, delay, catchError } from 'rxjs/operators';
import { LoginRequest, LoginResponse, AuthState, UserInfo } from '../interface/auth.interface';
import { StorageService } from './storage.service';
import { AuthStateService } from './auth-state.service';
import { AuthApiService } from './auth-api.service';
@Injectable({
providedIn: 'root'
})
@Injectable({ providedIn: 'root' })
export class AuthService {
private readonly API_URL = 'http://localhost:3000'; // Replace with your actual API domain
private readonly TOKEN_KEY = 'auth_token';
private readonly USER_KEY = 'user_info';
private readonly storageService = inject(StorageService);
private readonly authStateService = inject(AuthStateService);
private readonly authApiService = inject(AuthApiService);
// Reactive state management
private authStateSubject = new BehaviorSubject<AuthState>({
isAuthenticated: false,
user: null,
token: null
});
// Expose reactive state
authState$ = this.authStateService.authState$;
public authState$ = this.authStateSubject.asObservable();
constructor(private http: HttpClient) {
// Load saved auth state on service init
this.loadAuthState();
}
/**
* Login with username and password
*/
login(credentials: LoginRequest): Observable<LoginResponse> {
const headers = new HttpHeaders({
'Content-Type': 'application/json'
});
return this.http.post<LoginResponse>(`${this.API_URL}/login`, credentials, { headers })
.pipe(
tap(response => this.handleLoginSuccess(response, credentials.username)),
catchError(error => this.handleError(error))
);
constructor() {
this.authStateService.loadStateFromStorage();
}
/**
* Logout user
*/
logout(): Observable<any> {
// Clear local storage and auth state immediately
this.handleLogoutSuccess();
// Return success observable
return of({ success: true, message: 'Logged out successfully' }).pipe(
delay(500) // Small delay to show loading state
// Main API methods
login = (credentials: LoginRequest): Observable<LoginResponse> =>
this.authApiService.login(credentials).pipe(
tap(response => this.handleLoginSuccess(response, credentials.username)),
catchError(error => {
if (error.status === 401) this.authStateService.logout();
return throwError(error);
})
);
}
/**
* Refresh access token
*/
refreshToken(): Observable<LoginResponse> {
const refreshToken = localStorage.getItem('refresh_token');
const currentUser = this.authStateSubject.value.user;
if (!refreshToken) {
this.logout();
return throwError('No refresh token available');
}
return this.http.post<LoginResponse>(`${this.API_URL}/refresh`, {
refreshToken
}).pipe(
tap(response => this.handleLoginSuccess(response, currentUser?.username || 'Unknown')),
logout = (): Observable<any> => {
this.authStateService.logout();
return of({ success: true, message: 'Logged out successfully' }).pipe(delay(500));
};
refreshToken = (): Observable<LoginResponse> =>
this.authApiService.refreshToken().pipe(
tap(response => {
const username = this.authStateService.getCurrentUser()?.username || 'Unknown';
this.handleLoginSuccess(response, username);
}),
catchError(error => {
this.logout();
this.authStateService.logout();
return throwError(error);
})
);
}
/**
* Get current user info
*/
getCurrentUser(): Observable<UserInfo> {
const headers = this.getAuthHeaders();
// Auth state getters (delegate to AuthStateService)
isAuthenticated = (): boolean => this.authStateService.isAuthenticated();
getToken = (): string | null => this.storageService.getToken();
getUser = (): UserInfo | null => this.authStateService.getCurrentUser();
hasRole = (role: string): boolean => this.authStateService.hasRole(role);
isAdmin = (): boolean => this.authStateService.isAdmin();
isCustomer = (): boolean => this.authStateService.isCustomer();
// Private helpers
private handleLoginSuccess = (response: LoginResponse, username: string): void => {
if (!response.success) throw new Error(response.message || 'Login failed');
return this.http.get<UserInfo>(`${this.API_URL}/me`, { headers })
.pipe(
tap(user => this.updateUserInfo(user)),
catchError(error => this.handleError(error))
);
}
/**
* Check if user is authenticated
*/
isAuthenticated(): boolean {
return this.authStateSubject.value.isAuthenticated;
}
/**
* Check if user has specific role
*/
hasRole(role: string): boolean {
const currentUser = this.authStateSubject.value.user;
return currentUser?.role === role;
}
/**
* Check if user is admin
*/
isAdmin(): boolean {
return this.hasRole('admin');
}
/**
* Check if user is customer
*/
isCustomer(): boolean {
return this.hasRole('customer');
}
/**
* Get stored token
*/
getToken(): string | null {
return localStorage.getItem(this.TOKEN_KEY);
}
/**
* Get current user info
*/
getUser(): UserInfo | null {
return this.authStateSubject.value.user;
}
/**
* Get auth headers for API calls
*/
private getAuthHeaders(): HttpHeaders {
const token = this.getToken();
return new HttpHeaders({
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
});
}
/**
* Handle successful login
*/
private handleLoginSuccess(response: LoginResponse, username: string): void {
if (!response.success) {
throw new Error(response.message || 'Login failed');
}
const { token, role, expirationTime } = response.responseData;
const user: UserInfo = { username, role };
// Store tokens
localStorage.setItem(this.TOKEN_KEY, token);
localStorage.setItem('token_expiration', expirationTime.toString());
// Create user info from response
const userInfo: UserInfo = {
username: username, // Use the username from login credentials
role: role
};
localStorage.setItem(this.USER_KEY, JSON.stringify(userInfo));
// Update auth state
this.authStateSubject.next({
isAuthenticated: true,
user: userInfo,
token: token
});
}
/**
* Handle logout
*/
private handleLogoutSuccess(): void {
// Clear storage
localStorage.removeItem(this.TOKEN_KEY);
localStorage.removeItem('refresh_token');
localStorage.removeItem(this.USER_KEY);
// Reset auth state
this.authStateSubject.next({
isAuthenticated: false,
user: null,
token: null
});
}
/**
* Load auth state from storage
*/
private loadAuthState(): void {
const token = localStorage.getItem(this.TOKEN_KEY);
const userInfo = localStorage.getItem(this.USER_KEY);
if (token && userInfo) {
try {
const user = JSON.parse(userInfo);
this.authStateSubject.next({
isAuthenticated: true,
user,
token
});
} catch (error) {
this.handleLogoutSuccess();
}
}
}
/**
* Update user info
*/
private updateUserInfo(user: UserInfo): void {
localStorage.setItem(this.USER_KEY, JSON.stringify(user));
const currentState = this.authStateSubject.value;
this.authStateSubject.next({
...currentState,
user
});
}
/**
* Handle API errors
*/
private handleError(error: any): Observable<never> {
let errorMessage = 'An error occurred';
if (error.error?.message) {
errorMessage = error.error.message;
} else if (error.message) {
errorMessage = error.message;
}
switch (error.status) {
case 401:
errorMessage = 'Invalid credentials';
this.handleLogoutSuccess();
break;
case 403:
errorMessage = 'Access denied';
break;
case 500:
errorMessage = 'Server error. Please try again later.';
break;
}
return throwError(errorMessage);
}
this.storageService.storeAuthData(token, user, expirationTime);
this.authStateService.setAuthenticatedState(user, token);
};
}
import { Injectable } from '@angular/core';
import { UserInfo } from '../interface/auth.interface';
@Injectable({ providedIn: 'root' })
export class StorageService {
private readonly TOKEN_KEY = 'token';
private readonly USER_KEY = 'user_info';
private readonly REFRESH_KEY = 'refresh_token';
private readonly EXPIRY_KEY = 'token_expiration';
// Token operations
setToken = (token: string): void => localStorage.setItem(this.TOKEN_KEY, token);
getToken = (): string | null => localStorage.getItem(this.TOKEN_KEY);
removeToken = (): void => localStorage.removeItem(this.TOKEN_KEY);
// User operations
setUser = (user: UserInfo): void => localStorage.setItem(this.USER_KEY, JSON.stringify(user));
getUser = (): UserInfo | null => {
const userStr = localStorage.getItem(this.USER_KEY);
if (!userStr) return null;
try {
return JSON.parse(userStr);
} catch {
return null;
}
};
removeUser = (): void => localStorage.removeItem(this.USER_KEY);
// Expiry operations
setExpiry = (expiry: number): void => localStorage.setItem(this.EXPIRY_KEY, expiry.toString());
getExpiry = (): number | null => {
const expiry = localStorage.getItem(this.EXPIRY_KEY);
return expiry ? parseInt(expiry, 10) : null;
};
removeExpiry = (): void => localStorage.removeItem(this.EXPIRY_KEY);
// Refresh token operations
setRefreshToken = (token: string): void => localStorage.setItem(this.REFRESH_KEY, token);
getRefreshToken = (): string | null => localStorage.getItem(this.REFRESH_KEY);
removeRefreshToken = (): void => localStorage.removeItem(this.REFRESH_KEY);
// Batch operations
storeAuthData = (token: string, user: UserInfo, expiry: number): void => {
this.setToken(token);
this.setUser(user);
this.setExpiry(expiry);
};
clearAllAuth = (): void => {
this.removeToken();
this.removeUser();
this.removeExpiry();
this.removeRefreshToken();
};
// Validation
hasValidAuthData = (): boolean => {
const token = this.getToken();
const user = this.getUser();
return Boolean(token && user);
};
}
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError, BehaviorSubject } from 'rxjs';
import { catchError, filter, take, switchMap } from 'rxjs/operators';
import { AuthService } from '../../data-access/service/auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
constructor(private authService: AuthService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Add auth token to requests
const authToken = this.authService.getToken();
if (authToken) {
request = this.addToken(request, authToken);
}
return next.handle(request).pipe(
catchError(error => {
if (error instanceof HttpErrorResponse && error.status === 401) {
return this.handle401Error(request, next);
} else {
return throwError(error);
}
})
);
}
private addToken(request: HttpRequest<any>, token: string): HttpRequest<any> {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
private handle401Error(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
return this.authService.refreshToken().pipe(
switchMap((response: any) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(response.accessToken);
return next.handle(this.addToken(request, response.accessToken));
}),
catchError((error) => {
this.isRefreshing = false;
this.authService.logout();
return throwError(error);
})
);
} else {
return this.refreshTokenSubject.pipe(
filter(token => token != null),
take(1),
switchMap(jwt => {
return next.handle(this.addToken(request, jwt));
})
);
}
}
}
......@@ -24,15 +24,20 @@ export const interceptors: HttpInterceptorFn = (
const showLoading = req.context.get(SHOW_LOADING);
const showApiMessage = req.context.get(SHOW_API_MESSAGE);
const showApiErrorMessage = req.context.get(SHOW_API_ERROR_MESSAGE);
const token = localStorage.getItem('token'); //custom
const token = localStorage.getItem('token');
// Clone request with headers
const headers: { [key: string]: string } = {
StandardTimeZoneName: `${Intl.DateTimeFormat().resolvedOptions().timeZone}`,
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
req = req.clone({
url: !req.url.includes('http') ? environment.API_DOMAIN + req.url : req.url,
setHeaders: {
Authorization: `Bearer ${token ? token : ''}`,
StandardTimeZoneName: `${
Intl.DateTimeFormat().resolvedOptions().timeZone
}`,
},
setHeaders: headers,
});
runIf(showLoading, () => {});
return next(req).pipe(
......
// ng-zorro-antd styles
@import 'ng-zorro-antd/ng-zorro-antd.min.css';
// Custom ant design overrides
@import "input";
export const environment = {
API_DOMAIN: 'http://localhost:5051/api/v1.0/', //custom
API_DOMAIN: 'http://localhost:3000', //custom
production: false,
staging: false,
development: true,
};
export const environment = {
API_DOMAIN: ' ', //custom
production: true,
staging: false,
development: false,
};
export const environment = {
API_DOMAIN: ' ', //custom
production: false,
staging: true,
development: false,
};
export const environment = {
API_DOMAIN: '', //custom
production: false,
staging: false,
development: false,
};
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment