feature: user logout and auth states

added s authState which helps conditonaly render components based on this state

stored user details in localStoarge so that server side end point does not get hit in every page load.

add a guard which protects routes and redirects to login if user is not logged in.

create a logout route
This commit is contained in:
kusowl 2026-02-24 18:14:21 +05:30
parent 0427d1c62d
commit 4a4c8bd4e3
11 changed files with 202 additions and 15 deletions

View File

@ -1,10 +1,12 @@
import { Routes } from "@angular/router";
import { Home } from "./features/home/home";
import { authGuard } from "./core/guards/auth-guard";
export const routes: Routes = [
{
path: "",
component: Home,
canActivate: [authGuard],
},
{
path: "",

View File

@ -0,0 +1,17 @@
import { TestBed } from "@angular/core/testing";
import { CanActivateFn } from "@angular/router";
import { authGuard } from "./auth-guard";
describe("authGuard", () => {
const executeGuard: CanActivateFn = (...guardParameters) =>
TestBed.runInInjectionContext(() => authGuard(...guardParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it("should be created", () => {
expect(executeGuard).toBeTruthy();
});
});

View File

@ -0,0 +1,11 @@
import { CanActivateFn, Router } from "@angular/router";
import { inject } from "@angular/core";
import { AuthService } from "../../features/auth/services/auth-service";
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) return true;
return router.navigate(["/login"]);
};

View File

@ -3,14 +3,14 @@
class="bg-gray-50 wrapper py-4 flex gap-x-5 sm:gap-x-10 items-center shadow-lg shadow-gray-400/20"
>
<div class="">
<a href="/" class="px-3 py-1 bg-gray-800 text-white">eKart</a>
<a class="px-3 py-1 bg-gray-800 text-white" href="/">eKart</a>
</div>
<div class="flex-1 grid grid-cols-[1fr_auto]">
<input
type="text"
class="w-full border border-gray-300 text-sm rounded-full rounded-r-none px-6 py-1"
placeholder="Search watches, brands, products..."
type="text"
/>
<button class="btn btn-ghost rounded-l-none! py-1 px-3 border-l-0!">
<lucide-angular [img]="SearchIcon" class="w-5" />
@ -20,9 +20,9 @@
<div class="flex space-x-4">
<div class="flex text-gray-600">
<button
class="btn btn-ghost py-1 px-2 rounded-r-none!"
popovertarget="popover-1"
style="anchor-name: --anchor-1"
class="btn btn-ghost py-1 px-2 rounded-r-none!"
>
<lucide-angular [img]="UserIcon" class="w-5" />
</button>
@ -32,8 +32,14 @@
</div>
</div>
</nav>
<ul class="dropdown" popover id="popover-1" style="position-anchor: --anchor-1">
<li><a routerLink="/login" class="block h-full w-full" href="">Login</a></li>
<ul class="dropdown" id="popover-1" popover style="position-anchor: --anchor-1">
@if (authService.authState() === AuthState.Unauthenticated) {
<li><a class="block h-full w-full" routerLink="/login">Login</a></li>
} @else if (authService.authState() === AuthState.Loading) {
<li><a class="block h-full w-full">Loading</a></li>
} @else {
<li><a class="block h-full w-full" routerLink="/logout">Logout</a></li>
}
<li><a class="block h-full w-full" href="">My Account</a></li>
<li><a class="block h-full w-full" href="">Orders</a></li>
<li><a class="block h-full w-full" href="">Wishlist</a></li>

View File

@ -1,6 +1,8 @@
import { Component } from "@angular/core";
import { LucideAngularModule, User, ShoppingCart, Search } from "lucide-angular";
import { Component, inject } from "@angular/core";
import { LucideAngularModule, Search, ShoppingCart, User } from "lucide-angular";
import { RouterLink } from "@angular/router";
import { AuthService, AuthState } from "../../../features/auth/services/auth-service";
@Component({
selector: "app-header",
imports: [LucideAngularModule, RouterLink],
@ -11,4 +13,6 @@ export class Header {
readonly UserIcon = User;
readonly CartIcon = ShoppingCart;
readonly SearchIcon = Search;
readonly authService = inject(AuthService);
protected readonly AuthState = AuthState;
}

View File

@ -6,3 +6,11 @@ export interface RegisterUserRequest {
password_confirmation: string | null;
city: string | null;
}
export interface User {
id: number;
name: string;
email: string;
mobileNumber: string;
city: string;
}

View File

@ -0,0 +1,39 @@
import { Injectable } from "@angular/core";
@Injectable({
providedIn: "root",
})
export class LocalStorageService {
setItem<T>(key: string, value: T) {
try {
const item = JSON.stringify(value);
localStorage.setItem(key, item);
} catch (e) {
console.error("Error storing item in local storage: ", e);
}
}
getItem<T>(key: string): T | null {
try {
const item = localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : null;
} catch (err) {
console.error("Error getting item from local storage: ", err);
return null;
}
}
/**
* @throws Error if item is not found
*/
removeItem(key: string) {
localStorage.removeItem(key);
}
/**
* @Throws Error if localstorage API is not available
*/
clear() {
localStorage.clear();
}
}

View File

@ -0,0 +1,16 @@
import { TestBed } from "@angular/core/testing";
import { LocalStorageService } from "./local-storage.service";
describe("LocalStorage", () => {
let service: LocalStorageService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(LocalStorageService);
});
it("should be created", () => {
expect(service).toBeTruthy();
});
});

View File

@ -1,6 +1,8 @@
import { Routes } from "@angular/router";
import { Router, Routes } from "@angular/router";
import { Login } from "./components/login/login";
import { Register } from "./components/register/register";
import { inject } from "@angular/core";
import { AuthService } from "./services/auth-service";
export const AuthRoutes: Routes = [
{
@ -14,6 +16,19 @@ export const AuthRoutes: Routes = [
path: "register",
component: Register,
},
{
path: "logout",
component: Login,
canActivate: [
() => {
const authService = inject(AuthService);
const router = inject(Router);
authService.logout().subscribe(() => router.navigate(["/login"]));
return false;
},
],
},
],
},
];

View File

@ -32,7 +32,7 @@ export class Login {
}
this.authService.login(this.loginForm.value as { email: string; password: string }).subscribe({
next: () => console.log("success"),
next: () => this.router.navigate(["/"]),
});
}
}

View File

@ -1,31 +1,100 @@
import { Injectable, inject } from "@angular/core";
import { RegisterUserRequest } from "../../../core/models/user.model";
import { HttpClient } from "@angular/common/http";
import { computed, inject, Injectable, Signal, signal, WritableSignal } from "@angular/core";
import { RegisterUserRequest, User } from "../../../core/models/user.model";
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { API_URL, BACKEND_URL } from "../../../core/tokens/api-url-tokens";
import { switchMap } from "rxjs";
import { switchMap, tap } from "rxjs";
import { LocalStorageService } from "../../../core/services/local-storage.service";
export enum AuthState {
Loading = "loading",
Authenticated = "authenticated",
Unauthenticated = "unauthenticated",
}
/**
* UserService - manages user related operations
*
* ## Auth States
* - loading: user is being fetched from the server
* - authenticated: user is authenticated
* - unauthenticated: user is not authenticated
*/
@Injectable({
providedIn: "root",
})
export class AuthService {
// User states
readonly authState: WritableSignal<AuthState>;
readonly user: WritableSignal<User | null>;
// Computed state for easy checking
readonly isAuthenticated: Signal<boolean>;
// Dependent services
private localStorage = inject(LocalStorageService);
private http: HttpClient = inject(HttpClient);
// Constants
private readonly userKey = "ekart_user";
private apiUrl = inject(API_URL);
private backendURL = inject(BACKEND_URL);
constructor() {
const cachedUser = this.localStorage.getItem<User>(this.userKey);
this.authState = signal<AuthState>(
cachedUser ? AuthState.Authenticated : AuthState.Unauthenticated,
);
this.user = signal<User | null>(cachedUser);
this.isAuthenticated = computed(() => !!this.user());
}
register(userRequest: RegisterUserRequest) {
console.log(this.apiUrl);
return this.http.post(`${this.apiUrl}/register`, userRequest);
}
/**
* Laravel API expects the csrf cookie to be set before making a request.
* First set the cookie then attempt to login.
* If the login is successful, set the user in the state.
*/
login(credentials: { email: string; password: string }) {
return this.getCsrfCookie().pipe(
switchMap(() =>
this.http.post(`${this.backendURL}/login`, credentials, { observe: "response" }),
),
switchMap(() => this.getCurrentUser()),
);
}
getCsrfCookie() {
getCurrentUser() {
return this.http.get<User>(`${this.apiUrl}/user`).pipe(
tap({
next: (user) => this.setAuth(user),
error: (error: HttpErrorResponse) => this.purgeAuth(),
}),
);
}
logout() {
return this.http.post(`${this.backendURL}/logout`, {}).pipe(
tap({
next: () => this.purgeAuth(),
}),
);
}
private getCsrfCookie() {
return this.http.get(`${this.backendURL}/sanctum/csrf-cookie`);
}
private setAuth(user: User) {
this.localStorage.setItem<User>(this.userKey, user);
this.user.set(user);
this.authState.set(AuthState.Authenticated);
}
private purgeAuth() {
this.localStorage.removeItem(this.userKey);
this.user.set(null);
this.authState.set(AuthState.Unauthenticated);
}
}