forked from mirrors/homebox
setup api client
This commit is contained in:
parent
c780a0d3ac
commit
9c1cced576
8 changed files with 227 additions and 0 deletions
17
frontend/src/api/base/base-api.ts
Normal file
17
frontend/src/api/base/base-api.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { Requests } from '../../lib/requests';
|
||||||
|
// <
|
||||||
|
// TGetResult,
|
||||||
|
// TPostData,
|
||||||
|
// TPostResult,
|
||||||
|
// TPutData = TPostData,
|
||||||
|
// TPutResult = TPostResult,
|
||||||
|
// TDeleteResult = void
|
||||||
|
// >
|
||||||
|
|
||||||
|
export class BaseAPI {
|
||||||
|
http: Requests;
|
||||||
|
|
||||||
|
constructor(requests: Requests) {
|
||||||
|
this.http = requests;
|
||||||
|
}
|
||||||
|
}
|
24
frontend/src/api/base/index.test.ts
Normal file
24
frontend/src/api/base/index.test.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { UrlBuilder } from '.';
|
||||||
|
|
||||||
|
describe('UrlBuilder', () => {
|
||||||
|
it('basic query parameter', () => {
|
||||||
|
const result = UrlBuilder('/test', { a: 'b' });
|
||||||
|
expect(result).toBe('/api/v1/test?a=b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multiple query parameters', () => {
|
||||||
|
const result = UrlBuilder('/test', { a: 'b', c: 'd' });
|
||||||
|
expect(result).toBe('/api/v1/test?a=b&c=d');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('no query parameters', () => {
|
||||||
|
const result = UrlBuilder('/test');
|
||||||
|
expect(result).toBe('/api/v1/test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('list-like query parameters', () => {
|
||||||
|
const result = UrlBuilder('/test', { a: ['b', 'c'] });
|
||||||
|
expect(result).toBe('/api/v1/test?a=b&a=c');
|
||||||
|
});
|
||||||
|
});
|
2
frontend/src/api/base/index.ts
Normal file
2
frontend/src/api/base/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { BaseAPI } from './base-api';
|
||||||
|
export { UrlBuilder } from './urls';
|
31
frontend/src/api/base/urls.ts
Normal file
31
frontend/src/api/base/urls.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
export const prefix = '/api/v1';
|
||||||
|
|
||||||
|
export type QueryValue =
|
||||||
|
| string
|
||||||
|
| string[]
|
||||||
|
| number
|
||||||
|
| number[]
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
export function UrlBuilder(
|
||||||
|
rest: string,
|
||||||
|
params: Record<string, QueryValue> = {}
|
||||||
|
): string {
|
||||||
|
// we use a stub base URL to leverage the URL class
|
||||||
|
const url = new URL(prefix + rest, 'http://localhost.com');
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of value) {
|
||||||
|
url.searchParams.append(key, String(item));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
url.searchParams.append(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we return the path only, without the base URL
|
||||||
|
return url.toString().replace('http://localhost.com', '');
|
||||||
|
}
|
39
frontend/src/api/public.ts
Normal file
39
frontend/src/api/public.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { BaseAPI, UrlBuilder } from './base';
|
||||||
|
|
||||||
|
export type LoginResult = {
|
||||||
|
token: string;
|
||||||
|
expiresAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LoginPayload = {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RegisterPayload = {
|
||||||
|
user: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
groupName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PublicApi extends BaseAPI {
|
||||||
|
public login(username: string, password: string) {
|
||||||
|
return this.http.post<LoginPayload, LoginResult>(
|
||||||
|
UrlBuilder('/users/login'),
|
||||||
|
{
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public register(payload: RegisterPayload) {
|
||||||
|
return this.http.post<RegisterPayload, LoginResult>(
|
||||||
|
UrlBuilder('/users/register'),
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
18
frontend/src/api/user.ts
Normal file
18
frontend/src/api/user.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { BaseAPI, UrlBuilder } from './base';
|
||||||
|
|
||||||
|
export type Result<T> = {
|
||||||
|
item: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
isSuperuser: boolean;
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class UserApi extends BaseAPI {
|
||||||
|
public self() {
|
||||||
|
return this.http.get<Result<User>>(UrlBuilder('/users/self'));
|
||||||
|
}
|
||||||
|
}
|
1
frontend/src/lib/requests/index.ts
Normal file
1
frontend/src/lib/requests/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { Requests, type TResponse } from './requests';
|
95
frontend/src/lib/requests/requests.ts
Normal file
95
frontend/src/lib/requests/requests.ts
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
export enum Method {
|
||||||
|
GET = 'GET',
|
||||||
|
POST = 'POST',
|
||||||
|
PUT = 'PUT',
|
||||||
|
DELETE = 'DELETE',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TResponse<T> {
|
||||||
|
status: number;
|
||||||
|
error: boolean;
|
||||||
|
data: T;
|
||||||
|
response: Response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Requests {
|
||||||
|
private baseUrl: string;
|
||||||
|
private token: () => string;
|
||||||
|
private headers: Record<string, string> = {};
|
||||||
|
private logger?: (response: Response) => void;
|
||||||
|
|
||||||
|
private url(rest: string): string {
|
||||||
|
return this.baseUrl + rest;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
baseUrl: string,
|
||||||
|
token: string | (() => string) = '',
|
||||||
|
headers: Record<string, string> = {},
|
||||||
|
logger?: (response: Response) => void
|
||||||
|
) {
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
this.token = typeof token === 'string' ? () => token : token;
|
||||||
|
this.headers = headers;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get<T>(url: string): Promise<TResponse<T>> {
|
||||||
|
return this.do<T>(Method.GET, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public post<T, U>(url: string, payload: T): Promise<TResponse<U>> {
|
||||||
|
return this.do<U>(Method.POST, url, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
public put<T, U>(url: string, payload: T): Promise<TResponse<U>> {
|
||||||
|
return this.do<U>(Method.PUT, url, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delete<T>(url: string): Promise<TResponse<T>> {
|
||||||
|
return this.do<T>(Method.DELETE, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
private methodSupportsBody(method: Method): boolean {
|
||||||
|
return method === Method.POST || method === Method.PUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async do<T>(
|
||||||
|
method: Method,
|
||||||
|
url: string,
|
||||||
|
payload: Object = {}
|
||||||
|
): Promise<TResponse<T>> {
|
||||||
|
const args: RequestInit = {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...this.headers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const token = this.token();
|
||||||
|
if (token !== '' && args.headers !== undefined) {
|
||||||
|
// @ts-expect-error -- headers is always defined at this point
|
||||||
|
args.headers['Authorization'] = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.methodSupportsBody(method)) {
|
||||||
|
args.body = JSON.stringify(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(this.url(url), args);
|
||||||
|
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
error: !response.ok,
|
||||||
|
data,
|
||||||
|
response,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue