-
Notifications
You must be signed in to change notification settings - Fork 8
fix(kanban-simple): wrap JSON.parse calls with safe fallback #568
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,19 @@ type BoardProps = { | |
| getProfile: (did: string) => Promise<Profile>; | ||
| }; | ||
|
|
||
| /** Safely parse a JSON string, returning the fallback value on failure. | ||
| * This handles cases where the value is a literal:// URI string or a | ||
| * signed expression envelope that cannot be parsed as a JSON array. */ | ||
| function safeJsonParse<T>(value: string | undefined | null, fallback: T): T { | ||
| if (!value) return fallback; | ||
| try { | ||
| return JSON.parse(value); | ||
| } catch { | ||
| console.warn('Failed to parse JSON value, using fallback:', value?.slice(0, 80)); | ||
| return fallback; | ||
| } | ||
| } | ||
|
|
||
| export type ColumnWithTasks = TaskColumn & { tasks: Task[] }; | ||
|
|
||
| export default function Board({ perspective, channelId, agent, getProfile }: BoardProps) { | ||
|
|
@@ -106,7 +119,7 @@ export default function Board({ perspective, channelId, agent, getProfile }: Boa | |
| setUpdating(false); | ||
| return; | ||
| } | ||
| const newOrderedColumnIds = [...JSON.parse(currentBoard.orderedColumnIds), newColumn.id]; | ||
| const newOrderedColumnIds = [...safeJsonParse<string[]>(currentBoard.orderedColumnIds, []), newColumn.id]; | ||
| currentBoard.orderedColumnIds = JSON.stringify(newOrderedColumnIds); | ||
| await currentBoard.save(); | ||
|
|
||
|
|
@@ -137,7 +150,7 @@ export default function Board({ perspective, channelId, agent, getProfile }: Boa | |
| setUpdating(false); | ||
| return; | ||
| } | ||
| const orderedColumnIds = JSON.parse(currentBoard.orderedColumnIds); | ||
| const orderedColumnIds = safeJsonParse<string[]>(currentBoard.orderedColumnIds, []); | ||
| const newOrderedColumnIds = orderedColumnIds.filter((id: string) => id !== columnId); | ||
|
|
||
| // Update the UI | ||
|
|
@@ -150,7 +163,7 @@ export default function Board({ perspective, channelId, agent, getProfile }: Boa | |
|
|
||
| // Delete the columns tasks and their links to the perspective | ||
| const column = columns.find((col) => col.id === columnId); | ||
| const taskIds = column.orderedTaskIds ? JSON.parse(column.orderedTaskIds) : []; | ||
| const taskIds = safeJsonParse<string[]>(column.orderedTaskIds, []); | ||
| const columnTasks = await Task.findAll(perspective, { where: { id: taskIds } }); | ||
|
Comment on lines
165
to
167
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Also applies to: 185-185 🤖 Prompt for AI Agents |
||
| await Promise.all( | ||
| columnTasks.map(async (task) => { | ||
|
|
@@ -202,7 +215,7 @@ export default function Board({ perspective, channelId, agent, getProfile }: Boa | |
|
|
||
| // Update the perspective | ||
| const currentBoard = (await TaskBoard.findAll(perspective, { parent: { model: Channel, id: channelId } }))[0]; | ||
| const newOrderedColumnIds = JSON.parse(currentBoard.orderedColumnIds); | ||
| const newOrderedColumnIds = safeJsonParse<string[]>(currentBoard.orderedColumnIds, []); | ||
| newOrderedColumnIds.splice(source.index, 1); | ||
| newOrderedColumnIds.splice(destination.index, 0, draggableId); | ||
| currentBoard.orderedColumnIds = JSON.stringify(newOrderedColumnIds); | ||
|
|
@@ -230,12 +243,12 @@ export default function Board({ perspective, channelId, agent, getProfile }: Boa | |
| destinationColumn.tasks.splice(destination.index, 0, movedTask); | ||
|
|
||
| // Update orderedTaskIds in source column | ||
| const sourceOrderedTaskIds = JSON.parse(sourceColumn.orderedTaskIds); | ||
| const sourceOrderedTaskIds = safeJsonParse<string[]>(sourceColumn.orderedTaskIds, []); | ||
| sourceOrderedTaskIds.splice(source.index, 1); | ||
| sourceColumn.orderedTaskIds = JSON.stringify(sourceOrderedTaskIds); | ||
|
|
||
| // Update orderedTaskIds in destination column | ||
| const destOrderedTaskIds = JSON.parse(destinationColumn.orderedTaskIds); | ||
| const destOrderedTaskIds = safeJsonParse<string[]>(destinationColumn.orderedTaskIds, []); | ||
| destOrderedTaskIds.splice(destination.index, 0, draggableId); | ||
| destinationColumn.orderedTaskIds = JSON.stringify(destOrderedTaskIds); | ||
|
|
||
|
|
@@ -273,7 +286,7 @@ export default function Board({ perspective, channelId, agent, getProfile }: Boa | |
| } | ||
|
|
||
| // Create the new orderedTaskIds | ||
| const newOrderedTaskIds = JSON.parse(column.orderedTaskIds); | ||
| const newOrderedTaskIds = safeJsonParse<string[]>(column.orderedTaskIds, []); | ||
| newOrderedTaskIds.splice(source.index, 1); | ||
| newOrderedTaskIds.splice(destination.index, 0, draggableId); | ||
|
|
||
|
|
@@ -304,7 +317,7 @@ export default function Board({ perspective, channelId, agent, getProfile }: Boa | |
| const newColumnsWithTasks = (await Promise.all( | ||
| newColumns.map(async (column) => { | ||
| // Get tasks and order them by the columns orderedTaskIds property | ||
| const taskIds = column.orderedTaskIds ? JSON.parse(column.orderedTaskIds) : []; | ||
| const taskIds = safeJsonParse<string[]>(column.orderedTaskIds, []); | ||
| const columnTasks = await Task.findAll(perspective, { where: { id: taskIds } }); | ||
| const taskMap = new Map(columnTasks.map((t) => [t.id, t])); | ||
| const orderedTasks = taskIds.map((id) => taskMap.get(id)).filter(Boolean); | ||
|
|
@@ -332,7 +345,7 @@ export default function Board({ perspective, channelId, agent, getProfile }: Boa | |
|
|
||
| // Update columns with tasks when columns or tasks change, unless an update is ongoing | ||
| useEffect(() => { | ||
| if (board && !updatingRef.current) getColumnsWithTasks(columns, JSON.parse(board.orderedColumnIds)); | ||
| if (board && !updatingRef.current) getColumnsWithTasks(columns, safeJsonParse<string[]>(board.orderedColumnIds, [])); | ||
| }, [board, columns, tasks]); | ||
|
|
||
| // Update board when boards subscription updates | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,17 @@ import { Task, TaskColumn } from '@coasys/flux-api'; | |
| import AvatarGroup from '../AvatarGroup'; | ||
| import { ColumnWithTasks } from '../Board/Board'; | ||
|
|
||
| /** Safely parse a JSON string, returning the fallback value on failure. */ | ||
| function safeJsonParse<T>(value: string | undefined | null, fallback: T): T { | ||
| if (!value) return fallback; | ||
| try { | ||
| return JSON.parse(value); | ||
| } catch { | ||
| console.warn('Failed to parse JSON value, using fallback:', value?.slice(0, 80)); | ||
| return fallback; | ||
| } | ||
|
Comment on lines
+10
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: fd TaskSettings.tsxRepository: coasys/flux Length of output: 124 🏁 Script executed: cat -n views/kanban-view-simple/src/components/TaskSettings/TaskSettings.tsx | head -200Repository: coasys/flux Length of output: 8566 🏁 Script executed: wc -l views/kanban-view-simple/src/components/TaskSettings/TaskSettings.tsxRepository: coasys/flux Length of output: 128 Add runtime validation to
Add a type guard parameter to validate the parsed value: Proposed fix-function safeJsonParse<T>(value: string | undefined | null, fallback: T): T {
+function safeJsonParse<T>(
+ value: string | undefined | null,
+ fallback: T,
+ isValid: (parsed: unknown) => parsed is T,
+): T {
if (!value) return fallback;
try {
- return JSON.parse(value);
+ const parsed: unknown = JSON.parse(value);
+ return isValid(parsed) ? parsed : fallback;
} catch {
console.warn('Failed to parse JSON value, using fallback:', value?.slice(0, 80));
return fallback;
}
}
+
+const isStringArray = (v: unknown): v is string[] =>
+ Array.isArray(v) && v.every((x) => typeof x === 'string');Then update call sites to pass the validator: -const sourceOrderedTaskIds = safeJsonParse<string[]>(source.orderedTaskIds, []).filter((id) => id !== task.id);
+const sourceOrderedTaskIds = safeJsonParse<string[]>(source.orderedTaskIds, [], isStringArray).filter((id) => id !== task.id);🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| type Props = { | ||
| perspective: PerspectiveProxy; | ||
| channelId: string; | ||
|
|
@@ -59,12 +70,12 @@ export default function TaskSettings({ | |
| const destination = columns.find((col) => col.columnName === taskColumn); | ||
|
|
||
| // Update orderedTaskIds in source column | ||
| const sourceOrderedTaskIds = JSON.parse(source.orderedTaskIds).filter((id) => id !== task.id); | ||
| const sourceOrderedTaskIds = safeJsonParse<string[]>(source.orderedTaskIds, []).filter((id) => id !== task.id); | ||
| source.orderedTaskIds = JSON.stringify(sourceOrderedTaskIds); | ||
| await source.save(batchId); | ||
|
Comment on lines
+73
to
75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard The code dereferences Proposed fix (defensive null checks) const source = columns.find((col) => col.columnName === column.columnName);
const destination = columns.find((col) => col.columnName === taskColumn);
+if (!source || !destination) {
+ console.error('Source or destination column not found');
+ setSaving(false);
+ return;
+}
@@
const columnModel = columns.find((col) => col.columnName === taskColumn);
+if (!columnModel) {
+ console.error('Column not found when creating task');
+ setSaving(false);
+ return;
+}
@@
const columnModel = columns.find((col) => col.id === column.id);
+if (!columnModel) {
+ console.error('Column not found when deleting task');
+ setDeleting(false);
+ return;
+}Also applies to: 78-80, 123-126, 151-154 🤖 Prompt for AI Agents |
||
|
|
||
| // Update orderedTaskIds in destination column | ||
| const destinationOrderedTaskIds = [...(JSON.parse(destination.orderedTaskIds) || []), task.id]; | ||
| const destinationOrderedTaskIds = [...safeJsonParse<string[]>(destination.orderedTaskIds, []), task.id]; | ||
| destination.orderedTaskIds = JSON.stringify(destinationOrderedTaskIds); | ||
| await destination.save(batchId); | ||
| } | ||
|
|
@@ -110,7 +121,7 @@ export default function TaskSettings({ | |
|
|
||
| // Store the task position in the column | ||
| const columnModel = columns.find((col) => col.columnName === taskColumn); | ||
| const newOrderedTaskIds = [...(JSON.parse(columnModel.orderedTaskIds) || []), newTaskModel.id]; | ||
| const newOrderedTaskIds = [...safeJsonParse<string[]>(columnModel.orderedTaskIds, []), newTaskModel.id]; | ||
| columnModel.orderedTaskIds = JSON.stringify(newOrderedTaskIds); | ||
| await columnModel.save(batchId); | ||
|
|
||
|
|
@@ -138,7 +149,7 @@ export default function TaskSettings({ | |
|
|
||
| // Update orderedTaskIds in column | ||
| const columnModel = columns.find((col) => col.id === column.id); | ||
| const newOrderedTaskIds = JSON.parse(column.orderedTaskIds).filter((id: string) => id !== task.id); | ||
| const newOrderedTaskIds = safeJsonParse<string[]>(column.orderedTaskIds, []).filter((id: string) => id !== task.id); | ||
| columnModel.orderedTaskIds = JSON.stringify(newOrderedTaskIds); | ||
| await columnModel.save(batchId); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: coasys/flux
Length of output: 1144
🏁 Script executed:
Repository: coasys/flux
Length of output: 114
🏁 Script executed:
cat -n views/kanban-view-simple/src/components/Board/Board.tsx | head -40Repository: coasys/flux
Length of output: 2066
🏁 Script executed:
Repository: coasys/flux
Length of output: 6141
🏁 Script executed:
Repository: coasys/flux
Length of output: 3205
🏁 Script executed:
Repository: coasys/flux
Length of output: 1803
Add runtime type validation to
safeJsonParse<T>before returning parsed valuesThe function catches JSON parse errors but cannot prevent valid-but-wrong JSON (e.g., objects, numbers, or non-string arrays) from breaking array operations at call sites. This is especially critical in drag-and-drop reorder logic (lines 122, 153, 166, 218, 246, 251, 289, 320, 348) where
.splice(),.filter(), or.map()are called immediately on results without type guards. Add anArray.isArray()check before returning, or implement shape validation forstring[]expectations.🤖 Prompt for AI Agents