Skip to content

Instantly share code, notes, and snippets.

@Symbitic
Last active June 22, 2021 00:54
Show Gist options
  • Save Symbitic/2564241e0b2477153e65457e2b285d34 to your computer and use it in GitHub Desktop.
Save Symbitic/2564241e0b2477153e65457e2b285d34 to your computer and use it in GitHub Desktop.
// 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