From 66e858e3864a5567bb32550d87668b1d40c5b2eb Mon Sep 17 00:00:00 2001 From: kushal-saha Date: Tue, 28 Apr 2026 05:25:06 +0000 Subject: [PATCH] feature: Fix Foritify error, implement logput on frontend. persist authentication on accross app --- .idea/compiler.xml | 7 ++ .idea/jsLinters/eslint.xml | 6 ++ .idea/laravel-idea.xml | 1 + .idea/prettier.xml | 1 + backend/app/Http/Responses/LoginResponse.php | 25 ++++++ backend/app/Http/Responses/LogoutResponse.php | 24 +++++ .../app/Http/Responses/RegisterResponse.php | 24 +++++ .../app/Providers/FortifyServiceProvider.php | 10 ++- backend/bootstrap/app.php | 1 + backend/config/fortify.php | 2 +- frontend/angular.json | 12 ++- frontend/src/app/app.config.ts | 5 +- frontend/src/app/app.html | 1 + frontend/src/app/app.ts | 3 +- frontend/src/app/auth/auth-service.spec.ts | 16 ++++ frontend/src/app/auth/auth-service.ts | 41 +++++++++ frontend/src/app/auth/auth.routes.ts | 12 +++ frontend/src/app/auth/auth.store.ts | 88 +++++++++++++++++++ frontend/src/app/auth/auth.types.ts | 30 +++++++ frontend/src/app/auth/login/login.css | 16 ++++ frontend/src/app/auth/login/login.html | 53 +++++++++++ frontend/src/app/auth/login/login.spec.ts | 22 +++++ frontend/src/app/auth/login/login.ts | 33 +++++++ frontend/src/app/auth/register/register.css | 17 ++++ frontend/src/app/auth/register/register.html | 75 ++++++++++++++++ .../src/app/auth/register/register.spec.ts | 22 +++++ frontend/src/app/auth/register/register.ts | 34 +++++++ frontend/src/app/chat/chat.css | 13 --- frontend/src/app/chat/chat.html | 2 +- .../interceptors/sanctum-interceptor.spec.ts | 17 ++++ .../core/interceptors/sanctum-interceptor.ts | 20 +++++ .../src/app/core/layout/header/header.css | 4 + .../src/app/core/layout/header/header.html | 41 +++++++++ .../src/app/core/layout/header/header.spec.ts | 22 +++++ frontend/src/app/core/layout/header/header.ts | 14 +++ frontend/src/app/core/tokens/api-urls.ts | 12 +++ .../validation-errors/validation-errors.css | 0 .../validation-errors/validation-errors.html | 21 +++++ .../validation-errors.spec.ts | 22 +++++ .../validation-errors/validation-errors.ts | 18 ++++ .../environments/environment.development.ts | 5 ++ frontend/src/environments/environment.ts | 5 ++ frontend/src/index.html | 2 +- frontend/src/styles.css | 14 ++- 44 files changed, 788 insertions(+), 25 deletions(-) create mode 100644 .idea/compiler.xml create mode 100644 .idea/jsLinters/eslint.xml create mode 100644 backend/app/Http/Responses/LoginResponse.php create mode 100644 backend/app/Http/Responses/LogoutResponse.php create mode 100644 backend/app/Http/Responses/RegisterResponse.php create mode 100644 frontend/src/app/auth/auth-service.spec.ts create mode 100644 frontend/src/app/auth/auth-service.ts create mode 100644 frontend/src/app/auth/auth.routes.ts create mode 100644 frontend/src/app/auth/auth.store.ts create mode 100644 frontend/src/app/auth/auth.types.ts create mode 100644 frontend/src/app/auth/login/login.css create mode 100644 frontend/src/app/auth/login/login.html create mode 100644 frontend/src/app/auth/login/login.spec.ts create mode 100644 frontend/src/app/auth/login/login.ts create mode 100644 frontend/src/app/auth/register/register.css create mode 100644 frontend/src/app/auth/register/register.html create mode 100644 frontend/src/app/auth/register/register.spec.ts create mode 100644 frontend/src/app/auth/register/register.ts create mode 100644 frontend/src/app/core/interceptors/sanctum-interceptor.spec.ts create mode 100644 frontend/src/app/core/interceptors/sanctum-interceptor.ts create mode 100644 frontend/src/app/core/layout/header/header.css create mode 100644 frontend/src/app/core/layout/header/header.html create mode 100644 frontend/src/app/core/layout/header/header.spec.ts create mode 100644 frontend/src/app/core/layout/header/header.ts create mode 100644 frontend/src/app/core/tokens/api-urls.ts create mode 100644 frontend/src/app/shared/validation-errors/validation-errors.css create mode 100644 frontend/src/app/shared/validation-errors/validation-errors.html create mode 100644 frontend/src/app/shared/validation-errors/validation-errors.spec.ts create mode 100644 frontend/src/app/shared/validation-errors/validation-errors.ts create mode 100644 frontend/src/environments/environment.development.ts create mode 100644 frontend/src/environments/environment.ts diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..4d169be --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/jsLinters/eslint.xml b/.idea/jsLinters/eslint.xml new file mode 100644 index 0000000..541945b --- /dev/null +++ b/.idea/jsLinters/eslint.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/laravel-idea.xml b/.idea/laravel-idea.xml index 72ebbed..5a2c748 100644 --- a/.idea/laravel-idea.xml +++ b/.idea/laravel-idea.xml @@ -11,6 +11,7 @@ diff --git a/.idea/prettier.xml b/.idea/prettier.xml index b0c1c68..0c83ac4 100644 --- a/.idea/prettier.xml +++ b/.idea/prettier.xml @@ -2,5 +2,6 @@ \ No newline at end of file diff --git a/backend/app/Http/Responses/LoginResponse.php b/backend/app/Http/Responses/LoginResponse.php new file mode 100644 index 0000000..5f0c466 --- /dev/null +++ b/backend/app/Http/Responses/LoginResponse.php @@ -0,0 +1,25 @@ +json([ + 'message' => 'Logged in successfully.', + 'two_factor' => false, + ]); + } +} diff --git a/backend/app/Http/Responses/LogoutResponse.php b/backend/app/Http/Responses/LogoutResponse.php new file mode 100644 index 0000000..dad6a45 --- /dev/null +++ b/backend/app/Http/Responses/LogoutResponse.php @@ -0,0 +1,24 @@ +json([ + 'message' => 'Logged out successfully.', + ]); + } +} diff --git a/backend/app/Http/Responses/RegisterResponse.php b/backend/app/Http/Responses/RegisterResponse.php new file mode 100644 index 0000000..a376dfc --- /dev/null +++ b/backend/app/Http/Responses/RegisterResponse.php @@ -0,0 +1,24 @@ +json([ + 'message' => 'Registered successfully.', + ], 201); + } +} diff --git a/backend/app/Providers/FortifyServiceProvider.php b/backend/app/Providers/FortifyServiceProvider.php index 004ced4..e7e320a 100644 --- a/backend/app/Providers/FortifyServiceProvider.php +++ b/backend/app/Providers/FortifyServiceProvider.php @@ -6,12 +6,18 @@ use App\Actions\Fortify\ResetUserPassword; use App\Actions\Fortify\UpdateUserPassword; use App\Actions\Fortify\UpdateUserProfileInformation; +use App\Http\Responses\LoginResponse; +use App\Http\Responses\LogoutResponse; +use App\Http\Responses\RegisterResponse; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Http\Request; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Str; use Laravel\Fortify\Actions\RedirectIfTwoFactorAuthenticatable; +use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract; +use Laravel\Fortify\Contracts\LogoutResponse as LogoutResponseContract; +use Laravel\Fortify\Contracts\RegisterResponse as RegisterResponseContract; use Laravel\Fortify\Fortify; class FortifyServiceProvider extends ServiceProvider @@ -21,7 +27,9 @@ class FortifyServiceProvider extends ServiceProvider */ public function register(): void { - // + $this->app->singleton(LoginResponseContract::class, LoginResponse::class); + $this->app->singleton(RegisterResponseContract::class, RegisterResponse::class); + $this->app->singleton(LogoutResponseContract::class, LogoutResponse::class); } /** diff --git a/backend/bootstrap/app.php b/backend/bootstrap/app.php index ea0d646..a00ccef 100644 --- a/backend/bootstrap/app.php +++ b/backend/bootstrap/app.php @@ -3,6 +3,7 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; +use Illuminate\Http\Request; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( diff --git a/backend/config/fortify.php b/backend/config/fortify.php index dfc5deb..95e1c15 100644 --- a/backend/config/fortify.php +++ b/backend/config/fortify.php @@ -73,7 +73,7 @@ | */ - 'home' => '/home', + 'home' => env('FRONTEND_URL', 'http://localhost:4200/').'/', /* |-------------------------------------------------------------------------- diff --git a/frontend/angular.json b/frontend/angular.json index 8816d60..1dffa47 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -24,9 +24,7 @@ "input": "public" } ], - "styles": [ - "src/styles.css" - ] + "styles": ["src/styles.css"] }, "configurations": { "production": { @@ -47,7 +45,13 @@ "development": { "optimization": false, "extractLicenses": false, - "sourceMap": true + "sourceMap": true, + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.development.ts" + } + ] } }, "defaultConfiguration": "production" diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 4451829..2fc0566 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,13 +1,14 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; import { provideRouter } from '@angular/router'; -import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClient, withInterceptors} from '@angular/common/http'; import { routes } from './app.routes'; +import {sanctumInterceptor} from './core/interceptors/sanctum-interceptor'; export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), provideRouter(routes), - provideHttpClient() + provideHttpClient(withInterceptors([sanctumInterceptor])), ] }; diff --git a/frontend/src/app/app.html b/frontend/src/app/app.html index 67e7bd4..aba7bee 100644 --- a/frontend/src/app/app.html +++ b/frontend/src/app/app.html @@ -1 +1,2 @@ + diff --git a/frontend/src/app/app.ts b/frontend/src/app/app.ts index ade0fcb..04f6d3b 100644 --- a/frontend/src/app/app.ts +++ b/frontend/src/app/app.ts @@ -1,9 +1,10 @@ import { Component, signal } from '@angular/core'; import { RouterOutlet } from '@angular/router'; +import {Header} from './core/layout/header/header'; @Component({ selector: 'app-root', - imports: [RouterOutlet], + imports: [RouterOutlet, Header], templateUrl: './app.html', styleUrl: './app.css' }) diff --git a/frontend/src/app/auth/auth-service.spec.ts b/frontend/src/app/auth/auth-service.spec.ts new file mode 100644 index 0000000..ef933a9 --- /dev/null +++ b/frontend/src/app/auth/auth-service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthService } from './auth-service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/auth/auth-service.ts b/frontend/src/app/auth/auth-service.ts new file mode 100644 index 0000000..92bdf53 --- /dev/null +++ b/frontend/src/app/auth/auth-service.ts @@ -0,0 +1,41 @@ +import {inject, Injectable } from '@angular/core'; +import {HttpClient, HttpHeaders} from '@angular/common/http'; +import {environment} from '../../environments/environment'; +import {LoginRequest, RegisterRequest, User} from './auth.types'; +import {Observable, switchMap} from 'rxjs'; +import {API_URL, BASE_URL} from '../core/tokens/api-urls'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthService { + private http: HttpClient = inject(HttpClient); + private baseUrl = inject(BASE_URL); + private apiUrl = inject(API_URL); + + public login (credentials: LoginRequest){ + return this.getSanctumCookie().pipe( + switchMap(() => this.http.post(`${this.baseUrl}/login`, credentials)) + ) + } + + public register (credentials: RegisterRequest ){ + return this.getSanctumCookie().pipe( + switchMap(() => this.http.post(`${this.baseUrl}/register`, credentials)) + ); + } + + public logout(){ + return this.http.post(`${this.baseUrl}/logout`, {}); + } + + public getCurrentUser():Observable + { + return this.http.get(`${this.apiUrl}/me`); + } + + private getSanctumCookie(): Observable + { + return this.http.get(`${this.baseUrl}/sanctum/csrf-cookie`); + } +} diff --git a/frontend/src/app/auth/auth.routes.ts b/frontend/src/app/auth/auth.routes.ts new file mode 100644 index 0000000..a5397c7 --- /dev/null +++ b/frontend/src/app/auth/auth.routes.ts @@ -0,0 +1,12 @@ +import {Routes} from '@angular/router'; + +export const authRoutes : Routes = [ + { + path: 'login', + loadComponent: () => import('./login/login').then(m => m.Login), + }, + { + path: 'register', + loadComponent: () => import('./register/register').then(m => m.Register), + }, +] diff --git a/frontend/src/app/auth/auth.store.ts b/frontend/src/app/auth/auth.store.ts new file mode 100644 index 0000000..32af13d --- /dev/null +++ b/frontend/src/app/auth/auth.store.ts @@ -0,0 +1,88 @@ +import {AuthState, LoginRequest, RegisterRequest, User} from './auth.types'; +import {patchState, signalStore, withHooks, withMethods, withState} from '@ngrx/signals'; +import {inject} from '@angular/core'; +import {AuthService} from './auth-service'; +import {rxMethod} from '@ngrx/signals/rxjs-interop'; +import {catchError, Observable, of, pipe, switchMap, tap} from 'rxjs'; +import {HttpErrorResponse} from '@angular/common/http'; +import {Router} from '@angular/router'; + +const initialState : AuthState = { + user: null, + isLoading: true, + error: null, + validationErrors: null +}; + +export const AuthStore = signalStore( + { providedIn: 'root' }, + withState(initialState), + withMethods((store , authService = inject(AuthService), router = inject(Router))=> { + const handleAuthFlow = (apiCall: (requestData: T) => Observable, fallbackError: string ) => { + return pipe( + tap((_req: T) => patchState(store, {isLoading: true, error: null, validationErrors: null})), + switchMap((requestData : T) => { + return apiCall(requestData).pipe( + switchMap(() => authService.getCurrentUser()), + tap((user) => { + patchState(store, {user: user as User, isLoading: false}); + router.navigate(['/']); + }), + catchError((error: HttpErrorResponse) => { + const errorMessage = error.error?.message || fallbackError; + const validationErrors = error.error?.errors || null; + patchState(store, {isLoading : false, error: errorMessage, validationErrors}); + return of(null); + }) + ) + }) + ); + }; + return { + login: rxMethod( + handleAuthFlow((credentials : LoginRequest) => authService.login(credentials), 'Login Failed'), + ), + register: rxMethod( + handleAuthFlow((credentials: RegisterRequest) => authService.register(credentials), 'Registration Failed') + ), + checkAuth: rxMethod( + pipe( + switchMap(() => { + return authService.getCurrentUser().pipe( + tap((user) => patchState(store, {user: user as User, isLoading: false})), + catchError(() => { + patchState(store, {user: null, isLoading: false}) + return of(null); + }) + ) + }) + ), + ), + logout: rxMethod( + pipe( + tap(() => patchState(store, { isLoading: true })), + switchMap(() => { + return authService.logout().pipe( + tap(() => { + patchState(store, { user: null, isLoading: false }); + router.navigate(['/']); + }), + catchError(() => { + patchState(store, { user: null, isLoading: false }); + router.navigate(['/']); + return of(null); + }) + ) + }) + ) + ) + } + }), + withHooks( + { + onInit(store){ + store.checkAuth(); + } + } + ) +) diff --git a/frontend/src/app/auth/auth.types.ts b/frontend/src/app/auth/auth.types.ts new file mode 100644 index 0000000..fcfd1fc --- /dev/null +++ b/frontend/src/app/auth/auth.types.ts @@ -0,0 +1,30 @@ +export interface User { + id: string; + name: string; + email: string; + email_verified_at: Date | null; + created_at: Date; + updated_at: Date; + two_factor_secret: string | null; + two_factor_recovery_codes: string[] | null; + two_factor_confirmed_at: Date | null; +} + +export interface LoginRequest { + email: string; + password: string; +} + +export interface RegisterRequest { + name: string; + email: string; + password: string; + password_confirmation: string; +} + +export interface AuthState { + user: User | null; + isLoading: boolean; + error: string | null; + validationErrors: Record | null; +} diff --git a/frontend/src/app/auth/login/login.css b/frontend/src/app/auth/login/login.css new file mode 100644 index 0000000..04f7e98 --- /dev/null +++ b/frontend/src/app/auth/login/login.css @@ -0,0 +1,16 @@ +@keyframes slideUpFade { + from { + opacity: 0; + transform: translateY(40px) scale(0.98); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes pulse { + 0% { box-shadow: 0 0 0 0 rgba(79, 172, 254, 0.4); } + 70% { box-shadow: 0 0 0 10px rgba(79, 172, 254, 0); } + 100% { box-shadow: 0 0 0 0 rgba(79, 172, 254, 0); } +} diff --git a/frontend/src/app/auth/login/login.html b/frontend/src/app/auth/login/login.html new file mode 100644 index 0000000..2d9a869 --- /dev/null +++ b/frontend/src/app/auth/login/login.html @@ -0,0 +1,53 @@ +
+
+
+ + + +
+

Welcome Back

+

Sign in to continue to Post Assistant.

+
+ + + +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ + +
+ +

+ Don't have an account? + Register +

+
diff --git a/frontend/src/app/auth/login/login.spec.ts b/frontend/src/app/auth/login/login.spec.ts new file mode 100644 index 0000000..1ee4152 --- /dev/null +++ b/frontend/src/app/auth/login/login.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Login } from './login'; + +describe('Login', () => { + let component: Login; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Login], + }).compileComponents(); + + fixture = TestBed.createComponent(Login); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/auth/login/login.ts b/frontend/src/app/auth/login/login.ts new file mode 100644 index 0000000..19677a0 --- /dev/null +++ b/frontend/src/app/auth/login/login.ts @@ -0,0 +1,33 @@ +import { Component, inject} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import {AuthStore} from '../auth.store'; +import {environment} from '../../../environments/environment'; +import { ValidationErrors } from '../../shared/validation-errors/validation-errors'; + +@Component({ + selector: 'app-login', + standalone: true, + imports: [CommonModule, ReactiveFormsModule, RouterLink, ValidationErrors], + templateUrl: './login.html', + styleUrl: './login.css', +}) +export class Login { + loginForm: FormGroup; + protected readonly authStore = inject(AuthStore); + + constructor(private fb: FormBuilder) { + this.loginForm = this.fb.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', Validators.required] + }); + } + + login() { + if (this.loginForm.valid) { + if(!environment.production) console.log('Login submitted:', this.loginForm.value); + this.authStore.login(this.loginForm.value); + } + } +} diff --git a/frontend/src/app/auth/register/register.css b/frontend/src/app/auth/register/register.css new file mode 100644 index 0000000..d5adff7 --- /dev/null +++ b/frontend/src/app/auth/register/register.css @@ -0,0 +1,17 @@ + +@keyframes slideUpFade { + from { + opacity: 0; + transform: translateY(40px) scale(0.98); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes pulse { + 0% { box-shadow: 0 0 0 0 rgba(79, 172, 254, 0.4); } + 70% { box-shadow: 0 0 0 10px rgba(79, 172, 254, 0); } + 100% { box-shadow: 0 0 0 0 rgba(79, 172, 254, 0); } +} diff --git a/frontend/src/app/auth/register/register.html b/frontend/src/app/auth/register/register.html new file mode 100644 index 0000000..47414ba --- /dev/null +++ b/frontend/src/app/auth/register/register.html @@ -0,0 +1,75 @@ +
+
+
+ + + +
+

Create Account

+

Join us and start using Post Assistant.

+
+ +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ + +
+ +

+ Already have an account? + Sign in +

+
diff --git a/frontend/src/app/auth/register/register.spec.ts b/frontend/src/app/auth/register/register.spec.ts new file mode 100644 index 0000000..02f8661 --- /dev/null +++ b/frontend/src/app/auth/register/register.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Register } from './register'; + +describe('Register', () => { + let component: Register; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Register], + }).compileComponents(); + + fixture = TestBed.createComponent(Register); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/auth/register/register.ts b/frontend/src/app/auth/register/register.ts new file mode 100644 index 0000000..eb5431b --- /dev/null +++ b/frontend/src/app/auth/register/register.ts @@ -0,0 +1,34 @@ +import { Component, inject} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import {environment} from '../../../environments/environment'; +import {AuthStore} from '../auth.store'; + +@Component({ + selector: 'app-register', + standalone: true, + imports: [CommonModule, ReactiveFormsModule, RouterLink], + templateUrl: './register.html', + styleUrl: './register.css', +}) +export class Register { + registerForm: FormGroup; + private readonly authStore = inject(AuthStore); + + constructor(private fb: FormBuilder) { + this.registerForm = this.fb.group({ + name: ['', Validators.required], + email: ['', [Validators.required, Validators.email]], + password: ['', [Validators.required, Validators.minLength(8)]], + password_confirmation: ['', [Validators.required, Validators.minLength(8)]], + }); + } + + register() { + if (this.registerForm.valid) { + if(!environment.production) console.log('Register submitted:', this.registerForm.value); + this.authStore.register(this.registerForm.value); + } + } +} diff --git a/frontend/src/app/chat/chat.css b/frontend/src/app/chat/chat.css index 2bc3261..731a71e 100644 --- a/frontend/src/app/chat/chat.css +++ b/frontend/src/app/chat/chat.css @@ -1,18 +1,5 @@ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600&display=swap'); -:host { - display: block; - height: 100vh; - width: 100vw; - color: white; - overflow: hidden; - margin: 0; - padding: 0; - box-sizing: border-box; - font-family: 'Outfit', sans-serif; - background: radial-gradient(circle at top right, #1a1a2e 0%, #16213e 50%, #0f3460 100%); -} - .custom-scrollbar::-webkit-scrollbar { width: 6px; } diff --git a/frontend/src/app/chat/chat.html b/frontend/src/app/chat/chat.html index 82d34dd..60e21b9 100644 --- a/frontend/src/app/chat/chat.html +++ b/frontend/src/app/chat/chat.html @@ -1,4 +1,4 @@ -
+
AI
diff --git a/frontend/src/app/core/interceptors/sanctum-interceptor.spec.ts b/frontend/src/app/core/interceptors/sanctum-interceptor.spec.ts new file mode 100644 index 0000000..6fb3bef --- /dev/null +++ b/frontend/src/app/core/interceptors/sanctum-interceptor.spec.ts @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpInterceptorFn } from '@angular/common/http'; + +import { sanctumInterceptor } from './sanctum-interceptor'; + +describe('sanctumInterceptor', () => { + const interceptor: HttpInterceptorFn = (req, next) => + TestBed.runInInjectionContext(() => sanctumInterceptor(req, next)); + + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + + it('should be created', () => { + expect(interceptor).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/core/interceptors/sanctum-interceptor.ts b/frontend/src/app/core/interceptors/sanctum-interceptor.ts new file mode 100644 index 0000000..25fcb89 --- /dev/null +++ b/frontend/src/app/core/interceptors/sanctum-interceptor.ts @@ -0,0 +1,20 @@ +import { HttpInterceptorFn } from "@angular/common/http"; + +export const sanctumInterceptor: HttpInterceptorFn = (req, next) => { + const getCookie = (name: string): string | null => { + const match = document.cookie.match(new RegExp("(^|;\\s*)(" + name + ")=([^;]*)")); + return match ? decodeURIComponent(match[3]) : null; + }; + + let headers = req.headers.set("Accept", "application/json"); + const xsrfToken = getCookie("XSRF-TOKEN"); + if (xsrfToken) { + headers = headers.set("X-XSRF-TOKEN", xsrfToken); + } + const clonedRequest = req.clone({ + withCredentials: true, + headers: headers, + }); + + return next(clonedRequest); +}; diff --git a/frontend/src/app/core/layout/header/header.css b/frontend/src/app/core/layout/header/header.css new file mode 100644 index 0000000..e5ccefc --- /dev/null +++ b/frontend/src/app/core/layout/header/header.css @@ -0,0 +1,4 @@ +:host{ + display: block; + width: 100svw; +} diff --git a/frontend/src/app/core/layout/header/header.html b/frontend/src/app/core/layout/header/header.html new file mode 100644 index 0000000..1ec8b7c --- /dev/null +++ b/frontend/src/app/core/layout/header/header.html @@ -0,0 +1,41 @@ + diff --git a/frontend/src/app/core/layout/header/header.spec.ts b/frontend/src/app/core/layout/header/header.spec.ts new file mode 100644 index 0000000..9ef7403 --- /dev/null +++ b/frontend/src/app/core/layout/header/header.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Header } from './header'; + +describe('Header', () => { + let component: Header; + let fixture: ComponentFixture
; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Header], + }).compileComponents(); + + fixture = TestBed.createComponent(Header); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/core/layout/header/header.ts b/frontend/src/app/core/layout/header/header.ts new file mode 100644 index 0000000..eb4a806 --- /dev/null +++ b/frontend/src/app/core/layout/header/header.ts @@ -0,0 +1,14 @@ +import { Component, inject} from '@angular/core'; +import { RouterLink } from '@angular/router'; +import {AuthStore} from '../../../auth/auth.store'; + +@Component({ + selector: 'app-header', + imports: [RouterLink], + templateUrl: './header.html', + styleUrl: './header.css', +}) +export class Header { + protected readonly authStore = inject(AuthStore); + +} diff --git a/frontend/src/app/core/tokens/api-urls.ts b/frontend/src/app/core/tokens/api-urls.ts new file mode 100644 index 0000000..5dd5387 --- /dev/null +++ b/frontend/src/app/core/tokens/api-urls.ts @@ -0,0 +1,12 @@ +import { InjectionToken } from "@angular/core"; +import { environment } from "../../../environments/environment"; + +export const API_URL = new InjectionToken("API_URL", { + providedIn: "root", + factory: () => environment.apiUrl, +}); + +export const BASE_URL = new InjectionToken("BASE_URL", { + providedIn: "root", + factory: () => environment.baseApiUrl, +}); diff --git a/frontend/src/app/shared/validation-errors/validation-errors.css b/frontend/src/app/shared/validation-errors/validation-errors.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/shared/validation-errors/validation-errors.html b/frontend/src/app/shared/validation-errors/validation-errors.html new file mode 100644 index 0000000..78f1525 --- /dev/null +++ b/frontend/src/app/shared/validation-errors/validation-errors.html @@ -0,0 +1,21 @@ +@if (errorMessages().length > 0) { +
+
+
+ +
+
+

There were errors with your submission

+
+
    + @for (error of errorMessages(); track error) { +
  • {{ error }}
  • + } +
+
+
+
+
+} diff --git a/frontend/src/app/shared/validation-errors/validation-errors.spec.ts b/frontend/src/app/shared/validation-errors/validation-errors.spec.ts new file mode 100644 index 0000000..4dbcf87 --- /dev/null +++ b/frontend/src/app/shared/validation-errors/validation-errors.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ValidationErrors } from './validation-errors'; + +describe('ValidationErrors', () => { + let component: ValidationErrors; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ValidationErrors], + }).compileComponents(); + + fixture = TestBed.createComponent(ValidationErrors); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/shared/validation-errors/validation-errors.ts b/frontend/src/app/shared/validation-errors/validation-errors.ts new file mode 100644 index 0000000..dc33151 --- /dev/null +++ b/frontend/src/app/shared/validation-errors/validation-errors.ts @@ -0,0 +1,18 @@ +import { Component, computed, input } from '@angular/core'; + +@Component({ + selector: 'app-validation-errors', + imports: [], + templateUrl: './validation-errors.html', + styleUrl: './validation-errors.css', +}) +export class ValidationErrors { + errors = input | null | undefined>(); + + errorMessages = computed(() => { + const errs = this.errors(); + if (!errs) return []; + + return Object.values(errs).reduce((acc, curr) => acc.concat(curr), []); + }); +} diff --git a/frontend/src/environments/environment.development.ts b/frontend/src/environments/environment.development.ts new file mode 100644 index 0000000..4315e2b --- /dev/null +++ b/frontend/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + apiUrl: 'http://localhost:8000/api', + baseApiUrl: 'http://localhost:8000', +}; diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts new file mode 100644 index 0000000..d1f00ff --- /dev/null +++ b/frontend/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + apiUrl: 'my-deverything-api.com/api', + baseApiUrl: 'my-deverything-api.com', +}; diff --git a/frontend/src/index.html b/frontend/src/index.html index 3af61ec..06cfd45 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -7,7 +7,7 @@ - + diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 70d3686..0ed28b4 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1,3 +1,15 @@ /* You can add global styles to this file, and also import other style files */ - +@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600&display=swap'); @import 'tailwindcss'; + +body { + color: white; + margin: 0; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + font-family: 'Outfit', sans-serif; + background: radial-gradient(circle at top right, #1a1a2e 0%, #16213e 50%, #0f3460 100%); +}