feature: login

parent f87a738f
This diff is collapsed.
import { Route } from '@angular/router';
import { AdminLayoutComponent } from './layout/feature/ui/layout.component';
import { AdminGuard } from '../+login/data-access/guard/admin.guard';
const ADMIN_ROUTES: Route[] = [
{
path: '',
component: AdminLayoutComponent,
canActivate: [AdminGuard],
children: [
{
path: '',
......
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
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',
})
export class AdminGuard implements CanActivate {
constructor() {}
canActivate(): boolean {
return true;
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(): Observable<boolean> {
return this.authService.authState$.pipe(
take(1),
map(authState => {
// Check authentication first
if (!authState.isAuthenticated) {
this.router.navigate(['/login']);
return false;
}
// Check admin role
if (authState.user?.role !== 'admin') {
this.router.navigate(['/']);
return false;
}
return true;
})
);
}
}
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
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',
})
export class AuthGuard implements CanActivate {
constructor() {}
canActivate(): boolean {
return true;
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
return this.authService.authState$.pipe(
take(1),
map(authState => {
// Check if user is authenticated
if (!authState.isAuthenticated) {
this.router.navigate(['/login']);
return false;
}
// Check role-based access if required
const requiredRole = route.data?.['role'];
if (requiredRole && authState.user?.role !== requiredRole) {
this.router.navigate(['/unauthorized']);
return false;
}
return true;
})
);
}
}
This diff is collapsed.
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
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';
@Component({
selector: 'meu-login',
standalone: true,
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
imports: [CommonModule],
imports: [
CommonModule,
ReactiveFormsModule
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LoginComponent implements OnInit {
constructor() {}
loginForm: FormGroup;
isLoading = false;
errorMessage = '';
constructor(
private fb: FormBuilder,
private authService: AuthService,
private router: Router,
private cdr: ChangeDetectorRef
) {
this.loginForm = this.fb.group({
username: ['', [Validators.required]],
password: ['', [Validators.required, Validators.minLength(6)]]
});
}
ngOnInit() {
if (this.authService.isAuthenticated()) {
this.redirectBasedOnRole();
}
}
onSubmit(): void {
if (this.loginForm.invalid) {
this.markFormGroupTouched();
return;
}
this.isLoading = true;
this.errorMessage = '';
const credentials: LoginRequest = {
username: this.loginForm.value.username,
password: this.loginForm.value.password
};
this.authService.login(credentials).subscribe({
next: (response) => {
this.isLoading = false;
this.cdr.detectChanges();
this.redirectBasedOnRole();
},
error: (error) => {
this.errorMessage = error;
this.isLoading = false;
this.cdr.detectChanges();
}
});
}
private redirectBasedOnRole(): void {
if (this.authService.isAdmin()) {
this.router.navigate(['/admin']);
} else if (this.authService.isCustomer()) {
this.router.navigate(['/']);
} else {
this.router.navigate(['/']);
}
}
private markFormGroupTouched(): void {
Object.keys(this.loginForm.controls).forEach(key => {
this.loginForm.get(key)?.markAsTouched();
});
}
ngOnInit() {}
// Getter methods for template
get username() { return this.loginForm.get('username'); }
get password() { return this.loginForm.get('password'); }
}
import { Routes } from '@angular/router';
import { LayoutComponent } from '../ui/layout.component';
import { LoginComponent } from '../../+login/feature/login.component';
import { AuthGuard } from '../../+login/data-access/guard/auth.guard';
import { AdminGuard } from '../../+login/data-access/guard/admin.guard';
export const shellRoutes: Routes = [
......@@ -33,7 +31,6 @@ export const shellRoutes: Routes = [
},
{
path: 'login',
component: LoginComponent,
canActivate: [AuthGuard],
loadChildren: () => import('../../+login/login.routes'),
},
];
<header>
<h1>
It is a header
</h1>
<header class="tw-bg-white tw-shadow-sm tw-border-b tw-border-gray-200">
<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">
<!-- 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>
<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>
<!-- 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>
</div>
</div>
</header>
\ No newline at end of file
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
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 { AuthState } from '../../../../../shared/data-access/interface/auth.interface';
@Component({
selector: 'meu-header',
standalone: true,
......@@ -8,5 +13,33 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HeaderComponent implements OnInit {
private authService = inject(AuthService);
private router = inject(Router);
private cdr = inject(ChangeDetectorRef);
authState$: Observable<AuthState> = this.authService.authState$;
isLoggingOut = false;
ngOnInit(): void {}
onLogout(): void {
if (this.isLoggingOut) return;
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();
}
});
}
}
export interface LoginRequest {
username: string;
password: string;
}
export interface LoginResponse {
message: string;
responseData: {
token: string;
expirationTime: number;
role: UserRole;
};
success: boolean;
status: number;
violations: any;
path: string;
timestamp: number;
}
export interface UserInfo {
id?: number;
username: string;
name?: string;
role: UserRole;
}
export type UserRole = 'admin' | 'customer';
export interface AuthState {
isAuthenticated: boolean;
user: UserInfo | null;
token: string | null;
}
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 { LoginRequest, LoginResponse, AuthState, UserInfo } from '../interface/auth.interface';
@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';
// Reactive state management
private authStateSubject = new BehaviorSubject<AuthState>({
isAuthenticated: false,
user: null,
token: null
});
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))
);
}
/**
* 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
);
}
/**
* 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')),
catchError(error => {
this.logout();
return throwError(error);
})
);
}
/**
* Get current user info
*/
getCurrentUser(): Observable<UserInfo> {
const headers = this.getAuthHeaders();
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;
// 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);
}
}
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));
})
);
}
}
}
......@@ -26,8 +26,102 @@ module.exports = {
},
},
colors: {
white: "#ffffff",
black: "#000000",
inherit: 'inherit',
current: 'currentColor',
transparent: 'transparent',
black: '#000',
white: '#fff',
slate: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
950: '#020617',
},
gray: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827',
950: '#030712',
},
red: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
950: '#450a0a',
},
blue: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
950: '#172554',
},
indigo: {
50: '#eef2ff',
100: '#e0e7ff',
200: '#c7d2fe',
300: '#a5b4fc',
400: '#818cf8',
500: '#6366f1',
600: '#4f46e5',
700: '#4338ca',
800: '#3730a3',
900: '#312e81',
950: '#1e1b4b',
},
purple: {
50: '#faf5ff',
100: '#f3e8ff',
200: '#e9d5ff',
300: '#d8b4fe',
400: '#c084fc',
500: '#a855f7',
600: '#9333ea',
700: '#7c3aed',
800: '#6b21a8',
900: '#581c87',
950: '#3b0764',
},
green: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
950: '#052e16',
},
},
fontFamily: {
"roboto-black": ["Roboto-Black", "sans-serif"],
......
This diff is collapsed.
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