backup page done
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
"form-data": "^4.0.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"multer": "^2.0.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"passport": "^0.7.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"ws": "^8.18.0",
|
||||
|
||||
@@ -6,6 +6,7 @@ import { apiLogger } from "./middlewares/logger.middleware";
|
||||
import authRoutes from "./routes/auth";
|
||||
import { authenticateJWT } from "./middlewares/auth.middleware";
|
||||
import dotenv from "dotenv";
|
||||
import { startBackupCron } from "./cron/backupCheck";
|
||||
|
||||
dotenv.config();
|
||||
const FRONTEND_URL = process.env.FRONTEND_URL;
|
||||
@@ -30,4 +31,7 @@ app.use("/api", authenticateJWT, routes);
|
||||
|
||||
app.use(errorHandler);
|
||||
|
||||
export default app;
|
||||
//startig cron job
|
||||
startBackupCron();
|
||||
|
||||
export default app;
|
||||
|
||||
50
apps/Backend/src/cron/backupCheck.ts
Normal file
50
apps/Backend/src/cron/backupCheck.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import cron from "node-cron";
|
||||
import { storage } from "../storage";
|
||||
import { NotificationTypes } from "@repo/db/types";
|
||||
|
||||
/**
|
||||
* Daily cron job to check if users haven't backed up in 7 days
|
||||
* Creates a backup notification if overdue
|
||||
*/
|
||||
export const startBackupCron = () => {
|
||||
cron.schedule("0 9 * * *", async () => {
|
||||
console.log("🔄 Running daily backup check...");
|
||||
|
||||
const userBatchSize = 100;
|
||||
let userOffset = 0;
|
||||
|
||||
while (true) {
|
||||
// Fetch a batch of users
|
||||
const users = await storage.getUsers(userBatchSize, userOffset);
|
||||
if (!users || users.length === 0) break;
|
||||
|
||||
for (const user of users) {
|
||||
try {
|
||||
if (user.id == null) {
|
||||
continue;
|
||||
}
|
||||
const lastBackup = await storage.getLastBackup(user.id);
|
||||
const daysSince = lastBackup?.createdAt
|
||||
? (Date.now() - new Date(lastBackup.createdAt).getTime()) /
|
||||
(1000 * 60 * 60 * 24)
|
||||
: Infinity;
|
||||
|
||||
if (daysSince >= 7) {
|
||||
await storage.createNotification(
|
||||
user.id,
|
||||
"BACKUP" as NotificationTypes,
|
||||
"⚠️ It has been more than 7 days since your last backup."
|
||||
);
|
||||
console.log(`Notification created for user ${user.id}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error processing user ${user.id}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
userOffset += userBatchSize; // next user batch
|
||||
}
|
||||
|
||||
console.log("✅ Daily backup check completed.");
|
||||
});
|
||||
};
|
||||
@@ -1,11 +1,38 @@
|
||||
import app from './app';
|
||||
import dotenv from 'dotenv';
|
||||
import app from "./app";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const HOST = process.env.HOST;
|
||||
const PORT = process.env.PORT;
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running at http://${HOST}:${PORT}`);
|
||||
});
|
||||
const server = app.listen(PORT, () => {
|
||||
console.log(`✅ Server running at http://${HOST}:${PORT}`);
|
||||
});
|
||||
|
||||
// Handle startup errors
|
||||
server.on("error", (err: NodeJS.ErrnoException) => {
|
||||
if (err.code === "EADDRINUSE") {
|
||||
console.error(`❌ Port ${PORT} is already in use`);
|
||||
} else {
|
||||
console.error("❌ Server failed to start:", err);
|
||||
}
|
||||
process.exit(1); // Exit with failure
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
const shutdown = (signal: string) => {
|
||||
console.log(`⚡ Received ${signal}, shutting down gracefully...`);
|
||||
|
||||
server.close(() => {
|
||||
console.log("✅ HTTP server closed");
|
||||
|
||||
// TODO: Close DB connections if needed
|
||||
// db.$disconnect().then(() => console.log("✅ Database disconnected"));
|
||||
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
|
||||
process.on("SIGINT", () => shutdown("SIGINT"));
|
||||
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "path";
|
||||
import os from "os";
|
||||
import fs from "fs";
|
||||
import { prisma } from "@repo/db/client";
|
||||
import { storage } from "../storage";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -11,8 +12,13 @@ const router = Router();
|
||||
* Create a database backup
|
||||
*/
|
||||
|
||||
router.post("/backup", async (req: Request, res: Response) => {
|
||||
router.post("/backup", async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
const fileName = `dental_backup_${Date.now()}.dump`;
|
||||
const tmpFile = path.join(os.tmpdir(), fileName);
|
||||
|
||||
@@ -56,8 +62,16 @@ router.post("/backup", async (req: Request, res: Response) => {
|
||||
const fileStream = fs.createReadStream(tmpFile);
|
||||
fileStream.pipe(res);
|
||||
|
||||
fileStream.on("close", () => {
|
||||
fileStream.on("close", async () => {
|
||||
fs.unlink(tmpFile, () => {}); // cleanup temp file
|
||||
|
||||
// ✅ Then, in background, update DB
|
||||
try {
|
||||
await storage.createBackup(userId);
|
||||
await storage.deleteNotificationsByType(userId, "BACKUP");
|
||||
} catch (err) {
|
||||
console.error("Backup saved but metadata update failed:", err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error("pg_dump failed:", errorMessage);
|
||||
@@ -91,18 +105,25 @@ router.post("/backup", async (req: Request, res: Response) => {
|
||||
/**
|
||||
* Get database status (connected, size, records count)
|
||||
*/
|
||||
router.get("/status", async (req: Request, res: Response) => {
|
||||
router.get("/status", async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
const size = await prisma.$queryRawUnsafe<{ size: string }[]>(
|
||||
"SELECT pg_size_pretty(pg_database_size(current_database())) as size"
|
||||
);
|
||||
|
||||
const patientsCount = await prisma.patient.count();
|
||||
const patientsCount = await storage.getTotalPatientCount();
|
||||
const lastBackup = await storage.getLastBackup(userId);
|
||||
|
||||
res.json({
|
||||
connected: true,
|
||||
size: size[0]?.size,
|
||||
patients: patientsCount,
|
||||
lastBackup: lastBackup?.createdAt ?? null,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Status error:", err);
|
||||
|
||||
@@ -10,6 +10,7 @@ import documentsRoutes from "./documents";
|
||||
import insuranceEligibilityRoutes from "./insuranceEligibility";
|
||||
import paymentsRoutes from "./payments";
|
||||
import databaseManagementRoutes from "./database-management";
|
||||
import notificationsRoutes from "./notifications";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -24,5 +25,6 @@ router.use("/documents", documentsRoutes);
|
||||
router.use("/insuranceEligibility", insuranceEligibilityRoutes);
|
||||
router.use("/payments", paymentsRoutes);
|
||||
router.use("/database-management", databaseManagementRoutes);
|
||||
router.use("/notifications", notificationsRoutes);
|
||||
|
||||
export default router;
|
||||
|
||||
34
apps/Backend/src/routes/notifications.ts
Normal file
34
apps/Backend/src/routes/notifications.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { prisma } from "@repo/db/client";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
const userId = (req as any).user?.id;
|
||||
const notifications = await prisma.notification.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 20,
|
||||
});
|
||||
res.json(notifications);
|
||||
});
|
||||
|
||||
router.post("/:id/read", async (req, res) => {
|
||||
const userId = (req as any).user?.id;
|
||||
await prisma.notification.updateMany({
|
||||
where: { id: Number(req.params.id), userId },
|
||||
data: { read: true },
|
||||
});
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
router.post("/read-all", async (req, res) => {
|
||||
const userId = (req as any).user?.id;
|
||||
await prisma.notification.updateMany({
|
||||
where: { userId },
|
||||
data: { read: true },
|
||||
});
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Appointment,
|
||||
Claim,
|
||||
ClaimWithServiceLines,
|
||||
DatabaseBackup,
|
||||
InsertAppointment,
|
||||
InsertClaim,
|
||||
InsertInsuranceCredential,
|
||||
@@ -11,6 +12,8 @@ import {
|
||||
InsertPayment,
|
||||
InsertUser,
|
||||
InsuranceCredential,
|
||||
Notification,
|
||||
NotificationTypes,
|
||||
Patient,
|
||||
Payment,
|
||||
PaymentWithExtras,
|
||||
@@ -27,6 +30,7 @@ import {
|
||||
export interface IStorage {
|
||||
// User methods
|
||||
getUser(id: number): Promise<User | undefined>;
|
||||
getUsers(limit: number, offset: number): Promise<User[]>;
|
||||
getUserByUsername(username: string): Promise<User | undefined>;
|
||||
createUser(user: InsertUser): Promise<User>;
|
||||
updateUser(id: number, updates: Partial<User>): Promise<User | undefined>;
|
||||
@@ -176,10 +180,7 @@ export interface IStorage {
|
||||
|
||||
// Payment methods:
|
||||
createPayment(data: InsertPayment): Promise<Payment>;
|
||||
updatePayment(
|
||||
id: number,
|
||||
updates: UpdatePayment,
|
||||
): Promise<Payment>;
|
||||
updatePayment(id: number, updates: UpdatePayment): Promise<Payment>;
|
||||
deletePayment(id: number, userId: number): Promise<void>;
|
||||
getPaymentById(id: number): Promise<PaymentWithExtras | null>;
|
||||
getRecentPaymentsByPatientId(
|
||||
@@ -188,19 +189,41 @@ export interface IStorage {
|
||||
offset: number
|
||||
): Promise<PaymentWithExtras[] | null>;
|
||||
getTotalPaymentCountByPatient(patientId: number): Promise<number>;
|
||||
getPaymentsByClaimId(
|
||||
claimId: number,
|
||||
): Promise<PaymentWithExtras | null>;
|
||||
getPaymentsByClaimId(claimId: number): Promise<PaymentWithExtras | null>;
|
||||
getRecentPaymentsByUser(
|
||||
userId: number,
|
||||
limit: number,
|
||||
offset: number
|
||||
): Promise<PaymentWithExtras[]>;
|
||||
getPaymentsByDateRange(
|
||||
from: Date,
|
||||
to: Date
|
||||
): Promise<PaymentWithExtras[]>;
|
||||
getPaymentsByDateRange(from: Date, to: Date): Promise<PaymentWithExtras[]>;
|
||||
getTotalPaymentCountByUser(userId: number): Promise<number>;
|
||||
|
||||
// Database Backup methods
|
||||
createBackup(userId: number): Promise<DatabaseBackup>;
|
||||
getLastBackup(userId: number): Promise<DatabaseBackup | null>;
|
||||
getBackups(userId: number, limit?: number): Promise<DatabaseBackup[]>;
|
||||
deleteBackups(userId: number): Promise<number>; // clears all for user
|
||||
|
||||
// Notification methods
|
||||
createNotification(
|
||||
userId: number,
|
||||
type: NotificationTypes,
|
||||
message: string
|
||||
): Promise<Notification>;
|
||||
getNotifications(
|
||||
userId: number,
|
||||
limit?: number,
|
||||
offset?: number
|
||||
): Promise<Notification[]>;
|
||||
markNotificationRead(
|
||||
userId: number,
|
||||
notificationId: number
|
||||
): Promise<boolean>;
|
||||
markAllNotificationsRead(userId: number): Promise<number>;
|
||||
deleteNotificationsByType(
|
||||
userId: number,
|
||||
type: NotificationTypes
|
||||
): Promise<number>;
|
||||
}
|
||||
|
||||
export const storage: IStorage = {
|
||||
@@ -210,6 +233,10 @@ export const storage: IStorage = {
|
||||
return user ?? undefined;
|
||||
},
|
||||
|
||||
async getUsers(limit: number, offset: number): Promise<User[]> {
|
||||
return await db.user.findMany({ skip: offset, take: limit });
|
||||
},
|
||||
|
||||
async getUserByUsername(username: string): Promise<User | undefined> {
|
||||
const user = await db.user.findUnique({ where: { username } });
|
||||
return user ?? undefined;
|
||||
@@ -736,10 +763,7 @@ export const storage: IStorage = {
|
||||
return db.payment.create({ data: payment as Payment });
|
||||
},
|
||||
|
||||
async updatePayment(
|
||||
id: number,
|
||||
updates: UpdatePayment,
|
||||
): Promise<Payment> {
|
||||
async updatePayment(id: number, updates: UpdatePayment): Promise<Payment> {
|
||||
const existing = await db.payment.findFirst({ where: { id } });
|
||||
if (!existing) {
|
||||
throw new Error("Payment not found");
|
||||
@@ -799,9 +823,7 @@ export const storage: IStorage = {
|
||||
});
|
||||
},
|
||||
|
||||
async getPaymentById(
|
||||
id: number,
|
||||
): Promise<PaymentWithExtras | null> {
|
||||
async getPaymentById(id: number): Promise<PaymentWithExtras | null> {
|
||||
const payment = await db.payment.findFirst({
|
||||
where: { id },
|
||||
include: {
|
||||
@@ -830,7 +852,7 @@ export const storage: IStorage = {
|
||||
},
|
||||
|
||||
async getPaymentsByClaimId(
|
||||
claimId: number,
|
||||
claimId: number
|
||||
): Promise<PaymentWithExtras | null> {
|
||||
const payment = await db.payment.findFirst({
|
||||
where: { claimId },
|
||||
@@ -930,4 +952,76 @@ export const storage: IStorage = {
|
||||
async getTotalPaymentCountByUser(userId: number): Promise<number> {
|
||||
return db.payment.count({ where: { userId } });
|
||||
},
|
||||
|
||||
// ==============================
|
||||
// Database Backup methods
|
||||
// ==============================
|
||||
async createBackup(userId) {
|
||||
return await db.databaseBackup.create({ data: { userId } });
|
||||
},
|
||||
|
||||
async getLastBackup(userId) {
|
||||
return await db.databaseBackup.findFirst({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
},
|
||||
|
||||
async getBackups(userId, limit = 10) {
|
||||
return await db.databaseBackup.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: limit,
|
||||
});
|
||||
},
|
||||
|
||||
async deleteBackups(userId) {
|
||||
const result = await db.databaseBackup.deleteMany({ where: { userId } });
|
||||
return result.count;
|
||||
},
|
||||
|
||||
// ==============================
|
||||
// Notification methods
|
||||
// ==============================
|
||||
async createNotification(userId, type, message) {
|
||||
return await db.notification.create({
|
||||
data: { userId, type, message },
|
||||
});
|
||||
},
|
||||
|
||||
async getNotifications(
|
||||
userId: number,
|
||||
limit = 50,
|
||||
offset = 0
|
||||
): Promise<Notification[]> {
|
||||
return await db.notification.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: limit,
|
||||
skip: offset,
|
||||
});
|
||||
},
|
||||
|
||||
async markNotificationRead(userId, notificationId) {
|
||||
const result = await db.notification.updateMany({
|
||||
where: { id: notificationId, userId },
|
||||
data: { read: true },
|
||||
});
|
||||
return result.count > 0;
|
||||
},
|
||||
|
||||
async markAllNotificationsRead(userId) {
|
||||
const result = await db.notification.updateMany({
|
||||
where: { userId },
|
||||
data: { read: true },
|
||||
});
|
||||
return result.count;
|
||||
},
|
||||
|
||||
async deleteNotificationsByType(userId, type) {
|
||||
const result = await db.notification.deleteMany({
|
||||
where: { userId, type },
|
||||
});
|
||||
return result.count;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user