feat(profile): add /profile/[did] route - #44
Conversation
Adds a canonical profile URL at `/profile/{did}` so viewing a profile
reflects its DID in the address bar. The root `/` redirects to
`/profile/{did}` (user's DID or active org's) and the URL DID is kept
in sync with the active-org context.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdded a DID-based profile page and client profile component; canonicalized authenticated root ("/") to redirect to Changes
Sequence DiagramsequenceDiagram
participant User as User/Client
participant Router as Next.js Router
participant HomeClient as HomeClient
participant Auth as Auth Context
participant Org as Org Context
participant ProfileClient as ProfileClient
participant Nav as Navbar/Sidebar
User->>Router: navigate to "/"
Router->>HomeClient: render (pathname = "/")
HomeClient->>Auth: get auth state & did
Auth-->>HomeClient: returns isAuthenticated, did, isLoading
HomeClient->>Org: get orgs, activeOrg, orgsLoading
Org-->>HomeClient: returns orgs, activeOrg, orgsLoading
HomeClient->>HomeClient: wait for auth/org loading to finish
alt unauthenticated
HomeClient->>Router: replace -> /welcome
else authenticated
HomeClient->>Router: replace -> /profile/{targetDid}
Router->>ProfileClient: render /profile/[did]
ProfileClient->>ProfileClient: decode params.did, compute effective PDS
ProfileClient->>Org: optionally switch/validate activeOrg
ProfileClient->>ProfileClient: parallel fetches (getOrgProfile, getOrgMetadata, resolveHandle, resolvePdsUrl)
ProfileClient-->>User: render loading / error / profile UI (banner, avatar, edit button)
end
User->>Nav: interact with account switcher
Nav->>Org: switchOrg(selected)
Nav->>Router: navigate -> /profile/{targetDid}
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/landing/home-client.tsx`:
- Around line 18-20: params.did is user-controlled and must have invisible
Unicode characters stripped before decoding/using; update this component to
sanitize params.did client-side by removing invisible Unicode chars (e.g.,
zero-width spaces, bidi marks) before calling decodeURIComponent and before any
comparisons or re-encoding. Locate the usage around useParams()/params and the
urlDid constant and replace the raw decode/compare flow with a sanitizedDid =
stripInvisibleChars(params.did) (apply the same helper when you later compare or
encode values for pathname redirects), and add a matching server-side sanitizer
in the corresponding API/route per the project guideline for defense-in-depth.
- Around line 44-70: The component sometimes renders the wrong profile because
the sync useEffect runs after paint; to prevent this, extend the loading guard
so the component stays in loading state while urlDid is present but not yet
resolved to the user's DID or an org: add to the existing if-check (isLoading ||
(isAuthenticated && pathname === "/")) an extra condition that returns true when
urlDid is truthy and does not match either did or activeOrg?.groupDid and there
is no matching group in groups (use the same groups.find(g => g.groupDid ===
urlDid) logic used in the useEffect); this ensures the component waits until the
urlDid sync (useEffect) can switchOrg or redirect before rendering.
In `@src/components/layout/navbar.tsx`:
- Around line 173-177: The isActive(href: string) predicate in Navbar
incorrectly omits org group edit-profile routes (e.g.
/groups/{groupDid}/edit-profile), so update the logic in the isActive function
to treat any path that ends with or includes "/edit-profile" (in addition to
"/settings/edit-profile") as part of the Profile section; refactor this matcher
into a shared helper (e.g. isProfilePath or isProfileActive) that both
src/components/layout/navbar.tsx and src/components/layout/sidebar.tsx can
import and use, ensuring the new helper checks for "/" root, "/profile/*" and
any "/.../edit-profile" patterns.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9eaa7cc2-f28a-4536-83df-b494a6407425
📒 Files selected for processing (4)
src/app/profile/[did]/page.tsxsrc/components/landing/home-client.tsxsrc/components/layout/navbar.tsxsrc/components/layout/sidebar.tsx
| const isActive = (href: string) => { | ||
| if (href === "/") return pathname === "/" || pathname === "/settings/edit-profile"; | ||
| if (href.startsWith("/profile/")) { | ||
| return pathname === "/" || pathname.startsWith("/profile/") || pathname === "/settings/edit-profile"; | ||
| } |
There was a problem hiding this comment.
Keep “Profile” active on org edit-profile screens.
When org mode clicks Edit, HomeClient routes to /groups/{groupDid}/edit-profile, but this predicate only treats /settings/edit-profile as part of the profile section. The result is that the Profile nav loses its active state on the group edit screen. The same matcher was added in src/components/layout/sidebar.tsx, so it would be worth sharing one helper for both places.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/layout/navbar.tsx` around lines 173 - 177, The isActive(href:
string) predicate in Navbar incorrectly omits org group edit-profile routes
(e.g. /groups/{groupDid}/edit-profile), so update the logic in the isActive
function to treat any path that ends with or includes "/edit-profile" (in
addition to "/settings/edit-profile") as part of the Profile section; refactor
this matcher into a shared helper (e.g. isProfilePath or isProfileActive) that
both src/components/layout/navbar.tsx and src/components/layout/sidebar.tsx can
import and use, ensuring the new helper checks for "/" root, "/profile/*" and
any "/.../edit-profile" patterns.
Prevents a flash of the wrong profile when the URL DID doesn't yet agree with activeOrg (e.g., persisted org from localStorage + visiting own DID, or visiting a group DID with no active org). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/components/landing/home-client.tsx (1)
19-20:⚠️ Potential issue | 🟠 MajorSanitize the route DID before decoding and comparing it.
params.didis still flowing intodecodeURIComponent(...)and the DID matching logic without stripping invisible Unicode first. That can make visually identical DIDs miss the self/org checks and send users down the wrong redirect path.Based on learnings: Applies to
src/{lib,app/api,components,hooks}/**/*.ts?(x): Sanitize input by stripping invisible Unicode chars client-side AND server-side (defense in depth)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/landing/home-client.tsx` around lines 19 - 20, params.did is passed into decodeURIComponent and later DID matching without removing invisible Unicode characters, causing visually identical DIDs to fail comparisons; fix by sanitizing the route DID before decoding and any comparisons: create or import a helper (e.g., stripInvisibleChars or sanitizeDid) and apply it to params.did first, then decodeURIComponent on the sanitized value to produce urlDid, and use the sanitized/normalized DID in the self/org matching logic (functions/variables to update: useParams, params, urlDid and any subsequent DID comparison code). Ensure the same sanitizer is applied server-side too for defense-in-depth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/landing/home-client.tsx`:
- Around line 59-63: The redirect for unknown-profile routes currently computes
selfDid as activeOrg?.groupDid || did which can wrongly send users to a
persisted org profile; change the fallback to use the user's canonical DID (did)
instead of activeOrg?.groupDid so that when urlDid !== did the code calls
router.replace with encodeURIComponent(did). Update the logic around
selfDid/urlDid and the router.replace call in the same block (symbols: selfDid,
activeOrg, did, urlDid, router.replace) to ensure the user's own profile is used
for the unknown-route fallback.
---
Duplicate comments:
In `@src/components/landing/home-client.tsx`:
- Around line 19-20: params.did is passed into decodeURIComponent and later DID
matching without removing invisible Unicode characters, causing visually
identical DIDs to fail comparisons; fix by sanitizing the route DID before
decoding and any comparisons: create or import a helper (e.g.,
stripInvisibleChars or sanitizeDid) and apply it to params.did first, then
decodeURIComponent on the sanitized value to produce urlDid, and use the
sanitized/normalized DID in the self/org matching logic (functions/variables to
update: useParams, params, urlDid and any subsequent DID comparison code).
Ensure the same sanitizer is applied server-side too for defense-in-depth.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9559350b-679c-40e2-bd38-612cb0c6cd91
📒 Files selected for processing (1)
src/components/landing/home-client.tsx
…only
Removes the reverse-sync effect that read the URL DID and forced
switchOrg/redirect, and the related loading guard. The URL is now
just a display of the current context's DID — navigating via the
account switcher updates the URL, and `/` still canonicalizes to
`/profile/{did}`. Keeps orgsLoading guard so a stale persisted org
doesn't become a bad URL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces ProfileClient that fetches the profile record + org metadata + handle + PDS URL directly from the DID in the URL, so the page works for any DID (user or group) regardless of the viewer's activeOrg. The "Edit" button only shows when viewing your own DID or a group you're an owner/admin of. HomeClient at / is now a redirector only — the profile view itself lives entirely at /profile/:did. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/components/profile/profile-client.tsx (1)
69-75: Type assertion bypasses type safety.Casting
profiletoCertifiedProfileassumes the shape matches without runtime validation. IfOrgProfileandCertifiedProfilediverge, this could cause runtime errors ingetAvatarUrl/getBannerUrl.Consider adding a type guard or ensuring
OrgProfileextends the required shape, or update the helper functions to acceptOrgProfiledirectly.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/profile/profile-client.tsx` around lines 69 - 75, The code currently casts profile to CertifiedProfile when computing avatarUrl/bannerUrl which bypasses type safety; add a type guard like isCertifiedProfile(profile): profile is CertifiedProfile and use it to conditionally call getAvatarUrl/getBannerUrl (passing profile only after the guard), or alternatively update the getAvatarUrl/getBannerUrl signatures to accept OrgProfile | CertifiedProfile and handle both shapes internally; reference effectivePdsUrl, avatarUrl, bannerUrl, getAvatarUrl, getBannerUrl, CertifiedProfile, OrgProfile and did when making the change so the calls only pass validated types.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/profile/profile-client.tsx`:
- Around line 237-248: The metadata.urls array is rendered directly into anchor
hrefs (metadata.urls.map...) which allows untrusted schemes like "javascript:";
update the rendering in the ProfileClient component so each url is
validated/sanitized before use (reuse your existing URL validation utility e.g.
isValidUrl or sanitizeUrl if present, or implement a whitelist check for
http/https/mailto/tel). For each entry in metadata.urls, compute a safeHref and
only render the <a> when safeHref passes validation (otherwise skip or render
plain text), keeping target="_blank" and rel="noopener noreferrer" intact.
- Around line 18-22: The URL-derived DID in ProfileClient (const params, const
did, using decodeURIComponent) must be sanitized to strip invisible Unicode
characters before decoding and any comparisons with currentUserDid; update the
params.did handling in ProfileClient to first remove invisible/zero-width chars
(e.g., using a shared helper like sanitizeInput or a small normalize function)
then decodeURIComponent and use that cleaned value for comparisons and
downstream logic; also ensure you call the same sanitizer on server-side entry
points per the project's defense-in-depth guideline.
- Around line 196-204: The anchor uses user-provided profile.website directly;
validate/sanitize it before rendering by checking the URL protocol and only
allowing http:// or https:// (reject or normalize others). In the render branch
that references profile.website and the anchor with className
"personal-info__field--link", create a safeHref (e.g., parse with the URL
constructor inside try/catch and ensure protocol is "http:" or "https:"), and
only render the <a href={safeHref}> when safeHref is valid (or else render plain
text or omit the link).
---
Nitpick comments:
In `@src/components/profile/profile-client.tsx`:
- Around line 69-75: The code currently casts profile to CertifiedProfile when
computing avatarUrl/bannerUrl which bypasses type safety; add a type guard like
isCertifiedProfile(profile): profile is CertifiedProfile and use it to
conditionally call getAvatarUrl/getBannerUrl (passing profile only after the
guard), or alternatively update the getAvatarUrl/getBannerUrl signatures to
accept OrgProfile | CertifiedProfile and handle both shapes internally;
reference effectivePdsUrl, avatarUrl, bannerUrl, getAvatarUrl, getBannerUrl,
CertifiedProfile, OrgProfile and did when making the change so the calls only
pass validated types.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 585823d2-c723-44c3-aea5-57c94d88640c
📒 Files selected for processing (3)
src/app/profile/[did]/page.tsxsrc/components/landing/home-client.tsxsrc/components/profile/profile-client.tsx
✅ Files skipped from review due to trivial changes (1)
- src/app/profile/[did]/page.tsx
| export default function ProfileClient() { | ||
| const params = useParams(); | ||
| const did = typeof params?.did === "string" ? decodeURIComponent(params.did) : ""; | ||
| const { did: currentUserDid } = useAuth(); | ||
| const { groups } = useOrg(); |
There was a problem hiding this comment.
Sanitize params.did before use.
The DID extracted from URL params is user-controlled and should have invisible Unicode characters stripped before decoding and comparison. This aligns with the defense-in-depth requirement.
As per coding guidelines: src/{lib,app/api,components,hooks}/**/*.ts?(x): Sanitize input by stripping invisible Unicode chars client-side AND server-side (defense in depth)
🛡️ Proposed fix
+import { stripInvisibleChars } from "@/lib/utils/sanitize";
+
export default function ProfileClient() {
const params = useParams();
- const did = typeof params?.did === "string" ? decodeURIComponent(params.did) : "";
+ const rawDid = typeof params?.did === "string" ? params.did : "";
+ const did = stripInvisibleChars(decodeURIComponent(rawDid));
const { did: currentUserDid } = useAuth();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/profile/profile-client.tsx` around lines 18 - 22, The
URL-derived DID in ProfileClient (const params, const did, using
decodeURIComponent) must be sanitized to strip invisible Unicode characters
before decoding and any comparisons with currentUserDid; update the params.did
handling in ProfileClient to first remove invisible/zero-width chars (e.g.,
using a shared helper like sanitizeInput or a small normalize function) then
decodeURIComponent and use that cleaned value for comparisons and downstream
logic; also ensure you call the same sanitizer on server-side entry points per
the project's defense-in-depth guideline.
| {profile?.website ? ( | ||
| <a | ||
| href={profile.website} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="personal-info__field--link" | ||
| > | ||
| {profile.website} | ||
| </a> |
There was a problem hiding this comment.
Validate external URL protocol before rendering.
profile.website is user-provided and rendered directly in an anchor's href. Malicious values like javascript: or data: URIs could execute code. Consider validating that the URL starts with https:// or http://.
🛡️ Proposed fix
+const isValidHttpUrl = (url: string) => {
+ try {
+ const parsed = new URL(url);
+ return parsed.protocol === "https:" || parsed.protocol === "http:";
+ } catch {
+ return false;
+ }
+};
+
// In render:
-{profile?.website ? (
+{profile?.website && isValidHttpUrl(profile.website) ? (
<a
href={profile.website}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/profile/profile-client.tsx` around lines 196 - 204, The anchor
uses user-provided profile.website directly; validate/sanitize it before
rendering by checking the URL protocol and only allowing http:// or https://
(reject or normalize others). In the render branch that references
profile.website and the anchor with className "personal-info__field--link",
create a safeHref (e.g., parse with the URL constructor inside try/catch and
ensure protocol is "http:" or "https:"), and only render the <a href={safeHref}>
when safeHref is valid (or else render plain text or omit the link).
| {metadata.urls.map((u, i) => ( | ||
| <span key={i}> | ||
| {i > 0 && " · "} | ||
| <a | ||
| href={u.url} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="personal-info__field--link" | ||
| > | ||
| {u.label || u.url} | ||
| </a> | ||
| </span> |
There was a problem hiding this comment.
Apply the same URL protocol validation to metadata links.
The metadata.urls[].url values are also user-provided and should be validated before rendering in anchor tags to prevent javascript: or other malicious URI schemes.
🛡️ Proposed fix
{metadata.urls.map((u, i) => (
- <span key={i}>
+ <span key={u.url || i}>
{i > 0 && " · "}
+ {isValidHttpUrl(u.url) ? (
<a
href={u.url}
target="_blank"
rel="noopener noreferrer"
className="personal-info__field--link"
>
{u.label || u.url}
</a>
+ ) : (
+ <span>{u.label || u.url}</span>
+ )}
</span>
))}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/profile/profile-client.tsx` around lines 237 - 248, The
metadata.urls array is rendered directly into anchor hrefs
(metadata.urls.map...) which allows untrusted schemes like "javascript:"; update
the rendering in the ProfileClient component so each url is validated/sanitized
before use (reuse your existing URL validation utility e.g. isValidUrl or
sanitizeUrl if present, or implement a whitelist check for
http/https/mailto/tel). For each entry in metadata.urls, compute a safeHref and
only render the <a> when safeHref passes validation (otherwise skip or render
plain text), keeping target="_blank" and rel="noopener noreferrer" intact.
Adds a canonical profile URL at
/profile/{did}so viewing a profile reflects its DID in the address bar. The root/redirects to/profile/{did}(user's DID or active org's) and the URL DID is kept in sync with the active-org context.Summary by CodeRabbit
New Features
Bug Fixes