diff --git a/src/app/app.config.ts b/src/app/app.config.ts index ce7a77b..979665a 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,7 +1,11 @@ -import { ApplicationConfig, provideBrowserGlobalErrorListeners, APP_INITIALIZER } from '@angular/core'; +import { + ApplicationConfig, + provideBrowserGlobalErrorListeners, + APP_INITIALIZER, +} from '@angular/core'; import { provideRouter } from '@angular/router'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; -import { catchError, of } from 'rxjs'; +import { catchError, of, throwError } from 'rxjs'; import { routes } from './app.routes'; import { httpInterceptor } from './interceptors/http.interceptor'; @@ -11,28 +15,30 @@ import { AuthService } from './services/auth.service'; * Initialize auth state on app startup by checking for existing session */ function initializeAuth(authService: AuthService) { - return () => authService.checkSession().pipe( - catchError((error) => { - // Session check failed - user is not logged in or session expired - console.log('No active session or session expired'); - authService.clearAuthState(); - return of(null); - }) - ); + return () => + authService.checkSession().pipe( + catchError((error) => { + console.error('Session check failed:', error); + if (error?.status === 401 || error?.status === 422) { + authService.clearAuthState(); + return of(null); + } + authService.clearAuthState(); + return throwError(() => error); + }), + ); } export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), provideRouter(routes), - provideHttpClient( - withInterceptors([httpInterceptor]) - ), + provideHttpClient(withInterceptors([httpInterceptor])), { provide: APP_INITIALIZER, useFactory: initializeAuth, deps: [AuthService], - multi: true - } - ] + multi: true, + }, + ], }; diff --git a/src/app/app.html b/src/app/app.html index c9f68e3..363c8a6 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1,8 +1,5 @@ -
- - +
+ +
- - - diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 5efd1c7..30d1fbd 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -4,15 +4,28 @@ import { LoginComponent } from './pages/login/login.component'; export const routes: Routes = [ // Public routes - { - path: 'login', - component: LoginComponent + { + path: 'login', + component: LoginComponent }, // Protected routes - require authentication - { - path: '', + { + path: '', canActivate: [authGuard], - loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent) + children: [ + { + path: '', + loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent) + }, + { + path: 'projects/new', + loadComponent: () => import('./pages/project-create/project-create.component').then(m => m.ProjectCreateComponent) + }, + { + path: 'projects/:id', + loadComponent: () => import('./pages/project-details/project-details.component').then(m => m.ProjectDetailsComponent) + } + ] } ]; diff --git a/src/app/app.ts b/src/app/app.ts index 4b39967..b974fe7 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -2,11 +2,12 @@ import { Component, signal, inject } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { AuthService } from './services/auth.service'; import { CommonModule } from '@angular/common'; -import { Navbar } from './navbar/navbar'; +import { NavbarComponent } from './components/navbar/navbar.component'; +import { FooterComponent } from './components/footer/footer.component'; @Component({ selector: 'app-root', - imports: [RouterOutlet, CommonModule, Navbar], + imports: [RouterOutlet, CommonModule, NavbarComponent, FooterComponent], templateUrl: './app.html', styleUrl: './app.css' }) diff --git a/src/app/interceptors/http.interceptor.ts b/src/app/interceptors/http.interceptor.ts index 0925840..1fae508 100644 --- a/src/app/interceptors/http.interceptor.ts +++ b/src/app/interceptors/http.interceptor.ts @@ -15,33 +15,24 @@ export const httpInterceptor: HttpInterceptorFn = (req, next) => { // Clone the request to add withCredentials flag // This ensures cookies are sent with every request const reqWithCredentials = req.clone({ - withCredentials: true + withCredentials: true, }); // Pass the cloned request to the next handler return next(reqWithCredentials).pipe( catchError((error: HttpErrorResponse) => { - // Handle different HTTP error codes if (error.status === 401) { - // Unauthorized - redirect to login (but not for session check endpoint) - // Skip redirect for /me endpoint to avoid issues during app initialization - if (!req.url.endsWith('/me')) { - console.error('Unauthorized access - redirecting to login'); + if (router.url !== '/login') { router.navigate(['/login']); } } else if (error.status === 403) { - // Forbidden console.error('Access forbidden:', error.message); - } else if (error.status === 0) { - // Network error - console.error('Network error - check if the server is running'); } else { - // Other errors console.error(`HTTP Error ${error.status}:`, error.message); } // Re-throw the error so components can handle it if needed return throwError(() => error); - }) + }), ); }; diff --git a/src/app/models/projects.models.ts b/src/app/models/projects.models.ts index 2111465..9c80e6d 100644 --- a/src/app/models/projects.models.ts +++ b/src/app/models/projects.models.ts @@ -17,7 +17,7 @@ export interface ProjectFull { export interface CreateProjectRequest { name: string; - description?: string; + description: string; } export interface UpdateProjectRequest { diff --git a/src/app/pages/project-create/project-create.component.html b/src/app/pages/project-create/project-create.component.html index 7709931..016ed05 100644 --- a/src/app/pages/project-create/project-create.component.html +++ b/src/app/pages/project-create/project-create.component.html @@ -26,7 +26,8 @@ name="description" [(ngModel)]="description" rows="4" - placeholder="Optional description" + placeholder="Enter a project description" + required > diff --git a/src/app/pages/project-create/project-create.component.ts b/src/app/pages/project-create/project-create.component.ts index fbb7af7..2e1392f 100644 --- a/src/app/pages/project-create/project-create.component.ts +++ b/src/app/pages/project-create/project-create.component.ts @@ -10,7 +10,7 @@ import { CreateProjectRequest, Project } from '../../models/projects.models'; standalone: true, imports: [CommonModule, FormsModule], templateUrl: './project-create.component.html', - styleUrl: './project-create.component.css' + styleUrl: './project-create.component.css', }) export class ProjectCreateComponent { private apiService = inject(ApiService); @@ -27,9 +27,14 @@ export class ProjectCreateComponent { return; } + if (!this.description.trim()) { + this.errorMessage.set('Project description is required.'); + return; + } + const payload: CreateProjectRequest = { name: this.name.trim(), - description: this.description.trim() || undefined + description: this.description.trim(), }; this.isSaving.set(true); @@ -39,7 +44,7 @@ export class ProjectCreateComponent { next: (project) => { this.isSaving.set(false); if (project?.id != null) { - this.router.navigate(['/projects', project.id]); + this.router.navigate(['/projects/', project.id]); } else { this.router.navigate(['/']); } @@ -47,9 +52,9 @@ export class ProjectCreateComponent { error: (error) => { this.isSaving.set(false); this.errorMessage.set( - error?.error?.message || 'Failed to create project. Please try again.' + error?.error?.message || 'Failed to create project. Please try again.', ); - } + }, }); } diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index 18f3d00..765bc2c 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -1,6 +1,6 @@ import { Injectable, inject, signal, computed } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { Observable, tap } from 'rxjs'; +import { Observable, tap, catchError, throwError } from 'rxjs'; import { Router } from '@angular/router'; import { LoginRequest, LoginResponse, User, AuthState } from '../models/auth.models'; import { environment } from '../config/environment'; @@ -36,11 +36,11 @@ export class AuthService { credentials ).pipe( tap(response => { - if (response.success && response.user) { - // Update auth state + if (response.success || response.user) { + // Update auth state even if the backend only sets a cookie this.authState.set({ isAuthenticated: true, - user: response.user + user: response.user ?? null }); } }) @@ -52,7 +52,7 @@ export class AuthService { * Clears the session cookie on the backend */ logout(): Observable { - return this.http.post(`${environment.apiBaseUrl}/me/logout`, {}).pipe( + return this.http.get(`${environment.apiBaseUrl}/me/logout`).pipe( tap(() => { // Clear auth state this.authState.set({ @@ -61,6 +61,11 @@ export class AuthService { }); // Redirect to login this.router.navigate(['/login']); + }), + catchError((error) => { + // Even if logout fails on backend, clear local state + this.clearAuthState(); + return throwError(() => error); }) ); } @@ -88,5 +93,6 @@ export class AuthService { isAuthenticated: false, user: null }); + this.router.navigate(['/login']); } }