mirror of
https://github.com/apricote/Listory.git
synced 2026-01-13 21:21:02 +00:00
feat: implement long-lived sessions
This commit is contained in:
parent
d0705afca8
commit
44f7e26270
35 changed files with 739 additions and 190 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { AxiosInstance } from "axios";
|
||||
import { formatISO, parseISO } from "date-fns";
|
||||
import { qs } from "../util/queryString";
|
||||
import { Listen } from "./entities/listen";
|
||||
import { ListenReportItem } from "./entities/listen-report-item";
|
||||
import { ListenReportOptions } from "./entities/listen-report-options";
|
||||
|
|
@ -7,58 +7,18 @@ import { Pagination } from "./entities/pagination";
|
|||
import { PaginationOptions } from "./entities/pagination-options";
|
||||
import { TopArtistsItem } from "./entities/top-artists-item";
|
||||
import { TopArtistsOptions } from "./entities/top-artists-options";
|
||||
import { User } from "./entities/user";
|
||||
|
||||
export class UnauthenticatedError extends Error {}
|
||||
|
||||
const getToken = (): string => {
|
||||
const cookieValue = document.cookie.replace(
|
||||
/(?:(?:^|.*;\s*)listory_access_token\s*=\s*([^;]*).*$)|^.*$/,
|
||||
"$1"
|
||||
);
|
||||
|
||||
return cookieValue;
|
||||
};
|
||||
|
||||
const getDefaultHeaders = (): Headers => {
|
||||
const headers = new Headers();
|
||||
|
||||
headers.append("Content-Type", "application/json");
|
||||
headers.append("Authorization", `Bearer ${getToken()}`);
|
||||
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const getUsersMe = async (): Promise<User> => {
|
||||
const res = await fetch(`/api/v1/users/me`, { headers: getDefaultHeaders() });
|
||||
|
||||
switch (res.status) {
|
||||
case 200: {
|
||||
break;
|
||||
}
|
||||
case 401: {
|
||||
throw new UnauthenticatedError(`No token or token expired`);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unable to getUsersMe: ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
const user: User = await res.json();
|
||||
return user;
|
||||
};
|
||||
|
||||
export const getRecentListens = async (
|
||||
options: PaginationOptions = { page: 1, limit: 10 }
|
||||
options: PaginationOptions = { page: 1, limit: 10 },
|
||||
client: AxiosInstance
|
||||
): Promise<Pagination<Listen>> => {
|
||||
const { page, limit } = options;
|
||||
|
||||
const res = await fetch(
|
||||
`/api/v1/listens?${qs({ page: page.toString(), limit: limit.toString() })}`,
|
||||
{
|
||||
headers: getDefaultHeaders(),
|
||||
}
|
||||
);
|
||||
const res = await client.get<Pagination<Listen>>(`/api/v1/listens`, {
|
||||
params: { page, limit },
|
||||
});
|
||||
|
||||
switch (res.status) {
|
||||
case 200: {
|
||||
|
|
@ -72,27 +32,27 @@ export const getRecentListens = async (
|
|||
}
|
||||
}
|
||||
|
||||
const listens: Pagination<Listen> = await res.json();
|
||||
return listens;
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const getListensReport = async (
|
||||
options: ListenReportOptions
|
||||
options: ListenReportOptions,
|
||||
client: AxiosInstance
|
||||
): Promise<ListenReportItem[]> => {
|
||||
const {
|
||||
timeFrame,
|
||||
time: { timePreset, customTimeStart, customTimeEnd },
|
||||
} = options;
|
||||
|
||||
const res = await fetch(
|
||||
`/api/v1/reports/listens?${qs({
|
||||
timeFrame,
|
||||
timePreset,
|
||||
customTimeStart: formatISO(customTimeStart),
|
||||
customTimeEnd: formatISO(customTimeEnd),
|
||||
})}`,
|
||||
const res = await client.get<{ items: { count: number; date: string }[] }>(
|
||||
`/api/v1/reports/listens`,
|
||||
{
|
||||
headers: getDefaultHeaders(),
|
||||
params: {
|
||||
timeFrame,
|
||||
timePreset,
|
||||
customTimeStart: formatISO(customTimeStart),
|
||||
customTimeEnd: formatISO(customTimeEnd),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -108,25 +68,28 @@ export const getListensReport = async (
|
|||
}
|
||||
}
|
||||
|
||||
const rawItems: { count: number; date: string }[] = (await res.json()).items;
|
||||
const {
|
||||
data: { items: rawItems },
|
||||
} = res;
|
||||
return rawItems.map(({ count, date }) => ({ count, date: parseISO(date) }));
|
||||
};
|
||||
|
||||
export const getTopArtists = async (
|
||||
options: TopArtistsOptions
|
||||
options: TopArtistsOptions,
|
||||
client: AxiosInstance
|
||||
): Promise<TopArtistsItem[]> => {
|
||||
const {
|
||||
time: { timePreset, customTimeStart, customTimeEnd },
|
||||
} = options;
|
||||
|
||||
const res = await fetch(
|
||||
`/api/v1/reports/top-artists?${qs({
|
||||
timePreset,
|
||||
customTimeStart: formatISO(customTimeStart),
|
||||
customTimeEnd: formatISO(customTimeEnd),
|
||||
})}`,
|
||||
const res = await client.get<{ items: TopArtistsItem[] }>(
|
||||
`/api/v1/reports/top-artists`,
|
||||
{
|
||||
headers: getDefaultHeaders(),
|
||||
params: {
|
||||
timePreset,
|
||||
customTimeStart: formatISO(customTimeStart),
|
||||
customTimeEnd: formatISO(customTimeEnd),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -142,6 +105,8 @@ export const getTopArtists = async (
|
|||
}
|
||||
}
|
||||
|
||||
const items: TopArtistsItem[] = (await res.json()).items;
|
||||
const {
|
||||
data: { items },
|
||||
} = res;
|
||||
return items;
|
||||
};
|
||||
|
|
|
|||
56
frontend/src/api/auth-api.ts
Normal file
56
frontend/src/api/auth-api.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* These calls are seperate from the others because they are only
|
||||
* used in the useAuth hook which is used before the useApiClient hook.
|
||||
*
|
||||
* They do not use the apiClient/axios.
|
||||
*/
|
||||
|
||||
import { UnauthenticatedError } from "./api";
|
||||
import { RefreshTokenResponse } from "./entities/refresh-token-response";
|
||||
import { User } from "./entities/user";
|
||||
|
||||
export const getUsersMe = async (accessToken: string): Promise<User> => {
|
||||
const res = await fetch(`/api/v1/users/me`, {
|
||||
headers: new Headers({
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
}),
|
||||
});
|
||||
|
||||
switch (res.status) {
|
||||
case 200: {
|
||||
break;
|
||||
}
|
||||
case 401: {
|
||||
throw new UnauthenticatedError(`No token or token expired`);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unable to getUsersMe: ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
const user: User = await res.json();
|
||||
return user;
|
||||
};
|
||||
|
||||
export const postAuthTokenRefresh = async (): Promise<RefreshTokenResponse> => {
|
||||
const res = await fetch(`/api/v1/auth/token/refresh`, {
|
||||
method: "POST",
|
||||
headers: new Headers({ "Content-Type": "application/json" }),
|
||||
});
|
||||
|
||||
switch (res.status) {
|
||||
case 201: {
|
||||
break;
|
||||
}
|
||||
case 401: {
|
||||
throw new UnauthenticatedError(`No Refresh Token or token expired`);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unable to postAuthTokenRefresh: ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
const refreshTokenResponse: RefreshTokenResponse = await res.json();
|
||||
return refreshTokenResponse;
|
||||
};
|
||||
3
frontend/src/api/entities/refresh-token-response.ts
Normal file
3
frontend/src/api/entities/refresh-token-response.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export interface RefreshTokenResponse {
|
||||
accessToken: string;
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { format, formatDistanceToNow } from "date-fns";
|
||||
import React, { useEffect, useState, useMemo } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { getRecentListens } from "../api/api";
|
||||
import { Listen } from "../api/entities/listen";
|
||||
import { useRecentListens } from "../hooks/use-api";
|
||||
import { useAuth } from "../hooks/use-auth";
|
||||
import { ReloadIcon } from "../icons/Reload";
|
||||
import { getPaginationItems } from "../util/getPaginationItems";
|
||||
|
|
@ -14,35 +14,18 @@ export const RecentListens: React.FC = () => {
|
|||
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [listens, setListens] = useState<Listen[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const loadListensForPage = useMemo(
|
||||
() => async () => {
|
||||
setIsLoading(true);
|
||||
const options = useMemo(() => ({ page, limit: LISTENS_PER_PAGE }), [page]);
|
||||
|
||||
try {
|
||||
const listensFromApi = await getRecentListens({
|
||||
page,
|
||||
limit: LISTENS_PER_PAGE,
|
||||
});
|
||||
|
||||
if (totalPages !== listensFromApi.meta.totalPages) {
|
||||
setTotalPages(listensFromApi.meta.totalPages);
|
||||
}
|
||||
setListens(listensFromApi.items);
|
||||
} catch (err) {
|
||||
console.error("Error while fetching recent listens:", err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[page, totalPages, setIsLoading, setTotalPages, setListens]
|
||||
const { recentListens, paginationMeta, isLoading, reload } = useRecentListens(
|
||||
options
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadListensForPage();
|
||||
}, [loadListensForPage]);
|
||||
if (paginationMeta && totalPages !== paginationMeta.totalPages) {
|
||||
setTotalPages(paginationMeta.totalPages);
|
||||
}
|
||||
}, [totalPages, paginationMeta]);
|
||||
|
||||
if (!user) {
|
||||
return <Redirect to="/" />;
|
||||
|
|
@ -55,7 +38,7 @@ export const RecentListens: React.FC = () => {
|
|||
<p className="text-2xl font-normal text-gray-700">Recent listens</p>
|
||||
<button
|
||||
className="flex-shrink-0 mx-2 bg-transparent hover:bg-green-500 text-green-500 hover:text-white font-semibold py-2 px-4 border border-green-500 hover:border-transparent rounded"
|
||||
onClick={loadListensForPage}
|
||||
onClick={reload}
|
||||
>
|
||||
<ReloadIcon className="w-5 h-5 fill-current" />
|
||||
</button>
|
||||
|
|
@ -66,14 +49,14 @@ export const RecentListens: React.FC = () => {
|
|||
<span>Loading Listens</span>
|
||||
</div>
|
||||
)}
|
||||
{listens.length === 0 && (
|
||||
{recentListens.length === 0 && (
|
||||
<div>
|
||||
<p>Could not find any listens!</p>
|
||||
</div>
|
||||
)}
|
||||
{listens.length > 0 && (
|
||||
{recentListens.length > 0 && (
|
||||
<div className="table-auto my-2 w-full text-gray-700">
|
||||
{listens.map((listen) => (
|
||||
{recentListens.map((listen) => (
|
||||
<ListenItem listen={listen} key={listen.id} />
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,12 +11,11 @@ import {
|
|||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { getListensReport } from "../api/api";
|
||||
import { ListenReportItem } from "../api/entities/listen-report-item";
|
||||
import { ListenReportOptions } from "../api/entities/listen-report-options";
|
||||
import { TimeOptions } from "../api/entities/time-options";
|
||||
import { TimePreset } from "../api/entities/time-preset.enum";
|
||||
import { useAsync } from "../hooks/use-async";
|
||||
import { useListensReport } from "../hooks/use-api";
|
||||
import { useAuth } from "../hooks/use-auth";
|
||||
import { ReportTimeOptions } from "./ReportTimeOptions";
|
||||
|
||||
|
|
@ -33,12 +32,12 @@ export const ReportListens: React.FC = () => {
|
|||
customTimeEnd: new Date(),
|
||||
});
|
||||
|
||||
const fetchData = useMemo(
|
||||
() => () => getListensReport({ timeFrame, time: timeOptions }),
|
||||
[timeFrame, timeOptions]
|
||||
);
|
||||
const reportOptions = useMemo(() => ({ timeFrame, time: timeOptions }), [
|
||||
timeFrame,
|
||||
timeOptions,
|
||||
]);
|
||||
|
||||
const { value: report, pending: isLoading } = useAsync(fetchData, []);
|
||||
const { report, isLoading } = useListensReport(reportOptions);
|
||||
|
||||
const reportHasItems = !isLoading && report.length !== 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
import React, { useMemo, useState } from "react";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { getTopArtists } from "../api/api";
|
||||
import { TimeOptions } from "../api/entities/time-options";
|
||||
import { TimePreset } from "../api/entities/time-preset.enum";
|
||||
import { TopArtistsItem } from "../api/entities/top-artists-item";
|
||||
import { useAsync } from "../hooks/use-async";
|
||||
import { useTopArtists } from "../hooks/use-api";
|
||||
import { useAuth } from "../hooks/use-auth";
|
||||
import { ReportTimeOptions } from "./ReportTimeOptions";
|
||||
|
||||
const INITIAL_REPORT_DATA: TopArtistsItem[] = [];
|
||||
|
||||
export const ReportTopArtists: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
|
||||
|
|
@ -19,16 +15,16 @@ export const ReportTopArtists: React.FC = () => {
|
|||
customTimeEnd: new Date(),
|
||||
});
|
||||
|
||||
const fetchData = useMemo(() => () => getTopArtists({ time: timeOptions }), [
|
||||
timeOptions,
|
||||
]);
|
||||
|
||||
const { value: report, pending: isLoading } = useAsync(
|
||||
fetchData,
|
||||
INITIAL_REPORT_DATA
|
||||
const options = useMemo(
|
||||
() => ({
|
||||
time: timeOptions,
|
||||
}),
|
||||
[timeOptions]
|
||||
);
|
||||
|
||||
const reportHasItems = !isLoading && report.length !== 0;
|
||||
const { topArtists, isLoading } = useTopArtists(options);
|
||||
|
||||
const reportHasItems = !isLoading && topArtists.length !== 0;
|
||||
|
||||
if (!user) {
|
||||
return <Redirect to="/" />;
|
||||
|
|
@ -56,7 +52,7 @@ export const ReportTopArtists: React.FC = () => {
|
|||
</div>
|
||||
)}
|
||||
{reportHasItems &&
|
||||
report.map(({ artist, count }) => (
|
||||
topArtists.map(({ artist, count }) => (
|
||||
<div key={artist.id}>
|
||||
{count} - {artist.name}
|
||||
</div>
|
||||
|
|
|
|||
95
frontend/src/hooks/use-api-client.tsx
Normal file
95
frontend/src/hooks/use-api-client.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useAuth } from "./use-auth";
|
||||
|
||||
interface ApiClientContext {
|
||||
client: AxiosInstance;
|
||||
}
|
||||
|
||||
const apiClientContext = createContext<ApiClientContext>(
|
||||
(undefined as any) as ApiClientContext
|
||||
);
|
||||
|
||||
export const ProvideApiClient: React.FC = ({ children }) => {
|
||||
const auth = useProvideApiClient();
|
||||
|
||||
return (
|
||||
<apiClientContext.Provider value={auth}>
|
||||
{children}
|
||||
</apiClientContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useApiClient() {
|
||||
return useContext(apiClientContext);
|
||||
}
|
||||
|
||||
function useProvideApiClient(): ApiClientContext {
|
||||
const { accessToken, refreshAccessToken } = useAuth();
|
||||
|
||||
// Wrap value to immediatly update when refreshing access token
|
||||
// and always having access to newest access token in interceptor
|
||||
const localAccessToken = useRef(accessToken);
|
||||
|
||||
// initialState must be passed as function as return value of axios.create()
|
||||
// is also callable and react will call it and then use that result (promise)
|
||||
// as the initial state instead of the axios instace.
|
||||
const [client] = useState<AxiosInstance>(() => axios.create());
|
||||
|
||||
useEffect(() => {
|
||||
localAccessToken.current = accessToken;
|
||||
}, [localAccessToken, accessToken]);
|
||||
|
||||
// TODO Implement lock to avoid multiple parallel refreshes, maybe in useAuth?
|
||||
|
||||
useEffect(() => {
|
||||
// Setup Axios Interceptors
|
||||
const requestInterceptor = client.interceptors.request.use(
|
||||
(config) => {
|
||||
if (!config.headers) {
|
||||
config.headers = {};
|
||||
}
|
||||
|
||||
config.headers.Authorization = `Bearer ${localAccessToken.current}`;
|
||||
|
||||
return config;
|
||||
},
|
||||
(err) => Promise.reject(err)
|
||||
);
|
||||
const responseInterceptor = client.interceptors.response.use(
|
||||
(data) => data,
|
||||
async (err: any) => {
|
||||
if (!err.response || !err.config) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
const { response, config } = err as {
|
||||
response: AxiosResponse;
|
||||
config: AxiosRequestConfig;
|
||||
};
|
||||
|
||||
if (response && response.status !== 401) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
// TODO error handling
|
||||
localAccessToken.current = await refreshAccessToken();
|
||||
|
||||
return client.request(config);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
client.interceptors.request.eject(requestInterceptor);
|
||||
client.interceptors.response.eject(responseInterceptor);
|
||||
};
|
||||
}, [client, localAccessToken, refreshAccessToken]);
|
||||
|
||||
return { client };
|
||||
}
|
||||
65
frontend/src/hooks/use-api.tsx
Normal file
65
frontend/src/hooks/use-api.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { useMemo, useState } from "react";
|
||||
import { getListensReport, getRecentListens, getTopArtists } from "../api/api";
|
||||
import { ListenReportOptions } from "../api/entities/listen-report-options";
|
||||
import { PaginationOptions } from "../api/entities/pagination-options";
|
||||
import { TopArtistsOptions } from "../api/entities/top-artists-options";
|
||||
import { useApiClient } from "./use-api-client";
|
||||
import { useAsync } from "./use-async";
|
||||
|
||||
const INITIAL_EMPTY_ARRAY: [] = [];
|
||||
Object.freeze(INITIAL_EMPTY_ARRAY);
|
||||
|
||||
export const useRecentListens = (options: PaginationOptions) => {
|
||||
const { client } = useApiClient();
|
||||
|
||||
const fetchData = useMemo(() => () => getRecentListens(options, client), [
|
||||
options,
|
||||
client,
|
||||
]);
|
||||
|
||||
const { value, pending: isLoading, error, reload } = useAsync(
|
||||
fetchData,
|
||||
undefined
|
||||
);
|
||||
|
||||
const recentListens = value ? value.items : [];
|
||||
const paginationMeta = value ? value.meta : undefined;
|
||||
|
||||
return { recentListens, paginationMeta, isLoading, error, reload };
|
||||
};
|
||||
|
||||
export const useListensReport = (options: ListenReportOptions) => {
|
||||
const { client } = useApiClient();
|
||||
|
||||
const [initialData] = useState(INITIAL_EMPTY_ARRAY);
|
||||
|
||||
const fetchData = useMemo(() => () => getListensReport(options, client), [
|
||||
options,
|
||||
client,
|
||||
]);
|
||||
|
||||
const { value: report, pending: isLoading, error } = useAsync(
|
||||
fetchData,
|
||||
initialData
|
||||
);
|
||||
|
||||
return { report, isLoading, error };
|
||||
};
|
||||
|
||||
export const useTopArtists = (options: TopArtistsOptions) => {
|
||||
const { client } = useApiClient();
|
||||
|
||||
const [initialData] = useState(INITIAL_EMPTY_ARRAY);
|
||||
|
||||
const fetchData = useMemo(() => () => getTopArtists(options, client), [
|
||||
options,
|
||||
client,
|
||||
]);
|
||||
|
||||
const { value: topArtists, pending: isLoading, error } = useAsync(
|
||||
fetchData,
|
||||
initialData
|
||||
);
|
||||
|
||||
return { topArtists, isLoading, error };
|
||||
};
|
||||
|
|
@ -3,7 +3,12 @@ import { useCallback, useEffect, useState } from "react";
|
|||
type UseAsync = <T>(
|
||||
asyncFunction: () => Promise<T>,
|
||||
initialValue: T
|
||||
) => { pending: boolean; value: T; error: Error | null };
|
||||
) => {
|
||||
pending: boolean;
|
||||
value: T;
|
||||
error: Error | null;
|
||||
reload: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const useAsync: UseAsync = <T extends any>(
|
||||
asyncFunction: () => Promise<T>,
|
||||
|
|
@ -34,5 +39,5 @@ export const useAsync: UseAsync = <T extends any>(
|
|||
execute();
|
||||
}, [execute]);
|
||||
|
||||
return { execute, pending, value, error };
|
||||
return { reload: execute, pending, value, error };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +1,26 @@
|
|||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
import { getUsersMe, UnauthenticatedError } from "../api/api";
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { getUsersMe, postAuthTokenRefresh } from "../api/auth-api";
|
||||
import { User } from "../api/entities/user";
|
||||
|
||||
interface AuthContext {
|
||||
isLoaded: boolean;
|
||||
user: User | null;
|
||||
accessToken: string;
|
||||
error: Error | null;
|
||||
refreshAccessToken: () => Promise<string>;
|
||||
loginWithSpotifyProps: () => { href: string };
|
||||
}
|
||||
|
||||
const authContext = createContext<AuthContext>(
|
||||
(undefined as any) as AuthContext
|
||||
);
|
||||
|
||||
// Provider component that wraps your app and makes auth object ...
|
||||
// ... available to any child component that calls useAuth().
|
||||
|
||||
interface AuthContext {
|
||||
user: { id: string; displayName: string } | null;
|
||||
isLoaded: boolean;
|
||||
loginWithSpotifyProps: () => { href: string };
|
||||
}
|
||||
|
||||
export const ProvideAuth: React.FC = ({ children }) => {
|
||||
const auth = useProvideAuth();
|
||||
|
||||
|
|
@ -28,25 +34,51 @@ export function useAuth() {
|
|||
function useProvideAuth(): AuthContext {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [accessToken, setAccessToken] = useState<string>("");
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const loginWithSpotifyProps = () => ({ href: "/api/v1/auth/spotify" });
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const currentUser = await getUsersMe();
|
||||
setUser(currentUser);
|
||||
} catch (err) {
|
||||
if (err instanceof UnauthenticatedError) {
|
||||
// User is not logged in
|
||||
} else {
|
||||
console.error("Error while checking login state:", err);
|
||||
}
|
||||
} finally {
|
||||
setIsLoaded(true);
|
||||
}
|
||||
})();
|
||||
const refreshAccessToken = useCallback(async () => {
|
||||
try {
|
||||
const { accessToken: newAccessToken } = await postAuthTokenRefresh();
|
||||
setAccessToken(newAccessToken);
|
||||
return newAccessToken;
|
||||
} catch (err) {
|
||||
setAccessToken("");
|
||||
setUser(null);
|
||||
setIsLoaded(true);
|
||||
setError(err);
|
||||
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { isLoaded, user, loginWithSpotifyProps };
|
||||
useEffect(() => {
|
||||
refreshAccessToken().catch(() => {
|
||||
console.log("Unable to refresh access token");
|
||||
});
|
||||
}, [refreshAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
async function getUser(token: string) {
|
||||
const newUser = await getUsersMe(token);
|
||||
setUser(newUser);
|
||||
setIsLoaded(true);
|
||||
}
|
||||
|
||||
getUser(accessToken);
|
||||
}, [accessToken]);
|
||||
|
||||
return {
|
||||
isLoaded,
|
||||
user,
|
||||
accessToken,
|
||||
error,
|
||||
refreshAccessToken,
|
||||
loginWithSpotifyProps,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,18 @@ import React from "react";
|
|||
import ReactDOM from "react-dom";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { App } from "./App";
|
||||
import { ProvideApiClient } from "./hooks/use-api-client";
|
||||
import { ProvideAuth } from "./hooks/use-auth";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<ProvideAuth>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
<ProvideApiClient>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</ProvideApiClient>
|
||||
</ProvideAuth>
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue