notification added in with subs :D
This commit is contained in:
29
backend/db/schema/notifications.schema.ts
Normal file
29
backend/db/schema/notifications.schema.ts
Normal 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>;
|
||||||
30
backend/db/schema/notifications.sub.schema.ts
Normal file
30
backend/db/schema/notifications.sub.schema.ts
Normal 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>;
|
||||||
52
backend/middleware/auth.requiredPerms.middleware.ts
Normal file
52
backend/middleware/auth.requiredPerms.middleware.ts
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
153
backend/notification/notification.controller.ts
Normal file
153
backend/notification/notification.controller.ts
Normal 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",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
6
backend/notification/notification.reprintLabels.ts
Normal file
6
backend/notification/notification.reprintLabels.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
const reprint = (data: any, emails: string) => {
|
||||||
|
console.log(data);
|
||||||
|
console.log(emails);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reprint;
|
||||||
54
backend/notification/notification.route.ts
Normal file
54
backend/notification/notification.route.ts
Normal 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;
|
||||||
20
backend/notification/notification.routes.ts
Normal file
20
backend/notification/notification.routes.ts
Normal 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/*
|
||||||
|
};
|
||||||
81
backend/notification/notification.update.route.ts
Normal file
81
backend/notification/notification.update.route.ts
Normal 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;
|
||||||
83
backend/notification/notificationSub.delete.route.ts
Normal file
83
backend/notification/notificationSub.delete.route.ts
Normal 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;
|
||||||
55
backend/notification/notificationSub.get.route.ts
Normal file
55
backend/notification/notificationSub.get.route.ts
Normal 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;
|
||||||
75
backend/notification/notificationSub.post.route.ts
Normal file
75
backend/notification/notificationSub.post.route.ts
Normal 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;
|
||||||
84
backend/notification/notificationSub.update.route.ts
Normal file
84
backend/notification/notificationSub.update.route.ts
Normal 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;
|
||||||
50
backend/notification/notifications.master.ts
Normal file
50
backend/notification/notifications.master.ts
Normal 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");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Express } from "express";
|
import type { Express } from "express";
|
||||||
|
import { setupNotificationRoutes } from "notification/notification.routes.js";
|
||||||
import { setupAuthRoutes } from "./auth/auth.routes.js";
|
import { setupAuthRoutes } from "./auth/auth.routes.js";
|
||||||
// import the routes and route setups
|
// import the routes and route setups
|
||||||
import { setupApiDocsRoutes } from "./configs/scaler.config.js";
|
import { setupApiDocsRoutes } from "./configs/scaler.config.js";
|
||||||
@@ -17,4 +18,5 @@ export const setupRoutes = (baseUrl: string, app: Express) => {
|
|||||||
setupAuthRoutes(baseUrl, app);
|
setupAuthRoutes(baseUrl, app);
|
||||||
setupUtilsRoutes(baseUrl, app);
|
setupUtilsRoutes(baseUrl, app);
|
||||||
setupOpendockRoutes(baseUrl, app);
|
setupOpendockRoutes(baseUrl, app);
|
||||||
|
setupNotificationRoutes(baseUrl, app);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { createServer } from "node:http";
|
import { createServer } from "node:http";
|
||||||
import os from "node:os";
|
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 createApp from "./app.js";
|
||||||
import { db } from "./db/db.controller.js";
|
import { db } from "./db/db.controller.js";
|
||||||
import { dbCleanup } from "./db/dbCleanup.controller.js";
|
import { dbCleanup } from "./db/dbCleanup.controller.js";
|
||||||
@@ -47,6 +49,10 @@ const start = async () => {
|
|||||||
dbCleanup("jobs", 30),
|
dbCleanup("jobs", 30),
|
||||||
);
|
);
|
||||||
createCronJob("logsCleanup", "0 15 5 * * *", () => dbCleanup("logs", 120));
|
createCronJob("logsCleanup", "0 15 5 * * *", () => dbCleanup("logs", 120));
|
||||||
|
|
||||||
|
// one shots only needed to run on server startups
|
||||||
|
createNotifications();
|
||||||
|
startNotifications();
|
||||||
}, 5 * 1000);
|
}, 5 * 1000);
|
||||||
|
|
||||||
server.listen(port, async () => {
|
server.listen(port, async () => {
|
||||||
|
|||||||
26
backend/utils/auth.permissions.ts
Normal file
26
backend/utils/auth.permissions.ts
Normal 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"],
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
import {
|
import {
|
||||||
admin,
|
admin as adminPlugin,
|
||||||
// apiKey,
|
// apiKey,
|
||||||
// createAuthMiddleware,
|
// createAuthMiddleware,
|
||||||
//customSession,
|
//customSession,
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
//import { eq } from "drizzle-orm";
|
//import { eq } from "drizzle-orm";
|
||||||
import { db } from "../db/db.controller.js";
|
import { db } from "../db/db.controller.js";
|
||||||
import * as rawSchema from "../db/schema/auth.schema.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 { allowedOrigins } from "./cors.utils.js";
|
||||||
import { sendEmail } from "./sendEmail.utils.js";
|
import { sendEmail } from "./sendEmail.utils.js";
|
||||||
|
|
||||||
@@ -44,7 +45,14 @@ export const auth = betterAuth({
|
|||||||
plugins: [
|
plugins: [
|
||||||
jwt({ jwt: { expirationTime: "1h" } }),
|
jwt({ jwt: { expirationTime: "1h" } }),
|
||||||
//apiKey(),
|
//apiKey(),
|
||||||
admin(),
|
adminPlugin({
|
||||||
|
ac,
|
||||||
|
roles: {
|
||||||
|
admin,
|
||||||
|
user,
|
||||||
|
systemAdmin,
|
||||||
|
},
|
||||||
|
}),
|
||||||
lastLoginMethod(),
|
lastLoginMethod(),
|
||||||
username({
|
username({
|
||||||
minUsernameLength: 5,
|
minUsernameLength: 5,
|
||||||
|
|||||||
38
backend/utils/croner.minConvert.ts
Normal file
38
backend/utils/croner.minConvert.ts
Normal 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} * * * *`;
|
||||||
|
};
|
||||||
@@ -3,7 +3,14 @@ import { createLogger } from "../logger/logger.controller.js";
|
|||||||
|
|
||||||
interface Data<T = unknown[]> {
|
interface Data<T = unknown[]> {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
module: "system" | "ocp" | "routes" | "datamart" | "utils" | "opendock";
|
module:
|
||||||
|
| "system"
|
||||||
|
| "ocp"
|
||||||
|
| "routes"
|
||||||
|
| "datamart"
|
||||||
|
| "utils"
|
||||||
|
| "opendock"
|
||||||
|
| "notification";
|
||||||
subModule:
|
subModule:
|
||||||
| "db"
|
| "db"
|
||||||
| "labeling"
|
| "labeling"
|
||||||
@@ -15,7 +22,13 @@ interface Data<T = unknown[]> {
|
|||||||
| "datamart"
|
| "datamart"
|
||||||
| "jobs"
|
| "jobs"
|
||||||
| "apt"
|
| "apt"
|
||||||
| "settings";
|
| "settings"
|
||||||
|
| "get"
|
||||||
|
| "update"
|
||||||
|
| "delete"
|
||||||
|
| "post"
|
||||||
|
| "notification"
|
||||||
|
| "delete";
|
||||||
level: "info" | "error" | "debug" | "fatal";
|
level: "info" | "error" | "debug" | "fatal";
|
||||||
message: string;
|
message: string;
|
||||||
room?: string;
|
room?: string;
|
||||||
|
|||||||
20
brunoApi/notifications/Get All notifications.bru
Normal file
20
brunoApi/notifications/Get All notifications.bru
Normal 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
|
||||||
|
}
|
||||||
24
brunoApi/notifications/Subscribe to notification.bru
Normal file
24
brunoApi/notifications/Subscribe to notification.bru
Normal 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
|
||||||
|
}
|
||||||
8
brunoApi/notifications/folder.bru
Normal file
8
brunoApi/notifications/folder.bru
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
meta {
|
||||||
|
name: notifications
|
||||||
|
seq: 7
|
||||||
|
}
|
||||||
|
|
||||||
|
auth {
|
||||||
|
mode: inherit
|
||||||
|
}
|
||||||
24
brunoApi/notifications/remove sub notification.bru
Normal file
24
brunoApi/notifications/remove sub notification.bru
Normal 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
|
||||||
|
}
|
||||||
24
brunoApi/notifications/subscriptions.bru
Normal file
24
brunoApi/notifications/subscriptions.bru
Normal 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
|
||||||
|
}
|
||||||
31
brunoApi/notifications/update notification.bru
Normal file
31
brunoApi/notifications/update notification.bru
Normal 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
|
||||||
|
}
|
||||||
24
brunoApi/notifications/update sub notification.bru
Normal file
24
brunoApi/notifications/update sub notification.bru
Normal 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
|
||||||
|
}
|
||||||
@@ -1,9 +1,19 @@
|
|||||||
import { adminClient } from "better-auth/client/plugins";
|
import { adminClient } from "better-auth/client/plugins";
|
||||||
import { createAuthClient } from "better-auth/react";
|
import { createAuthClient } from "better-auth/react";
|
||||||
|
import { ac, admin, systemAdmin, user } from "./auth-permissions";
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
baseURL: `${window.location.origin}/lst/api/auth`,
|
baseURL: `${window.location.origin}/lst/api/auth`,
|
||||||
plugins: [adminClient()],
|
plugins: [
|
||||||
|
adminClient({
|
||||||
|
ac,
|
||||||
|
roles: {
|
||||||
|
admin,
|
||||||
|
user,
|
||||||
|
systemAdmin,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { useSession, signUp, signIn, signOut } = authClient;
|
export const { useSession, signUp, signIn, signOut } = authClient;
|
||||||
|
|||||||
21
frontend/src/lib/auth-permissions.ts
Normal file
21
frontend/src/lib/auth-permissions.ts
Normal 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"],
|
||||||
|
});
|
||||||
9
migrations/0016_colorful_exiles.sql
Normal file
9
migrations/0016_colorful_exiles.sql
Normal 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");
|
||||||
1
migrations/0017_famous_joseph.sql
Normal file
1
migrations/0017_famous_joseph.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "notifications" ADD COLUMN "interval" text DEFAULT '5';
|
||||||
10
migrations/0018_lowly_wallow.sql
Normal file
10
migrations/0018_lowly_wallow.sql
Normal 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;
|
||||||
1209
migrations/meta/0016_snapshot.json
Normal file
1209
migrations/meta/0016_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1216
migrations/meta/0017_snapshot.json
Normal file
1216
migrations/meta/0017_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1291
migrations/meta/0018_snapshot.json
Normal file
1291
migrations/meta/0018_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -113,6 +113,27 @@
|
|||||||
"when": 1772415040979,
|
"when": 1772415040979,
|
||||||
"tag": "0015_neat_donald_blake",
|
"tag": "0015_neat_donald_blake",
|
||||||
"breakpoints": true
|
"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
679
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@@ -39,54 +39,54 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.4.2",
|
"@biomejs/biome": "2.4.8",
|
||||||
"@changesets/cli": "^2.29.8",
|
"@changesets/cli": "^2.30.0",
|
||||||
"@commitlint/cli": "^20.4.1",
|
"@commitlint/cli": "^20.5.0",
|
||||||
"@commitlint/config-conventional": "^20.4.1",
|
"@commitlint/config-conventional": "^20.5.0",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/morgan": "^1.9.10",
|
"@types/morgan": "^1.9.10",
|
||||||
"@types/mssql": "^9.1.9",
|
"@types/mssql": "^9.1.9",
|
||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.1.0",
|
||||||
"@types/net-snmp": "^3.23.0",
|
"@types/net-snmp": "^3.23.0",
|
||||||
"@types/node": "^25.2.3",
|
"@types/node": "^25.5.0",
|
||||||
"@types/nodemailer": "^7.0.10",
|
"@types/nodemailer": "^7.0.11",
|
||||||
"@types/nodemailer-express-handlebars": "^4.0.6",
|
"@types/nodemailer-express-handlebars": "^4.0.6",
|
||||||
"@types/pg": "^8.16.0",
|
"@types/pg": "^8.18.0",
|
||||||
"@types/supertest": "^6.0.3",
|
"@types/supertest": "^7.2.0",
|
||||||
"@types/swagger-jsdoc": "^6.0.4",
|
"@types/swagger-jsdoc": "^6.0.4",
|
||||||
"@types/swagger-ui-express": "^4.1.8",
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
"commitizen": "^4.3.1",
|
"commitizen": "^4.3.1",
|
||||||
"cz-conventional-changelog": "^3.3.0",
|
"cz-conventional-changelog": "^3.3.0",
|
||||||
"npm-check-updates": "^19.3.2",
|
"npm-check-updates": "^19.6.5",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
"ts-node-dev": "^2.0.0",
|
"ts-node-dev": "^2.0.0",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dotenvx/dotenvx": "^1.52.0",
|
"@dotenvx/dotenvx": "^1.57.0",
|
||||||
"@scalar/express-api-reference": "^0.8.41",
|
"@scalar/express-api-reference": "^0.9.4",
|
||||||
"@socket.io/admin-ui": "^0.5.1",
|
"@socket.io/admin-ui": "^0.5.1",
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.13.6",
|
||||||
"better-auth": "^1.5.5",
|
"better-auth": "^1.5.5",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"croner": "^10.0.1",
|
"croner": "^10.0.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"date-fns-tz": "^3.2.0",
|
"date-fns-tz": "^3.2.0",
|
||||||
"drizzle-kit": "^0.31.9",
|
"drizzle-kit": "^0.31.10",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
"drizzle-zod": "^0.8.3",
|
"drizzle-zod": "^0.8.3",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"morgan": "^1.10.1",
|
"morgan": "^1.10.1",
|
||||||
"mssql": "^12.2.0",
|
"mssql": "^12.2.1",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.1.1",
|
||||||
"net-snmp": "^3.26.1",
|
"net-snmp": "^3.26.1",
|
||||||
"nodemailer": "^8.0.1",
|
"nodemailer": "^8.0.3",
|
||||||
"nodemailer-express-handlebars": "^7.0.0",
|
"nodemailer-express-handlebars": "^7.0.0",
|
||||||
"pg": "^8.18.0",
|
"pg": "^8.20.0",
|
||||||
"pino": "^10.3.1",
|
"pino": "^10.3.1",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"postgres": "^3.4.8",
|
"postgres": "^3.4.8",
|
||||||
|
|||||||
Reference in New Issue
Block a user