[object Object]

← 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

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 →