From 12cf9f1e9526c848fa4b7d252d9fa49f25c47921 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 18 Feb 2026 06:00:27 -0600 Subject: [PATCH] feat(web): implement auth service --- src/FiscalOS.Web/src/services/authService.ts | 112 +++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 src/FiscalOS.Web/src/services/authService.ts diff --git a/src/FiscalOS.Web/src/services/authService.ts b/src/FiscalOS.Web/src/services/authService.ts new file mode 100644 index 0000000..b97c27a --- /dev/null +++ b/src/FiscalOS.Web/src/services/authService.ts @@ -0,0 +1,112 @@ +import { Err, Ok, Result } from 'ts-results'; +import { type InjectionKey } from 'vue'; +import { ClientRequestWithBody, type IClient } from './client'; + +type AuthServiceFactoryKeyType = InjectionKey; + +export const AuthServiceFactoryKey: AuthServiceFactoryKeyType = Symbol('AuthServiceFactory'); + +export interface IAuthServiceFactory { + create: (client: IClient) => IAuthService; +} + +export class AuthServiceFactory implements IAuthServiceFactory { + create(client: IClient): IAuthService { + return new AuthService(client); + } +} + +export interface IAuthService { + login: (username: string, password: string) => Promise>; + refreshToken: () => Promise>; +} + +export class AuthService implements IAuthService { + private readonly client: IClient; + private readonly endpoints = { + login: '/api/auth/login', + refreshToken: '/api/auth/refresh', + }; + + constructor(client: IClient) { + this.client = client; + } + + async login(username: string, password: string) { + const request = new ClientRequestWithBody( + this.endpoints.login, + undefined, + new LoginRequest(username, password) + ); + + try { + const res = await this.client.post(request); + + if (res.status === 400) { + const body = await res.json(); + const validationErrors = body.errors as Record; + const errors = Object.values(validationErrors) + .flat() + .map(e => new Error(e)); + + return Err(errors); + } + + if (res.status === 401) { + return Err([new Error('Email/Password combination is not valid')]); + } + + if (res.ok === false) { + return Err([new Error('Login failed. Please try again.')]); + } + + const body = await res.json(); + return Ok(body as LoginResponse); + } catch (e) { + console.error(e); + return Err([new Error('Login failed. Please try again.')]); + } + } + + async refreshToken() { + const request = new ClientRequestWithBody(this.endpoints.refreshToken, undefined, undefined); + + try { + const res = await this.client.post(request); + + if (res.status === 401) { + return Err([new Error('Refresh and/or access token is not valid')]); + } + + if (res.ok === false) { + return Err([new Error('Refreshing token failed')]); + } + + const body = await res.json(); + return Ok(body as LoginResponse); + } catch (e) { + console.error(e); + return Err([new Error('Refreshing token failed.')]); + } + } +} + +class BaseAuthRequest { + readonly username: string; + readonly password: string; + + constructor(username: string, password: string) { + this.username = username; + this.password = password; + } +} + +class LoginRequest extends BaseAuthRequest { + constructor(username: string, password: string) { + super(username, password); + } +} + +type LoginResponse = { + accessToken: string; +};