First Commit. Duplicate bunext-sqlite preset
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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 }),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user