feature: Fix Foritify error, implement logput on frontend. persist authentication on accross app

This commit is contained in:
kushal-saha 2026-04-28 05:25:06 +00:00
parent 19a05a1a5b
commit 66e858e386
44 changed files with 788 additions and 25 deletions

7
.idea/compiler.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="TypeScriptCompiler">
<option name="memoryAutoIncrease" value="true" />
<option name="nodeInterpreterTextField" value="wsl://Ubuntu@/home/krbfx/.nvm/versions/node/v16.20.2/bin/node" />
</component>
</project>

6
.idea/jsLinters/eslint.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EslintConfiguration">
<option name="fix-on-save" value="true" />
</component>
</project>

View File

@ -11,6 +11,7 @@
<option name="generationStringSettings">
<map>
<entry key="createEloquentScope:namespace" value="Models\Scopes" />
<entry key="createFormRequestDto:namespace" value="Data" />
<entry key="createModel:namespace" value="Models" />
</map>
</option>

1
.idea/prettier.xml generated
View File

@ -2,5 +2,6 @@
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
<option name="myRunOnSave" value="true" />
</component>
</project>

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Responses;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;
use Symfony\Component\HttpFoundation\Response;
class LoginResponse implements LoginResponseContract
{
/**
* Create an HTTP response that represents the object.
*
* @param Request $request
* @return Response
*/
public function toResponse($request): JsonResponse
{
return response()->json([
'message' => 'Logged in successfully.',
'two_factor' => false,
]);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Responses;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Laravel\Fortify\Contracts\LogoutResponse as LogoutResponseContract;
use Symfony\Component\HttpFoundation\Response;
class LogoutResponse implements LogoutResponseContract
{
/**
* Create an HTTP response that represents the object.
*
* @param Request $request
* @return Response
*/
public function toResponse($request): JsonResponse
{
return response()->json([
'message' => 'Logged out successfully.',
]);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Responses;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Laravel\Fortify\Contracts\RegisterResponse as RegisterResponseContract;
use Symfony\Component\HttpFoundation\Response;
class RegisterResponse implements RegisterResponseContract
{
/**
* Create an HTTP response that represents the object.
*
* @param Request $request
* @return Response
*/
public function toResponse($request): JsonResponse
{
return response()->json([
'message' => 'Registered successfully.',
], 201);
}
}

View File

@ -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);
}
/**

View File

@ -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(

View File

@ -73,7 +73,7 @@
|
*/
'home' => '/home',
'home' => env('FRONTEND_URL', 'http://localhost:4200/').'/',
/*
|--------------------------------------------------------------------------

View File

@ -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"

View File

@ -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])),
]
};

View File

@ -1 +1,2 @@
<app-header />
<router-outlet />

View File

@ -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'
})

View File

@ -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();
});
});

View File

@ -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<User>
{
return this.http.get<User>(`${this.apiUrl}/me`);
}
private getSanctumCookie(): Observable<null>
{
return this.http.get<null>(`${this.baseUrl}/sanctum/csrf-cookie`);
}
}

View File

@ -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),
},
]

View File

@ -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 = <T>(apiCall: (requestData: T) => Observable<any>, 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<LoginRequest>(
handleAuthFlow((credentials : LoginRequest) => authService.login(credentials), 'Login Failed'),
),
register: rxMethod<RegisterRequest>(
handleAuthFlow((credentials: RegisterRequest) => authService.register(credentials), 'Registration Failed')
),
checkAuth: rxMethod<void>(
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<void>(
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();
}
}
)
)

View File

@ -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<string, string[]> | null;
}

View File

@ -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); }
}

View File

@ -0,0 +1,53 @@
<div class="w-full max-w-[450px] mx-auto mt-27 flex flex-col bg-white/5 backdrop-blur-[20px] border border-white/10 rounded-3xl shadow-[0_25px_50px_-12px_rgba(0,0,0,0.5),inset_0_1px_0_rgba(255,255,255,0.1)] overflow-hidden animate-[slideUpFade_0.8s_cubic-bezier(0.16,1,0.3,1)] p-8">
<div class="text-center mb-8">
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-gradient-to-br from-[#00f2fe] to-[#4facfe] flex items-center justify-center font-semibold text-2xl shadow-[0_0_20px_rgba(79,172,254,0.4)] animate-[pulse_2s_infinite]">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
</div>
<h1 class="m-0 text-3xl font-semibold tracking-wide bg-gradient-to-r from-white to-indigo-300 bg-clip-text text-transparent">Welcome Back</h1>
<p class="m-0 mt-2 text-sm text-slate-400">Sign in to continue to Post Assistant.</p>
</div>
<app-validation-errors [errors]="authStore.validationErrors()" />
<form [formGroup]="loginForm" (ngSubmit)="login()" class="flex flex-col gap-5">
<div class="flex flex-col gap-1.5">
<label class="text-sm font-medium text-slate-300 ml-1">Email</label>
<div class="flex bg-white/5 px-4 py-3 rounded-xl border border-white/10 transition-all duration-300 focus-within:bg-white/10 focus-within:border-[#4facfe]/50 focus-within:shadow-[0_0_20px_rgba(79,172,254,0.2)]">
<input
class="flex-1 bg-transparent border-none text-white text-base outline-none placeholder-slate-500"
type="email"
formControlName="email"
placeholder="name@example.com"
/>
</div>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-sm font-medium text-slate-300 ml-1">Password</label>
<div class="flex bg-white/5 px-4 py-3 rounded-xl border border-white/10 transition-all duration-300 focus-within:bg-white/10 focus-within:border-[#4facfe]/50 focus-within:shadow-[0_0_20px_rgba(79,172,254,0.2)]">
<input
class="flex-1 bg-transparent border-none text-white text-base outline-none placeholder-slate-500"
type="password"
formControlName="password"
placeholder="••••••••"
/>
</div>
</div>
<button
type="submit"
class="mt-4 bg-gradient-to-br from-[#00f2fe] to-[#4facfe] border-none py-3.5 rounded-xl font-semibold text-white text-lg transition-all duration-200 hover:scale-[1.02] hover:-translate-y-0.5 hover:shadow-[0_10px_20px_rgba(79,172,254,0.3)] active:scale-[0.98] disabled:opacity-70 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-none"
[disabled]="loginForm.invalid"
>
Sign In
</button>
</form>
<p class="mt-8 text-center text-sm text-slate-400">
Don't have an account?
<a routerLink="/user/register" class="text-[#4facfe] hover:text-[#00f2fe] font-medium transition-colors">Register</a>
</p>
</div>

View File

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Login } from './login';
describe('Login', () => {
let component: Login;
let fixture: ComponentFixture<Login>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Login],
}).compileComponents();
fixture = TestBed.createComponent(Login);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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);
}
}
}

View File

@ -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); }
}

View File

@ -0,0 +1,75 @@
<div class="w-full max-w-[450px] mx-auto mt-27 flex flex-col bg-white/5 backdrop-blur-[20px] border border-white/10 rounded-3xl shadow-[0_25px_50px_-12px_rgba(0,0,0,0.5),inset_0_1px_0_rgba(255,255,255,0.1)] overflow-hidden animate-[slideUpFade_0.8s_cubic-bezier(0.16,1,0.3,1)] p-8">
<div class="text-center mb-8">
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-gradient-to-br from-[#00f2fe] to-[#4facfe] flex items-center justify-center font-semibold text-2xl shadow-[0_0_20px_rgba(79,172,254,0.4)] animate-[pulse_2s_infinite]">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"></path>
</svg>
</div>
<h1 class="m-0 text-3xl font-semibold tracking-wide bg-gradient-to-r from-white to-indigo-300 bg-clip-text text-transparent">Create Account</h1>
<p class="m-0 mt-2 text-sm text-slate-400">Join us and start using Post Assistant.</p>
</div>
<form [formGroup]="registerForm" (ngSubmit)="register()" class="flex flex-col gap-5">
<div class="flex flex-col gap-1.5">
<label class="text-sm font-medium text-slate-300 ml-1">Name</label>
<div class="flex bg-white/5 px-4 py-3 rounded-xl border border-white/10 transition-all duration-300 focus-within:bg-white/10 focus-within:border-[#4facfe]/50 focus-within:shadow-[0_0_20px_rgba(79,172,254,0.2)]">
<input
class="flex-1 bg-transparent border-none text-white text-base outline-none placeholder-slate-500"
type="text"
formControlName="name"
placeholder="John Doe"
/>
</div>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-sm font-medium text-slate-300 ml-1">Email</label>
<div class="flex bg-white/5 px-4 py-3 rounded-xl border border-white/10 transition-all duration-300 focus-within:bg-white/10 focus-within:border-[#4facfe]/50 focus-within:shadow-[0_0_20px_rgba(79,172,254,0.2)]">
<input
class="flex-1 bg-transparent border-none text-white text-base outline-none placeholder-slate-500"
type="email"
formControlName="email"
placeholder="name@example.com"
/>
</div>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-sm font-medium text-slate-300 ml-1">Password</label>
<div class="flex bg-white/5 px-4 py-3 rounded-xl border border-white/10 transition-all duration-300 focus-within:bg-white/10 focus-within:border-[#4facfe]/50 focus-within:shadow-[0_0_20px_rgba(79,172,254,0.2)]">
<input
class="flex-1 bg-transparent border-none text-white text-base outline-none placeholder-slate-500"
type="password"
formControlName="password"
placeholder="••••••••"
/>
</div>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-sm font-medium text-slate-300 ml-1">Confirm Password</label>
<div class="flex bg-white/5 px-4 py-3 rounded-xl border border-white/10 transition-all duration-300 focus-within:bg-white/10 focus-within:border-[#4facfe]/50 focus-within:shadow-[0_0_20px_rgba(79,172,254,0.2)]">
<input
class="flex-1 bg-transparent border-none text-white text-base outline-none placeholder-slate-500"
type="password"
formControlName="password_confirmation"
placeholder="••••••••"
/>
</div>
</div>
<button
type="submit"
class="mt-4 bg-gradient-to-br from-[#00f2fe] to-[#4facfe] border-none py-3.5 rounded-xl font-semibold text-white text-lg transition-all duration-200 hover:scale-[1.02] hover:-translate-y-0.5 hover:shadow-[0_10px_20px_rgba(79,172,254,0.3)] active:scale-[0.98] disabled:opacity-70 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-none"
[disabled]="registerForm.invalid"
>
Sign Up
</button>
</form>
<p class="mt-8 text-center text-sm text-slate-400">
Already have an account?
<a routerLink="/user/login" class="text-[#4facfe] hover:text-[#00f2fe] font-medium transition-colors">Sign in</a>
</p>
</div>

View File

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Register } from './register';
describe('Register', () => {
let component: Register;
let fixture: ComponentFixture<Register>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Register],
}).compileComponents();
fixture = TestBed.createComponent(Register);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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);
}
}
}

View File

@ -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;
}

View File

@ -1,4 +1,4 @@
<div class="max-w-[900px] h-[90vh] my-[5vh] mx-auto flex flex-col bg-white/5 backdrop-blur-[20px] border border-white/10 rounded-3xl shadow-[0_25px_50px_-12px_rgba(0,0,0,0.5),inset_0_1px_0_rgba(255,255,255,0.1)] overflow-hidden animate-[slideUpFade_0.8s_cubic-bezier(0.16,1,0.3,1)]">
<div class="max-w-[900px] mt-15 h-[80vh] mx-auto my-auto flex flex-col bg-white/5 backdrop-blur-[20px] border border-white/10 rounded-3xl shadow-[0_25px_50px_-12px_rgba(0,0,0,0.5),inset_0_1px_0_rgba(255,255,255,0.1)] overflow-hidden animate-[slideUpFade_0.8s_cubic-bezier(0.16,1,0.3,1)]">
<div class="px-8 py-6 border-b border-white/10 flex items-center gap-4 bg-black/20">
<div class="w-12 h-12 rounded-full bg-gradient-to-br from-[#00f2fe] to-[#4facfe] flex items-center justify-center font-semibold text-xl shadow-[0_0_20px_rgba(79,172,254,0.4)] animate-[pulse_2s_infinite]">AI</div>
<div>

View File

@ -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();
});
});

View File

@ -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);
};

View File

@ -0,0 +1,4 @@
:host{
display: block;
width: 100svw;
}

View File

@ -0,0 +1,41 @@
<nav
class="bg-gray-900/80 backdrop-blur-md border-b border-gray-800 shadow-sm fixed w-full top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<!-- Logo -->
<div class="flex-shrink-0 flex items-center cursor-pointer" routerLink="/">
<span
class="text-2xl font-extrabold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-blue-500 to-indigo-700 dark:from-blue-400 dark:to-indigo-400">
NeoBan
</span>
</div>
@if (authStore.isLoading()) {
<p>Loading...</p>
}
@else if (authStore.user() === null) {
<!-- Actions -->
<div class="flex items-center space-x-3">
<a routerLink="/user/login"
class="text-sm font-semibold text-gray-400 hover:text-blue-600 transition-colors duration-200 px-3 py-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800">
Login
</a>
<a routerLink="/user/register"
class="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-semibold rounded-lg text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:-translate-y-0.5">
Register
</a>
</div>
}
@else{
<div class="flex font-normal items-center space-x-3">
<span class="text-sm text-gray-200 dark:text-gray-300">
{{ authStore.user()?.name }}
</span>
<button (click)="authStore.logout()"
class="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-semibold rounded-lg text-gray-400 bg-gray-200/9 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:-translate-y-0.5">
Logout
</button>
</div>
}
</div>
</div>
</nav>

View File

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Header } from './header';
describe('Header', () => {
let component: Header;
let fixture: ComponentFixture<Header>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Header],
}).compileComponents();
fixture = TestBed.createComponent(Header);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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);
}

View File

@ -0,0 +1,12 @@
import { InjectionToken } from "@angular/core";
import { environment } from "../../../environments/environment";
export const API_URL = new InjectionToken<string>("API_URL", {
providedIn: "root",
factory: () => environment.apiUrl,
});
export const BASE_URL = new InjectionToken<string>("BASE_URL", {
providedIn: "root",
factory: () => environment.baseApiUrl,
});

View File

@ -0,0 +1,21 @@
@if (errorMessages().length > 0) {
<div class="rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-4 mb-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400 dark:text-red-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800 dark:text-red-400">There were errors with your submission</h3>
<div class="mt-2 text-sm text-red-700 dark:text-red-300">
<ul role="list" class="list-disc space-y-1 pl-5">
@for (error of errorMessages(); track error) {
<li>{{ error }}</li>
}
</ul>
</div>
</div>
</div>
</div>
}

View File

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ValidationErrors } from './validation-errors';
describe('ValidationErrors', () => {
let component: ValidationErrors;
let fixture: ComponentFixture<ValidationErrors>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ValidationErrors],
}).compileComponents();
fixture = TestBed.createComponent(ValidationErrors);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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<Record<string, string[]> | null | undefined>();
errorMessages = computed(() => {
const errs = this.errors();
if (!errs) return [];
return Object.values(errs).reduce((acc, curr) => acc.concat(curr), []);
});
}

View File

@ -0,0 +1,5 @@
export const environment = {
production: false,
apiUrl: 'http://localhost:8000/api',
baseApiUrl: 'http://localhost:8000',
};

View File

@ -0,0 +1,5 @@
export const environment = {
production: true,
apiUrl: 'my-deverything-api.com/api',
baseApiUrl: 'my-deverything-api.com',
};

View File

@ -7,7 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<body class="min-h-screen antialiased">
<app-root></app-root>
</body>
</html>

View File

@ -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%);
}