feat(web): implement auth service

This commit is contained in:
Stevan Freeborn
2026-02-18 06:00:27 -06:00
parent 417df002a0
commit 12cf9f1e95
@@ -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<IAuthServiceFactory>;
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<Result<LoginResponse, Error[]>>;
refreshToken: () => Promise<Result<LoginResponse, Error[]>>;
}
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<string, string[]>;
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;
};