diff --git a/package.json b/package.json
index 960b5bc..dc2ce30 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"@types/node": "^14.14.37",
"@types/react": "^17.0.3",
"@types/react-router-dom": "^5.1.7",
+ "axios": "^0.21.1",
"bootstrap": "^4.6.0",
"jquery": "^3.6.0",
"popper.js": "^1.16.1",
diff --git a/src/App.tsx b/src/App.tsx
index 6cb2d84..612b174 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -16,10 +16,11 @@ import Main from './components/Main';
//import 'bootstrap/dist/js/bootstrap.bundle.min';
import './App.css';
import Authenticator from './components/Authenticator';
+import AuthenticationWrapper from './components/AuthenticationWrapper';
function App() {
return (
-
+
@@ -37,7 +38,7 @@ function App() {
-
+
);
}
diff --git a/src/api.ts b/src/api.ts
deleted file mode 100644
index 79bd110..0000000
--- a/src/api.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-// eslint-disable-next-line
-const dev = process.env.NODE_ENV === 'development';
-export const API_ENDPOINT = 'http://localhost:5000/api';
-export const ION_AUTHORIZATION_ENDPOINT =
- 'https://ion.tjhsst.edu/oauth/authorize?response_type=code&client_id=rNa6n9YSg8ftINdyVPpUsaMuxNbHLo9dh1OsOktR&scope=read&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fion%2Fcallback';
diff --git a/src/api/api.ts b/src/api/api.ts
new file mode 100644
index 0000000..794d9bf
--- /dev/null
+++ b/src/api/api.ts
@@ -0,0 +1,42 @@
+import axios from 'axios';
+import { APIError } from './error';
+import { makeAPIGetCall } from './utils';
+
+// eslint-disable-next-line
+const dev = process.env.NODE_ENV === 'development';
+export const API_ENDPOINT = 'http://localhost:5000/api';
+export const ION_AUTHORIZATION_ENDPOINT =
+ 'https://ion.tjhsst.edu/oauth/authorize?response_type=code&client_id=rNa6n9YSg8ftINdyVPpUsaMuxNbHLo9dh1OsOktR&scope=read&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fion%2Fcallback';
+
+axios.defaults.baseURL = API_ENDPOINT;
+
+/**
+ *
+ * @param code The code sent by the OAuth provider
+ * @param provider The provider that send the OAuth code
+ */
+export async function createSession(
+ code: string,
+ provider: string
+): Promise {
+ const response = await axios.post('/auth/create_session', {
+ code,
+ provider,
+ });
+
+ if (response.status === 200) {
+ return response.data.session_id;
+ } else {
+ throw new APIError('/auth/create_session', response.data.error);
+ }
+}
+
+export async function getMe(): Promise {
+ let result = await makeAPIGetCall('/users/@me');
+ return result.data.data;
+}
+
+export async function getPublicUser(id: string): Promise {
+ let result = await makeAPIGetCall('/users/' + id);
+ return result.data.data;
+}
diff --git a/src/api/error.ts b/src/api/error.ts
new file mode 100644
index 0000000..40815f7
--- /dev/null
+++ b/src/api/error.ts
@@ -0,0 +1,5 @@
+export class APIError extends Error {
+ constructor(route: string, message: string) {
+ super('API Error @' + route + ': ' + message);
+ }
+}
diff --git a/src/api/utils.ts b/src/api/utils.ts
new file mode 100644
index 0000000..7c6f25d
--- /dev/null
+++ b/src/api/utils.ts
@@ -0,0 +1,102 @@
+import axios, { AxiosResponse } from 'axios';
+import getSessionId from '../lib/getSessionId';
+import { APIError } from './error';
+
+export function createUrlWithGetParameters(
+ url: string,
+ params?: Record
+) {
+ if (params) {
+ // Stringify the parameters
+ let stringifiedParameters = '';
+ for (let [name, value] of Object.entries(params)) {
+ if (value) {
+ stringifiedParameters +=
+ encodeURIComponent(name) + '=' + encodeURIComponent(value);
+ }
+ }
+ if (stringifiedParameters) {
+ url += '?' + stringifiedParameters;
+ }
+ }
+ return url;
+}
+
+export function graphql(query: string, variables?: any) {
+ return new Promise((resolve, reject) => {
+ axios
+ .post(
+ '/graphql',
+ { query, variables },
+ { headers: { Authorization: 'Bearer ' + getSessionId() } }
+ )
+ .then((successfulResponse) => {
+ resolve(successfulResponse.data.data);
+ })
+ .catch((error) => {
+ if (error.response.status === 401) {
+ localStorage.removeItem('session_id');
+ window.location.pathname = '/';
+ } else {
+ reject(new APIError('/api/users/@me', error.response.data.error));
+ }
+ });
+ });
+}
+
+export function makeAPIPostCall(url: string, data?: any) {
+ return new Promise((resolve, reject) => {
+ axios
+ .post(url, data, {
+ headers: { Authorization: 'Bearer ' + getSessionId() },
+ })
+ .then((successfulResponse) => resolve(successfulResponse))
+ .catch((error) => {
+ if (error.response?.status === 401) {
+ localStorage.removeItem('session_id');
+ window.location.pathname = '/';
+ } else {
+ reject(new APIError(url, error.response?.data?.error));
+ }
+ });
+ });
+}
+
+export function makeAPIGetCall(
+ url: string,
+ params?: Record
+) {
+ url = createUrlWithGetParameters(url, params);
+ return new Promise((resolve, reject) => {
+ axios
+ .get(url, {
+ headers: { Authorization: 'Bearer ' + getSessionId() },
+ })
+ .then((successfulResponse) => resolve(successfulResponse))
+ .catch((error) => {
+ if (error.response?.status === 401) {
+ localStorage.removeItem('session_id');
+ window.location.pathname = '/';
+ } else {
+ reject(error);
+ }
+ });
+ });
+}
+
+export function makeAPIDeleteCall(url: string) {
+ return new Promise((resolve, reject) => {
+ axios
+ .delete(url, {
+ headers: { Authorization: 'Bearer ' + getSessionId() },
+ })
+ .then((successfulResponse) => resolve(successfulResponse))
+ .catch((error) => {
+ if (error.response?.status === 401) {
+ localStorage.removeItem('session_id');
+ } else {
+ reject(new APIError(url, error.response.data.error));
+ }
+ });
+ });
+}
diff --git a/src/components/AuthenticationContext.ts b/src/components/AuthenticationContext.ts
new file mode 100644
index 0000000..90c2050
--- /dev/null
+++ b/src/components/AuthenticationContext.ts
@@ -0,0 +1,19 @@
+import { createContext } from 'react';
+
+export type AuthState = {
+ isLoggedIn: boolean | null;
+ user: Carpool.User | null;
+
+ /**
+ * Function that can be used to trigger an auth state refresh.
+ */
+ refreshAuthState: (() => void) | null;
+};
+
+const AuthContext = createContext({
+ isLoggedIn: false,
+ user: null,
+ refreshAuthState: null,
+});
+
+export default AuthContext;
diff --git a/src/components/AuthenticationWrapper.tsx b/src/components/AuthenticationWrapper.tsx
new file mode 100644
index 0000000..00aefd5
--- /dev/null
+++ b/src/components/AuthenticationWrapper.tsx
@@ -0,0 +1,39 @@
+import { useCallback, useEffect, useState } from 'react';
+import { getMe } from '../api/api';
+import AuthContext, { AuthState } from './AuthenticationContext';
+
+export default function AuthenticationWrapper({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const sessionId = localStorage.getItem('session_id');
+ // Prevent race conditions
+ const [authState, setAuthState] = useState({
+ isLoggedIn: null,
+ user: null,
+ refreshAuthState: null,
+ });
+
+ const refreshAuthState = useCallback(() => {
+ if (sessionId) {
+ getMe().then((user) => {
+ setAuthState({ isLoggedIn: true, user, refreshAuthState });
+ });
+ } else {
+ setAuthState({ isLoggedIn: false, user: null, refreshAuthState });
+ }
+ }, [sessionId]);
+
+ useEffect(() => {
+ refreshAuthState();
+ }, [refreshAuthState]);
+
+ if (authState?.isLoggedIn == null) {
+ return null;
+ } else {
+ return (
+ {children}
+ );
+ }
+}
diff --git a/src/components/Authenticator.tsx b/src/components/Authenticator.tsx
index 5a22f91..2002aa3 100644
--- a/src/components/Authenticator.tsx
+++ b/src/components/Authenticator.tsx
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { Redirect, useLocation, useParams } from 'react-router-dom';
-import { API_ENDPOINT } from '../api';
+import { API_ENDPOINT } from '../api/api';
export default function Authenticator() {
const { provider } = useParams<{ provider: string }>();
diff --git a/src/components/Group.tsx b/src/components/Group.tsx
index b9eadcf..92749d0 100644
--- a/src/components/Group.tsx
+++ b/src/components/Group.tsx
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
-import { API_ENDPOINT } from '../api';
+import { API_ENDPOINT } from '../api/api';
const maybePluralize = (count: number, noun: string, suffix = 's') =>
`${count} ${noun}${count !== 1 ? suffix : ''}`;
diff --git a/src/components/Home.tsx b/src/components/Home.tsx
index cd1d105..a5555ad 100644
--- a/src/components/Home.tsx
+++ b/src/components/Home.tsx
@@ -1,4 +1,4 @@
-import { ION_AUTHORIZATION_ENDPOINT } from '../api';
+import { ION_AUTHORIZATION_ENDPOINT } from '../api/api';
import Button from '@material-ui/core/Button';
export default function Home() {
diff --git a/src/components/MyPools.tsx b/src/components/MyPools.tsx
index 2973c70..0dc3fc4 100644
--- a/src/components/MyPools.tsx
+++ b/src/components/MyPools.tsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
-import { API_ENDPOINT } from '../api';
+import { API_ENDPOINT } from '../api/api';
const MyPools = () => {
// const id = props.match.params.id;
diff --git a/src/components/Pools.tsx b/src/components/Pools.tsx
index bc9b8bf..b5c1457 100644
--- a/src/components/Pools.tsx
+++ b/src/components/Pools.tsx
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
-import { API_ENDPOINT } from '../api';
+import { API_ENDPOINT } from '../api/api';
const maybePluralize = (count: number, noun: string, suffix = 's') =>
`${count} ${noun}${count !== 1 ? suffix : ''}`;
diff --git a/src/lib/getSessionId.ts b/src/lib/getSessionId.ts
new file mode 100644
index 0000000..f1421d5
--- /dev/null
+++ b/src/lib/getSessionId.ts
@@ -0,0 +1,3 @@
+export default function getSessionId() {
+ return localStorage.getItem('session_id');
+}
diff --git a/src/types.d.ts b/src/types.d.ts
index e1553f1..f1dfc35 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -4,6 +4,14 @@ declare namespace Carpool {
member_ids: string[];
}
+ // Omits the email attribute
+ export interface PublicUser {
+ id: string;
+ username: string;
+ first_name: string;
+ last_name: string;
+ }
+
export interface User {
id: string;
email: string;
diff --git a/yarn.lock b/yarn.lock
index 71f9dca..0d7f88d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2647,6 +2647,13 @@ axe-core@^4.0.2:
resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.1.2.tgz"
integrity sha512-V+Nq70NxKhYt89ArVcaNL9FDryB3vQOd+BFXZIfO3RP6rwtj+2yqqqdHEkacutglPaZLkJeuXKCjCJDMGPtPqg==
+axios@^0.21.1:
+ version "0.21.1"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
+ integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
+ dependencies:
+ follow-redirects "^1.10.0"
+
axobject-query@^2.2.0:
version "2.2.0"
resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz"
@@ -5163,6 +5170,11 @@ follow-redirects@^1.0.0:
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz"
integrity sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==
+follow-redirects@^1.10.0:
+ version "1.13.3"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267"
+ integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==
+
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz"