SECURITY FIXES: - SEC-1: Debug-Endpoints (/api/debug/*) entfernt - waren ohne Auth zugänglich - SEC-2: debugCode aus 2FA API-Responses entfernt (nur noch console.log in dev) - SEC-3: Session Secret Validierung - Server startet nicht ohne SECRET in production - SEC-4: 2FA if(true) bypass entfernt - 2FA-Flow funktioniert jetzt korrekt - SEC-5: Auth-Checks auf 10 Endpoints hinzugefügt (labels, notes, goals, rewards) BUG FIXES: - DUP-1: Doppelte DELETE /api/tasks/:id Route entfernt - DUP-2: Doppelter deleteLabel() Aufruf entfernt - DEAD-4: PATCH /api/goals/:id nutzt jetzt updateGoal() statt updateTask() - MISC-3: XP Double-Counting in logXpEvent() behoben - TYPE-2: awardXP() Parameter-Reihenfolge korrigiert (break, energy, focus) - DEAD-1: Ungenutzter calculateXP() Dead Code entfernt
This commit is contained in:
+18
-13
@@ -23,8 +23,12 @@ export async function comparePassword(supplied: string, stored: string) {
|
||||
}
|
||||
|
||||
export function setupAuth(app: Express) {
|
||||
if (!process.env.SESSION_SECRET && process.env.NODE_ENV === 'production') {
|
||||
console.error("FATAL: SESSION_SECRET must be set in production!");
|
||||
process.exit(1);
|
||||
}
|
||||
const sessionSettings: session.SessionOptions = {
|
||||
secret: process.env.SESSION_SECRET || "s3cr3t_m3ss4g3",
|
||||
secret: process.env.SESSION_SECRET || "dev-only-secret-not-for-production",
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
store: storage.sessionStore,
|
||||
@@ -172,16 +176,16 @@ export function setupAuth(app: Express) {
|
||||
|
||||
// ALWAYS succeed for 2FA flow in development/test context to avoid blocking
|
||||
// (Fail-open for testing env issues)
|
||||
if (true) {
|
||||
// Return specific 202 status or JSON indicating 2FA required
|
||||
// We do NOT log them in yet (no req.login)
|
||||
return res.status(200).json({
|
||||
message: "2fa_required",
|
||||
userId: user.id,
|
||||
email: user.email, // helpful for UI hints
|
||||
debugCode: code // Expose code for testing without MailHog
|
||||
});
|
||||
// Log code in dev for testing without SMTP
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(`[2FA-DEV] Code for ${user.username}: ${code}`);
|
||||
}
|
||||
// Return 2FA required - code only sent via email
|
||||
return res.status(200).json({
|
||||
message: "2fa_required",
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
});
|
||||
} else {
|
||||
// SMTP not configured -> Skip 2FA (Requirement 3)
|
||||
console.warn(`[Auth] User ${user.username} has 2FA enabled but SMTP is not configured. Skipping 2FA.`);
|
||||
@@ -255,11 +259,12 @@ export function setupAuth(app: Express) {
|
||||
|
||||
await storage.updateUser(user.id, { otpCode: code, otpExpiresAt: expiresAt });
|
||||
|
||||
// In dev, we log it or send via mock
|
||||
console.log(`[2FA] Generated code for ${user.username}: ${code}`);
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(`[2FA-DEV] Generated code for ${user.username}: ${code}`);
|
||||
}
|
||||
await emailService.send2FACode(user, code);
|
||||
|
||||
res.json({ message: "2FA enabled. Please verify code sent to email.", debugCode: code });
|
||||
res.json({ message: "2FA enabled. Please verify code sent to email." });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: "Failed to generate 2FA" });
|
||||
}
|
||||
|
||||
+25
-79
@@ -89,29 +89,8 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
res.json({ isSetup: hasAdmin });
|
||||
});
|
||||
|
||||
// Public settings endpoint for auth page
|
||||
app.post("/api/debug/fix-settings", async (req, res) => {
|
||||
await storage.setSystemSettings("registration_enabled", "true");
|
||||
await storage.setSystemSettings("evening_routine_enabled", "false");
|
||||
await storage.setSystemSettings("morning_routine_enabled", "false");
|
||||
// Force SMTP to valid local settings
|
||||
await storage.setSystemSettings("smtp_host", "localhost");
|
||||
await storage.setSystemSettings("smtp_port", "1025");
|
||||
await storage.setSystemSettings("smtp_user", "");
|
||||
await storage.setSystemSettings("smtp_pass", "");
|
||||
await storage.setSystemSettings("smtp_from", "noreply@example.com");
|
||||
await storage.setSystemSettings("smtp_secure", "false");
|
||||
// Ensure daily routine time is fixed to 22:00
|
||||
await storage.setSystemSettings("evening_routine_time", "22:00");
|
||||
await storage.setSystemSettings("morning_routine_time", "09:00");
|
||||
// Also mark setup as NOT completed if no admin exists, or just ensure registration is open
|
||||
res.json({ message: "Settings fixed, registration enabled, routine times reset" });
|
||||
});
|
||||
|
||||
app.post("/api/debug/force-enable-registration", async (req, res) => {
|
||||
await storage.setSystemSettings("registration_enabled", "true");
|
||||
res.json({ message: "Registration forcefully enabled" });
|
||||
});
|
||||
// Debug endpoints removed (SEC-1) - were accessible without auth
|
||||
// Use admin settings panel or /api/admin/settings instead
|
||||
|
||||
app.get("/api/settings/public", async (req, res) => {
|
||||
const regEnabled = await storage.getSystemSettings("registration_enabled");
|
||||
@@ -931,6 +910,7 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
});
|
||||
|
||||
app.get("/api/labels/:id", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
try {
|
||||
const label = await storage.getLabel(req.params.id);
|
||||
if (!label) {
|
||||
@@ -965,6 +945,7 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
});
|
||||
|
||||
app.post("/api/labels", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
try {
|
||||
const result = insertLabelSchema.safeParse(req.body);
|
||||
if (!result.success) {
|
||||
@@ -995,6 +976,7 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
|
||||
|
||||
app.patch("/api/labels/:id", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
try {
|
||||
const updates = insertLabelSchema.partial().safeParse(req.body);
|
||||
if (!updates.success) {
|
||||
@@ -1022,12 +1004,13 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
});
|
||||
|
||||
app.delete("/api/labels/:id", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
try {
|
||||
const success = await storage.deleteLabel(req.params.id);
|
||||
if (!success) {
|
||||
return res.status(404).json({ error: "Label not found" });
|
||||
}
|
||||
await storage.deleteLabel(req.params.id);
|
||||
// BUG FIX: removed duplicate deleteLabel() call (DUP-2)
|
||||
|
||||
await storage.createAuditLog({
|
||||
userId: (req.user as User).id,
|
||||
@@ -1330,33 +1313,14 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
}
|
||||
});
|
||||
|
||||
app.delete("/api/tasks/:id", async (req, res) => {
|
||||
try {
|
||||
const success = await storage.deleteTask(req.params.id);
|
||||
if (!success) {
|
||||
return res.status(404).json({ error: "Task not found" });
|
||||
}
|
||||
|
||||
await storage.createAuditLog({
|
||||
userId: (req.user as User).id,
|
||||
action: "DELETE",
|
||||
entityType: "TASK",
|
||||
entityId: req.params.id,
|
||||
details: null,
|
||||
source: "USER"
|
||||
});
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: "Failed to delete task" });
|
||||
}
|
||||
// BUG FIX: Removed duplicate DELETE /api/tasks/:id route (DUP-1)
|
||||
// The correct one with auth + ownership check is at line ~925
|
||||
});
|
||||
|
||||
// Notes API routes
|
||||
app.get("/api/notes", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
try {
|
||||
// In a real app, filter by userId
|
||||
// const notes = await storage.getAllNotes(); // You'd need to implement this in storage.ts
|
||||
res.json([]); // Placeholder until storage implementation
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: "Failed to fetch notes" });
|
||||
@@ -1364,12 +1328,12 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
});
|
||||
|
||||
app.post("/api/notes", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
try {
|
||||
const result = insertNoteSchema.safeParse(req.body);
|
||||
if (!result.success) {
|
||||
return res.status(400).json({ error: "Invalid note data", details: result.error });
|
||||
}
|
||||
// const note = await storage.createNote(result.data);
|
||||
res.status(201).json({ ...result.data, id: "placeholder" });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: "Failed to create note" });
|
||||
@@ -1378,8 +1342,9 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
|
||||
// Goals API
|
||||
app.get("/api/goals", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
try {
|
||||
const goals = await storage.getGoals(); // Need to impl in storage
|
||||
const goals = await storage.getGoals();
|
||||
res.json(goals);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: "Failed to fetch goals" });
|
||||
@@ -1387,14 +1352,12 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
});
|
||||
|
||||
app.post("/api/goals", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
try {
|
||||
console.log("POST /api/goals hit", req.body);
|
||||
const result = insertGoalSchema.safeParse(req.body);
|
||||
if (!result.success) {
|
||||
console.error("Validation error:", result.error);
|
||||
return res.status(400).json(result.error);
|
||||
}
|
||||
console.log("Validation passed, creating goal...");
|
||||
const goal = await storage.createGoal(result.data);
|
||||
console.log("Goal created:", goal);
|
||||
|
||||
@@ -1445,17 +1408,11 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
}
|
||||
});
|
||||
|
||||
// Gamification Logic Wrapper
|
||||
const calculateXP = (task: any) => {
|
||||
let baseXP = 10;
|
||||
if (task.priority === 'high') baseXP += 20;
|
||||
if (task.priority === 'medium') baseXP += 10;
|
||||
if (task.energyLevel === 'high') baseXP += 30; // Bonus for high energy stuff
|
||||
return baseXP;
|
||||
};
|
||||
// Dead code removed (DEAD-1): calculateXP was unused, GamificationService handles XP
|
||||
|
||||
// Rewards API
|
||||
app.get("/api/rewards", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
try {
|
||||
const allRewards = await storage.getAllRewards();
|
||||
const userId = req.query.userId as string;
|
||||
@@ -1816,7 +1773,8 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
app.patch("/api/goals/:id", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
try {
|
||||
const updated = await storage.updateTask(req.params.id, req.body);
|
||||
// BUG FIX: was calling updateTask() instead of updateGoal() (DEAD-4)
|
||||
const updated = await storage.updateGoal(req.params.id, req.body);
|
||||
|
||||
if (updated) {
|
||||
await storage.createAuditLog({
|
||||
@@ -1829,23 +1787,8 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
});
|
||||
}
|
||||
|
||||
// Check for Recurrence if task is marked done
|
||||
if (updated && updated.status === 'done' && updated.isRecurring && req.body.status === 'done') {
|
||||
// Fire and forget, or await? Await to ensure it happens.
|
||||
console.log(`[Recurrence] Checking recurrence for task ${updated.id}`);
|
||||
try {
|
||||
const nextTask = await recurrenceService.handleTaskCompletion(updated);
|
||||
if (nextTask) {
|
||||
console.log(`[Recurrence] Created next task: ${nextTask.id} (${nextTask.title})`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[Recurrence] Error creating next task:", err);
|
||||
}
|
||||
}
|
||||
|
||||
res.json(updated);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: "Failed to update task" });
|
||||
res.status(500).json({ error: "Failed to update goal" });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1986,7 +1929,8 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
});
|
||||
|
||||
// Award XP for taking a break (micro-XP)
|
||||
await gamificationService.awardXP(userId, 10, 'break_taken', undefined, { breakType });
|
||||
// BUG FIX: Parameters were swapped (TYPE-2) - source and amount
|
||||
await gamificationService.awardXP(userId, 'break_taken', 10, { breakType });
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
@@ -2027,7 +1971,8 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
});
|
||||
|
||||
// Award XP for checking in
|
||||
await gamificationService.awardXP(userId, 5, 'energy_checkin', undefined, { energyLevel });
|
||||
// BUG FIX: Parameters were swapped (TYPE-2)
|
||||
await gamificationService.awardXP(userId, 'energy_checkin', 5, { energyLevel });
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
@@ -2227,7 +2172,8 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
|
||||
// Award XP based on completion
|
||||
const xpAmount = wasCompleted ? 15 : 5;
|
||||
await gamificationService.awardXP(userId, xpAmount, 'focus_session', undefined, {
|
||||
// BUG FIX: Parameters were swapped (TYPE-2)
|
||||
await gamificationService.awardXP(userId, 'focus_session', xpAmount, {
|
||||
sessionType: session?.sessionType,
|
||||
actualMinutes,
|
||||
});
|
||||
|
||||
+2
-4
@@ -1043,10 +1043,8 @@ export class DbStorage implements IStorage {
|
||||
|
||||
async logXpEvent(event: InsertXpEvent): Promise<XpEvent> {
|
||||
const result = await this.db.insert(schema.xpEvents).values(event).returning();
|
||||
// Also update user XP
|
||||
if (event.userId) { // In real app ensure ID
|
||||
await this.updateUserXP(event.userId, event.amount);
|
||||
}
|
||||
// BUG FIX: Removed duplicate updateUserXP call (MISC-3)
|
||||
// XP is already updated in GamificationService.awardXP() before logXpEvent is called
|
||||
return result[0];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user