Skip to content

fix(admin): let moderators preview a post before approving it - #1340

Merged
NiallJoeMaher merged 1 commit into
developfrom
fix/admin-preview-in-review-posts
Aug 12, 2026
Merged

fix(admin): let moderators preview a post before approving it#1340
NiallJoeMaher merged 1 commit into
developfrom
fix/admin-preview-in-review-posts

Conversation

@NiallJoeMaher

Copy link
Copy Markdown
Contributor

What

The in review queue at /admin/moderation listed a title, author and (now) an excerpt, but there was no way to actually read the post — so Approve/Decline was a judgement call with nothing to judge.

Each queued item now has a Preview link that opens where the post really renders:

Post type Preview goes to
discussion, question /d/{slug}
link the linked destination (that is the submission)
everything else /{username}/{slug}

For the last case the reader resolvers now grant admins the bypass authors already had for their own in_review / rejected posts, so a moderator sees the article exactly as readers eventually will — "Awaiting review" banner included — rather than a bespoke admin rendering that could drift from the real thing.

Why the shared filter

That published-or-owner rule was about to exist in a third place, in a third shape. It moves to server/lib/postVisibility.ts and both reader resolvers call it.

The author predicate in there is the only thing stopping one member reading another's drafts, so it gets unit tests: anonymous sees live posts only, a signed-in member only bypasses for their own posts, an admin bypasses for any author, and no viewer ever sees draft.

Verified locally

  • Anonymous → 404 on an in_review post; signed-in non-author → 404; author → 200; admin → 200.
  • Preview hrefs resolve correctly for all three post kinds, including a link post moved into review (which would otherwise 404, since the link resolver is published-only).
  • npm run lint, npm run prettier, npm run test:unit (123 passing), npm run build.

The "in review" queue showed a title, author and excerpt but gave no way
to read the post, so there was nothing to base an Approve/Decline on.

Each queued item now has a Preview link that opens where the post
actually renders: /d/{slug} for discussions and questions, the
destination itself for shared links, and /{username}/{slug} for
everything the site renders. For that last case the reader resolvers now
grant admins the bypass authors already had for their own in_review and
rejected posts, so the preview is the article exactly as readers would
eventually see it, "Awaiting review" banner and all.

That visibility rule was about to be a third copy of the same
published-or-owner check, so it moves to server/lib/postVisibility.ts
with unit tests covering the anonymous, author and admin branches — the
author predicate is the only thing stopping one member reading another's
drafts, so it is worth pinning down.
@NiallJoeMaher
NiallJoeMaher requested a review from a team as a code owner August 12, 2026 06:50
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
codu Ready Ready Preview Aug 12, 2026 6:52am

Request Review

@github-actions

Copy link
Copy Markdown

Uh oh! @vercel[bot], the image you shared is missing helpful alt text. Check #1340 (comment).

Alt text is an invisible description that helps screen readers describe images to blind or low-vision users. If you are using markdown to display images, add your alt text inside the brackets of the markdown image.

Learn more about alt text at Basic writing and formatting syntax: images on GitHub Docs.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This change centralizes post visibility rules, passes administrator context to post pages, and adds preview links for posts in the moderation queue.

Changes

Moderation preview visibility

Layer / File(s) Summary
Shared post visibility rules
server/lib/postVisibility.ts, server/lib/postVisibility.test.ts
postVisibilityFilter defines access for published, in-review, rejected, and draft posts. Tests cover anonymous viewers, members, authors, and administrators.
Post page visibility integration
app/(app)/[username]/[slug]/page.tsx, app/(app)/d/[slug]/page.tsx, components/ContentDetail/PostReader.tsx
Member and discussion pages use the shared filter and pass administrator state through metadata and rendering. The reader comment documents administrator previews.
Moderation queue preview routing
server/api/router/admin.ts, app/(admin)/admin/moderation/_client.tsx
The review queue returns preview fields and adds routing for external links, discussions, questions, and author-owned posts.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AdminModerationQueue
  participant PreviewLink
  participant PostPage
  participant postVisibilityFilter
  AdminModerationQueue->>PreviewLink: provide post type and destination fields
  PreviewLink->>PostPage: navigate to selected preview route
  PostPage->>postVisibilityFilter: provide viewer ID and administrator role
  postVisibilityFilter-->>PostPage: return visibility predicate
Loading

Possibly related PRs

  • codu-code/codu#941: Adds unpublished article preview functionality with a related moderation preview flow.

Poem

A rabbit hops through posts in review,
With preview paths clear and visibility true.
Admins may peek, authors may see,
Drafts stay hidden as they should be.
The moderation queue now links with glee.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: moderators can preview posts before approval.
Description check ✅ Passed The description clearly explains preview destinations, visibility changes, tests, and verification results, but omits the repository template headings.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/admin-preview-in-review-posts

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/`(admin)/admin/moderation/_client.tsx:
- Around line 55-70: Update postPreviewHref and the RSS import path to validate
external links with safeExternalHref before navigation or persistence. For link
posts, pass post.externalUrl through safeExternalHref and return null for
rejected schemes; ensure RSS imports also reject invalid item.link schemes
rather than storing them in posts.
🪄 Autofix

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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6e6e3da8-4c83-4d41-bf3b-421f01f4b4b2

📥 Commits

Reviewing files that changed from the base of the PR and between 2ac41e9 and 4d09857.

📒 Files selected for processing (7)
  • app/(admin)/admin/moderation/_client.tsx
  • app/(app)/[username]/[slug]/page.tsx
  • app/(app)/d/[slug]/page.tsx
  • components/ContentDetail/PostReader.tsx
  • server/api/router/admin.ts
  • server/lib/postVisibility.test.ts
  • server/lib/postVisibility.ts

Comment on lines +55 to +70
function postPreviewHref(post: PreviewablePost): string | null {
if (post.type === "link") return post.externalUrl;
if (!post.slug) return null;
if (post.type === "discussion" || post.type === "question") {
return `/d/${post.slug}`;
}
if (!post.authorUsername) return null;
return `/${post.authorUsername}/${post.slug}`;
}

const PreviewLink = ({ post }: { post: PreviewablePost }) => {
const href = postPreviewHref(post);
if (!href) return null;

return (
<Link href={href} target="_blank" className="secondary-button">

Copy link
Copy Markdown
Contributor

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

# Locate the schema and all TypeScript write/read paths for external URLs.
fd -t f -e ts -e tsx | while IFS= read -r file; do
  rg -n -C 3 'externalUrl|external_url' "$file" || true
done

Repository: codu-code/codu

Length of output: 35426


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- httpUrl and safeExternalHref definitions ---'
rg -n -C 8 'function httpUrl|const httpUrl|export .*httpUrl|safeExternalHref' .

printf '%s\n' '--- post write handlers and schemas ---'
rg -l 'externalUrl' --glob '*.ts' --glob '*.tsx' | while IFS= read -r file; do
  if rg -q 'insert\(posts\)|update\(posts\)|INSERT INTO posts|externalUrl: input\.externalUrl|externalUrl: item\.link|externalUrl = ' "$file"; then
    printf '\n### %s\n' "$file"
    rg -n -C 12 'insert\(posts\)|update\(posts\)|INSERT INTO posts|externalUrl: input\.externalUrl|externalUrl: item\.link|externalUrl = ' "$file"
  fi
done

printf '%s\n' '--- URL normalization and direct navigation helpers ---'
rg -n -C 8 'normalizeUrl|ensureHttps|new URL\(|startsWith\("http"\)|startsWith\('\''http' "$(
  rg -l 'externalUrl' --glob '*.ts' --glob '*.tsx' .
)" || true

Repository: codu-code/codu

Length of output: 16142


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- URL schema and render guard ---'
cat -n schema/shared.ts | sed -n '1,35p'
cat -n utils/url.ts | sed -n '1,70p'

printf '%s\n' '--- direct post write sites ---'
rg -l --glob '*.ts' --glob '*.tsx' \
  'externalUrl: input\.externalUrl|externalUrl: item\.link|INSERT INTO posts|createLinkPost' . |
while IFS= read -r file; do
  printf '\n### %s\n' "$file"
  rg -n -C 18 \
    'externalUrl: input\.externalUrl|externalUrl: item\.link|INSERT INTO posts|createLinkPost' \
    "$file"
done

printf '%s\n' '--- all externalUrl validation declarations ---'
rg -n -C 6 \
  'externalUrl: (httpUrl\(\)|z\.string\(\)\.url|z\.string\(\)\.trim|item\.link|input\.externalUrl)' \
  --glob '*.ts' --glob '*.tsx' .

Repository: codu-code/codu

Length of output: 17334


Guard externalUrl before navigation.

RSS importers persist item.link directly to posts, so httpUrl() does not cover every write path. Use safeExternalHref before passing externalUrl to Link, and reject invalid schemes during RSS imports.

🤖 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/`(admin)/admin/moderation/_client.tsx around lines 55 - 70, Update
postPreviewHref and the RSS import path to validate external links with
safeExternalHref before navigation or persistence. For link posts, pass
post.externalUrl through safeExternalHref and return null for rejected schemes;
ensure RSS imports also reject invalid item.link schemes rather than storing
them in posts.

@github-actions

Copy link
Copy Markdown

Uh oh! @vercel[bot], the image you shared is missing helpful alt text. Check #1340 (comment).

Alt text is an invisible description that helps screen readers describe images to blind or low-vision users. If you are using markdown to display images, add your alt text inside the brackets of the markdown image.

Learn more about alt text at Basic writing and formatting syntax: images on GitHub Docs.

@NiallJoeMaher
NiallJoeMaher merged commit 379fc4c into develop Aug 12, 2026
6 of 8 checks passed
@NiallJoeMaher
NiallJoeMaher deleted the fix/admin-preview-in-review-posts branch August 12, 2026 07:03
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.

1 participant