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:
parent
0427d1c62d
commit
4a4c8bd4e3
@ -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: "",
|
||||
|
||||
17
src/app/core/guards/auth-guard.spec.ts
Normal file
17
src/app/core/guards/auth-guard.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
11
src/app/core/guards/auth-guard.ts
Normal file
11
src/app/core/guards/auth-guard.ts
Normal 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"]);
|
||||
};
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
39
src/app/core/services/local-storage.service.ts
Normal file
39
src/app/core/services/local-storage.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
16
src/app/core/services/local-storage.spec.ts
Normal file
16
src/app/core/services/local-storage.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@ -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(["/"]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user