diff --git a/src/app/routes/Landing.tsx b/src/app/routes/Landing.tsx index 0bd4f03..4333587 100644 --- a/src/app/routes/Landing.tsx +++ b/src/app/routes/Landing.tsx @@ -2,18 +2,13 @@ import { useNavigate } from 'react-router' import { useEffect} from "react" import BungieLogin from '../../features/auth/BungieLogin' -import { generateToken, regenerateTokens } from '../../lib/TokenService' -import { isAuthenticated } from '../../lib/AuthService' +import { generateToken, regenerateTokens } from '../../lib/bungie_api/TokenService' +import { isAuthenticated } from '../../lib/bungie_api/AuthService' export const LandingRoute = () => { const navigate = useNavigate() - - function getAuthCodeFromURL(): string | null { - return window.location.href.includes("code=") ? window.location.href.split('code=')[1] : null - } - useEffect( () => { if (isAuthenticated()) { @@ -24,20 +19,6 @@ export const LandingRoute = () => { console.log("Token regenerated and authenticated") navigate('/app') } - else { - console.log("Not authenticated") - const authCode = getAuthCodeFromURL() - - if (authCode !== null) { - console.log("Auth code found, storing and attempting token generation") - localStorage.setItem("authCode", "" + authCode) - - if (generateToken(false)) { - console.log("Fresh token generated") - navigate('/app') - } - } - } }, []) diff --git a/src/app/routes/Return.tsx b/src/app/routes/Return.tsx new file mode 100644 index 0000000..595763d --- /dev/null +++ b/src/app/routes/Return.tsx @@ -0,0 +1,22 @@ +import { useEffect } from "react" +import { useNavigate } from "react-router-dom" +import { handleAuthReturn } from "../../features/auth/AuthReturn" + +export const ReturnRoute = () => { + + const navigate = useNavigate() + + useEffect( () => { + + if (handleAuthReturn()) { + navigate('/') + } + + }, []) + + return ( +
+ Authentication Error +
+ ) +} \ No newline at end of file diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index fe85dac..9a50442 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -1,6 +1,6 @@ import { createBrowserRouter } from 'react-router-dom'; import { Dashboard } from './Dashboard'; -import { ProtectedRoute } from '../../lib/AuthService'; +import { ProtectedRoute } from '../../lib/bungie_api/AuthService'; export const createRouter = () => { return createBrowserRouter ([ @@ -11,6 +11,13 @@ export const createRouter = () => { return { Component: LandingRoute } } }, + { + path: '/return', + lazy: async () => { + const { ReturnRoute } = await import ('./Return') + return { Component: ReturnRoute } + } + }, { path: '/app', element: ( diff --git a/src/features/auth/AuthReturn.tsx b/src/features/auth/AuthReturn.tsx new file mode 100644 index 0000000..cbd6461 --- /dev/null +++ b/src/features/auth/AuthReturn.tsx @@ -0,0 +1,32 @@ +import { generateToken } from "../../lib/bungie_api/TokenService" +import { setTokens } from "../../lib/bungie_api/TokensStore" + +function getAuthCodeFromURL(): string | null { + return window.location.href.includes("code=") ? window.location.href.split('code=')[1] : null +} + +export function handleAuthReturn(): boolean { + + const code = getAuthCodeFromURL() + + if (!code?.length) { + console.log("Could not find authorization code") + return false + } + + try { + const tokens = generateToken(false, code) + + if (tokens) { + setTokens(tokens) + + return true + } + + } + catch (error) { + console.log(error) + } + + return false +} \ No newline at end of file diff --git a/src/features/auth/BungieLogin.tsx b/src/features/auth/BungieLogin.tsx index 4156187..114bf75 100644 --- a/src/features/auth/BungieLogin.tsx +++ b/src/features/auth/BungieLogin.tsx @@ -1,5 +1,5 @@ import React from "react" -import { authenticate } from "../../lib/AuthService" +import { authenticate } from "../../lib/bungie_api/AuthService" const BungieLogin: React.FC = () => { diff --git a/src/features/membership/BungieAccount.ts b/src/features/membership/BungieAccount.ts index 9f51ef9..e4d75e8 100644 --- a/src/features/membership/BungieAccount.ts +++ b/src/features/membership/BungieAccount.ts @@ -1,5 +1,5 @@ -import { _get } from "../../lib/BungieApiClient"; -import { getMembershipId, getTokens } from "../../lib/TokenStoreService"; +import { _get } from "../../lib/bungie_api/BungieApiClient"; +import { getMembershipId, getTokens } from "../../lib/bungie_api/TokensStore"; export function getCurrentMembershipData() { diff --git a/src/features/profile/DestinyProfile.ts b/src/features/profile/DestinyProfile.ts index 445a86a..e521211 100644 --- a/src/features/profile/DestinyProfile.ts +++ b/src/features/profile/DestinyProfile.ts @@ -1,5 +1,5 @@ -import { _get } from "../../lib/BungieApiClient" -import { getTokens } from "../../lib/TokenStoreService" +import { _get } from "../../lib/bungie_api/BungieApiClient" +import { getTokens } from "../../lib/bungie_api/TokensStore" export function getProfile(destinyMembershipId: string) { diff --git a/src/lib/TokenService.ts b/src/lib/TokenService.ts deleted file mode 100644 index 3b70b53..0000000 --- a/src/lib/TokenService.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { Token, Tokens, getTokens, setTokens } from "./TokenStoreService" -import { _post } from "./BungieApiClient" - -export function canTokensRefresh() { - const tokens = getTokens() - - if (!tokens) { - return false - } - - return tokens && !isTokenExpired(tokens.refreshToken) -} - -export function expireTokens() { - const tokens = getTokens() - - if (tokens) { - tokens.accessToken.acquired = 0 - tokens.accessToken.expires = 0 - setTokens(tokens) - } -} - -export function isTokenExpired(token?: Token) { - - if (!token) { - return true - } - - const expiration = getTokenExpiration(token) - - return Date.now() > expiration -} - -function getTokenExpiration(token?: Token): number { - return (token && 'acquired' in token && 'expires' in token) ? token.acquired + token.expires * 1000 : 0 -} - -export function generateToken(refresh: boolean): boolean { - - const CLIENT_ID = import.meta.env.VITE_CLIENT_ID - const CLIENT_SECRET = import.meta.env.VITE_CLIENT_SECRET - const AUTH_CODE = localStorage.getItem("authCode") - const REFRESH_TOKEN = getTokens()?.refreshToken - - let body = refresh === false ? - `grant_type=authorization_code&code=${AUTH_CODE}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}` : - `grant_type=refresh_token&refresh_token=${REFRESH_TOKEN}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}` - - _post('/Platform/App/OAuth/Token/', body, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${window.btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)}` - } - }) - .then(response => { - if (response.data.access_token) { - - const aquired = Date.now() - - const accessToken: Token = { - value: response.data.access_token, - expires: response.data.expires_in, - name: 'access', - acquired: aquired - } - - const refreshToken: Token = { - value: response.data.refresh_token, - expires: response.data.refresh_expires_in, - name: 'refresh', - acquired: aquired - } - - const tokens: Tokens = { - accessToken, - refreshToken, - membershipId: response.data.membership_id - } - - setTokens(tokens) - - return true - } - else { - console.log("Could not get access token") - return false - } - }) - - return false -} - -export function regenerateTokens(): boolean { - - if (canTokensRefresh()) { - return generateToken(true) - } - - return false -} \ No newline at end of file diff --git a/src/lib/AuthService.tsx b/src/lib/bungie_api/AuthService.tsx similarity index 93% rename from src/lib/AuthService.tsx rename to src/lib/bungie_api/AuthService.tsx index 7b9ccd2..e94a914 100644 --- a/src/lib/AuthService.tsx +++ b/src/lib/bungie_api/AuthService.tsx @@ -1,6 +1,6 @@ import { Navigate } from "react-router-dom" import { isTokenExpired } from "./TokenService" -import { getTokens } from "./TokenStoreService" +import { getTokens } from "./TokensStore" export function authenticate(): void { window.location.replace(`https://www.bungie.net/en/OAuth/Authorize?client_id=${import.meta.env.VITE_CLIENT_ID}&response_type=code`) diff --git a/src/lib/BungieApiClient.ts b/src/lib/bungie_api/BungieApiClient.ts similarity index 100% rename from src/lib/BungieApiClient.ts rename to src/lib/bungie_api/BungieApiClient.ts diff --git a/src/lib/bungie_api/TokenService.ts b/src/lib/bungie_api/TokenService.ts new file mode 100644 index 0000000..be22f52 --- /dev/null +++ b/src/lib/bungie_api/TokenService.ts @@ -0,0 +1,111 @@ +import { Token, Tokens, getTokens, setTokens } from "./TokensStore" +import { _post } from "./BungieApiClient" +import { CLIENT_ID, CLIENT_SECRET } from "./utils" +import { AxiosResponse } from "axios" + +export function canTokensRefresh() { + const tokens = getTokens() + + if (!tokens) { + return false + } + + return tokens && !isTokenExpired(tokens.refreshToken) +} + +export function expireTokens() { + const tokens = getTokens() + + if (tokens) { + tokens.accessToken.acquired = 0 + tokens.accessToken.expires = 0 + setTokens(tokens) + } +} + +export function isTokenExpired(token?: Token) { + + if (!token) { + return true + } + + const expiration = getTokenExpiration(token) + + return Date.now() > expiration +} + +function getTokenExpiration(token?: Token): number { + return (token && 'acquired' in token && 'expires' in token) ? token.acquired + token.expires * 1000 : 0 +} + +export function generateToken(refresh: boolean, authCode=""): Tokens | null { + const REFRESH_TOKEN = getTokens()?.refreshToken + var returnToken = null + + let body = refresh === false ? + `grant_type=authorization_code&code=${authCode}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}` : + `grant_type=refresh_token&refresh_token=${REFRESH_TOKEN}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}` + + _post('/Platform/App/OAuth/Token/', body, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': `Basic ${window.btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)}` + } + }) + .then(response => { + + returnToken = handleTokenResponse(response) + + if (refresh) { + setTokens(returnToken) + console.log("Tokens successfully generated") + } + }) + .catch(error => { + throw new Error(error) + }) + + return returnToken +} + +function handleTokenResponse(response: AxiosResponse): Tokens { + if (response.data.access_token) { + + const aquired = Date.now() + + const accessToken: Token = { + value: response.data.access_token, + expires: response.data.expires_in, + name: 'access', + acquired: aquired + } + + const refreshToken: Token = { + value: response.data.refresh_token, + expires: response.data.refresh_expires_in, + name: 'refresh', + acquired: aquired + } + + const tokens: Tokens = { + accessToken, + refreshToken, + membershipId: response.data.membership_id + } + + return tokens + } + else { + throw new Error(`Invalid response: ${JSON.stringify(response)}`) + } +} + +export function regenerateTokens(): boolean { + + if (canTokensRefresh()) { + generateToken(true) + return true + } + + return false +} \ No newline at end of file diff --git a/src/lib/TokenStoreService.ts b/src/lib/bungie_api/TokensStore.ts similarity index 96% rename from src/lib/TokenStoreService.ts rename to src/lib/bungie_api/TokensStore.ts index dd2f2a8..87d37aa 100644 --- a/src/lib/TokenStoreService.ts +++ b/src/lib/bungie_api/TokensStore.ts @@ -12,7 +12,7 @@ export interface Tokens { membershipId: string } -const key = 'Tokens' +const key = 'authTokens' export function getTokens(): Tokens | null { const tokenString = localStorage.getItem(key) diff --git a/src/lib/bungie_api/utils.ts b/src/lib/bungie_api/utils.ts new file mode 100644 index 0000000..4171a17 --- /dev/null +++ b/src/lib/bungie_api/utils.ts @@ -0,0 +1,6 @@ + +export const API_KEY = import.meta.env.VITE_API_KEY + +export const CLIENT_ID = import.meta.env.VITE_CLIENT_ID + +export const CLIENT_SECRET = import.meta.env.VITE_CLIENT_SECRET \ No newline at end of file