WIP
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
# Authentication Guard System
|
||||
|
||||
This implementation provides a complete authentication system that automatically detects if a user is logged in and redirects to the login page if not.
|
||||
|
||||
## Components
|
||||
|
||||
### AuthGuard (`auth.guard.ts`)
|
||||
- **Purpose**: Protects routes that require authentication
|
||||
- **Functionality**:
|
||||
- Checks if user is authenticated using `AuthService.isAuthenticated()`
|
||||
- Stores attempted URL for redirect after login
|
||||
- Redirects to `/login` if not authenticated
|
||||
- Allows access if authenticated
|
||||
|
||||
### LoginPage (`login-page.ts`)
|
||||
- **Purpose**: Full-page login interface for routing
|
||||
- **Features**:
|
||||
- Beautiful landing page with company branding
|
||||
- Integrated login dialog
|
||||
- Demo credentials display
|
||||
- Automatic redirect after successful login
|
||||
- Prevents access if already logged in
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. Route Protection
|
||||
All protected routes use the `AuthGuard`:
|
||||
```typescript
|
||||
{ path: 'dashboard', component: Dashboard, canActivate: [AuthGuard] }
|
||||
```
|
||||
|
||||
### 2. Authentication Flow
|
||||
1. User tries to access protected route
|
||||
2. `AuthGuard` checks authentication status
|
||||
3. If not authenticated:
|
||||
- Stores attempted URL
|
||||
- Redirects to `/login`
|
||||
4. If authenticated:
|
||||
- Allows access to requested route
|
||||
|
||||
### 3. Login Process
|
||||
1. User lands on `/login` page
|
||||
2. Clicks "Sign In" button
|
||||
3. Login dialog opens with MFA support
|
||||
4. After successful login:
|
||||
- User data stored in localStorage
|
||||
- Redirected to originally requested URL or dashboard
|
||||
|
||||
### 4. Logout Process
|
||||
1. User clicks logout from header dropdown
|
||||
2. `AuthService.logout()` clears user data
|
||||
3. Redirected to `/login` page
|
||||
|
||||
## User State Management
|
||||
|
||||
### AuthService Features
|
||||
- **Persistent Login**: User stays logged in across browser sessions
|
||||
- **State Management**: Reactive user state with RxJS
|
||||
- **Redirect URLs**: Remembers where user was trying to go
|
||||
- **localStorage**: Automatic persistence and restoration
|
||||
|
||||
### Header Integration
|
||||
- **Dynamic User Menu**: Shows different options based on auth state
|
||||
- **User Information**: Displays current user name/email
|
||||
- **Logout Button**: Easy access to logout functionality
|
||||
|
||||
## Route Structure
|
||||
|
||||
```
|
||||
/login - Public login page
|
||||
/dashboard - Protected (requires auth)
|
||||
/schedule - Protected (requires auth)
|
||||
/patients - Protected (requires auth)
|
||||
... - All other routes protected
|
||||
/** - Catch-all redirects to /login
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Adding New Protected Routes
|
||||
```typescript
|
||||
{ path: 'new-feature', component: NewFeatureComponent, canActivate: [AuthGuard] }
|
||||
```
|
||||
|
||||
### Checking Auth State in Components
|
||||
```typescript
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.authService.currentUser$.subscribe(user => {
|
||||
if (user) {
|
||||
// User is logged in
|
||||
} else {
|
||||
// User is not logged in
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Manual Logout
|
||||
```typescript
|
||||
this.authService.logout();
|
||||
this.router.navigate(['/login']);
|
||||
```
|
||||
|
||||
## Security Features
|
||||
|
||||
- **Route Protection**: All sensitive routes are protected
|
||||
- **Persistent Sessions**: Users stay logged in across browser sessions
|
||||
- **Automatic Redirects**: Seamless user experience
|
||||
- **State Validation**: Authentication state is checked on every route change
|
||||
- **Clean Logout**: Complete session cleanup on logout
|
||||
|
||||
## Testing
|
||||
|
||||
Use these test credentials:
|
||||
- **Direct Login**: `user@example.com` / `password123`
|
||||
- **MFA Required**: `admin@example.com` / `password123` / `123456`
|
||||
|
||||
The system will automatically redirect unauthenticated users to the login page and remember where they were trying to go.
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map, catchError } from 'rxjs/operators';
|
||||
import { AuthService } from '../../shared/services/auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
canActivate(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Observable<boolean> | Promise<boolean> | boolean {
|
||||
// Check if user is authenticated
|
||||
if (this.authService.isAuthenticated()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Store the attempted URL for redirecting after login
|
||||
this.authService.setRedirectUrl(state.url);
|
||||
|
||||
// Redirect to login page
|
||||
this.router.navigate(['/login']);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
import { catchError, throwError } from 'rxjs';
|
||||
import { AuthService } from '../../shared/services/auth.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { ApiConfigService } from '../services/api-config.service';
|
||||
|
||||
export const authInterceptor: HttpInterceptorFn = (request, next) => {
|
||||
const authService = inject(AuthService);
|
||||
const apiConfigService = inject(ApiConfigService);
|
||||
const router = inject(Router);
|
||||
|
||||
// Get the current user and token
|
||||
const token = authService.getToken();
|
||||
|
||||
// Clone the request and add the Authorization header if token exists
|
||||
if (token && shouldAddToken(apiConfigService, request)) {
|
||||
request = request.clone({
|
||||
setHeaders: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle the request and catch 401 errors
|
||||
return next(request).pipe(
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
if (error.status === 401) {
|
||||
// Token is invalid or expired, logout user
|
||||
authService.logout();
|
||||
router.navigate(['/login']);
|
||||
}
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if the token should be added to this request
|
||||
* Skip adding token to login requests and other public endpoints
|
||||
*/
|
||||
function shouldAddToken(apiConfigService: ApiConfigService, request: any): boolean {
|
||||
// Don't add token to outbound requests to other domains
|
||||
if (!request.url.startsWith(apiConfigService.getBaseUrl())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't add token to login requests
|
||||
if (request.url.includes('/Auth/login') || request.url.includes('/Token/Create')) {
|
||||
return false;
|
||||
}
|
||||
// Don't add token to public endpoints (you can customize this list)
|
||||
const publicEndpoints = [
|
||||
'/Auth/register',
|
||||
'/Auth/forgot-password',
|
||||
'/Auth/reset-password'
|
||||
];
|
||||
|
||||
return !publicEndpoints.some(endpoint => request.url.includes(endpoint));
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApiConfigService {
|
||||
private readonly baseUrl = environment.apiUrl;
|
||||
|
||||
constructor() { }
|
||||
|
||||
/**
|
||||
* Get the full API URL for a specific endpoint
|
||||
* @param endpoint - The API endpoint (e.g., 'Auth', 'Users', 'Transactions')
|
||||
* @returns Full API URL
|
||||
*/
|
||||
getApiUrl(endpoint: string): string {
|
||||
return `${this.baseUrl}/${endpoint}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base API URL
|
||||
* @returns Base API URL
|
||||
*/
|
||||
getBaseUrl(): string {
|
||||
return this.baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific API endpoints
|
||||
*/
|
||||
get authUrl(): string {
|
||||
return this.getApiUrl('Auth');
|
||||
}
|
||||
|
||||
get tokenUrl(): string {
|
||||
return this.getApiUrl('Token');
|
||||
}
|
||||
|
||||
get usersUrl(): string {
|
||||
return this.getApiUrl('Users');
|
||||
}
|
||||
|
||||
get transactionsUrl(): string {
|
||||
return this.getApiUrl('Transactions');
|
||||
}
|
||||
|
||||
get dashboardUrl(): string {
|
||||
return this.getApiUrl('Dashboard');
|
||||
}
|
||||
get ordersUrl(): string {
|
||||
return this.getApiUrl('ClientBridgeOrder');
|
||||
}
|
||||
|
||||
get orderDetailUrl(): string {
|
||||
return this.getApiUrl('OrderDetail');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { ApiConfigService } from './api-config.service';
|
||||
|
||||
// Type definitions
|
||||
type TextResponse = { message: string };
|
||||
/**
|
||||
* Base CRUD service that targets the provided controller path.
|
||||
*
|
||||
* It mirrors the endpoints of CrudBaseApiController<T>:
|
||||
* GET /api/{controller}
|
||||
* GET /api/{controller}/{id}
|
||||
* POST /api/{controller} -> string
|
||||
* POST /api/{controller}/batch -> string[]
|
||||
* PUT /api/{controller}
|
||||
* PUT /api/{controller}/batch -> number
|
||||
* DELETE /api/{controller}/{id}
|
||||
* DELETE /api/{controller}/batch -> text summary
|
||||
* GET /api/{controller}/{id}/exists -> boolean
|
||||
* GET /api/{controller}/count -> number
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CrudBaseApiService<T extends object> {
|
||||
/**
|
||||
* Example: baseUrl = 'https://your-api', controller = 'Customer' →
|
||||
* endpoint = 'https://your-api/api/Customer'
|
||||
*/
|
||||
protected readonly endpoint: string;
|
||||
|
||||
|
||||
/**
|
||||
* @param http Angular HttpClient
|
||||
* @param baseUrl API root without trailing slash (e.g., environment.apiBaseUrl)
|
||||
* @param controllerName Controller name (e.g., 'Customer', 'Orders')
|
||||
*/
|
||||
constructor(
|
||||
protected http: HttpClient,
|
||||
protected apiConfig: ApiConfigService,
|
||||
@Inject(String) private controllerName: string
|
||||
) {
|
||||
this.endpoint = apiConfig.getApiUrl(this.controllerName);
|
||||
}
|
||||
|
||||
|
||||
/** Optional default headers (JSON). Override in subclasses if needed. */
|
||||
protected get jsonHeaders(): HttpHeaders {
|
||||
return new HttpHeaders({ 'Content-Type': 'application/json' });
|
||||
}
|
||||
|
||||
|
||||
/** Shared error handler that surfaces useful messages. */
|
||||
protected handleError(error: HttpErrorResponse): Observable<never> {
|
||||
let msg = 'Unknown error';
|
||||
if (error.error instanceof Blob) {
|
||||
// In case backend returns text/plain; charset=utf-8 as Blob
|
||||
return throwError(() => new Error('Server returned an error blob'));
|
||||
}
|
||||
if (typeof error.error === 'string') msg = error.error;
|
||||
else if (error.error?.message) msg = error.error.message;
|
||||
else if (error.message) msg = error.message;
|
||||
return throwError(() => new Error(msg));
|
||||
}
|
||||
/** Prepare the response for the given entity. Override in subclasses if needed. */
|
||||
protected prepareResponse(response: T): T {
|
||||
// Do nothing by default
|
||||
return response;
|
||||
}
|
||||
|
||||
/** GET /api/{controller} */
|
||||
getAll(): Observable<T[]> {
|
||||
return this.http
|
||||
.get<T[]>(this.endpoint)
|
||||
.pipe(
|
||||
map(response => {
|
||||
|
||||
for (let i = 0; i < response.length; i++) {
|
||||
const element = response[i];
|
||||
response[i] = this.prepareResponse(element);
|
||||
}
|
||||
return response;
|
||||
}),
|
||||
catchError(err => this.handleError(err)));
|
||||
}
|
||||
|
||||
|
||||
/** GET /api/{controller}/{id} */
|
||||
getById(id: string): Observable<T> {
|
||||
return this.http
|
||||
.get<T>(`${this.endpoint}/${id}`)
|
||||
.pipe(
|
||||
map(response => this.prepareResponse(response)),
|
||||
catchError(err => this.handleError(err)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** POST /api/{controller} -> string */
|
||||
create(entity: T): Observable<string> {
|
||||
return this.http
|
||||
.post<string>(this.endpoint, entity, { headers: this.jsonHeaders })
|
||||
.pipe(catchError(err => this.handleError(err)));
|
||||
}
|
||||
|
||||
|
||||
/** POST /api/{controller}/batch -> string[] */
|
||||
createRange(entities: T[]): Observable<string[]> {
|
||||
return this.http
|
||||
.post<string[]>(`${this.endpoint}/batch`, entities, { headers: this.jsonHeaders })
|
||||
.pipe(catchError(err => this.handleError(err)));
|
||||
}
|
||||
|
||||
|
||||
/** PUT /api/{controller} */
|
||||
update(entity: T): Observable<void> {
|
||||
return this.http
|
||||
.put<void>(this.endpoint, entity, { headers: this.jsonHeaders })
|
||||
.pipe(catchError(err => this.handleError(err)));
|
||||
}
|
||||
|
||||
|
||||
/** PUT /api/{controller}/batch -> number (updated count) */
|
||||
updateRange(entities: T[]): Observable<number> {
|
||||
return this.http
|
||||
.put<number>(`${this.endpoint}/batch`, entities, { headers: this.jsonHeaders })
|
||||
.pipe(catchError(err => this.handleError(err)));
|
||||
}
|
||||
|
||||
|
||||
/** DELETE /api/{controller}/{id} */
|
||||
delete(id: string): Observable<void> {
|
||||
return this.http
|
||||
.delete<void>(`${this.endpoint}/${id}`)
|
||||
.pipe(catchError(err => this.handleError(err)));
|
||||
}
|
||||
|
||||
|
||||
/** DELETE /api/{controller}/batch -> text summary */
|
||||
deleteRange(ids: string[]): Observable<TextResponse> {
|
||||
// API returns a plain text message; map it into a TextResponse for convenience
|
||||
return this.http
|
||||
.delete(`${this.endpoint}/batch`, {
|
||||
body: ids,
|
||||
headers: this.jsonHeaders
|
||||
})
|
||||
.pipe(
|
||||
map((response: any) => ({ message: response || 'Batch delete completed' })),
|
||||
catchError(err => this.handleError(err))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/** GET /api/{controller}/{id}/exists -> boolean */
|
||||
exists(id: string): Observable<boolean> {
|
||||
return this.http
|
||||
.get<boolean>(`${this.endpoint}/${id}/exists`)
|
||||
.pipe(catchError(err => this.handleError(err)));
|
||||
}
|
||||
|
||||
|
||||
/** GET /api/{controller}/count -> number */
|
||||
count(): Observable<number> {
|
||||
return this.http
|
||||
.get<number>(`${this.endpoint}/count`)
|
||||
.pipe(catchError(err => this.handleError(err)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user