import type { Express } from "express"; import { createServer, type Server } from "http"; import { storage } from "./storage.js"; import { insertLabelSchema, insertUserSchema, insertTaskSchema, insertNoteSchema, insertGoalSchema, insertRewardSchema, insertUserRewardSchema, User, Task } from "../shared/schema.js"; import { z } from "zod"; import { EmailService } from "./email.js"; import { mcpServer } from "./mcp"; import { AiService, DEFAULT_SYSTEM_PROMPT } from "./ai.js"; import { RecurrenceService } from "./services/recurrence.js"; import { GamificationService } from "./gamification.js"; import { getVapidPublicKey, savePushSubscription, removePushSubscription, removeUserSubscriptions, sendPushNotification } from "./push.js"; const emailService = new EmailService(storage); const aiService = new AiService(storage); const recurrenceService = new RecurrenceService(storage); const gamificationService = new GamificationService(storage); import { setupAuth, hashPassword, comparePassword } from "./auth.js"; import { apiKeyAuth } from "./api-key-auth.js"; function isAdmin(req: any, res: any, next: any) { if (req.isAuthenticated() && req.user.role === 'admin') { return next(); } res.status(403).json({ error: "Unauthorized: Admin access required" }); } export async function registerRoutes(app: Express): Promise { setupAuth(app); // API Key auth middleware - must be after session setup // Allows external tools to authenticate via X-API-Key header app.use("/api", apiKeyAuth()); // Update user schedule app.patch("/api/user/schedule", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { const { start, end, days, availability } = req.body; const updates: Partial = {}; // Backward compatibility / Simple Mode if (start && end && days) { updates.workHours = { start, end, days }; // Sync to availability.work if availability not explicitly provided? if (!availability) { updates.availability = { work: { start, end, days }, personal: (req.user as User).availability?.personal || { start: "18:00", end: "22:00", days: [1, 2, 3, 4, 5, 0, 6] } }; } } // New Mode if (availability) { updates.availability = availability; // Sync workHours to availability.work for legacy support if (availability.work) { updates.workHours = availability.work; } } if (Object.keys(updates).length === 0) { return res.status(400).json({ error: "No schedule data provided" }); } const updated = await storage.updateUser(userId, updates); await storage.createAuditLog({ userId, action: "UPDATE", entityType: "USER", entityId: userId, details: { action: "UPDATE_SCHEDULE", updates }, source: "USER" }); res.json(updated); } catch (e) { res.status(500).json({ error: "Failed to update schedule" }); } }); // --- Setup Routes --- app.get("/api/setup/status", async (req, res) => { const hasAdmin = await storage.hasAdminUser(); res.json({ isSetup: hasAdmin }); }); // 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"); // Default to true if not set, or specifically check for "false" res.json({ registration_enabled: regEnabled !== "false" }); }); app.post("/api/setup", async (req, res) => { const hasAdmin = await storage.hasAdminUser(); if (hasAdmin) { return res.status(403).json({ error: "Setup already completed" }); } // Create Super Admin try { const hashedPassword = await hashPassword(req.body.password); const adminUser = await storage.createUser({ username: req.body.username, email: req.body.email, password: hashedPassword, role: 'admin', isActive: true }); // Auto-enable registration by default on setup await storage.setSystemSettings("registration_enabled", "true"); req.login(adminUser, (err) => { if (err) return res.status(500).json({ error: "Login failed after setup" }); req.session.save(() => { return res.json(adminUser); }); }); } catch (e) { res.status(500).json({ error: "Failed to create admin user" }); } }); // Check if AI is configured (for UI empty states) app.get("/api/ai/status", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const key = await storage.getSystemSettings("ai_api_key"); const provider = await storage.getSystemSettings("ai_provider"); // Consider configured if we have a key OR if provider is 'ollama' const isConfigured = (provider === 'ollama') || (!!key && key.length > 0); res.json({ configured: isConfigured, provider }); }); // --- Password Reset Routes --- app.post("/api/auth/forgot-password", async (req, res) => { try { const { email } = req.body; if (!email) return res.status(400).json({ error: "Email required" }); const user = await storage.getUserByEmail(email); if (!user) { // Check security best practices: delay response or return success to avoid enumeration? // For now, let's behave nicely. return res.json({ message: "If an account exists, a reset email has been sent." }); } const tokenString = crypto.randomUUID(); // Expires in 1 hour const expiresAt = new Date(Date.now() + 60 * 60 * 1000); await storage.createPasswordResetToken({ userId: user.id, token: tokenString, expiresAt, isUsed: false }); await emailService.sendPasswordResetEmail(user, tokenString); res.json({ message: "If an account exists, a reset email has been sent." }); } catch (e) { console.error("Forgot Password Error:", e); res.status(500).json({ error: "Server error" }); } }); app.post("/api/auth/reset-password", async (req, res) => { try { const { token, newPassword } = req.body; if (!token || !newPassword) return res.status(400).json({ error: "Token and password required" }); const resetToken = await storage.getPasswordResetToken(token); if (!resetToken) { return res.status(400).json({ error: "Invalid or expired token" }); } if (resetToken.isUsed) { return res.status(400).json({ error: "Token already used" }); } if (new Date() > new Date(resetToken.expiresAt)) { return res.status(400).json({ error: "Token expired" }); } // Update User Password const hashedPassword = await hashPassword(newPassword); await storage.updateUser(resetToken.userId, { password: hashedPassword }); // Mark token used await storage.markPasswordResetTokenUsed(resetToken.id); res.json({ message: "Password reset successfully. You can now login." }); } catch (e) { console.error("Reset Password Error:", e); res.status(500).json({ error: "Server error" }); } }); // --- Admin Routes --- app.get("/api/admin/users", isAdmin, async (req, res) => { try { const users = await storage.getAllUsers(); res.json(users); } catch (e) { res.status(500).json({ error: "Failed to fetch users" }); } }); app.post("/api/admin/users", isAdmin, async (req, res) => { try { const hashedPassword = await hashPassword(req.body.password); const newUser = await storage.createUser({ username: req.body.username, email: req.body.email, password: hashedPassword, role: req.body.role || 'user', isActive: true }); await storage.createAuditLog({ userId: (req.user as User).id, action: "CREATE", entityType: "USER", entityId: newUser.id, details: { username: newUser.username, role: newUser.role }, source: "USER" }); res.json(newUser); } catch (e) { res.status(500).json({ error: "Failed to create user" }); } }); app.post("/api/admin/users/:id/toggle-active", isAdmin, async (req, res) => { try { const user = await storage.getUser(req.params.id); if (!user) return res.status(404).json({ error: "User not found" }); if (user.role === 'admin' && user.id === (req.user as User).id) { return res.status(400).json({ error: "Cannot deactivate yourself" }); } const updated = await storage.updateUser(user.id, { isActive: !user.isActive }); await storage.createAuditLog({ userId: (req.user as User).id, action: "UPDATE", entityType: "USER", entityId: user.id, details: { isActive: !user.isActive }, source: "USER" }); res.json(updated); } catch (e) { res.status(500).json({ error: "Failed to toggle user status" }); } }); app.delete("/api/admin/users/:id", isAdmin, async (req, res) => { try { const user = await storage.getUser(req.params.id); if (!user) return res.status(404).json({ error: "User not found" }); if (user.role === 'admin' && user.id === (req.user as User).id) { return res.status(400).json({ error: "Cannot delete yourself" }); } await storage.deleteUser(user.id); await storage.createAuditLog({ userId: (req.user as User).id, action: "DELETE", entityType: "USER", entityId: user.id, details: { username: user.username }, source: "USER" }); res.sendStatus(204); } catch (e) { res.status(500).json({ error: "Failed to delete user" }); } }); app.post("/api/admin/users/:id/reset-xp", isAdmin, async (req, res) => { try { const user = await storage.getUser(req.params.id); if (!user) return res.status(404).json({ error: "User not found" }); const updated = await storage.updateUser(user.id, { xp: 0, level: 1 }); await storage.createAuditLog({ userId: (req.user as User).id, action: "UPDATE", entityType: "USER", entityId: user.id, details: { action: "RESET_XP", previousXp: user.xp, previousLevel: user.level }, source: "ADMIN" }); res.json(updated); } catch (e) { res.status(500).json({ error: "Failed to reset user XP" }); } }); // --- MCP Routes --- app.get("/api/mcp/sse", async (req, res) => { const enabled = await storage.getSystemSettings("mcp_enabled"); if (enabled !== "true") return res.status(503).send("MCP Server Disabled"); await mcpServer.handleSse(req, res); }); app.post("/api/mcp/messages", async (req, res) => { const enabled = await storage.getSystemSettings("mcp_enabled"); if (enabled !== "true") return res.status(503).json({ error: "MCP Server Disabled" }); await mcpServer.handleMessage(req, res); }); app.get("/api/admin/audit-logs", isAdmin, async (req, res) => { try { const logs = await storage.getAuditLogs(); res.json(logs); } catch (e) { res.status(500).json({ error: "Failed to fetch audit logs" }); } }); app.get("/api/admin/settings", isAdmin, async (req, res) => { const keys = [ "registration_enabled", "smtp_host", "smtp_port", "smtp_user", "smtp_pass", "smtp_from", "smtp_secure", "ai_provider", "ai_api_key", "ai_model", "ai_base_url", "mcp_enabled", "mcp_port" ]; const settings: any = {}; for (const key of keys) { const val = await storage.getSystemSettings(key); if (key === "registration_enabled" || key === "smtp_secure" || key === "mcp_enabled") { settings[key] = val === "true"; } else { settings[key] = val || ""; // Return empty string if undefined for inputs } } // Inject System Prompt with Default Fallback const storedPrompt = await storage.getSystemSettings("ai_system_prompt"); settings["ai_system_prompt"] = storedPrompt || DEFAULT_SYSTEM_PROMPT; res.json(settings); }); app.post("/api/admin/settings", isAdmin, async (req, res) => { const updates = req.body; const currentPrompt = await storage.getSystemSettings("ai_system_prompt"); // Check for System Prompt changes & Log if (updates.ai_system_prompt && updates.ai_system_prompt !== currentPrompt) { await storage.createAuditLog({ userId: (req.user as User).id, action: "UPDATE", entityType: "SYSTEM_SETTINGS", entityId: "ai_system_prompt", details: { message: "AI System Prompt updated" }, source: "ADMIN" }); } const keys = [ "registration_enabled", "smtp_host", "smtp_port", "smtp_user", "smtp_pass", "smtp_from", "smtp_secure", "ai_provider", "ai_api_key", "ai_model", "ai_base_url", "ai_system_prompt", "mcp_enabled", "mcp_port" ]; for (const key of keys) { if (req.body[key] !== undefined) { await storage.setSystemSettings(key, String(req.body[key])); } } await storage.createAuditLog({ userId: (req.user as User).id, action: "UPDATE", entityType: "SYSTEM_SETTINGS", entityId: null, details: req.body, source: "USER" }); res.json({ success: true }); }); app.post("/api/admin/ollama/tags", isAdmin, async (req, res) => { const { baseUrl } = req.body; // Default to localhost:11434 if not provided, or stored setting? // Client should send the value from the input field. const url = (baseUrl || "http://localhost:11434").replace(/\/$/, "") + "/api/tags"; try { const resp = await fetch(url); if (!resp.ok) throw new Error(`Ollama Error: ${resp.statusText}`); const data = await resp.json(); res.json(data); } catch (e: any) { console.error("Ollama Tags Error:", e); res.status(500).json({ error: "Failed to fetch Ollama tags: " + e.message }); } }); app.post("/api/admin/ollama/pull", isAdmin, async (req, res) => { const { baseUrl, model } = req.body; if (!model) return res.status(400).json({ error: "Model name required" }); const url = (baseUrl || "http://localhost:11434").replace(/\/$/, "") + "/api/pull"; console.log(`Pulling Ollama model ${model} from ${url}...`); try { // connecting to ollama const resp = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: model, stream: false }), }); if (!resp.ok) { const errText = await resp.text(); throw new Error(`Ollama Pull Error: ${errText}`); } const data = await resp.json(); res.json(data); } catch (e: any) { console.error("Ollama Pull Error:", e); res.status(500).json({ error: "Failed to pull model: " + e.message }); } }); // --- AI Chat History Routes --- // Get all conversations for user app.get("/api/ai/conversations", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const conversations = await storage.getConversations((req.user as User).id); res.json(conversations); } catch (e) { res.status(500).json({ error: "Failed to fetch conversations" }); } }); // Create new conversation app.post("/api/ai/conversations", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const user = req.user as User; try { const { title } = req.body; const conversation = await storage.createConversation(user.id, title); await storage.createAuditLog({ userId: user.id, action: "CREATE", entityType: "CONVERSATION", entityId: conversation.id, details: { title }, source: "USER" }); res.json(conversation); } catch (e) { res.status(500).json({ error: "Failed to create conversation" }); } }); // Rename conversation app.patch("/api/ai/conversations/:id", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const user = req.user as User; try { const { title } = req.body; if (!title) return res.status(400).json({ error: "Title is required" }); const conv = await storage.getConversation(req.params.id); if (!conv) return res.status(404).json({ error: "Conversation not found" }); if (conv.userId !== user.id) return res.sendStatus(403); const updated = await storage.updateConversation(req.params.id, title); await storage.createAuditLog({ userId: user.id, action: "UPDATE", entityType: "CONVERSATION", entityId: req.params.id, details: { title }, source: "USER" }); res.json(updated); } catch (e) { res.status(500).json({ error: "Failed to update conversation" }); } }); // Purchase Reward app.post("/api/rewards/:rewardId/purchase", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; const rewardId = req.params.rewardId; try { const reward = await storage.getReward(rewardId); if (!reward) return res.status(404).json({ error: "Reward not found" }); const purchase = await storage.purchaseReward(userId, reward.id, reward.cost); await storage.createAuditLog({ userId: userId, action: "PURCHASE", entityType: "REWARD", entityId: reward.id, details: { name: reward.title, cost: reward.cost }, source: "USER" }); res.json(purchase); } catch (e) { res.status(500).json({ error: "Purchase failed" }); } }); // Get single conversation app.get("/api/ai/conversations/:id", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const conv = await storage.getConversation(req.params.id); if (!conv) return res.status(404).json({ error: "Conversation not found" }); if (conv.userId !== (req.user as User).id) return res.sendStatus(403); res.json(conv); } catch (e) { res.status(500).json({ error: "Failed to fetch conversation" }); } }); // Delete conversation app.delete("/api/ai/conversations/:id", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const conv = await storage.getConversation(req.params.id); if (!conv) return res.status(404).json({ error: "Conversation not found" }); if (conv.userId !== (req.user as User).id) return res.sendStatus(403); await storage.deleteConversation(req.params.id); await storage.createAuditLog({ userId: (req.user as User).id, action: "DELETE", entityType: "CONVERSATION", entityId: req.params.id, details: null, source: "USER" }); res.sendStatus(204); } catch (e) { res.status(500).json({ error: "Failed to delete conversation" }); } }); // Get messages for conversation app.get("/api/ai/conversations/:id/messages", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const conv = await storage.getConversation(req.params.id); if (!conv) return res.status(404).json({ error: "Conversation not found" }); if (conv.userId !== (req.user as User).id) return res.sendStatus(403); const messages = await storage.getMessages(req.params.id); res.json(messages); } catch (e) { res.status(500).json({ error: "Failed to fetch messages" }); } }); // Generate conversation title app.post("/api/ai/generate-title", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const user = req.user as User; if (!user.aiEnabled) return res.status(403).json({ error: "AI Assistant is disabled for this user" }); try { const { messages } = req.body; if (!messages || !Array.isArray(messages)) return res.status(400).json({ error: "Messages array is required" }); const title = await aiService.generateTitle(messages); res.json({ title }); } catch (e: any) { console.error("Generate Title Error:", e); res.status(500).json({ error: e.message }); } }); // Send message (Chat) app.post("/api/ai/chat", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const user = req.user as User; if (!user.aiEnabled) return res.status(403).json({ error: "AI Assistant is disabled for this user" }); try { const { conversationId, content, clientTime } = req.body; if (!conversationId || !content) return res.status(400).json({ error: "Missing conversationId or content" }); // Verify ownership const conv = await storage.getConversation(conversationId); if (!conv) return res.status(404).json({ error: "Conversation not found" }); if (conv.userId !== user.id) return res.sendStatus(403); // Store User Message const userMessage = await storage.addMessage({ conversationId, role: 'user', content }); // Send immediate response to client (Session Aware: request completes) res.json(userMessage); // Background Processing (Fire & Forget) (async () => { try { // Fetch history for context const dbMessages = await storage.getMessages(conversationId); // Convert to format expected by AiService (role, content) const history = dbMessages.map(m => ({ role: m.role as 'user' | 'assistant' | 'system', content: m.content })); // Build User Context (Tasks etc) const tasks = await storage.getTasksForUser(user.id); const activeTasks = tasks.filter(t => t.status !== 'done'); const completedTasks = tasks.filter(t => t.status === 'done'); const currentClientTime = clientTime || new Date().toLocaleString(); const context = ` User Context: - User ID: ${user.id} - Username: ${user.username} - XP: ${user.xp} (Level ${user.level}) - Current Date/Time: ${currentClientTime} Task Summary: - Total Active Tasks: ${activeTasks.length} - Total Completed Tasks: ${completedTasks.length} High Priority Active Tasks: ${activeTasks.filter(t => t.priority === 'high').map(t => `- ${t.title} (Due: ${t.dueDate})`).join('\n') || 'None'} Recent Active Tasks: ${activeTasks.slice(0, 5).map(t => `- [${t.priority}] ${t.title}`).join('\n')} `; // Call AI Service const responseContent = await aiService.chat(history, user, context); // Store AI Response await storage.addMessage({ conversationId, role: 'assistant', content: responseContent }); } catch (bgError) { console.error("Background AI Processing Error:", bgError); // Optional: Add a system message saying it failed? } })(); } catch (e: any) { console.error("AI Route Error:", e); res.status(500).json({ error: e.message || "Failed to generate AI response" }); } }); // Analyze completed task for follow-up app.post("/api/ai/analyze-completion", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const user = req.user as User; if (!user.aiEnabled) return res.json({ needed: false }); // Silently fail/return false try { const { taskId } = req.body; const task = await storage.getTask(taskId); if (!task) return res.status(404).json({ error: "Task not found" }); // Build context const context = ` User Context: - User ID: ${user.id} - Username: ${user.username} - Current Date/Time: ${new Date().toLocaleString()} `; const analysis = await aiService.analyzeTaskCompletion(task, context); res.json(analysis); } catch (e: any) { console.error("Analysis Error:", e); res.status(500).json({ error: "Failed to analyze task" }); } }); // Schedule a task using AI app.post("/api/ai/schedule", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const user = req.user as User; if (!user.aiEnabled) return res.status(403).json({ error: "AI Assistant is disabled for this user" }); try { const { taskId } = req.body; if (!taskId) return res.status(400).json({ error: "Task ID is required" }); const result = await aiService.scheduleTask(taskId, user.id); if (result.success && result.scheduledDate) { // Log it await storage.createAuditLog({ userId: user.id, action: "UPDATE", entityType: "TASK", entityId: taskId, details: { action: "AUTO_SCHEDULE", date: result.scheduledDate }, source: "AI" }); } res.json(result); } catch (e: any) { console.error("Scheduling Error:", e); res.status(500).json({ error: e.message || "Failed to schedule task" }); } }); // Edit message and regenerate (Regenerate Response) app.put("/api/ai/chat/:messageId", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const user = req.user as User; if (!user.aiEnabled) return res.status(403).json({ error: "AI Assistant is disabled for this user" }); try { const { messageId } = req.params; const { content, clientTime } = req.body; if (!content) return res.status(400).json({ error: "Content is required" }); // Verify message and ownership const message = await storage.getMessage(messageId); if (!message) return res.status(404).json({ error: "Message not found" }); const conv = await storage.getConversation(message.conversationId); if (!conv || conv.userId !== user.id) return res.sendStatus(403); if (message.role !== 'user') return res.status(400).json({ error: "Can only edit user messages" }); // 1. Update the message content const updatedMessage = await storage.updateMessage(messageId, content); // 2. Delete all subsequent messages (history truncation) await storage.deleteMessagesAfter(message.conversationId, message.createdAt as Date, message.id); // 3. Prepare context for regeneration const dbMessages = await storage.getMessages(message.conversationId); const history = dbMessages.map(m => ({ role: m.role as 'user' | 'assistant' | 'system', content: m.content })); const tasks = await storage.searchTasks("", user.id); // Get all tasks const activeTasks = tasks.filter(t => t.status !== 'done'); const completedTasks = tasks.filter(t => t.status === 'done'); // Fetch all labels to map IDs to names const labels = await storage.getAllLabels(); const labelMap = new Map(labels.map(l => [l.id, l.name])); const activeTasksWithLabels = activeTasks.map(t => ({ ...t, label: t.labelId ? labelMap.get(t.labelId) || "No Label" : "No Label" })); const context = ` User Context: - User ID: ${user.id} - Username: ${user.username} - XP: ${user.xp} (Level ${user.level}) - Current Date/Time: ${clientTime || new Date().toLocaleString()} Task Summary: - Total Active Tasks: ${activeTasks.length} - Total Completed Tasks: ${completedTasks.length} High Priority Active Tasks: ${activeTasksWithLabels.filter(t => t.priority === 'high').map(t => `- [${t.label}] ${t.title} (Due: ${t.dueDate})`).join('\n') || 'None'} Recent Active Tasks: ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t.title}`).join('\n')} `; // 4. Call AI Service (Regenerate) const responseContent = await aiService.chat(history, user, context); // 5. Store AI Response const botMessage = await storage.addMessage({ conversationId: message.conversationId, role: 'assistant', content: responseContent }); res.json(botMessage); } catch (e: any) { console.error("AI Edit Error:", e); res.status(500).json({ error: e.message || "Failed to regenerate AI response" }); } }); // --- API Key Management Routes --- // Get current API key (masked) app.get("/api/user/api-key", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const user = req.user as User; if (user.apiKey) { // Show first 8 and last 4 chars const masked = user.apiKey.substring(0, 8) + "..." + user.apiKey.substring(user.apiKey.length - 4); res.json({ hasKey: true, maskedKey: masked }); } else { res.json({ hasKey: false, maskedKey: null }); } }); // Generate new API key app.post("/api/user/api-key", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const user = req.user as User; const crypto = await import("crypto"); const newKey = "tf_" + crypto.randomBytes(32).toString("hex"); await storage.updateUserApiKey(user.id, newKey); await storage.createAuditLog({ userId: user.id, action: "CREATE", entityType: "API_KEY", entityId: user.id, details: { message: "API key generated" }, source: "USER" }); // Return full key ONCE (user must save it) res.json({ apiKey: newKey, message: "Save this key - it won't be shown again in full." }); }); // Revoke API key app.delete("/api/user/api-key", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const user = req.user as User; await storage.updateUserApiKey(user.id, null); await storage.createAuditLog({ userId: user.id, action: "DELETE", entityType: "API_KEY", entityId: user.id, details: { message: "API key revoked" }, source: "USER" }); res.json({ message: "API key revoked" }); }); // Health check endpoint app.get("/api/health", (req, res) => { res.status(200).json({ status: "ok", timestamp: new Date().toISOString() }); }); // Labels API routes app.get("/api/labels", async (req, res) => { try { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; const allLabels = await storage.getAllLabels(); const sharedLabels = await storage.getSharedLabels(userId); const sharedLabelIds = new Set(sharedLabels.map(sl => sl.labelId)); // Filter: Created by me OR Shared with me // If creatorId is null (legacy/default), everyone sees it? Or system public? // Assumption: labels with creatorId=null are "System Defaults" visible to all. // Or we should update getAllLabels to filter in DB. const visibleLabels = allLabels.filter(l => l.creatorId === userId || sharedLabelIds.has(l.id) || l.creatorId === null ); res.json(visibleLabels); } catch (error) { res.status(500).json({ error: "Failed to fetch labels" }); } }); 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) { return res.status(404).json({ error: "Label not found" }); } res.json(label); } catch (error) { res.status(500).json({ error: "Failed to fetch label" }); } }); app.delete("/api/tasks/:id", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const { hasAccess, task } = await checkTaskAccess(req.user as User, req.params.id, 'write'); if (!task) return res.status(404).json({ error: "Task not found" }); // DELETE usually requires ownership or explicit 'write' permission. // Shared read-only users should NOT be able to delete. // checkTaskAccess('write') should cover this if we implement strict permissions in sharedTasks later. // For now, let's assume if they have 'write', they can delete (or we restrict delete to Owner). // Let's restrict DELETE to Owner for safety unless specifically allowed. if (task.userId !== (req.user as User).id) { return res.status(403).json({ error: "Only the owner can delete a task" }); } await storage.deleteTask(req.params.id); res.sendStatus(204); } catch (error) { res.status(500).json({ error: "Failed to delete task" }); } }); app.post("/api/labels", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const result = insertLabelSchema.safeParse(req.body); if (!result.success) { return res.status(400).json({ error: "Invalid label data", details: result.error }); } const label = await storage.createLabel({ ...result.data, creatorId: (req.user as User).id // Assign creator }); await storage.createAuditLog({ userId: (req.user as User).id, action: "CREATE", entityType: "LABEL", entityId: label.id, details: { name: label.name }, source: "USER" }); res.status(201).json(label); } catch (error) { console.error("Create Label Error:", error); res.status(500).json({ error: "Failed to create label" }); } }); 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) { return res.status(400).json({ error: "Invalid label data", details: updates.error }); } const label = await storage.updateLabel(req.params.id, updates.data); if (!label) { return res.status(404).json({ error: "Label not found" }); } await storage.createAuditLog({ userId: (req.user as User).id, action: "UPDATE", entityType: "LABEL", entityId: label.id, details: updates.data, source: "USER" }); res.json(label); } catch (error) { res.status(500).json({ error: "Failed to update label" }); } }); 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" }); } // BUG FIX: removed duplicate deleteLabel() call (DUP-2) await storage.createAuditLog({ userId: (req.user as User).id, action: "DELETE", entityType: "LABEL", entityId: req.params.id, details: null, source: "USER" }); res.status(204).send(); } catch (error) { res.status(500).json({ error: "Failed to delete label" }); } }); // Shared Label Routes app.get("/api/labels/:id/share", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const label = await storage.getLabel(req.params.id); if (!label) return res.status(404).json({ error: "Label not found" }); // Only creator or admin or write permission can view shares? // Actually creator, or anyone with 'write'/'admin' permission on the label? // For simplicity: Only creator can manage shares. if (label.creatorId && label.creatorId !== (req.user as User).id) { return res.status(403).json({ error: "Only the label owner can manage shares" }); } // const shares = await storage.getLabelShares(req.params.id); // Not needed as we fetch below // We need user details for the frontend const users = await storage.getLabelSharedUsers(req.params.id); const sharesWithDetails = await Promise.all(users.map(async u => { const shareInfos = await storage.getLabelShares(req.params.id); const specificShare = shareInfos.find(s => s.sharedWithUserId === u.id); return { userId: u.id, username: u.username, permission: specificShare?.permission || 'read' }; })); res.json(sharesWithDetails); } catch (e) { res.status(500).json({ error: "Failed to fetch label shares" }); } }); app.post("/api/labels/:id/share", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const { username, permission } = req.body; if (!username) return res.status(400).json({ error: "Username required" }); const label = await storage.getLabel(req.params.id); if (!label) return res.status(404).json({ error: "Label not found" }); if (label.creatorId && label.creatorId !== (req.user as User).id) { return res.status(403).json({ error: "Only owner can share label" }); } const targetUser = await storage.getUserByUsername(username); if (!targetUser) return res.status(404).json({ error: "User not found" }); if (targetUser.id === (req.user as User).id) return res.status(400).json({ error: "Cannot share with yourself" }); const share = await storage.shareLabel(label.id, targetUser.id, (req.user as User).id, permission || 'read'); res.json(share); } catch (e) { res.status(500).json({ error: "Failed to share label" }); } }); app.delete("/api/labels/:id/share/:userId", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const label = await storage.getLabel(req.params.id); if (!label) return res.status(404).json({ error: "Label not found" }); if (label.creatorId && label.creatorId !== (req.user as User).id) { // Also allow user to unshare themselves? if (req.params.userId !== (req.user as User).id) { return res.status(403).json({ error: "Only owner can remove other collaborators" }); } } await storage.unshareLabel(label.id, req.params.userId); res.sendStatus(204); } catch (e) { res.status(500).json({ error: "Failed to remove share" }); } }); // Tasks API routes (updated GET) app.get("/api/tasks", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const tasks = await storage.getTasksForUser((req.user as User).id); res.json(tasks); } catch (error) { res.status(500).json({ error: "Failed to fetch tasks" }); } }); // Search tasks - MUST be before /api/tasks/:id to avoid route conflict app.get("/api/tasks/search", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const query = (req.query.q as string) || ""; const tasks = await storage.searchTasks(query, (req.user as User).id); res.json(tasks); } catch (error) { res.status(500).json({ error: "Failed to search tasks" }); } }); // Helper for RBAC const checkTaskAccess = async (user: User, taskId: string, requiredPermission: 'read' | 'write' = 'read'): Promise<{ hasAccess: boolean, task?: Task }> => { const task = await storage.getTask(taskId); if (!task) return { hasAccess: false }; // 1. Ownership if (task.userId === user.id) return { hasAccess: true, task }; // 2. Shared Task (Direct) // We need a storage method for this efficiently, but for now we might need to query // Since storage interface is generic, let's assume valid access if we can find a record // Optimization: Add storage.hasTaskAccess(userId, taskId)? // For now, let's fallback to checking if the task is in the user's "visible" list or simple logic // Implementation Plan Step: "Shared Access (query sharedTasks table)" // We'll trust the current `storage.getTask` usually returns raw task. // But we need to verify IF the user is allowed. // Check Shared Tasks const shared = await storage.getSharedTask(taskId, user.id); if (shared) { // Shared tasks currently imply 'read'. If we need 'write', we might need more fields. // For now, let's assume shared = read/write or just read. // The schema `sharedTasks` doesn't have permissions, so full access? // Start with READ access for shared. WRITE might need schema update. // Let's assume shared tasks are R/W for now for simplicity unless specified. return { hasAccess: true, task }; } // 3. Global Access (UserTaskAccess) // Check if user has access to the owner's tasks if (!task.userId) return { hasAccess: false, task }; // Should not happen for user tasks const hasGlobalAccess = await storage.checkUserTaskAccess(task.userId, user.id); if (hasGlobalAccess) return { hasAccess: true, task }; // "Share All" return { hasAccess: false, task }; // Task exists but no access }; app.get("/api/tasks/:id", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const { hasAccess, task } = await checkTaskAccess(req.user as User, req.params.id, 'read'); if (!task) return res.status(404).json({ error: "Task not found" }); if (!hasAccess) return res.status(403).json({ error: "Access denied" }); res.json(task); } catch (error) { res.status(500).json({ error: "Failed to fetch task" }); } }); app.post("/api/tasks/:id/schedule", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const taskId = req.params.id; const task = await storage.getTask(taskId); if (!task) return res.status(404).json({ error: "Task not found" }); if (task.userId !== (req.user as User).id) return res.sendStatus(403); const result = await aiService.scheduleTask(taskId, (req.user as User).id); if (result.success) { res.json(result); // Award XP for using AI scheduling if (req.user) { await gamificationService.awardXP((req.user as User).id, 'ai_action'); } await storage.createAuditLog({ userId: (req.user as User).id, action: "UPDATE", entityType: "TASK", entityId: taskId, details: { action: "AI_SCHEDULE" }, source: "AI" }); } else { res.status(400).json(result); } } catch (e: any) { console.error("Schedule Task Error:", e); res.status(500).json({ error: e.message || "Failed to schedule task" }); } }); app.post("/api/tasks", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const result = insertTaskSchema.safeParse(req.body); if (!result.success) { return res.status(400).json({ error: "Invalid task data", details: result.error }); } const task = await storage.createTask({ ...result.data, userId: (req.user as User).id }); // Award XP for creating a task if (req.user) { const source = req.body.parentTaskId ? 'create_subtask' : 'create_task'; await gamificationService.awardXP((req.user as User).id, source, undefined, { taskId: task.id, taskTitle: task.title }); } await storage.createAuditLog({ userId: (req.user as User).id, action: "CREATE", entityType: "TASK", entityId: task.id, details: { title: task.title }, source: "USER" }); res.status(201).json(task); } catch (error) { res.status(500).json({ error: "Failed to create task" }); } }); app.patch("/api/tasks/:id", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { // 1. Check Access (Security Fix + Fetch) // We use checkTaskAccess to ensure the user has write permissions (ownership or shared write access) const { hasAccess, task: previousTask } = await checkTaskAccess(req.user as User, req.params.id, 'write'); if (!previousTask) return res.status(404).json({ error: "Task not found" }); if (!hasAccess) return res.status(403).json({ error: "Access denied" }); const updates = insertTaskSchema.partial().safeParse(req.body); if (!updates.success) { return res.status(400).json({ error: "Invalid task data", details: updates.error }); } // Award XP using GamificationService if (req.user) { if (updates.data.status === 'done' && previousTask.status !== 'done') { const isLate = previousTask.dueDate && new Date(previousTask.dueDate) < new Date(); const source = isLate ? 'complete_task_late' : 'complete_task'; await gamificationService.awardXP((req.user as User).id, source, undefined, { taskId: previousTask.id, taskTitle: previousTask.title }); } else if (Object.keys(updates.data).length > 0) { // Small points for any other update (title, description, etc) await gamificationService.awardXP((req.user as User).id, 'update_task', undefined, { taskId: previousTask.id, taskTitle: previousTask.title }); } } const updatedTask = await storage.updateTask(req.params.id, updates.data); if (!updatedTask) { return res.status(404).json({ error: "Task not found" }); } // 2. Time Tracking Logic // If timeTracked has increased, log the delta if (updates.data.timeTracked && updatedTask.timeTracked > previousTask.timeTracked) { const delta = updatedTask.timeTracked - previousTask.timeTracked; await storage.logTaskTime({ taskId: updatedTask.id, userId: (req.user as User).id, timeSpent: delta }); } await storage.createAuditLog({ userId: (req.user as User).id, action: "UPDATE", entityType: "TASK", entityId: updatedTask.id, details: updates.data, source: "USER" }); res.json(updatedTask); } catch (error: any) { console.error("PATCH Task Error:", error); res.status(500).json({ error: "Failed to update task", details: String(error) }); } }); // Time Analytics Endpoint app.get("/api/analytics/time-distribution", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const period = req.query.period as 'day' | 'week' | 'month' | 'year'; if (!['day', 'week', 'month', 'year'].includes(period)) { return res.status(400).json({ error: "Invalid period. Must be day, week, month, or year." }); } try { const distribution = await storage.getAnalyticsTimeDistribution((req.user as User).id, period); res.json(distribution); } catch (error) { console.error("Analytics Error:", error); res.status(500).json({ error: "Failed to fetch time distribution" }); } }); // 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 { res.json([]); // Placeholder until storage implementation } catch (error) { res.status(500).json({ error: "Failed to fetch notes" }); } }); 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 }); } res.status(201).json({ ...result.data, id: "placeholder" }); } catch (error) { res.status(500).json({ error: "Failed to create note" }); } }); // Goals API app.get("/api/goals", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const goals = await storage.getGoals(); res.json(goals); } catch (error) { res.status(500).json({ error: "Failed to fetch goals" }); } }); app.post("/api/goals", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const result = insertGoalSchema.safeParse(req.body); if (!result.success) { return res.status(400).json(result.error); } const goal = await storage.createGoal(result.data); console.log("Goal created:", goal); await storage.createAuditLog({ userId: (req.user as User).id || null, action: "CREATE", entityType: "GOAL", entityId: goal.id, details: { title: goal.title }, source: "USER" }); res.json(goal); } catch (error) { console.error("Error in POST /api/goals:", error); res.status(500).json({ error: "Failed to create goal" }); } }); // Analytics API app.get("/api/analytics/weekly", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const data = await gamificationService.getWeeklyAnalytics((req.user as User).id); res.json(data); } catch (e) { res.status(500).json({ error: "Failed to fetch analytics" }); } }); app.get("/api/analytics/yearly", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const data = await gamificationService.getYearlyAnalytics((req.user as User).id); res.json(data); } catch (e) { res.status(500).json({ error: "Failed to fetch analytics" }); } }); app.get("/api/analytics/monthly", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const data = await gamificationService.getMonthlyAnalytics((req.user as User).id); res.json(data); } catch (e) { res.status(500).json({ error: "Failed to fetch analytics" }); } }); // 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; // Filter rewards: System rewards OR User's own rewards const visibleRewards = allRewards.filter(r => r.isSystem || (userId && r.userId === userId)); // If userId is provided, check ownership of visible rewards if (userId) { const userRewards = await storage.getUserRewards(userId); const ownedIds = new Set(userRewards.map(ur => ur.rewardId)); return res.json(visibleRewards.map(r => ({ ...r, owned: ownedIds.has(r.id) }))); } res.json(visibleRewards); } catch (error) { res.status(500).json({ error: "Failed to fetch rewards" }); } }); app.post("/api/rewards/purchase", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { // 1. Get User & Reward const userId = (req.user as User).id; const { rewardId } = req.body; if (!rewardId) return res.status(400).json({ error: "Missing rewardId" }); const user = await storage.getUser(userId); // Fetch user here const allRewards = await storage.getAllRewards(); const reward = allRewards.find(r => r.id === rewardId); if (!user || !reward) return res.status(404).json({ error: "User or Reward not found" }); // 2. Check Ownership (if one-time) // For now, allow multiple purchases unless type is 'feature_unlock' if (reward.type === 'feature_unlock') { const userRewards = await storage.getUserRewards(userId); if (userRewards.some(ur => ur.rewardId === rewardId)) { return res.status(400).json({ error: "Reward already owned" }); } } // 3. Check Funds if (user.xp < reward.cost) { return res.status(400).json({ error: "Insufficient XP" }); } // 4. Transaction const updatedUser = await storage.updateUserXP(userId, -reward.cost); await storage.createUserReward({ userId, rewardId }); await storage.createAuditLog({ userId, action: "PURCHASE", entityType: "REWARD", entityId: rewardId.toString(), source: "USER", details: { cost: reward.cost, rewardName: reward.title } }); res.json({ success: true, user: updatedUser }); } catch (e) { res.status(500).json({ error: "Purchase failed" }); } }); app.post("/api/rewards", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const rewardData = { ...req.body, userId: (req.user as User).id, isSystem: (req.user as User).role === 'admin' && req.body.isSystem !== false, }; const reward = await storage.createReward(rewardData); await storage.createAuditLog({ userId: (req.user as User).id, action: "CREATE", entityType: "REWARD", entityId: reward.id.toString(), source: "ADMIN", details: { title: reward.title, cost: reward.cost } }); res.json(reward); } catch (err) { res.status(500).json({ error: "Failed to create reward" }); } }); // User Gamification Endpoints app.get("/api/user/history", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const history = await storage.getXpEvents((req.user as User).id); res.json(history); } catch (e) { res.status(500).json({ error: "Failed to fetch history" }); } }); app.get("/api/user/inventory", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const userRewards = await storage.getUserRewards((req.user as User).id); // Join with rewards details const allRewards = await storage.getAllRewards(); const inventory = userRewards.map(ur => { const reward = allRewards.find(r => r.id === ur.rewardId); return { ...ur, reward // Nested details }; }).filter(item => item.reward); // Filter out any broken links res.json(inventory); } catch (e) { res.status(500).json({ error: "Failed to fetch inventory" }); } }); // --- Social & Leaderboard Routes --- app.get("/api/leaderboard", async (req, res) => { try { const users = await storage.getLeaderboard(); // Return public info only const leaderboard = users.map(u => ({ username: u.username, xp: u.xp, level: u.level, id: u.id // Needed? Maybe for linking profile })); res.json(leaderboard); } catch (e) { res.status(500).json({ error: "Failed to fetch leaderboard" }); } }); app.patch("/api/user/privacy", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const { showOnLeaderboard, isSearchable, aiEnabled, is2faEnabled, language } = req.body; const updates: any = {}; if (showOnLeaderboard !== undefined) updates.showOnLeaderboard = showOnLeaderboard; if (isSearchable !== undefined) updates.isSearchable = isSearchable; if (aiEnabled !== undefined) updates.aiEnabled = aiEnabled; if (is2faEnabled !== undefined) updates.is2faEnabled = is2faEnabled; if (language !== undefined) updates.language = language; const updated = await storage.updateUser((req.user as User).id, updates); await storage.createAuditLog({ userId: (req.user as User).id, action: "UPDATE", entityType: "USER_PRIVACY", entityId: (req.user as User).id.toString(), source: "USER", details: updates }); res.json(updated); } catch (e) { res.status(500).json({ error: "Failed to update privacy settings" }); } }); app.patch("/api/user/profile", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const { email } = req.body; if (!email || !email.includes('@')) return res.status(400).json({ error: "Invalid email" }); const existing = await storage.getUserByEmail(email); if (existing && existing.id !== (req.user as User).id) { return res.status(400).json({ error: "Email already taken" }); } const updated = await storage.updateUser((req.user as User).id, { email }); await storage.createAuditLog({ userId: (req.user as User).id, action: "UPDATE", entityType: "USER_PROFILE", entityId: (req.user as User).id.toString(), source: "USER", details: { change: "email" } }); res.json(updated); } catch (e) { res.status(500).json({ error: "Failed to update profile" }); } }); app.patch("/api/user/password", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const { currentPassword, newPassword } = req.body; if (!currentPassword || !newPassword) return res.status(400).json({ error: "Missing fields" }); const user = await storage.getUser((req.user as User).id); if (!user) return res.status(404).json({ error: "User not found" }); const isValid = await comparePassword(currentPassword, user.password); if (!isValid) return res.status(400).json({ error: "Incorrect current password" }); const hashedPassword = await hashPassword(newPassword); await storage.updateUser(user.id, { password: hashedPassword }); await storage.createAuditLog({ userId: user.id, action: "UPDATE", entityType: "USER_PASSWORD", entityId: user.id.toString(), source: "USER", details: {} }); res.json({ message: "Password updated" }); } catch (e) { res.status(500).json({ error: "Failed to update password" }); } }); app.get("/api/users/search", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const query = req.query.q as string; try { const users = await storage.searchUsers(query); // Filter out self const others = users.filter(u => u.id !== (req.user as User).id); res.json(others.map(u => ({ id: u.id, username: u.username }))); } catch (e) { res.status(500).json({ error: "Search failed" }); } }); app.post("/api/tasks/:id/share", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const { targetUserId } = req.body; const taskId = req.params.id; // Verify ownership const task = await storage.getTask(taskId); if (!task) return res.status(404).json({ error: "Task not found" }); if (task.userId !== (req.user as User).id) { return res.status(403).json({ error: "Unauthorized" }); } await storage.shareTask({ taskId, sharedByUserId: (req.user as User).id, sharedWithUserId: targetUserId }); await storage.createAuditLog({ userId: (req.user as User).id, action: "SHARE", entityType: "TASK", entityId: taskId, source: "USER", details: { sharedWith: targetUserId } }); res.json({ success: true }); } catch (e) { res.status(500).json({ error: "Failed to share task" }); } }); app.get("/api/tasks/:id/shared-users", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const taskId = req.params.id; // Verify ownership or access? Ideally only owner can see who else sees it. const task = await storage.getTask(taskId); if (!task) return res.status(404).json({ error: "Task not found" }); if (task.userId !== (req.user as User).id) { return res.status(403).json({ error: "Unauthorized" }); } const users = await storage.getTaskSharedUsers(taskId); // Return minimal info res.json(users.map(u => ({ id: u.id, username: u.username }))); } catch (e) { res.status(500).json({ error: "Failed to fetch shared users" }); } }); app.delete("/api/tasks/:id/share/:userId", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const taskId = req.params.id; const targetUserId = req.params.userId; // Verify ownership const task = await storage.getTask(taskId); if (!task) return res.status(404).json({ error: "Task not found" }); if (task.userId !== (req.user as User).id) { return res.status(403).json({ error: "Unauthorized" }); } const success = await storage.unshareTask(taskId, targetUserId); if (success) { await storage.createAuditLog({ userId: (req.user as User).id, action: "UNSHARE", entityType: "TASK", entityId: taskId, source: "USER", details: { unsharedWith: targetUserId } }); res.json({ success: true }); } else { res.status(404).json({ error: "Share not found" }); } } catch (e) { res.status(500).json({ error: "Failed to unshare task" }); } }); app.post("/api/users/share-all", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const { targetUserId } = req.body; await storage.shareAllTasks({ ownerId: (req.user as User).id, viewerId: targetUserId }); await storage.createAuditLog({ userId: (req.user as User).id, action: "SHARE", entityType: "ALL_TASKS", entityId: "0", source: "USER", details: { sharedWith: targetUserId } }); res.json({ success: true }); } catch (e) { res.status(500).json({ error: "Failed to share all tasks" }); } }); const httpServer = createServer(app); // Storage needs to support Goal Update // Storage needs to support Goal Update app.patch("/api/goals/:id", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { // 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({ userId: (req.user as User).id, action: "UPDATE", entityType: "GOAL", entityId: updated.id.toString(), source: "USER", details: req.body }); } } catch (e) { res.status(500).json({ error: "Failed to update goal" }); } }); // Routine Completion Endpoint app.post("/api/user/routine/:type/complete", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const user = req.user as User; const type = req.params.type; try { if (type === 'morning') { await storage.updateUser(user.id, { lastMorningRoutine: new Date() }); await gamificationService.awardXP(user.id, 'morning_routine', 20); // Bonus } else if (type === 'evening') { await storage.updateUser(user.id, { lastEveningRoutine: new Date() }); await gamificationService.awardXP(user.id, 'evening_routine', 20); // Bonus } else { return res.status(400).json({ error: "Invalid routine type" }); } await storage.createAuditLog({ userId: user.id, action: "COMPLETE", entityType: "ROUTINE", entityId: type, source: "USER", details: { type } }); res.json({ success: true }); } catch (e) { res.status(500).json({ error: "Failed to complete routine" }); } }); // Export User Data app.post("/api/user/data-export", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const user = req.user as User; const { includeTasks, includeLabels, includeSettings } = req.body; const exportData: any = { exportedAt: new Date(), user: { username: user.username, email: user.email } }; if (includeTasks) { exportData.tasks = await storage.getTasksForUser(user.id); } if (includeLabels) { exportData.labels = await storage.getLabels(user.id); } if (includeSettings && user.role === 'admin') { // Only admins can request system settings dump, though arguably they might mean "User Settings" // The prompt said "system settings". I will include system settings if they are admin. // But maybe they meant user preferences? The prompt specifically said "system settings". const allSettings: Record = {}; const keys = [ "registration_enabled", "smtp_host", "smtp_port", "smtp_user", "smtp_from", "ai_provider", "ai_model", "ai_base_url", "ai_system_prompt", "mcp_enabled" ]; for (const key of keys) { const val = await storage.getSystemSettings(key); if (val) allSettings[key] = val; } exportData.systemSettings = allSettings; } await storage.createAuditLog({ userId: user.id, action: "EXPORT_DATA", entityType: "USER_DATA", entityId: user.id.toString(), source: "USER", details: { includeTasks, includeLabels, includeSettings } }); res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Disposition', `attachment; filename="taskflow_export_${user.username}.json"`); res.json(exportData); } catch (e) { console.error("Export Failed:", e); res.status(500).json({ error: "Failed to export data" }); } }); // ============================================ // ADHD MODE ROUTES // ============================================ // Update ADHD settings app.patch("/api/user/adhd-settings", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { const { adhdMode, adhdSettings } = req.body; const updated = await storage.updateUser(userId, { adhdMode, adhdSettings, }); await storage.createAuditLog({ userId, action: "UPDATE", entityType: "USER", entityId: userId, details: { action: "UPDATE_ADHD_SETTINGS", adhdMode }, source: "USER" }); res.json(updated); } catch (e) { console.error("Update ADHD settings failed:", e); res.status(500).json({ error: "Failed to update ADHD settings" }); } }); // Log a break app.post("/api/user/log-break", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { const { breakType, durationMinutes } = req.body; // Log the break await storage.createBreakLog({ userId, breakType, durationMinutes: durationMinutes || 5, }); // Update user's lastBreakAt await storage.updateUser(userId, { lastBreakAt: new Date(), }); // Award XP for taking a break (micro-XP) // BUG FIX: Parameters were swapped (TYPE-2) - source and amount await gamificationService.awardXP(userId, 'break_taken', 10, { breakType }); res.json({ success: true }); } catch (e) { console.error("Log break failed:", e); res.status(500).json({ error: "Failed to log break" }); } }); // Get break stats app.get("/api/user/break-stats", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { const stats = await storage.getBreakStats(userId); res.json(stats); } catch (e) { res.status(500).json({ error: "Failed to get break stats" }); } }); // Log energy level app.post("/api/energy/log", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { const { energyLevel, notes } = req.body; await storage.createEnergyLog({ userId, energyLevel, notes, }); // Update user's current energy level await storage.updateUser(userId, { currentEnergyLevel: energyLevel, todayEnergyCheckedIn: true, }); // Award XP for checking in // BUG FIX: Parameters were swapped (TYPE-2) await gamificationService.awardXP(userId, 'energy_checkin', 5, { energyLevel }); res.json({ success: true }); } catch (e) { console.error("Log energy failed:", e); res.status(500).json({ error: "Failed to log energy" }); } }); // Get energy history app.get("/api/energy/history", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { const history = await storage.getEnergyHistory(userId); res.json(history); } catch (e) { res.status(500).json({ error: "Failed to get energy history" }); } }); // Get energy-based task suggestions app.get("/api/tasks/energy-based", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { const user = req.user as User; const tasks = await storage.getTasksForUser(userId); const energyLevel = user.currentEnergyLevel || 'medium'; // Filter tasks based on energy level let recommended = tasks.filter(t => t.status !== 'done'); if (energyLevel === 'low') { // Prefer low-energy tasks for low energy recommended = recommended.filter(t => t.energyLevel === 'low' || (t.estimatedDuration && t.estimatedDuration <= 15) || t.priority === 'low' ); } else if (energyLevel === 'high') { // Prefer high-energy/challenging tasks recommended = recommended.filter(t => t.energyLevel === 'high' || t.priority === 'high' || (t.estimatedDuration && t.estimatedDuration > 30) ); } res.json(recommended.slice(0, 10)); } catch (e) { res.status(500).json({ error: "Failed to get energy-based tasks" }); } }); // Get quick wins app.get("/api/tasks/quick-wins", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { const user = req.user as User; const tasks = await storage.getTasksForUser(userId); const threshold = (user.adhdSettings as any)?.quickWinThreshold || 10; const quickWins = tasks .filter(t => { if (t.status === 'done') return false; if (t.estimatedDuration && t.estimatedDuration > threshold * 1.5) return false; return true; }) .slice(0, 10); res.json(quickWins); } catch (e) { res.status(500).json({ error: "Failed to get quick wins" }); } }); // AI Task Breakdown app.post("/api/ai/breakdown-task", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { const { taskId, title, description, estimatedDuration } = req.body; const breakdown = await aiService.breakdownTask({ title, description, estimatedDuration, }); await storage.createAuditLog({ userId, action: "AI_BREAKDOWN", entityType: "TASK", entityId: taskId, details: { title, subtaskCount: breakdown.subtasks?.length }, source: "AI" }); res.json(breakdown); } catch (e) { console.error("AI breakdown failed:", e); res.status(500).json({ error: "Failed to break down task" }); } }); // ============================================ // BODY DOUBLING SESSION ROUTES // ============================================ // Get all sessions app.get("/api/sessions", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { const sessions = await storage.getBodyDoublingSessions(userId); res.json(sessions); } catch (e) { res.status(500).json({ error: "Failed to get sessions" }); } }); // Create session app.post("/api/sessions", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { const session = await storage.createBodyDoublingSession({ hostId: userId, ...req.body, }); // Auto-join as participant await storage.joinSession(session.id, userId); res.json(session); } catch (e) { console.error("Create session failed:", e); res.status(500).json({ error: "Failed to create session" }); } }); // Join session app.post("/api/sessions/:id/join", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; const sessionId = req.params.id; try { await storage.joinSession(sessionId, userId); res.json({ success: true }); } catch (e) { console.error("Join session failed:", e); res.status(500).json({ error: "Failed to join session" }); } }); // Leave session app.post("/api/sessions/:id/leave", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; const sessionId = req.params.id; try { await storage.leaveSession(sessionId, userId); res.json({ success: true }); } catch (e) { console.error("Leave session failed:", e); res.status(500).json({ error: "Failed to leave session" }); } }); // ============================================ // FOCUS SESSION ROUTES // ============================================ // Start focus session app.post("/api/focus-sessions", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { const session = await storage.createFocusSession({ userId, ...req.body, }); res.json(session); } catch (e) { res.status(500).json({ error: "Failed to start focus session" }); } }); // Complete focus session app.patch("/api/focus-sessions/:id/complete", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { const { actualMinutes, wasCompleted } = req.body; const session = await storage.completeFocusSession(req.params.id, actualMinutes, wasCompleted); // Award XP based on completion const xpAmount = wasCompleted ? 15 : 5; // BUG FIX: Parameters were swapped (TYPE-2) await gamificationService.awardXP(userId, 'focus_session', xpAmount, { sessionType: session?.sessionType, actualMinutes, }); res.json(session); } catch (e) { res.status(500).json({ error: "Failed to complete focus session" }); } }); // ============================================ // DAILY CHALLENGES ROUTES // ============================================ // Get today's challenges app.get("/api/challenges/today", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { let challenges = await storage.getTodaysChallenges(userId); // Generate new challenges if none exist for today if (challenges.length === 0) { challenges = await storage.generateDailyChallenges(userId); } res.json(challenges); } catch (e) { res.status(500).json({ error: "Failed to get challenges" }); } }); // Update challenge progress app.post("/api/challenges/:id/progress", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { const { increment } = req.body; const challenge = await storage.updateChallengeProgress(req.params.id, increment || 1); // Award XP if completed if (challenge?.completedAt && challenge.xpReward) { await gamificationService.awardXP(userId, 'daily_challenge', challenge.xpReward, { challengeType: challenge.challengeType, }); } res.json(challenge); } catch (e) { res.status(500).json({ error: "Failed to update challenge progress" }); } }); // ============================================ // PUSH NOTIFICATION ROUTES // ============================================ // Get VAPID public key (needed by client to subscribe) app.get("/api/push/vapid-public-key", async (req, res) => { try { const publicKey = await getVapidPublicKey(); if (!publicKey) { return res.status(503).json({ error: "Push notifications not configured" }); } res.json({ publicKey }); } catch (e) { console.error("Get VAPID key failed:", e); res.status(500).json({ error: "Failed to get VAPID key" }); } }); // Subscribe to push notifications app.post("/api/push/subscribe", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { const { subscription } = req.body; if (!subscription || !subscription.endpoint || !subscription.keys) { return res.status(400).json({ error: "Invalid subscription data" }); } const userAgent = req.headers['user-agent']; const success = await savePushSubscription(userId, subscription, userAgent); if (success) { // Send a welcome notification to confirm it works await sendPushNotification(userId, { title: "Notifications Enabled", body: "You'll now receive task reminders even when the app is closed!", tag: "welcome" }); res.json({ success: true }); } else { res.status(500).json({ error: "Failed to save subscription" }); } } catch (e) { console.error("Subscribe failed:", e); res.status(500).json({ error: "Failed to subscribe" }); } }); // Unsubscribe from push notifications app.post("/api/push/unsubscribe", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); try { const { endpoint } = req.body; if (!endpoint) { return res.status(400).json({ error: "Endpoint required" }); } const success = await removePushSubscription(endpoint); res.json({ success }); } catch (e) { console.error("Unsubscribe failed:", e); res.status(500).json({ error: "Failed to unsubscribe" }); } }); // Unsubscribe all devices for current user app.delete("/api/push/subscriptions", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { const success = await removeUserSubscriptions(userId); res.json({ success }); } catch (e) { console.error("Remove subscriptions failed:", e); res.status(500).json({ error: "Failed to remove subscriptions" }); } }); // Handle subscription changes (from service worker) app.post("/api/push/resubscribe", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { const { oldEndpoint, newSubscription } = req.body; // Remove old subscription if (oldEndpoint) { await removePushSubscription(oldEndpoint); } // Save new subscription if (newSubscription) { const userAgent = req.headers['user-agent']; await savePushSubscription(userId, newSubscription, userAgent); } res.json({ success: true }); } catch (e) { console.error("Resubscribe failed:", e); res.status(500).json({ error: "Failed to resubscribe" }); } }); // Test notification (useful for debugging) app.post("/api/push/test", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); const userId = (req.user as User).id; try { const result = await sendPushNotification(userId, { title: "Test Notification", body: "Push notifications are working!", tag: "test" }); res.json(result); } catch (e) { console.error("Test notification failed:", e); res.status(500).json({ error: "Failed to send test notification" }); } }); // End of routes return httpServer; }