diff --git a/package.json b/package.json index 5242d3b..0611550 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.82.1", + "version": "1.83.0", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index 61ea853..5f838e3 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -6,6 +6,20 @@ import { notifyUser } from '@/lib/sse'; const prisma = new PrismaClient(); +// Validate and sanitize a URL — only allow http/https, reject javascript: and data: schemes +function sanitizeUrl(raw: string | null | undefined): string | null { + if (!raw) return null; + const trimmed = raw.trim(); + if (!trimmed) return null; + try { + const parsed = new URL(trimmed.startsWith('http') ? trimmed : `https://${trimmed}`); + if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return null; + return parsed.href; + } catch { + return null; + } +} + // Helper to generate a deterministic virtual ID const generateVirtualId = (originalId: string, dateStr: string) => { return `virtual-${originalId}-${dateStr}`; @@ -345,7 +359,7 @@ export async function POST(request: NextRequest) { parentTaskId: parentTaskId || null, ...(projectId !== undefined && { projectId: projectId || null }), ...(kanbanStage !== undefined && { kanbanStage: kanbanStage || null }), - ...(url !== undefined && { url: url || null }), + ...(url !== undefined && { url: sanitizeUrl(url) }), ...(externalId && { externalId, externalProvider, externalListId }), }, }); @@ -482,7 +496,7 @@ export async function PATCH(request: NextRequest) { ...(projectId !== undefined && { projectId: projectId || null }), ...(kanbanStage !== undefined && { kanbanStage: kanbanStage || null }), ...(externalProvider !== undefined && { externalProvider: externalProvider || null }), - ...(url !== undefined && { url: url || null }), + ...(url !== undefined && { url: sanitizeUrl(url) }), }, }); diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index e096fa3..cc1b025 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -7365,7 +7365,8 @@ export default function WeeklyView() { onEdit={() => setEditingTaskId(task.id)} onUpdate={(newTitle) => updateTask(task.id, newTitle)} onDelete={() => deleteTask(task.id)} - onNotes={() => setSelectedTaskForNotes(task)} + onNotes={(notes) => updateTaskNotes(task.id, notes)} + onUrl={(url) => updateTaskUrl(task.id, url)} onRollToggle={() => toggleTaskRolling(task.id)} onRecurrence={() => setSelectedTaskForRecurrence(task) @@ -8065,7 +8066,7 @@ export default function WeeklyView() { onEdit={() => setEditingTaskId(task.id)} onUpdate={(title) => updateTask(task.id, title)} onDelete={() => deleteTask(task.id)} - onNotes={() => setSelectedTaskForNotes(task)} + onNotes={(notes) => updateTaskNotes(task.id, notes)} onUrl={(url) => updateTaskUrl(task.id, url)} onRollToggle={() => toggleTaskRolling(task.id)} onRecurrence={() => setSelectedTaskForRecurrence(task)} @@ -8138,7 +8139,8 @@ export default function WeeklyView() { onEdit={() => setEditingTaskId(task.id)} onUpdate={(title) => updateTask(task.id, title)} onDelete={() => deleteTask(task.id)} - onNotes={() => setSelectedTaskForNotes(task)} + onNotes={(notes) => updateTaskNotes(task.id, notes)} + onUrl={(url) => updateTaskUrl(task.id, url)} onRollToggle={() => toggleTaskRolling(task.id)} onRecurrence={() => setSelectedTaskForRecurrence(task)} onDragStart={(e, t) => handleDragStart(e, t)} @@ -8776,6 +8778,7 @@ export default function WeeklyView() { task={selectedTaskForNotes} onClose={() => setSelectedTaskForNotes(null)} updateTaskNotes={updateTaskNotes} + updateTaskUrl={updateTaskUrl} /> ) } @@ -8894,21 +8897,45 @@ export default function WeeklyView() { + {/* URL */} +
+ +
+ { + const raw = e.target.value.trim(); + const normalised = raw ? (raw.startsWith("http") ? raw : `https://${raw}`) : ""; + if (normalised !== (liveTask.url || "")) { + updateTaskUrl(liveTask.id, normalised); + setKanbanDetailTask({ ...liveTask, url: normalised || null }); + } + }} + onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); }} + placeholder="https://..." + className="kanban-detail-url" + style={{ flex: 1 }} + /> + {liveTask.url && ( + + + + + + + )} +
+
+ {/* Notes */}
-