fuzzyworld

hawk / time-arrows / files / public / signin.js

Branch time-arrows · default branch · all branches

signin.js

4.8 KB · history · blame

1const clientId = '6c4c04a3751b4e409ddad7d71aad9949';
2const scope = 'user-library-read user-modify-playback-state user-read-playback-state';
3const prematureRefreshSeconds = 60 * 5; // 5 minutes before expiration
4
5function getSpotifyRedirectURL() {
6 // If the current hostname is 'localhost'
7 if (window.location.hostname === 'localhost') {
8 return 'http://localhost:5500/public/index.html';
9 } else if (window.location.hostname === '127.0.0.1') {
10 return 'http://127.0.0.1:5500/public/index.html';
11 } else {
12 return 'https://pavelsafronov.gitlab.io/hawk/index.html';
13 }
14}
15
16async function signOut() {
17 localStorage.removeItem('access_token');
18 localStorage.removeItem('refresh_token');
19 localStorage.removeItem('expires_at');
20 await deleteDb();
21 window.location = getSpotifyRedirectURL();
22}
23
24async function signIn() {
25 let codeVerifier = generateRandomString(128);
26 const codeChallenge = await generateCodeChallenge(codeVerifier)
27 let state = generateRandomString(16);
28
29 localStorage.setItem('code_verifier', codeVerifier);
30
31 const redirectUrl = getSpotifyRedirectURL();
32 let args = new URLSearchParams({
33 response_type: 'code',
34 client_id: clientId,
35 scope: scope,
36 redirect_uri: redirectUrl,
37 state: state,
38 code_challenge_method: 'S256',
39 code_challenge: codeChallenge
40 });
41
42 window.location = 'https://accounts.spotify.com/authorize?' + args;
43}
44
45async function getToken(code) {
46 let codeVerifier = localStorage.getItem('code_verifier');
47
48 const redirectUrl = getSpotifyRedirectURL();
49 let body = new URLSearchParams({
50 grant_type: 'authorization_code',
51 code: code,
52 redirect_uri: redirectUrl,
53 client_id: clientId,
54 code_verifier: codeVerifier
55 });
56
57 try {
58 const response = await fetch('https://accounts.spotify.com/api/token', {
59 method: 'POST',
60 headers: {
61 'Content-Type': 'application/x-www-form-urlencoded'
62 },
63 body: body
64 });
65 if (!response.ok) {
66 console.error('HTTP status ' + response.status);
67 throw new Error('HTTP status ' + response.status);
68 }
69 const data = await response.json();
70 const expiresInSeconds = data.expires_in;
71 localStorage.setItem('access_token', data.access_token);
72 localStorage.setItem('refresh_token', data.refresh_token);
73 localStorage.setItem('expires_at', Date.now() + expiresInSeconds * 1000);
74 } catch (error) {
75 console.error('Error:', error);
76 }
77}
78
79function generateRandomString(length) {
80 let text = '';
81 let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
82
83 for (let i = 0; i < length; i++) {
84 text += possible.charAt(Math.floor(Math.random() * possible.length));
85 }
86 return text;
87}
88
89async function generateCodeChallenge(codeVerifier) {
90 function base64encode(string) {
91 return btoa(String.fromCharCode.apply(null, new Uint8Array(string)))
92 .replace(/\+/g, '-')
93 .replace(/\//g, '_')
94 .replace(/=+$/, '');
95 }
96
97 const encoder = new TextEncoder();
98 const data = encoder.encode(codeVerifier);
99 const digest = await window.crypto.subtle.digest('SHA-256', data);
100
101 return base64encode(digest);
102}
103
104async function getAccessToken() {
105 let accessToken = localStorage.getItem('access_token');
106 if (!accessToken) {
107 throw 'No access token found';
108 }
109
110 const expiresAtText = localStorage.getItem('expires_at');
111 if (!expiresAtText) {
112 throw 'No expiration found';
113 }
114 const expiresAt = parseInt(expiresAtText);
115 const shouldRefresh = Date.now() > expiresAt - prematureRefreshSeconds * 1000;
116 if (shouldRefresh) {
117 console.log('Refreshing access token');
118 accessToken = await refreshToken();
119 }
120
121 return accessToken;
122}
123
124async function refreshToken() {
125 const refreshToken = localStorage.getItem('refresh_token');
126 if (!refreshToken) {
127 throw 'No refresh token found';
128 }
129
130 let body = new URLSearchParams({
131 grant_type: 'refresh_token',
132 refresh_token: refreshToken,
133 client_id: clientId
134 });
135
136 const response = await fetch('https://accounts.spotify.com/api/token', {
137 method: 'POST',
138 headers: {
139 'Content-Type': 'application/x-www-form-urlencoded'
140 },
141 body: body,
142 });
143 if (!response.ok) {
144 console.error('HTTP status ' + response.status);
145 throw new Error('HTTP status ' + response.status);
146 }
147 const data = await response.json();
148 const expiresInSeconds = data.expires_in;
149 localStorage.setItem('access_token', data.access_token);
150 localStorage.setItem('refresh_token', data.refresh_token);
151 localStorage.setItem('expires_at', Date.now() + expiresInSeconds * 1000);
152 return data.access_token;
153}