Skip to content

feat(bio): Phase 4 — bio self-edit UI at /[lang]/my-bio (closes #2)#174

Merged
JohnRDOrazio merged 5 commits into
mainfrom
feat/bio-editor-ui
Jun 5, 2026
Merged

feat(bio): Phase 4 — bio self-edit UI at /[lang]/my-bio (closes #2)#174
JohnRDOrazio merged 5 commits into
mainfrom
feat/bio-editor-ui

Conversation

@JohnRDOrazio
Copy link
Copy Markdown
Member

@JohnRDOrazio JohnRDOrazio commented Jun 5, 2026

Summary

Final phase of issue #2. Adds the TipTap-based bio editor UI on top of the bearer validator + Auth.js wiring + REST endpoints from #172. Closes the issue.

What lands

Pages + routes

Path Purpose
app/[lang]/my-bio/page.tsx Server component. requireSession() → fetch discovery → pick initial language (Zitadel locale → URL locale → first available) → fetch current content → render editor. Friendly localized fallbacks for no-link / no-langs / load-error.
app/api/my-bio/check/route.ts GET → {linked, available_languages}. Used by AuthButton; collapses all "not really linked" paths (anon / WP 403 / no link) to {linked: false} so the dropdown never blinks an error.
app/api/my-bio/load/[lang]/route.ts GET → editable content for the lang. Resolves post_id server-side from discovery — client never picks which post it loads.
app/api/my-bio/save/route.ts POST → forwards PATCH to WP with bearer attached server-side. Access token never reaches the browser.

Components

  • components/BioEditor.tsx — Client component. TipTap StarterKit + Link (rel=noopener, target=_blank, no autolink). Editable: post_content, member_title, member_linkedin_url, member_github_url. Read-only: post_title (the team member's name — admin-managed). Language <select> triggers /api/my-bio/load/{lang} with an unsaved-changes confirm() guard. Save disabled until isDirty.
  • components/AuthButton.tsx — adds a one-shot fire-and-forget /api/my-bio/check on transitioning to authenticated. Conditional "Edit my bio" entry rendered between the email block and the sign-out button.

Shared lib

  • lib/bio-api.ts — server-only helpers (fetchMyTeamMember, fetchTeamMemberPost, saveMyTeamMember) reused by the server page + all three route handlers. BioApiError class carries WP status + code through to the Next.js error path. import 'server-only' preserved for the bundler-level safety net (vitest stubs it).

i18n

messages/{en,it,es,fr,pt,de}.json:

  • Auth.editMyBio (one new key for the header dropdown)
  • MyBio.* (17 new keys: page chrome, form labels, save status, unsaved-changes confirm, three error fallbacks)

All edits applied via append-only Python splice to avoid whole-file reformatting.

Tests

11 new vitest tests in lib/bio-api.test.ts covering:

  • Bearer attachment + payload parsing for the discovery call
  • 401 fallthrough when the session has no access token
  • 403 / 404 / 500 (config_missing) error shapes
  • /wp/v2/team_member/{id}?context=edit URL + raw → rendered normalisation
  • PATCH method + body + headers
  • Malformed lang rejected before network
  • rest_invalid_url 400 and rest_forbidden 403 surfaced

Vitest now also resolves server-only to a stub + the @/ alias (config tweak in vitest.config.ts).

Verification:

next build:        clean, 4 new routes registered
npm run lint:      clean (no setState-in-effect cascades)
npm test:          145 / 145  (was 134 — +11 bio-api)
npm run lint:md:   0 errors
prettier check:    clean

Deliberately out of scope

Locked decisions from the plan held:

  • Featured-image edits — deferred to a separate issue (decision Bump dompurify from 3.3.1 to 3.3.3 in the npm_and_yarn group across 1 directory #4).
  • Role gating beyond author_team_member link presence — the Zitadel team_member role is wired but not yet enforced; the link itself is the canonical ownership signal that's already on by default for every linked user.
  • Optimistic UI / persistent toasts — the inline status banner is enough for MVP.

Deploy notes

  • This is a frontend-only PR (no theme/handler changes), so gh workflow run deploy.yml with default staging is sufficient. Production deploy happens via the next release.
  • Both AUTH_* env vars (on the frontend) and CDCF_ZITADEL_EXPECTED_AUD (in wp-config.php on the shared WP backend) must be configured per cdcf-infra handoff before the editor will actually authenticate. Without them, sign-in either fails at the OIDC redirect (frontend) or every PATCH 403s at WP's bearer validator.

Test plan

  • Sign in via Zitadel → header shows email dropdown
  • Authenticated user with author_team_member link → "Edit my bio" appears in dropdown
  • Authenticated user without link → "Edit my bio" hidden
  • /my-bio page redirects unauthenticated users to /api/auth/signin
  • Page loads with the user's Zitadel locale's bio pre-populated (or URL locale fallback)
  • Switching language with no unsaved changes → seamless content swap
  • Switching language with unsaved changes → confirm dialog
  • Save with no changes → button disabled
  • Save with changes → success status + "translations queued for it/es/fr/pt/de" banner
  • Bad URL (e.g. https://gitlab.com/me in GitHub field) → 400 surfaced inline
  • About page renders updated bio after re-translation worker completes

Summary by CodeRabbit

  • New Features

    • Authenticated users can edit their multilingual bio with a rich-text editor, update position/LinkedIn/GitHub, switch languages, and see save status.
  • UI

    • “Edit my bio” added to the auth dropdown; unsaved-change confirmation when switching languages.
  • API / Backend

    • New endpoints support loading, checking, and saving bio content with validation and sanitization.
  • Localization

    • Added MyBio translations for multiple locales.
  • Tests

    • Expanded tests covering bio API behavior.
  • Bug Fix

    • Improved sign-in redirect origin fallback.

Final phase of the cdcf-bio-edit-zitadel plan. Adds the TipTap-based
editor backed by the Phase 3 REST endpoints. Authenticated team
members can now click "Edit my bio" in the header dropdown, land on a
language-switchable form, write in any of their available locales,
and trigger the re-translation fan-out via Save.

New surface
-----------
- app/[lang]/my-bio/page.tsx       Server component. requireSession() →
                                   fetchMyTeamMember() (discovery) →
                                   pick initial lang (Zitadel locale →
                                   URL locale → first available) →
                                   fetchTeamMemberPost() → render the
                                   editor. Friendly fallbacks for the
                                   no-link / no-langs / load-error
                                   paths surface localized copy
                                   instead of internal errors.

- components/BioEditor.tsx         Client. TipTap StarterKit + Link
                                   (rel=noopener, target=_blank, no
                                   autolink). Editable: post_content,
                                   member_title, member_linkedin_url,
                                   member_github_url. Read-only:
                                   post_title (the team member's name —
                                   admin-managed). Language <select>
                                   triggers /api/my-bio/load/{lang}
                                   with an unsaved-changes
                                   window.confirm() guard. Save is
                                   disabled until isDirty AND wired up
                                   so editing again clears the prior
                                   status banner via a shared
                                   markDirty() callback (no
                                   setState-in-effect cascade).

- app/api/my-bio/check/route.ts    GET → {linked, available_languages}.
                                   Used by AuthButton to decide whether
                                   to render the "Edit my bio" entry.
                                   Anon / not-linked / 403 from WP all
                                   collapse to {linked: false} so the
                                   dropdown never blinks an error.

- app/api/my-bio/load/[lang]/      GET → editable post content for the
  route.ts                         language. Resolves post_id
                                   server-side from the discovery
                                   payload (the client never gets to
                                   pick which post it loads).

- app/api/my-bio/save/route.ts     POST → forwards the PATCH to WP
                                   with the bearer attached server-
                                   side. The access token never reaches
                                   the browser. Surfaces BioApiError
                                   shape back as JSON.

- lib/bio-api.ts                   Server-only helpers reused by the
                                   page + all three route handlers:
                                   fetchMyTeamMember,
                                   fetchTeamMemberPost (context=edit,
                                   raw → rendered fallback), and
                                   saveMyTeamMember. BioApiError class
                                   carries WP status + code into the
                                   Next.js error path. `server-only`
                                   guard preserved for the bundler-
                                   level safety net.

Header dropdown
---------------
- components/AuthButton.tsx        Adds a one-shot fire-and-forget
                                   /api/my-bio/check on transitioning
                                   to authenticated. Stale state never
                                   leaks because the menu is gated by
                                   session.user; on sign-out the menu
                                   disappears entirely so a stale
                                   hasBioLink=true is invisible.

i18n
----
- messages/{en,it,es,fr,pt,de}.json
  + Auth.editMyBio (header dropdown entry)
  + MyBio.* (17 keys covering page chrome, form labels, save status,
              unsaved-changes confirm, and three error fallbacks)
- All edits applied via append-only Python splice (no whole-file
  reformatting) to keep diffs minimal.

Vitest setup
------------
- vitest.config.ts adds a `server-only` alias to a no-op stub plus an
  `@/` resolver so library tests can import server modules without the
  real `server-only` package's runtime throw.
- tests/stubs/server-only.ts is the stub.

Tests added (11 new in lib/bio-api.test.ts; 145 total vitest)
- fetchMyTeamMember: attaches bearer + parses payload; 401 when no
  access token on session; surfaces WP 403; throws 500 when
  WP_REST_URL unset.
- fetchTeamMemberPost: hits /wp/v2 with context=edit, normalises
  raw → rendered, surfaces 404.
- saveMyTeamMember: PATCH method + body shape; rejects malformed lang
  before network; surfaces WP rest_invalid_url 400 + rest_forbidden
  403.

Deliberately out of scope (consistent with the locked plan)
-----------------------------------------------------------
- Featured image edits (separate issue per locked decision #4).
- Server-side role gating beyond presence of the
  author_team_member link — the Zitadel `team_member` role is
  available but not yet enforced; the link itself is the canonical
  ownership signal.
- Optimistic UI / persistent toasts beyond the inline status banner.

Verification
- next build:        clean, 4 new routes registered
- npm run lint:      clean (no setState-in-effect cascades)
- npm test:          145 / 145 (was 134 — +11 bio-api)
- npm run lint:md:   0 errors
- prettier check:    clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 5, 2026

Review Change Stack

Warning

Review limit reached

@JohnRDOrazio, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 33 minutes and 22 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a47daea-c414-4ec4-9302-a8fdd8710578

📥 Commits

Reviewing files that changed from the base of the PR and between 458e14d and 3fcd935.

📒 Files selected for processing (1)
  • app/api/my-bio/check/route.ts
📝 Walkthrough

Walkthrough

This PR adds a complete "bio self-edit" feature enabling team members to edit their multilingual profiles. It introduces server-side API helpers for WordPress REST integration, three HTTP routes exposing those helpers, a server page to render the editor, a Tiptap-powered client editor component, auth dropdown integration, and full multilingual support with tests.

Changes

Team Member Bio Self-Edit Feature

Layer / File(s) Summary
Bio API types and server helpers
lib/bio-api.ts
Exports BioLanguage, BioDiscovery, BioPostContent, BioSaveResponse, and BioApiError; implements fetchMyTeamMember, fetchTeamMemberPost, and saveMyTeamMember with WP URL/auth helpers, JSON/error mapping, and normalization.
Bio API test suite
lib/bio-api.test.ts
Vitest coverage for helper requests, bearer header behavior, JSON normalization, lang validation, and BioApiError mapping for auth/config/WP responses.
HTTP route handlers
app/api/my-bio/check/route.ts, app/api/my-bio/load/[lang]/route.ts, app/api/my-bio/save/route.ts
GET /check returns soft-fail linked state for UI, GET /load/{lang} validates lang and returns editable post content, POST /save enforces auth, validates/parses JSON body, allow-lists payload fields, and forwards to save helper with error mapping.
Server page for bio editing
app/[lang]/my-bio/page.tsx
Authenticates user (redirects if unauthenticated), fetches discovery and initial post, selects initial language (claim → URL → first available), handles missing/forbidden/404 cases with user-facing UI, and renders BioEditor with initial props.
Client editor component
components/BioEditor.tsx
Tiptap-based client editor managing HTML content and member fields, dirty/loading/saving state, language switching with unsaved-change confirmation and load call, save submission to /api/my-bio/save, and status UI.
Auth button dropdown integration
components/AuthButton.tsx
After sign-in, calls /api/my-bio/check to determine linkage and conditionally shows an “Edit my bio” menu entry linking to /my-bio.
Internationalization
messages/en.json, messages/de.json, messages/es.json, messages/fr.json, messages/it.json, messages/pt.json
Adds Auth.editMyBio and a MyBio namespace across locales with labels, prompts, save states, and error messages for the editor flow.
Build & test config
package.json, tests/stubs/server-only.ts, vitest.config.ts, .env.local.example
Adds Tiptap deps (@tiptap/starter-kit, @tiptap/react, @tiptap/extension-link, @tiptap/pm), creates a server-only test stub and Vitest aliases, and documents an optional AUTH_URL env fallback.
Auth env fallback
lib/auth.ts
Promotes NEXT_PUBLIC_SITE_URL into AUTH_URL at runtime when AUTH_URL is unset to stabilize Auth.js redirect URI origin handling.

Sequence Diagram

sequenceDiagram
  participant User
  participant MyBioPage
  participant BioEditor
  participant LoadAPI as /api/my-bio/load/{lang}
  participant SaveAPI as /api/my-bio/save
  participant WordPress as WordPressREST

  User->>MyBioPage: open /[lang]/my-bio (server-page)
  MyBioPage->>LoadAPI: resolve discovery -> post_id -> fetch initial post
  LoadAPI->>WordPress: GET /wp/v2/team_member/{postId}?context=edit
  WordPress-->>LoadAPI: Post JSON
  MyBioPage-->>User: render BioEditor(initialPost, availableLanguages)

  User->>BioEditor: change language
  BioEditor->>LoadAPI: GET selected lang
  LoadAPI->>WordPress: GET post
  WordPress-->>LoadAPI: Post JSON
  LoadAPI-->>BioEditor: BioPostContent

  User->>BioEditor: click Save
  BioEditor->>SaveAPI: POST {lang, content, member_*}
  SaveAPI->>WordPress: PATCH /cdcf/v1/my-team-member/{lang}
  WordPress-->>SaveAPI: Save response / errors
  SaveAPI-->>BioEditor: queued or error JSON
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • CatholicOS/cdcf-website#172: Implements the WordPress REST endpoints and bearer auth that the new bio API helpers and routes consume and map errors from.

Poem

🐰 A rabbit hops through bios bright,
With Tiptap's keys, the edits take flight,
Languages singing, fields set right,
Save queued softly into the night. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(bio): Phase 4 — bio self-edit UI at /[lang]/my-bio (closes #2)' directly and accurately summarizes the main change: introducing a bio self-edit user interface at the specified route as the final phase of issue #2.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/bio-editor-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented Jun 5, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 142 complexity · 6 duplication

Metric Results
Complexity 142
Duplication 6

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
package.json (1)

23-26: ⚡ Quick win

Tiptap versions/security look OK; consider dropping @tiptap/pm if unused

  • @tiptap/extension-link, @tiptap/pm, @tiptap/react, and @tiptap/starter-kit all exist at 3.26.0 on npm (as pinned in package.json lines 23–26).
  • GitHub security advisories show no issues for @tiptap/pm, @tiptap/react, or @tiptap/starter-kit; the @tiptap/extension-link XSS advisory applies to < 2.10.4, so 3.26.0 is outside the vulnerable range.
  • Repo-wide search finds no direct imports/require of @tiptap/pm (only the dependency entry in package.json), so it can likely be removed to simplify dependency management if nothing depends on it transitively.
🤖 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 `@package.json` around lines 23 - 26, package.json declares "`@tiptap/pm`" at
version "^3.26.0" but the repo search shows no imports/requires of that package;
remove the "`@tiptap/pm`" dependency entry from package.json (the unique symbol to
locate is the "`@tiptap/pm`" key), then run npm/yarn install and the test suite
and optionally npm audit / npm ls `@tiptap/pm` to ensure nothing else depends on
it transitively; if any consumer requires it, restore or add a comment
explaining why it must remain.
components/BioEditor.tsx (1)

176-180: ⚡ Quick win

Replace <label> with <div> for read-only display.

The <label> element should only be used for form controls. Since the post title is read-only text (not an input), use a <div> or <span> instead.

♻️ Proposed fix
         <div>
-          <label className="block text-xs font-medium uppercase tracking-wide text-gray-500">
+          <div className="block text-xs font-medium uppercase tracking-wide text-gray-500">
             {t('postTitleLabel')}
-          </label>
+          </div>
           <p className="mt-1 text-base">{currentTitle}</p>
         </div>
🤖 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 `@components/BioEditor.tsx` around lines 176 - 180, In the BioEditor component
replace the semantically incorrect <label> used for read-only text with a
non-form container: change the <label className="block text-xs font-medium
uppercase tracking-wide text-gray-500"> that renders {t('postTitleLabel')} to a
<div> (or <span>) preserving the same className and text; ensure the surrounding
structure that displays {currentTitle} remains unchanged so styling and layout
are preserved.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@app/`[lang]/my-bio/page.tsx:
- Line 32: The variable `discovery` is declared without a type; replace `let
discovery` with an explicit annotation such as `let discovery:
Awaited<ReturnType<typeof getDiscovery>> | undefined` (or `Discovery |
undefined` if you have a `Discovery` interface/type) — if you use a different
fetch helper, substitute `getDiscovery` with that function so the type is
inferred from its return type and avoids `any`.

In `@app/api/my-bio/save/route.ts`:
- Around line 25-30: The handler currently spreads body into payload without
runtime validation (symbols: lang, payload, BioSavePayload) before calling
saveMyTeamMember; add explicit runtime validation: ensure lang is string
(already done), then whitelist expected payload keys (e.g., content and any
other allowed fields) and check each required/optional field has the correct
type (e.g., typeof content === 'string'), reject requests with extra keys or
wrong types by returning a 400 JSON error, and only pass the sanitized object to
saveMyTeamMember.

---

Nitpick comments:
In `@components/BioEditor.tsx`:
- Around line 176-180: In the BioEditor component replace the semantically
incorrect <label> used for read-only text with a non-form container: change the
<label className="block text-xs font-medium uppercase tracking-wide
text-gray-500"> that renders {t('postTitleLabel')} to a <div> (or <span>)
preserving the same className and text; ensure the surrounding structure that
displays {currentTitle} remains unchanged so styling and layout are preserved.

In `@package.json`:
- Around line 23-26: package.json declares "`@tiptap/pm`" at version "^3.26.0" but
the repo search shows no imports/requires of that package; remove the
"`@tiptap/pm`" dependency entry from package.json (the unique symbol to locate is
the "`@tiptap/pm`" key), then run npm/yarn install and the test suite and
optionally npm audit / npm ls `@tiptap/pm` to ensure nothing else depends on it
transitively; if any consumer requires it, restore or add a comment explaining
why it must remain.
🪄 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: a4369129-dc9c-4fb2-a057-7ff155d5c780

📥 Commits

Reviewing files that changed from the base of the PR and between a6a12e4 and 4338b4d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (17)
  • app/[lang]/my-bio/page.tsx
  • app/api/my-bio/check/route.ts
  • app/api/my-bio/load/[lang]/route.ts
  • app/api/my-bio/save/route.ts
  • components/AuthButton.tsx
  • components/BioEditor.tsx
  • lib/bio-api.test.ts
  • lib/bio-api.ts
  • messages/de.json
  • messages/en.json
  • messages/es.json
  • messages/fr.json
  • messages/it.json
  • messages/pt.json
  • package.json
  • tests/stubs/server-only.ts
  • vitest.config.ts

Comment thread app/[lang]/my-bio/page.tsx Outdated
Comment thread app/api/my-bio/save/route.ts Outdated
Three findings verified valid, one skipped with reason.

[HIGH, no-implicit-any] app/[lang]/my-bio/page.tsx
  `let discovery` was inferred as `any`. Added the BioDiscovery type
  import and an explicit annotation `let discovery: BioDiscovery`.

[HIGH, jsx-a11y/label-has-associated-control] components/BioEditor.tsx
  The "Name" label for the read-only display had no associated input
  (the team member's display name is admin-managed, so there's no
  control to label). Swapped <label> for <span> with the same
  className and a comment explaining the choice.

[best practice, input validation] app/api/my-bio/save/route.ts
  Previous handler spread the JSON body into `payload` via `{lang,
  ...payload}` and trusted the TS cast. Replaced with an
  ALLOWED_PAYLOAD_FIELDS allow-list constant + a per-field type check:
  unknown keys are silently dropped (forward-compat friendly), allow-
  listed keys present with a non-string value reject the whole
  request with HTTP 400 + {error: 'invalid_field', field}. WP-side
  sanitize_callbacks still run downstream — this is the first
  validation checkpoint.

[skipped, package.json — @tiptap/pm unused]
  CodeRabbit flagged @tiptap/pm as unused because no source file
  imports it directly. The package IS a required peer dependency of
  @tiptap/react, @tiptap/starter-kit, @tiptap/core, and
  @tiptap/extension-link (verified via the peerDependencies blocks in
  each package.json). Removing it would trigger peer-dep warnings and
  break runtime ProseMirror plugin resolution. Keeping the explicit
  install entry is the documented TipTap pattern. No change.

Verification
- next build:        clean, 4 routes registered
- npm run lint:      clean
- npm test:          145 / 145
- npm run lint:md:   0 errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JohnRDOrazio
Copy link
Copy Markdown
Member Author

Codacy + CodeRabbit findings addressed in 902bf71. Three fixed, one skipped with reason.

Tool / Severity File Status
Codacy HIGH / CodeRabbit app/[lang]/my-bio/page.tsx L32 ✅ Added BioDiscovery type import + explicit let discovery: BioDiscovery annotation
Codacy HIGH / CodeRabbit nitpick components/BioEditor.tsx L176 <label><span> for the read-only "Name" display (no editable control to label — display name is admin-managed). Added explanatory comment.
CodeRabbit app/api/my-bio/save/route.ts L25 ✅ Replaced {lang, ...payload} spread + TS cast with ALLOWED_PAYLOAD_FIELDS allow-list + per-field type check. Unknown keys silently dropped (forward-compat); allow-listed keys with wrong type → HTTP 400 {error: 'invalid_field', field}.
CodeRabbit nitpick package.json @tiptap/pm ⏭️ Skipped. @tiptap/pm is a required peer dependency of @tiptap/react, @tiptap/starter-kit, @tiptap/core, and @tiptap/extension-link — verified via the peerDependencies blocks in each node_modules/@tiptap/*/package.json. Removing the explicit install entry would trigger peer-dep warnings and break runtime ProseMirror plugin resolution. Keeping it is the documented TipTap install pattern.

Note on the stale TS diagnostics: the system reminder also listed several Cannot find module '@/lib/auth', '@/lib/bio-api' errors plus 'err' is of type 'unknown' warnings. npx tsc --noEmit is clean on the branch — those diagnostics were captured during a mainfeat/bio-editor-ui checkout shuffle in the IDE and weren't reflective of the actual codebase state.

Verification:

next build:        clean, 4 routes registered
npm run lint:      clean
npm test:          145 / 145
npm run lint:md:   0 errors

@JohnRDOrazio
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 5, 2026

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

JohnRDOrazio and others added 2 commits June 5, 2026 13:48
…ssenger bind-address leak

Sign-in on staging produced an OIDC redirect_uri of
https://0.0.0.0:3000/api/auth/callback/zitadel, which Zitadel
rejected: "invalid_request: requested redirect_uri is missing in the
client configuration." Same root cause as the existing locale-redirect
Location-header leak (memory project_plesk_passenger_port_leak): Next
standalone under Phusion Passenger surfaces its bind address
(0.0.0.0:3000) to the app — and unlike Express et al., Next.js does
NOT honor X-Forwarded-Host / X-Forwarded-Port here, so trustHost:true
alone isn't enough to fix Auth.js's request.url-derived callback URL.

Auth.js v5 reads process.env.AUTH_URL first when constructing the
callback. We already configure NEXT_PUBLIC_SITE_URL per-environment
at build time AND it's available at runtime in Plesk's app env
(per-domain Settings). Promote it to AUTH_URL at lib/auth.ts module
load time so the operator doesn't have to set a second env var:

  if (!process.env.AUTH_URL && process.env.NEXT_PUBLIC_SITE_URL) {
    process.env.AUTH_URL = process.env.NEXT_PUBLIC_SITE_URL
  }

This runs BEFORE the NextAuth({}) call, so Auth.js picks up the value
on first request. Doesn't override an explicit AUTH_URL if one is
deliberately set (e.g. behind multiple proxies where SITE_URL and the
public origin differ).

.env.local.example gets a commented-out AUTH_URL stub + explanation
of the auto-promotion so deployers know the fallback exists.

Memory update: project_plesk_passenger_port_leak.md gains the sibling-
symptom section so the next person hitting the OIDC version of this
finds the connection back to the locale-redirect case in proxy.ts.

Verification
- next build: clean
- npm run lint: clean
- npm test: 145 / 145

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_GRAPHQL_URL

Staging surfaced a 500 from /api/my-bio/check after a user signed in
but wasn't yet linked to a team_member post. Two issues compounded:

1. The check route only collapsed BioApiError 401/403 to
   {linked: false}; any other status (500 from a missing env var,
   network errors that escape BioApiError entirely) propagated as a
   500 to the browser console. This is a UI-decoration endpoint, not
   a security boundary — the dropdown's "Edit my bio" entry should
   silently disappear on any failure, never blink an error.

2. The bio-api helpers required WP_REST_URL explicitly, but the
   historical default on these deployments is WP_GRAPHQL_URL (set in
   deploy.yml via vars.WP_GRAPHQL_URL). WP_REST_URL was added later
   for the Python CLI's host-perspective default and isn't reliably
   set at the Next.js runtime in Plesk's per-domain env. With only
   WP_GRAPHQL_URL set, every helper call threw
   BioApiError(500, 'config_missing') — that 500 plus the unconditional
   re-throw in /check is exactly what produced the console error.

Changes:

- lib/bio-api.ts::getWpRestUrl() now falls back to
  WP_GRAPHQL_URL?.replace(/\/graphql\/?$/, '/wp-json') when
  WP_REST_URL is unset. Both vars resolve to the same WP origin in
  every production-shaped deploy; same trick as the deferred
  feature/zitadel-integration branch's WP sync logic. Only when BOTH
  are unset do we throw the config_missing error.

- app/api/my-bio/check/route.ts replaces the narrow 401/403 catch
  with a broad catch-all that returns {linked: false} for every
  failure and console.error's the underlying error for server-side
  diagnosis. Anonymous / unlinked / expired token / unreachable WP /
  missing env var all collapse to the same HTTP 200 payload. The
  /[lang]/my-bio page still surfaces real errors as localized copy
  via its own per-status handling, so this fail-soft only affects
  the dropdown-decoration endpoint.

Tests
- Renamed the WP_REST_URL-unset test to make the "both unset"
  precondition explicit.
- Added a new fallback test confirming `https://cms.example.org/graphql`
  → `https://cms.example.org/wp-json/cdcf/v1/my-team-member`.
- bio-api.test.ts beforeEach now clears WP_GRAPHQL_URL too, so
  fallback-branch tests start from a clean slate.

Verification
- npm test: 146 / 146 (was 145, +1 for the fallback test)
- npm run lint: clean
- next build: clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JohnRDOrazio
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 5, 2026

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
app/api/my-bio/check/route.ts (1)

24-24: ⚡ Quick win

Consider adding an explicit type annotation for clarity.

While TypeScript infers the type from fetchMyTeamMember, an explicit annotation would improve readability and align with the pattern from commit 902bf71 where similar type annotations were added.

📝 Suggested change
-    const discovery = await fetchMyTeamMember(session)
+    const discovery: BioDiscovery = await fetchMyTeamMember(session)

Add the import if not present:

-import { fetchMyTeamMember } from '`@/lib/bio-api`'
+import { fetchMyTeamMember, type BioDiscovery } from '`@/lib/bio-api`'
🤖 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/api/my-bio/check/route.ts` at line 24, Add an explicit type annotation
for the variable `discovery` returned by `fetchMyTeamMember` in the `route.ts`
handler: import the appropriate type (the same interface/type used elsewhere for
team member discovery) and annotate `discovery` (e.g., const discovery:
<TypeName> = await fetchMyTeamMember(session)). Update the import list to
include that type if missing and ensure the annotation matches the return type
of `fetchMyTeamMember` for consistency with commit 902bf71.
🤖 Prompt for all review comments with 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.

Nitpick comments:
In `@app/api/my-bio/check/route.ts`:
- Line 24: Add an explicit type annotation for the variable `discovery` returned
by `fetchMyTeamMember` in the `route.ts` handler: import the appropriate type
(the same interface/type used elsewhere for team member discovery) and annotate
`discovery` (e.g., const discovery: <TypeName> = await
fetchMyTeamMember(session)). Update the import list to include that type if
missing and ensure the annotation matches the return type of `fetchMyTeamMember`
for consistency with commit 902bf71.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8e03941d-c6c2-42db-a2fe-8b001fd8e1e7

📥 Commits

Reviewing files that changed from the base of the PR and between 4338b4d and 458e14d.

📒 Files selected for processing (8)
  • .env.local.example
  • app/[lang]/my-bio/page.tsx
  • app/api/my-bio/check/route.ts
  • app/api/my-bio/save/route.ts
  • components/BioEditor.tsx
  • lib/auth.ts
  • lib/bio-api.test.ts
  • lib/bio-api.ts
✅ Files skipped from review due to trivial changes (1)
  • .env.local.example
🚧 Files skipped from review as they are similar to previous changes (5)
  • app/api/my-bio/save/route.ts
  • lib/bio-api.test.ts
  • app/[lang]/my-bio/page.tsx
  • components/BioEditor.tsx
  • lib/bio-api.ts

…(CodeRabbit nitpick)

Mirror the same fix applied in 902bf71 for app/[lang]/my-bio/page.tsx.
The discovery variable in app/api/my-bio/check/route.ts was missed at
the time and inferred as `any` once it reaches the destructuring. Add
the BioDiscovery type import + explicit annotation so the const is
typed end-to-end.

Verification
- tsc --noEmit: clean
- npm run lint: clean
- npm test: 146 / 146
- next build: clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JohnRDOrazio
Copy link
Copy Markdown
Member Author

CodeRabbit nitpick addressed in 3fcd935.

Finding Status
app/api/my-bio/check/route.ts L24 — discovery lacked explicit type annotation ✅ Added BioDiscovery to the import + explicit const discovery: BioDiscovery = .... Mirrors the same fix applied in 902bf71 for app/[lang]/my-bio/page.tsx — the check route was missed at the time.

Verification: tsc --noEmit / npm run lint / npm test (146/146) / next build all clean.

@JohnRDOrazio JohnRDOrazio merged commit 3e520f7 into main Jun 5, 2026
12 checks passed
@JohnRDOrazio JohnRDOrazio deleted the feat/bio-editor-ui branch June 5, 2026 19:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants