Skip to content
This repository was archived by the owner on Aug 2, 2025. It is now read-only.

Commit 71096a5

Browse files
Merge pull request #64 from debater-coder/revert-62-revert-59-50-use-react-routers-data-fetching-mechanism
Revert "Revert "#50 use react routers data fetching mechanism""
2 parents cd8478d + dd57a1f commit 71096a5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1967
-1845
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# [Timetabl](https://www.timetabl.app)
2+
23
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
4+
35
[![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-)
6+
47
<!-- ALL-CONTRIBUTORS-BADGE:END -->
58

69
![GitHub deployments](https://img.shields.io/github/deployments/debater-coder/timetabl-app/production?label=vercel&logo=vercel)
@@ -21,7 +24,7 @@ Timetabl is built on three principles:
2124

2225
3. **Be secure**
2326
Unlike other bell time apps, Timetabl stores your tokens in HTTPS-only cookies instead of `localStorage`, so that even in the event of an XSS attack, the tokens are secure.
24-
27+
2528
## Contributors
2629

2730
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->

package-lock.json

Lines changed: 1071 additions & 978 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"@react-three/fiber": "^8.9.1",
4141
"@rollup/plugin-replace": "^4.0.0",
4242
"@tanstack/query-sync-storage-persister": "^4.0.10",
43-
"@tanstack/react-query": "4.0.10",
43+
"@tanstack/react-query": "^4.20.4",
4444
"@tanstack/react-query-devtools": "^4.0.10",
4545
"@tanstack/react-query-persist-client": "^4.0.10",
4646
"@types/react-dom": "^18.0.9",
@@ -62,7 +62,8 @@
6262
"react": "^18.1.0",
6363
"react-dom": "^18.1.0",
6464
"react-icons": "^4.4.0",
65-
"react-router-dom": "^6.3.0",
65+
"react-query-kit": "^1.3.1",
66+
"react-router-dom": "^6.5.0",
6667
"rollup-plugin-workbox": "^6.2.0",
6768
"three": "^0.146.0",
6869
"web-vitals": "^3.0.1",
@@ -81,7 +82,7 @@
8182
"@typescript-eslint/eslint-plugin": "^5.38.0",
8283
"@typescript-eslint/parser": "^5.38.0",
8384
"@vercel/node": "^2.6.3",
84-
"@vitejs/plugin-react": "^2.0.0",
85+
"@vitejs/plugin-react": "^3.0.0",
8586
"cypress": "^11.2.0",
8687
"eslint": "^8.23.1",
8788
"eslint-config-prettier": "^8.5.0",
@@ -94,6 +95,6 @@
9495
"npm-run-all": "^4.1.5",
9596
"prettier": "2.6.2",
9697
"start-server-and-test": "^1.14.0",
97-
"vite": "^3.0.2"
98+
"vite": "^4.0.2"
9899
}
99100
}

src/Auth.tsx

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { QueryClient } from "@tanstack/react-query";
2+
import _ from "lodash";
3+
import config from "../config";
4+
import HTTPError from "./errors/HTTPError";
5+
import { toast } from "./toast";
6+
7+
export class Auth {
8+
loggedIn: boolean;
9+
loading: boolean;
10+
shouldRedirect: boolean;
11+
refreshing: boolean;
12+
shouldLogin: boolean;
13+
queryClient: QueryClient;
14+
15+
constructor(queryClient: QueryClient) {
16+
this.login = this.login.bind(this);
17+
this.logout = this.logout.bind(this);
18+
this.refresh = this.refresh.bind(this);
19+
this.handleRedirect = this.handleRedirect.bind(this);
20+
this.loggedIn = false;
21+
this.loading = false;
22+
this.shouldRedirect = false;
23+
this.refreshing = false;
24+
this.shouldLogin = false;
25+
this.queryClient = queryClient;
26+
}
27+
28+
/**
29+
* Generate a secure random string using the browser crypto functions
30+
*/
31+
protected generateRandomString() {
32+
const array = new Uint32Array(28);
33+
window.crypto.getRandomValues(array);
34+
return Array.from(array, (dec) => ("0" + dec.toString(16)).substr(-2)).join(
35+
""
36+
);
37+
}
38+
39+
/** Calculate the SHA256 hash of the input text.
40+
* Returns a promise that resolves to an ArrayBuffer
41+
*/
42+
protected sha256(plain: string) {
43+
const encoder = new TextEncoder();
44+
const data = encoder.encode(plain);
45+
return window.crypto.subtle.digest("SHA-256", data);
46+
}
47+
48+
/**
49+
* Base64-urlencodes the input string
50+
*/
51+
protected base64urlencode(str: ArrayBuffer) {
52+
// Convert the ArrayBuffer to string using Uint8 array to conver to what btoa accepts.
53+
// btoa accepts chars only within ascii 0-255 and base64 encodes them.
54+
// Then convert the base64 encoded to base64url encoded
55+
// (replace + with -, replace / with _, trim trailing =)
56+
return window
57+
.btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
58+
.replace(/\+/g, "-")
59+
.replace(/\//g, "_")
60+
.replace(/=+$/, "");
61+
}
62+
63+
/**
64+
* Return the base64-urlencoded sha256 hash for the PKCE challenge
65+
*/
66+
protected async pkceChallengeFromVerifier(v: string) {
67+
const hashed = await this.sha256(v);
68+
return this.base64urlencode(hashed);
69+
}
70+
71+
async login() {
72+
// Create and store a random "state" value
73+
const state = this.generateRandomString();
74+
localStorage.setItem("pkce_state", state);
75+
76+
// Create and store a new PKCE code_verifier (the plaintext random secret)
77+
const code_verifier = this.generateRandomString();
78+
localStorage.setItem("pkce_code_verifier", code_verifier);
79+
80+
// Hash and base64-urlencode the secret to use as the challenge
81+
const code_challenge = await this.pkceChallengeFromVerifier(code_verifier);
82+
83+
// Build the authorization URL
84+
// Redirect to the authorization server
85+
window.location.href =
86+
config.authorization_endpoint +
87+
"?response_type=code" +
88+
"&client_id=" +
89+
encodeURIComponent(config.client_id) +
90+
"&state=" +
91+
encodeURIComponent(state) +
92+
"&scope=" +
93+
encodeURIComponent(config.scopes) +
94+
"&redirect_uri=" +
95+
encodeURIComponent(config.redirect_uri) +
96+
"&code_challenge=" +
97+
encodeURIComponent(code_challenge) +
98+
"&code_challenge_method=S256";
99+
}
100+
101+
async logout(rerender?: () => void) {
102+
await fetch("/api/token", {
103+
method: "DELETE",
104+
});
105+
localStorage.setItem("loggedIn", "false");
106+
this.queryClient.clear();
107+
this.loggedIn = false;
108+
rerender?.();
109+
}
110+
111+
refresh = _.throttle(async (rerender?: () => void) => {
112+
this.refreshing = true;
113+
try {
114+
const res = await fetch("/api/token", {
115+
method: "PATCH",
116+
body: JSON.stringify({
117+
client_id: config.client_id,
118+
}),
119+
headers: {
120+
"Content-Type": "application/json; charset=UTF-8",
121+
},
122+
});
123+
124+
if (!res.ok) {
125+
throw new HTTPError(res.status);
126+
}
127+
128+
this.refreshing = false;
129+
} catch (error) {
130+
this.refreshing = false;
131+
this.shouldLogin = true;
132+
toast({
133+
title:
134+
"Something went wrong, try logging in and out if the issue persists.",
135+
description: error.message,
136+
status: "error",
137+
isClosable: true,
138+
});
139+
throw error;
140+
} finally {
141+
rerender?.();
142+
}
143+
}, 1000 * 60);
144+
145+
handleRedirect(rerender?: () => void) {
146+
// Get query
147+
const query = Object.fromEntries(
148+
new URLSearchParams(window.location.search).entries()
149+
);
150+
151+
// Clear query
152+
window.history.replaceState({}, null, location.pathname);
153+
154+
// Oauth Redirect Handling
155+
try {
156+
// Error check
157+
if (query.error) {
158+
toast({
159+
title: query.error,
160+
description: query.error_description,
161+
status: "error",
162+
});
163+
}
164+
165+
// If the server returned an authorization code, attempt to exchange it for an access token
166+
if (query.code) {
167+
// Verify state matches what we set at the beginning
168+
if (localStorage.getItem("pkce_state") !== query.state) {
169+
throw new Error(
170+
"Invalid state! If you are seeing this message it may mean that the authorisation server is FAKE"
171+
);
172+
}
173+
174+
// Optimistically login but set loading to true
175+
this.loggedIn = true;
176+
this.shouldRedirect = true;
177+
this.loading = true;
178+
179+
rerender?.();
180+
181+
// Exchange code for access token
182+
fetch("/api/token", {
183+
method: "POST",
184+
headers: {
185+
"Content-Type": "application/json;charset=utf-8",
186+
},
187+
body: JSON.stringify({
188+
code: query.code,
189+
client_id: config.client_id,
190+
redirect_uri: config.redirect_uri,
191+
code_verifier: localStorage.getItem("pkce_code_verifier"),
192+
}),
193+
})
194+
.then((r) => {
195+
if (!r.ok) {
196+
throw new Error(String(r.status) + r.statusText);
197+
}
198+
199+
// Persist logged in state and stop loading
200+
localStorage.setItem("loggedIn", "true");
201+
this.loading = false;
202+
rerender?.();
203+
})
204+
.catch(() => {
205+
this.loading = false;
206+
rerender?.();
207+
});
208+
}
209+
} finally {
210+
// Clean these up since we don't need them anymore
211+
localStorage.removeItem("pkce_state");
212+
localStorage.removeItem("pkce_code_verifier");
213+
}
214+
215+
// Log in if already logged in
216+
if (localStorage.getItem("loggedIn") === "true") {
217+
this.loggedIn = true;
218+
}
219+
220+
this.shouldRedirect = true;
221+
rerender?.();
222+
}
223+
}

src/components/App/App.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,14 @@ import Nav from "../Nav";
22
import { Outlet } from "react-router-dom";
33
import { Flex } from "@chakra-ui/react";
44
import { Box } from "@chakra-ui/react";
5-
import { ErrorBoundary } from "../ErrorBoundary";
65

76
export default () => {
87
return (
98
<Flex direction={"column"} width={"100vw"} maxW="full" height={"100vh"}>
109
<Nav />
1110
<Box mt={"100px"} />
1211
<Flex direction={"column"} w="full" h="full" maxH={"calc(100% - 100px)"}>
13-
<ErrorBoundary>
14-
<Outlet />
15-
</ErrorBoundary>
12+
<Outlet />
1613
</Flex>
1714
</Flex>
1815
);

src/components/BottomNavSheet/BottomNavSheet.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
import SidebarButton from "../Sidebar/SidebarButton";
88
import { motion, PanInfo } from "framer-motion";
99
import { useEffect, useRef, useState } from "react";
10-
import { routes } from "../../routes";
10+
import { pages } from "../../pages";
1111

1212
const Flex = motion(ChakraFlex);
1313

@@ -80,7 +80,7 @@ export const BottomNavSheet = () => {
8080
borderTop="none"
8181
borderColor={useColorModeValue("gray.200", "gray.700")}
8282
>
83-
{routes.pinned.map((routes) => (
83+
{pages.pinned.map((routes) => (
8484
<SidebarButton
8585
key={routes.path}
8686
name={routes.name}
@@ -92,7 +92,7 @@ export const BottomNavSheet = () => {
9292
))}
9393
</Flex>
9494
<SimpleGrid columns={4} w="full">
95-
{routes.unpinned.map((routes) => (
95+
{pages.unpinned.map((routes) => (
9696
<SidebarButton
9797
key={routes.path}
9898
name={routes.name}

src/components/DTTPeriod/DTTPeriod.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { chakra, useBoolean, useMediaQuery } from "@chakra-ui/react";
22
import { DateTime } from "luxon";
33
import { useState } from "react";
4-
import { TimetablPeriod } from "../../hooks/sbhsQuery/use/useDTT";
54
import useSettings from "../../hooks/useSettings";
5+
import { TimetablPeriod } from "../../services/sbhsApi/types";
66
import { Period } from "../Period";
77

88
export const DTTPeriod = ({

src/components/QueryError/QueryError.tsx renamed to src/components/ErrorAlert.tsx/ErrorAlert.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import {
66
Flex,
77
} from "@chakra-ui/react";
88

9-
export default ({ error }: { error: { message?: string } }) => (
9+
export default () => (
1010
<Flex>
1111
<Alert status="error" rounded={5} m={6}>
1212
<AlertIcon />
1313
<AlertTitle>An error occured.</AlertTitle>
1414
<AlertDescription>
15-
{error.message}. Try logging in and out if the error persists.
15+
Try logging in and out if the error persists.
1616
</AlertDescription>
1717
</Alert>
1818
</Flex>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from "./ErrorAlert";

src/components/QueriesHandler/QueriesHandler.tsx

Lines changed: 0 additions & 42 deletions
This file was deleted.

0 commit comments

Comments
 (0)