This commit is contained in:
Chris Chen
2026-05-25 17:32:18 -07:00
parent 9b28fbcfb6
commit d5648315a0
262 changed files with 32074 additions and 0 deletions
+120
View File
@@ -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.
+32
View File
@@ -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)));
}
}