feat: enhance audit logging, add MCP settings, and production docker setup
continuous-integration/drone/push Build is passing

- Implemented comprehensive audit logging for Tasks, Users, Settings, Goals, Labels, AI Chat, and Rewards.
- Added Admin UI for MCP Server settings and Audit Logs.
- Created docker-compose-production.yml with Traefik configuration.
- Fixed backend bugs (missing storage methods, route closure).
- Added Audit Logging Guidelines.
This commit is contained in:
2025-12-15 15:53:31 +01:00
parent fdf321cde9
commit d1736c5991
35 changed files with 5165 additions and 499 deletions
+475 -153
View File
@@ -4,11 +4,13 @@ import { storage } from "./storage.js";
import { insertLabelSchema, insertTaskSchema, insertNoteSchema, insertXpEventSchema, insertGoalSchema, insertRewardSchema, rewards, userRewards, User } from "../shared/schema.js";
import { z } from "zod";
import { EmailService } from "./email.js";
import { mcpServer } from "./mcp";
import { AiService } from "./ai.js";
import { GamificationService } from "./gamification.js";
const emailService = new EmailService(storage);
const aiService = new AiService(storage);
const gamificationService = new GamificationService(storage);
import { setupAuth, hashPassword, comparePassword } from "./auth.js";
@@ -148,6 +150,16 @@ export async function registerRoutes(app: Express): Promise<Server> {
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" });
@@ -164,6 +176,16 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
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" });
@@ -180,18 +202,55 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
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" });
}
});
// --- 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"];
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") {
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
@@ -201,12 +260,27 @@ export async function registerRoutes(app: Express): Promise<Server> {
});
app.post("/api/admin/settings", isAdmin, async (req, res) => {
const keys = ["registration_enabled", "smtp_host", "smtp_port", "smtp_user", "smtp_pass", "smtp_from", "smtp_secure"];
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"
];
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 });
});
@@ -255,17 +329,197 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// --- AI Routes ---
app.post("/api/ai/chat", async (req, res) => {
// --- 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 (!Array.isArray(messages)) return res.status(400).json({ error: "Messages must be an array" });
if (!messages || !Array.isArray(messages)) return res.status(400).json({ error: "Messages array is required" });
// Build User Context
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
await storage.addMessage({
conversationId,
role: 'user',
content
});
// 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');
@@ -275,6 +529,7 @@ 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}
@@ -287,14 +542,100 @@ Recent Active Tasks:
${activeTasks.slice(0, 5).map(t => `- [${t.priority}] ${t.title}`).join('\n')}
`;
const response = await aiService.chat(messages, user, context);
res.json({ role: "assistant", content: response });
// Call AI Service
const responseContent = await aiService.chat(history, user, context);
// Store AI Response
const botMessage = await storage.addMessage({
conversationId,
role: 'assistant',
content: responseContent
});
res.json(botMessage);
} catch (e: any) {
console.error("AI Route Error:", e);
res.status(500).json({ error: e.message || "Failed to generate AI response" });
}
});
// 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" });
}
});
// Health check endpoint
app.get("/api/health", (req, res) => {
res.status(200).json({ status: "ok", timestamp: new Date().toISOString() });
@@ -349,6 +690,16 @@ ${activeTasks.slice(0, 5).map(t => `- [${t.priority}] ${t.title}`).join('\n')}
...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);
@@ -367,6 +718,16 @@ ${activeTasks.slice(0, 5).map(t => `- [${t.priority}] ${t.title}`).join('\n')}
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" });
@@ -379,6 +740,17 @@ ${activeTasks.slice(0, 5).map(t => `- [${t.priority}] ${t.title}`).join('\n')}
if (!success) {
return res.status(404).json({ error: "Label not found" });
}
await storage.deleteLabel(req.params.id);
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" });
@@ -488,6 +860,41 @@ ${activeTasks.slice(0, 5).map(t => `- [${t.priority}] ${t.title}`).join('\n')}
}
});
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 {
@@ -500,6 +907,21 @@ ${activeTasks.slice(0, 5).map(t => `- [${t.priority}] ${t.title}`).join('\n')}
...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);
}
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" });
@@ -516,149 +938,33 @@ ${activeTasks.slice(0, 5).map(t => `- [${t.priority}] ${t.title}`).join('\n')}
return res.status(400).json({ error: "Invalid task data", details: updates.error });
}
// Gamification: Award XP on completion
if (previousTask && previousTask.status !== 'done' && updates.data.status === 'done') {
try {
const xpEarned = calculateXP(previousTask);
const user = await storage.getUser((req.user as User).id);
if (!user) throw new Error("User not found for gamification");
let newStreak = user.currentStreak || 0;
let streakBonus = 0;
let diffDays = 0;
if (user) {
const now = new Date();
const lastDate = user.lastTaskDate ? new Date(user.lastTaskDate) : null;
if (!lastDate) {
newStreak = 1;
} else {
const diffTime = Math.abs(now.setHours(0, 0, 0, 0) - lastDate.setHours(0, 0, 0, 0));
diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 1) {
newStreak += 1;
streakBonus = Math.min(newStreak * 5, 50);
} else if (diffDays > 1) {
newStreak = 1;
newStreak = 1;
}
}
// --- Daily Clear Bonus Check ---
// Check if this was the last 'todo' task for today
const startOfDay = new Date();
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999);
// Re-fetch all tasks (inefficient but safe for now, better: optimize storage method)
const allTasks = await storage.getTasksForUser(user.id);
const remainingToday = allTasks.filter(t =>
t.id !== previousTask.id && // exclude current
t.status !== 'done' && // is remaining
t.dueDate && // has due date
new Date(t.dueDate) >= startOfDay &&
new Date(t.dueDate) <= endOfDay
);
if (remainingToday.length === 0) {
// Bonus!
const clearBonus = 50;
await storage.logXpEvent({
userId: user.id,
amount: clearBonus,
source: 'daily_clear_bonus', // Ensure translation key exists
});
console.log(`[Gamification] Awarded ${clearBonus} XP for Daily Clear`);
}
if (diffDays !== 0 || !lastDate) {
await storage.updateUser(user.id, {
currentStreak: newStreak,
lastTaskDate: new Date()
});
}
}
// Log XP Event (Task)
await storage.logXpEvent({
userId: (req.user as User).id,
amount: xpEarned,
source: 'task_completion',
taskId: previousTask.id
});
// Log XP Event (Streak Bonus)
if (streakBonus > 0) {
await storage.logXpEvent({
userId: (req.user as User).id,
amount: streakBonus,
source: 'daily_streak',
});
console.log(`[Gamification] Awarded ${streakBonus} XP for streak of ${newStreak}`);
}
console.log(`[Gamification] Awarded ${xpEarned} XP for task ${previousTask.title}`);
// --- Goal Progress Check ---
try {
// Fetch active goals
const goals = await storage.getGoals(); // TODO: Filter by userId in storage
const userGoals = goals.filter(g => g.userId === user.id && !g.completed);
for (const goal of userGoals) {
let progress = 0;
// Calculate progress based on type
if (goal.type === 'weekly_tasks') {
// Count tasks completed this week
// Simplified: just update goal.current + 1 for now if we don't have full count logic
// Ideally we recount from history, but incremental update is easier
progress = goal.current + 1;
} else if (goal.type === 'streak') {
progress = newStreak;
} else if (goal.type === 'total_xp') {
progress = user.xp + xpEarned; // XP updated via logXpEvent side-effect? No, explicitly.
// The user obj here is stale, user.xp is old.
// But we just added xpEarned in logXpEvent (via side effect in storage).
// Let's assume +xpEarned.
// A better way is to re-fetch user, or rely on client/server sync.
progress = user.xp + xpEarned + streakBonus;
}
// Update Goal
if (progress !== goal.current) {
await storage.updateGoal(goal.id, { current: progress, completed: progress >= goal.target });
if (progress >= goal.target && !goal.completed) {
// Goal Completion Bonus
const goalBonus = 100;
await storage.logXpEvent({
userId: user.id,
amount: goalBonus,
source: 'goal_completed'
});
console.log(`[Gamification] Goal "${goal.title}" Completed! +${goalBonus} XP`);
}
}
}
} catch (goalErr) {
console.error("[Gamification] Error checking goals:", goalErr);
}
} catch (err) {
console.error("[Gamification] Error processing rewards:", err);
// Do not fail the request, just log
// Award XP using GamificationService
if (req.user && previousTask) { // Ensure previousTask exists for comparison
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);
} else if (Object.keys(updates.data).length > 0) { // Only award if there are actual updates
// Small points for any other update (title, description, etc)
await gamificationService.awardXP((req.user as User).id, 'update_task');
}
}
const task = await storage.updateTask(req.params.id, updates.data);
if (!task) {
const updatedTask = await storage.updateTask(req.params.id, updates.data);
if (!updatedTask) {
return res.status(404).json({ error: "Task not found" });
}
res.json(task);
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) });
@@ -671,6 +977,16 @@ ${activeTasks.slice(0, 5).map(t => `- [${t.priority}] ${t.title}`).join('\n')}
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" });
@@ -722,6 +1038,16 @@ ${activeTasks.slice(0, 5).map(t => `- [${t.priority}] ${t.title}`).join('\n')}
console.log("Validation passed, creating goal...");
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);
@@ -860,11 +1186,6 @@ ${activeTasks.slice(0, 5).map(t => `- [${t.priority}] ${t.title}`).join('\n')}
isSystem: (req.user as User).role === 'admin' && req.body.isSystem !== false,
};
// Force isSystem=false for non-admins
if ((req.user as User).role !== 'admin') {
rewardData.isSystem = false;
}
const reward = await storage.createReward(rewardData);
res.json(reward);
} catch (err) {
@@ -1076,6 +1397,7 @@ ${activeTasks.slice(0, 5).map(t => `- [${t.priority}] ${t.title}`).join('\n')}
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);