From 1fdf7516a5c8989697857cc25414cb1d5da88b3e Mon Sep 17 00:00:00 2001 From: Alexis Bruneteau Date: Wed, 4 Jun 2025 03:32:57 +0200 Subject: [PATCH] tag --- src/app/app.routes.ts | 29 +++--- src/app/auth-interceptor.spec.ts | 17 ++++ src/app/auth-interceptor.ts | 21 ++++ .../dashboard/dashboard.component.css | 0 .../dashboard/dashboard.component.html | 96 +++++++++++++++++++ .../dashboard/dashboard.component.spec.ts | 23 +++++ .../dashboard/dashboard.component.ts | 58 +++++++++++ src/app/components/login/login.component.ts | 35 ++++--- .../components/register/register.component.ts | 1 + src/app/models/portfolio.model.ts | 8 ++ src/app/services/api.ts | 18 ++-- src/app/services/portfolio.service.spec.ts | 16 ++++ src/app/services/portfolio.service.ts | 30 ++++++ src/environments/environment.prod.ts | 4 + src/environments/environment.ts | 4 +- src/main.ts | 21 ++-- 16 files changed, 335 insertions(+), 46 deletions(-) create mode 100644 src/app/auth-interceptor.spec.ts create mode 100644 src/app/auth-interceptor.ts create mode 100644 src/app/components/dashboard/dashboard.component.css create mode 100644 src/app/components/dashboard/dashboard.component.html create mode 100644 src/app/components/dashboard/dashboard.component.spec.ts create mode 100644 src/app/components/dashboard/dashboard.component.ts create mode 100644 src/app/models/portfolio.model.ts create mode 100644 src/app/services/portfolio.service.spec.ts create mode 100644 src/app/services/portfolio.service.ts create mode 100644 src/environments/environment.prod.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index c2ea077..3d1819d 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,24 +1,23 @@ -import { Routes } from '@angular/router'; +import {Routes} from '@angular/router'; import {LoginComponent} from './components/login/login.component'; import {RegisterComponent} from './components/register/register.component'; import {LandingComponent} from './components/landing/landing.component'; +import {DashboardComponent} from './components/dashboard/dashboard.component'; + export const routes: Routes = [ - { - path: 'login', - component: LoginComponent, - title: 'Login - Your App Name' - }, - - { - path: 'register', - component: RegisterComponent, - title: 'Register - Your App Name' + { + path: 'login', component: LoginComponent, title: 'Login - Your App Name' }, { - path: '', - component: LandingComponent, - title: 'Home' - } + path: 'register', component: RegisterComponent, title: 'Register - Your App Name' + }, + + { + path: '', component: LandingComponent, title: 'Home' + }, + + {path: 'dashboard', component: DashboardComponent} + ]; diff --git a/src/app/auth-interceptor.spec.ts b/src/app/auth-interceptor.spec.ts new file mode 100644 index 0000000..de59f96 --- /dev/null +++ b/src/app/auth-interceptor.spec.ts @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpInterceptorFn } from '@angular/common/http'; + +import { authInterceptor } from './auth-interceptor'; + +describe('authInterceptor', () => { + const interceptor: HttpInterceptorFn = (req, next) => + TestBed.runInInjectionContext(() => authInterceptor(req, next)); + + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + + it('should be created', () => { + expect(interceptor).toBeTruthy(); + }); +}); diff --git a/src/app/auth-interceptor.ts b/src/app/auth-interceptor.ts new file mode 100644 index 0000000..4cd532d --- /dev/null +++ b/src/app/auth-interceptor.ts @@ -0,0 +1,21 @@ +import {HttpEvent, HttpHandlerFn, HttpRequest} from '@angular/common/http'; +import {Observable} from 'rxjs'; + + +export function authInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { + + const token = localStorage.getItem('auth_token'); + console.log('setting token ' + token); + if (token) { + const authReq = req.clone({ + setHeaders: { + Authorization: `Bearer ${token}` + } + }); + return next(authReq); + } + + return next(req); +} + + diff --git a/src/app/components/dashboard/dashboard.component.css b/src/app/components/dashboard/dashboard.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/dashboard/dashboard.component.html b/src/app/components/dashboard/dashboard.component.html new file mode 100644 index 0000000..d7c1704 --- /dev/null +++ b/src/app/components/dashboard/dashboard.component.html @@ -0,0 +1,96 @@ +
+ + +
+

📁 Your Portfolios

+
+ + +
+ @for (p of portfolios; track p.id) { +
+ +
+

{{ p.name }}

+ + {{ p.path ? 'Uploaded' : 'Pending Upload' }} + +
+ + {{ p.domain }}.portfolio-host.com + +
+ + + + + + @if (p.path) { + + } +
+ + + @if (uploading[p.id]) { +

Uploading...

+ } +
+ } +
+ + +
+ + +
+

➕ Create New Portfolio

+ +
+
+ + +
+ +
+ + +
+ + +
+
+
diff --git a/src/app/components/dashboard/dashboard.component.spec.ts b/src/app/components/dashboard/dashboard.component.spec.ts new file mode 100644 index 0000000..30e39a2 --- /dev/null +++ b/src/app/components/dashboard/dashboard.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DashboardComponent } from './dashboard.component'; + +describe('DashboardComponent', () => { + let component: DashboardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DashboardComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/dashboard/dashboard.component.ts b/src/app/components/dashboard/dashboard.component.ts new file mode 100644 index 0000000..f314b21 --- /dev/null +++ b/src/app/components/dashboard/dashboard.component.ts @@ -0,0 +1,58 @@ +import {Component, OnInit} from '@angular/core'; +import {PortfolioService} from '../../services/portfolio.service'; +import {Portfolio} from '../../models/portfolio.model'; +import {FormsModule} from '@angular/forms'; +import {NgClass} from '@angular/common'; + +@Component({ + selector: 'app-dashboard', + imports: [ + FormsModule, + NgClass + ], + templateUrl: './dashboard.component.html' +}) +export class DashboardComponent implements OnInit { + portfolios: Portfolio[] = []; + newPortfolio = { name: '', domain: '' }; + uploading: { [key: number]: boolean } = {}; + + constructor(private portfolioService: PortfolioService) {} + + ngOnInit(): void { + this.loadPortfolios(); + } + + loadPortfolios() { + this.portfolioService.getPortfolios().subscribe({ + next: (res) => this.portfolios = res.data || [] + }); + } + + create() { + this.portfolioService.createPortfolio(this.newPortfolio).subscribe({ + next: () => { + this.newPortfolio = { name: '', domain: '' }; + this.loadPortfolios(); + } + }); + } + + uploadZip(event: Event, portfolioId: number) { + const file = (event.target as HTMLInputElement).files?.[0]; + if (!file) return; + + this.uploading[portfolioId] = true; + this.portfolioService.uploadZip(portfolioId, file).subscribe({ + next: () => { + this.uploading[portfolioId] = false; + this.loadPortfolios(); + }, + error: () => this.uploading[portfolioId] = false + }); + } + + deploy( portfolioId: number){ + this.portfolioService.deploy(portfolioId).subscribe({}); + } +} diff --git a/src/app/components/login/login.component.ts b/src/app/components/login/login.component.ts index ddb7205..d87a526 100644 --- a/src/app/components/login/login.component.ts +++ b/src/app/components/login/login.component.ts @@ -1,6 +1,6 @@ -import {Component, OnInit, OnDestroy} from '@angular/core'; +import {Component, OnDestroy, OnInit} from '@angular/core'; import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; -import {Router, ActivatedRoute} from '@angular/router'; +import {ActivatedRoute, Router} from '@angular/router'; import {Subject} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; import {ApiService, LoginCredentials} from '../../services/api'; @@ -43,6 +43,11 @@ export class LoginComponent implements OnInit, OnDestroy { }); } + // Getter for easy access to form fields + get f() { + return this.loginForm.controls; + } + ngOnInit(): void { // Get return url from route parameters or default to dashboard this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard'; @@ -56,11 +61,6 @@ export class LoginComponent implements OnInit, OnDestroy { this.destroy$.complete(); } - // Getter for easy access to form fields - get f() { - return this.loginForm.controls; - } - // Get specific field error message getFieldError(fieldName: string): string { const field = this.loginForm.get(fieldName); @@ -84,11 +84,6 @@ export class LoginComponent implements OnInit, OnDestroy { return !!(field?.errors && field.touched); } - // Capitalize first letter - private capitalizeFirst(str: string): string { - return str.charAt(0).toUpperCase() + str.slice(1); - } - // Toggle password visibility togglePasswordVisibility(): void { this.showPassword = !this.showPassword; @@ -126,6 +121,17 @@ export class LoginComponent implements OnInit, OnDestroy { if (response.success) { this.success = 'Login successful! Redirecting...'; + console.log(response.data?.token); + localStorage.setItem('auth_token_', 'TEST_TOKEN'); + console.log(localStorage.getItem('auth_token_')); // Should be 'TEST_TOKEN' + localStorage.setItem('auth_token', response.data?.token || ''); + + + console.log('Saving token:', response.data?.token); + console.log('Current origin:', window.location.origin); + localStorage.setItem('auth_token', response.data?.token || ''); + console.log('Token in localStorage:', localStorage.getItem('auth_token')); + // Store remember me preference if needed if (this.f['rememberMe'].value) { @@ -187,4 +193,9 @@ export class LoginComponent implements OnInit, OnDestroy { password: 'password123' }); } + + // Capitalize first letter + private capitalizeFirst(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); + } } diff --git a/src/app/components/register/register.component.ts b/src/app/components/register/register.component.ts index ad2ebaa..d98c25b 100644 --- a/src/app/components/register/register.component.ts +++ b/src/app/components/register/register.component.ts @@ -137,6 +137,7 @@ export class RegisterComponent implements OnInit, OnDestroy { this.router.navigate([this.returnUrl]); }, 1000); } else { + console.log(response); this.error = response.message || 'Registration failed. Please try again.'; } }, diff --git a/src/app/models/portfolio.model.ts b/src/app/models/portfolio.model.ts new file mode 100644 index 0000000..c022cc4 --- /dev/null +++ b/src/app/models/portfolio.model.ts @@ -0,0 +1,8 @@ +export interface Portfolio { + id: number; + name: string; + domain: string; + path?: string; + created_at: string; + updated_at: string; +} diff --git a/src/app/services/api.ts b/src/app/services/api.ts index 20f011b..abd90b9 100644 --- a/src/app/services/api.ts +++ b/src/app/services/api.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; import { Observable, throwError, BehaviorSubject } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; -import { environment as env } from '../../environments/environment' ; +import { environmentProd as env } from '../../environments/environment' ; export interface LoginCredentials { email: string; @@ -33,11 +33,11 @@ export interface AuthResponse { providedIn: 'root' }) export class ApiService { - private baseUrl = env.apiUrl || 'http://localhost:8000/api'; + protected baseUrl = env.apiUrl || 'http://localhost:8000/api'; private tokenSubject = new BehaviorSubject(this.getStoredToken()); public token$ = this.tokenSubject.asObservable(); - constructor(private http: HttpClient) {} + constructor(protected http: HttpClient) {} // Get stored token from localStorage private getStoredToken(): string | null { @@ -58,6 +58,7 @@ export class ApiService { // Remove token from localStorage and update subject private removeToken(): void { if (typeof window !== 'undefined') { + console.log('?? ?? ??') localStorage.removeItem('auth_token'); } this.tokenSubject.next(null); @@ -76,8 +77,8 @@ export class ApiService { // Get HTTP headers with auth token private getHeaders(): HttpHeaders { let headers = new HttpHeaders({ - 'Content-Type': 'application/json', - 'Accept': 'application/json' + //'Content-Type': 'application/json', + //'Accept': 'application/json' }); const token = this.currentToken; @@ -98,7 +99,7 @@ export class ApiService { } else { // Server-side error if (error.status === 401) { - this.removeToken(); + //this.removeToken(); errorMessage = 'Unauthorized. Please login again.'; } else if (error.status === 422) { errorMessage = 'Validation error'; @@ -114,7 +115,7 @@ export class ApiService { } // Generic HTTP methods - private get(endpoint: string): Observable { + protected get(endpoint: string): Observable { return this.http.get(`${this.baseUrl}${endpoint}`, { headers: this.getHeaders() }).pipe( @@ -122,7 +123,7 @@ export class ApiService { ); } - private post(endpoint: string, data: any): Observable { + protected post(endpoint: string, data: any): Observable { return this.http.post(`${this.baseUrl}${endpoint}`, data, { headers: this.getHeaders() }).pipe( @@ -150,6 +151,7 @@ export class ApiService { register(userData: RegisterData): Observable> { return this.post>('/auth/register', userData).pipe( tap(response => { + console.log(response) if (response.success && response.data?.token) { this.setToken(response.data.token); } diff --git a/src/app/services/portfolio.service.spec.ts b/src/app/services/portfolio.service.spec.ts new file mode 100644 index 0000000..b112596 --- /dev/null +++ b/src/app/services/portfolio.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { PortfolioService } from './portfolio.service'; + +describe('PortfolioService', () => { + let service: PortfolioService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(PortfolioService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/portfolio.service.ts b/src/app/services/portfolio.service.ts new file mode 100644 index 0000000..6fa2fd5 --- /dev/null +++ b/src/app/services/portfolio.service.ts @@ -0,0 +1,30 @@ +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; +import {ApiResponse, ApiService} from './api'; +import {HttpClient} from '@angular/common/http'; +import {Portfolio} from '../models/portfolio.model'; +import { environmentProd as env } from '../../environments/environment' ; + +@Injectable({ providedIn: 'root' }) +export class PortfolioService extends ApiService{ + + + getPortfolios(): Observable> { + return this.get>(`/portfolios`); + } + + createPortfolio(data: { name: string; domain: string }): Observable> { + return this.post>(`/portfolios`, data); + } + + uploadZip(portfolioId: number, file: File): Observable { + const formData = new FormData(); + formData.append('file', file); + return this.http.post(this.baseUrl + `/portfolios/${portfolioId}/upload`, formData); + } + + deploy(portfolioId: number): Observable { + console.log('Deploying portfolio with ID:', portfolioId); + return this.http.post(this.baseUrl + `/portfolios/${portfolioId}/deploy`, {}); + } +} diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts new file mode 100644 index 0000000..506b45b --- /dev/null +++ b/src/environments/environment.prod.ts @@ -0,0 +1,4 @@ +export const environmentProd = { + production: true, + apiUrl: 'https://api.portfolio-host.com/api' +}; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 324c437..5800dc1 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,4 +1,4 @@ -export const environment = { +export const environmentProd = { production: true, - apiUrl: 'https://api.portfolio-host.com/api' + apiUrl: 'http://localhost:8000/api' }; diff --git a/src/main.ts b/src/main.ts index 3846c7d..874de77 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,16 +1,19 @@ -import { bootstrapApplication } from '@angular/platform-browser'; -import { provideRouter } from '@angular/router'; -import { provideHttpClient } from '@angular/common/http'; -import { importProvidersFrom } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideRouter} from '@angular/router'; +import {provideHttpClient, withInterceptors} from '@angular/common/http'; +import {importProvidersFrom} from '@angular/core'; +import {ReactiveFormsModule} from '@angular/forms'; -import { App } from './app/app'; -import { routes } from './app/app.routes'; +import {App} from './app/app'; +import {routes} from './app/app.routes'; +import {authInterceptor} from './app/auth-interceptor'; bootstrapApplication(App, { providers: [ provideRouter(routes), - provideHttpClient(), - importProvidersFrom(ReactiveFormsModule) + provideHttpClient(withInterceptors([ + authInterceptor + ])), + importProvidersFrom(ReactiveFormsModule), ] }).catch(err => console.error(err));