First Commit. Duplicate bunext-sqlite preset

This commit is contained in:
2026-10-18 16:47:11 +01:00
commit 81e4a8aecc
718 changed files with 41695 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
import grabSuperAdminUser from "@/src/functions/backend/backups/grab-super-admin-user";
import createDbBackup from "@/src/functions/backend/backups/create-db-backup";
import logEvent, { grabRequestIP, logError } from "@/src/functions/backend/logger";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
req,
}) => {
const user = await grabSuperAdminUser({ req });
if (!user?.id) {
return {
success: false,
msg: `Unauthorized`,
};
}
try {
await createDbBackup();
await logEvent({
title: "Database backup created",
category: "system",
user_id: user.id,
plain_text: `User #${user.id} created a database backup.`,
ip_address: grabRequestIP(req),
});
return {
success: true,
};
} catch (error: any) {
await logError({
title: "Database backup failed",
error,
user_id: user.id,
ip_address: grabRequestIP(req),
});
return {
success: false,
msg: error.message,
};
}
};
+40
View File
@@ -0,0 +1,40 @@
import grabSuperAdminUser from "@/src/functions/backend/backups/grab-super-admin-user";
import resolveDbBackupPath from "@/src/functions/backend/backups/resolve-db-backup-path";
import type { ApiReqParams } from "@/src/types";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
import path from "path";
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
req,
query,
}) => {
const user = await grabSuperAdminUser({ req });
if (!user?.id) {
return {
success: false,
msg: `Unauthorized`,
};
}
try {
const backup_path = resolveDbBackupPath({
backup_name: (query as ApiReqParams | undefined)?.backup_name,
});
return new Response(Bun.file(backup_path), {
headers: {
"Content-Type": "application/vnd.sqlite3",
"Content-Disposition": `attachment; filename="${path.basename(backup_path)}.sqlite"`,
},
});
} catch (error: any) {
return {
success: false,
msg: error.message,
};
}
};
+62
View File
@@ -0,0 +1,62 @@
import grabSuperAdminUser from "@/src/functions/backend/backups/grab-super-admin-user";
import restoreDbBackup from "@/src/functions/backend/backups/restore-db-backup";
import logEvent, { grabRequestIP, logError } from "@/src/functions/backend/logger";
import type { ApiReqParams } from "@/src/types";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
req,
body,
}) => {
const user = await grabSuperAdminUser({ req });
if (!user?.id) {
return {
success: false,
msg: `Unauthorized`,
};
}
const backup_name = (body as ApiReqParams | undefined)?.backup_name;
try {
await restoreDbBackup({ backup_name });
/**
* Logged after the restore so the entry lands in the restored
* `logs` table.
*/
await logEvent({
title: "Database restored",
category: "system",
user_id: user.id,
plain_text: `User #${user.id} restored the database from backup ${backup_name}.`,
metadata: {
backup_name,
},
ip_address: grabRequestIP(req),
});
return {
success: true,
};
} catch (error: any) {
await logError({
title: "Database restore failed",
error,
user_id: user.id,
metadata: {
backup_name,
},
ip_address: grabRequestIP(req),
});
return {
success: false,
msg: error.message,
};
}
};
@@ -0,0 +1,78 @@
import grabSuperAdminUser from "@/src/functions/backend/backups/grab-super-admin-user";
import saveUploadedDbBackup from "@/src/functions/backend/backups/save-uploaded-db-backup";
import restoreDbBackup from "@/src/functions/backend/backups/restore-db-backup";
import logEvent, { grabRequestIP, logError } from "@/src/functions/backend/logger";
import type {
APIResponseObject,
BunextAPIRouteHandler,
BunextServerRouteConfig,
} from "@moduletrace/bunext/types";
export const config: BunextServerRouteConfig = {
max_request_body_mb: 500,
};
/**
* Multipart upload (`file` field) of a SQLite database file. The file is
* saved into the backups directory, then restored.
*/
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
req,
}) => {
const user = await grabSuperAdminUser({ req });
if (!user?.id) {
return {
success: false,
msg: `Unauthorized`,
};
}
let file_name: string | undefined;
let backup_name: string | undefined;
try {
const file = (await req.formData()).get("file");
if (!(file instanceof File)) {
throw new Error(`No backup file uploaded`);
}
file_name = file.name;
backup_name = await saveUploadedDbBackup({ file });
await restoreDbBackup({ backup_name });
await logEvent({
title: "Database restored from upload",
category: "system",
user_id: user.id,
plain_text: `User #${user.id} restored the database from uploaded file ${file_name} (saved as ${backup_name}).`,
metadata: {
file_name,
backup_name,
},
ip_address: grabRequestIP(req),
});
return {
success: true,
};
} catch (error: any) {
await logError({
title: "Database restore from upload failed",
error,
user_id: user.id,
metadata: {
file_name,
backup_name,
},
ip_address: grabRequestIP(req),
});
return {
success: false,
msg: error.message,
};
}
};
@@ -0,0 +1,28 @@
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
export default async function ({
user,
user_types,
}: AdminCrudAPIParams): Promise<APIResponseObject> {
if (!user?.id) {
return {
success: false,
msg: `No user passed`,
};
}
const is_super_admin = checkUserAccess({ user_types });
if (!is_super_admin.success) {
return {
success: false,
msg: `Unauthorized`,
};
}
return {
success: true,
};
}
@@ -0,0 +1,28 @@
import _ from "lodash";
import defaultChecks from "../default-checks";
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, user, user_types, body, id, query } = params;
const default_check = await defaultChecks(params);
const is_super_admin = checkUserAccess({ user_types }).success;
if (!default_check.success) {
return default_check;
}
if (!is_super_admin) {
return {
success: false,
};
}
return {
success: true,
};
}
@@ -0,0 +1,28 @@
import _ from "lodash";
import checks from "./checks";
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import BunSQLite from "@moduletrace/bun-sqlite";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, body, id, query } = params;
const check = await checks(params);
if (!check?.success) {
return {
success: false,
msg: `Delete Unauthorized`,
};
}
const res = await BunSQLite.delete({
table,
query: _.merge(query?.sql_query, body?.sql_query),
targetId: id,
});
return res;
}
@@ -0,0 +1,20 @@
import _ from "lodash";
import defaultChecks from "../default-checks";
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, user, user_types, body, id, query } = params;
const default_check = await defaultChecks(params);
if (!default_check.success) {
return default_check;
}
return {
success: true,
};
}
@@ -0,0 +1,31 @@
import _ from "lodash";
import checks from "./checks";
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import BunSQLite from "@moduletrace/bun-sqlite";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, body, id, query } = params;
const check = await checks(params);
if (!check?.success) {
return {
success: false,
msg: `Select Unauthorized`,
};
}
let final_sql_query = _.merge(query?.sql_query, body?.sql_query);
const res = await BunSQLite.select({
table,
query: final_sql_query,
targetId: id,
count: query?.count || body?.count,
});
return res;
}
@@ -0,0 +1,18 @@
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import _ from "lodash";
import defaultChecks from "../default-checks";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const default_check = await defaultChecks(params);
if (!default_check.success) {
return default_check;
}
return {
success: true,
};
}
@@ -0,0 +1,37 @@
import _ from "lodash";
import checks from "./checks";
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import BunSQLite from "@moduletrace/bun-sqlite";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, body } = params;
const check = await checks(params);
if (!check?.success) {
return {
success: false,
msg: `Insert Unauthorized`,
};
}
if (!body?.insert_data) {
return {
success: false,
msg: `No data to insert`,
};
}
let final_insert_data = [...body.insert_data];
const res = await BunSQLite.insert({
table,
data: final_insert_data,
update_on_duplicate: body.update_on_duplicate,
});
return res;
}
@@ -0,0 +1,30 @@
import _ from "lodash";
import defaultChecks from "../default-checks";
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { user_types } = params;
const default_check = await defaultChecks(params);
if (!default_check.success) {
return default_check;
}
const is_admin = checkUserAccess({
user_types,
includes: ["admin"],
});
if (!is_admin.success) {
return is_admin;
}
return {
success: true,
};
}
@@ -0,0 +1,36 @@
import _ from "lodash";
import checks from "./checks";
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import BunSQLite from "@moduletrace/bun-sqlite";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, body, id, query } = params;
const check = await checks(params);
if (!check?.success) {
return {
success: false,
msg: `Update Unauthorized`,
};
}
if (!body?.update_data) {
return {
success: false,
msg: `No data to update`,
};
}
const res = await BunSQLite.update({
table,
data: body.update_data,
query: _.merge(query?.sql_query, body?.sql_query),
targetId: id,
});
return res;
}
@@ -0,0 +1,29 @@
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import {
gatedCrudDelete,
gatedCrudGet,
gatedCrudPost,
gatedCrudPut,
} from "../../(utils)/gated-crud";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
switch (params.req.method) {
case "GET":
return await gatedCrudGet(params);
case "POST":
return await gatedCrudPost(params);
case "PUT":
return await gatedCrudPut(params);
case "DELETE":
return await gatedCrudDelete(params);
default:
return {
success: false,
msg: `Unhandled`,
};
}
}
@@ -0,0 +1,29 @@
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import {
gatedCrudDelete,
gatedCrudGet,
gatedCrudPost,
gatedCrudPut,
} from "../../(utils)/gated-crud";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
switch (params.req.method) {
case "GET":
return await gatedCrudGet(params);
case "POST":
return await gatedCrudPost(params);
case "PUT":
return await gatedCrudPut(params);
case "DELETE":
return await gatedCrudDelete(params);
default:
return {
success: false,
msg: `Unhandled`,
};
}
}
@@ -0,0 +1,29 @@
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import {
gatedCrudDelete,
gatedCrudGet,
gatedCrudPost,
gatedCrudPut,
} from "../../(utils)/gated-crud";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
switch (params.req.method) {
case "GET":
return await gatedCrudGet(params);
case "POST":
return await gatedCrudPost(params);
case "PUT":
return await gatedCrudPut(params);
case "DELETE":
return await gatedCrudDelete(params);
default:
return {
success: false,
msg: `Unhandled`,
};
}
}
@@ -0,0 +1,29 @@
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import {
gatedCrudDelete,
gatedCrudGet,
gatedCrudPost,
gatedCrudPut,
} from "../../(utils)/gated-crud";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
switch (params.req.method) {
case "GET":
return await gatedCrudGet(params);
case "POST":
return await gatedCrudPost(params);
case "PUT":
return await gatedCrudPut(params);
case "DELETE":
return await gatedCrudDelete(params);
default:
return {
success: false,
msg: `Unhandled`,
};
}
}
@@ -0,0 +1,107 @@
import type { BUN_SQLITE_BSQLITE_INVOICES } from "@/db/types/db";
import { SiteData } from "@/src/data/site-data";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import grabNextDocumentSequence from "@/src/functions/backend/db/documents/grab-next-document-sequence";
import type { AdminCrudAPIParams } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import {
gatedCrudDelete,
gatedCrudGet,
gatedCrudPut,
} from "../../(utils)/gated-crud";
function invoiceNumberPrefix(): string {
const now = new Date();
return `${SiteData["SiteSlug"].toUpperCase()}-${now.getFullYear()}${String(
now.getMonth() + 1,
).padStart(2, "0")}`;
}
async function buildInvoiceRows({
rows,
}: {
rows: BUN_SQLITE_BSQLITE_INVOICES[];
}): Promise<BUN_SQLITE_BSQLITE_INVOICES[]> {
const prefix = invoiceNumberPrefix();
let next_sequence: number | undefined;
const final_rows: BUN_SQLITE_BSQLITE_INVOICES[] = [];
for (const row of rows) {
const invoice_row = { ...row } as BUN_SQLITE_BSQLITE_INVOICES;
if (!invoice_row.invoice_number) {
next_sequence ??= await grabNextDocumentSequence({
table: "invoices",
column: "invoice_number",
prefix,
});
invoice_row.invoice_number = `${prefix}-${String(next_sequence).padStart(3, "0")}`;
next_sequence += 1;
}
final_rows.push(invoice_row);
}
return final_rows;
}
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
switch (params.req.method) {
case "GET":
return await gatedCrudGet(params);
case "POST": {
const { table, body, user_types } = params;
const access = checkUserAccess({
user_types,
includes: ["admin"],
});
if (!access.success) {
return access;
}
if (!body?.insert_data) {
return {
success: false,
msg: `No data to insert`,
};
}
for (let attempt = 0; attempt < 5; attempt++) {
const final_insert_data = await buildInvoiceRows({
rows: body.insert_data as BUN_SQLITE_BSQLITE_INVOICES[],
});
try {
return await BunSQLite.insert({
table,
data: final_insert_data,
update_on_duplicate: body.update_on_duplicate,
});
} catch (error: any) {
if (attempt >= 4) {
throw error;
}
}
}
throw new Error(`Couldn't create invoice`);
}
case "PUT":
return await gatedCrudPut(params);
case "DELETE":
return await gatedCrudDelete(params);
default:
return {
success: false,
msg: `Unhandled`,
};
}
}
@@ -0,0 +1,29 @@
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import {
gatedCrudDelete,
gatedCrudGet,
gatedCrudPost,
gatedCrudPut,
} from "../../(utils)/gated-crud";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
switch (params.req.method) {
case "GET":
return await gatedCrudGet(params);
case "POST":
return await gatedCrudPost(params);
case "PUT":
return await gatedCrudPut(params);
case "DELETE":
return await gatedCrudDelete(params);
default:
return {
success: false,
msg: `Unhandled`,
};
}
}
@@ -0,0 +1,80 @@
import _ from "lodash";
import type { AdminCrudAPIParams, TableType } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import type { ServerQueryParam } from "@moduletrace/bun-sqlite";
import type {
BUN_SQLITE_BSQLITE_ALL_TYPEDEFS,
BUN_SQLITE_BSQLITE_MEDIA,
} from "@/db/types/db";
import BunSQLite from "@moduletrace/bun-sqlite";
import { rmSync } from "fs";
import path from "path";
import grabDirNames from "@/src/utils/grab-dir-names";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { user, user_types, body, id } = params;
const can_delete_media = checkUserAccess({
user_types,
includes: ["admin"],
});
if (!can_delete_media.success) {
return {
success: false,
msg: `Can't delete media`,
};
}
let query = _.merge<
ServerQueryParam<BUN_SQLITE_BSQLITE_ALL_TYPEDEFS>,
ServerQueryParam<BUN_SQLITE_BSQLITE_ALL_TYPEDEFS>
>(body?.sql_query || {}, {});
const media_to_delete = await BunSQLite.select<
BUN_SQLITE_BSQLITE_MEDIA,
TableType
>({
table: "media",
query,
targetId: id,
});
if (!media_to_delete.payload) {
return {
success: false,
msg: `No Media to Delete.`,
};
}
const { DATA_DIR } = grabDirNames();
for (let i = 0; i < media_to_delete.payload.length; i++) {
const media = media_to_delete.payload[i];
if (!media?.id) continue;
if (media.media_write_path) {
rmSync(path.join(DATA_DIR, media.media_write_path), {
force: true,
});
}
if (media.media_thumbnail_write_path) {
rmSync(path.join(DATA_DIR, media.media_thumbnail_write_path), {
force: true,
});
}
await BunSQLite.delete<BUN_SQLITE_BSQLITE_MEDIA, TableType>({
table: "media",
targetId: media.id,
});
}
return {
success: true,
};
}
@@ -0,0 +1,34 @@
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import grabMedia from "@/src/functions/backend/db/media/grab-media";
import type { AdminCrudAPIParams } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import _ from "lodash";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, user_types, body, id, query, user } = params;
let final_sql_query = _.merge(query?.sql_query, body?.sql_query) || {};
delete final_sql_query.join;
const can_user_see_all_media = checkUserAccess({
user_types,
includes: ["admin"],
});
if (!can_user_see_all_media.success) {
final_sql_query.query = {
...final_sql_query.query,
user_id: {
value: user.id,
},
};
}
const GET = await grabMedia({ query, user });
return GET;
}
@@ -0,0 +1,29 @@
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import _ from "lodash";
import get from "./get";
import post from "./post";
import put from "./put";
import del from "./del";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { req, query } = params;
switch (req.method) {
case "GET":
return await get(params);
case "POST":
return await post(params);
case "PUT":
return await put(params);
case "DELETE":
return await del(params);
default:
return {
success: false,
};
}
}
@@ -0,0 +1,36 @@
import type { BUN_SQLITE_BSQLITE_MEDIA } from "@/db/types/db";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import type { AdminCrudAPIParams } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import _ from "lodash";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, body, user_types, user } = params;
let final_insert_data: BUN_SQLITE_BSQLITE_MEDIA[] = [
...(body?.insert_data || []),
].map((d) => ({ ...d, user_id: user.id }));
const is_user_allowed_to_post_any = checkUserAccess({
includes: ["admin"],
user_types,
});
if (!is_user_allowed_to_post_any.success) {
final_insert_data = final_insert_data.map((fid) => ({
...fid,
user_id: user.id,
}));
}
const POST = await BunSQLite.insert({
table,
data: final_insert_data,
update_on_duplicate: body?.update_on_duplicate,
});
return POST;
}
@@ -0,0 +1,40 @@
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import type { AdminCrudAPIParams } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import _ from "lodash";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, body, user_types, id, query, user } = params;
if (!body?.update_data) {
return {
success: false,
msg: `No Update Data`,
};
}
const is_user_allowed_to_put = checkUserAccess({
includes: ["admin"],
user_types,
});
let final_sql_query = _.merge(query?.sql_query, body?.sql_query) || {};
let targetId = id;
if (!is_user_allowed_to_put.success) {
final_sql_query = {};
targetId = user.id;
}
const PUT = await BunSQLite.update({
table,
data: body?.update_data,
targetId,
query: final_sql_query,
});
return PUT;
}
@@ -0,0 +1,29 @@
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import {
gatedCrudDelete,
gatedCrudGet,
gatedCrudPost,
gatedCrudPut,
} from "../../(utils)/gated-crud";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
switch (params.req.method) {
case "GET":
return await gatedCrudGet(params);
case "POST":
return await gatedCrudPost(params);
case "PUT":
return await gatedCrudPut(params);
case "DELETE":
return await gatedCrudDelete(params);
default:
return {
success: false,
msg: `Unhandled`,
};
}
}
@@ -0,0 +1,107 @@
import type { BUN_SQLITE_BSQLITE_QUOTES } from "@/db/types/db";
import { SiteData } from "@/src/data/site-data";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import grabNextDocumentSequence from "@/src/functions/backend/db/documents/grab-next-document-sequence";
import type { AdminCrudAPIParams } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import {
gatedCrudDelete,
gatedCrudGet,
gatedCrudPut,
} from "../../(utils)/gated-crud";
function quoteNumberPrefix(): string {
const now = new Date();
return `${SiteData["SiteSlug"].toUpperCase()}Q-${now.getFullYear()}${String(
now.getMonth() + 1,
).padStart(2, "0")}`;
}
async function buildQuoteRows({
rows,
}: {
rows: BUN_SQLITE_BSQLITE_QUOTES[];
}): Promise<BUN_SQLITE_BSQLITE_QUOTES[]> {
const prefix = quoteNumberPrefix();
let next_sequence: number | undefined;
const final_rows: BUN_SQLITE_BSQLITE_QUOTES[] = [];
for (const row of rows) {
const quote_row = { ...row } as BUN_SQLITE_BSQLITE_QUOTES;
if (!quote_row.quote_number) {
next_sequence ??= await grabNextDocumentSequence({
table: "quotes",
column: "quote_number",
prefix,
});
quote_row.quote_number = `${prefix}-${String(next_sequence).padStart(3, "0")}`;
next_sequence += 1;
}
final_rows.push(quote_row);
}
return final_rows;
}
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
switch (params.req.method) {
case "GET":
return await gatedCrudGet(params);
case "POST": {
const { table, body, user_types } = params;
const access = checkUserAccess({
user_types,
includes: ["admin"],
});
if (!access.success) {
return access;
}
if (!body?.insert_data) {
return {
success: false,
msg: `No data to insert`,
};
}
for (let attempt = 0; attempt < 5; attempt++) {
const final_insert_data = await buildQuoteRows({
rows: body.insert_data as BUN_SQLITE_BSQLITE_QUOTES[],
});
try {
return await BunSQLite.insert({
table,
data: final_insert_data,
update_on_duplicate: body.update_on_duplicate,
});
} catch (error: any) {
if (attempt >= 4) {
throw error;
}
}
}
throw new Error(`Couldn't create quote`);
}
case "PUT":
return await gatedCrudPut(params);
case "DELETE":
return await gatedCrudDelete(params);
default:
return {
success: false,
msg: `Unhandled`,
};
}
}
@@ -0,0 +1,87 @@
import type { BUN_SQLITE_BSQLITE_ALL_TYPEDEFS } from "@/db/types/db";
import {
RolesAdminCantUpdate,
SuperAdminOnlyEditableUserTypes,
UneditableUserTypes,
} from "@/src/dict/user-types-dict";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import type { AdminCrudAPIParams, UserType } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import _ from "lodash";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, user_types, body, id, query } = params;
let final_sql_query = _.merge(query?.sql_query, body?.sql_query) || {};
const records_to_delete =
await BunSQLite.select<BUN_SQLITE_BSQLITE_ALL_TYPEDEFS>({
table,
query: final_sql_query,
targetId: id,
});
if (!records_to_delete.payload?.[0]) {
return {
success: false,
msg: `No Records to Delete`,
};
}
const is_user_super_admin = checkUserAccess({
user_types,
});
const admin_includes: UserType[] = ["admin"];
const can_delete_user_type = checkUserAccess({
user_types,
includes: admin_includes,
});
const is_user_admin = Boolean(
user_types.find(
(ut) => ut.user_type && admin_includes.includes(ut.user_type as any),
),
);
if (!can_delete_user_type.success) {
return can_delete_user_type;
}
const ignored_user_types = [...UneditableUserTypes] as UserType[];
if (!is_user_super_admin.success) {
ignored_user_types.push(...SuperAdminOnlyEditableUserTypes);
}
const user_types_to_delete = records_to_delete.payload.filter(
(rec) => !ignored_user_types.includes(rec.user_type as any),
);
for (let i = 0; i < user_types_to_delete.length; i++) {
const user_type_to_delete = user_types_to_delete[i];
if (!user_type_to_delete?.id) continue;
if (
is_user_admin &&
user_type_to_delete.user_type &&
RolesAdminCantUpdate.includes(user_type_to_delete.user_type as any)
) {
continue;
}
await BunSQLite.delete({
table,
targetId: user_type_to_delete.id,
});
}
return {
success: true,
};
}
@@ -0,0 +1,38 @@
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import type { AdminCrudAPIParams } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import _ from "lodash";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, user_types, body, id, query, user } = params;
let final_sql_query = _.merge(query?.sql_query, body?.sql_query) || {};
delete final_sql_query.join;
const can_user_see_all_user_types = checkUserAccess({
user_types,
includes: ["admin"],
});
if (!can_user_see_all_user_types.success) {
final_sql_query.query = {
...final_sql_query.query,
user_id: {
value: user.id,
},
};
}
const GET = await BunSQLite.select({
table,
query: final_sql_query,
targetId: id,
count: query?.count || body?.count,
});
return GET;
}
@@ -0,0 +1,29 @@
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import _ from "lodash";
import get from "./get";
import post from "./post";
import put from "./put";
import del from "./del";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { req, query } = params;
switch (req.method) {
case "GET":
return await get(params);
case "POST":
return await post(params);
case "PUT":
return await put(params);
case "DELETE":
return await del(params);
default:
return {
success: false,
};
}
}
@@ -0,0 +1,61 @@
import {
RolesAdminCantUpdate,
SuperAdminOnlyEditableUserTypes,
UneditableUserTypes,
} from "@/src/dict/user-types-dict";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import type { AdminCrudAPIParams, UserType } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import _ from "lodash";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, body, user_types } = params;
const is_user_super_admin = checkUserAccess({
user_types,
});
const ignored_user_types = [...UneditableUserTypes] as UserType[];
if (!is_user_super_admin.success) {
ignored_user_types.push(...SuperAdminOnlyEditableUserTypes);
}
let final_insert_data = [...(body?.insert_data || [])].filter(
(i) => !ignored_user_types.includes(i.user_type as any),
);
const admin_includes: UserType[] = ["admin"];
const is_user_allowed_to_post = checkUserAccess({
includes: admin_includes,
user_types,
});
if (!is_user_allowed_to_post.success) {
return is_user_allowed_to_post;
}
const is_user_admin = Boolean(
user_types.find(
(ut) => ut.user_type && admin_includes.includes(ut.user_type as any),
),
);
if (is_user_admin) {
final_insert_data = final_insert_data.filter(
(d) => d.user_type && !RolesAdminCantUpdate.includes(d.user_type as any),
);
}
const POST = await BunSQLite.insert({
table,
data: final_insert_data,
update_on_duplicate: body?.update_on_duplicate,
});
return POST;
}
@@ -0,0 +1,14 @@
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import _ from "lodash";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, body, user_types, id, query } = params;
return {
success: false,
msg: `Can't update user_type. Can only delete and insert.`,
};
}
@@ -0,0 +1,11 @@
/**
* The only `users` columns writable through the CRUD API. Anything else
* (`id`, `archived`, timestamps) is stripped from insert/update payloads.
*/
export const UserUpdatableFields = [
"first_name",
"last_name",
"email",
"username",
"bio",
] as const;
@@ -0,0 +1,52 @@
import type { BUN_SQLITE_BSQLITE_ALL_TYPEDEFS } from "@/db/types/db";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import checkAllowedJoins from "@/src/functions/backend/db/check-allowed-joins";
import grabUsers from "@/src/functions/backend/db/users/grab-users";
import type { AdminCrudAPIParams, ApiReqParams, TableType } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import _ from "lodash";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { req, table, user_types, body, id, query, user } = params;
let final_sql_query = _.merge(query?.sql_query, body?.sql_query) || {};
let final_query =
_.merge<
ApiReqParams<BUN_SQLITE_BSQLITE_ALL_TYPEDEFS>,
ApiReqParams<BUN_SQLITE_BSQLITE_ALL_TYPEDEFS>
>(query || {}, { sql_query: final_sql_query }) || {};
const allowed_joins: TableType[] = ["user_types", "media"];
const check_allowed_joins = checkAllowedJoins({
joins: allowed_joins,
query: final_sql_query,
});
if (!check_allowed_joins.success) {
return check_allowed_joins;
}
const can_user_see_all_users = checkUserAccess({
user_types,
includes: ["admin"],
});
if (!can_user_see_all_users.success) {
final_sql_query.query = {
...final_sql_query.query,
user_id: {
value: user.id,
},
};
}
const GET = await grabUsers({
query: final_query,
user_id: id,
});
return GET;
}
@@ -0,0 +1,31 @@
import type { AdminCrudAPIParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import _ from "lodash";
import get from "./get";
import post from "./post";
import put from "./put";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { req } = params;
switch (req.method) {
case "GET":
return await get(params);
case "POST":
return await post(params);
case "PUT":
return await put(params);
case "DELETE":
return {
success: false,
msg: `Can't delete any user`,
};
default:
return {
success: false,
};
}
}
@@ -0,0 +1,43 @@
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import sanitizeUserBio from "@/src/functions/backend/db/users/sanitize-user-bio";
import type { AdminCrudAPIParams } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import _ from "lodash";
import { UserUpdatableFields } from "./(data)/user-updatable-fields";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, body, user_types } = params;
const is_user_allowed_to_post = checkUserAccess({
includes: ["admin"],
user_types,
});
if (!is_user_allowed_to_post.success) {
return is_user_allowed_to_post;
}
const final_insert_data = (body?.insert_data || []).map((row) =>
sanitizeUserBio({
user: _.pick(row, UserUpdatableFields),
}),
);
if (!final_insert_data[0]) {
return {
success: false,
msg: `No data to insert`,
};
}
const POST = await BunSQLite.insert({
table,
data: final_insert_data,
update_on_duplicate: body?.update_on_duplicate,
});
return POST;
}
@@ -0,0 +1,63 @@
import type { AdminCrudAPIParams } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import _ from "lodash";
import canUpdateUser from "@/src/utils/rbac/can-update-user";
import grabUserTypes from "@/src/functions/backend/db/users/grab-user-types";
import sanitizeUserBio from "@/src/functions/backend/db/users/sanitize-user-bio";
import { UserUpdatableFields } from "./(data)/user-updatable-fields";
export default async function (
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, body, user_types, id, user } = params;
if (!body?.update_data) {
return {
success: false,
msg: `No Update Data`,
};
}
const update_data = sanitizeUserBio({
user: _.pick(body.update_data, UserUpdatableFields),
});
const update_fields = Object.keys(update_data);
if (!update_fields[0]) {
return {
success: false,
msg: `No updatable fields passed`,
};
}
const target_user_id = id || user.id;
const target_user_types =
String(target_user_id) === String(user.id)
? user_types
: await grabUserTypes({ user_id: target_user_id });
const can_update = canUpdateUser({
actor_user_types: user_types,
actor_user_id: user.id,
target_user_id,
target_user_types,
update_fields,
});
if (!can_update) {
return {
success: false,
msg: `Unauthorized`,
};
}
const PUT = await BunSQLite.update({
table,
data: update_data,
targetId: target_user_id,
});
return PUT;
}
@@ -0,0 +1,100 @@
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import type { AdminCrudAPIParams } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import _ from "lodash";
/**
* # Shared admin-gated CRUD actions for app tables.
* Gates every action on `admin` (or super_admin) then performs the
* standard table operation. Keeps table folders thin.
*/
export async function gatedCrudGet(
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, body, id, query, user_types } = params;
const access = checkUserAccess({ user_types, includes: ["admin"] });
if (!access.success) {
return access;
}
const final_sql_query = _.merge(query?.sql_query, body?.sql_query);
return await BunSQLite.select({
table,
query: final_sql_query,
targetId: id,
count: query?.count || body?.count,
});
}
export async function gatedCrudPost(
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, body, user_types } = params;
const access = checkUserAccess({ user_types, includes: ["admin"] });
if (!access.success) {
return access;
}
if (!body?.insert_data) {
return {
success: false,
msg: `No data to insert`,
};
}
return await BunSQLite.insert({
table,
data: [...body.insert_data],
update_on_duplicate: body.update_on_duplicate,
});
}
export async function gatedCrudPut(
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, body, id, query, user_types } = params;
const access = checkUserAccess({ user_types, includes: ["admin"] });
if (!access.success) {
return access;
}
if (!body?.update_data) {
return {
success: false,
msg: `No data to update`,
};
}
return await BunSQLite.update({
table,
data: body.update_data,
query: _.merge(query?.sql_query, body?.sql_query),
targetId: id,
});
}
export async function gatedCrudDelete(
params: AdminCrudAPIParams,
): Promise<APIResponseObject> {
const { table, body, id, query, user_types } = params;
const access = checkUserAccess({ user_types, includes: ["admin"] });
if (!access.success) {
return access;
}
return await BunSQLite.delete({
table,
query: _.merge(query?.sql_query, body?.sql_query),
targetId: id,
});
}
+146
View File
@@ -0,0 +1,146 @@
import _ from "lodash";
import users from "./(tables)/users";
import userTypes from "./(tables)/user_types";
import media from "./(tables)/media";
import letters from "./(tables)/letters";
import emails from "./(tables)/emails";
import emailAttachments from "./(tables)/email_attachments";
import invoices from "./(tables)/invoices";
import invoiceItems from "./(tables)/invoice_items";
import quotes from "./(tables)/quotes";
import quoteItems from "./(tables)/quote_items";
import invoiceQuotes from "./(tables)/invoice_quotes";
import get from "./(functions)/get";
import put from "./(functions)/put";
import del from "./(functions)/delete";
import post from "./(functions)/post";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
import type { AdminCrudAPIParams, ApiReqParams, TableType } from "@/src/types";
import userAuth from "@/src/functions/backend/auth/user-auth";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
import { PrivateTables } from "@/src/dict/private-tables";
import safeErrorMessage from "@/src/utils/safe-error-message";
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
params,
) => {
const req = params.req;
const body = params.body as ApiReqParams;
const query = params.query as ApiReqParams;
const { user, user_types } = await userAuth({ req });
if (!user?.logged_in_status) {
return {
success: false,
msg: `Unauthorized`,
logoutUser: true,
};
}
if (!user_types?.[0]) {
return {
success: false,
msg: `User types not found`,
logoutUser: true,
};
}
try {
const [table, id] = params.query.paths.split("/") as [
TableType,
string | number,
];
if (PrivateTables.includes(table)) {
return {
success: false,
msg: `Unauthorized`,
};
}
if (isReadOnlyUser({ user_types }) && !req.method.match(/get/)) {
const is_self_user_profile_update =
req.method == "PUT" &&
table == "users" &&
(!id || String(id) === String(user.id));
if (!is_self_user_profile_update) {
throw new Error(`This user can only read records.`);
}
}
const crud_params: AdminCrudAPIParams = {
table,
id,
user: user || undefined,
user_types: user_types || [],
body,
query,
req,
};
switch (table) {
case "users":
return await users(crud_params);
case "user_types":
return await userTypes(crud_params);
case "media":
return await media(crud_params);
case "letters":
return await letters(crud_params);
case "emails":
return await emails(crud_params);
case "email_attachments":
return await emailAttachments(crud_params);
case "invoices":
return await invoices(crud_params);
case "invoice_items":
return await invoiceItems(crud_params);
case "quotes":
return await quotes(crud_params);
case "quote_items":
return await quoteItems(crud_params);
case "invoice_quotes":
return await invoiceQuotes(crud_params);
default:
break;
}
const is_super_admin = checkUserAccess({ user_types });
if (!is_super_admin.success) {
return {
success: false,
msg: `Unauthorized`,
};
}
switch (req.method) {
case "GET":
return await get(crud_params);
case "POST":
return await post(crud_params);
case "PUT":
return await put(crud_params);
case "DELETE":
return await del(crud_params);
default:
return {
success: false,
msg: `Unhandled`,
};
}
} catch (error: unknown) {
return {
success: false,
msg: safeErrorMessage({ error }),
};
}
};
+58
View File
@@ -0,0 +1,58 @@
import _ from "lodash";
import userAuth from "@/src/functions/backend/auth/user-auth";
import type { ApiReqParams } from "@/src/types";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
import deleteMedia from "@/src/functions/backend/db/media/delete-media";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
params,
) => {
const req = params.req;
const body = params.body as ApiReqParams;
if (req.method !== "DELETE") {
return {
success: false,
};
}
const { user, user_types } = await userAuth({ req });
if (!user?.logged_in_status) {
return {
success: false,
msg: `Unauthorized`,
logoutUser: true,
};
}
try {
const { id, ids, media_paradigm, user_id } = body;
const can_delete_all_media = checkUserAccess({
includes: ["admin"],
user_types,
});
if (user_id && user.id !== user_id && !can_delete_all_media.success) {
throw new Error(`Operation not allowed!`);
}
return await deleteMedia({
id,
ids,
media_paradigm,
user_id,
can_delete_all_media: can_delete_all_media.success,
});
} catch (error: any) {
return {
success: false,
msg: `API Error: ${error.message}`,
};
}
};
+244
View File
@@ -0,0 +1,244 @@
import TurndownService from "turndown";
import { SiteData } from "@/src/data/site-data";
import grabEmail from "@/src/functions/backend/db/documents/grab-email";
import grabLetter from "@/src/functions/backend/db/documents/grab-letter";
import grabEmailAttachments from "@/src/functions/backend/db/documents/grab-email-attachments";
import grabInvoice, {
grabInvoiceItems,
} from "@/src/functions/backend/db/invoices/grab-invoice";
import grabQuote, {
grabQuoteItems,
} from "@/src/functions/backend/db/quotes/grab-quote";
import sendEmail from "@/src/functions/backend/email/send-email";
import generateLetterPdf from "@/src/functions/backend/pdf/letter-pdf";
import generateInvoicePdf from "@/src/functions/backend/pdf/invoice-pdf";
import userAuth from "@/src/functions/backend/auth/user-auth";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import twuiSlugify from "@moduletrace/twui/utils/slugify";
import grabDirNames from "@/src/utils/grab-dir-names";
import type { ApiReqParams } from "@/src/types";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import path from "path";
const turndown_service = new TurndownService({
headingStyle: "atx",
codeBlockStyle: "fenced",
});
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
params,
) => {
try {
const body = params.body as ApiReqParams;
const { user, user_types } = await userAuth({ req: params.req });
if (!user?.logged_in_status) {
return {
success: false,
msg: `Unauthorized`,
logoutUser: true,
};
}
const access = checkUserAccess({
user_types,
includes: ["admin"],
});
if (!access.success) {
return access;
}
const email_id = body.email_id;
if (!email_id) {
throw new Error(`Email not found`);
}
const email_res = await grabEmail({ id: email_id });
const email = email_res.singleRes;
if (!email) {
throw new Error(`Email not found`);
}
if (email.status == "sent") {
throw new Error(`This email has already been sent`);
}
const to_emails = (email.to_emails || "").trim();
if (!to_emails) {
throw new Error(
`Please provide a recipient email address`,
);
}
let attachments: {
filename: string;
content: Buffer;
contentType: string;
}[] | undefined;
const attachments_res = await grabEmailAttachments({ email_id });
const attachment_rows = attachments_res.payload || [];
if (attachment_rows[0]) {
attachments = [];
for (const attachment of attachment_rows) {
if (attachment.type == "letter" && attachment.letter_id) {
const letter_res = await grabLetter({
id: attachment.letter_id,
});
const letter = letter_res.singleRes;
if (!letter) {
throw new Error(`Attached letter not found`);
}
const pdf = await generateLetterPdf({
template: letter,
});
attachments.push({
filename: `${twuiSlugify(letter.title || "letter")}.pdf`,
content: Buffer.from(pdf),
contentType: "application/pdf",
});
} else if (attachment.type == "invoice" && attachment.invoice_id) {
const invoice_res = await grabInvoice({
id: attachment.invoice_id,
});
const invoice = invoice_res.singleRes;
if (!invoice) {
throw new Error(`Attached invoice not found`);
}
const items_res = await grabInvoiceItems({
invoice_id: attachment.invoice_id,
});
const items = items_res.payload || [];
const pdf = await generateInvoicePdf({
invoice,
items,
});
attachments.push({
filename: `${twuiSlugify(invoice.invoice_number || "invoice")}.pdf`,
content: Buffer.from(pdf),
contentType: "application/pdf",
});
} else if (attachment.type == "quote" && attachment.quote_id) {
const quote_res = await grabQuote({
id: attachment.quote_id,
});
const quote = quote_res.singleRes;
if (!quote) {
throw new Error(`Attached quote not found`);
}
const items_res = await grabQuoteItems({
quote_id: attachment.quote_id,
});
const items = items_res.payload || [];
const pdf = await generateInvoicePdf({
invoice: {
...quote,
invoice_number: quote.quote_number || "",
},
items,
kind: "quote",
});
attachments.push({
filename: `${twuiSlugify(quote.quote_number || "quote")}.pdf`,
content: Buffer.from(pdf),
contentType: "application/pdf",
});
} else if (attachment.media_write_path) {
const { DATA_DIR } = grabDirNames();
const media_file = Bun.file(
path.join(DATA_DIR, attachment.media_write_path),
);
if (!media_file.size) {
throw new Error(
`Attachment file not found: ${attachment.file_name || "untitled"}`,
);
}
attachments.push({
filename:
attachment.file_name ||
`${attachment.id || "attachment"}`,
content: Buffer.from(await media_file.arrayBuffer()),
contentType:
attachment.mime_type ||
attachment.media_mime_type ||
"application/octet-stream",
});
}
}
if (!attachments[0]) {
attachments = undefined;
}
}
const plain_body = turndown_service.turndown(
email.body_html || "",
);
const send_res = await sendEmail({
to: to_emails,
subject:
email.subject ||
email.title ||
`Email from ${SiteData["SiteName"]}`,
text: plain_body,
content: (
<div
dangerouslySetInnerHTML={{
__html: email.body_html || "",
}}
/>
),
title: `${SiteData["SiteName"]}`,
options: attachments ? { attachments } : undefined,
});
if (!send_res.success) {
return send_res;
}
await BunSQLite.update({
table: "emails",
data: {
status: "sent",
sent_at: Date.now(),
},
targetId: email.id,
});
return {
success: true,
msg: `Email sent successfully to ${to_emails}`,
};
} catch (error: any) {
return {
success: false,
msg: error.message,
};
}
};
@@ -0,0 +1,134 @@
import type {
BUN_SQLITE_BSQLITE_EMAIL_ATTACHMENTS,
} from "@/db/types/db";
import deleteMedia from "@/src/functions/backend/db/media/delete-media";
import userAuth from "@/src/functions/backend/auth/user-auth";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import type { ApiReqParams, TableType } from "@/src/types";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
import BunSQLite from "@moduletrace/bun-sqlite";
export type EmailAttachmentDraft = {
type: "letter" | "invoice" | "quote" | "file";
letter_id?: number;
invoice_id?: number;
quote_id?: number;
media_id?: number;
file_name?: string;
mime_type?: string;
};
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
params,
) => {
try {
const body = params.body as ApiReqParams;
const { user, user_types } = await userAuth({ req: params.req });
if (!user?.logged_in_status) {
return {
success: false,
msg: `Unauthorized`,
logoutUser: true,
};
}
const access = checkUserAccess({
user_types,
includes: ["admin"],
});
if (!access.success) {
return access;
}
const email_id = body.email_id;
const attachments: EmailAttachmentDraft[] =
(
body as ApiReqParams & {
attachments?: EmailAttachmentDraft[];
}
).attachments || [];
if (!email_id) {
throw new Error(`Email not found`);
}
const existing_res = await BunSQLite.select<BUN_SQLITE_BSQLITE_EMAIL_ATTACHMENTS>({
table: "email_attachments",
query: {
query: { email_id: { value: email_id } },
},
});
const existing = existing_res.payload || [];
const new_file_media_ids = attachments
.filter((a) => a.type == "file" && a.media_id)
.map((a) => Number(a.media_id));
for (const existing_attachment of existing) {
if (
existing_attachment.type == "file" &&
existing_attachment.media_id &&
!new_file_media_ids.includes(
Number(existing_attachment.media_id),
)
) {
await deleteMedia({
ids: [existing_attachment.media_id],
can_delete_all_media: true,
});
}
}
await BunSQLite.delete({
table: "email_attachments",
query: {
query: { email_id: { value: email_id } },
},
});
if (attachments[0]) {
const insert_data = attachments.map((attachment) => ({
email_id: Number(email_id),
type: attachment.type,
letter_id:
attachment.type == "letter" ? attachment.letter_id : undefined,
invoice_id:
attachment.type == "invoice"
? attachment.invoice_id
: undefined,
quote_id:
attachment.type == "quote" ? attachment.quote_id : undefined,
media_id:
attachment.type == "file" ? attachment.media_id : undefined,
file_name: attachment.file_name,
mime_type: attachment.mime_type,
}));
const insert_res = await BunSQLite.insert<BUN_SQLITE_BSQLITE_EMAIL_ATTACHMENTS, TableType>({
table: "email_attachments",
data: insert_data,
});
if (!insert_res.success) {
throw new Error(insert_res.msg || `Couldn't save attachments`);
}
}
return {
success: true,
msg: `Attachments saved`,
};
} catch (error: any) {
return {
success: false,
msg: error.message,
};
}
};
+39
View File
@@ -0,0 +1,39 @@
import userAuth from "@/src/functions/backend/auth/user-auth";
import grabUsers from "@/src/functions/backend/db/users/grab-users";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
import _ from "lodash";
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
req,
query,
}) => {
const { user, user_types } = await userAuth({ req });
if (!user?.logged_in_status) {
return {
success: false,
msg: `Unauthorized`,
};
}
try {
if (!user_types?.[0]) {
return {
success: false,
msg: `Unauthorized`,
};
}
return await grabUsers({
query,
});
} catch (error: any) {
return {
success: false,
msg: error.message,
};
}
};
@@ -0,0 +1,76 @@
import grabInvoice, {
grabInvoiceItems,
} from "@/src/functions/backend/db/invoices/grab-invoice";
import generateInvoicePdf from "@/src/functions/backend/pdf/invoice-pdf";
import userAuth from "@/src/functions/backend/auth/user-auth";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import twuiSlugify from "@moduletrace/twui/utils/slugify";
import type { ApiReqParams } from "@/src/types";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
params,
) => {
try {
const body = params.body as ApiReqParams;
const { user, user_types } = await userAuth({ req: params.req });
if (!user?.logged_in_status) {
return {
success: false,
msg: `Unauthorized`,
logoutUser: true,
};
}
const access = checkUserAccess({
user_types,
includes: ["admin"],
});
if (!access.success) {
return access;
}
const invoice_id = body?.invoice_id;
if (!invoice_id) {
throw new Error(`Invoice not found`);
}
const invoice_res = await grabInvoice({ id: invoice_id });
const invoice = invoice_res.singleRes;
if (!invoice) {
throw new Error(`Invoice not found`);
}
const items_res = await grabInvoiceItems({
invoice_id,
});
const pdf = await generateInvoicePdf({
invoice,
items: items_res.payload || [],
});
const filename = `${twuiSlugify(invoice.invoice_number || "invoice")}.pdf`;
return new Response(new Uint8Array(pdf), {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition":
`attachment; filename="${filename}"`,
},
});
} catch (error: any) {
return {
success: false,
msg: error.message,
};
}
};
+111
View File
@@ -0,0 +1,111 @@
import grabInvoice, {
grabInvoiceItems,
} from "@/src/functions/backend/db/invoices/grab-invoice";
import { SiteData } from "@/src/data/site-data";
import { InvoiceEmailContent } from "@/src/functions/backend/email/document-emails";
import sendEmail from "@/src/functions/backend/email/send-email";
import generateInvoicePdf from "@/src/functions/backend/pdf/invoice-pdf";
import userAuth from "@/src/functions/backend/auth/user-auth";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import twuiSlugify from "@moduletrace/twui/utils/slugify";
import type { ApiReqParams } from "@/src/types";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
params,
) => {
try {
const body = params.body as ApiReqParams;
const { user, user_types } = await userAuth({ req: params.req });
if (!user?.logged_in_status) {
return {
success: false,
msg: `Unauthorized`,
logoutUser: true,
};
}
const access = checkUserAccess({
user_types,
includes: ["admin"],
});
if (!access.success) {
return access;
}
const invoice_id = body?.invoice_id;
if (!invoice_id) {
throw new Error(`Invoice not found`);
}
const invoice_res = await grabInvoice({ id: invoice_id });
const invoice = invoice_res.singleRes;
if (!invoice) {
throw new Error(`Invoice not found`);
}
const to_email = body?.to_email || invoice.client_email;
if (!to_email) {
throw new Error(
`Please provide a recipient email address`,
);
}
const items_res = await grabInvoiceItems({
invoice_id,
});
const items = items_res.payload || [];
const pdf = await generateInvoicePdf({
invoice,
items,
});
const pdf_filename = `${twuiSlugify(invoice.invoice_number || "invoice")}.pdf`;
const send_res = await sendEmail({
to: to_email,
subject: `Invoice ${invoice.invoice_number || ""} from ${SiteData["SiteName"]}`,
text: `Please find attached invoice ${invoice.invoice_number || ""} for your records.`,
content: (
<InvoiceEmailContent
invoice={invoice}
items={items}
/>
),
title: `${SiteData["SiteName"]}`,
options: {
attachments: [
{
filename: pdf_filename,
content: Buffer.from(pdf),
contentType: "application/pdf",
},
],
},
});
if (!send_res.success) {
return send_res;
}
return {
success: true,
msg: `Invoice sent successfully to ${to_email}`,
};
} catch (error: any) {
return {
success: false,
msg: error.message,
};
}
};
@@ -0,0 +1,73 @@
import grabLetter from "@/src/functions/backend/db/documents/grab-letter";
import generateLetterPdf from "@/src/functions/backend/pdf/letter-pdf";
import userAuth from "@/src/functions/backend/auth/user-auth";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import twuiSlugify from "@moduletrace/twui/utils/slugify";
import type { ApiReqParams } from "@/src/types";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
params,
) => {
try {
const body = params.body as ApiReqParams;
const { user, user_types } = await userAuth({ req: params.req });
if (!user?.logged_in_status) {
return {
success: false,
msg: `Unauthorized`,
logoutUser: true,
};
}
const access = checkUserAccess({
user_types,
includes: ["admin"],
});
if (!access.success) {
return access;
}
const letter_id = body?.letter_id;
if (!letter_id) {
throw new Error(`Letter not found`);
}
const letter_res = await grabLetter({ id: letter_id });
const letter = letter_res.singleRes;
if (!letter) {
throw new Error(`Letter not found`);
}
const pdf = await generateLetterPdf({
template: letter,
to_name: body?.to_name,
to_address: body?.to_address,
letter_date: body?.letter_date,
});
const filename = `${twuiSlugify(letter.title || "letter")}.pdf`;
return new Response(new Uint8Array(pdf), {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition":
`attachment; filename="${filename}"`,
},
});
} catch (error: any) {
return {
success: false,
msg: error.message,
};
}
};
@@ -0,0 +1,80 @@
import grabQuote, {
grabQuoteItems,
} from "@/src/functions/backend/db/quotes/grab-quote";
import generateInvoicePdf from "@/src/functions/backend/pdf/invoice-pdf";
import userAuth from "@/src/functions/backend/auth/user-auth";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import twuiSlugify from "@moduletrace/twui/utils/slugify";
import type { ApiReqParams } from "@/src/types";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
params,
) => {
try {
const body = params.body as ApiReqParams;
const { user, user_types } = await userAuth({ req: params.req });
if (!user?.logged_in_status) {
return {
success: false,
msg: `Unauthorized`,
logoutUser: true,
};
}
const access = checkUserAccess({
user_types,
includes: ["admin"],
});
if (!access.success) {
return access;
}
const quote_id = body?.quote_id;
if (!quote_id) {
throw new Error(`Quote not found`);
}
const quote_res = await grabQuote({ id: quote_id });
const quote = quote_res.singleRes;
if (!quote) {
throw new Error(`Quote not found`);
}
const items_res = await grabQuoteItems({
quote_id,
});
const pdf = await generateInvoicePdf({
invoice: {
...quote,
invoice_number: quote.quote_number || "",
},
items: items_res.payload || [],
kind: "quote",
});
const filename = `${twuiSlugify(quote.quote_number || "quote")}.pdf`;
return new Response(new Uint8Array(pdf), {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition":
`attachment; filename="${filename}"`,
},
});
} catch (error: any) {
return {
success: false,
msg: error.message,
};
}
};
+115
View File
@@ -0,0 +1,115 @@
import grabQuote, {
grabQuoteItems,
} from "@/src/functions/backend/db/quotes/grab-quote";
import { SiteData } from "@/src/data/site-data";
import { QuoteEmailContent } from "@/src/functions/backend/email/document-emails";
import sendEmail from "@/src/functions/backend/email/send-email";
import generateInvoicePdf from "@/src/functions/backend/pdf/invoice-pdf";
import userAuth from "@/src/functions/backend/auth/user-auth";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import twuiSlugify from "@moduletrace/twui/utils/slugify";
import type { ApiReqParams } from "@/src/types";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
params,
) => {
try {
const body = params.body as ApiReqParams;
const { user, user_types } = await userAuth({ req: params.req });
if (!user?.logged_in_status) {
return {
success: false,
msg: `Unauthorized`,
logoutUser: true,
};
}
const access = checkUserAccess({
user_types,
includes: ["admin"],
});
if (!access.success) {
return access;
}
const quote_id = body?.quote_id;
if (!quote_id) {
throw new Error(`Quote not found`);
}
const quote_res = await grabQuote({ id: quote_id });
const quote = quote_res.singleRes;
if (!quote) {
throw new Error(`Quote not found`);
}
const to_email = body?.to_email || quote.client_email;
if (!to_email) {
throw new Error(
`Please provide a recipient email address`,
);
}
const items_res = await grabQuoteItems({
quote_id,
});
const items = items_res.payload || [];
const pdf = await generateInvoicePdf({
invoice: {
...quote,
invoice_number: quote.quote_number || "",
},
items,
kind: "quote",
});
const pdf_filename = `${twuiSlugify(quote.quote_number || "quote")}.pdf`;
const send_res = await sendEmail({
to: to_email,
subject: `Quotation ${quote.quote_number || ""} from ${SiteData["SiteName"]}`,
text: `Please find attached quotation ${quote.quote_number || ""} for our proposed works.`,
content: (
<QuoteEmailContent
quote={quote}
items={items}
/>
),
title: `${SiteData["SiteName"]}`,
options: {
attachments: [
{
filename: pdf_filename,
content: Buffer.from(pdf),
contentType: "application/pdf",
},
],
},
});
if (!send_res.success) {
return send_res;
}
return {
success: true,
msg: `Quotation sent successfully to ${to_email}`,
};
} catch (error: any) {
return {
success: false,
msg: error.message,
};
}
};
@@ -0,0 +1,107 @@
import userAuth from "@/src/functions/backend/auth/user-auth";
import grabUserPasswordHash from "@/src/functions/backend/auth/grab-user-password-hash";
import verifyUserPassword from "@/src/functions/backend/auth/verify-user-password";
import setUserPassword from "@/src/functions/backend/auth/set-user-password";
import grabLoginMethod from "@/src/functions/backend/auth/grab-login-method";
import logEvent, { grabRequestIP } from "@/src/functions/backend/logger";
import { AppData } from "@/src/data/app-data";
import type { ApiReqParams } from "@/src/types";
import rateLimit from "@/src/utils/rate-limit";
import validatePassword from "@/src/utils/validate-password";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
/**
* Sets or changes the logged-in user's own password (password login mode
* only). Acts only on the session user; changing an existing password
* needs the current one.
*/
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
req,
body,
}) => {
try {
if (req.method != "POST") {
throw new Error(`Method not allowed`);
}
const { user } = await userAuth({ req });
if (!user?.logged_in_status || !user.id) {
return {
success: false,
msg: `Unauthorized`,
logoutUser: true,
};
}
if (grabLoginMethod() != "password") {
throw new Error(`Password login is turned off.`);
}
const user_id = user.id;
const { password_update } = body as ApiReqParams;
if (!password_update) {
throw new Error(`No password update provided`);
}
const has_password = Boolean(await grabUserPasswordHash({ user_id }));
if (has_password) {
const attempts = rateLimit({
key: `password-update:${user_id}`,
limit: AppData["PasswordAttemptLimit"],
window_ms: AppData["RateLimitWindowMinutes"] * 60 * 1000,
});
if (!attempts.allowed) {
throw new Error(`Too many attempts. Please try again later.`);
}
const is_valid = password_update.current_password
? await verifyUserPassword({
user_id,
password: password_update.current_password,
})
: false;
if (!is_valid) {
throw new Error(`Your current password is incorrect.`);
}
}
const password_error = validatePassword({
password: password_update.new_password,
});
if (password_error || !password_update.new_password) {
throw new Error(password_error);
}
await setUserPassword({
user_id,
password: password_update.new_password,
});
await logEvent({
title: has_password ? "Password changed" : "Password set",
category: "auth",
user_id,
plain_text: `User #${user_id} ${has_password ? "changed" : "set"} their password.`,
ip_address: grabRequestIP(req),
});
return {
success: true,
msg: has_password ? `Password changed.` : `Password set.`,
};
} catch (error: any) {
return {
success: false,
msg: error.message,
};
}
};
+127
View File
@@ -0,0 +1,127 @@
import twuiNumberfy from "@moduletrace/twui/utils/numberfy";
import loginUser from "@/src/functions/backend/auth/login-user";
import userAuth from "@/src/functions/backend/auth/user-auth";
import uploadAndRecordMedia from "@/src/functions/backend/db/users/media/upload-and-record-media";
import type { ApiReqParams } from "@/src/types";
import bufferFromBase64 from "@/src/utils/buffer-from-base-64";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
import _ from "lodash";
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
import canUpdateUser from "@/src/utils/rbac/can-update-user";
import grabUserTypes from "@/src/functions/backend/db/users/grab-user-types";
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
params,
) => {
const req = params.req;
const body = params.body as ApiReqParams;
if (req.method !== "POST") {
return {
success: false,
};
}
const { user, user_types } = await userAuth({ req });
if (!user?.logged_in_status) {
return {
success: false,
msg: `Unauthorized`,
logoutUser: true,
};
}
try {
const {
user_id,
media_base_64_data_url,
media_paradigms,
media_type,
is_media_primary,
update_on_duplicate,
reauth,
return_media_text_content,
media,
is_media_private,
media_name,
} = body;
/**
* Media is owned by the logged in user unless an actor with
* permission to update the target user uploads on their behalf.
*/
const target_user_id = user_id ? twuiNumberfy(user_id) : user.id;
const is_self = String(target_user_id) === String(user.id);
/**
* `read_only` users cannot modify records, but they may still manage
* their own profile image.
*/
if (isReadOnlyUser({ user_types })) {
const is_own_profile_image =
is_self && media_paradigms?.includes("user-profile-image");
if (!is_own_profile_image) {
throw new Error(`This user can only read records.`);
}
}
if (!is_self) {
const target_user_types = await grabUserTypes({
user_id: target_user_id,
});
const can_update_target = canUpdateUser({
actor_user_types: user_types,
actor_user_id: user.id,
target_user_id,
target_user_types,
});
if (!can_update_target) {
throw new Error(`Operation not allowed!`);
}
}
if (!media_base_64_data_url) {
throw new Error(
`No image payload sent! Base64 data URL with mime type is required.`,
);
}
const { buffer, ext } = await bufferFromBase64(media_base_64_data_url);
const new_media = await uploadAndRecordMedia({
data: buffer,
media_paradigms,
media_type,
is_primary: is_media_primary,
update_on_duplicate,
extract_media_text_content: return_media_text_content,
media,
media_mime_type: ext,
user: { id: target_user_id },
is_private: is_media_private,
media_name,
});
if (reauth) {
return await loginUser({
user_id: Number(user.id),
});
}
return {
...new_media,
};
} catch (error: any) {
return {
success: false,
msg: error.message,
};
}
};
+23
View File
@@ -0,0 +1,23 @@
import userAuth from "@/src/functions/backend/auth/user-auth";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
req,
}) => {
const { user, user_types } = await userAuth({ req });
if (!user?.logged_in_status) {
return {
success: false,
msg: `Unauthorized`,
};
}
return {
success: true,
singleRes: user,
payload: user_types,
};
};