-
-
Notifications
You must be signed in to change notification settings - Fork 113
Standardize Checklist and Note Identity on UUIDs #561
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: develop
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 |
|---|---|---|
| @@ -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"; | ||
|
|
||
| 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("/"); | ||
| } | ||
| 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
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. 🔒 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 5Repository: 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 10Repository: 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 10Repository: 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'
doneRepository: fccview/jotty Length of output: 13563 Fail closed before the checklist lookup
🤖 Prompt for AI Agents |
||
|
|
||
| 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> | ||
| ); | ||
| } | ||
| 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("/"); | ||
| } |
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.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: fccview/jotty
Length of output: 13203
🏁 Script executed:
Repository: fccview/jotty
Length of output: 2476
🏁 Script executed:
Repository: fccview/jotty
Length of output: 24989
🏁 Script executed:
Repository: fccview/jotty
Length of output: 3663
🏁 Script executed:
Repository: fccview/jotty
Length of output: 204
🏁 Script executed:
Repository: fccview/jotty
Length of output: 217
🏁 Script executed:
Repository: fccview/jotty
Length of output: 1202
🏁 Script executed:
Repository: fccview/jotty
Length of output: 4216
🏁 Script executed:
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