feat(web): implement auth service
This commit is contained in:
@@ -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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user