Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/(loggedInRoutes)/admin/checklist/[uuid]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const dynamic = "force-dynamic";
export async function generateMetadata(props: AdminChecklistPageProps): Promise<Metadata> {
const params = await props.params;
const { uuid } = params;
return getMedatadaTitle(Modes.CHECKLISTS, uuid, "Admin");
return getMedatadaTitle(Modes.CHECKLISTS, uuid);
}

export default async function AdminChecklistPage(props: AdminChecklistPageProps) {
Expand Down
2 changes: 1 addition & 1 deletion app/(loggedInRoutes)/admin/note/[uuid]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const dynamic = "force-dynamic";
export async function generateMetadata(props: AdminNotePageProps): Promise<Metadata> {
const params = await props.params;
const { uuid } = params;
return getMedatadaTitle(Modes.NOTES, uuid, "Admin");
return getMedatadaTitle(Modes.NOTES, uuid);
}

export default async function AdminNotePage(props: AdminNotePageProps) {
Expand Down
86 changes: 13 additions & 73 deletions app/(loggedInRoutes)/checklist/[...categoryPath]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,89 +1,29 @@
import { redirect } from "next/navigation";
import { getListById } from "@/app/_server/actions/checklist";
import { getCategories } from "@/app/_server/actions/category";
import { getCurrentUser, canAccessAllContent } from "@/app/_server/actions/users";
import { ChecklistClient } from "@/app/_components/FeatureComponents/Checklists/Parts/ChecklistClient";
import { redirect, permanentRedirect } from "next/navigation";
import { Modes } from "@/app/_types/enums";
import type { Metadata } from "next";
import { getMedatadaTitle } from "@/app/_server/actions/config";
import { decodeCategoryPath, decodeId } from "@/app/_utils/global-utils";
import { PermissionsProvider } from "@/app/_providers/PermissionsProvider";
import { MetadataProvider } from "@/app/_providers/MetadataProvider";
import { sanitizeUserForClient } from "@/app/_utils/user-sanitize-utils";
import { legacyResolve } from "@/app/_server/lib/legacy-lookup";
import { decodeCategoryPath } from "@/app/_utils/global-utils";
Comment on lines +1 to +4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and the legacy lookup implementation, plus related API route.
git ls-files 'app/(loggedInRoutes)/checklist/[...categoryPath]/page.tsx' \
  'app/_server/lib/legacy-lookup.ts' \
  'app/api/checklists/[listId]/items/reorder/route.ts' \
  'app/_server/actions/users.ts' \
  'app/_server/lib/*' \
  'app/api/checklists/*/items/reorder/route.ts' | sed 's#^`#-` #'

echo '--- page.tsx ---'
cat -n 'app/(loggedInRoutes)/checklist/[...categoryPath]/page.tsx' | sed -n '1,220p'

echo '--- legacy-lookup.ts ---'
cat -n 'app/_server/lib/legacy-lookup.ts' | sed -n '1,260p'

echo '--- reorder route ---'
cat -n 'app/api/checklists/[listId]/items/reorder/route.ts' | sed -n '1,260p'

echo '--- users actions ---'
cat -n 'app/_server/actions/users.ts' | sed -n '1,260p'

Repository: fccview/jotty

Length of output: 13203


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Focused inspection of the relevant functions and call sites.
grep -nE 'legacyResolve|resolveApiId|getCurrentUser|username|redirect|permanentRedirect' \
  'app/(loggedInRoutes)/checklist/[...categoryPath]/page.tsx' \
  'app/_server/lib/legacy-lookup.ts' \
  'app/api/checklists/[listId]/items/reorder/route.ts' \
  'app/_server/actions/users.ts' || true

Repository: fccview/jotty

Length of output: 2476


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate checklist canonical page and any ownership checks around it.
fd -a 'page.tsx' app | sed -n '1,200p'
echo '--- checklist route matches ---'
rg -n 'checklist/\[|legacyResolve|resolveApiId|getCurrentUser|owner|permission|auth|redirect\(' app -g '!**/node_modules/**' | sed -n '1,260p'

Repository: fccview/jotty

Length of output: 24989


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the canonical logged-in checklist page and the surrounding layout/auth behavior.
cat -n 'app/(loggedInRoutes)/checklist/[uuid]/page.tsx' | sed -n '1,220p'
echo '--- loggedIn checklist layout ---'
cat -n 'app/(loggedInRoutes)/checklist/layout.tsx' | sed -n '1,220p'
echo '--- loggedIn root layout ---'
cat -n 'app/(loggedInRoutes)/layout.tsx' | sed -n '1,220p'
echo '--- public legacy redirect page for comparison ---'
cat -n 'app/public/checklist/[...categoryPath]/page.tsx' | sed -n '1,120p'

Repository: fccview/jotty

Length of output: 3663


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether the current route is already username-scoped elsewhere in the file tree.
rg -n 'legacyResolve\(Modes\.CHECKLISTS, category, id(,| )|legacyResolve\(Modes\.CHECKLISTS, category, id,|resolveApiId\(Modes\.CHECKLISTS' app/(loggedInRoutes) app/public app/api -g '!**/node_modules/**' | sed -n '1,200p'

Repository: fccview/jotty

Length of output: 204


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the route-group layout and any auth gating for the logged-in routes.
cat -n 'app/(loggedInRoutes)/layout.tsx' | sed -n '1,220p'
echo '--- checklist route tree ---'
find 'app/(loggedInRoutes)/checklist' -maxdepth 2 -type f | sort

Repository: fccview/jotty

Length of output: 217


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the public legacy redirect page to compare behavior and scope.
cat -n 'app/public/checklist/[...categoryPath]/page.tsx' | sed -n '1,220p'

Repository: fccview/jotty

Length of output: 1202


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether the current user is loaded in the route-group layout.
rg -n 'getCurrentUser|redirect\("/auth/login"|canAccessAllContent|auth' 'app/(loggedInRoutes)' -g '!**/node_modules/**' | sed -n '1,200p'

Repository: fccview/jotty

Length of output: 4216


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n 'app/(loggedInRoutes)/layout.tsx' | sed -n '1,220p'
echo '--- public checklist legacy page ---'
cat -n 'app/public/checklist/[...categoryPath]/page.tsx' | sed -n '1,220p'
echo '--- logged-in checklist route tree ---'
find 'app/(loggedInRoutes)/checklist' -maxdepth 2 -type f | sort

Repository: fccview/jotty

Length of output: 217


Scope legacy resolution to the current user.

legacyResolve(...) scans every user's directory here, so duplicate legacy slugs can resolve to the wrong checklist UUID. Pass the signed-in username to keep the redirect within the current account.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(loggedInRoutes)/checklist/[...categoryPath]/page.tsx around lines 1 -
4, Update the legacy resolution call in this checklist page to pass the
signed-in username to legacyResolve, ensuring duplicate legacy slugs resolve
only within the current user’s account. Reuse the page’s existing
authenticated-user value and preserve the current redirect behavior.


interface ChecklistPageProps {
interface LegacyChecklistPageProps {
params: Promise<{
categoryPath: string[];
}>;
}

export const dynamic = "force-dynamic";

export async function generateMetadata(props: ChecklistPageProps): Promise<Metadata> {
export default async function LegacyChecklistPage(
props: LegacyChecklistPageProps,
) {
const params = await props.params;
const { categoryPath } = params;
const id = decodeId(categoryPath[categoryPath.length - 1]);
const encodedCategoryPath = categoryPath.slice(0, -1).join("/");
const category =
categoryPath.length === 1
? "Uncategorized"
: decodeCategoryPath(encodedCategoryPath);
const id = decodeURIComponent(categoryPath[categoryPath.length - 1]);
const category = decodeCategoryPath(categoryPath.slice(0, -1).join("/"));

return getMedatadaTitle(Modes.CHECKLISTS, id, category);
}

export default async function ChecklistPage(props: ChecklistPageProps) {
const params = await props.params;
const { categoryPath } = params;
const id = decodeId(categoryPath[categoryPath.length - 1]);
const encodedCategoryPath = categoryPath.slice(0, -1).join("/");
const category =
categoryPath.length === 1
? "Uncategorized"
: decodeCategoryPath(encodedCategoryPath);
const userRecord = await getCurrentUser();
const username = userRecord?.username || "";
const hasContentAccess = await canAccessAllContent();

const categoriesResult = await getCategories(Modes.CHECKLISTS);

let checklist = await getListById(id, username, category);
const uuid = await legacyResolve(Modes.CHECKLISTS, category, id);

if (!checklist && hasContentAccess) {
checklist = await getListById(id, undefined, category);
if (uuid) {
permanentRedirect(`/checklist/${uuid}`);
}

if (!checklist) {
redirect("/");
}

const categories =
categoriesResult.success && categoriesResult.data
? categoriesResult.data
: [];

const metadata = {
id: checklist.id,
uuid: checklist.uuid,
title: checklist.title,
category: checklist.category || "Uncategorized",
owner: checklist.owner,
createdAt: checklist.createdAt,
updatedAt: checklist.updatedAt,
type: "checklist" as const,
};

const user = sanitizeUserForClient(userRecord);

return (
<MetadataProvider metadata={metadata}>
<PermissionsProvider item={checklist}>
<ChecklistClient
checklist={checklist}
categories={categories}
user={user}
/>
</PermissionsProvider>
</MetadataProvider>
);
redirect("/");
}
92 changes: 92 additions & 0 deletions app/(loggedInRoutes)/checklist/[uuid]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { redirect, permanentRedirect } from "next/navigation";
import { getListById } from "@/app/_server/actions/checklist";
import { getCategories } from "@/app/_server/actions/category";
import { getCurrentUser, canAccessAllContent } from "@/app/_server/actions/users";
import { ChecklistClient } from "@/app/_components/FeatureComponents/Checklists/Parts/ChecklistClient";
import { Modes } from "@/app/_types/enums";
import type { Metadata } from "next";
import { getMedatadaTitle } from "@/app/_server/actions/config";
import { isUuid } from "@/app/_consts/identity";
import { UNCATEGORIZED } from "@/app/_consts/notes";
import { PermissionsProvider } from "@/app/_providers/PermissionsProvider";
import { MetadataProvider } from "@/app/_providers/MetadataProvider";
import { sanitizeUserForClient } from "@/app/_utils/user-sanitize-utils";

interface ChecklistPageProps {
params: Promise<{
uuid: string;
}>;
}

export const dynamic = "force-dynamic";

export async function generateMetadata(props: ChecklistPageProps): Promise<Metadata> {
const params = await props.params;
return getMedatadaTitle(Modes.CHECKLISTS, params.uuid);
}

export default async function ChecklistPage(props: ChecklistPageProps) {
const params = await props.params;
const { uuid } = params;

if (!isUuid(uuid)) {
const { legacyResolve } = await import("@/app/_server/lib/legacy-lookup");
const resolved = await legacyResolve(
Modes.CHECKLISTS,
UNCATEGORIZED,
decodeURIComponent(uuid),
);

if (resolved) {
permanentRedirect(`/checklist/${resolved}`);
}

redirect("/");
}

const userRecord = await getCurrentUser();
const username = userRecord?.username || "";
const hasContentAccess = await canAccessAllContent();

const categoriesResult = await getCategories(Modes.CHECKLISTS);

let checklist = await getListById(uuid, username);

if (!checklist && hasContentAccess) {
checklist = await getListById(uuid);
}

if (!checklist) {
redirect("/");
}
Comment on lines +47 to +61

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether an edge/proxy middleware enforces auth for (loggedInRoutes)
fd -e ts -e tsx 'middleware|proxy' app -x cat {}

Repository: fccview/jotty

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Relevant files =="
git ls-files 'app/(loggedInRoutes)/checklist/[uuid]/page.tsx' 'app/**/note/[uuid]/page.tsx' 'app/**/middleware.ts' 'app/**/proxy.ts' 'app/**/middleware.js' 'app/**/proxy.js' 'lib/**' 'src/**' | sed -n '1,200p'

echo
echo "== Outline checklist page =="
ast-grep outline 'app/(loggedInRoutes)/checklist/[uuid]/page.tsx' --view expanded || true

echo
echo "== Outline admin note page =="
ast-grep outline 'app/**/note/[uuid]/page.tsx' --view expanded || true

echo
echo "== Search for getListById and getCurrentUser =="
rg -n "function getListById|getListById\\(|function getCurrentUser|getCurrentUser\\(" app lib src --glob '!**/node_modules/**' -A 20 -B 5

Repository: fccview/jotty

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find auth middleware/proxy files =="
fd -a -e ts -e tsx -e js -e jsx 'middleware|proxy' app . 2>/dev/null | sed -n '1,200p'

echo
echo "== Locate getListById definition =="
rg -n "getListById" . --glob '!**/node_modules/**' -A 20 -B 10

echo
echo "== Locate getCurrentUser definition =="
rg -n "getCurrentUser" . --glob '!**/node_modules/**' -A 20 -B 10

Repository: fccview/jotty

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== checklist page =="
cat -n 'app/(loggedInRoutes)/checklist/[uuid]/page.tsx' | sed -n '1,220p'

echo
echo "== getListById implementation =="
rg -n "export const getListById|const getListById" 'app/_server/actions/checklist' -A 120 -B 20

echo
echo "== getCurrentUser implementation =="
rg -n "export const getCurrentUser|const getCurrentUser" 'app/_server/actions/users' -A 120 -B 20

echo
echo "== redirectGuards usage =="
rg -n "redirectGuards\\(" app -A 10 -B 10

Repository: fccview/jotty

Length of output: 18132


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Root auth/middleware files =="
git ls-files | rg '(^|/)(middleware|proxy)\.(ts|tsx|js|jsx)$|(^|/)layout\.(ts|tsx|js|jsx)$' | sed -n '1,200p'

echo
echo "== redirectGuards references =="
rg -n "redirectGuards" . --glob '!**/node_modules/**' -A 8 -B 8

echo
echo "== Root middleware/proxy contents =="
for f in $(git ls-files | rg '(^|/)(middleware|proxy)\.(ts|tsx|js|jsx)$'); do
  echo "--- $f ---"
  cat -n "$f" | sed -n '1,220p'
done

Repository: fccview/jotty

Length of output: 13563


Fail closed before the checklist lookup

getListById(uuid, "") drops into the unscoped UUID path, so this page should redirect as soon as getCurrentUser() is null instead of relying on the later admin fallback check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(loggedInRoutes)/checklist/[uuid]/page.tsx around lines 47 - 61, Update
the checklist page flow around getCurrentUser so it redirects immediately when
no user record is returned, before deriving username or calling getListById.
Preserve the existing authenticated lookup and hasContentAccess fallback
behavior for valid users.


const categories =
categoriesResult.success && categoriesResult.data
? categoriesResult.data
: [];

const metadata = {
id: checklist.id,
uuid: checklist.uuid,
title: checklist.title,
category: checklist.category || UNCATEGORIZED,
owner: checklist.owner,
createdAt: checklist.createdAt,
updatedAt: checklist.updatedAt,
type: "checklist" as const,
};

const user = sanitizeUserForClient(userRecord);

return (
<MetadataProvider metadata={metadata}>
<PermissionsProvider item={checklist}>
<ChecklistClient
checklist={checklist}
categories={categories}
user={user}
/>
</PermissionsProvider>
</MetadataProvider>
);
}
85 changes: 11 additions & 74 deletions app/(loggedInRoutes)/note/[...categoryPath]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,90 +1,27 @@
import { redirect } from "next/navigation";
import {
CheckForNeedsMigration,
getNoteById,
} from "@/app/_server/actions/note";
import {
getCurrentUser,
canAccessAllContent,
} from "@/app/_server/actions/users";
import { NoteClient } from "@/app/_components/FeatureComponents/Notes/NoteClient";
import { redirect, permanentRedirect } from "next/navigation";
import { Modes } from "@/app/_types/enums";
import { getCategories } from "@/app/_server/actions/category";
import type { Metadata } from "next";
import { getMedatadaTitle } from "@/app/_server/actions/config";
import { decodeCategoryPath, decodeId } from "@/app/_utils/global-utils";
import { PermissionsProvider } from "@/app/_providers/PermissionsProvider";
import { MetadataProvider } from "@/app/_providers/MetadataProvider";
import { legacyResolve } from "@/app/_server/lib/legacy-lookup";
import { decodeCategoryPath } from "@/app/_utils/global-utils";

interface NotePageProps {
interface LegacyNotePageProps {
params: Promise<{
categoryPath: string[];
}>;
}

export const dynamic = "force-dynamic";

export async function generateMetadata(props: NotePageProps): Promise<Metadata> {
export default async function LegacyNotePage(props: LegacyNotePageProps) {
const params = await props.params;
const { categoryPath } = params;
const id = decodeId(categoryPath[categoryPath.length - 1]);
const encodedCategoryPath = categoryPath.slice(0, -1).join("/");
const category =
categoryPath.length === 1
? "Uncategorized"
: decodeCategoryPath(encodedCategoryPath);
const id = decodeURIComponent(categoryPath[categoryPath.length - 1]);
const category = decodeCategoryPath(categoryPath.slice(0, -1).join("/"));

return getMedatadaTitle(Modes.NOTES, id, category);
}

export default async function NotePage(props: NotePageProps) {
const params = await props.params;
const { categoryPath } = params;
const id = decodeId(categoryPath[categoryPath.length - 1]);
const encodedCategoryPath = categoryPath.slice(0, -1).join("/");
const category =
categoryPath.length === 1
? "Uncategorized"
: decodeCategoryPath(encodedCategoryPath);
const user = await getCurrentUser();
const username = user?.username || "";
const hasContentAccess = await canAccessAllContent();

await CheckForNeedsMigration();

const categoriesResult = await getCategories(Modes.NOTES);
const uuid = await legacyResolve(Modes.NOTES, category, id);

let note = await getNoteById(id, category, username);

if (!note && hasContentAccess) {
note = await getNoteById(id, category);
}

if (!note) {
redirect("/");
if (uuid) {
permanentRedirect(`/note/${uuid}`);
}

const docsCategories =
categoriesResult.success && categoriesResult.data
? categoriesResult.data
: [];

const metadata = {
id: note.id,
uuid: note.uuid,
title: note.title,
category: note.category || "Uncategorized",
owner: note.owner,
createdAt: note.createdAt,
updatedAt: note.updatedAt,
type: "note" as const,
};

return (
<MetadataProvider metadata={metadata}>
<PermissionsProvider item={note}>
<NoteClient note={note} categories={docsCategories} />
</PermissionsProvider>
</MetadataProvider>
);
redirect("/");
}
Loading
Loading