Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: spotify pkce auth #6

Merged
merged 1 commit into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
VITE_CLIENT_ID=<YOUR_SPOTIFY_CLIENT_ID>
VITE_CLIENT_SECRET=<YOUR_SPOTIFY_CLIENT_SECRET>
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?

coverage/
.env
131 changes: 123 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
"commitlint": "commitlint --edit"
},
"dependencies": {
"axios": "^1.7.3",
"query-string": "^9.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.0"
},
"devDependencies": {
"@commitlint/cli": "^19.4.0",
Expand Down
24 changes: 5 additions & 19 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,8 @@

import './App.css'

import "./App.css";
import { RouterProvider } from "react-router-dom";
import { router } from "./routes";
function App() {

return (
<div className="hero bg-base-200 min-h-screen">
<div className="hero-content text-center">
<div className="max-w-md">
<h1 className="text-5xl font-bold">Hello there</h1>
<p className="py-6">
Provident cupiditate voluptatem et in. Quaerat fugiat ut assumenda excepturi exercitationem
quasi. In deleniti eaque aut repudiandae et a id nisi.
</p>
<button className="btn btn-primary">Get Started</button>
</div>
</div>
</div>
)
return <RouterProvider router={router} />;
}

export default App
export default App;
Empty file.
83 changes: 83 additions & 0 deletions src/api/auth/service/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { TokenScopeResponse } from "../../../data-objects/interface";

const authorizationEndpoint = "https://accounts.spotify.com/authorize";
const clientId = import.meta.env.VITE_CLIENT_ID;
const scope = "user-read-private user-read-email";
const redirectUrl = "http://localhost:5173/";
const tokenEndpoint = "https://accounts.spotify.com/api/token";

export async function redirectToSpotifyAuthorize(): Promise<void> {
const possible: string =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const randomValues: Uint8Array = crypto.getRandomValues(new Uint8Array(64));
const randomString: string = randomValues.reduce(
(acc, x) => acc + possible[x % possible.length],
"",
);

const code_verifier: string = randomString;
const data: Uint8Array = new TextEncoder().encode(code_verifier);
const hashed: ArrayBuffer = await crypto.subtle.digest("SHA-256", data);

const code_challenge_base64: string = btoa(
String.fromCharCode(...new Uint8Array(hashed)),
)
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");

window.localStorage.setItem("code_verifier", code_verifier);

const authUrl: URL = new URL(authorizationEndpoint);
const params: Record<string, string> = {
response_type: "code",
client_id: clientId,
scope: scope,
code_challenge_method: "S256",
code_challenge: code_challenge_base64,
redirect_uri: redirectUrl,
};

authUrl.search = new URLSearchParams(params).toString();
window.location.href = authUrl.toString(); // Redirect the user to the authorization server for login
}

export async function getToken(code: string): Promise<TokenScopeResponse> {
const code_verifier = localStorage.getItem("code_verifier");

if (!code_verifier) {
throw new Error("Code verifier not found in localStorage");
}

const payload = {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: clientId,
grant_type: "authorization_code",
code,
redirect_uri: redirectUrl,
code_verifier: code_verifier,
}),
};

const body = await fetch(tokenEndpoint, payload);
if (!body.ok) {
throw new Error("Failed to fetch token");
}

const response = await body.json();

return response;
}

export async function getUserData(accessToken: string) {
const response = await fetch("https://api.spotify.com/v1/me", {
method: "GET",
headers: { Authorization: "Bearer " + accessToken },
});

return await response.json();
}
11 changes: 11 additions & 0 deletions src/axios/createBaseAxiosInstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import axios, { CreateAxiosDefaults } from "axios";

export const createBaseAxiosInstance = (
configOverrides: CreateAxiosDefaults = {},
) => {
const baseURL = import.meta.env.VITE_TMDB_API_URL;
return axios.create({
baseURL,
...configOverrides,
});
};
10 changes: 10 additions & 0 deletions src/axios/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createBaseAxiosInstance } from './createBaseAxiosInstance';
import Qs from 'query-string';

export const axios = createBaseAxiosInstance({
// Add params serializer to convert objects to query string
// This is useful when you want to pass an object as a query parameter
paramsSerializer: function (params) {
return Qs.stringify(params);
},
});
Loading
Loading