feat: enhance audit logging, add MCP settings, and production docker setup
continuous-integration/drone/push Build is passing
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:
+92
-45
@@ -27,9 +27,9 @@ import ProjectTemplate from './components/ProjectTemplate';
|
||||
import WeekListView from './components/WeekListView';
|
||||
import Settings from './pages/settings';
|
||||
import AchievementsPage from './pages/AchievementsPage';
|
||||
import UnscheduledTasksPage from './pages/UnscheduledTasksPage';
|
||||
import { Task, Label } from '@shared/schema';
|
||||
import { useTimer } from './hooks/useTimer';
|
||||
|
||||
import { Plus } from 'lucide-react';
|
||||
import { SidebarProvider, SidebarInset, SidebarTrigger } from "@/components/ui/sidebar"
|
||||
import { AppSidebar } from "./components/AppSidebar"
|
||||
@@ -45,16 +45,17 @@ import NotFound from "@/pages/not-found";
|
||||
import { AiChat } from "@/components/AiChat";
|
||||
import ForgotPasswordPage from "@/pages/ForgotPasswordPage";
|
||||
import ResetPasswordPage from "@/pages/ResetPasswordPage";
|
||||
import AiChatPage from "@/pages/AiChatPage";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { User } from "@shared/schema";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
|
||||
function App() {
|
||||
const { t } = useTranslation();
|
||||
const [, setLocation] = useLocation();
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [labels, setLabels] = useState<Label[]>([]);
|
||||
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [isTaskDetailsOpen, setIsTaskDetailsOpen] = useState(false);
|
||||
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
||||
@@ -62,6 +63,8 @@ function App() {
|
||||
const [showPomodoro, setShowPomodoro] = useState(false);
|
||||
const [activePomodoroTaskId, setActivePomodoroTaskId] = useState<string | null>(null);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: user, isLoading: isLoadingUser } = useQuery<User>({
|
||||
queryKey: ["/api/user"],
|
||||
retry: false,
|
||||
@@ -71,32 +74,20 @@ function App() {
|
||||
queryKey: ['/api/setup/status'],
|
||||
});
|
||||
|
||||
// Fetch tasks and labels
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const tasksResponse = await fetch('/api/tasks');
|
||||
if (tasksResponse.ok) {
|
||||
const serverTasks: Task[] = await tasksResponse.json();
|
||||
const normalizedTasks = serverTasks.map(task => ({
|
||||
...task,
|
||||
dueDate: task.dueDate ? new Date(task.dueDate) : null
|
||||
}));
|
||||
setTasks(normalizedTasks);
|
||||
}
|
||||
// Fetch tasks and labels using React Query
|
||||
const { data: tasks = [] } = useQuery<Task[]>({
|
||||
queryKey: ['/api/tasks'],
|
||||
enabled: !!user,
|
||||
select: (data) => data.map(task => ({
|
||||
...task,
|
||||
dueDate: task.dueDate ? new Date(task.dueDate) : null
|
||||
}))
|
||||
});
|
||||
|
||||
const labelsResponse = await fetch('/api/labels');
|
||||
if (labelsResponse.ok) {
|
||||
const serverLabels: Label[] = await labelsResponse.json();
|
||||
setLabels(serverLabels);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [user]);
|
||||
const { data: labels = [] } = useQuery<Label[]>({
|
||||
queryKey: ['/api/labels'],
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
const handleCreateTask = async (newTask: Partial<Task>) => {
|
||||
try {
|
||||
@@ -122,17 +113,31 @@ function App() {
|
||||
if (!response.ok) throw new Error('Failed to create task');
|
||||
|
||||
const createdTask: Task = await response.json();
|
||||
const normalizedTask = {
|
||||
...createdTask,
|
||||
dueDate: createdTask.dueDate ? new Date(createdTask.dueDate) : null
|
||||
};
|
||||
setTasks(prev => [...prev, normalizedTask]);
|
||||
|
||||
// Update cache optimistically or invalidation
|
||||
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => {
|
||||
const normalizedTask = {
|
||||
...createdTask,
|
||||
dueDate: createdTask.dueDate ? new Date(createdTask.dueDate) : null
|
||||
};
|
||||
return [...old, normalizedTask];
|
||||
});
|
||||
// Invalidate to be sure
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating task:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTaskUpdate = async (taskId: string, updates: Partial<Task>) => {
|
||||
// Optimistic Update
|
||||
const previousTasks = queryClient.getQueryData<Task[]>(['/api/tasks']);
|
||||
|
||||
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) =>
|
||||
old.map(task => task.id === taskId ? { ...task, ...updates } : task)
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}`, {
|
||||
method: 'PATCH',
|
||||
@@ -143,19 +148,28 @@ function App() {
|
||||
if (!response.ok) throw new Error('Failed to update task');
|
||||
|
||||
const updatedTask: Task = await response.json();
|
||||
const normalizedTask = {
|
||||
...updatedTask,
|
||||
dueDate: updatedTask.dueDate ? new Date(updatedTask.dueDate) : null
|
||||
};
|
||||
setTasks(prev => prev.map(task => task.id === taskId ? normalizedTask : task));
|
||||
|
||||
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => uniqueTasks(old, updatedTask));
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating task:', error);
|
||||
setTasks(prev => prev.map(task => task.id === taskId ? { ...task, ...updates } : task));
|
||||
// Rollback
|
||||
if (previousTasks) {
|
||||
queryClient.setQueryData(['/api/tasks'], previousTasks);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to merge updated task into array
|
||||
const uniqueTasks = (tasks: Task[], updated: Task) => {
|
||||
const normalized = { ...updated, dueDate: updated.dueDate ? new Date(updated.dueDate) : null };
|
||||
return tasks.map(t => t.id === updated.id ? normalized : t);
|
||||
};
|
||||
|
||||
const handleCreateFromTemplate = (templateId: string, startDate: Date, projectName: string) => {
|
||||
console.log('Creating project from template:', { templateId, startDate, projectName });
|
||||
// Assuming this might create tasks, we should invalidate
|
||||
setTimeout(() => queryClient.invalidateQueries({ queryKey: ['/api/tasks'] }), 1000);
|
||||
};
|
||||
|
||||
const { startTimer, stopTimer } = useTimer({
|
||||
@@ -197,8 +211,14 @@ function App() {
|
||||
|
||||
const confirmTaskDelete = async () => {
|
||||
if (!deleteTaskId) return;
|
||||
|
||||
const previousTasks = queryClient.getQueryData<Task[]>(['/api/tasks']);
|
||||
const taskToDelete = tasks.find(task => task.id === deleteTaskId);
|
||||
setTasks(prev => prev.filter(task => task.id !== deleteTaskId));
|
||||
|
||||
// Optimistic delete
|
||||
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) =>
|
||||
old.filter(task => task.id !== deleteTaskId)
|
||||
);
|
||||
|
||||
if (selectedTask?.id === deleteTaskId) {
|
||||
handleTaskDetailsClose();
|
||||
@@ -208,9 +228,13 @@ function App() {
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${deleteTaskId}`, { method: 'DELETE' });
|
||||
if (!response.ok && response.status !== 404) throw new Error('Failed to delete task');
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
|
||||
} catch (error) {
|
||||
console.error('Error deleting task:', error);
|
||||
if (taskToDelete) setTasks(prev => [...prev, taskToDelete]);
|
||||
// Rollback
|
||||
if (previousTasks) {
|
||||
queryClient.setQueryData(['/api/tasks'], previousTasks);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -315,16 +339,36 @@ function App() {
|
||||
<Route path="/templates">
|
||||
<ProjectTemplate
|
||||
onCreateFromTemplate={handleCreateFromTemplate}
|
||||
onCreateTasks={(newTasks) => setTasks(prev => [...prev, ...newTasks])}
|
||||
onCreateTasks={(newTasks) => {
|
||||
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => [...old, ...newTasks]);
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
|
||||
}}
|
||||
onNavigateToSettings={() => setLocation('/settings')}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/achievements">
|
||||
<AchievementsPage user={user} />
|
||||
{user ? <AchievementsPage user={user} /> : <AuthPage />}
|
||||
</Route>
|
||||
|
||||
<Route path="/unscheduled">
|
||||
{user ? (
|
||||
<UnscheduledTasksPage
|
||||
user={user}
|
||||
onToggleCompletion={(taskId, currentStatus) => handleTaskStatusChange(taskId, currentStatus === 'done' ? 'todo' : 'done')}
|
||||
onDelete={(id) => setDeleteTaskId(id)}
|
||||
onUpdate={handleTaskUpdate}
|
||||
onSelect={setSelectedTask}
|
||||
/>
|
||||
) : (
|
||||
<AuthPage />
|
||||
)}
|
||||
</Route>
|
||||
<Route path="/leaderboard">
|
||||
<LeaderboardPage />
|
||||
</Route>
|
||||
<Route path="/ai">
|
||||
<AiChatPage />
|
||||
</Route>
|
||||
<Route path="/settings">
|
||||
<Settings onNavigateToTemplates={() => setLocation('/templates')} />
|
||||
</Route>
|
||||
@@ -370,7 +414,6 @@ function App() {
|
||||
/>
|
||||
|
||||
<Toaster />
|
||||
{user?.aiEnabled && <AiChat />}
|
||||
|
||||
<TaskCreationModal
|
||||
isOpen={isCreateModalOpen}
|
||||
@@ -384,6 +427,10 @@ function App() {
|
||||
task={selectedTask}
|
||||
onSave={handleTaskDetailsSave}
|
||||
labels={labels}
|
||||
onNavigate={(id) => {
|
||||
const t = tasks.find(x => x.id === id);
|
||||
if (t) setSelectedTask(t);
|
||||
}}
|
||||
/>
|
||||
|
||||
<AlertDialog open={deleteTaskId !== null} onOpenChange={(open) => !open && setDeleteTaskId(null)}>
|
||||
|
||||
Reference in New Issue
Block a user