aboutsummaryrefslogtreecommitdiffstats
path: root/code/app/src/lib/api
diff options
context:
space:
mode:
authorivarlovlie <git@ivarlovlie.no>2022-10-25 11:51:37 +0200
committerivarlovlie <git@ivarlovlie.no>2022-10-25 11:51:37 +0200
commit0005595703b2f3f7083ce4ba19bf5770057c75bd (patch)
tree193a897f61a9a5e566961601de4cf42ae85984a0 /code/app/src/lib/api
parent585c5c8537eb21dfc9f16108548e63d9ced3d971 (diff)
downloadgreatoffice-0005595703b2f3f7083ce4ba19bf5770057c75bd.tar.xz
greatoffice-0005595703b2f3f7083ce4ba19bf5770057c75bd.zip
.
Diffstat (limited to 'code/app/src/lib/api')
-rw-r--r--code/app/src/lib/api/_fetch.ts121
-rw-r--r--code/app/src/lib/api/account/index.ts39
-rw-r--r--code/app/src/lib/api/account/models/CreateAccountPayload.ts4
-rw-r--r--code/app/src/lib/api/account/models/LoginPayload.ts5
-rw-r--r--code/app/src/lib/api/account/models/UpdateProfilePayload.ts4
-rw-r--r--code/app/src/lib/api/internal-fetch.ts170
-rw-r--r--code/app/src/lib/api/password-reset-request/index.ts17
-rw-r--r--code/app/src/lib/api/root.ts12
-rw-r--r--code/app/src/lib/api/time-entry.ts83
-rw-r--r--code/app/src/lib/api/user.ts47
10 files changed, 199 insertions, 303 deletions
diff --git a/code/app/src/lib/api/_fetch.ts b/code/app/src/lib/api/_fetch.ts
new file mode 100644
index 0000000..b28e398
--- /dev/null
+++ b/code/app/src/lib/api/_fetch.ts
@@ -0,0 +1,121 @@
+import { Temporal } from "temporal-polyfill";
+import { clear_session_data } from "$lib/session";
+import type { Result } from "rustic";
+import { Err, Ok } from "rustic";
+import { redirect } from "@sveltejs/kit";
+import { browser } from "$app/environment";
+import { goto } from "$app/navigation";
+import { SignInPageMessage, signInPageMessageQueryKey } from "$routes/(main)/(public)/sign-in";
+import { log_error } from "$lib/logger";
+
+export async function http_post_async<T>(url: string, body?: object | string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise<InternalFetchResponse<T>> {
+ const init = make_request_init("post", body, abort_signal);
+ const response = await internal_fetch_async({ url, init, timeout });
+ if (!skip_401_check && await redirect_if_401_async(response)) return Err("Server returned 401");
+ return make_response_async(response);
+}
+
+export async function http_get_async<T>(url: string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise<Result<InternalFetchResponse<T>, string>> {
+ const init = make_request_init("get", undefined, abort_signal);
+ const response = await internal_fetch_async({ url, init, timeout });
+ if (!skip_401_check && await redirect_if_401_async(response)) return Err("Server returned 401");
+ return make_response_async(response);
+}
+
+export async function http_delete_async<T>(url: string, body?: object | string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise<Result<InternalFetchResponse<T>, string>> {
+ const init = make_request_init("delete", body, abort_signal);
+ const response = await internal_fetch_async({ url, init, timeout });
+ if (!skip_401_check && await redirect_if_401_async(response)) return Err("Server returned 401");
+ return make_response_async(response);
+}
+
+async function internal_fetch_async(request: InternalFetchRequest): Promise<Response> {
+ if (!request.init) throw new Error("request.init is required");
+ const fetch_request = new Request(request.url, request.init);
+ let response: any;
+
+ try {
+ if (request.timeout && request.timeout > 500) {
+ response = await Promise.race([
+ fetch(fetch_request),
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), request.timeout))
+ ]);
+ } else {
+ response = await fetch(fetch_request);
+ }
+ } catch (error: any) {
+ log_error(error);
+ if (error.message === "Timeout") {
+ console.error("Request timed out");
+ } else if (error.message === "Network request failed") {
+ console.error("No internet connection");
+ } else {
+ throw error;
+ }
+ }
+
+ return response;
+}
+
+async function redirect_if_401_async(response: Response): Promise<boolean> {
+ if (response.status === 401) {
+ const redirectUrl = `/sign-in?${signInPageMessageQueryKey}=${SignInPageMessage.LOGGED_OUT}`;
+ clear_session_data();
+ if (browser) {
+ await goto(redirectUrl)
+ } else {
+ throw redirect(307, redirectUrl);
+ }
+ }
+ return false;
+}
+
+async function make_response_async<T>(response: Response): Promise<Result<InternalFetchResponse<T>, string>> {
+ const result = {
+ ok: response.ok,
+ status: response.status,
+ http_response: response,
+ } as InternalFetchResponse<T>;
+
+ if (response.status !== 204) {
+ try {
+ result.data = await response.json() as T;
+ } catch (error) {
+ log_error("", { error, result })
+ return Err("Deserialisation threw");
+ }
+ }
+ return Ok(result);
+}
+
+function make_request_init(method: string, body?: any, signal?: AbortSignal): RequestInit {
+ const init = {
+ method,
+ signal,
+ headers: {
+ "X-TimeZone": Temporal.Now.timeZone().id,
+ }
+ } as RequestInit;
+
+ if (body) {
+ init.body = JSON.stringify(body);
+ init.headers["Content-Type"] = "application/json;charset=UTF-8"
+ }
+
+ return init;
+}
+
+
+export type InternalFetchRequest = {
+ url: string,
+ init: RequestInit,
+ timeout?: number
+ retry_count?: number,
+}
+
+export type InternalFetchResponse<T> = {
+ ok: boolean,
+ status: number,
+ data: T | undefined,
+ http_response: Response
+} \ No newline at end of file
diff --git a/code/app/src/lib/api/account/index.ts b/code/app/src/lib/api/account/index.ts
new file mode 100644
index 0000000..305bd9f
--- /dev/null
+++ b/code/app/src/lib/api/account/index.ts
@@ -0,0 +1,39 @@
+import { api_base } from "$lib/configuration";
+import { http_delete_async, http_get_async, http_post_async, type InternalFetchResponse } from "../_fetch";
+import type { LoginPayload } from "$lib/api/account/models/LoginPayload";
+import type { Result } from "rustic";
+import { isOk, Ok, Err } from "rustic";
+import type { SessionData } from "$lib/models/base/SessionData";
+import type { CreateAccountPayload } from "./models/CreateAccountPayload";
+import type { UpdateProfilePayload } from "./models/UpdateProfilePayload";
+import type { ErrorResult } from "$lib/models/internal/ErrorResult";
+
+export const http_account = {
+ async login_async(payload: LoginPayload): Promise<InternalFetchResponse<Result<void, ErrorResult>>> {
+ const response = await http_post_async<Result<void, ErrorResult>>(api_base("_/account/login"), payload);
+ if (isOk(response)) {
+ return Ok();
+ }
+ return Err(response.data);
+ },
+ logout_async(): Promise<InternalFetchResponse<void>> {
+ return http_get_async<void>(api_base("_/account/logout"));
+ },
+ delete_account_async(): Promise<InternalFetchResponse> {
+ return http_delete_async(api_base("_/account/delete"));
+ },
+ update_profile_async(payload: UpdateProfilePayload): Promise<InternalFetchResponse> {
+ if (!payload.password && !payload.username) throw new Error("Password and Username is empty");
+ return http_post_async(api_base("_/account/update"), payload);
+ },
+ create_account_async(payload: CreateAccountPayload): Promise<InternalFetchResponse> {
+ if (!payload.password && !payload.username) throw new Error("Password and Username is empty");
+ return http_post_async(api_base("_/account/create"), payload);
+ },
+ async get_profile_async(suppress_401: boolean): Promise<Result<SessionData, string>> {
+ const response = await http_get_async<SessionData>(api_base("_/account"), 0, true);
+ if (isOk(response)) {
+ return Ok(response.data.data);
+ }
+ }
+} \ No newline at end of file
diff --git a/code/app/src/lib/api/account/models/CreateAccountPayload.ts b/code/app/src/lib/api/account/models/CreateAccountPayload.ts
new file mode 100644
index 0000000..d116308
--- /dev/null
+++ b/code/app/src/lib/api/account/models/CreateAccountPayload.ts
@@ -0,0 +1,4 @@
+export interface CreateAccountPayload {
+ username: string,
+ password: string
+}
diff --git a/code/app/src/lib/api/account/models/LoginPayload.ts b/code/app/src/lib/api/account/models/LoginPayload.ts
new file mode 100644
index 0000000..beb96cf
--- /dev/null
+++ b/code/app/src/lib/api/account/models/LoginPayload.ts
@@ -0,0 +1,5 @@
+export interface LoginPayload {
+ username: string,
+ password: string,
+ persist: boolean
+}
diff --git a/code/app/src/lib/api/account/models/UpdateProfilePayload.ts b/code/app/src/lib/api/account/models/UpdateProfilePayload.ts
new file mode 100644
index 0000000..d2983ff
--- /dev/null
+++ b/code/app/src/lib/api/account/models/UpdateProfilePayload.ts
@@ -0,0 +1,4 @@
+export interface UpdateProfilePayload {
+ username?: string,
+ password?: string,
+}
diff --git a/code/app/src/lib/api/internal-fetch.ts b/code/app/src/lib/api/internal-fetch.ts
deleted file mode 100644
index b21d669..0000000
--- a/code/app/src/lib/api/internal-fetch.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-import { Temporal } from "temporal-polyfill";
-import { clear_session_data } from "$lib/session";
-import { resolve_references } from "$lib/helpers";
-import type { IInternalFetchResponse } from "$lib/models/IInternalFetchResponse";
-import type { IInternalFetchRequest } from "$lib/models/IInternalFetchRequest";
-import { redirect } from "@sveltejs/kit";
-
-export async function http_post(url: string, body?: object | string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise<IInternalFetchResponse> {
- const init = {
- method: "post",
- } as RequestInit;
-
- if (abort_signal) {
- init.signal = abort_signal;
- }
-
- if (body) {
- init.headers = {
- "Content-Type": "application/json;charset=UTF-8",
- };
- init.body = JSON.stringify(body);
- }
-
- const response = await internal_fetch({ url, init, timeout });
- const result = {} as IInternalFetchResponse;
-
- if (!skip_401_check && await is_401(response)) return result;
-
- result.ok = response.ok;
- result.status = response.status;
- result.http_response = response;
-
- if (response.status !== 204) {
- try {
- const ct = response.headers.get("Content-Type")?.toString() ?? "";
- if (ct.startsWith("application/json")) {
- const data = await response.json();
- result.data = resolve_references(data);
- } else if (ct.startsWith("text/plain")) {
- const text = await response.text();
- result.data = text as string;
- }
- } catch {
- // Ignored
- }
- }
-
- return result;
-}
-
-export async function http_get(url: string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise<IInternalFetchResponse> {
- const init = {
- method: "get",
- } as RequestInit;
-
- if (abort_signal) {
- init.signal = abort_signal;
- }
-
- const response = await internal_fetch({ url, init, timeout });
- const result = {} as IInternalFetchResponse;
-
- if (!skip_401_check && await is_401(response)) return result;
-
- result.ok = response.ok;
- result.status = response.status;
- result.http_response = response;
-
- if (response.status !== 204) {
- try {
- const ct = response.headers.get("Content-Type")?.toString() ?? "";
- if (ct.startsWith("application/json")) {
- const data = await response.json();
- result.data = resolve_references(data);
- } else if (ct.startsWith("text/plain")) {
- const text = await response.text();
- result.data = text as string;
- }
- } catch {
- // Ignored
- }
- }
-
- return result;
-}
-
-export async function http_delete(url: string, body?: object | string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise<IInternalFetchResponse> {
- const init = {
- method: "delete",
- } as RequestInit;
-
- if (abort_signal) {
- init.signal = abort_signal;
- }
-
- if (body) {
- init.headers = {
- "Content-Type": "application/json;charset=UTF-8",
- };
- init.body = JSON.stringify(body);
- }
-
- const response = await internal_fetch({ url, init, timeout });
- const result = {} as IInternalFetchResponse;
-
- if (!skip_401_check && await is_401(response)) return result;
-
- result.ok = response.ok;
- result.status = response.status;
- result.http_response = response;
-
- if (response.status !== 204) {
- try {
- const ct = response.headers.get("Content-Type")?.toString() ?? "";
- if (ct.startsWith("application/json")) {
- const data = await response.json();
- result.data = resolve_references(data);
- } else if (ct.startsWith("text/plain")) {
- const text = await response.text();
- result.data = text as string;
- }
- } catch (error) {
- // ignored
- }
- }
-
- return result;
-}
-
-async function internal_fetch(request: IInternalFetchRequest): Promise<Response> {
- if (!request.init) request.init = {};
- request.init.credentials = "include";
- request.init.headers = {
- "X-TimeZone": Temporal.Now.timeZone().id,
- ...request.init.headers
- };
-
- const fetch_request = new Request(request.url, request.init);
- let response: any;
-
- try {
- if (request.timeout && request.timeout > 500) {
- response = await Promise.race([
- fetch(fetch_request),
- new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), request.timeout))
- ]);
- } else {
- response = await fetch(fetch_request);
- }
- } catch (error: any) {
- console.log(error);
- if (error.message === "Timeout") {
- console.error("Request timed out");
- } else if (error.message === "Network request failed") {
- console.error("No internet connection");
- } else {
- throw error; // rethrow other unexpected errors
- }
- }
-
- return response;
-}
-
-async function is_401(response: Response): Promise<boolean> {
- if (response.status === 401) {
- clear_session_data();
- throw redirect(307, "/login");
- }
- return false;
-}
diff --git a/code/app/src/lib/api/password-reset-request/index.ts b/code/app/src/lib/api/password-reset-request/index.ts
new file mode 100644
index 0000000..9d6f0dc
--- /dev/null
+++ b/code/app/src/lib/api/password-reset-request/index.ts
@@ -0,0 +1,17 @@
+import { api_base } from "$lib/configuration";
+import { http_get_async, http_post_async, type InternalFetchResponse } from "../_fetch";
+
+export const http_password_reset_request = {
+ create_forgot_password_request(username: string): Promise<InternalFetchResponse> {
+ if (!username) throw new Error("Username is empty");
+ return http_get_async(api_base("_/forgot-password-requests/create?username=" + username));
+ },
+ check_forgot_password_request(public_id: string): Promise<InternalFetchResponse> {
+ if (!public_id) throw new Error("Id is empty");
+ return http_get_async(api_base("_/forgot-password-requests/is-valid?id=" + public_id));
+ },
+ fulfill_forgot_password_request(public_id: string, newPassword: string): Promise<InternalFetchResponse> {
+ if (!public_id) throw new Error("Id is empty");
+ return http_post_async(api_base("_/forgot-password-requests/fulfill"), { id: public_id, newPassword });
+ },
+} \ No newline at end of file
diff --git a/code/app/src/lib/api/root.ts b/code/app/src/lib/api/root.ts
index 3e5bda2..661f24b 100644
--- a/code/app/src/lib/api/root.ts
+++ b/code/app/src/lib/api/root.ts
@@ -1,6 +1,12 @@
-import {http_post} from "$lib/api/internal-fetch";
-import {api_base} from "$lib/configuration";
+import { http_get_async, http_post_async } from "$lib/api/_fetch";
+import { api_base } from "$lib/configuration";
+import type { IInternalFetchResponse } from "$lib/models/internal/IInternalFetchResponse";
+import type { Result } from "rustic";
export function server_log(message: string): void {
- http_post(api_base("_/api/log"), message);
+ http_post_async(api_base("_/api/log"), message);
}
+
+export function server_version(): Promise<Result<IInternalFetchResponse<string>, string>> {
+ return http_get_async(api_base("/version.txt"));
+} \ No newline at end of file
diff --git a/code/app/src/lib/api/time-entry.ts b/code/app/src/lib/api/time-entry.ts
deleted file mode 100644
index faedb48..0000000
--- a/code/app/src/lib/api/time-entry.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { api_base } from "$lib/configuration";
-import { is_guid } from "$lib/helpers";
-import { http_delete, http_get, http_post } from "./internal-fetch";
-import type { WorkCategory } from "$lib/models/work/WorkCategory";
-import type { WorkLabel } from "$lib/models/work/WorkLabel";
-import type { WorkEntry } from "$lib/models/work/WorkEntry";
-import type { WorkQuery } from "$lib/models/work/WorkQuery";
-import type { IInternalFetchResponse } from "$lib/models/internal/IInternalFetchResponse";
-
-
-// ENTRIES
-
-export async function create_time_entry(payload: WorkEntry): Promise<IInternalFetchResponse> {
- return http_post(api_base("v1/entries/create"), payload);
-}
-
-export async function get_time_entry(entryId: string): Promise<IInternalFetchResponse> {
- if (is_guid(entryId)) {
- return http_get(api_base("v1/entries/" + entryId));
- }
- throw new Error("entryId is not a valid guid.");
-}
-
-export async function get_time_entries(entryQuery: WorkQuery): Promise<IInternalFetchResponse> {
- return http_post(api_base("v1/entries/query"), entryQuery);
-}
-
-export async function delete_time_entry(id: string): Promise<IInternalFetchResponse> {
- if (!is_guid(id)) throw new Error("id is not a valid guid");
- return http_delete(api_base("v1/entries/" + id + "/delete"));
-}
-
-export async function update_time_entry(entryDto: WorkEntry): Promise<IInternalFetchResponse> {
- if (!is_guid(entryDto.id ?? "")) throw new Error("id is not a valid guid");
- if (!entryDto.category) throw new Error("category is empty");
- if (!entryDto.stop) throw new Error("stop is empty");
- if (!entryDto.start) throw new Error("start is empty");
- return http_post(api_base("v1/entries/update"), entryDto);
-}
-
-// LABELS
-export async function create_time_label(labelDto: WorkLabel): Promise<IInternalFetchResponse> {
- return http_post(api_base("v1/labels/create"), labelDto);
-}
-
-export async function get_time_labels(): Promise<IInternalFetchResponse> {
- return http_get(api_base("v1/labels"));
-}
-
-export async function delete_time_label(id: string): Promise<IInternalFetchResponse> {
- if (!is_guid(id)) throw new Error("id is not a valid guid");
- return http_delete(api_base("v1/labels/" + id + "/delete"));
-}
-
-export async function update_time_label(labelDto: WorkLabel): Promise<IInternalFetchResponse> {
- if (!is_guid(labelDto.id ?? "")) throw new Error("id is not a valid guid");
- if (!labelDto.name) throw new Error("name is empty");
- if (!labelDto.color) throw new Error("color is empty");
- return http_post(api_base("v1/labels/update"), labelDto);
-}
-
-// CATEGORIES
-export async function create_time_category(category: WorkCategory): Promise<IInternalFetchResponse> {
- if (!category.name) throw new Error("name is empty");
- if (!category.color) throw new Error("color is empty");
- return http_post(api_base("v1/categories/create"), category);
-}
-
-export async function get_time_categories(): Promise<IInternalFetchResponse> {
- return http_get(api_base("v1/categories"));
-}
-
-export async function delete_time_category(id: string): Promise<IInternalFetchResponse> {
- if (!is_guid(id)) throw new Error("id is not a valid guid");
- return http_delete(api_base("v1/categories/" + id + "/delete"));
-}
-
-export async function update_time_category(category: WorkCategory): Promise<IInternalFetchResponse> {
- if (!is_guid(category.id ?? "")) throw new Error("id is not a valid guid");
- if (!category.name) throw new Error("name is empty");
- if (!category.color) throw new Error("color is empty");
- return http_post(api_base("v1/categories/update"), category);
-}
diff --git a/code/app/src/lib/api/user.ts b/code/app/src/lib/api/user.ts
deleted file mode 100644
index f08fb6d..0000000
--- a/code/app/src/lib/api/user.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { api_base } from "$lib/configuration";
-import { http_delete, http_get, http_post } from "./internal-fetch";
-import type { LoginPayload } from "$lib/models/internal/LoginPayload";
-import type { UpdateProfilePayload } from "$lib/models/internal/UpdateProfilePayload";
-import type { CreateAccountPayload } from "$lib/models/internal/CreateAccountPayload";
-import type { IInternalFetchResponse } from "$lib/models/internal/IInternalFetchResponse";
-
-export async function login(payload: LoginPayload): Promise<IInternalFetchResponse> {
- return http_post(api_base("_/account/login"), payload);
-}
-
-export async function logout(): Promise<IInternalFetchResponse> {
- return http_get(api_base("_/account/logout"));
-}
-
-export async function create_forgot_password_request(username: string): Promise<IInternalFetchResponse> {
- if (!username) throw new Error("Username is empty");
- return http_get(api_base("_/forgot-password-requests/create?username=" + username));
-}
-
-export async function check_forgot_password_request(public_id: string): Promise<IInternalFetchResponse> {
- if (!public_id) throw new Error("Id is empty");
- return http_get(api_base("_/forgot-password-requests/is-valid?id=" + public_id));
-}
-
-export async function fulfill_forgot_password_request(public_id: string, newPassword: string): Promise<IInternalFetchResponse> {
- if (!public_id) throw new Error("Id is empty");
- return http_post(api_base("_/forgot-password-requests/fulfill"), { id: public_id, newPassword });
-}
-
-export async function delete_account(): Promise<IInternalFetchResponse> {
- return http_delete(api_base("_/account/delete"));
-}
-
-export async function update_profile(payload: UpdateProfilePayload): Promise<IInternalFetchResponse> {
- if (!payload.password && !payload.username) throw new Error("Password and Username is empty");
- return http_post(api_base("_/account/update"), payload);
-}
-
-export async function create_account(payload: CreateAccountPayload): Promise<IInternalFetchResponse> {
- if (!payload.password && !payload.username) throw new Error("Password and Username is empty");
- return http_post(api_base("_/account/create"), payload);
-}
-
-export async function get_profile_for_active_check(): Promise<IInternalFetchResponse> {
- return http_get(api_base("_/account"), 0, true);
-}