First Commit. Duplicate bunext-sqlite preset
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import type { BUN_SQLITE_BSQLITE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type { ApiReqParams, ClientAdminCrudParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
|
||||
export default async function adminCrudHandler<
|
||||
T extends {} = BUN_SQLITE_BSQLITE_ALL_TYPEDEFS,
|
||||
>(params: ClientAdminCrudParams<T>) {
|
||||
const {
|
||||
action,
|
||||
table,
|
||||
count,
|
||||
insert_data,
|
||||
sql_insert_params,
|
||||
sql_query,
|
||||
sql_select_params,
|
||||
update_data,
|
||||
update_on_duplicate,
|
||||
id,
|
||||
} = params;
|
||||
|
||||
let url = `/api/admin/crud/${table}`;
|
||||
|
||||
if (id) {
|
||||
url += `/${id}`;
|
||||
}
|
||||
|
||||
let query: ApiReqParams<T> = {
|
||||
count,
|
||||
insert_data,
|
||||
sql_insert_params,
|
||||
sql_query,
|
||||
sql_select_params,
|
||||
update_data,
|
||||
update_on_duplicate,
|
||||
...params.request_query,
|
||||
...params.request_body,
|
||||
};
|
||||
|
||||
const res = await fetchApi<ApiReqParams<T>, APIResponseObject<T>>(url, {
|
||||
method:
|
||||
action == "update"
|
||||
? "PUT"
|
||||
: action == "delete"
|
||||
? "DELETE"
|
||||
: action == "insert"
|
||||
? "POST"
|
||||
: "GET",
|
||||
body: action == "get" ? undefined : query,
|
||||
query: action == "get" ? query : undefined,
|
||||
});
|
||||
|
||||
if (!res.debug) {
|
||||
res.debug = {};
|
||||
}
|
||||
res.debug.url = url;
|
||||
res.debug.query = query;
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { BUN_SQLITE_BSQLITE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type { AdminFetchParams, ApiReqParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
|
||||
export default async function adminFetchHandler<
|
||||
T extends {} = BUN_SQLITE_BSQLITE_ALL_TYPEDEFS,
|
||||
>({ url: passed_url, method = "GET", query, body }: AdminFetchParams<T>) {
|
||||
let url = passed_url;
|
||||
|
||||
const res = await fetchApi<ApiReqParams<T>, APIResponseObject<T>>(url, {
|
||||
method,
|
||||
body,
|
||||
query,
|
||||
});
|
||||
|
||||
if (!res.debug) {
|
||||
res.debug = {};
|
||||
}
|
||||
|
||||
res.debug.url = url;
|
||||
res.debug.query = query;
|
||||
res.debug.body = body;
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
|
||||
export default async function clientDeleteMediaHandler(params: ApiReqParams) {
|
||||
const res = await fetchApi<ApiReqParams, APIResponseObject>(
|
||||
`/api/admin/delete-media`,
|
||||
{
|
||||
method: "DELETE",
|
||||
body: params,
|
||||
},
|
||||
);
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { BUN_SQLITE_BSQLITE_MEDIA } from "@/db/types/db";
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bun-sqlite";
|
||||
|
||||
export default async function clientUploadMediaHandler(params: ApiReqParams) {
|
||||
const res = await fetchApi<
|
||||
ApiReqParams,
|
||||
APIResponseObject<BUN_SQLITE_BSQLITE_MEDIA>
|
||||
>(`/api/admin/upload-media`, {
|
||||
method: "POST",
|
||||
body: params,
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* # Client-side actions for document/invoice generation & emailing.
|
||||
* PDF endpoints return binary PDFs (not JSON), so plain fetch is used
|
||||
* here instead of `fetchApi`.
|
||||
*/
|
||||
|
||||
async function postAdminAction({
|
||||
url,
|
||||
body,
|
||||
}: {
|
||||
url: string;
|
||||
body: any;
|
||||
}): Promise<{ success: boolean; msg?: string }> {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body || {}),
|
||||
});
|
||||
|
||||
const content_type = res.headers.get("Content-Type") || "";
|
||||
|
||||
if (content_type.includes("application/json")) {
|
||||
const json = await res.json();
|
||||
return {
|
||||
success: Boolean(json.success),
|
||||
msg: json.msg,
|
||||
};
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Request failed with status ${res.status}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message || "Request failed",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadGeneratedPdf({
|
||||
url,
|
||||
body,
|
||||
fallback_filename,
|
||||
}: {
|
||||
url: string;
|
||||
body: any;
|
||||
fallback_filename?: string;
|
||||
}): Promise<{ success: boolean; msg?: string }> {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body || {}),
|
||||
});
|
||||
|
||||
const content_type = res.headers.get("Content-Type") || "";
|
||||
|
||||
if (content_type.includes("application/json")) {
|
||||
const json = await res.json();
|
||||
return {
|
||||
success: Boolean(json.success),
|
||||
msg: json.msg,
|
||||
};
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Request failed with status ${res.status}`);
|
||||
}
|
||||
|
||||
const blob = await res.blob();
|
||||
|
||||
if (blob.size == 0) {
|
||||
throw new Error(`Received an empty PDF`);
|
||||
}
|
||||
|
||||
const disposition = res.headers.get("Content-Disposition") || "";
|
||||
const filename_match = disposition.match(/filename="?([^";]+)"?/i);
|
||||
const filename =
|
||||
filename_match?.[1] || fallback_filename || "document.pdf";
|
||||
|
||||
const object_url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
|
||||
anchor.href = object_url;
|
||||
anchor.download = filename;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(object_url);
|
||||
}, 1000);
|
||||
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message || "Couldn't download document",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendEmailRecord({
|
||||
url,
|
||||
body,
|
||||
}: {
|
||||
url: string;
|
||||
body: any;
|
||||
}) {
|
||||
return await postAdminAction({ url, body });
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import type { SSOPendingLogin } from "@/src/types";
|
||||
import EJSON from "@/src/utils/ejson";
|
||||
|
||||
export default function readSSOPendingLogin(): SSOPendingLogin | undefined {
|
||||
try {
|
||||
const stored = sessionStorage.getItem(
|
||||
AppData["SSOPendingLoginStorageKey"],
|
||||
);
|
||||
|
||||
return stored
|
||||
? (EJSON.parse(stored) as SSOPendingLogin | undefined)
|
||||
: undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type useFormInit from "@/src/hooks/use-form-init";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
|
||||
type Params = Pick<ReturnType<typeof useFormInit>, "setLoading" | "setStatus">;
|
||||
|
||||
export default async function resendLoginCode({
|
||||
setLoading,
|
||||
setStatus,
|
||||
}: Params) {
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetchApi<ApiReqParams, APIResponseObject>(
|
||||
`/api/auth/resend-login-code`,
|
||||
{
|
||||
method: "POST",
|
||||
body: {},
|
||||
},
|
||||
);
|
||||
|
||||
setStatus(
|
||||
res.success
|
||||
? {
|
||||
success: true,
|
||||
msg: "A new code is on its way. Previous codes no longer work.",
|
||||
}
|
||||
: {
|
||||
error: true,
|
||||
msg: res.msg || "Couldn't resend the code. Please try again.",
|
||||
},
|
||||
);
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import type { SSOPendingLogin } from "@/src/types";
|
||||
import EJSON from "@/src/utils/ejson";
|
||||
|
||||
/**
|
||||
* Remembers which login is waiting for a code, so the SSO page can word
|
||||
* itself to match. Best effort only.
|
||||
*/
|
||||
export default function saveSSOPendingLogin(pending: SSOPendingLogin) {
|
||||
try {
|
||||
sessionStorage.setItem(
|
||||
AppData["SSOPendingLoginStorageKey"],
|
||||
EJSON.stringify(pending) || "",
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
Reference in New Issue
Block a user