11import { NextResponse } from 'next/server' ;
22import { createClient } from '@supabase/supabase-js' ;
3- export async function GET ( request , { params } = { } ) {
3+
4+ // Regular (anon) client used ONLY to validate the caller's JWT. Never use the
5+ // service-role key for auth checks.
6+ const supabaseClient = createClient (
7+ process . env . NEXT_PUBLIC_SUPABASE_URL ,
8+ process . env . NEXT_PUBLIC_SUPABASE_ANON_KEY
9+ ) ;
10+
11+ /**
12+ * Authenticate user from request cookies. Mirrors the check in ../route.js so
13+ * this admin-scoped endpoint cannot be called anonymously.
14+ * @param {Request } request
15+ * @returns {Promise<{user?: any, error?: string}> }
16+ */
17+ async function authenticateUser ( request ) {
18+ try {
19+ const cookieHeader = request . headers . get ( 'cookie' ) ;
20+ if ( ! cookieHeader ) {
21+ return { error : 'No cookies found' } ;
22+ }
23+
24+ const cookies = Object . fromEntries (
25+ cookieHeader . split ( '; ' ) . map ( ( cookie ) => {
26+ const [ name , value ] = cookie . split ( '=' ) ;
27+ return [ name , decodeURIComponent ( value ) ] ;
28+ } )
29+ ) ;
30+
31+ let accessToken = null ;
32+
33+ // Supabase-specific auth cookie first
34+ if ( cookies [ 'sb-xydzwxwsbgmznthiiscl-auth-token' ] ) {
35+ try {
36+ let tokenData = cookies [ 'sb-xydzwxwsbgmznthiiscl-auth-token' ] ;
37+ if ( tokenData . startsWith ( 'base64-' ) ) {
38+ tokenData = Buffer . from ( tokenData . substring ( 7 ) , 'base64' ) . toString ( 'utf-8' ) ;
39+ }
40+ const parsed = JSON . parse ( tokenData ) ;
41+ accessToken = parsed . access_token ;
42+ } catch ( error ) {
43+ console . log ( 'Failed to parse Supabase auth cookie:' , error . message ) ;
44+ }
45+ }
46+
47+ // Fallback to a session cookie that looks like a JWT
48+ if ( ! accessToken && cookies . session ) {
49+ const sessionToken = cookies . session ;
50+ if ( sessionToken . split ( '.' ) . length === 3 ) {
51+ accessToken = sessionToken ;
52+ }
53+ }
54+
55+ if ( ! accessToken ) {
56+ return { error : 'No access token found' } ;
57+ }
58+
59+ const { data : { user } , error } = await supabaseClient . auth . getUser ( accessToken ) ;
60+ if ( error || ! user ) {
61+ return { error : `Invalid token: ${ error ?. message } ` } ;
62+ }
63+
64+ return { user } ;
65+ } catch ( error ) {
66+ return { error : `Authentication error: ${ error . message } ` } ;
67+ }
68+ }
69+
70+ export async function GET ( request ) {
471 try {
5- // Create service role client for admin access to all user data
72+ // SECURITY: require an authenticated session. This endpoint uses the
73+ // service-role key (which bypasses RLS) to read every user's public key,
74+ // so without this gate any anonymous caller could enumerate the entire
75+ // user base (all user_ids). Public keys are non-secret, but the full
76+ // membership list is. The legitimate caller (key-sync-service) is always
77+ // an authenticated client.
78+ const { user, error : authError } = await authenticateUser ( request ) ;
79+ if ( authError || ! user ) {
80+ return NextResponse . json ( { error : 'Unauthorized' } , { status : 401 } ) ;
81+ }
82+
83+ // Service role client for admin access to all user data
684 const supabase = createClient ( process . env . NEXT_PUBLIC_SUPABASE_URL , process . env . SUPABASE_SERVICE_ROLE_KEY , {
785 auth : {
886 autoRefreshToken : false ,
@@ -34,4 +112,4 @@ export async function GET(request, { params } = {}) {
34112 console . error ( 'Error in public-keys/all endpoint:' , error ) ;
35113 return NextResponse . json ( { error : 'Internal server error' } , { status : 500 } ) ;
36114 }
37- }
115+ }
0 commit comments