@@ -8,11 +8,19 @@ function corsHeaders(origin) {
88 const allowedOrigin = ALLOWED_ORIGINS . includes ( origin ) ? origin : ALLOWED_ORIGINS [ 0 ] ;
99 return {
1010 'Access-Control-Allow-Origin' : allowedOrigin ,
11- 'Access-Control-Allow-Methods' : 'POST, OPTIONS' ,
12- 'Access-Control-Allow-Headers' : 'Content-Type' ,
11+ 'Access-Control-Allow-Methods' : 'GET, POST, OPTIONS' ,
12+ 'Access-Control-Allow-Headers' : 'Content-Type, Authorization' ,
13+ 'Access-Control-Allow-Credentials' : 'true' ,
1314 } ;
1415}
1516
17+ function jsonResponse ( data , status = 200 , origin = '' ) {
18+ return new Response ( JSON . stringify ( data ) , {
19+ status,
20+ headers : { ...corsHeaders ( origin ) , 'Content-Type' : 'application/json' }
21+ } ) ;
22+ }
23+
1624export default {
1725 async fetch ( request , env ) {
1826 const origin = request . headers . get ( 'Origin' ) || '' ;
@@ -24,29 +32,25 @@ export default {
2432 const url = new URL ( request . url ) ;
2533
2634 if ( url . pathname === '/oauth/config' && request . method === 'GET' ) {
27- return new Response ( JSON . stringify ( {
35+ return jsonResponse ( {
2836 client_id : env . GITHUB_CLIENT_ID ,
2937 redirect_uri : env . OAUTH_REDIRECT_URI || 'http://localhost:8888/api/auth/callback'
30- } ) , {
31- headers : { ...corsHeaders ( origin ) , 'Content-Type' : 'application/json' }
32- } ) ;
38+ } , 200 , origin ) ;
3339 }
3440
35- if ( request . method !== 'POST' ) {
36- return new Response ( JSON . stringify ( { error : 'Method not allowed' } ) , {
37- status : 405 ,
38- headers : { ...corsHeaders ( origin ) , 'Content-Type' : 'application/json' }
39- } ) ;
41+ if ( url . pathname === '/oauth/token' && request . method === 'POST' ) {
42+ return handleTokenExchange ( request , env , origin ) ;
4043 }
4144
42- if ( url . pathname === '/oauth/token ' ) {
43- return handleTokenExchange ( request , env , origin ) ;
45+ if ( url . pathname === '/leaderboard' && request . method === 'GET ') {
46+ return handleGetLeaderboard ( request , env , origin ) ;
4447 }
4548
46- return new Response ( JSON . stringify ( { error : 'Not found' } ) , {
47- status : 404 ,
48- headers : { ...corsHeaders ( origin ) , 'Content-Type' : 'application/json' }
49- } ) ;
49+ if ( url . pathname === '/leaderboard/sync' && request . method === 'POST' ) {
50+ return handleSyncScore ( request , env , origin ) ;
51+ }
52+
53+ return jsonResponse ( { error : 'Not found' } , 404 , origin ) ;
5054 }
5155} ;
5256
@@ -56,10 +60,7 @@ async function handleTokenExchange(request, env, origin) {
5660 const { code, redirect_uri } = body ;
5761
5862 if ( ! code ) {
59- return new Response ( JSON . stringify ( { error : 'Missing code parameter' } ) , {
60- status : 400 ,
61- headers : { ...corsHeaders ( origin ) , 'Content-Type' : 'application/json' }
62- } ) ;
63+ return jsonResponse ( { error : 'Missing code parameter' } , 400 , origin ) ;
6364 }
6465
6566 const tokenResponse = await fetch ( 'https://github.com/login/oauth/access_token' , {
@@ -79,27 +80,118 @@ async function handleTokenExchange(request, env, origin) {
7980 const tokenData = await tokenResponse . json ( ) ;
8081
8182 if ( tokenData . error ) {
82- return new Response ( JSON . stringify ( {
83+ return jsonResponse ( {
8384 error : tokenData . error ,
8485 error_description : tokenData . error_description
85- } ) , {
86- status : 400 ,
87- headers : { ...corsHeaders ( origin ) , 'Content-Type' : 'application/json' }
88- } ) ;
86+ } , 400 , origin ) ;
8987 }
9088
91- return new Response ( JSON . stringify ( {
89+ return jsonResponse ( {
9290 access_token : tokenData . access_token ,
9391 token_type : tokenData . token_type ,
9492 scope : tokenData . scope
95- } ) , {
96- headers : { ...corsHeaders ( origin ) , 'Content-Type' : 'application/json' }
97- } ) ;
93+ } , 200 , origin ) ;
94+
95+ } catch ( error ) {
96+ return jsonResponse ( { error : 'Token exchange failed' } , 500 , origin ) ;
97+ }
98+ }
99+
100+ async function handleGetLeaderboard ( request , env , origin ) {
101+ try {
102+ const url = new URL ( request . url ) ;
103+ const limit = Math . min ( parseInt ( url . searchParams . get ( 'limit' ) || '10' ) , 100 ) ;
104+ const offset = parseInt ( url . searchParams . get ( 'offset' ) || '0' ) ;
105+
106+ const result = await env . DB . prepare ( `
107+ SELECT github_id, username, avatar_url, score,
108+ DENSE_RANK() OVER (ORDER BY score DESC) as rank
109+ FROM players
110+ WHERE score > 0
111+ ORDER BY score DESC
112+ LIMIT ? OFFSET ?
113+ ` ) . bind ( limit , offset ) . all ( ) ;
114+
115+ const countResult = await env . DB . prepare (
116+ 'SELECT COUNT(*) as total FROM players WHERE score > 0'
117+ ) . first ( ) ;
118+
119+ return jsonResponse ( {
120+ entries : result . results . map ( row => ( {
121+ rank : row . rank ,
122+ github_id : row . github_id ,
123+ username : row . username ,
124+ avatar_url : row . avatar_url ,
125+ score : row . score
126+ } ) ) ,
127+ total_players : countResult ?. total || 0 ,
128+ has_more : ( offset + limit ) < ( countResult ?. total || 0 )
129+ } , 200 , origin ) ;
98130
99131 } catch ( error ) {
100- return new Response ( JSON . stringify ( { error : 'Token exchange failed' } ) , {
101- status : 500 ,
102- headers : { ...corsHeaders ( origin ) , 'Content-Type' : 'application/json' }
132+ return jsonResponse ( { error : 'Failed to fetch leaderboard' , details : error . message } , 500 , origin ) ;
133+ }
134+ }
135+
136+ async function handleSyncScore ( request , env , origin ) {
137+ try {
138+ const authHeader = request . headers . get ( 'Authorization' ) ;
139+ if ( ! authHeader || ! authHeader . startsWith ( 'Bearer ' ) ) {
140+ return jsonResponse ( { error : 'Missing authorization' } , 401 , origin ) ;
141+ }
142+
143+ const accessToken = authHeader . substring ( 7 ) ;
144+
145+ const userResponse = await fetch ( 'https://api.github.com/user' , {
146+ headers : {
147+ 'Authorization' : `token ${ accessToken } ` ,
148+ 'Accept' : 'application/vnd.github+json' ,
149+ 'User-Agent' : 'ELF-Leaderboard'
150+ }
103151 } ) ;
152+
153+ if ( ! userResponse . ok ) {
154+ return jsonResponse ( { error : 'Invalid token' } , 401 , origin ) ;
155+ }
156+
157+ const userData = await userResponse . json ( ) ;
158+ const body = await request . json ( ) ;
159+ const { score } = body ;
160+
161+ if ( typeof score !== 'number' || score < 0 ) {
162+ return jsonResponse ( { error : 'Invalid score' } , 400 , origin ) ;
163+ }
164+
165+ const existing = await env . DB . prepare (
166+ 'SELECT score FROM players WHERE github_id = ?'
167+ ) . bind ( userData . id ) . first ( ) ;
168+
169+ if ( existing ) {
170+ if ( score > existing . score ) {
171+ await env . DB . prepare ( `
172+ UPDATE players SET score = ?, username = ?, avatar_url = ?, updated_at = datetime('now')
173+ WHERE github_id = ?
174+ ` ) . bind ( score , userData . login , userData . avatar_url , userData . id ) . run ( ) ;
175+ }
176+ } else {
177+ await env . DB . prepare ( `
178+ INSERT INTO players (github_id, username, avatar_url, score)
179+ VALUES (?, ?, ?, ?)
180+ ` ) . bind ( userData . id , userData . login , userData . avatar_url , score ) . run ( ) ;
181+ }
182+
183+ const rankResult = await env . DB . prepare ( `
184+ SELECT COUNT(*) + 1 as rank FROM players WHERE score > ?
185+ ` ) . bind ( score ) . first ( ) ;
186+
187+ return jsonResponse ( {
188+ success : true ,
189+ score : score ,
190+ rank : rankResult ?. rank || 1 ,
191+ username : userData . login
192+ } , 200 , origin ) ;
193+
194+ } catch ( error ) {
195+ return jsonResponse ( { error : 'Sync failed' , details : error . message } , 500 , origin ) ;
104196 }
105197}
0 commit comments