Merge branch 'feature/main/login'
This commit is contained in:
commit
03525280db
@ -1,10 +1,12 @@
|
|||||||
import { Routes } from "@angular/router";
|
import { Routes } from "@angular/router";
|
||||||
import { Home } from "./features/home/home";
|
import { Home } from "./features/home/home";
|
||||||
|
import { authGuard } from "./core/guards/auth-guard";
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
component: Home,
|
component: Home,
|
||||||
|
canActivate: [authGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "",
|
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"
|
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="">
|
<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>
|
||||||
|
|
||||||
<div class="flex-1 grid grid-cols-[1fr_auto]">
|
<div class="flex-1 grid grid-cols-[1fr_auto]">
|
||||||
<input
|
<input
|
||||||
type="text"
|
|
||||||
class="w-full border border-gray-300 text-sm rounded-full rounded-r-none px-6 py-1"
|
class="w-full border border-gray-300 text-sm rounded-full rounded-r-none px-6 py-1"
|
||||||
placeholder="Search watches, brands, products..."
|
placeholder="Search watches, brands, products..."
|
||||||
|
type="text"
|
||||||
/>
|
/>
|
||||||
<button class="btn btn-ghost rounded-l-none! py-1 px-3 border-l-0!">
|
<button class="btn btn-ghost rounded-l-none! py-1 px-3 border-l-0!">
|
||||||
<lucide-angular [img]="SearchIcon" class="w-5" />
|
<lucide-angular [img]="SearchIcon" class="w-5" />
|
||||||
@ -20,9 +20,9 @@
|
|||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<div class="flex text-gray-600">
|
<div class="flex text-gray-600">
|
||||||
<button
|
<button
|
||||||
|
class="btn btn-ghost py-1 px-2 rounded-r-none!"
|
||||||
popovertarget="popover-1"
|
popovertarget="popover-1"
|
||||||
style="anchor-name: --anchor-1"
|
style="anchor-name: --anchor-1"
|
||||||
class="btn btn-ghost py-1 px-2 rounded-r-none!"
|
|
||||||
>
|
>
|
||||||
<lucide-angular [img]="UserIcon" class="w-5" />
|
<lucide-angular [img]="UserIcon" class="w-5" />
|
||||||
</button>
|
</button>
|
||||||
@ -32,8 +32,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<ul class="dropdown" popover id="popover-1" style="position-anchor: --anchor-1">
|
<ul class="dropdown" id="popover-1" popover style="position-anchor: --anchor-1">
|
||||||
<li><a routerLink="/login" class="block h-full w-full" href="">Login</a></li>
|
@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="">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="">Orders</a></li>
|
||||||
<li><a class="block h-full w-full" href="">Wishlist</a></li>
|
<li><a class="block h-full w-full" href="">Wishlist</a></li>
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, inject } from "@angular/core";
|
||||||
import { LucideAngularModule, User, ShoppingCart, Search } from "lucide-angular";
|
import { LucideAngularModule, Search, ShoppingCart, User } from "lucide-angular";
|
||||||
import { RouterLink } from "@angular/router";
|
import { RouterLink } from "@angular/router";
|
||||||
|
import { AuthService, AuthState } from "../../../features/auth/services/auth-service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-header",
|
selector: "app-header",
|
||||||
imports: [LucideAngularModule, RouterLink],
|
imports: [LucideAngularModule, RouterLink],
|
||||||
@ -11,4 +13,6 @@ export class Header {
|
|||||||
readonly UserIcon = User;
|
readonly UserIcon = User;
|
||||||
readonly CartIcon = ShoppingCart;
|
readonly CartIcon = ShoppingCart;
|
||||||
readonly SearchIcon = Search;
|
readonly SearchIcon = Search;
|
||||||
|
readonly authService = inject(AuthService);
|
||||||
|
protected readonly AuthState = AuthState;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,3 +6,11 @@ export interface RegisterUserRequest {
|
|||||||
password_confirmation: string | null;
|
password_confirmation: string | null;
|
||||||
city: 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 { Login } from "./components/login/login";
|
||||||
import { Register } from "./components/register/register";
|
import { Register } from "./components/register/register";
|
||||||
|
import { inject } from "@angular/core";
|
||||||
|
import { AuthService } from "./services/auth-service";
|
||||||
|
|
||||||
export const AuthRoutes: Routes = [
|
export const AuthRoutes: Routes = [
|
||||||
{
|
{
|
||||||
@ -14,6 +16,19 @@ export const AuthRoutes: Routes = [
|
|||||||
path: "register",
|
path: "register",
|
||||||
component: 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({
|
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 { computed, inject, Injectable, Signal, signal, WritableSignal } from "@angular/core";
|
||||||
import { RegisterUserRequest } from "../../../core/models/user.model";
|
import { RegisterUserRequest, User } from "../../../core/models/user.model";
|
||||||
import { HttpClient } from "@angular/common/http";
|
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
|
||||||
import { API_URL, BACKEND_URL } from "../../../core/tokens/api-url-tokens";
|
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({
|
@Injectable({
|
||||||
providedIn: "root",
|
providedIn: "root",
|
||||||
})
|
})
|
||||||
export class AuthService {
|
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);
|
private http: HttpClient = inject(HttpClient);
|
||||||
|
// Constants
|
||||||
|
private readonly userKey = "ekart_user";
|
||||||
private apiUrl = inject(API_URL);
|
private apiUrl = inject(API_URL);
|
||||||
private backendURL = inject(BACKEND_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) {
|
register(userRequest: RegisterUserRequest) {
|
||||||
console.log(this.apiUrl);
|
|
||||||
return this.http.post(`${this.apiUrl}/register`, userRequest);
|
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 }) {
|
login(credentials: { email: string; password: string }) {
|
||||||
return this.getCsrfCookie().pipe(
|
return this.getCsrfCookie().pipe(
|
||||||
switchMap(() =>
|
switchMap(() =>
|
||||||
this.http.post(`${this.backendURL}/login`, credentials, { observe: "response" }),
|
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`);
|
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