From c6d21faaee5a4d28350eba0dc0f9c58e4e5a5967 Mon Sep 17 00:00:00 2001 From: mgrablo <51509107+mgrablo@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:31:50 +0200 Subject: [PATCH 1/4] refactor(ui/auth): extract auth dto into seperate files and move them to features/auth/data/dto directory --- .../src/app/core/services/auth.service.ts | 78 ++++++++++--------- .../auth/data/dto/login-request.model.ts | 4 + .../auth/data/dto/login-response.model.ts | 6 ++ .../auth/data/dto/register-request.model.ts | 5 ++ biblionode-ui/src/app/models/auth.model.ts | 16 ---- 5 files changed, 55 insertions(+), 54 deletions(-) create mode 100644 biblionode-ui/src/app/features/auth/data/dto/login-request.model.ts create mode 100644 biblionode-ui/src/app/features/auth/data/dto/login-response.model.ts create mode 100644 biblionode-ui/src/app/features/auth/data/dto/register-request.model.ts delete mode 100644 biblionode-ui/src/app/models/auth.model.ts diff --git a/biblionode-ui/src/app/core/services/auth.service.ts b/biblionode-ui/src/app/core/services/auth.service.ts index 1e8e455..6a06dc6 100644 --- a/biblionode-ui/src/app/core/services/auth.service.ts +++ b/biblionode-ui/src/app/core/services/auth.service.ts @@ -1,38 +1,40 @@ -import { computed, inject, Injectable, signal } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { LoginRequest, LoginResponse, RegisterRequest } from '../../models/auth.model'; -import { catchError, tap } from 'rxjs'; -import { ReaderProfile } from '../../models/reader-profile.model'; - -@Injectable({ - providedIn: 'root', -}) -export class AuthService { - private http = inject(HttpClient); - private tokenSignal = signal(localStorage.getItem('token')); - - isAuthenticated = computed(() => !!this.tokenSignal()); - - login(credentials: LoginRequest) { - return this.http.post('api/auth/login', credentials).pipe( - tap((response) => { - localStorage.setItem('token', response.token); - this.tokenSignal.set(response.token); - }), - catchError((error) => { - localStorage.removeItem('token'); - this.tokenSignal.set(null); - throw error; - }), - ); - } - - register(request: RegisterRequest) { - return this.http.post('api/auth/register', request); - } - - logout() { - localStorage.removeItem('token'); - this.tokenSignal.set(null); - } -} +import { computed, inject, Injectable, signal } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { RegisterRequest } from "../../features/auth/data/dto/register-request.model"; +import { LoginRequest } from "../../features/auth/data/dto/login-request.model"; +import { LoginResponse } from "../../features/auth/data/dto/login-response.model"; +import { catchError, tap } from 'rxjs'; +import { ReaderProfile } from '../../models/reader-profile.model'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthService { + private http = inject(HttpClient); + private tokenSignal = signal(localStorage.getItem('token')); + + isAuthenticated = computed(() => !!this.tokenSignal()); + + login(credentials: LoginRequest) { + return this.http.post('api/auth/login', credentials).pipe( + tap((response) => { + localStorage.setItem('token', response.token); + this.tokenSignal.set(response.token); + }), + catchError((error) => { + localStorage.removeItem('token'); + this.tokenSignal.set(null); + throw error; + }), + ); + } + + register(request: RegisterRequest) { + return this.http.post('api/auth/register', request); + } + + logout() { + localStorage.removeItem('token'); + this.tokenSignal.set(null); + } +} diff --git a/biblionode-ui/src/app/features/auth/data/dto/login-request.model.ts b/biblionode-ui/src/app/features/auth/data/dto/login-request.model.ts new file mode 100644 index 0000000..b57ebf6 --- /dev/null +++ b/biblionode-ui/src/app/features/auth/data/dto/login-request.model.ts @@ -0,0 +1,4 @@ +export interface LoginRequest { + email: string; + password: string; +} diff --git a/biblionode-ui/src/app/features/auth/data/dto/login-response.model.ts b/biblionode-ui/src/app/features/auth/data/dto/login-response.model.ts new file mode 100644 index 0000000..f0d9119 --- /dev/null +++ b/biblionode-ui/src/app/features/auth/data/dto/login-response.model.ts @@ -0,0 +1,6 @@ + +export interface LoginResponse { + token: string; + email: string; + roles: string[]; +} diff --git a/biblionode-ui/src/app/features/auth/data/dto/register-request.model.ts b/biblionode-ui/src/app/features/auth/data/dto/register-request.model.ts new file mode 100644 index 0000000..070642d --- /dev/null +++ b/biblionode-ui/src/app/features/auth/data/dto/register-request.model.ts @@ -0,0 +1,5 @@ +export interface RegisterRequest { + email: string; + password: string; + fullName: string; +} diff --git a/biblionode-ui/src/app/models/auth.model.ts b/biblionode-ui/src/app/models/auth.model.ts deleted file mode 100644 index 9af5285..0000000 --- a/biblionode-ui/src/app/models/auth.model.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface LoginRequest { - email: string; - password: string; -} - -export interface LoginResponse { - token: string; - email: string; - roles: string[]; -} - -export interface RegisterRequest { - email: string; - password: string; - fullName: string; -} From 09fdeb940540cc27fad97bc58b6cf0449a307687 Mon Sep 17 00:00:00 2001 From: mgrablo <51509107+mgrablo@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:38:21 +0200 Subject: [PATCH 2/4] refactor(ui/auth): move auth service from core/services to features/auth/data/services --- .../interceptors/auth.interceptor.spec.ts | 188 ++++++------ .../app/core/interceptors/auth.interceptor.ts | 58 ++-- .../auth/data}/services/auth.service.spec.ts | 286 +++++++++--------- .../auth/data}/services/auth.service.ts | 8 +- .../features/auth/login/login.component.ts | 122 ++++---- .../app/features/auth/login/login.stories.ts | 2 +- .../auth/register/register.component.ts | 160 +++++----- .../auth/register/register.stories.ts | 54 ++-- 8 files changed, 439 insertions(+), 439 deletions(-) rename biblionode-ui/src/app/{core => features/auth/data}/services/auth.service.spec.ts (96%) rename biblionode-ui/src/app/{core => features/auth/data}/services/auth.service.ts (74%) diff --git a/biblionode-ui/src/app/core/interceptors/auth.interceptor.spec.ts b/biblionode-ui/src/app/core/interceptors/auth.interceptor.spec.ts index 515908f..58697be 100644 --- a/biblionode-ui/src/app/core/interceptors/auth.interceptor.spec.ts +++ b/biblionode-ui/src/app/core/interceptors/auth.interceptor.spec.ts @@ -1,94 +1,94 @@ -import { TestBed } from '@angular/core/testing'; -import { - HttpClient, - HttpInterceptorFn, - provideHttpClient, - withInterceptors, -} from '@angular/common/http'; - -import { authInterceptor } from './auth.interceptor'; -import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; -import { AuthService } from '../services/auth.service'; -import { Router } from '@angular/router'; - -describe('authInterceptor', () => { - const interceptor: HttpInterceptorFn = (req, next) => - TestBed.runInInjectionContext(() => authInterceptor(req, next)); - - let httpTesting: HttpTestingController; - let httpClient: HttpClient; - let authService: AuthService; - let router: Router; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - provideHttpClient(withInterceptors([authInterceptor])), - provideHttpClientTesting(), - { - provide: AuthService, - useValue: { logout: vi.fn() }, - }, - { - provide: Router, - useValue: { navigate: vi.fn() }, - }, - ], - }); - - httpTesting = TestBed.inject(HttpTestingController); - httpClient = TestBed.inject(HttpClient); - authService = TestBed.inject(AuthService); - router = TestBed.inject(Router); - - localStorage.clear(); - }); - - afterEach(() => { - httpTesting.verify(); - }); - - it('should be created', () => { - expect(interceptor).toBeTruthy(); - }); - - it('should add Authorization header when token is present', () => { - const token = 'fake-jwt-token'; - localStorage.setItem('token', token); - - httpClient.get('api/test').subscribe(); - - const req = httpTesting.expectOne('api/test'); - expect(req.request.headers.has('Authorization')).toBe(true); - expect(req.request.headers.get('Authorization')).toBe(`Bearer ${token}`); - }); - - it('should not add Authorization header when token is absent', () => { - httpClient.get('api/test').subscribe(); - - const req = httpTesting.expectOne('api/test'); - expect(req.request.headers.has('Authorization')).toBe(false); - }); - - it('should handle 401 error by logging out', () => { - httpClient.get('api/test').subscribe({ - error: () => {}, - }); - - const req = httpTesting.expectOne('api/test'); - req.flush('Unauthorized', { status: 401, statusText: 'Unauthorized' }); - - expect(authService.logout).toHaveBeenCalled(); - }); - - it('should navigate to login page on 401 error', async () => { - httpClient.get('api/test').subscribe({ - error: () => {}, - }); - - const req = httpTesting.expectOne('api/test'); - req.flush('Unauthorized', { status: 401, statusText: 'Unauthorized' }); - - expect(router.navigate).toHaveBeenCalledWith(['/auth/login']); - }); -}); +import { TestBed } from '@angular/core/testing'; +import { + HttpClient, + HttpInterceptorFn, + provideHttpClient, + withInterceptors, +} from '@angular/common/http'; + +import { authInterceptor } from './auth.interceptor'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { AuthService } from '../../features/auth/data/services/auth.service'; +import { Router } from '@angular/router'; + +describe('authInterceptor', () => { + const interceptor: HttpInterceptorFn = (req, next) => + TestBed.runInInjectionContext(() => authInterceptor(req, next)); + + let httpTesting: HttpTestingController; + let httpClient: HttpClient; + let authService: AuthService; + let router: Router; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(withInterceptors([authInterceptor])), + provideHttpClientTesting(), + { + provide: AuthService, + useValue: { logout: vi.fn() }, + }, + { + provide: Router, + useValue: { navigate: vi.fn() }, + }, + ], + }); + + httpTesting = TestBed.inject(HttpTestingController); + httpClient = TestBed.inject(HttpClient); + authService = TestBed.inject(AuthService); + router = TestBed.inject(Router); + + localStorage.clear(); + }); + + afterEach(() => { + httpTesting.verify(); + }); + + it('should be created', () => { + expect(interceptor).toBeTruthy(); + }); + + it('should add Authorization header when token is present', () => { + const token = 'fake-jwt-token'; + localStorage.setItem('token', token); + + httpClient.get('api/test').subscribe(); + + const req = httpTesting.expectOne('api/test'); + expect(req.request.headers.has('Authorization')).toBe(true); + expect(req.request.headers.get('Authorization')).toBe(`Bearer ${token}`); + }); + + it('should not add Authorization header when token is absent', () => { + httpClient.get('api/test').subscribe(); + + const req = httpTesting.expectOne('api/test'); + expect(req.request.headers.has('Authorization')).toBe(false); + }); + + it('should handle 401 error by logging out', () => { + httpClient.get('api/test').subscribe({ + error: () => {}, + }); + + const req = httpTesting.expectOne('api/test'); + req.flush('Unauthorized', { status: 401, statusText: 'Unauthorized' }); + + expect(authService.logout).toHaveBeenCalled(); + }); + + it('should navigate to login page on 401 error', async () => { + httpClient.get('api/test').subscribe({ + error: () => {}, + }); + + const req = httpTesting.expectOne('api/test'); + req.flush('Unauthorized', { status: 401, statusText: 'Unauthorized' }); + + expect(router.navigate).toHaveBeenCalledWith(['/auth/login']); + }); +}); diff --git a/biblionode-ui/src/app/core/interceptors/auth.interceptor.ts b/biblionode-ui/src/app/core/interceptors/auth.interceptor.ts index eac245c..b4b72be 100644 --- a/biblionode-ui/src/app/core/interceptors/auth.interceptor.ts +++ b/biblionode-ui/src/app/core/interceptors/auth.interceptor.ts @@ -1,29 +1,29 @@ -import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http'; -import { catchError, throwError } from 'rxjs'; -import { inject } from '@angular/core'; -import { Router } from '@angular/router'; -import { AuthService } from '../services/auth.service'; - -export const authInterceptor: HttpInterceptorFn = (req, next) => { - const authService = inject(AuthService); - const router = inject(Router); - const token = localStorage.getItem('token'); - - let clonedReq = req; - if (token) { - clonedReq = req.clone({ - setHeaders: { Authorization: `Bearer ${token}` }, - }); - } - - return next(clonedReq).pipe( - catchError((error: HttpErrorResponse) => { - if (error.status === 401) { - authService.logout(); - localStorage.removeItem('token'); - router.navigate(['/auth/login']); - } - return throwError(() => error); - }), - ); -}; +import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http'; +import { catchError, throwError } from 'rxjs'; +import { inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthService } from '../../features/auth/data/services/auth.service'; + +export const authInterceptor: HttpInterceptorFn = (req, next) => { + const authService = inject(AuthService); + const router = inject(Router); + const token = localStorage.getItem('token'); + + let clonedReq = req; + if (token) { + clonedReq = req.clone({ + setHeaders: { Authorization: `Bearer ${token}` }, + }); + } + + return next(clonedReq).pipe( + catchError((error: HttpErrorResponse) => { + if (error.status === 401) { + authService.logout(); + localStorage.removeItem('token'); + router.navigate(['/auth/login']); + } + return throwError(() => error); + }), + ); +}; diff --git a/biblionode-ui/src/app/core/services/auth.service.spec.ts b/biblionode-ui/src/app/features/auth/data/services/auth.service.spec.ts similarity index 96% rename from biblionode-ui/src/app/core/services/auth.service.spec.ts rename to biblionode-ui/src/app/features/auth/data/services/auth.service.spec.ts index de623f6..29c0e00 100644 --- a/biblionode-ui/src/app/core/services/auth.service.spec.ts +++ b/biblionode-ui/src/app/features/auth/data/services/auth.service.spec.ts @@ -1,143 +1,143 @@ -import { TestBed } from '@angular/core/testing'; - -import { AuthService } from './auth.service'; -import { provideHttpClient } from '@angular/common/http'; -import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; -import { firstValueFrom } from 'rxjs'; - -describe('AuthService', () => { - let service: AuthService; - let httpTesting: HttpTestingController; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [provideHttpClient(), provideHttpClientTesting()], - }); - service = TestBed.inject(AuthService); - httpTesting = TestBed.inject(HttpTestingController); - - let store: Record = {}; - const mockLocalStorage = { - getItem: (key: string) => store[key] || null, - setItem: (key: string, value: string) => { - store[key] = value; - }, - removeItem: (key: string) => { - delete store[key]; - }, - clear: () => { - store = {}; - }, - }; - - vi.spyOn(localStorage, 'getItem').mockImplementation(mockLocalStorage.getItem); - vi.spyOn(localStorage, 'setItem').mockImplementation(mockLocalStorage.setItem); - vi.spyOn(localStorage, 'removeItem').mockImplementation(mockLocalStorage.removeItem); - vi.spyOn(localStorage, 'clear').mockImplementation(mockLocalStorage.clear); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - it('should send correct login request', async () => { - const creds = { email: 'valid@email.com', password: 'password1234' }; - service.login(creds).subscribe(); - - const req = httpTesting.expectOne('api/auth/login', ''); - expect(req.request.method).toBe('POST'); - expect(req.request.body).toEqual(creds); - - req.flush({ token: 'mocked-jwt' }); - - httpTesting.verify(); - }); - - it('should set token on login', async () => { - const loginPromise = firstValueFrom( - service.login({ email: 'valid@email.com', password: 'password1234' }), - ); - - const req = httpTesting.expectOne('api/auth/login', ''); - expect(req.request.method).toBe('POST'); - req.flush({ - token: 'mocked-jwt', - }); - expect(await loginPromise).toEqual({ token: 'mocked-jwt' }); - expect(localStorage.getItem('token')).toBe('mocked-jwt'); - - httpTesting.verify(); - }); - - it('should handle login error', async () => { - const loginPromise = firstValueFrom( - service.login({ email: 'user@email.com', password: 'wrongpassword' }), - ); - const req = httpTesting.expectOne('api/auth/login', ''); - expect(req.request.method).toBe('POST'); - - req.flush({ message: 'Invalid credentials' }, { status: 401, statusText: 'Unauthorized' }); - - await expect(loginPromise).rejects.toThrow(); - expect(localStorage.getItem('token')).toBeNull(); - - httpTesting.verify(); - }); - - it('should remove token on logout', () => { - localStorage.setItem('token', 'mocked-jwt'); - service.logout(); - expect(localStorage.getItem('token')).toBeNull(); - }); - - it('should return authentication status', () => { - const loginPromise = firstValueFrom( - service.login({ email: 'valid@email.com', password: 'password1234' }), - ); - - const req = httpTesting.expectOne('api/auth/login', ''); - expect(req.request.method).toBe('POST'); - req.flush({ - token: 'mocked-jwt', - }); - expect(service.isAuthenticated()).toBe(true); - - service.logout(); - expect(service.isAuthenticated()).toBe(false); - }); - - it('should send correct register request', () => { - const data = { fullName: 'John Doe', email: 'john.doe@email.com', password: 'password1234' }; - service.register(data).subscribe(); - - const req = httpTesting.expectOne('api/auth/register', ''); - expect(req.request.method).toBe('POST'); - expect(req.request.body).toEqual(data); - - req.flush({ id: 1, fullName: 'John Doe', email: 'john.doe@email.com' }); - - httpTesting.verify(); - }); - - it('should handle register error', async () => { - const registerPromise = firstValueFrom( - service.register({ - fullName: 'John Doe', - email: 'john.doe@email.com', - password: 'password1234', - }), - ); - const req = httpTesting.expectOne('api/auth/register', ''); - expect(req.request.method).toBe('POST'); - - req.flush({ message: 'Email already exists' }, { status: 400, statusText: 'Bad Request' }); - - await expect(registerPromise).rejects.toThrow(); - - httpTesting.verify(); - }); -}); +import { TestBed } from '@angular/core/testing'; + +import { AuthService } from './auth.service'; +import { provideHttpClient } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { firstValueFrom } from 'rxjs'; + +describe('AuthService', () => { + let service: AuthService; + let httpTesting: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideHttpClient(), provideHttpClientTesting()], + }); + service = TestBed.inject(AuthService); + httpTesting = TestBed.inject(HttpTestingController); + + let store: Record = {}; + const mockLocalStorage = { + getItem: (key: string) => store[key] || null, + setItem: (key: string, value: string) => { + store[key] = value; + }, + removeItem: (key: string) => { + delete store[key]; + }, + clear: () => { + store = {}; + }, + }; + + vi.spyOn(localStorage, 'getItem').mockImplementation(mockLocalStorage.getItem); + vi.spyOn(localStorage, 'setItem').mockImplementation(mockLocalStorage.setItem); + vi.spyOn(localStorage, 'removeItem').mockImplementation(mockLocalStorage.removeItem); + vi.spyOn(localStorage, 'clear').mockImplementation(mockLocalStorage.clear); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should send correct login request', async () => { + const creds = { email: 'valid@email.com', password: 'password1234' }; + service.login(creds).subscribe(); + + const req = httpTesting.expectOne('api/auth/login', ''); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(creds); + + req.flush({ token: 'mocked-jwt' }); + + httpTesting.verify(); + }); + + it('should set token on login', async () => { + const loginPromise = firstValueFrom( + service.login({ email: 'valid@email.com', password: 'password1234' }), + ); + + const req = httpTesting.expectOne('api/auth/login', ''); + expect(req.request.method).toBe('POST'); + req.flush({ + token: 'mocked-jwt', + }); + expect(await loginPromise).toEqual({ token: 'mocked-jwt' }); + expect(localStorage.getItem('token')).toBe('mocked-jwt'); + + httpTesting.verify(); + }); + + it('should handle login error', async () => { + const loginPromise = firstValueFrom( + service.login({ email: 'user@email.com', password: 'wrongpassword' }), + ); + const req = httpTesting.expectOne('api/auth/login', ''); + expect(req.request.method).toBe('POST'); + + req.flush({ message: 'Invalid credentials' }, { status: 401, statusText: 'Unauthorized' }); + + await expect(loginPromise).rejects.toThrow(); + expect(localStorage.getItem('token')).toBeNull(); + + httpTesting.verify(); + }); + + it('should remove token on logout', () => { + localStorage.setItem('token', 'mocked-jwt'); + service.logout(); + expect(localStorage.getItem('token')).toBeNull(); + }); + + it('should return authentication status', () => { + const loginPromise = firstValueFrom( + service.login({ email: 'valid@email.com', password: 'password1234' }), + ); + + const req = httpTesting.expectOne('api/auth/login', ''); + expect(req.request.method).toBe('POST'); + req.flush({ + token: 'mocked-jwt', + }); + expect(service.isAuthenticated()).toBe(true); + + service.logout(); + expect(service.isAuthenticated()).toBe(false); + }); + + it('should send correct register request', () => { + const data = { fullName: 'John Doe', email: 'john.doe@email.com', password: 'password1234' }; + service.register(data).subscribe(); + + const req = httpTesting.expectOne('api/auth/register', ''); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(data); + + req.flush({ id: 1, fullName: 'John Doe', email: 'john.doe@email.com' }); + + httpTesting.verify(); + }); + + it('should handle register error', async () => { + const registerPromise = firstValueFrom( + service.register({ + fullName: 'John Doe', + email: 'john.doe@email.com', + password: 'password1234', + }), + ); + const req = httpTesting.expectOne('api/auth/register', ''); + expect(req.request.method).toBe('POST'); + + req.flush({ message: 'Email already exists' }, { status: 400, statusText: 'Bad Request' }); + + await expect(registerPromise).rejects.toThrow(); + + httpTesting.verify(); + }); +}); diff --git a/biblionode-ui/src/app/core/services/auth.service.ts b/biblionode-ui/src/app/features/auth/data/services/auth.service.ts similarity index 74% rename from biblionode-ui/src/app/core/services/auth.service.ts rename to biblionode-ui/src/app/features/auth/data/services/auth.service.ts index 6a06dc6..98d55d2 100644 --- a/biblionode-ui/src/app/core/services/auth.service.ts +++ b/biblionode-ui/src/app/features/auth/data/services/auth.service.ts @@ -1,10 +1,10 @@ import { computed, inject, Injectable, signal } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { RegisterRequest } from "../../features/auth/data/dto/register-request.model"; -import { LoginRequest } from "../../features/auth/data/dto/login-request.model"; -import { LoginResponse } from "../../features/auth/data/dto/login-response.model"; +import { RegisterRequest } from "../dto/register-request.model"; +import { LoginRequest } from "../dto/login-request.model"; +import { LoginResponse } from "../dto/login-response.model"; import { catchError, tap } from 'rxjs'; -import { ReaderProfile } from '../../models/reader-profile.model'; +import { ReaderProfile } from '../../../../models/reader-profile.model'; @Injectable({ providedIn: 'root', diff --git a/biblionode-ui/src/app/features/auth/login/login.component.ts b/biblionode-ui/src/app/features/auth/login/login.component.ts index 7689a4c..d32c1e0 100644 --- a/biblionode-ui/src/app/features/auth/login/login.component.ts +++ b/biblionode-ui/src/app/features/auth/login/login.component.ts @@ -1,61 +1,61 @@ -import { Component, inject, signal } from '@angular/core'; -import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; -import { Router } from '@angular/router'; -import { IconInputComponent } from '../../../shared/components/icon-input.component/icon-input.component'; -import { AuthService } from '../../../core/services/auth.service'; - -@Component({ - selector: 'app-login', - imports: [ReactiveFormsModule, IconInputComponent], - templateUrl: './login.component.html', - styleUrl: './login.component.scss', -}) -export class LoginComponent { - private router = inject(Router); - private authService = inject(AuthService); - - loginForm = new FormGroup({ - email: new FormControl('', { - validators: [Validators.required, Validators.email], - nonNullable: true, - }), - password: new FormControl('', { - validators: Validators.required, - nonNullable: true, - }), - }); - - get email() { - return this.loginForm.get('email') as FormControl; - } - get password() { - return this.loginForm.get('password') as FormControl; - } - - passwordVisible = signal(false); - incorrectCredentialsMessage = signal(false); - - signIn() { - if (this.loginForm.valid) { - this.authService.login({ email: this.email.value, password: this.password.value }).subscribe({ - next: () => this.router.navigate(['/']), - error: (err) => { - this.incorrectCredentialsMessage.set(true); - this.loginForm.markAsTouched(); - }, - }); - } - } - - moveToSignUpPage() { - this.router.navigate(['/auth/signup']); - } - - togglePasswordVisibility() { - this.passwordVisible.update((visible) => !visible); - } - - hideIncorrectCredentialsMessage() { - this.incorrectCredentialsMessage.set(false); - } -} +import { Component, inject, signal } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { IconInputComponent } from '../../../shared/components/icon-input.component/icon-input.component'; +import { AuthService } from '../data/services/auth.service'; + +@Component({ + selector: 'app-login', + imports: [ReactiveFormsModule, IconInputComponent], + templateUrl: './login.component.html', + styleUrl: './login.component.scss', +}) +export class LoginComponent { + private router = inject(Router); + private authService = inject(AuthService); + + loginForm = new FormGroup({ + email: new FormControl('', { + validators: [Validators.required, Validators.email], + nonNullable: true, + }), + password: new FormControl('', { + validators: Validators.required, + nonNullable: true, + }), + }); + + get email() { + return this.loginForm.get('email') as FormControl; + } + get password() { + return this.loginForm.get('password') as FormControl; + } + + passwordVisible = signal(false); + incorrectCredentialsMessage = signal(false); + + signIn() { + if (this.loginForm.valid) { + this.authService.login({ email: this.email.value, password: this.password.value }).subscribe({ + next: () => this.router.navigate(['/']), + error: (err) => { + this.incorrectCredentialsMessage.set(true); + this.loginForm.markAsTouched(); + }, + }); + } + } + + moveToSignUpPage() { + this.router.navigate(['/auth/signup']); + } + + togglePasswordVisibility() { + this.passwordVisible.update((visible) => !visible); + } + + hideIncorrectCredentialsMessage() { + this.incorrectCredentialsMessage.set(false); + } +} diff --git a/biblionode-ui/src/app/features/auth/login/login.stories.ts b/biblionode-ui/src/app/features/auth/login/login.stories.ts index cad2653..2e1cb41 100644 --- a/biblionode-ui/src/app/features/auth/login/login.stories.ts +++ b/biblionode-ui/src/app/features/auth/login/login.stories.ts @@ -2,7 +2,7 @@ import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; import { LoginComponent } from './login.component'; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { signal } from '@angular/core'; -import { AuthService } from '../../../core/services/auth.service'; +import { AuthService } from '../data/services/auth.service'; import { of } from 'rxjs'; import { Router } from '@angular/router'; diff --git a/biblionode-ui/src/app/features/auth/register/register.component.ts b/biblionode-ui/src/app/features/auth/register/register.component.ts index 9f7bb9d..5f07214 100644 --- a/biblionode-ui/src/app/features/auth/register/register.component.ts +++ b/biblionode-ui/src/app/features/auth/register/register.component.ts @@ -1,80 +1,80 @@ -import { Component, inject, signal } from '@angular/core'; -import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; -import { Router } from '@angular/router'; -import { AuthService } from '../../../core/services/auth.service'; -import { IconInputComponent } from '../../../shared/components/icon-input.component/icon-input.component'; -import { AuthValidators } from '../../../core/validators/auth.validators'; - -@Component({ - selector: 'app-register', - imports: [ReactiveFormsModule, IconInputComponent], - templateUrl: './register.component.html', - styleUrl: './register.component.scss', -}) -export class RegisterComponent { - private router = inject(Router); - private authService = inject(AuthService); - - registerForm = new FormGroup( - { - fullName: new FormControl('', { - validators: Validators.required, - nonNullable: true, - }), - email: new FormControl('', { - validators: [Validators.required, Validators.email], - nonNullable: true, - }), - password: new FormControl('', { - validators: [Validators.required, Validators.minLength(8), AuthValidators.strongPassword()], - nonNullable: true, - }), - confirmPassword: new FormControl('', { - validators: [Validators.required, Validators.minLength(8)], - nonNullable: true, - }), - }, - { - validators: AuthValidators.passwordMatch, - }, - ); - - passwordVisible = signal(false); - isLoading = signal(false); - - get fullName() { - return this.registerForm.get('fullName') as FormControl; - } - get email() { - return this.registerForm.get('email') as FormControl; - } - get password() { - return this.registerForm.get('password') as FormControl; - } - get confirmPassword() { - return this.registerForm.get('confirmPassword') as FormControl; - } - - onSubmit() { - if (this.registerForm.valid) { - this.isLoading.set(true); - this.authService - .register({ - fullName: this.fullName.value, - email: this.email.value, - password: this.password.value, - }) - .subscribe({ - next: () => this.router.navigate(['/auth/login']), - error: (err) => { - console.error('Registration failed', err); - this.isLoading.set(false); - }, - }); - } - } - - moveToSignInPage() { - this.router.navigate(['/auth/login']); - } -} +import { Component, inject, signal } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { AuthService } from '../data/services/auth.service'; +import { IconInputComponent } from '../../../shared/components/icon-input.component/icon-input.component'; +import { AuthValidators } from '../../../core/validators/auth.validators'; + +@Component({ + selector: 'app-register', + imports: [ReactiveFormsModule, IconInputComponent], + templateUrl: './register.component.html', + styleUrl: './register.component.scss', +}) +export class RegisterComponent { + private router = inject(Router); + private authService = inject(AuthService); + + registerForm = new FormGroup( + { + fullName: new FormControl('', { + validators: Validators.required, + nonNullable: true, + }), + email: new FormControl('', { + validators: [Validators.required, Validators.email], + nonNullable: true, + }), + password: new FormControl('', { + validators: [Validators.required, Validators.minLength(8), AuthValidators.strongPassword()], + nonNullable: true, + }), + confirmPassword: new FormControl('', { + validators: [Validators.required, Validators.minLength(8)], + nonNullable: true, + }), + }, + { + validators: AuthValidators.passwordMatch, + }, + ); + + passwordVisible = signal(false); + isLoading = signal(false); + + get fullName() { + return this.registerForm.get('fullName') as FormControl; + } + get email() { + return this.registerForm.get('email') as FormControl; + } + get password() { + return this.registerForm.get('password') as FormControl; + } + get confirmPassword() { + return this.registerForm.get('confirmPassword') as FormControl; + } + + onSubmit() { + if (this.registerForm.valid) { + this.isLoading.set(true); + this.authService + .register({ + fullName: this.fullName.value, + email: this.email.value, + password: this.password.value, + }) + .subscribe({ + next: () => this.router.navigate(['/auth/login']), + error: (err) => { + console.error('Registration failed', err); + this.isLoading.set(false); + }, + }); + } + } + + moveToSignInPage() { + this.router.navigate(['/auth/login']); + } +} diff --git a/biblionode-ui/src/app/features/auth/register/register.stories.ts b/biblionode-ui/src/app/features/auth/register/register.stories.ts index 1117785..e4f0865 100644 --- a/biblionode-ui/src/app/features/auth/register/register.stories.ts +++ b/biblionode-ui/src/app/features/auth/register/register.stories.ts @@ -1,27 +1,27 @@ -import { RegisterComponent } from './register.component'; -import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; -import { ReactiveFormsModule } from '@angular/forms'; -import { AuthService } from '../../../core/services/auth.service'; -import { Router } from '@angular/router'; -import { of } from 'rxjs'; - -const meta: Meta = { - title: 'Features/Auth/Register', - component: RegisterComponent, - tags: ['autodocs'], - parameters: { layout: 'centered' }, - decorators: [ - moduleMetadata({ - imports: [ReactiveFormsModule], - providers: [ - { provide: AuthService, useValue: { register: () => of({}) } }, - { provide: Router, useValue: { navigate: () => {} } }, - ], - }), - ], -}; -export default meta; - -type Story = StoryObj; - -export const Default: Story = {}; +import { RegisterComponent } from './register.component'; +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { ReactiveFormsModule } from '@angular/forms'; +import { AuthService } from '../data/services/auth.service'; +import { Router } from '@angular/router'; +import { of } from 'rxjs'; + +const meta: Meta = { + title: 'Features/Auth/Register', + component: RegisterComponent, + tags: ['autodocs'], + parameters: { layout: 'centered' }, + decorators: [ + moduleMetadata({ + imports: [ReactiveFormsModule], + providers: [ + { provide: AuthService, useValue: { register: () => of({}) } }, + { provide: Router, useValue: { navigate: () => {} } }, + ], + }), + ], +}; +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; From 2c8a17bdb053d4bba23d240e9aec206516353a67 Mon Sep 17 00:00:00 2001 From: mgrablo <51509107+mgrablo@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:46:58 +0200 Subject: [PATCH 3/4] refactor(ui): move all shared models to shared/models directory loan and reader-profile models are currently only used by auth feature but they will be used by more features in the future --- .../auth/data/services/auth.service.ts | 2 +- .../books/data-access/book.service.ts | 34 +++++++++---------- .../src/app/{ => shared}/models/loan.model.ts | 22 ++++++------ .../app/{core => shared}/models/page.model.ts | 22 ++++++------ .../models/reader-profile.model.ts | 16 ++++----- 5 files changed, 48 insertions(+), 48 deletions(-) rename biblionode-ui/src/app/{ => shared}/models/loan.model.ts (95%) rename biblionode-ui/src/app/{core => shared}/models/page.model.ts (94%) rename biblionode-ui/src/app/{ => shared}/models/reader-profile.model.ts (94%) diff --git a/biblionode-ui/src/app/features/auth/data/services/auth.service.ts b/biblionode-ui/src/app/features/auth/data/services/auth.service.ts index 98d55d2..3aadec9 100644 --- a/biblionode-ui/src/app/features/auth/data/services/auth.service.ts +++ b/biblionode-ui/src/app/features/auth/data/services/auth.service.ts @@ -4,7 +4,7 @@ import { RegisterRequest } from "../dto/register-request.model"; import { LoginRequest } from "../dto/login-request.model"; import { LoginResponse } from "../dto/login-response.model"; import { catchError, tap } from 'rxjs'; -import { ReaderProfile } from '../../../../models/reader-profile.model'; +import { ReaderProfile } from '../../../../shared/models/reader-profile.model'; @Injectable({ providedIn: 'root', diff --git a/biblionode-ui/src/app/features/books/data-access/book.service.ts b/biblionode-ui/src/app/features/books/data-access/book.service.ts index c19aa33..8cf69c8 100644 --- a/biblionode-ui/src/app/features/books/data-access/book.service.ts +++ b/biblionode-ui/src/app/features/books/data-access/book.service.ts @@ -1,17 +1,17 @@ -import { inject, Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { BookResponse } from './dto/book-response'; -import { Page } from '../../../core/models/page.model'; - -@Injectable({ providedIn: 'root' }) -export class BookService { - private http = inject(HttpClient); - - getAllBooks() { - return this.http.get>('api/books'); - } - - getBookById(id: number) { - return this.http.get(`api/books/${id}`); - } -} +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BookResponse } from './dto/book-response'; +import { Page } from '../../../shared/models/page.model'; + +@Injectable({ providedIn: 'root' }) +export class BookService { + private http = inject(HttpClient); + + getAllBooks() { + return this.http.get>('api/books'); + } + + getBookById(id: number) { + return this.http.get(`api/books/${id}`); + } +} diff --git a/biblionode-ui/src/app/models/loan.model.ts b/biblionode-ui/src/app/shared/models/loan.model.ts similarity index 95% rename from biblionode-ui/src/app/models/loan.model.ts rename to biblionode-ui/src/app/shared/models/loan.model.ts index 841d3a2..510961a 100644 --- a/biblionode-ui/src/app/models/loan.model.ts +++ b/biblionode-ui/src/app/shared/models/loan.model.ts @@ -1,11 +1,11 @@ -export interface Loan { - id: number; - bookId: number; - bookTitle: string; - bookAuthorName: string; - bookIsbn: string; - readerId: number; - loanDate: string; - dueDate: string; - returnDate: string | null; -} +export interface Loan { + id: number; + bookId: number; + bookTitle: string; + bookAuthorName: string; + bookIsbn: string; + readerId: number; + loanDate: string; + dueDate: string; + returnDate: string | null; +} diff --git a/biblionode-ui/src/app/core/models/page.model.ts b/biblionode-ui/src/app/shared/models/page.model.ts similarity index 94% rename from biblionode-ui/src/app/core/models/page.model.ts rename to biblionode-ui/src/app/shared/models/page.model.ts index 20ebe98..d626e9f 100644 --- a/biblionode-ui/src/app/core/models/page.model.ts +++ b/biblionode-ui/src/app/shared/models/page.model.ts @@ -1,11 +1,11 @@ -export interface Page { - content: T[]; - empty: boolean; - first: boolean; - last: boolean; - number: number; - numberOfElements: number; - size: number; - totalElements: number; - totalPages: number; -} +export interface Page { + content: T[]; + empty: boolean; + first: boolean; + last: boolean; + number: number; + numberOfElements: number; + size: number; + totalElements: number; + totalPages: number; +} diff --git a/biblionode-ui/src/app/models/reader-profile.model.ts b/biblionode-ui/src/app/shared/models/reader-profile.model.ts similarity index 94% rename from biblionode-ui/src/app/models/reader-profile.model.ts rename to biblionode-ui/src/app/shared/models/reader-profile.model.ts index c9b3a57..685f8be 100644 --- a/biblionode-ui/src/app/models/reader-profile.model.ts +++ b/biblionode-ui/src/app/shared/models/reader-profile.model.ts @@ -1,8 +1,8 @@ -import { Loan } from './loan.model'; - -export interface ReaderProfile { - id: number; - fullName: string; - email: string; - loans: Loan[]; -} +import { Loan } from './loan.model'; + +export interface ReaderProfile { + id: number; + fullName: string; + email: string; + loans: Loan[]; +} From 750995cb2fa8a8bb9e231ab44bc683e16e953803 Mon Sep 17 00:00:00 2001 From: mgrablo <51509107+mgrablo@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:49:19 +0200 Subject: [PATCH 4/4] refactor(ui/auth): move auth validator to features/auth/validators --- .../src/app/features/auth/register/register.component.ts | 2 +- .../app/{core => features/auth}/validators/auth.validators.ts | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename biblionode-ui/src/app/{core => features/auth}/validators/auth.validators.ts (100%) diff --git a/biblionode-ui/src/app/features/auth/register/register.component.ts b/biblionode-ui/src/app/features/auth/register/register.component.ts index 5f07214..59e2e9c 100644 --- a/biblionode-ui/src/app/features/auth/register/register.component.ts +++ b/biblionode-ui/src/app/features/auth/register/register.component.ts @@ -3,7 +3,7 @@ import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angula import { Router } from '@angular/router'; import { AuthService } from '../data/services/auth.service'; import { IconInputComponent } from '../../../shared/components/icon-input.component/icon-input.component'; -import { AuthValidators } from '../../../core/validators/auth.validators'; +import { AuthValidators } from '../validators/auth.validators'; @Component({ selector: 'app-register', diff --git a/biblionode-ui/src/app/core/validators/auth.validators.ts b/biblionode-ui/src/app/features/auth/validators/auth.validators.ts similarity index 100% rename from biblionode-ui/src/app/core/validators/auth.validators.ts rename to biblionode-ui/src/app/features/auth/validators/auth.validators.ts