← back to Norma
fix(pulse-public): silence 401 spam, unblock topics for guests, hide Apple OAuth stub, add sign-in CTA
52ff6c85b5bd63b000b788ea8a81756aaf8be216 · 2026-05-20 02:19:49 -0700 · Steve Abrams
5 fixes for unauthenticated Pulse visitor UX (per click-through audit
2026-05-20 at /tmp/norma-audit-2026-05-20.md):
1. /api/auth/session: return 200 + {authenticated:false} for guests
instead of 401. Every Pulse page calls this on mount; the 401 was
spamming red errors in browser DevTools + triggering Next.js prefetch
retries. Client code already checks data.authenticated.
2. /api/petitions/topics: drop the requireRole gate. Route was 401'ing
guests despite middleware allow-listing /api/petitions/* for public
access. Topics list has no user-specific logic and should render
pre-signup so guests can browse before creating an account.
3. /login: hide 'Continue with Apple' button behind NEXT_PUBLIC_APPLE_OAUTH_ENABLED
env flag. /api/auth/apple route does not exist, so clicking the button
gave a 404. Handler kept for when Apple OAuth ships; just no UI render.
4. /pulse/activity: error banner with 'please sign in' message now renders
a green-themed card with an inline 'Sign in' CTA button linking to
/login. Previously was a generic red error box with no path forward
for guests.
Files touched
M app/api/auth/session/route.tsM app/api/petitions/topics/route.tsM app/login/page.tsxM app/pulse/activity/page.tsx
Diff
commit 52ff6c85b5bd63b000b788ea8a81756aaf8be216
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 20 02:19:49 2026 -0700
fix(pulse-public): silence 401 spam, unblock topics for guests, hide Apple OAuth stub, add sign-in CTA
5 fixes for unauthenticated Pulse visitor UX (per click-through audit
2026-05-20 at /tmp/norma-audit-2026-05-20.md):
1. /api/auth/session: return 200 + {authenticated:false} for guests
instead of 401. Every Pulse page calls this on mount; the 401 was
spamming red errors in browser DevTools + triggering Next.js prefetch
retries. Client code already checks data.authenticated.
2. /api/petitions/topics: drop the requireRole gate. Route was 401'ing
guests despite middleware allow-listing /api/petitions/* for public
access. Topics list has no user-specific logic and should render
pre-signup so guests can browse before creating an account.
3. /login: hide 'Continue with Apple' button behind NEXT_PUBLIC_APPLE_OAUTH_ENABLED
env flag. /api/auth/apple route does not exist, so clicking the button
gave a 404. Handler kept for when Apple OAuth ships; just no UI render.
4. /pulse/activity: error banner with 'please sign in' message now renders
a green-themed card with an inline 'Sign in' CTA button linking to
/login. Previously was a generic red error box with no path forward
for guests.
---
app/api/auth/session/route.ts | 5 ++++-
app/api/petitions/topics/route.ts | 8 ++++----
app/login/page.tsx | 35 +++++++++++++++++++----------------
app/pulse/activity/page.tsx | 24 ++++++++++++++++++++----
4 files changed, 47 insertions(+), 25 deletions(-)
diff --git a/app/api/auth/session/route.ts b/app/api/auth/session/route.ts
index 2d27614..de66685 100644
--- a/app/api/auth/session/route.ts
+++ b/app/api/auth/session/route.ts
@@ -33,5 +33,8 @@ export async function GET(request: NextRequest) {
});
}
- return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
+ // Guests get a 200 with authenticated:false so unauthenticated Pulse pages
+ // can render normally without spamming red 401s in the browser console.
+ // Clients should check `data.authenticated` rather than `res.ok`.
+ return NextResponse.json({ authenticated: false });
}
diff --git a/app/api/petitions/topics/route.ts b/app/api/petitions/topics/route.ts
index cec976b..0532051 100644
--- a/app/api/petitions/topics/route.ts
+++ b/app/api/petitions/topics/route.ts
@@ -7,13 +7,13 @@
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
-import { requireRole } from '@/lib/require-role';
import { getBrand } from '@/lib/brand';
+// PUBLIC route — topic feed shown on /pulse before sign-in. Middleware
+// already allow-lists /api/petitions/* for unauthenticated access; the
+// previous requireRole gate inside the handler contradicted that and
+// 401-spammed every guest visitor.
export async function GET(req: NextRequest) {
- const auth = requireRole(req, 'admin', 'staff', 'pulse');
- if (auth instanceof NextResponse) return auth;
-
const url = req.nextUrl;
const source = url.searchParams.get('source'); // pulse | headline | trending
const category = url.searchParams.get('category');
diff --git a/app/login/page.tsx b/app/login/page.tsx
index 9e2cc12..53fa475 100644
--- a/app/login/page.tsx
+++ b/app/login/page.tsx
@@ -144,22 +144,25 @@ function LoginFlow() {
Continue with Google
</button>
- {/* Apple */}
- <button
- type="button"
- onClick={handleAppleLogin}
- style={{
- ...btnBase,
- backgroundColor: '#000',
- color: '#fff',
- border: '1px solid #000',
- }}
- onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = '#1a1a1a'; }}
- onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = '#000'; }}
- >
- <AppleIcon />
- Continue with Apple
- </button>
+ {/* Apple — hidden until /api/auth/apple is wired. The handler still
+ exists for when Apple OAuth ships. */}
+ {process.env.NEXT_PUBLIC_APPLE_OAUTH_ENABLED === 'true' && (
+ <button
+ type="button"
+ onClick={handleAppleLogin}
+ style={{
+ ...btnBase,
+ backgroundColor: '#000',
+ color: '#fff',
+ border: '1px solid #000',
+ }}
+ onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = '#1a1a1a'; }}
+ onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = '#000'; }}
+ >
+ <AppleIcon />
+ Continue with Apple
+ </button>
+ )}
</div>
{/* Divider */}
diff --git a/app/pulse/activity/page.tsx b/app/pulse/activity/page.tsx
index c875512..356958e 100644
--- a/app/pulse/activity/page.tsx
+++ b/app/pulse/activity/page.tsx
@@ -123,14 +123,30 @@ export default function PulseActivityPage() {
</div>
</div>
- {/* Error state */}
+ {/* Error state — for "please sign in" cases, show a CTA button so
+ guests have a one-click path to /login instead of a dead red box. */}
{error && (
<div style={{
- backgroundColor: '#fef2f2', border: '1px solid #fecaca', borderRadius: 12,
- padding: 24, textAlign: 'center', color: '#991b1b',
+ backgroundColor: /sign in/i.test(error) ? '#f0fdf4' : '#fef2f2',
+ border: '1px solid ' + (/sign in/i.test(error) ? '#bbf7d0' : '#fecaca'),
+ borderRadius: 12, padding: 24, textAlign: 'center',
+ color: /sign in/i.test(error) ? '#14532d' : '#991b1b',
fontFamily: 'system-ui, sans-serif',
}}>
- {error}
+ <div style={{ marginBottom: /sign in/i.test(error) ? 12 : 0 }}>{error}</div>
+ {/sign in/i.test(error) && (
+ <Link
+ href="/login"
+ style={{
+ display: 'inline-block', padding: '10px 24px',
+ backgroundColor: C.darkGreen, color: '#fff',
+ borderRadius: 8, fontSize: 14, fontWeight: 600,
+ textDecoration: 'none',
+ }}
+ >
+ Sign in
+ </Link>
+ )}
</div>
)}
← 720506c fix(types): unblock SocialTab render, IntegrationsTab metada
·
back to Norma
·
docs: morning wakeup brief — DB password fix + verification c0eed7c →