Last active
June 22, 2021 00:54
-
-
Save Symbitic/2564241e0b2477153e65457e2b285d34 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://deno.com/deploy/docs/serve-static-assets | |
// https://deno.land/x/sift@0.3.2 | |
// https://deno.com/deploy/docs/tutorial-faunadb | |
// https://deno.com/deploy/docs/example-post-request | |
// https://deno.com/deploy/docs/runtime-request | |
// https://deno.com/deploy/docs/runtime-response | |
import querystring from 'https://cdn.skypack.dev/querystring@0.2.1?dts'; | |
const CLIENT_ID = Deno.env.get('SPOTIFY_CLIENT_ID'); | |
const CLIENT_SECRET = Deno.env.get('SPOTIFY_CLIENT_SECRET'); | |
const COOKIE_KEY = Deno.env.get('COOKIE_KEY') || 'secret-cookie-1'; | |
const PORT = 8989; | |
const CALLBACK_URL = 'https://spotifyxr.deno.dev/callback/'; | |
//const CALLBACK_URL = `http://localhost:${PORT}/callback/`; | |
const PROD_REDIRECT_URL = 'https://symbitic.github.io/spotifyxr/'; | |
const REDIRECT_URL = `http://localhost:${PORT}/`; | |
const json = (origin: string, body: Record<any, unknown>) => new Response(JSON.stringify(body), { | |
headers: { | |
"content-type": "application/json; charset=UTF-8", | |
"Access-Control-Allow-Origin": origin, | |
"Access-Control-Allow-Credentials": "true" | |
}, | |
}); | |
function login(request: Request) { | |
const randomBytes = new Uint8Array(16); | |
crypto.getRandomValues(randomBytes); | |
const state = randomBytes.join(''); | |
// your application requests authorization | |
const scope = [ | |
'streaming', | |
'user-read-email', | |
'user-read-private', | |
'user-library-read', | |
'user-library-modify', | |
'user-read-playback-state', | |
'user-modify-playback-state' | |
]; | |
const redirectUrl = 'https://accounts.spotify.com/authorize?' + | |
querystring.stringify({ | |
response_type: 'code', | |
client_id: CLIENT_ID, | |
scope: scope.join('%20'), | |
redirect_uri: CALLBACK_URL, | |
state: state | |
}); | |
return Response.redirect(redirectUrl, 302); | |
} | |
/* | |
URLSearchParams | |
https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams | |
searchParams | |
https://deno.land/x/oak@v7.6.3/request.ts | |
https://developer.mozilla.org/en-US/docs/Web/API/URL | |
https://developer.mozilla.org/en-US/docs/Web/API/URL/searchParams | |
searchParams.has("topic") === true; // true | |
searchParams.get("topic") === "api"; // true | |
*/ | |
async function callback(request: Request) { | |
const url = new URL(request.url); | |
const searchParams = url.searchParams; | |
const origin = request.headers.get('origin') || ''; | |
const code = searchParams.has('code') ? searchParams.get('code') : null; | |
const authToken = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`); | |
const response = await fetch('https://accounts.spotify.com/api/token', { | |
method: 'POST', | |
mode: 'cors', | |
cache: 'no-cache', | |
headers: { | |
'Authorization': `Basic ${authToken}`, | |
'Content-Type': 'application/x-www-form-urlencoded' | |
}, | |
body: querystring.stringify({ | |
code: code, | |
redirect_uri: CALLBACK_URL, | |
grant_type: 'authorization_code' | |
}) | |
}); | |
if (response.status !== 200) { | |
console.log(`Bad response: ${response.statusText}`); | |
return json(origin, { | |
'type': 'error', | |
'data': 'Error while authorizing Spotify' | |
}); | |
} | |
// deno-lint-ignore camelcase | |
const { access_token } = await response.json(); | |
const body = null; | |
//const expires = new Date(Date.now() + (this.maxAge * 1000)); | |
const init = { | |
headers: { | |
'Location': REDIRECT_URL, | |
'Set-Cookie': `access_token=${access_token}` // ; domain=localhost; path=/; httponly | |
}, | |
status: 302, | |
}; | |
return new Response(body, init); | |
} | |
async function refresh(request: Request) { | |
const url = new URL(request.url); | |
const searchParams = url.searchParams; | |
const origin = request.headers.get('origin') || ''; | |
const refreshToken = searchParams.has('refresh_token') ? searchParams.get('refresh_token') : null; | |
const authToken = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`); | |
const response = await fetch('https://accounts.spotify.com/api/token', { | |
method: 'POST', | |
mode: 'cors', | |
cache: 'no-cache', | |
headers: { | |
'Authorization': `Basic ${authToken}` | |
}, | |
body: JSON.stringify({ | |
grant_type: 'refresh_token', | |
refresh_token: refreshToken | |
}) | |
}); | |
const obj = await response.json(); | |
return json(origin, { | |
'type': 'success', | |
'data': obj.access_token | |
}); | |
} | |
async function test(request: Request) { | |
console.log(request.headers); | |
console.log(`cookie = ${request.headers.get('cookie')}`); | |
const origin = request.headers.get('origin') || ''; | |
console.log(`origin = ${origin}`); | |
const cookieStr = request.headers.get('cookie') || ''; | |
const cookies = cookieStr.split('; '); | |
console.log(cookies); | |
// deno-lint-ignore camelcase | |
//const { access_token } = request.headers.get(''); | |
const access_token = ''; | |
if (!access_token) { | |
return json(origin, { | |
'type': 'error', | |
'data': 'Missing token' | |
}); | |
} | |
const response = await fetch('https://api.spotify.com/v1/me', { | |
headers: { | |
'Authorization': `Bearer ${access_token}` | |
} | |
}); | |
if (response.status === 200) { | |
console.log(`API test failed with ${response.status} - ${response.statusText}`); | |
return json(origin, { | |
'type': 'error', | |
'data': 'Invalid or expired token' | |
}); | |
} | |
return json(origin, { | |
'type': 'success', | |
'data': access_token | |
}); | |
} | |
function handleRequest(request: Request) { | |
try { | |
const { pathname } = new URL(request.url); | |
console.log(`Requesting ${pathname}`); | |
if (pathname.startsWith("/login")) { | |
return login(request); | |
} else if (pathname.startsWith("/callback")) { | |
return callback(request); | |
} else if (pathname.startsWith("/refresh_token")) { | |
return refresh(request); | |
} else if (pathname.startsWith("/test") || pathname === '/') { | |
return test(request); | |
} else { | |
const origin = request.headers.get('origin') || ''; | |
return json(origin, { | |
'type': 'error', | |
'data': `Unrecognized path ${pathname}` | |
}); | |
} | |
} catch (err) { | |
console.error(`Caught error: ${err.message}`); | |
const origin = request.headers.get('origin') || ''; | |
return json(origin, { | |
'type': 'error', | |
'data': err.message | |
}); | |
} | |
} | |
/* | |
const response = await fetch("https://api.github.com/users/denoland", { | |
headers: { | |
// Servers use this header to decide on response body format. | |
// "application/json" implies that we accept the data in JSON format. | |
accept: "application/json", | |
}, | |
}); | |
*/ | |
/* | |
// fetch() doesn't throw for bad status codes. You need to handle them | |
// by checking if the response.ok is true or false. | |
// In this example we're just returning a generic error for simplicity but | |
// you might want to handle different cases based on response status code. | |
return new Response( | |
JSON.stringify({ message: "couldn't process your request" }), | |
{ | |
status: 500, | |
headers: { | |
"content-type": "application/json; charset=UTF-8", | |
}, | |
}, | |
); | |
*/ | |
addEventListener("fetch", (event: FetchEvent) => { | |
event.respondWith(handleRequest(event.request)); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment