First Commit
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
export default async function bufferFromBase64(
|
||||
base_64: string,
|
||||
): Promise<{ buffer: Buffer; ext: string }> {
|
||||
let mimeType = "";
|
||||
let data = base_64;
|
||||
|
||||
const dataUrlMatch = base_64.match(
|
||||
/^data:([^;]+);base64,([\s\S]+)$/i,
|
||||
);
|
||||
|
||||
if (dataUrlMatch?.[1] && dataUrlMatch[2]) {
|
||||
mimeType = dataUrlMatch[1].trim().toLowerCase();
|
||||
data = dataUrlMatch[2];
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(data, "base64");
|
||||
|
||||
let ext = "bin";
|
||||
if (mimeType.includes("image/jpeg") || mimeType.includes("image/jpg"))
|
||||
ext = "jpeg";
|
||||
else if (mimeType.includes("image/png")) ext = "png";
|
||||
else if (mimeType.includes("image/webp")) ext = "webp";
|
||||
else if (mimeType.includes("image/gif")) ext = "gif";
|
||||
else if (mimeType.includes("image/svg")) ext = "svg";
|
||||
else if (mimeType.includes("application/pdf")) ext = "pdf";
|
||||
else if (
|
||||
mimeType.includes(
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml",
|
||||
)
|
||||
)
|
||||
ext = "docx";
|
||||
else if (
|
||||
mimeType.includes(
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml",
|
||||
)
|
||||
)
|
||||
ext = "xlsx";
|
||||
else if (mimeType.includes("text/csv")) ext = "csv";
|
||||
else if (mimeType.includes("text/markdown")) ext = "md";
|
||||
else if (mimeType.includes("text/plain")) ext = "txt";
|
||||
else if (mimeType.includes("/")) {
|
||||
ext = mimeType.split("/").pop()?.split("+")[0] || "bin";
|
||||
}
|
||||
|
||||
return { buffer, ext };
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
export default async function bufferFromUrl(
|
||||
url: string,
|
||||
): Promise<{ buffer: Buffer; ext: string }> {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`);
|
||||
|
||||
const contentType = res.headers.get("content-type") ?? "";
|
||||
const arrayBuffer = await res.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
// Derive ext from content-type first, fall back to URL path
|
||||
let ext = "txt";
|
||||
if (contentType.includes("application/pdf")) ext = "pdf";
|
||||
else if (
|
||||
contentType.includes(
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml",
|
||||
)
|
||||
)
|
||||
ext = "docx";
|
||||
else if (
|
||||
contentType.includes(
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml",
|
||||
)
|
||||
)
|
||||
ext = "xlsx";
|
||||
else if (contentType.includes("text/csv")) ext = "csv";
|
||||
else if (contentType.includes("text/markdown")) ext = "md";
|
||||
else {
|
||||
const urlPath = new URL(url).pathname;
|
||||
ext = urlPath.split(".").pop()?.toLowerCase() ?? "txt";
|
||||
}
|
||||
|
||||
return { buffer, ext };
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Extracts a specific cookie value from a Request's Cookie header.
|
||||
* Returns undefined if the cookie is not found.
|
||||
*/
|
||||
export function getCookie(request: Request, name: string): string | undefined {
|
||||
const header = request.headers.get("Cookie");
|
||||
if (!header) return undefined;
|
||||
|
||||
const cookies = header.split(";");
|
||||
|
||||
for (const cookie of cookies) {
|
||||
const [key, ...rest] = cookie.split("=");
|
||||
if (!key) continue;
|
||||
if (key.trim() === name) {
|
||||
return rest.join("=").trim();
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export type CookieOptions = {
|
||||
/** Cookie name */
|
||||
name: string;
|
||||
/** Cookie value */
|
||||
value: string;
|
||||
/** Prevents client-side JS access (default: true) */
|
||||
httpOnly?: boolean;
|
||||
/** Only send over HTTPS (default: true) */
|
||||
secure?: boolean;
|
||||
/** CSRF protection strategy (default: "Lax") */
|
||||
sameSite?: "Strict" | "Lax" | "None";
|
||||
/** URL path scope (default: "/") */
|
||||
path?: string;
|
||||
/** Lifetime in seconds. Omit for a session cookie. */
|
||||
maxAge?: number;
|
||||
/** Restrict to a specific domain */
|
||||
domain?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends one or more Set-Cookie headers to a Response.
|
||||
* Uses `append` so multiple cookies don't overwrite each other.
|
||||
*/
|
||||
export function setCookies(response: Response, cookies: CookieOptions[]): void {
|
||||
for (const cookie of cookies) {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
httpOnly = true,
|
||||
secure = true,
|
||||
sameSite = "Lax",
|
||||
path = "/",
|
||||
maxAge,
|
||||
domain,
|
||||
} = cookie;
|
||||
|
||||
let header = `${name}=${value}; Path=${path}; SameSite=${sameSite}`;
|
||||
|
||||
if (httpOnly) header += "; HttpOnly";
|
||||
if (secure) header += "; Secure";
|
||||
if (maxAge !== undefined) header += `; Max-Age=${maxAge}`;
|
||||
if (domain) header += `; Domain=${domain}`;
|
||||
|
||||
response.headers.append("Set-Cookie", header);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a Set-Cookie header that expires the named cookie immediately.
|
||||
* Sets Max-Age=0 and an empty value to instruct the browser to remove it.
|
||||
* Path and domain must match the original cookie for deletion to work.
|
||||
*/
|
||||
export function deleteCookies(
|
||||
response: Response,
|
||||
cookies: Pick<CookieOptions, "name" | "path" | "domain" | "httpOnly">[],
|
||||
): void {
|
||||
for (const { name, path = "/", domain, httpOnly } of cookies) {
|
||||
let header = `${name}=; Path=${path}; Max-Age=0`;
|
||||
|
||||
if (httpOnly) header += "; HttpOnly";
|
||||
if (domain) header += `; Domain=${domain}`;
|
||||
|
||||
response.headers.append("Set-Cookie", header);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
const key = process.env.ENCRYPTION_KEY;
|
||||
const salt = process.env.ENCRYPTION_SALT;
|
||||
|
||||
async function getKey() {
|
||||
const material = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
new TextEncoder().encode(key),
|
||||
"PBKDF2",
|
||||
false,
|
||||
["deriveKey"],
|
||||
);
|
||||
return crypto.subtle.deriveKey(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
salt: new TextEncoder().encode(salt),
|
||||
iterations: 100000,
|
||||
hash: "SHA-256",
|
||||
},
|
||||
material,
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
false,
|
||||
["encrypt", "decrypt"],
|
||||
);
|
||||
}
|
||||
|
||||
export async function encrypt(text: string): Promise<string> {
|
||||
const derivedKey = await getKey();
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const ciphertext = await crypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", iv },
|
||||
derivedKey,
|
||||
new TextEncoder().encode(text),
|
||||
);
|
||||
const combined = new Uint8Array(
|
||||
iv.length + new Uint8Array(ciphertext).length,
|
||||
);
|
||||
combined.set(iv);
|
||||
combined.set(new Uint8Array(ciphertext), iv.length);
|
||||
return btoa(String.fromCharCode(...combined));
|
||||
}
|
||||
|
||||
export async function decrypt(encoded: string): Promise<string> {
|
||||
const derivedKey = await getKey();
|
||||
const combined = Uint8Array.from(atob(encoded), (c) => c.charCodeAt(0));
|
||||
const iv = combined.slice(0, 12);
|
||||
const ciphertext = combined.slice(12);
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: "AES-GCM", iv },
|
||||
derivedKey,
|
||||
ciphertext,
|
||||
);
|
||||
return new TextDecoder().decode(decrypted);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* # EJSON parse string
|
||||
*/
|
||||
function parse(
|
||||
string: string | null | number,
|
||||
reviver?: (this: any, key: string, value: any) => any
|
||||
): { [s: string]: any } | { [s: string]: any }[] | undefined {
|
||||
if (!string) return undefined;
|
||||
if (typeof string == "object") return string;
|
||||
if (typeof string !== "string") return undefined;
|
||||
try {
|
||||
return JSON.parse(string, reviver);
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* # EJSON stringify object
|
||||
*/
|
||||
function stringify(
|
||||
value: any,
|
||||
replacer?: ((this: any, key: string, value: any) => any) | null,
|
||||
space?: string | number
|
||||
): string | undefined {
|
||||
try {
|
||||
return JSON.stringify(value, replacer || undefined, space);
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const EJSON = {
|
||||
parse,
|
||||
stringify,
|
||||
};
|
||||
|
||||
export default EJSON;
|
||||
@@ -0,0 +1,41 @@
|
||||
import path from "path";
|
||||
import type { User } from "../types";
|
||||
import type { BUN_SQLITE_WGUI_USERS } from "@/db/types/db";
|
||||
|
||||
type Params = {
|
||||
user?: BUN_SQLITE_WGUI_USERS | User;
|
||||
};
|
||||
|
||||
export default function grabDirNames(params?: Params) {
|
||||
const DIRNAME = __dirname;
|
||||
const ROOT_DIR = DIRNAME.includes(`/.bunext`)
|
||||
? process.cwd()
|
||||
: path.resolve(__dirname, "../..");
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(ROOT_DIR, ".data");
|
||||
const MEDIA_RELATIVE_DIR = "media";
|
||||
const MEDIA_DIR = path.join(DATA_DIR, MEDIA_RELATIVE_DIR);
|
||||
|
||||
const USER_MEDIA_PUBLIC_DIR = params?.user?.id
|
||||
? path.join(MEDIA_DIR, String(params.user.id), "public")
|
||||
: undefined;
|
||||
const USER_MEDIA_PUBLIC_RELATIVE_DIR = params?.user?.id
|
||||
? path.join(MEDIA_RELATIVE_DIR, String(params.user.id), "public")
|
||||
: undefined;
|
||||
const USER_MEDIA_PRIVATE_DIR = params?.user?.id
|
||||
? path.join(MEDIA_DIR, String(params.user.id), "private")
|
||||
: undefined;
|
||||
const USER_MEDIA_PRIVATE_RELATIVE_DIR = params?.user?.id
|
||||
? path.join(MEDIA_RELATIVE_DIR, String(params.user.id), "private")
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
ROOT_DIR,
|
||||
MEDIA_DIR,
|
||||
USER_MEDIA_PUBLIC_DIR,
|
||||
USER_MEDIA_PRIVATE_DIR,
|
||||
DATA_DIR,
|
||||
USER_MEDIA_PUBLIC_RELATIVE_DIR,
|
||||
USER_MEDIA_PRIVATE_RELATIVE_DIR,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { ApiReqParams } from "../types";
|
||||
import type { BUN_SQLITE_WGUI_USERS_JOIN } from "../types/sql-joins";
|
||||
|
||||
export default function searchableFieldToQueryField({
|
||||
field,
|
||||
}: {
|
||||
field: keyof BUN_SQLITE_WGUI_USERS_JOIN;
|
||||
}): keyof ApiReqParams {
|
||||
switch (field) {
|
||||
case "email":
|
||||
return "search_term_email";
|
||||
case "last_name":
|
||||
return "search_term_last_name";
|
||||
case "user_types":
|
||||
return "search_term_user_type";
|
||||
case "first_name":
|
||||
default:
|
||||
return "search_term_first_name";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function uniqueStringArr(arr: string[]): string[] {
|
||||
return [...new Set(arr)];
|
||||
}
|
||||
Reference in New Issue
Block a user