notification added in with subs :D

This commit is contained in:
2026-03-20 23:43:52 -05:00
parent 751c8f21ab
commit 2021141967
37 changed files with 5174 additions and 359 deletions

View File

@@ -0,0 +1,29 @@
import {
boolean,
jsonb,
pgTable,
text,
uniqueIndex,
uuid,
} from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type z from "zod";
export const notifications = pgTable(
"notifications",
{
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
description: text("description").notNull(),
active: boolean("active").default(false),
interval: text("interval").default("5"),
options: jsonb("options").default([]),
},
(table) => [uniqueIndex("notify_name").on(table.name)],
);
export const notificationSchema = createSelectSchema(notifications);
export const newNotificationSchema = createInsertSchema(notifications);
export type Notification = z.infer<typeof notificationSchema>;
export type NewNotification = z.infer<typeof newNotificationSchema>;

View File

@@ -0,0 +1,30 @@
import { pgTable, text, unique, uuid } from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type z from "zod";
import { user } from "./auth.schema.js";
import { notifications } from "./notifications.schema.js";
export const notificationSub = pgTable(
"notification_sub",
{
id: uuid("id").defaultRandom().primaryKey(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
notificationId: uuid("notification_id")
.notNull()
.references(() => notifications.id, { onDelete: "cascade" }),
emails: text("emails").array().default([]),
},
(table) => ({
userNotificationUnique: unique(
"notification_sub_user_notification_unique",
).on(table.userId, table.notificationId),
}),
);
export const notificationSubSchema = createSelectSchema(notificationSub);
export const newNotificationSubSchema = createInsertSchema(notificationSub);
export type NotificationSub = z.infer<typeof notificationSubSchema>;
export type NewNotificationSub = z.infer<typeof newNotificationSubSchema>;

View File

@@ -0,0 +1,52 @@
import type { NextFunction, Request, Response } from "express";
import { auth } from "../utils/auth.utils.js";
type PermissionMap = Record<string, string[]>;
declare global {
namespace Express {
interface Request {
authz?: {
success: boolean;
permissions: PermissionMap;
};
}
}
}
function normalizeRoles(roles: unknown): string {
if (Array.isArray(roles)) return roles.join(",");
if (typeof roles === "string") return roles;
return "";
}
export function requirePermission(permissions: PermissionMap) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const role = normalizeRoles(req.user?.roles) as any;
const result = await auth.api.userHasPermission({
body: {
role,
permissions,
},
});
req.authz = {
success: !!result?.success,
permissions,
};
if (!result?.success) {
return res.status(403).json({
ok: false,
message: "You do not have permission to perform this action.",
});
}
next();
} catch (error) {
next(error);
}
};
}

View File

@@ -0,0 +1,153 @@
import { notificationSub } from "db/schema/notifications.sub.schema.js";
import { eq } from "drizzle-orm";
import { createLogger } from "logger/logger.controller.js";
import { minutesToCron } from "utils/croner.minConvert.js";
import { createCronJob, stopCronJob } from "utils/croner.utils.js";
import { db } from "../db/db.controller.js";
import { notifications } from "../db/schema/notifications.schema.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const log = createLogger({ module: "notifications", subModule: "start" });
export const startNotifications = async () => {
// get active notification
const { data, error } = await tryCatch(
db.select().from(notifications).where(eq(notifications.active, true)),
);
if (error) {
log.error(
{ error: error },
"There was an error when getting notifications.",
);
return;
}
if (data) {
if (data.length === 0) {
log.info(
{},
"There are know currently active notifications to start up.",
);
return;
}
// get the subs and see if we have any subs currently so we can fire up the notification
const { data: sub, error: subError } = await tryCatch(
db.select().from(notificationSub),
);
if (subError) {
log.error(
{ error: error },
"There was an error when getting subscriptions.",
);
return;
}
if (sub.length === 0) {
log.info({}, "There are know currently active subscriptions.");
return;
}
const emailString = [
...new Set(
sub.flatMap((e) =>
e.emails?.map((email) => email.trim().toLowerCase()),
),
),
].join(";");
for (const n of data) {
createCronJob(
n.name,
minutesToCron(parseInt(n.interval ?? "15", 10)),
async () => {
try {
const { default: runFun } = await import(
`./notification.${n.name.trim()}.js`
);
await runFun(n, emailString);
} catch (error) {
log.error(
{ error: error },
"There was an error starting the notification",
);
}
},
);
}
}
};
export const modifiedNotification = async (id: string) => {
// when a notifications subscribed to, updated, deleted we want to get the info and rerun the startup on the single notification.
const { data, error } = await tryCatch(
db.select().from(notifications).where(eq(notifications.id, id)),
);
if (error) {
log.error(
{ error: error },
"There was an error when getting notifications.",
);
return;
}
if (data) {
if (!data[0]?.active) {
stopCronJob(data[0]?.name ?? "");
return;
}
// get the subs for the specific id as we only want to up the modified one
const { data: sub, error: subError } = await tryCatch(
db
.select()
.from(notificationSub)
.where(eq(notificationSub.notificationId, id)),
);
if (subError) {
log.error(
{ error: error },
"There was an error when getting subscriptions.",
);
return;
}
if (sub.length === 0) {
log.info({}, "There are know currently active subscriptions.");
stopCronJob(data[0]?.name ?? "");
return;
}
const emailString = [
...new Set(
sub.flatMap((e) =>
e.emails?.map((email) => email.trim().toLowerCase()),
),
),
].join(";");
createCronJob(
data[0].name,
minutesToCron(parseInt(data[0].interval ?? "15", 10)),
async () => {
try {
const { default: runFun } = await import(
`./notification.${data[0]?.name.trim()}.js`
);
await runFun(data[0], emailString);
} catch (error) {
log.error(
{ error: error },
"There was an error starting the notification",
);
}
},
);
}
};

View File

@@ -0,0 +1,6 @@
const reprint = (data: any, emails: string) => {
console.log(data);
console.log(emails);
};
export default reprint;

View File

@@ -0,0 +1,54 @@
import { notifications } from "db/schema/notifications.schema.js";
import { eq } from "drizzle-orm";
import { type Response, Router } from "express";
import { auth } from "utils/auth.utils.js";
import { db } from "../db/db.controller.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res: Response) => {
const hasPermissions = await auth.api.userHasPermission({
body: {
//userId: req?.user?.id,
role: req.user?.roles as any,
permissions: {
notifications: ["readAll"], // This must match the structure in your access control
},
},
});
const { data: nName, error: nError } = await tryCatch(
db
.select()
.from(notifications)
.where(
!hasPermissions.success ? eq(notifications.active, true) : undefined,
)
.orderBy(notifications.name),
);
if (nError) {
return apiReturn(res, {
success: false,
level: "error",
module: "notification",
subModule: "get",
message: `There was an error getting the notifications `,
data: [nError],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "notification",
subModule: "get",
message: `All current notifications`,
data: nName ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,20 @@
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import getNotifications from "./notification.route.js";
import updateNote from "./notification.update.route.js";
import deleteSub from "./notificationSub.delete.route.js";
import subs from "./notificationSub.get.route.js";
import newSub from "./notificationSub.post.route.js";
import updateSub from "./notificationSub.update.route.js";
export const setupNotificationRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/notification`, requireAuth, getNotifications);
app.use(`${baseUrl}/api/notification`, requireAuth, updateNote);
app.use(`${baseUrl}/api/notification/sub`, requireAuth, subs);
app.use(`${baseUrl}/api/notification/sub`, requireAuth, newSub);
app.use(`${baseUrl}/api/notification/sub`, requireAuth, updateSub);
app.use(`${baseUrl}/api/notification/sub`, requireAuth, deleteSub);
// all other system should be under /api/system/*
};

View File

@@ -0,0 +1,81 @@
import { notifications } from "db/schema/notifications.schema.js";
import { eq } from "drizzle-orm";
import { type Response, Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { requirePermission } from "../middleware/auth.requiredPerms.middleware.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { modifiedNotification } from "./notification.controller.js";
const r = Router();
const updateNote = z.object({
description: z.string().optional(),
active: z.boolean().optional(),
interval: z.string().optional(),
options: z.array(z.record(z.string(), z.unknown())).optional(),
});
r.patch(
"/:id",
requirePermission({ notifications: ["update"] }),
async (req, res: Response) => {
const { id } = req.params;
try {
const validated = updateNote.parse(req.body);
const { data: nName, error: nError } = await tryCatch(
db
.update(notifications)
.set(validated)
.where(eq(notifications.id, id as string))
.returning(),
);
await modifiedNotification(id as string);
if (nError) {
return apiReturn(res, {
success: false,
level: "error",
module: "notification",
subModule: "update",
message: `There was an error getting the notifications `,
data: [nError],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "notification",
subModule: "update",
message: `Notification was updated`,
data: nName ?? [],
status: 200,
});
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
// return res.status(400).json({
// error: "Validation failed",
// details: flattened,
// });
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "routes",
subModule: "notification",
message: "Validation failed",
data: [flattened.fieldErrors],
status: 400, //connect.success ? 200 : 400,
});
}
}
},
);
export default r;

View File

@@ -0,0 +1,83 @@
import { notificationSub } from "db/schema/notifications.sub.schema.js";
import { and, eq } from "drizzle-orm";
import { type Response, Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { modifiedNotification } from "./notification.controller.js";
const newSubscribe = z.object({
emails: z
.email()
.array()
.describe("An array of emails"),
userId: z.string().describe("User id."),
notificationId: z
.string()
.describe("Notification id"),
});
const r = Router();
r.delete("/", async (req, res: Response) => {
try {
const validated = newSubscribe.parse(req.body);
const { data, error } = await tryCatch(
db
.delete(notificationSub)
.where(
and(
eq(notificationSub.userId, validated.userId),
eq(notificationSub.notificationId, validated.notificationId),
),
)
.returning(),
);
await modifiedNotification(validated.notificationId);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "notification",
subModule: "post",
message: `There was an error deleting the subscription `,
data: [error],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "notification",
subModule: "post",
message: `Subscription deleted`,
data: data ?? [],
status: 200,
});
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
// return res.status(400).json({
// error: "Validation failed",
// details: flattened,
// });
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "routes",
subModule: "notification",
message: "Validation failed",
data: [flattened.fieldErrors],
status: 400, //connect.success ? 200 : 400,
});
}
}
});
export default r;

View File

@@ -0,0 +1,55 @@
import { notificationSub } from "db/schema/notifications.sub.schema.js";
import { eq } from "drizzle-orm";
import { type Response, Router } from "express";
import { auth } from "utils/auth.utils.js";
import { db } from "../db/db.controller.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res: Response) => {
const hasPermissions = await auth.api.userHasPermission({
body: {
//userId: req?.user?.id,
role: req.user?.roles as any,
permissions: {
notifications: ["readAll"], // This must match the structure in your access control
},
},
});
const { data, error } = await tryCatch(
db
.select()
.from(notificationSub)
.where(
!hasPermissions.success
? eq(notificationSub.userId, `${req?.user?.id ?? ""}`)
: undefined,
),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "notification",
subModule: "post",
message: `There was an error getting subscriptions `,
data: [error],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "notification",
subModule: "post",
message: `Subscription deleted`,
data: data ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,75 @@
import { notificationSub } from "db/schema/notifications.sub.schema.js";
import { type Response, Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { modifiedNotification } from "./notification.controller.js";
const newSubscribe = z.object({
emails: z
.email()
.array()
.describe("An array of emails"),
userId: z.string().describe("User id."),
notificationId: z
.string()
.describe("Notification id"),
});
const r = Router();
r.post("/", async (req, res: Response) => {
try {
const validated = newSubscribe.parse(req.body);
const { data, error } = await tryCatch(
db.insert(notificationSub).values(validated).returning(),
);
await modifiedNotification(validated.notificationId);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "notification",
subModule: "post",
message: `There was an error getting the notifications `,
data: [error],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "notification",
subModule: "post",
message: `Subscribed to notification`,
data: data ?? [],
status: 200,
});
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
// return res.status(400).json({
// error: "Validation failed",
// details: flattened,
// });
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "routes",
subModule: "notification",
message: "Validation failed",
data: [flattened.fieldErrors],
status: 400, //connect.success ? 200 : 400,
});
}
}
});
export default r;

View File

@@ -0,0 +1,84 @@
import { notificationSub } from "db/schema/notifications.sub.schema.js";
import { and, eq } from "drizzle-orm";
import { type Response, Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { modifiedNotification } from "./notification.controller.js";
const newSubscribe = z.object({
emails: z.email().array().describe("An array of emails"),
userId: z.string().describe("User id."),
notificationId: z.string().describe("Notification id"),
});
const r = Router();
r.patch("/", async (req, res: Response) => {
try {
const validated = newSubscribe.parse(req.body);
const emails = validated.emails
.map((e) => e.trim().toLowerCase())
.filter(Boolean);
const uniqueEmails = [...new Set(emails)];
const { data, error } = await tryCatch(
db
.update(notificationSub)
.set({ emails: uniqueEmails })
.where(
and(
eq(notificationSub.userId, validated.userId),
eq(notificationSub.notificationId, validated.notificationId),
),
)
.returning(),
);
await modifiedNotification(validated.notificationId);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "notification",
subModule: "update",
message: `There was an error updating the notifications `,
data: [error],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "notification",
subModule: "update",
message: `Subscription updated`,
data: data ?? [],
status: 200,
});
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
// return res.status(400).json({
// error: "Validation failed",
// details: flattened,
// });
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "routes",
subModule: "notification",
message: "Validation failed",
data: [flattened.fieldErrors],
status: 400, //connect.success ? 200 : 400,
});
}
}
});
export default r;

View File

@@ -0,0 +1,50 @@
import { db } from "db/db.controller.js";
import {
type NewNotification,
notifications,
} from "db/schema/notifications.schema.js";
import { sql } from "drizzle-orm";
import { tryCatch } from "utils/trycatch.utils.js";
import { createLogger } from "../logger/logger.controller.js";
const note: NewNotification[] = [
{
name: "reprintLabels",
description:
"Monitors the labels that are printed and returns a there data, if one falls withing the time frame.",
active: false,
interval: "10",
options: [{ prodID: 1 }],
},
];
export const createNotifications = async () => {
const log = createLogger({ module: "notifications", subModule: "create" });
const { data, error } = await tryCatch(
db
.insert(notifications)
.values(note)
.onConflictDoUpdate({
target: notifications.name,
set: {
description: sql`excluded.description`,
},
// where: sql`
// settings.seed_version IS NULL
// OR settings.seed_version < excluded.seed_version
// `,
})
.returning(),
);
if (error) {
log.error(
{ error: error },
"There was an error when adding or updating the notifications.",
);
}
if (data) {
log.info({}, "All notifications were added/updated");
}
};

View File

@@ -1,4 +1,5 @@
import type { Express } from "express";
import { setupNotificationRoutes } from "notification/notification.routes.js";
import { setupAuthRoutes } from "./auth/auth.routes.js";
// import the routes and route setups
import { setupApiDocsRoutes } from "./configs/scaler.config.js";
@@ -17,4 +18,5 @@ export const setupRoutes = (baseUrl: string, app: Express) => {
setupAuthRoutes(baseUrl, app);
setupUtilsRoutes(baseUrl, app);
setupOpendockRoutes(baseUrl, app);
setupNotificationRoutes(baseUrl, app);
};

View File

@@ -1,5 +1,7 @@
import { createServer } from "node:http";
import os from "node:os";
import { startNotifications } from "notification/notification.controller.js";
import { createNotifications } from "notification/notifications.master.js";
import createApp from "./app.js";
import { db } from "./db/db.controller.js";
import { dbCleanup } from "./db/dbCleanup.controller.js";
@@ -47,6 +49,10 @@ const start = async () => {
dbCleanup("jobs", 30),
);
createCronJob("logsCleanup", "0 15 5 * * *", () => dbCleanup("logs", 120));
// one shots only needed to run on server startups
createNotifications();
startNotifications();
}, 5 * 1000);
server.listen(port, async () => {

View File

@@ -0,0 +1,26 @@
import { createAccessControl } from "better-auth/plugins/access";
export const statement = {
app: ["read", "create", "share", "update", "delete", "readAll"],
user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"],
} as const;
export const ac = createAccessControl(statement);
export const user = ac.newRole({
app: ["read", "create"],
notifications: ["read", "create"],
});
export const admin = ac.newRole({
app: ["read", "create", "update"],
});
export const systemAdmin = ac.newRole({
app: ["read", "create", "share", "update", "delete", "readAll"],
user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"],
});

View File

@@ -1,7 +1,7 @@
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import {
admin,
admin as adminPlugin,
// apiKey,
// createAuthMiddleware,
//customSession,
@@ -12,6 +12,7 @@ import {
//import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import * as rawSchema from "../db/schema/auth.schema.js";
import { ac, admin, systemAdmin, user } from "./auth.permissions.js";
import { allowedOrigins } from "./cors.utils.js";
import { sendEmail } from "./sendEmail.utils.js";
@@ -44,7 +45,14 @@ export const auth = betterAuth({
plugins: [
jwt({ jwt: { expirationTime: "1h" } }),
//apiKey(),
admin(),
adminPlugin({
ac,
roles: {
admin,
user,
systemAdmin,
},
}),
lastLoginMethod(),
username({
minUsernameLength: 5,

View File

@@ -0,0 +1,38 @@
import { createLogger } from "../logger/logger.controller.js";
const minTime = 5;
export const minutesToCron = (minutes: number): string => {
const log = createLogger({ module: "system", subModule: "croner" });
if (minutes < minTime) {
log.error(
{},
`Conversion of less then ${minTime} min and is not allowed on this server`,
);
}
// Every X minutes (under 60)
if (minutes < 60) {
return `*/${minutes} * * * *`;
}
// Every X hours (clean division)
if (minutes % 60 === 0 && minutes < 1440) {
const hours = minutes / 60;
return `0 0 */${hours} * * *`;
}
// Every day
if (minutes === 1440) {
return `0 0 8 * * *`; // 8am
}
// Every X days (clean division)
if (minutes % 1440 === 0) {
const days = minutes / 1440;
return `0 0 0 */${days} * *`;
}
// Fallback (not cleanly divisible)
// Cron can't represent this perfectly → run every X minutes as approximation
return `0 */${minutes} * * * *`;
};

View File

@@ -3,7 +3,14 @@ import { createLogger } from "../logger/logger.controller.js";
interface Data<T = unknown[]> {
success: boolean;
module: "system" | "ocp" | "routes" | "datamart" | "utils" | "opendock";
module:
| "system"
| "ocp"
| "routes"
| "datamart"
| "utils"
| "opendock"
| "notification";
subModule:
| "db"
| "labeling"
@@ -15,7 +22,13 @@ interface Data<T = unknown[]> {
| "datamart"
| "jobs"
| "apt"
| "settings";
| "settings"
| "get"
| "update"
| "delete"
| "post"
| "notification"
| "delete";
level: "info" | "error" | "debug" | "fatal";
message: string;
room?: string;

View File

@@ -0,0 +1,20 @@
meta {
name: Get All notifications.
type: http
seq: 1
}
get {
url: {{url}}/api/notification
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}
docs {
Passing all as a query param will return all queries active and none active
}

View File

@@ -0,0 +1,24 @@
meta {
name: Subscribe to notification
type: http
seq: 2
}
post {
url: {{url}}/api/notification/sub
body: json
auth: inherit
}
body:json {
{
"userId":"m6AbQXFwOXoX3YKLfwWgq2LIdDqS5jqv",
"notificationId": "0399eb2a-39df-48b7-9f1c-d233cec94d2e",
"emails": ["blake.mattes@alpla.com"]
}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,8 @@
meta {
name: notifications
seq: 7
}
auth {
mode: inherit
}

View File

@@ -0,0 +1,24 @@
meta {
name: remove sub notification
type: http
seq: 4
}
delete {
url: {{url}}/api/notification/sub
body: json
auth: inherit
}
body:json {
{
"userId":"0kHd6Kkdub4GW6rK1qa1yjWwqXtvykqT",
"notificationId": "0399eb2a-39df-48b7-9f1c-d233cec94d2e",
"emails": ["blake.mattes@alpla.com"]
}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,24 @@
meta {
name: subscriptions
type: http
seq: 5
}
get {
url: {{url}}/api/notification/sub
body: json
auth: inherit
}
body:json {
{
"userId":"0kHd6Kkdub4GW6rK1qa1yjWwqXtvykqT",
"notificationId": "0399eb2a-39df-48b7-9f1c-d233cec94d2e",
"emails": ["blake.mattes@alpla.com"]
}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,31 @@
meta {
name: update notification
type: http
seq: 6
}
patch {
url: {{url}}/api/notification/:id
body: json
auth: inherit
}
params:path {
id: 0399eb2a-39df-48b7-9f1c-d233cec94d2e
}
body:json {
{
"active" : false,
"options": [{"prodId": 5}]
}
}
settings {
encodeUrl: true
timeout: 0
}
docs {
Passing all as a query param will return all queries active and none active
}

View File

@@ -0,0 +1,24 @@
meta {
name: update sub notification
type: http
seq: 3
}
patch {
url: {{url}}/api/notification/sub
body: json
auth: inherit
}
body:json {
{
"userId":"m6AbQXFwOXoX3YKLfwWgq2LIdDqS5jqv",
"notificationId": "0399eb2a-39df-48b7-9f1c-d233cec94d2e",
"emails": ["cowchmonkey@gmail.com"]
}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -1,9 +1,19 @@
import { adminClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
import { ac, admin, systemAdmin, user } from "./auth-permissions";
export const authClient = createAuthClient({
baseURL: `${window.location.origin}/lst/api/auth`,
plugins: [adminClient()],
plugins: [
adminClient({
ac,
roles: {
admin,
user,
systemAdmin,
},
}),
],
});
export const { useSession, signUp, signIn, signOut } = authClient;

View File

@@ -0,0 +1,21 @@
import { createAccessControl } from "better-auth/plugins/access";
export const statement = {
project: ["create", "share", "update", "delete"],
user: ["ban"],
} as const;
export const ac = createAccessControl(statement);
export const user = ac.newRole({
project: ["create"],
});
export const admin = ac.newRole({
project: ["create", "update"],
});
export const systemAdmin = ac.newRole({
project: ["create", "update", "delete"],
user: ["ban"],
});

View File

@@ -0,0 +1,9 @@
CREATE TABLE "notifications" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text NOT NULL,
"description" text NOT NULL,
"active" boolean DEFAULT false,
"options" jsonb DEFAULT '[]'::jsonb
);
--> statement-breakpoint
CREATE UNIQUE INDEX "notify_name" ON "notifications" USING btree ("name");

View File

@@ -0,0 +1 @@
ALTER TABLE "notifications" ADD COLUMN "interval" text DEFAULT '5';

View File

@@ -0,0 +1,10 @@
CREATE TABLE "notification_sub" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" text NOT NULL,
"notification_id" uuid NOT NULL,
"emails" text[] DEFAULT '{}',
CONSTRAINT "notification_sub_user_notification_unique" UNIQUE("user_id","notification_id")
);
--> statement-breakpoint
ALTER TABLE "notification_sub" ADD CONSTRAINT "notification_sub_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "notification_sub" ADD CONSTRAINT "notification_sub_notification_id_notifications_id_fk" FOREIGN KEY ("notification_id") REFERENCES "public"."notifications"("id") ON DELETE cascade ON UPDATE no action;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -113,6 +113,27 @@
"when": 1772415040979,
"tag": "0015_neat_donald_blake",
"breakpoints": true
},
{
"idx": 16,
"version": "7",
"when": 1774026354811,
"tag": "0016_colorful_exiles",
"breakpoints": true
},
{
"idx": 17,
"version": "7",
"when": 1774030327338,
"tag": "0017_famous_joseph",
"breakpoints": true
},
{
"idx": 18,
"version": "7",
"when": 1774032587305,
"tag": "0018_lowly_wallow",
"breakpoints": true
}
]
}

679
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -39,54 +39,54 @@
"license": "ISC",
"type": "module",
"devDependencies": {
"@biomejs/biome": "2.4.2",
"@changesets/cli": "^2.29.8",
"@commitlint/cli": "^20.4.1",
"@commitlint/config-conventional": "^20.4.1",
"@biomejs/biome": "2.4.8",
"@changesets/cli": "^2.30.0",
"@commitlint/cli": "^20.5.0",
"@commitlint/config-conventional": "^20.5.0",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/morgan": "^1.9.10",
"@types/mssql": "^9.1.9",
"@types/multer": "^2.0.0",
"@types/multer": "^2.1.0",
"@types/net-snmp": "^3.23.0",
"@types/node": "^25.2.3",
"@types/nodemailer": "^7.0.10",
"@types/node": "^25.5.0",
"@types/nodemailer": "^7.0.11",
"@types/nodemailer-express-handlebars": "^4.0.6",
"@types/pg": "^8.16.0",
"@types/supertest": "^6.0.3",
"@types/pg": "^8.18.0",
"@types/supertest": "^7.2.0",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8",
"commitizen": "^4.3.1",
"cz-conventional-changelog": "^3.3.0",
"npm-check-updates": "^19.3.2",
"npm-check-updates": "^19.6.5",
"openapi-types": "^12.1.3",
"ts-node-dev": "^2.0.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
},
"dependencies": {
"@dotenvx/dotenvx": "^1.52.0",
"@scalar/express-api-reference": "^0.8.41",
"@dotenvx/dotenvx": "^1.57.0",
"@scalar/express-api-reference": "^0.9.4",
"@socket.io/admin-ui": "^0.5.1",
"axios": "^1.13.5",
"axios": "^1.13.6",
"better-auth": "^1.5.5",
"concurrently": "^9.2.1",
"cors": "^2.8.6",
"croner": "^10.0.1",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"drizzle-kit": "^0.31.9",
"drizzle-kit": "^0.31.10",
"drizzle-orm": "^0.45.1",
"drizzle-zod": "^0.8.3",
"express": "^5.2.1",
"husky": "^9.1.7",
"morgan": "^1.10.1",
"mssql": "^12.2.0",
"multer": "^2.0.2",
"mssql": "^12.2.1",
"multer": "^2.1.1",
"net-snmp": "^3.26.1",
"nodemailer": "^8.0.1",
"nodemailer": "^8.0.3",
"nodemailer-express-handlebars": "^7.0.0",
"pg": "^8.18.0",
"pg": "^8.20.0",
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"postgres": "^3.4.8",