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

fix: authentication flow #8

Merged
merged 1 commit into from
Aug 14, 2024
Merged
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
fix: authentication flow
  • Loading branch information
frank-mendez committed Aug 14, 2024
commit 6889d7b3e32c2b07290e92d583d4f478f00de872
11 changes: 10 additions & 1 deletion coverage/coverage-final.json

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions coverage/coverage-summary.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{"total": {"lines":{"total":61,"covered":16,"skipped":0,"pct":26.22},"statements":{"total":61,"covered":16,"skipped":0,"pct":26.22},"functions":{"total":4,"covered":1,"skipped":0,"pct":25},"branches":{"total":4,"covered":1,"skipped":0,"pct":25},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":100}}
{"total": {"lines":{"total":227,"covered":77,"skipped":0,"pct":33.92},"statements":{"total":227,"covered":77,"skipped":0,"pct":33.92},"functions":{"total":21,"covered":7,"skipped":0,"pct":33.33},"branches":{"total":15,"covered":8,"skipped":0,"pct":53.33},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":100}}
,"/home/fmendez/Projects/react-typescript-spotify/commitlint.config.js": {"lines":{"total":1,"covered":0,"skipped":0,"pct":0},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":1,"covered":0,"skipped":0,"pct":0},"branches":{"total":1,"covered":0,"skipped":0,"pct":0}}
,"/home/fmendez/Projects/react-typescript-spotify/tailwind.config.ts": {"lines":{"total":35,"covered":0,"skipped":0,"pct":0},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":35,"covered":0,"skipped":0,"pct":0},"branches":{"total":1,"covered":0,"skipped":0,"pct":0}}
,"/home/fmendez/Projects/react-typescript-spotify/src/App.tsx": {"lines":{"total":16,"covered":16,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":16,"covered":16,"skipped":0,"pct":100},"branches":{"total":1,"covered":1,"skipped":0,"pct":100}}
,"/home/fmendez/Projects/react-typescript-spotify/src/App.tsx": {"lines":{"total":7,"covered":7,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":7,"covered":7,"skipped":0,"pct":100},"branches":{"total":1,"covered":1,"skipped":0,"pct":100}}
,"/home/fmendez/Projects/react-typescript-spotify/src/main.tsx": {"lines":{"total":9,"covered":0,"skipped":0,"pct":0},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":9,"covered":0,"skipped":0,"pct":0},"branches":{"total":1,"covered":0,"skipped":0,"pct":0}}
,"/home/fmendez/Projects/react-typescript-spotify/src/api/auth/hooks/useUserQuery.ts": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":1,"covered":1,"skipped":0,"pct":100}}
,"/home/fmendez/Projects/react-typescript-spotify/src/api/auth/service/auth.service.ts": {"lines":{"total":67,"covered":10,"skipped":0,"pct":14.92},"functions":{"total":3,"covered":1,"skipped":0,"pct":33.33},"statements":{"total":67,"covered":10,"skipped":0,"pct":14.92},"branches":{"total":2,"covered":1,"skipped":0,"pct":50}}
,"/home/fmendez/Projects/react-typescript-spotify/src/axios/createBaseAxiosInstance.ts": {"lines":{"total":10,"covered":0,"skipped":0,"pct":0},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":10,"covered":0,"skipped":0,"pct":0},"branches":{"total":1,"covered":0,"skipped":0,"pct":0}}
,"/home/fmendez/Projects/react-typescript-spotify/src/axios/index.ts": {"lines":{"total":7,"covered":0,"skipped":0,"pct":0},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":7,"covered":0,"skipped":0,"pct":0},"branches":{"total":1,"covered":0,"skipped":0,"pct":0}}
,"/home/fmendez/Projects/react-typescript-spotify/src/components/Auth.tsx": {"lines":{"total":51,"covered":25,"skipped":0,"pct":49.01},"functions":{"total":8,"covered":2,"skipped":0,"pct":25},"statements":{"total":51,"covered":25,"skipped":0,"pct":49.01},"branches":{"total":4,"covered":3,"skipped":0,"pct":75}}
,"/home/fmendez/Projects/react-typescript-spotify/src/data-objects/interface/index.ts": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":1,"covered":1,"skipped":0,"pct":100}}
,"/home/fmendez/Projects/react-typescript-spotify/src/pages/Callback.tsx": {"lines":{"total":7,"covered":2,"skipped":0,"pct":28.57},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":7,"covered":2,"skipped":0,"pct":28.57},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
,"/home/fmendez/Projects/react-typescript-spotify/src/pages/Dashboard.tsx": {"lines":{"total":20,"covered":20,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":20,"covered":20,"skipped":0,"pct":100},"branches":{"total":1,"covered":1,"skipped":0,"pct":100}}
,"/home/fmendez/Projects/react-typescript-spotify/src/routes/index.tsx": {"lines":{"total":13,"covered":13,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":13,"covered":13,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
}
7 changes: 6 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import "./App.css";
import { RouterProvider } from "react-router-dom";
import { router } from "./routes";
import { AuthProvider } from "./context/AuthProvider.tsx";
function App() {
return <RouterProvider router={router} />;
return (
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
);
}

export default App;
30 changes: 23 additions & 7 deletions src/api/auth/service/auth.service.ts
Original file line number Diff line number Diff line change
@@ -73,11 +73,27 @@ export async function getToken(code: string): Promise<TokenScopeResponse> {
return response;
}

export async function getUserData(accessToken: string) {
const response = await fetch("https://api.spotify.com/v1/me", {
method: "GET",
headers: { Authorization: "Bearer " + accessToken },
});
export const getRefreshToken = async () => {
// refresh token that has been previously stored
const refreshToken = localStorage.getItem("refresh_token");
const url = "https://accounts.spotify.com/api/token";

return await response.json();
}
const payload = {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken ?? "",
client_id: clientId,
}),
};
const body = await fetch(url, payload);
const response = await body.json();

localStorage.setItem("access_token", response.accessToken);
if (response.refreshToken) {
localStorage.setItem("refresh_token", response.refreshToken);
}
};
4 changes: 4 additions & 0 deletions src/axios/createBaseAxiosInstance.ts
Original file line number Diff line number Diff line change
@@ -7,5 +7,9 @@ export const createBaseAxiosInstance = (
return axios.create({
baseURL,
...configOverrides,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
},
});
};
14 changes: 14 additions & 0 deletions src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createContext, useContext } from "react";
import { AuthContextType } from "../data-objects/interface";

export const AuthContext = createContext<AuthContextType | undefined>(
undefined,
);

export const useAuth = (): AuthContextType => {

Check warning on line 8 in src/context/AuthContext.tsx

GitHub Actions / eslint

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};
66 changes: 66 additions & 0 deletions src/context/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useEffect, useState } from "react";
import {
getRefreshToken,
getToken,
redirectToSpotifyAuthorize,
} from "../api/auth/service/auth.service.ts";
import { AuthContext } from "./AuthContext";

export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [accessToken, setAccessToken] = useState<string | null>(null);
const [refreshToken, setRefreshToken] = useState<string | null>(null);
const args = new URLSearchParams(window.location.search);
const code = args.get("code");

useEffect(() => {
const fetchToken = async () => {
if (code) {
const token = await getToken(code);

console.log("token", token);

setAccessToken(token.access_token);
setRefreshToken(token.refresh_token);
localStorage.setItem("access_token", token.access_token);
localStorage.setItem("refresh_token", token.refresh_token);
localStorage.setItem("expires_in", token.expires_in.toString());
localStorage.setItem(
"expires",
new Date(Date.now() + token.expires_in * 1000).toISOString(),
);
// Remove code from URL so we can refresh correctly.
const url = new URL(window.location.href);
url.searchParams.delete("code");

const updatedUrl = url.search ? url.href : url.href.replace("?", "");
window.history.replaceState({}, document.title, updatedUrl);
}
};

fetchToken();
}, [code]);

const login = async () => {
await redirectToSpotifyAuthorize();
};

const logout = () => {
setAccessToken(null);
setRefreshToken(null);
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
localStorage.removeItem("expires_in");
localStorage.removeItem("expires");
window.location.href = "/login";
};

return (
<AuthContext.Provider
value={{ accessToken, login, logout, refreshToken, getRefreshToken }}
>
{children}
</AuthContext.Provider>
);
};
8 changes: 8 additions & 0 deletions src/data-objects/interface/index.ts
Original file line number Diff line number Diff line change
@@ -11,3 +11,11 @@ export interface TokenScopeResponse {
refresh_token: string;
scope: string;
}

export interface AuthContextType {
accessToken: string | null;
login: () => Promise<void>;
logout: () => void;
refreshToken: string | null;
getRefreshToken: () => Promise<void>;
}
9 changes: 0 additions & 9 deletions src/pages/Callback.tsx

This file was deleted.

28 changes: 7 additions & 21 deletions src/pages/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,12 @@
import Auth from "../components/Auth.tsx";
import { getUserData } from "../api/auth/service/auth.service.ts";
import { useAuth } from "../context/AuthContext.tsx";
import { Navigate } from "react-router-dom";

const Dashboard = () => {
const accessToken = localStorage.getItem("access_token");
console.log("Dashboard accessToken", accessToken);
const userData = getUserData(accessToken ?? "");
console.log("Dashboard userData", userData);
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>
<Auth />
</div>
</div>
</div>
);
const { accessToken } = useAuth();
if (!accessToken) {
return <Navigate to="/login" />;
}
return <h1>Dashboard</h1>;
};

export default Dashboard;
27 changes: 27 additions & 0 deletions src/pages/Login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useAuth } from "../context/AuthContext.tsx";
import { Navigate } from "react-router-dom";

const Login = () => {
const { accessToken, login } = useAuth();
if (accessToken) {
return <Navigate to="/" />;
}
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 onClick={login} className="btn btn-primary">
Login
</button>
</div>
</div>
</div>
);
};
export default Login;
16 changes: 7 additions & 9 deletions src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { createBrowserRouter } from "react-router-dom";
import { createBrowserRouter, RouteObject } from "react-router-dom";
import Dashboard from "../pages/Dashboard.tsx";
import Callback from "../pages/Callback.tsx";

export const router = createBrowserRouter([
import Login from "../pages/Login.tsx";
const routes: RouteObject[] = [
{
path: "/",
element: <Dashboard />,
},
{
path: "/callback",
element: <Callback />,
},
]);
{ path: "/login", element: <Login /> },
];

export const router = createBrowserRouter(routes);
Loading