Skip to content

Commit 9c62f02

Browse files
committed
chore: v0.11.1
- Update Next.js to v15.5.7 for latest security patches - Add file existence validation in assets API to prevent directory access - Fix text overflow issues in task titles and descriptions with break-all
1 parent 83a4421 commit 9c62f02

7 files changed

Lines changed: 128 additions & 58 deletions

File tree

apps/web/app/api/v1/assets/[...path]/route.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NextResponse } from "next/server"
2-
import { readFile } from "fs/promises"
2+
import { readFile, stat } from "fs/promises"
33
import { getSecureAssetPath } from "@/lib/utils/path-validation"
44
import { ApiErrorCode } from "@tasktrove/types/api-errors"
55
import type { ErrorResponse } from "@tasktrove/types/api-responses"
@@ -65,6 +65,16 @@ async function serveAsset(request: EnhancedRequest, path: string[]) {
6565
}
6666

6767
try {
68+
const fileStats = await stat(securePath)
69+
if (!fileStats.isFile()) {
70+
const errorResponse: ErrorResponse = {
71+
code: ApiErrorCode.ASSET_NOT_FOUND,
72+
error: "Asset not found",
73+
message: "Requested asset path is not a file",
74+
}
75+
return NextResponse.json<ErrorResponse>(errorResponse, { status: 404 })
76+
}
77+
6878
// Read the file
6979
const fileBuffer = await readFile(securePath)
7080

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest"
2+
import { NextRequest } from "next/server"
3+
import { mkdtemp, readFile } from "fs/promises"
4+
import { tmpdir } from "os"
5+
import { join } from "path"
6+
7+
import { GET } from "./[...path]/route"
8+
import { getSecureAssetPath } from "@/lib/utils/path-validation"
9+
import { createMockEnhancedRequest } from "@/lib/utils/test-helpers"
10+
11+
vi.mock("fs/promises", async () => {
12+
const actual = await vi.importActual<typeof import("fs/promises")>("fs/promises")
13+
return {
14+
...actual,
15+
readFile: vi.fn(),
16+
}
17+
})
18+
19+
vi.mock("@/lib/utils/path-validation", () => ({
20+
getSecureAssetPath: vi.fn(),
21+
}))
22+
23+
const mockGetSecureAssetPath = vi.mocked(getSecureAssetPath)
24+
25+
describe("GET /api/v1/assets when path points to a directory", () => {
26+
beforeEach(() => {
27+
vi.clearAllMocks()
28+
})
29+
30+
it("returns 404 and does not attempt to read the directory", async () => {
31+
const tempDir = await mkdtemp(join(tmpdir(), "asset-dir-"))
32+
33+
mockGetSecureAssetPath.mockReturnValue(tempDir)
34+
35+
const readFileSpy = vi.mocked(readFile)
36+
37+
const request = new NextRequest("http://localhost:3000/api/v1/assets/avatar/test")
38+
const enhancedRequest = createMockEnhancedRequest(request)
39+
const response = await GET(enhancedRequest, {
40+
params: Promise.resolve({ path: ["avatar", "test"] }),
41+
})
42+
43+
expect(response.status).toBe(404)
44+
45+
const body = await response.json()
46+
expect(body).toEqual({
47+
code: "ASSET_NOT_FOUND",
48+
error: "Asset not found",
49+
message: "Requested asset path is not a file",
50+
})
51+
52+
expect(readFileSpy).not.toHaveBeenCalled()
53+
})
54+
})

apps/web/components/task/task-item.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,7 @@ export function TaskItem({
530530
>
531531
<div className="p-2">
532532
{/* Single row layout - simplified for non-mobile only */}
533-
<div className="flex items-center gap-2">
533+
<div className="flex items-center gap-2 min-w-0">
534534
{/* Task Completion Checkbox */}
535535
<TaskCheckbox
536536
checked={task.completed}
@@ -539,7 +539,7 @@ export function TaskItem({
539539
/>
540540

541541
{/* Title */}
542-
<div className="flex-1 min-w-0 max-w-full truncate">
542+
<div className="flex-1 min-w-0 max-w-full">
543543
<LinkifiedEditableDiv
544544
as="span"
545545
value={task.title}
@@ -548,8 +548,10 @@ export function TaskItem({
548548
updateTask({ updateRequest: { id: task.id, title: newTitle.trim() } })
549549
}
550550
}}
551+
onEditingChange={setIsTitleEditing}
551552
className={cn(
552-
"text-sm block w-fit max-w-full",
553+
"text-sm block w-full min-w-0 break-all",
554+
!isTitleEditing && "line-clamp-2",
553555
task.completed ? "line-through text-muted-foreground" : "text-foreground",
554556
)}
555557
data-action="edit"
@@ -1364,7 +1366,7 @@ export function TaskItem({
13641366
className={cn(
13651367
"text-xs sm:text-sm hover:bg-accent",
13661368
"w-full sm:w-[28rem] md:w-[32rem] lg:w-[36rem] xl:w-[40rem]",
1367-
"max-w-full break-words min-w-0",
1369+
"max-w-full break-all min-w-0",
13681370
task.description
13691371
? "text-muted-foreground"
13701372
: !isDefaultDescriptionEditing
@@ -1536,7 +1538,7 @@ export function TaskItem({
15361538
{taskLabels.map((label) => (
15371539
<span
15381540
key={label.id}
1539-
className="px-1.5 py-0.5 rounded text-xs flex items-center gap-1 hover:opacity-100 truncate max-w-20 sm:max-w-none"
1541+
className="px-1.5 py-0.5 rounded text-xs flex items-center gap-1 hover:opacity-100 truncate max-w-20 sm:max-w-none break-all"
15401542
style={{
15411543
backgroundColor: label.color,
15421544
color: getContrastColor(label.color),

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "web",
3-
"version": "0.11.0",
3+
"version": "0.11.1",
44
"private": true,
55
"type": "module",
66
"scripts": {

packages/atoms/src/ui/views.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -721,15 +721,19 @@ peopleAssigneesCollapsedAtom.debugLabel = "peopleAssigneesCollapsedAtom";
721721
* Enables global persistence for hide/dismiss toggles across the app
722722
*/
723723
export const dismissedUiMapAtom = atom(
724-
(get) => get(globalViewOptionsAtom).dismissedUi,
724+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
725+
(get) => get(globalViewOptionsAtom).dismissedUi ?? {},
725726
(
726727
get,
727728
set,
728729
update:
729730
| Record<string, boolean>
730731
| ((current: Record<string, boolean>) => Record<string, boolean>),
731732
) => {
732-
const current = get(globalViewOptionsAtom).dismissedUi;
733+
// Older persisted state might not have the dismissedUi key yet, so
734+
// normalize to an empty object before reading/updating.
735+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
736+
const current = get(globalViewOptionsAtom).dismissedUi ?? {};
733737
const next = typeof update === "function" ? update(current) : update;
734738
set(updateGlobalViewOptionsAtom, { dismissedUi: next });
735739
},

pnpm-lock.yaml

Lines changed: 48 additions & 48 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ catalog:
108108
lint-staged: ^16.2.0
109109
lucide-react: ^0.544.0
110110
motion: ^12.23.24
111-
next: ^15.5.6
111+
next: ^15.5.7
112112
next-logger: ^5.0.1
113113
next-themes: ^0.4.6
114114
node-cron: ^4.2.1

0 commit comments

Comments
 (0)