feat: Enhance task filtering, smart scheduling, audit logs and translations
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
+82
-40
@@ -160,17 +160,31 @@ CRITICAL: After executing tools, provide a concise summary of your actions.`;
|
||||
return { success: false, error: "Task not found." };
|
||||
}
|
||||
|
||||
const user = await this.storage.getUser(userId);
|
||||
if (!user) {
|
||||
return { success: false, error: "User not found." };
|
||||
}
|
||||
|
||||
// 1. Determine Context (Work vs Personal)
|
||||
let domain = "neutral";
|
||||
if (task.labelId) {
|
||||
const label = await this.storage.getLabel(task.labelId);
|
||||
if (label) {
|
||||
domain = label.domain; // 'work', 'personal', 'neutral'
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Get Availability Config
|
||||
// Fallback to old workHours if availability is missing (backward compatibility)
|
||||
const availability = user.availability || {
|
||||
work: user.workHours || { start: "09:00", end: "17:00", days: [1, 2, 3, 4, 5] },
|
||||
personal: { start: "18:00", end: "22:00", days: [1, 2, 3, 4, 5, 0, 6] }
|
||||
};
|
||||
|
||||
const startAfter = startAfterStr ? new Date(startAfterStr) : new Date();
|
||||
const durationMins = task.estimatedDuration || 60; // Default to 1h if not set
|
||||
const durationMins = task.estimatedDuration || 60;
|
||||
|
||||
const workStartHour = 9;
|
||||
// PRIORITY LOGIC: High priority tasks can be scheduled until 20:00 (8 PM)
|
||||
const workEndHour = task.priority === 'high' ? 20 : 18;
|
||||
|
||||
let scheduledDate: Date | null = null;
|
||||
|
||||
// PLANNED TIME LOGIC: If startDate is set, do not schedule before it.
|
||||
// If starteAfterStr is provided (e.g. "tomorrow"), use the max of both.
|
||||
// PLANNED TIME LOGIC
|
||||
let effectiveStart = startAfter;
|
||||
if (task.startDate) {
|
||||
const plannedStart = new Date(task.startDate);
|
||||
@@ -180,25 +194,35 @@ CRITICAL: After executing tools, provide a concise summary of your actions.`;
|
||||
}
|
||||
|
||||
let currentDay = new Date(effectiveStart);
|
||||
let scheduledDate: Date | null = null;
|
||||
|
||||
// Reset to next slot if passed
|
||||
if (currentDay.getHours() >= workEndHour) {
|
||||
currentDay.setDate(currentDay.getDate() + 1);
|
||||
currentDay.setHours(workStartHour, 0, 0, 0);
|
||||
} else if (currentDay.getHours() < workStartHour) {
|
||||
currentDay.setHours(workStartHour, 0, 0, 0);
|
||||
}
|
||||
// Helper to check if a specific time is within available hours
|
||||
const isTimeAvailable = (date: Date): boolean => {
|
||||
const day = date.getDay();
|
||||
const timeStr = date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
for (let dayOffset = 0; dayOffset < 3; dayOffset++) { // Look ahead 3 days
|
||||
// Checks
|
||||
const inSchedule = (sched: { start: string, end: string, days: number[] }) => {
|
||||
if (!sched.days.includes(day)) return false;
|
||||
return timeStr >= sched.start && timeStr < sched.end;
|
||||
};
|
||||
|
||||
if (domain === 'work') return inSchedule(availability.work);
|
||||
if (domain === 'personal') return inSchedule(availability.personal);
|
||||
|
||||
// Neutral: Available in either
|
||||
return inSchedule(availability.work) || inSchedule(availability.personal);
|
||||
};
|
||||
|
||||
// Look ahead 7 days
|
||||
for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
|
||||
const dayStart = new Date(currentDay);
|
||||
dayStart.setHours(workStartHour, 0, 0, 0);
|
||||
dayStart.setHours(0, 0, 0, 0);
|
||||
const dayEnd = new Date(currentDay);
|
||||
dayEnd.setHours(workEndHour, 0, 0, 0);
|
||||
dayEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
// Get all tasks for this day that have a due date (and time)
|
||||
// Fetch tasks for collision detection
|
||||
const allTasks = await this.storage.searchTasks("", userId);
|
||||
|
||||
// Filter for tasks on this day
|
||||
const dayTasks = allTasks.filter(t => {
|
||||
if (!t.dueDate) return false;
|
||||
const d = new Date(t.dueDate);
|
||||
@@ -207,25 +231,44 @@ CRITICAL: After executing tools, provide a concise summary of your actions.`;
|
||||
d.getFullYear() === currentDay.getFullYear();
|
||||
});
|
||||
|
||||
// Find gaps
|
||||
// Sort by time
|
||||
dayTasks.sort((a, b) => (a.dueDate!.getTime() - b.dueDate!.getTime()));
|
||||
|
||||
// Check slots
|
||||
// Start checking from 'currentDay' time (if today) or 9am
|
||||
// Iterate through the day in 15min chunks
|
||||
// Start from 'now' if checking today, otherwise start of day
|
||||
let attemptTime = new Date(currentDay);
|
||||
if (attemptTime < dayStart) attemptTime = dayStart;
|
||||
if (attemptTime < dayStart) attemptTime = dayStart; // Should not happen due to setHours logic but safety
|
||||
|
||||
while (attemptTime.getTime() + (durationMins * 60000) <= dayEnd.getTime()) {
|
||||
const attemptEnd = new Date(attemptTime.getTime() + (durationMins * 60000));
|
||||
// Advance to next 15m slot if needed
|
||||
const remainder = attemptTime.getMinutes() % 15;
|
||||
if (remainder !== 0) {
|
||||
attemptTime.setMinutes(attemptTime.getMinutes() + (15 - remainder));
|
||||
}
|
||||
attemptTime.setSeconds(0, 0);
|
||||
|
||||
// Check collision
|
||||
// Loop until end of day
|
||||
while (attemptTime < dayEnd) {
|
||||
// 1. Check if this START time is within allowed hours
|
||||
if (!isTimeAvailable(attemptTime)) {
|
||||
attemptTime.setMinutes(attemptTime.getMinutes() + 15);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Check if the END time is within allowed hours (don't span into offline time)
|
||||
const attemptEndTime = new Date(attemptTime.getTime() + durationMins * 60000);
|
||||
// We check the end time loosely, or strictly? Strictly ensures we don't work late.
|
||||
// But simplified: check if end is also available (or roughly available)
|
||||
// Let's check the End Time as well.
|
||||
// Note: If schedule is 9-5 and 6-10, a task could technically span 4:30-5:30 if we strictly check 'inSchedule' for all points.
|
||||
// Simplification: Check Start and End.
|
||||
if (!isTimeAvailable(new Date(attemptEndTime.getTime() - 1))) { // Check just before end
|
||||
attemptTime.setMinutes(attemptTime.getMinutes() + 15);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Collision Check
|
||||
const hasCollision = dayTasks.some(t => {
|
||||
const tStart = new Date(t.dueDate!);
|
||||
const tDuration = t.estimatedDuration || 60;
|
||||
const tEnd = new Date(tStart.getTime() + (tDuration * 60000));
|
||||
|
||||
return (attemptTime < tEnd && attemptEnd > tStart);
|
||||
return (attemptTime < tEnd && attemptEndTime > tStart);
|
||||
});
|
||||
|
||||
if (!hasCollision) {
|
||||
@@ -233,15 +276,14 @@ CRITICAL: After executing tools, provide a concise summary of your actions.`;
|
||||
break;
|
||||
}
|
||||
|
||||
// specific increment? 30 mins
|
||||
attemptTime = new Date(attemptTime.getTime() + 30 * 60000);
|
||||
attemptTime.setMinutes(attemptTime.getMinutes() + 15);
|
||||
}
|
||||
|
||||
if (scheduledDate) break;
|
||||
|
||||
// Move to next day
|
||||
// Prepare next day
|
||||
currentDay.setDate(currentDay.getDate() + 1);
|
||||
currentDay.setHours(workStartHour, 0, 0, 0);
|
||||
currentDay.setHours(0, 0, 0, 0); // Start at midnight
|
||||
}
|
||||
|
||||
if (scheduledDate) {
|
||||
@@ -249,10 +291,10 @@ CRITICAL: After executing tools, provide a concise summary of your actions.`;
|
||||
return {
|
||||
success: true,
|
||||
scheduledDate: scheduledDate.toISOString(),
|
||||
message: `Scheduled for ${scheduledDate.toLocaleDateString()} at ${scheduledDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}.`
|
||||
message: `Scheduled for ${scheduledDate.toLocaleDateString()} at ${scheduledDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} (${domain} time).`
|
||||
};
|
||||
} else {
|
||||
return { success: false, error: "Could not find a free slot in the next 3 days." };
|
||||
return { success: false, error: "Could not find a free slot in the next 7 days." };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+38
-1
@@ -225,7 +225,7 @@ export function setupAuth(app: Express) {
|
||||
}
|
||||
|
||||
// Valid! Clear code and login
|
||||
await storage.updateUser(user.id, { otpCode: null, otpExpiresAt: null });
|
||||
await storage.updateUser(user.id, { otpCode: null, otpExpiresAt: null, is2faEnabled: true });
|
||||
|
||||
req.login(user, (err) => {
|
||||
if (err) return next(err);
|
||||
@@ -238,6 +238,43 @@ export function setupAuth(app: Express) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/auth/2fa/generate", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const user = req.user as User;
|
||||
|
||||
// Generate and start 2FA flow
|
||||
try {
|
||||
// Enable 2FA flag
|
||||
await storage.updateUser(user.id, { is2faEnabled: true });
|
||||
|
||||
// Send initial code to verify
|
||||
const { EmailService } = await import("./email");
|
||||
const emailService = new EmailService(storage);
|
||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
const expiresAt = new Date(Date.now() + 10 * 60 * 1000);
|
||||
|
||||
await storage.updateUser(user.id, { otpCode: code, otpExpiresAt: expiresAt });
|
||||
|
||||
// In dev, we log it or send via mock
|
||||
console.log(`[2FA] Generated code for ${user.username}: ${code}`);
|
||||
await emailService.send2FACode(user, code);
|
||||
|
||||
res.json({ message: "2FA enabled. Please verify code sent to email.", debugCode: code });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: "Failed to generate 2FA" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/auth/2fa/disable", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
try {
|
||||
await storage.updateUser((req.user as User).id, { is2faEnabled: false, otpCode: null, otpExpiresAt: null });
|
||||
res.json({ message: "2FA disabled" });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: "Failed to disable 2FA" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/logout", (req, res, next) => {
|
||||
req.logout((err) => {
|
||||
if (err) return next(err);
|
||||
|
||||
+305
-28
@@ -1,7 +1,7 @@
|
||||
import type { Express } from "express";
|
||||
import { createServer, type Server } from "http";
|
||||
import { storage } from "./storage.js";
|
||||
import { insertLabelSchema, insertTaskSchema, insertNoteSchema, insertXpEventSchema, insertGoalSchema, insertRewardSchema, rewards, userRewards, User } from "../shared/schema.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";
|
||||
@@ -14,7 +14,7 @@ const aiService = new AiService(storage);
|
||||
const recurrenceService = new RecurrenceService(storage);
|
||||
const gamificationService = new GamificationService(storage);
|
||||
|
||||
import { setupAuth, hashPassword, comparePassword } from "./auth_debug.js";
|
||||
import { setupAuth, hashPassword, comparePassword } from "./auth.js";
|
||||
|
||||
function isAdmin(req: any, res: any, next: any) {
|
||||
if (req.isAuthenticated() && req.user.role === 'admin') {
|
||||
@@ -26,6 +26,57 @@ function isAdmin(req: any, res: any, next: any) {
|
||||
export async function registerRoutes(app: Express): Promise<Server> {
|
||||
setupAuth(app);
|
||||
|
||||
// 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<User> = {};
|
||||
|
||||
// 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();
|
||||
@@ -676,6 +727,37 @@ User Context:
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
@@ -796,6 +878,28 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
}
|
||||
});
|
||||
|
||||
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) => {
|
||||
try {
|
||||
const result = insertLabelSchema.safeParse(req.body);
|
||||
@@ -824,6 +928,44 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
}
|
||||
});
|
||||
|
||||
app.patch("/api/tasks/:id", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
// Validate request body against schema
|
||||
const cleanBody = insertTaskSchema.partial().safeParse(req.body);
|
||||
if (!cleanBody.success) {
|
||||
return res.status(400).json({ error: cleanBody.error });
|
||||
}
|
||||
|
||||
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" });
|
||||
if (!hasAccess) return res.status(403).json({ error: "Access denied" });
|
||||
|
||||
const updatedTask = await storage.updateTask(task.id, cleanBody.data);
|
||||
|
||||
if (cleanBody.data.status === 'done' && task.status !== 'done') {
|
||||
if (req.user) {
|
||||
// Check if late
|
||||
const now = new Date();
|
||||
const isLate = task.dueDate && new Date(task.dueDate) < now;
|
||||
const xpSource = isLate ? "complete_task_late" : "complete_task";
|
||||
|
||||
// Award XP for completion with Task Title context
|
||||
await gamificationService.awardXP(
|
||||
(req.user as User).id,
|
||||
xpSource,
|
||||
undefined,
|
||||
{ taskId: task.id, taskTitle: task.title }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
res.json(updatedTask);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: "Failed to update task" });
|
||||
}
|
||||
});
|
||||
|
||||
app.patch("/api/labels/:id", async (req, res) => {
|
||||
try {
|
||||
const updates = insertLabelSchema.partial().safeParse(req.body);
|
||||
@@ -963,14 +1105,51 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
}
|
||||
});
|
||||
|
||||
// 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) => {
|
||||
// TODO: Check if user has access to this specific task (Owns it OR is Shared)
|
||||
// For now, simple get
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
try {
|
||||
const task = await storage.getTask(req.params.id);
|
||||
if (!task) {
|
||||
return res.status(404).json({ error: "Task not found" });
|
||||
}
|
||||
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" });
|
||||
@@ -1265,6 +1444,15 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
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" });
|
||||
@@ -1281,6 +1469,16 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
};
|
||||
|
||||
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" });
|
||||
@@ -1347,6 +1545,16 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
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" });
|
||||
@@ -1365,32 +1573,23 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
}
|
||||
|
||||
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.post("/api/user/routine/:type/complete", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const type = req.params.type;
|
||||
if (type !== 'morning' && type !== 'evening') return res.status(400).json({ error: "Invalid routine type" });
|
||||
|
||||
try {
|
||||
const updates: any = {};
|
||||
const now = new Date();
|
||||
if (type === 'morning') {
|
||||
updates.lastMorningRoutine = now;
|
||||
} else {
|
||||
updates.lastEveningRoutine = now;
|
||||
}
|
||||
|
||||
const updatedUser = await storage.updateUser((req.user as User).id, updates);
|
||||
res.json(updatedUser);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: "Failed to complete routine" });
|
||||
}
|
||||
});
|
||||
|
||||
app.patch("/api/user/password", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
@@ -1407,6 +1606,15 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
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" });
|
||||
@@ -1445,6 +1653,15 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
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" });
|
||||
@@ -1487,6 +1704,14 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
|
||||
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" });
|
||||
@@ -1504,6 +1729,15 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
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" });
|
||||
@@ -1521,6 +1755,17 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
try {
|
||||
const updated = await storage.updateTask(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
|
||||
});
|
||||
}
|
||||
|
||||
// Check for Recurrence if task is marked done
|
||||
if (updated && updated.status === 'done' && updated.isRecurring && req.body.status === 'done') {
|
||||
// Fire and forget, or await? Await to ensure it happens.
|
||||
@@ -1541,8 +1786,40 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
}
|
||||
});
|
||||
|
||||
// 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/export", async (req, res) => {
|
||||
app.post("/api/user/data-export", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
try {
|
||||
const user = req.user as User;
|
||||
|
||||
+37
-3
@@ -32,7 +32,9 @@ export interface IStorage {
|
||||
createUserTaskAccess(access: InsertUserTaskAccess): Promise<UserTaskAccess>; // Alias
|
||||
|
||||
getSharedTasks(userId: string): Promise<SharedTask[]>; // Tasks shared WITH user
|
||||
getSharedTask(taskId: string, userId: string): Promise<SharedTask | undefined>; // Check specific share
|
||||
getUserTaskAccess(viewerId: string): Promise<UserTaskAccess[]>; // Access Viewer has to Owners
|
||||
checkUserTaskAccess(ownerId: string, viewerId: string): Promise<boolean>; // Check specific access
|
||||
getTaskSharedUsers(taskId: string): Promise<User[]>; // Get users a task is shared WITH
|
||||
unshareTask(taskId: string, userId: string): Promise<boolean>; // Unshare specific task from user
|
||||
|
||||
@@ -280,10 +282,18 @@ export class MemStorage implements IStorage {
|
||||
return Array.from(this.sharedTasks.values()).filter(st => st.sharedWithUserId === userId);
|
||||
}
|
||||
|
||||
async getSharedTask(taskId: string, userId: string): Promise<SharedTask | undefined> {
|
||||
return Array.from(this.sharedTasks.values()).find(st => st.taskId === taskId && st.sharedWithUserId === userId);
|
||||
}
|
||||
|
||||
async getUserTaskAccess(viewerId: string): Promise<UserTaskAccess[]> {
|
||||
return Array.from(this.userTaskAccess.values()).filter(uta => uta.viewerId === viewerId);
|
||||
}
|
||||
|
||||
async checkUserTaskAccess(ownerId: string, viewerId: string): Promise<boolean> {
|
||||
return Array.from(this.userTaskAccess.values()).some(uta => uta.ownerId === ownerId && uta.viewerId === viewerId);
|
||||
}
|
||||
|
||||
async getTaskSharedUsers(taskId: string): Promise<User[]> {
|
||||
const shares = Array.from(this.sharedTasks.values()).filter(st => st.taskId === taskId);
|
||||
const users: User[] = [];
|
||||
@@ -479,8 +489,13 @@ export class MemStorage implements IStorage {
|
||||
estimatedDuration: insertTask.estimatedDuration || null,
|
||||
parentTaskId: insertTask.parentTaskId || null,
|
||||
startDate: insertTask.startDate || null,
|
||||
dependencies: insertTask.dependencies || null,
|
||||
userId: insertTask.userId || null // Set ownership
|
||||
dependencies: insertTask.dependencies || [],
|
||||
userId: insertTask.userId || null, // Allow null for system/orphaned tasks support
|
||||
isRecurring: false,
|
||||
recurrenceInterval: null,
|
||||
recurrenceIntervalValue: 1,
|
||||
recurrenceDays: [],
|
||||
recurrenceEnd: null
|
||||
};
|
||||
this.tasks.set(id, task);
|
||||
return task;
|
||||
@@ -514,7 +529,8 @@ export class MemStorage implements IStorage {
|
||||
id,
|
||||
userId: event.userId || null,
|
||||
taskId: event.taskId || null,
|
||||
createdAt: new Date()
|
||||
createdAt: new Date(),
|
||||
details: event.details || null
|
||||
};
|
||||
this.xpEvents.set(id, xpEvent);
|
||||
// Also update user XP
|
||||
@@ -1085,10 +1101,28 @@ export class DbStorage implements IStorage {
|
||||
return await this.db.select().from(schema.sharedTasks).where(eq(schema.sharedTasks.sharedWithUserId, userId));
|
||||
}
|
||||
|
||||
async getSharedTask(taskId: string, userId: string): Promise<SharedTask | undefined> {
|
||||
const [share] = await this.db.select().from(schema.sharedTasks)
|
||||
.where(and(
|
||||
eq(schema.sharedTasks.taskId, taskId),
|
||||
eq(schema.sharedTasks.sharedWithUserId, userId)
|
||||
));
|
||||
return share;
|
||||
}
|
||||
|
||||
async getUserTaskAccess(viewerId: string): Promise<UserTaskAccess[]> {
|
||||
return await this.db.select().from(schema.userTaskAccess).where(eq(schema.userTaskAccess.viewerId, viewerId));
|
||||
}
|
||||
|
||||
async checkUserTaskAccess(ownerId: string, viewerId: string): Promise<boolean> {
|
||||
const [access] = await this.db.select().from(schema.userTaskAccess)
|
||||
.where(and(
|
||||
eq(schema.userTaskAccess.ownerId, ownerId),
|
||||
eq(schema.userTaskAccess.viewerId, viewerId)
|
||||
));
|
||||
return !!access;
|
||||
}
|
||||
|
||||
async getTaskSharedUsers(taskId: string): Promise<User[]> {
|
||||
const shares = await this.db.select().from(schema.sharedTasks).where(eq(schema.sharedTasks.taskId, taskId));
|
||||
if (shares.length === 0) return [];
|
||||
|
||||
Reference in New Issue
Block a user