Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,12 @@ EMAIL_FROM_ADDRESS=noreply@example.com
APP_PORT=3000
SERVER_ENVIRONMENT=DEVELOPMENT
AURORA_WEB_APP_BASE_URL=http://localhost:3000


# ========================
# User Reminder CronJob Settings
# ========================
REMINDER_ENABLED=true
CRON_SCHEDULE="0 2 * * *"
REMINDER_MAX_RETRIES=2
REMINDER_RETRY_INTERVAL_HOURS=24
5 changes: 0 additions & 5 deletions backend/logs/error.log
Original file line number Diff line number Diff line change
@@ -1,5 +0,0 @@
{"level":"error","message":"TypeError: Cannot read properties of undefined (reading 'id')\n at C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\src\\controllers\\question.controller.ts:8:39\n at Generator.next (<anonymous>)\n at C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\src\\controllers\\question.controller.ts:8:71\n at new Promise (<anonymous>)\n at __awaiter (C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\src\\controllers\\question.controller.ts:4:12)\n at C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\src\\controllers\\question.controller.ts:7:82\n at C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\src\\middlewares\\async.ts:6:19\n at Layer.handle [as handle_request] (C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at next (C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\express\\lib\\router\\route.js:149:13)\n at C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\src\\middlewares\\validator.ts:25:12"}
{"level":"error","message":"TypeError: Cannot read properties of undefined (reading 'id')\n at C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\src\\controllers\\question.controller.ts:8:39\n at Generator.next (<anonymous>)\n at C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\src\\controllers\\question.controller.ts:8:71\n at new Promise (<anonymous>)\n at __awaiter (C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\src\\controllers\\question.controller.ts:4:12)\n at C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\src\\controllers\\question.controller.ts:7:82\n at C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\src\\middlewares\\async.ts:6:19\n at Layer.handle [as handle_request] (C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at next (C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\express\\lib\\router\\route.js:149:13)\n at C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\src\\middlewares\\validator.ts:25:12"}
{"level":"error","message":"SyntaxError: Unexpected token '`', ...\"entence\": `\"My daugh\"... is not valid JSON\n at JSON.parse (<anonymous>)\n at parse (C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\body-parser\\lib\\types\\json.js:92:19)\n at C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\body-parser\\lib\\read.js:128:18\n at AsyncResource.runInAsyncScope (node:async_hooks:214:14)\n at invokeCallback (C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\raw-body\\index.js:238:16)\n at done (C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\raw-body\\index.js:227:7)\n at IncomingMessage.onEnd (C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\raw-body\\index.js:287:7)\n at IncomingMessage.emit (node:events:518:28)\n at IncomingMessage.emit (node:domain:489:12)\n at endReadableNT (node:internal/streams/readable:1698:12)"}
{"level":"error","message":"SyntaxError: Unexpected token '`', ...\"entence\": `\"My daugh\"... is not valid JSON\n at JSON.parse (<anonymous>)\n at parse (C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\body-parser\\lib\\types\\json.js:92:19)\n at C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\body-parser\\lib\\read.js:128:18\n at AsyncResource.runInAsyncScope (node:async_hooks:214:14)\n at invokeCallback (C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\raw-body\\index.js:238:16)\n at done (C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\raw-body\\index.js:227:7)\n at IncomingMessage.onEnd (C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\raw-body\\index.js:287:7)\n at IncomingMessage.emit (node:events:518:28)\n at IncomingMessage.emit (node:domain:489:12)\n at endReadableNT (node:internal/streams/readable:1698:12)"}
{"level":"error","message":"SyntaxError: Unexpected token '`', ...\"entence\": `\"My daugh\"... is not valid JSON\n at JSON.parse (<anonymous>)\n at parse (C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\body-parser\\lib\\types\\json.js:92:19)\n at C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\body-parser\\lib\\read.js:128:18\n at AsyncResource.runInAsyncScope (node:async_hooks:214:14)\n at invokeCallback (C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\raw-body\\index.js:238:16)\n at done (C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\raw-body\\index.js:227:7)\n at IncomingMessage.onEnd (C:\\projects\\others\\onlyDust\\AURORA\\AURORA-Backend\\backend\\node_modules\\raw-body\\index.js:287:7)\n at IncomingMessage.emit (node:events:518:28)\n at IncomingMessage.emit (node:domain:489:12)\n at endReadableNT (node:internal/streams/readable:1698:12)"}
17 changes: 17 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"start:prod": "node build/src/server.js",
"build": "rimraf ./build && tsc",
"start": "npm run build && node build/src/server.js",
"import:questions": "ts-node --project tsconfig.json scripts/importQuestions.ts"
"import:questions": "ts-node --project tsconfig.json scripts/importQuestions.ts",
"seed": "ts-node --project tsconfig.json prisma/seed.ts"
},
"keywords": [],
"author": "",
Expand All @@ -19,6 +20,7 @@
"dependencies": {
"@stellar/stellar-sdk": "13.3.0",
"@types/compression": "^1.7.5",
"@types/node-cron": "^3.0.11",
"axios": "^1.7.7",
"bcrypt": "^6.0.0",
"compression": "^1.8.0",
Expand All @@ -35,6 +37,7 @@
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"node-cron": "^4.2.1",
"nodemailer": "^6.10.0",
"pg": "^8.14.1",
"rimraf": "^6.0.1",
Expand Down Expand Up @@ -70,4 +73,4 @@
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
}
}
}
2 changes: 1 addition & 1 deletion backend/prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
provider = "postgresql"
3 changes: 3 additions & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ model User {
phoneNumber String?
profileImage String?
status Status @default(INACTIVE)
lastLoginAt DateTime?
lastActivityAt DateTime?
lastReminderSent DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
wallet Wallet?
Expand Down
3 changes: 3 additions & 0 deletions backend/src/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ export const login = asyncHandler(async (req: Request, res: Response) => {
const isPasswordValid = await Bcrypt.compare(password, user.password)
if (!isPasswordValid) throw new BadRequestError("Invalid credentials")

// Update login and activity timestamps
await UserService.updateLastLogin(user.id)

const token = Jwt.issue({ id: user.id }, "1d")

const userResponse = {
Expand Down
18 changes: 18 additions & 0 deletions backend/src/core/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ const envVarsSchema = Joi.object()
AURORA_WEB_APP_BASE_URL: Joi.string()
.required()
.description("Base URL for Aurora Web App"),
REMINDER_ENABLED: Joi.boolean()
.default(true)
.description("Enable reminder emails"),
CRON_SCHEDULE: Joi.string()
.default("0 2 * * *")
.description("CRON schedule for reminder job"),
REMINDER_MAX_RETRIES: Joi.number()
.default(2)
.description("Maximum retries for failed email reminders"),
REMINDER_RETRY_INTERVAL_HOURS: Joi.number()
.default(24)
.description("Hours between email retry attempts"),
})
.unknown();

Expand All @@ -44,6 +56,12 @@ const serverSettings = {
password: envVars.EMAIL_PASSWORD,
fromAddress: envVars.EMAIL_FROM_ADDRESS,
},
reminders: {
enabled: envVars.REMINDER_ENABLED,
cronSchedule: envVars.CRON_SCHEDULE,
maxRetries: envVars.REMINDER_MAX_RETRIES,
retryIntervalHours: envVars.REMINDER_RETRY_INTERVAL_HOURS,
},
};

export default serverSettings;
2 changes: 1 addition & 1 deletion backend/src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const connectDB = async () => {
console.log("πŸ“¦ Connected to the Supabase database successfully!");
client.release();
} catch (error) {
console.error("❌ Database connection error:", error);
console.error("❌ Database connection errors:", error);
process.exit(1); // Exit the process if the connection fails
}
};
Expand Down
154 changes: 154 additions & 0 deletions backend/src/jobs/userReminder.job.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import NotificationService from "../services/notification.service";
import EmailNotifier from "../utils/service/emailNotifier";
import UserService from "../services/user.service";

interface JobResult {
success: boolean;
stats: {
candidatesFound: number;
emailsSent: number;
emailsFailed: number;
};
errors: Array<{ email: string; error: string }>;
executionTime: number;
}

class UserReminderJob {
/**
* Main job function to check inactive users and send reminders
*/
public static async checkInactiveUsers(maxRetries: number = 2): Promise<JobResult> {
const startTime = Date.now();
console.log("πŸ”„ Starting user reminder job...");

try {
// Get reminder statistics for logging
const stats = await NotificationService.getReminderStats();
console.log("πŸ“Š Reminder Stats:", {
totalInactive: stats.totalInactive,
candidatesFound: stats.candidatesFound,
breakdown: stats.breakdown,
});

// Find users who need reminders
const candidates = await NotificationService.findReminderCandidates();

if (candidates.length === 0) {
console.log("βœ… No users found requiring reminders");
return {
success: true,
stats: {
candidatesFound: 0,
emailsSent: 0,
emailsFailed: 0,
},
errors: [],
executionTime: Date.now() - startTime,
};
}

console.log(`πŸ“§ Sending reminders to ${candidates.length} users`);

// Prepare email data
const reminderEmails = candidates.map(candidate => ({
email: candidate.email,
firstName: candidate.firstName || undefined,
reminderType: candidate.reminderType,
daysSinceActivity: candidate.daysSinceActivity,
}));

// Send emails with retry logic
const emailResults = await EmailNotifier.sendReminderEmails(reminderEmails, maxRetries);

// Update lastReminderSent for successfully sent emails
const successfulEmails = reminderEmails.slice(0, emailResults.success);
await this.updateReminderSentTimestamps(successfulEmails.map(email =>
candidates.find(c => c.email === email.email)!.id
));

const executionTime = Date.now() - startTime;

console.log("βœ… User reminder job completed:", {
candidatesFound: candidates.length,
emailsSent: emailResults.success,
emailsFailed: emailResults.failed,
executionTimeMs: executionTime,
});

return {
success: true,
stats: {
candidatesFound: candidates.length,
emailsSent: emailResults.success,
emailsFailed: emailResults.failed,
},
errors: emailResults.errors,
executionTime,
};

} catch (error) {
const executionTime = Date.now() - startTime;
console.error("❌ User reminder job failed:", error);

return {
success: false,
stats: {
candidatesFound: 0,
emailsSent: 0,
emailsFailed: 0,
},
errors: [{ email: "system", error: error instanceof Error ? error.message : "Unknown error" }],
executionTime,
};
}
}

/**
* Update lastReminderSent timestamp for users who received reminders
*/
private static async updateReminderSentTimestamps(userIds: string[]): Promise<void> {
try {
const updatePromises = userIds.map(userId =>
UserService.updateReminderSent(userId)
);

await Promise.all(updatePromises);
console.log(`βœ… Updated reminder timestamps for ${userIds.length} users`);
} catch (error) {
console.error("❌ Failed to update reminder timestamps:", error);
}
}

/**
* Manual trigger for testing purposes
*/
public static async triggerManual(): Promise<JobResult> {
console.log("πŸ”§ Manual trigger of user reminder job");
return this.checkInactiveUsers();
}

/**
* Get job health status
*/
public static async getHealthStatus(): Promise<{
isHealthy: boolean;
lastRun?: Date;
nextRun?: string;
}> {
try {
await NotificationService.getReminderStats();

return {
isHealthy: true,
lastRun: new Date(),
nextRun: "Based on CRON schedule",
};
} catch (error) {
return {
isHealthy: false,
};
}
}
}

export default UserReminderJob;
3 changes: 3 additions & 0 deletions backend/src/middlewares/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export const isAuthorized = () => {
const user = await UserService.readUserById(decoded.payload.id)
if (!user) return next(new UnauthorizedError("Unauthorized - User not found"))

// Track user activity
await UserService.updateLastActivity(user.id);

req.user = user
res.locals.account = user
next()
Expand Down
16 changes: 16 additions & 0 deletions backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,30 @@ import path from "path";
import app from "./app";
import settings from "./core/config/settings";
import { connectDB } from "./db";
import JobSchedulerService from "./services/jobScheduler.service";

dotenv.config();

const server = app;
const port = settings.serverPort || 8000;

// Connect to database
connectDB();

// Initialize scheduled jobs
JobSchedulerService.initializeJobs();


// Graceful shutdown
const gracefulShutdown = (signal: string) => {
console.log(`${signal} received, shutting down gracefully`);
JobSchedulerService.shutdown();
process.exit(0);
};

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

server.listen(port, () => {
console.log(`πŸš€πŸš€πŸš€ Aurora's server is running at http://localhost:${port} πŸš€πŸš€πŸš€`);
});
Loading