DS1 follow-up: close the sb-token trust-bug class across all surfaces
The OAuth-only 'sb-token' localStorage key was read by profile, slip, dashboard (recent-scans), settings, and tracker for their authenticated fetches. Email/password users never had that key, so those fetches sent no Authorization header and silently returned nothing. - web/src/lib/authToken.js — currentAccessToken() reads the REAL Supabase session (sb-<ref>-auth-token, v2 top-level or v1 currentSession), legacy fallback. CommonJS so Jest can unit-test it (5 tests). - Swept all 5 pages to the helper (scan already session-first from DS1). - lib/api.ts (0 callers) + ParlayTray (unmounted) left as dead code. 236 suites / 2842 tests green, next build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import { AccuracyBadge, Skeleton, SkeletonList } from '@/components/vyndr';
|
||||
import { emptyStateCopy } from '@/lib/emptyState';
|
||||
// Session 59 (work-order 2.3) — the real pipeline schedule for waiting states.
|
||||
import { nextRunLabelET } from '@/lib/pipelineSchedule';
|
||||
import { currentAccessToken } from '@/lib/authToken';
|
||||
|
||||
type Sport = 'NBA' | 'MLB' | 'WNBA';
|
||||
|
||||
@@ -170,7 +171,7 @@ export default function DashboardPage() {
|
||||
.catch(() => setMostParlayed([]));
|
||||
|
||||
if (user) {
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('sb-token') : null;
|
||||
const token = currentAccessToken();
|
||||
fetch('/api/user/recent-scans', {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { currentAccessToken } from '@/lib/authToken';
|
||||
|
||||
interface FullProfile {
|
||||
id: string;
|
||||
@@ -29,7 +30,7 @@ export default function ProfilePage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('sb-token') : null;
|
||||
const token = currentAccessToken();
|
||||
fetch('/api/user/profile', {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
})
|
||||
@@ -42,7 +43,7 @@ export default function ProfilePage() {
|
||||
if (!confirm('Cancel your subscription at the end of the current period?')) return;
|
||||
setWorking(true);
|
||||
setError('');
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('sb-token') : null;
|
||||
const token = currentAccessToken();
|
||||
const res = await fetch('/api/user/profile', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { currentAccessToken } from '@/lib/authToken';
|
||||
|
||||
/**
|
||||
* /settings (Session 42 — Player Intelligence design).
|
||||
@@ -173,7 +174,7 @@ export default function SettingsPage() {
|
||||
setDeleting(true);
|
||||
setDelError('');
|
||||
try {
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('sb-token') : null;
|
||||
const token = currentAccessToken();
|
||||
const res = await fetch('/api/user/profile', {
|
||||
method: 'DELETE',
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
|
||||
@@ -17,6 +17,7 @@ import SectionHead from '@/components/vyndr/SectionHead';
|
||||
import VBtn from '@/components/vyndr/VBtn';
|
||||
import GradeBadge from '@/components/vyndr/GradeBadge';
|
||||
import { useParlay } from '@/contexts/ParlayContext';
|
||||
import { currentAccessToken } from '@/lib/authToken';
|
||||
|
||||
type Sport = 'MLB' | 'NBA' | 'WNBA';
|
||||
|
||||
@@ -109,7 +110,7 @@ export default function SlipPage() {
|
||||
setGraded(false);
|
||||
setAdded(false);
|
||||
try {
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('sb-token') : null;
|
||||
const token = currentAccessToken();
|
||||
const res = await fetch('/api/slips/parse', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -185,7 +186,7 @@ export default function SlipPage() {
|
||||
if (!legs || grading) return;
|
||||
setGrading(true);
|
||||
setGrades(legs.map(() => ({ status: 'pending' })));
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('sb-token') : null;
|
||||
const token = currentAccessToken();
|
||||
const results: LegGrade[] = [];
|
||||
for (const leg of legs) {
|
||||
if (!legReady(leg)) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { currentAccessToken } from '@/lib/authToken';
|
||||
|
||||
type Period = 'weekly' | 'monthly' | 'all_time';
|
||||
|
||||
@@ -28,7 +29,7 @@ interface Bet {
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||
|
||||
function getAuthHeaders(): Record<string, string> {
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('sb-token') : null;
|
||||
const token = currentAccessToken();
|
||||
return token
|
||||
? { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
|
||||
: { 'Content-Type': 'application/json' };
|
||||
|
||||
Reference in New Issue
Block a user