From f38b852600e5678bb9096d5e4c1e1c41f945029b Mon Sep 17 00:00:00 2001 From: Aponia Date: Mon, 6 May 2024 08:26:08 -0700 Subject: [PATCH 01/18] feat: basic setup for login dialog --- apps/antalmanac/package.json | 1 + apps/antalmanac/src/App.tsx | 68 ++++----- .../src/components/Header/Header.tsx | 3 + .../src/components/Header/LoginButton.tsx | 129 ++++++++++++++++++ pnpm-lock.yaml | 13 ++ 5 files changed, 181 insertions(+), 33 deletions(-) create mode 100644 apps/antalmanac/src/components/Header/LoginButton.tsx diff --git a/apps/antalmanac/package.json b/apps/antalmanac/package.json index ee1079e93..578a902c6 100644 --- a/apps/antalmanac/package.json +++ b/apps/antalmanac/package.json @@ -34,6 +34,7 @@ "@mui/material": "^5.11.7", "@packages/antalmanac-types": "workspace:^", "@react-leaflet/core": "^2.1.0", + "@react-oauth/google": "^0.12.1", "@reactour/tour": "^3.6.1", "@tanstack/react-query": "^4.24.4", "@trpc/client": "^10.30.0", diff --git a/apps/antalmanac/src/App.tsx b/apps/antalmanac/src/App.tsx index 5adf53d2e..48ea199ef 100644 --- a/apps/antalmanac/src/App.tsx +++ b/apps/antalmanac/src/App.tsx @@ -1,18 +1,18 @@ import './App.css'; -import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import { GoogleOAuthProvider } from '@react-oauth/google'; +import { TourProvider } from '@reactour/tour'; import { SnackbarProvider } from 'notistack'; import { useEffect } from 'react'; import ReactGA4 from 'react-ga4'; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; -import { TourProvider } from '@reactour/tour'; import { undoDelete } from './actions/AppStoreActions'; import AppQueryProvider from './providers/Query'; import AppThemeProvider from './providers/Theme'; import AppThemev5Provider from './providers/Themev5'; - -import Home from './routes/Home'; import Feedback from './routes/Feedback'; +import Home from './routes/Home'; const BrowserRouter = createBrowserRouter([ { @@ -46,35 +46,37 @@ export default function App() { - ({ - // The highlighted area - ...base, - rx: 5, - }), - maskWrapper: (base, _) => ({ - // The background/overlay - ...base, - color: 'rgba(0, 0, 0, 0.3)', - }), - popover: (base, _) => ({ - // The text box - ...base, - background: '#fff', - color: 'black', - borderRadius: 5, - boxShadow: '0 0 10px #000', - padding: 20, - }), - }} - > - - - - + + ({ + // The highlighted area + ...base, + rx: 5, + }), + maskWrapper: (base, _) => ({ + // The background/overlay + ...base, + color: 'rgba(0, 0, 0, 0.3)', + }), + popover: (base, _) => ({ + // The text box + ...base, + background: '#fff', + color: 'black', + borderRadius: 5, + boxShadow: '0 0 10px #000', + padding: 20, + }), + }} + > + + + + + diff --git a/apps/antalmanac/src/components/Header/Header.tsx b/apps/antalmanac/src/components/Header/Header.tsx index 49464fd12..addc3ba58 100644 --- a/apps/antalmanac/src/components/Header/Header.tsx +++ b/apps/antalmanac/src/components/Header/Header.tsx @@ -5,6 +5,7 @@ import { ClassNameMap } from '@material-ui/core/styles/withStyles'; import Export from './Export'; import Import from './Import'; import LoadSaveScheduleFunctionality from './LoadSaveFunctionality'; +import LoginButton from './LoginButton'; import AppDrawer from './SettingsMenu'; import Logo from '$assets/logo.svg'; @@ -51,6 +52,8 @@ const Header = ({ classes }: CustomAppBarProps) => { />
+ + {isMobileScreen ? null : ( diff --git a/apps/antalmanac/src/components/Header/LoginButton.tsx b/apps/antalmanac/src/components/Header/LoginButton.tsx new file mode 100644 index 000000000..23f8161b2 --- /dev/null +++ b/apps/antalmanac/src/components/Header/LoginButton.tsx @@ -0,0 +1,129 @@ +import { + Box, + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + FormControlLabel, + Stack, + TextField, + Typography, +} from '@mui/material'; +import { GoogleLogin } from '@react-oauth/google'; +import { useState } from 'react'; + +/** + * Opens dialog for logging in. + */ +export function LoginButton() { + const [userId, setUserId] = useState(''); + const [rememberMe, setRememberMe] = useState(true); + const [open, setOpen] = useState(false); + + const handleOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + const handleUserIdChange = (event: React.ChangeEvent) => { + setUserId(event.target.value); + }; + + const handleRememberMeChange = (event: React.ChangeEvent) => { + setRememberMe(event.target.checked); + }; + + const handleSubmit = async () => { + console.log('Submitting: ', { userId, rememberMe }); + }; + + return ( + <> + + + Login + + + + + Username + + + + Enter your unique user ID here to login. + + Make sure the user ID is unique and secret, or someone else can overwrite your + schedule. + + + +
+ + + + } + label="Remember Me (Uncheck on shared computers)" + /> + + + + + +
+
+ + + + + Providers + + { + console.log(credentialResponse); + }} + onError={() => { + console.log('Login Failed'); + }} + /> + + +
+
+ + + + +
+ + ); +} + +export default LoginButton; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe1cbba64..655dc5954 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,6 +98,9 @@ importers: '@react-leaflet/core': specifier: ^2.1.0 version: 2.1.0(leaflet@1.9.3)(react-dom@18.2.0)(react@18.2.0) + '@react-oauth/google': + specifier: ^0.12.1 + version: 0.12.1(react-dom@18.2.0)(react@18.2.0) '@reactour/tour': specifier: ^3.6.1 version: 3.6.1(react@18.2.0) @@ -3390,6 +3393,16 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@react-oauth/google@0.12.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@reactour/mask@1.1.0(react@18.2.0): resolution: {integrity: sha512-GkJMLuTs3vTsm4Ryq2uXcE4sMzRP1p4xSd6juSOMqbHa7IVD/UiLCLqJWHR9xGSQPbYhpZAZAORUG5cS0U5tBA==} peerDependencies: From cabe8abb86cc3d90420c2af1ff5f0b2ab89a9580 Mon Sep 17 00:00:00 2001 From: Aponia Date: Mon, 6 May 2024 12:07:35 -0700 Subject: [PATCH 02/18] feat: request handler for username logging in and loading schedule --- apps/antalmanac/package.json | 2 + .../src/components/Header/LoginButton.tsx | 117 ++++++++++++----- apps/antalmanac/src/lib/api/trpc.ts | 4 +- apps/antalmanac/src/lib/trpc.ts | 11 ++ apps/antalmanac/src/providers/Query.tsx | 29 ++++- apps/backend/package.json | 9 +- apps/backend/src/index.ts | 15 ++- apps/backend/src/routers/index.ts | 69 +++++++++- pnpm-lock.yaml | 120 +++++++++++++++++- 9 files changed, 325 insertions(+), 51 deletions(-) create mode 100644 apps/antalmanac/src/lib/trpc.ts diff --git a/apps/antalmanac/package.json b/apps/antalmanac/package.json index 578a902c6..04e7f9751 100644 --- a/apps/antalmanac/package.json +++ b/apps/antalmanac/package.json @@ -38,7 +38,9 @@ "@reactour/tour": "^3.6.1", "@tanstack/react-query": "^4.24.4", "@trpc/client": "^10.30.0", + "@trpc/react-query": "^10.45.2", "@trpc/server": "^10.30.0", + "antalmanac-backend": "workspace:^", "chart.js": "^4.2.1", "classnames": "^2.3.2", "date-fns": "^2.29.3", diff --git a/apps/antalmanac/src/components/Header/LoginButton.tsx b/apps/antalmanac/src/components/Header/LoginButton.tsx index 23f8161b2..a2e8fc0a7 100644 --- a/apps/antalmanac/src/components/Header/LoginButton.tsx +++ b/apps/antalmanac/src/components/Header/LoginButton.tsx @@ -1,28 +1,37 @@ import { Box, Button, - Checkbox, Dialog, DialogActions, DialogContent, DialogTitle, Divider, - FormControlLabel, Stack, TextField, Typography, } from '@mui/material'; import { GoogleLogin } from '@react-oauth/google'; +import type { CredentialResponse } from '@react-oauth/google'; +import { useSnackbar } from 'notistack'; import { useState } from 'react'; +import analyticsEnum, { logAnalytics } from '$lib/analytics'; +import { trpc } from '$lib/trpc'; +import AppStore from '$stores/AppStore'; + /** * Opens dialog for logging in. */ export function LoginButton() { const [userId, setUserId] = useState(''); - const [rememberMe, setRememberMe] = useState(true); const [open, setOpen] = useState(false); + const snackbar = useSnackbar(); + + const usernameLoginMutation = trpc.auth.loginUsername.useMutation(); + + const googleLoginMutation = trpc.auth.loginGoogle.useMutation(); + const handleOpen = () => { setOpen(true); }; @@ -35,12 +44,80 @@ export function LoginButton() { setUserId(event.target.value); }; - const handleRememberMeChange = (event: React.ChangeEvent) => { - setRememberMe(event.target.checked); + const handleUsernameLogin = async (e: React.FormEvent) => { + e.preventDefault(); + + logAnalytics({ + category: analyticsEnum.nav.title, + action: analyticsEnum.nav.actions.LOAD_SCHEDULE, + label: userId, + value: 1, + }); + + const shouldStop = + AppStore.hasUnsavedChanges() && + !window.confirm('Are you sure you want to load a different schedule? You have unsaved changes!'); + + if (shouldStop) return; + + if (userId.length === 0) { + snackbar.enqueueSnackbar('Please enter a user ID.'); + return; + } + + console.log('Submitting user ID: ', userId); + + try { + const response = await usernameLoginMutation.mutateAsync(userId); + + console.log('response: ', response); + + if (response == null) { + snackbar.enqueueSnackbar(`Logged in as "${userId}", no schedules found.`); + setOpen(false); + return; + } + + const loadedSchedule = await AppStore.loadSchedule(response); + + if (loadedSchedule == null) { + AppStore.loadSkeletonSchedule(response); + snackbar.enqueueSnackbar( + `Network error loading course information for "${userId}". + If this continues to happen, please submit a feedback form.`, + { variant: 'error' } + ); + setOpen(false); + return; + } + + snackbar.enqueueSnackbar(`Schedule for username "${userId}" loaded.`, { variant: 'success' }); + setOpen(false); + } catch (e) { + console.error('Error occurred while loading schedules: ', e); + snackbar.enqueueSnackbar( + `Failed to load schedules. If this continues to happen, please submit a feedback form.`, + { variant: 'error' } + ); + setOpen(false); + } }; - const handleSubmit = async () => { - console.log('Submitting: ', { userId, rememberMe }); + const handleGoogleLogin = async (credential: CredentialResponse) => { + if (credential.credential == null) { + console.error('Did not receive google credential'); + return; + } + + return; + + const response = await googleLoginMutation.mutateAsync(credential.credential); + + console.log('response: ', response); + }; + + const handleGoogleError = async () => { + console.log('Login Failed'); }; return ( @@ -65,7 +142,7 @@ export function LoginButton() { -
+ - - } - label="Remember Me (Uncheck on shared computers)" - /> - - - @@ -103,14 +169,7 @@ export function LoginButton() { Providers - { - console.log(credentialResponse); - }} - onError={() => { - console.log('Login Failed'); - }} - /> + diff --git a/apps/antalmanac/src/lib/api/trpc.ts b/apps/antalmanac/src/lib/api/trpc.ts index 3e1ef8e89..0926cf705 100644 --- a/apps/antalmanac/src/lib/api/trpc.ts +++ b/apps/antalmanac/src/lib/api/trpc.ts @@ -1,8 +1,8 @@ import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'; +import type { AppRouter } from 'antalmanac-backend/src/routers'; import superjson from 'superjson'; -import type { AppRouter } from '../../../../backend/src/routers'; -function getEndpoint() { +export function getEndpoint() { if (import.meta.env.VITE_ENDPOINT) { return `https://${import.meta.env.VITE_ENDPOINT}.api.antalmanac.com`; } diff --git a/apps/antalmanac/src/lib/trpc.ts b/apps/antalmanac/src/lib/trpc.ts new file mode 100644 index 000000000..18b1293c1 --- /dev/null +++ b/apps/antalmanac/src/lib/trpc.ts @@ -0,0 +1,11 @@ +import { createTRPCReact } from '@trpc/react-query'; +import type { inferReactQueryProcedureOptions } from '@trpc/react-query'; +import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'; +import type { AppRouter } from 'antalmanac-backend/src/routers'; + +export const trpc = createTRPCReact(); + +// infer the types for your router +export type ReactQueryOptions = inferReactQueryProcedureOptions; +export type RouterInputs = inferRouterInputs; +export type RouterOutputs = inferRouterOutputs; diff --git a/apps/antalmanac/src/providers/Query.tsx b/apps/antalmanac/src/providers/Query.tsx index 64c6db91d..101dd3aa0 100644 --- a/apps/antalmanac/src/providers/Query.tsx +++ b/apps/antalmanac/src/providers/Query.tsx @@ -1,11 +1,36 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { httpBatchLink } from '@trpc/client'; +import { useState } from 'react'; +import transformer from 'superjson'; + +import { getEndpoint } from '$lib/api/trpc'; +import { trpc } from '$lib/trpc'; interface Props { children?: React.ReactNode; } export default function AppQueryProvider(props: Props) { - const queryClient = new QueryClient(); + const [queryClient] = useState(() => new QueryClient()); + const [trpcClient] = useState(() => + trpc.createClient({ + transformer, + links: [ + httpBatchLink({ + url: getEndpoint() + '/trpc', + headers: () => { + return { + credentials: 'include', + }; + }, + }), + ], + }) + ); - return {props.children}; + return ( + + {props.children}; + + ); } diff --git a/apps/backend/package.json b/apps/backend/package.json index 6f1d36e3e..2c2afc3e8 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -19,6 +19,8 @@ "dotenv": "^16.0.3", "envalid": "^7.3.1", "express": "^4.18.2", + "jose": "^5.2.4", + "jsonwebtoken": "^9.0.2", "mongodb": "^5.0.1", "mongoose": "^7.1.0j", "superjson": "^1.12.3", @@ -30,6 +32,7 @@ "@types/aws-lambda": "^8.10.110", "@types/cors": "^2.8.13", "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.6", "@typescript-eslint/eslint-plugin": "^5.52.0", "@typescript-eslint/parser": "^5.52.0", "concurrently": "^8.0.1", @@ -43,11 +46,5 @@ "prettier": "^2.8.4", "tsx": "^3.12.7", "typescript": "^4.9.5" - }, - "lint-staged": { - "*.{js,json,css,html}": [ - "prettier --write", - "git add" - ] } } diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index bb1e2a356..e21d77dd3 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -5,7 +5,7 @@ import { createExpressMiddleware } from '@trpc/server/adapters/express'; import AppRouter from './routers'; import createContext from './context'; import env from './env'; -import connectToMongoDB from '$db/mongodb'; +// import connectToMongoDB from '$db/mongodb'; const corsOptions: CorsOptions = { origin: ['https://antalmanac.com', 'https://www.antalmanac.com', 'https://icssc-projects.github.io/AntAlmanac'], @@ -16,8 +16,7 @@ const MAPBOX_API_URL = 'https://api.mapbox.com'; const PORT = 3000; export async function start(corsEnabled = false) { - await connectToMongoDB(); - + // await connectToMongoDB(); const app = express(); app.use(cors(corsEnabled ? corsOptions : undefined)); app.use(express.json()); @@ -33,16 +32,18 @@ export async function start(corsEnabled = false) { app.use('/mapbox/tiles/*', async (req, res) => { const searchParams = new URLSearchParams(req.query as any); searchParams.set('access_token', env.MAPBOX_ACCESS_TOKEN); - const url = `${MAPBOX_API_URL}/styles/v1/mapbox/streets-v11/tiles/${(req.params as any)[0]}?${searchParams.toString()}`; + const url = `${MAPBOX_API_URL}/styles/v1/mapbox/streets-v11/tiles/${ + (req.params as any)[0] + }?${searchParams.toString()}`; const buffer = await fetch(url).then((res) => res.arrayBuffer()); - res.type('image/png') - res.send(Buffer.from(buffer)) + res.type('image/png'); + res.send(Buffer.from(buffer)); // // res.header('Content-Security-Policy', "img-src 'self'"); // https://stackoverflow.com/questions/56386307/loading-of-a-resource-blocked-by-content-security-policy // // res.header('Access-Control-Allow-Methods', 'GET, OPTIONS') // res.type('image/png') // res.send(result) }); - + app.use( '/trpc', createExpressMiddleware({ diff --git a/apps/backend/src/routers/index.ts b/apps/backend/src/routers/index.ts index 2611cca4a..cb979618b 100644 --- a/apps/backend/src/routers/index.ts +++ b/apps/backend/src/routers/index.ts @@ -1,12 +1,75 @@ -import { router } from '../trpc'; +import { type } from 'arktype'; +import jwt from 'jsonwebtoken'; +import { ddbClient } from '../db/ddb'; +import { procedure, router } from '../trpc'; import newsRouter from './news'; import usersRouter from './users'; -import zotcourseRouter from "./zotcours"; +import zotcourseRouter from './zotcours'; + +export interface GoogleProfile extends Record { + aud: string; + azp: string; + email: string; + email_verified: boolean; + exp: number; + family_name?: string; + given_name: string; + hd?: string; + iat: number; + iss: string; + jti?: string; + locale?: string; + name: string; + nbf?: number; + picture: string; + sub: string; +} + +const privateKey = 'secret'; + +const authRouter = router({ + status: procedure.query(async (context) => { + const authCookie = context.ctx.req.cookies['auth']; + + if (authCookie == null) return; + + try { + const authUser = jwt.decode(authCookie); + return authUser; + } catch (e) { + console.error('Failed to decode auth cookie: ', e); + return; + } + }), + loginGoogle: procedure.input(type('string').assert).mutation(async (context) => { + try { + const idToken = jwt.decode(context.input) as GoogleProfile; + + // Set the auth cookie so the client can be recognized on future requests. + const authCookie = jwt.sign({ googleId: context.input }, privateKey); + context.ctx.res.cookie('auth', authCookie); + + return await ddbClient.getGoogleUserData(idToken.sub); + } catch { + return; + } + }), + loginUsername: procedure.input(type('string').assert).mutation(async (context) => { + // Set the auth cookie so the client can be recognized on future requests. + const authCookie = jwt.sign({ username: context.input }, privateKey); + context.ctx.res.cookie('auth', authCookie, { maxAge: 1000 * 60 * 60, path: '/' }); + + console.log({ authCookie }); + + return await ddbClient.getUserData(context.input); // ?? (await ddbClient.getLegacyUserData(context.input)); + }), +}); const appRouter = router({ + auth: authRouter, news: newsRouter, users: usersRouter, - zotcourse: zotcourseRouter + zotcourse: zotcourseRouter, }); export type AppRouter = typeof appRouter; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 655dc5954..0b087145a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,9 +110,15 @@ importers: '@trpc/client': specifier: ^10.30.0 version: 10.30.0(@trpc/server@10.30.0) + '@trpc/react-query': + specifier: ^10.45.2 + version: 10.45.2(@tanstack/react-query@4.24.10)(@trpc/client@10.30.0)(@trpc/server@10.30.0)(react-dom@18.2.0)(react@18.2.0) '@trpc/server': specifier: ^10.30.0 version: 10.30.0 + antalmanac-backend: + specifier: workspace:^ + version: link:../backend chart.js: specifier: ^4.2.1 version: 4.2.1 @@ -324,6 +330,12 @@ importers: express: specifier: ^4.18.2 version: 4.18.2 + jose: + specifier: ^5.2.4 + version: 5.2.4 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 mongodb: specifier: ^5.0.1 version: 5.0.1 @@ -352,6 +364,9 @@ importers: '@types/express': specifier: ^4.17.17 version: 4.17.17 + '@types/jsonwebtoken': + specifier: ^9.0.6 + version: 9.0.6 '@typescript-eslint/eslint-plugin': specifier: ^5.52.0 version: 5.57.1(@typescript-eslint/parser@5.57.1)(eslint@8.38.0)(typescript@4.9.5) @@ -4074,6 +4089,22 @@ packages: '@trpc/server': 10.30.0 dev: false + /@trpc/react-query@10.45.2(@tanstack/react-query@4.24.10)(@trpc/client@10.30.0)(@trpc/server@10.30.0)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-BAqb9bGZIscroradlNx+Cc9522R+idY3BOSf5z0jHUtkxdMbjeGKxSSMxxu7JzoLqSIEC+LVzL3VvF8sdDWaZQ==} + peerDependencies: + '@tanstack/react-query': ^4.18.0 + '@trpc/client': 10.45.2 + '@trpc/server': 10.45.2 + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@tanstack/react-query': 4.24.10(react-dom@18.2.0)(react@18.2.0) + '@trpc/client': 10.30.0(@trpc/server@10.30.0) + '@trpc/server': 10.30.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@trpc/server@10.30.0: resolution: {integrity: sha512-pRsrHCuar3fbyOdJvO4b80OMP1Tx/wOSy5Ozy6cFDFWVUmfAyIX3En5Hoysy4cmMUuCsQsfTEYQwo+OcpjzBkg==} dev: false @@ -4206,6 +4237,12 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true + /@types/jsonwebtoken@9.0.6: + resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==} + dependencies: + '@types/node': 20.11.6 + dev: true + /@types/leaflet-routing-machine@3.2.4: resolution: {integrity: sha512-CFcWghU+eDel/zY2Q1u1tI6rxL2V6SfJ85/prt7s2QEw8tK+OkH+o5PLm/7eYsZH86i30ff5rmEk8GN1nZ9ODg==} dependencies: @@ -4954,6 +4991,10 @@ packages: engines: {node: '>=14.20.1'} dev: false + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: true @@ -5533,6 +5574,12 @@ packages: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: false @@ -6948,6 +6995,10 @@ packages: resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} engines: {node: '>= 0.6.0'} + /jose@5.2.4: + resolution: {integrity: sha512-6ScbIk2WWCeXkmzF6bRPmEuaqy1m8SbsRFMa/FLrSCkGIhj8OLVG/IH+XHVmNMx/KUo8cVWEE6oKR4dJ+S0Rkg==} + dev: false + /js-sdsl@4.3.0: resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==} dev: true @@ -7042,6 +7093,22 @@ packages: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} dev: true + /jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.6.0 + dev: false + /jss-plugin-camel-case@10.10.0: resolution: {integrity: sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==} dependencies: @@ -7112,6 +7179,21 @@ packages: object.assign: 4.1.4 dev: true + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + /kareem@2.5.1: resolution: {integrity: sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==} engines: {node: '>=12.0.0'} @@ -7217,10 +7299,38 @@ packages: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} dev: false + /lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + dev: false + + /lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + dev: false + + /lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + dev: false + + /lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + dev: false + + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: false + + /lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + dev: false + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + dev: false + /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -7257,7 +7367,6 @@ packages: engines: {node: '>=10'} dependencies: yallist: 4.0.0 - dev: true /luxon@3.2.1: resolution: {integrity: sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==} @@ -8420,6 +8529,14 @@ packages: lru-cache: 6.0.0 dev: true + /semver@7.6.0: + resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: false + /send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} @@ -9524,7 +9641,6 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true /yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} From 4462a07fb512a4f819a67d216b76a8e23563f4eb Mon Sep 17 00:00:00 2001 From: Aponia Date: Mon, 13 May 2024 12:54:03 -0700 Subject: [PATCH 03/18] fix: include credentials in fetch request options --- apps/antalmanac/src/providers/Query.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/antalmanac/src/providers/Query.tsx b/apps/antalmanac/src/providers/Query.tsx index 101dd3aa0..1055339ff 100644 --- a/apps/antalmanac/src/providers/Query.tsx +++ b/apps/antalmanac/src/providers/Query.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { httpBatchLink } from '@trpc/client'; +import { httpLink } from '@trpc/client'; import { useState } from 'react'; import transformer from 'superjson'; @@ -16,13 +16,14 @@ export default function AppQueryProvider(props: Props) { trpc.createClient({ transformer, links: [ - httpBatchLink({ - url: getEndpoint() + '/trpc', - headers: () => { - return { + httpLink({ + fetch(url, options) { + return fetch(url, { + ...options, credentials: 'include', - }; + }); }, + url: getEndpoint() + '/trpc', }), ], }) From 970739406f9662ab00463a421197760949dd7467 Mon Sep 17 00:00:00 2001 From: Aponia Date: Mon, 13 May 2024 13:08:36 -0700 Subject: [PATCH 04/18] feat: enable credentials cors backend --- apps/backend/src/index.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index e21d77dd3..9524728ab 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -7,18 +7,23 @@ import createContext from './context'; import env from './env'; // import connectToMongoDB from '$db/mongodb'; -const corsOptions: CorsOptions = { - origin: ['https://antalmanac.com', 'https://www.antalmanac.com', 'https://icssc-projects.github.io/AntAlmanac'], -}; - const MAPBOX_API_URL = 'https://api.mapbox.com'; const PORT = 3000; +const origins = ['https://antalmanac.com', 'https://www.antalmanac.com', 'https://icssc-projects.github.io/AntAlmanac']; + export async function start(corsEnabled = false) { + const corsOptions: CorsOptions = { + credentials: true, + origin: corsEnabled ? origins : true, + }; + // await connectToMongoDB(); const app = express(); - app.use(cors(corsEnabled ? corsOptions : undefined)); + + app.use(cors(corsOptions)); + app.use(express.json()); app.use('/mapbox/directions/*', async (req, res) => { @@ -38,10 +43,6 @@ export async function start(corsEnabled = false) { const buffer = await fetch(url).then((res) => res.arrayBuffer()); res.type('image/png'); res.send(Buffer.from(buffer)); - // // res.header('Content-Security-Policy', "img-src 'self'"); // https://stackoverflow.com/questions/56386307/loading-of-a-resource-blocked-by-content-security-policy - // // res.header('Access-Control-Allow-Methods', 'GET, OPTIONS') - // res.type('image/png') - // res.send(result) }); app.use( From 1095141c1e69928a3f95b414e4c8ae8683ce859a Mon Sep 17 00:00:00 2001 From: Aponia Date: Wed, 15 May 2024 11:25:13 -0700 Subject: [PATCH 05/18] chore: fix pnpm lock --- pnpm-lock.yaml | 141 +++++++++---------------------------------------- 1 file changed, 24 insertions(+), 117 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b087145a..8f65086c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true @@ -34,7 +34,7 @@ importers: version: 3.1.0 turbo: specifier: latest - version: 1.10.13 + version: 1.13.3 vitest: specifier: ^0.34.4 version: 0.34.4(jsdom@22.1.0) @@ -275,7 +275,7 @@ importers: version: 8.8.0(eslint@8.37.0) eslint-plugin-import: specifier: ^2.27.5 - version: 2.27.5(eslint-import-resolver-typescript@3.6.1)(eslint@8.37.0) + version: 2.27.5(@typescript-eslint/parser@5.57.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.38.0) eslint-plugin-jsx-a11y: specifier: ^6.7.1 version: 6.7.1(eslint@8.37.0) @@ -5823,7 +5823,7 @@ packages: enhanced-resolve: 5.15.1 eslint: 8.38.0 eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.57.1)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.6.1)(eslint@8.38.0) - eslint-plugin-import: 2.27.5(eslint-import-resolver-typescript@3.6.1)(eslint@8.38.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.57.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.38.0) fast-glob: 3.3.2 get-tsconfig: 4.6.2 is-core-module: 2.11.0 @@ -5865,35 +5865,6 @@ packages: - supports-color dev: true - /eslint-module-utils@2.7.4(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.6.1)(eslint@8.37.0): - resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - dependencies: - debug: 3.2.7(supports-color@5.5.0) - eslint: 8.37.0 - eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.6.1(eslint-plugin-import@2.27.5)(eslint@8.38.0) - transitivePeerDependencies: - - supports-color - dev: true - /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.57.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.38.0): resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} engines: {node: '>=4'} @@ -5927,70 +5898,6 @@ packages: - supports-color dev: true - /eslint-plugin-import@2.27.5(eslint-import-resolver-typescript@3.6.1)(eslint@8.37.0): - resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - dependencies: - array-includes: 3.1.6 - array.prototype.flat: 1.3.1 - array.prototype.flatmap: 1.3.1 - debug: 3.2.7(supports-color@5.5.0) - doctrine: 2.1.0 - eslint: 8.37.0 - eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.7.4(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.6.1)(eslint@8.37.0) - has: 1.0.3 - is-core-module: 2.11.0 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.values: 1.1.6 - resolve: 1.22.1 - semver: 6.3.0 - tsconfig-paths: 3.14.1 - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - dev: true - - /eslint-plugin-import@2.27.5(eslint-import-resolver-typescript@3.6.1)(eslint@8.38.0): - resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - dependencies: - array-includes: 3.1.6 - array.prototype.flat: 1.3.1 - array.prototype.flatmap: 1.3.1 - debug: 3.2.7(supports-color@5.5.0) - doctrine: 2.1.0 - eslint: 8.38.0 - eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.57.1)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.6.1)(eslint@8.38.0) - has: 1.0.3 - is-core-module: 2.11.0 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.values: 1.1.6 - resolve: 1.22.1 - semver: 6.3.0 - tsconfig-paths: 3.14.1 - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - dev: true - /eslint-plugin-jsx-a11y@6.7.1(eslint@8.37.0): resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==} engines: {node: '>=4.0'} @@ -8997,64 +8904,64 @@ packages: engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} dev: false - /turbo-darwin-64@1.10.13: - resolution: {integrity: sha512-vmngGfa2dlYvX7UFVncsNDMuT4X2KPyPJ2Jj+xvf5nvQnZR/3IeDEGleGVuMi/hRzdinoxwXqgk9flEmAYp0Xw==} + /turbo-darwin-64@1.13.3: + resolution: {integrity: sha512-glup8Qx1qEFB5jerAnXbS8WrL92OKyMmg5Hnd4PleLljAeYmx+cmmnsmLT7tpaVZIN58EAAwu8wHC6kIIqhbWA==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@1.10.13: - resolution: {integrity: sha512-eMoJC+k7gIS4i2qL6rKmrIQGP6Wr9nN4odzzgHFngLTMimok2cGLK3qbJs5O5F/XAtEeRAmuxeRnzQwTl/iuAw==} + /turbo-darwin-arm64@1.13.3: + resolution: {integrity: sha512-/np2xD+f/+9qY8BVtuOQXRq5f9LehCFxamiQnwdqWm5iZmdjygC5T3uVSYuagVFsZKMvX3ycySwh8dylGTl6lg==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@1.10.13: - resolution: {integrity: sha512-0CyYmnKTs6kcx7+JRH3nPEqCnzWduM0hj8GP/aodhaIkLNSAGAa+RiYZz6C7IXN+xUVh5rrWTnU2f1SkIy7Gdg==} + /turbo-linux-64@1.13.3: + resolution: {integrity: sha512-G+HGrau54iAnbXLfl+N/PynqpDwi/uDzb6iM9hXEDG+yJnSJxaHMShhOkXYJPk9offm9prH33Khx2scXrYVW1g==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@1.10.13: - resolution: {integrity: sha512-0iBKviSGQQlh2OjZgBsGjkPXoxvRIxrrLLbLObwJo3sOjIH0loGmVIimGS5E323soMfi/o+sidjk2wU1kFfD7Q==} + /turbo-linux-arm64@1.13.3: + resolution: {integrity: sha512-qWwEl5VR02NqRyl68/3pwp3c/olZuSp+vwlwrunuoNTm6JXGLG5pTeme4zoHNnk0qn4cCX7DFrOboArlYxv0wQ==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@1.10.13: - resolution: {integrity: sha512-S5XySRfW2AmnTeY1IT+Jdr6Goq7mxWganVFfrmqU+qqq3Om/nr0GkcUX+KTIo9mPrN0D3p5QViBRzulwB5iuUQ==} + /turbo-windows-64@1.13.3: + resolution: {integrity: sha512-Nudr4bRChfJzBPzEmpVV85VwUYRCGKecwkBFpbp2a4NtrJ3+UP1VZES653ckqCu2FRyRuS0n03v9euMbAvzH+Q==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@1.10.13: - resolution: {integrity: sha512-nKol6+CyiExJIuoIc3exUQPIBjP9nIq5SkMJgJuxsot2hkgGrafAg/izVDRDrRduQcXj2s8LdtxJHvvnbI8hEQ==} + /turbo-windows-arm64@1.13.3: + resolution: {integrity: sha512-ouJCgsVLd3icjRLmRvHQDDZnmGzT64GBupM1Y+TjtYn2LVaEBoV6hicFy8x5DUpnqdLy+YpCzRMkWlwhmkX7sQ==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@1.10.13: - resolution: {integrity: sha512-vOF5IPytgQPIsgGtT0n2uGZizR2N3kKuPIn4b5p5DdeLoI0BV7uNiydT7eSzdkPRpdXNnO8UwS658VaI4+YSzQ==} + /turbo@1.13.3: + resolution: {integrity: sha512-n17HJv4F4CpsYTvKzUJhLbyewbXjq1oLCi90i5tW1TiWDz16ML1eDG7wi5dHaKxzh5efIM56SITnuVbMq5dk4g==} hasBin: true optionalDependencies: - turbo-darwin-64: 1.10.13 - turbo-darwin-arm64: 1.10.13 - turbo-linux-64: 1.10.13 - turbo-linux-arm64: 1.10.13 - turbo-windows-64: 1.10.13 - turbo-windows-arm64: 1.10.13 + turbo-darwin-64: 1.13.3 + turbo-darwin-arm64: 1.13.3 + turbo-linux-64: 1.13.3 + turbo-linux-arm64: 1.13.3 + turbo-windows-64: 1.13.3 + turbo-windows-arm64: 1.13.3 dev: true /type-check@0.4.0: From e87b3691df051c63fa607f09cbe444e4569d688b Mon Sep 17 00:00:00 2001 From: Aponia Date: Wed, 15 May 2024 11:44:50 -0700 Subject: [PATCH 06/18] feat: login with google sends request --- apps/antalmanac/src/components/Header/Header.tsx | 4 ++-- apps/antalmanac/src/components/Header/LoginButton.tsx | 5 ++--- apps/backend/src/routers/index.ts | 8 ++++---- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/apps/antalmanac/src/components/Header/Header.tsx b/apps/antalmanac/src/components/Header/Header.tsx index addc3ba58..f1b773364 100644 --- a/apps/antalmanac/src/components/Header/Header.tsx +++ b/apps/antalmanac/src/components/Header/Header.tsx @@ -52,8 +52,6 @@ const Header = ({ classes }: CustomAppBarProps) => { />
- - {isMobileScreen ? null : ( @@ -63,6 +61,8 @@ const Header = ({ classes }: CustomAppBarProps) => { )} + +
diff --git a/apps/antalmanac/src/components/Header/LoginButton.tsx b/apps/antalmanac/src/components/Header/LoginButton.tsx index a2e8fc0a7..8e0adb2b0 100644 --- a/apps/antalmanac/src/components/Header/LoginButton.tsx +++ b/apps/antalmanac/src/components/Header/LoginButton.tsx @@ -1,3 +1,4 @@ +import LoginIcon from '@mui/icons-material/Login'; import { Box, Button, @@ -109,8 +110,6 @@ export function LoginButton() { return; } - return; - const response = await googleLoginMutation.mutateAsync(credential.credential); console.log('response: ', response); @@ -122,7 +121,7 @@ export function LoginButton() { return ( <> - diff --git a/apps/backend/src/routers/index.ts b/apps/backend/src/routers/index.ts index cb979618b..b89bd7f34 100644 --- a/apps/backend/src/routers/index.ts +++ b/apps/backend/src/routers/index.ts @@ -47,6 +47,7 @@ const authRouter = router({ // Set the auth cookie so the client can be recognized on future requests. const authCookie = jwt.sign({ googleId: context.input }, privateKey); + context.ctx.res.cookie('auth', authCookie); return await ddbClient.getGoogleUserData(idToken.sub); @@ -55,13 +56,12 @@ const authRouter = router({ } }), loginUsername: procedure.input(type('string').assert).mutation(async (context) => { - // Set the auth cookie so the client can be recognized on future requests. const authCookie = jwt.sign({ username: context.input }, privateKey); - context.ctx.res.cookie('auth', authCookie, { maxAge: 1000 * 60 * 60, path: '/' }); - console.log({ authCookie }); + // Set the auth cookie so the client can be recognized on future requests. + context.ctx.res.cookie('auth', authCookie, { maxAge: 1000 * 60 * 60, path: '/' }); - return await ddbClient.getUserData(context.input); // ?? (await ddbClient.getLegacyUserData(context.input)); + return (await ddbClient.getUserData(context.input)) ?? (await ddbClient.getLegacyUserData(context.input)); }), }); From 2a86dbcf88775672b6d003bd8e1f4d730a3e8781 Mon Sep 17 00:00:00 2001 From: Aponia Date: Wed, 15 May 2024 14:25:22 -0700 Subject: [PATCH 07/18] feat: load button mvp --- .../src/components/Header/Header.tsx | 3 + .../src/components/Header/LoadButton.tsx | 125 ++++++++++++++++++ .../Header/LoadSaveFunctionality.tsx | 8 -- pnpm-lock.yaml | 75 ++++++++++- 4 files changed, 201 insertions(+), 10 deletions(-) create mode 100644 apps/antalmanac/src/components/Header/LoadButton.tsx diff --git a/apps/antalmanac/src/components/Header/Header.tsx b/apps/antalmanac/src/components/Header/Header.tsx index f1b773364..49a66aadd 100644 --- a/apps/antalmanac/src/components/Header/Header.tsx +++ b/apps/antalmanac/src/components/Header/Header.tsx @@ -7,6 +7,7 @@ import Import from './Import'; import LoadSaveScheduleFunctionality from './LoadSaveFunctionality'; import LoginButton from './LoginButton'; import AppDrawer from './SettingsMenu'; +import LoadButton from './LoadButton' import Logo from '$assets/logo.svg'; import MobileLogo from '$assets/mobile-logo.svg'; @@ -54,6 +55,8 @@ const Header = ({ classes }: CustomAppBarProps) => {
+ + {isMobileScreen ? null : ( <> diff --git a/apps/antalmanac/src/components/Header/LoadButton.tsx b/apps/antalmanac/src/components/Header/LoadButton.tsx new file mode 100644 index 000000000..d820a28a9 --- /dev/null +++ b/apps/antalmanac/src/components/Header/LoadButton.tsx @@ -0,0 +1,125 @@ +import { CloudDownload } from '@material-ui/icons'; +import { LoadingButton } from '@mui/lab'; +import { + Autocomplete, + Button, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Stack, + TextField, +} from '@mui/material'; +import { useEffect, useMemo, useState } from 'react'; +import { loadSchedule } from '$actions/AppStoreActions'; +import AppStore from '$stores/AppStore'; +import { useThemeStore } from '$stores/SettingsStore'; + +export function LoadButton() { + const [open, setOpen] = useState(false); + + const [loading, setLoading] = useState(false); + + const [skeletonMode, setSkeletonMode] = useState(AppStore.getSkeletonMode()); + + const [userId, setUserId] = useState(''); + + const isDark = useThemeStore((store) => store.isDark); + + const loadScheduleAndSetLoading = async (userID: string, rememberMe: boolean) => { + setLoading(true); + await loadSchedule(userID, rememberMe); + setLoading(false); + }; + + useEffect(() => { + const handleSkeletonModeChange = () => { + setSkeletonMode(AppStore.getSkeletonMode()); + }; + + AppStore.on('skeletonModeChange', handleSkeletonModeChange); + + return () => { + AppStore.off('skeletonModeChange', handleSkeletonModeChange); + }; + }, []); + + useEffect(() => { + if (typeof Storage !== 'undefined') { + const savedUserID = window.localStorage.getItem('userID'); + + if (savedUserID != null) { + // this `void` is for eslint "no floating promises" + void loadScheduleAndSetLoading(savedUserID, true); + } + } + }, []); + + const handleOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + const handleSubmit = () => {}; + + /** + * TODO: actually generate options. + */ + const options = useMemo(() => { + return ['a', 'b', 'c']; + }, []); + + return ( + <> + : } + disabled={skeletonMode} + loading={false} + > + Load + + + + Load {userId} + + + + Enter a unique user ID here to view a schedule. + + { + if (newValue != null) { + setUserId(newValue); + } + }} + sx={{ width: 300 }} + options={options} + renderInput={(params) => { + return ; + }} + /> + + + + + + + + + + ); +} + +export default LoadButton; diff --git a/apps/antalmanac/src/components/Header/LoadSaveFunctionality.tsx b/apps/antalmanac/src/components/Header/LoadSaveFunctionality.tsx index d0c418f6a..f35c46bf4 100644 --- a/apps/antalmanac/src/components/Header/LoadSaveFunctionality.tsx +++ b/apps/antalmanac/src/components/Header/LoadSaveFunctionality.tsx @@ -225,14 +225,6 @@ const LoadSaveScheduleFunctionality = () => { loading={saving} colorType={isDark ? 'secondary' : 'primary'} /> -
); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f65086c7..47c0b2d77 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -275,7 +275,7 @@ importers: version: 8.8.0(eslint@8.37.0) eslint-plugin-import: specifier: ^2.27.5 - version: 2.27.5(@typescript-eslint/parser@5.57.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.38.0) + version: 2.27.5(eslint@8.37.0) eslint-plugin-jsx-a11y: specifier: ^6.7.1 version: 6.7.1(eslint@8.37.0) @@ -5419,6 +5419,17 @@ packages: ms: 2.0.0 dev: false + /debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + /debug@3.2.7(supports-color@5.5.0): resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -5805,7 +5816,7 @@ packages: /eslint-import-resolver-node@0.3.7: resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7 is-core-module: 2.11.0 resolve: 1.22.1 transitivePeerDependencies: @@ -5865,6 +5876,34 @@ packages: - supports-color dev: true + /eslint-module-utils@2.7.4(eslint-import-resolver-node@0.3.7)(eslint@8.37.0): + resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + debug: 3.2.7 + eslint: 8.37.0 + eslint-import-resolver-node: 0.3.7 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.57.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.38.0): resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} engines: {node: '>=4'} @@ -5898,6 +5937,38 @@ packages: - supports-color dev: true + /eslint-plugin-import@2.27.5(eslint@8.37.0): + resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + array-includes: 3.1.6 + array.prototype.flat: 1.3.1 + array.prototype.flatmap: 1.3.1 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.37.0 + eslint-import-resolver-node: 0.3.7 + eslint-module-utils: 2.7.4(eslint-import-resolver-node@0.3.7)(eslint@8.37.0) + has: 1.0.3 + is-core-module: 2.11.0 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.values: 1.1.6 + resolve: 1.22.1 + semver: 6.3.0 + tsconfig-paths: 3.14.1 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + /eslint-plugin-jsx-a11y@6.7.1(eslint@8.37.0): resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==} engines: {node: '>=4.0'} From c185ae99fb2cff7c20b81cbe96edde3313226a9e Mon Sep 17 00:00:00 2001 From: Aponia Date: Wed, 15 May 2024 14:58:40 -0700 Subject: [PATCH 08/18] style(svelte-query): cleanup hooks order --- .../src/components/Header/LoadButton.tsx | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/apps/antalmanac/src/components/Header/LoadButton.tsx b/apps/antalmanac/src/components/Header/LoadButton.tsx index d820a28a9..699ea253c 100644 --- a/apps/antalmanac/src/components/Header/LoadButton.tsx +++ b/apps/antalmanac/src/components/Header/LoadButton.tsx @@ -13,6 +13,7 @@ import { TextField, } from '@mui/material'; import { useEffect, useMemo, useState } from 'react'; + import { loadSchedule } from '$actions/AppStoreActions'; import AppStore from '$stores/AppStore'; import { useThemeStore } from '$stores/SettingsStore'; @@ -34,28 +35,15 @@ export function LoadButton() { setLoading(false); }; - useEffect(() => { - const handleSkeletonModeChange = () => { - setSkeletonMode(AppStore.getSkeletonMode()); - }; - - AppStore.on('skeletonModeChange', handleSkeletonModeChange); - - return () => { - AppStore.off('skeletonModeChange', handleSkeletonModeChange); - }; - }, []); - - useEffect(() => { + const loadSavedSchedule = async () => { if (typeof Storage !== 'undefined') { const savedUserID = window.localStorage.getItem('userID'); if (savedUserID != null) { - // this `void` is for eslint "no floating promises" void loadScheduleAndSetLoading(savedUserID, true); } } - }, []); + }; const handleOpen = () => { setOpen(true); @@ -65,7 +53,9 @@ export function LoadButton() { setOpen(false); }; - const handleSubmit = () => {}; + const handleSubmit = () => { + console.log('Submitting'); + }; /** * TODO: actually generate options. @@ -74,6 +64,22 @@ export function LoadButton() { return ['a', 'b', 'c']; }, []); + useEffect(() => { + const handleSkeletonModeChange = () => { + setSkeletonMode(AppStore.getSkeletonMode()); + }; + + AppStore.on('skeletonModeChange', handleSkeletonModeChange); + + return () => { + AppStore.off('skeletonModeChange', handleSkeletonModeChange); + }; + }, []); + + useEffect(() => { + loadSavedSchedule(); + }, []); + return ( <> Enter a unique user ID here to view a schedule. - + { From 462b8c816ae5b9e9512f41a906edfd70fc3aa1b4 Mon Sep 17 00:00:00 2001 From: Aponia Date: Wed, 15 May 2024 15:07:18 -0700 Subject: [PATCH 09/18] style: guard clauses and await --- apps/antalmanac/src/components/Header/LoadButton.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/antalmanac/src/components/Header/LoadButton.tsx b/apps/antalmanac/src/components/Header/LoadButton.tsx index 699ea253c..90909bf56 100644 --- a/apps/antalmanac/src/components/Header/LoadButton.tsx +++ b/apps/antalmanac/src/components/Header/LoadButton.tsx @@ -36,13 +36,13 @@ export function LoadButton() { }; const loadSavedSchedule = async () => { - if (typeof Storage !== 'undefined') { - const savedUserID = window.localStorage.getItem('userID'); + if (typeof Storage === 'undefined') return; - if (savedUserID != null) { - void loadScheduleAndSetLoading(savedUserID, true); - } - } + const savedUserID = window.localStorage.getItem('userID'); + + if (savedUserID == null) return; + + await loadScheduleAndSetLoading(savedUserID, true); }; const handleOpen = () => { From 3badd46ade7204ac06ac9f1b4f2dc85213c6fc96 Mon Sep 17 00:00:00 2001 From: Aponia Date: Sun, 19 May 2024 22:34:42 -0700 Subject: [PATCH 10/18] feat(antalmanac): responsive login/logout buttons --- .../src/components/Header/Header.tsx | 18 +++- .../src/components/Header/LoginButton.tsx | 10 +- apps/backend/package.json | 2 + apps/backend/src/index.ts | 5 +- apps/backend/src/routers/index.ts | 28 ++++- pnpm-lock.yaml | 100 +++++------------- 6 files changed, 76 insertions(+), 87 deletions(-) diff --git a/apps/antalmanac/src/components/Header/Header.tsx b/apps/antalmanac/src/components/Header/Header.tsx index 49a66aadd..930f1604d 100644 --- a/apps/antalmanac/src/components/Header/Header.tsx +++ b/apps/antalmanac/src/components/Header/Header.tsx @@ -1,16 +1,17 @@ -import { AppBar, Toolbar, useMediaQuery } from '@material-ui/core'; +import { AppBar, Button, Toolbar, useMediaQuery } from '@material-ui/core'; import { withStyles } from '@material-ui/core/styles'; import { ClassNameMap } from '@material-ui/core/styles/withStyles'; import Export from './Export'; import Import from './Import'; +import LoadButton from './LoadButton'; import LoadSaveScheduleFunctionality from './LoadSaveFunctionality'; import LoginButton from './LoginButton'; import AppDrawer from './SettingsMenu'; -import LoadButton from './LoadButton' import Logo from '$assets/logo.svg'; import MobileLogo from '$assets/mobile-logo.svg'; +import { trpc } from '$lib/trpc'; const styles = { appBar: { @@ -42,6 +43,17 @@ interface CustomAppBarProps { const Header = ({ classes }: CustomAppBarProps) => { const isMobileScreen = useMediaQuery('(max-width:750px)'); + const authStatus = trpc.auth.status.useQuery(); + + const logoutMutation = trpc.auth.logout.useMutation(); + + const utils = trpc.useUtils(); + + const logout = async () => { + await logoutMutation.mutateAsync(); + await utils.auth.status.invalidate(); + }; + return ( @@ -64,7 +76,7 @@ const Header = ({ classes }: CustomAppBarProps) => { )} - + {authStatus.data ? : }
diff --git a/apps/antalmanac/src/components/Header/LoginButton.tsx b/apps/antalmanac/src/components/Header/LoginButton.tsx index 8e0adb2b0..e39a1aa5d 100644 --- a/apps/antalmanac/src/components/Header/LoginButton.tsx +++ b/apps/antalmanac/src/components/Header/LoginButton.tsx @@ -25,6 +25,7 @@ import AppStore from '$stores/AppStore'; */ export function LoginButton() { const [userId, setUserId] = useState(''); + const [open, setOpen] = useState(false); const snackbar = useSnackbar(); @@ -33,6 +34,8 @@ export function LoginButton() { const googleLoginMutation = trpc.auth.loginGoogle.useMutation(); + const utils = trpc.useUtils(); + const handleOpen = () => { setOpen(true); }; @@ -66,13 +69,9 @@ export function LoginButton() { return; } - console.log('Submitting user ID: ', userId); - try { const response = await usernameLoginMutation.mutateAsync(userId); - console.log('response: ', response); - if (response == null) { snackbar.enqueueSnackbar(`Logged in as "${userId}", no schedules found.`); setOpen(false); @@ -93,7 +92,10 @@ export function LoginButton() { } snackbar.enqueueSnackbar(`Schedule for username "${userId}" loaded.`, { variant: 'success' }); + setOpen(false); + + await utils.auth.status.invalidate(); } catch (e) { console.error('Error occurred while loading schedules: ', e); snackbar.enqueueSnackbar( diff --git a/apps/backend/package.json b/apps/backend/package.json index 2c2afc3e8..67d142b0b 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -15,6 +15,7 @@ "@vendia/serverless-express": "^4.10.1", "arktype": "1.0.14-alpha", "aws-lambda": "^1.0.7", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dotenv": "^16.0.3", "envalid": "^7.3.1", @@ -30,6 +31,7 @@ "@aws-sdk/client-dynamodb": "^3.332.0", "@aws-sdk/lib-dynamodb": "^3.332.0", "@types/aws-lambda": "^8.10.110", + "@types/cookie-parser": "^1.4.7", "@types/cors": "^2.8.13", "@types/express": "^4.17.17", "@types/jsonwebtoken": "^9.0.6", diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 9524728ab..9ed243bef 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -2,10 +2,10 @@ import express from 'express'; import cors from 'cors'; import type { CorsOptions } from 'cors'; import { createExpressMiddleware } from '@trpc/server/adapters/express'; +import cookieParser from 'cookie-parser'; import AppRouter from './routers'; import createContext from './context'; import env from './env'; -// import connectToMongoDB from '$db/mongodb'; const MAPBOX_API_URL = 'https://api.mapbox.com'; @@ -19,13 +19,14 @@ export async function start(corsEnabled = false) { origin: corsEnabled ? origins : true, }; - // await connectToMongoDB(); const app = express(); app.use(cors(corsOptions)); app.use(express.json()); + app.use(cookieParser()); + app.use('/mapbox/directions/*', async (req, res) => { const searchParams = new URLSearchParams(req.query as any); searchParams.set('access_token', env.MAPBOX_ACCESS_TOKEN); diff --git a/apps/backend/src/routers/index.ts b/apps/backend/src/routers/index.ts index b89bd7f34..13528975e 100644 --- a/apps/backend/src/routers/index.ts +++ b/apps/backend/src/routers/index.ts @@ -25,20 +25,35 @@ export interface GoogleProfile extends Record { sub: string; } +export const AccessTokenSchema = type([ + { + googleId: 'string', + }, + '|', + { username: 'string' }, +]); + const privateKey = 'secret'; const authRouter = router({ status: procedure.query(async (context) => { const authCookie = context.ctx.req.cookies['auth']; - if (authCookie == null) return; + if (authCookie == null) return null; try { - const authUser = jwt.decode(authCookie); - return authUser; + const decodedToken = jwt.decode(authCookie); + + const parsedToken = AccessTokenSchema(decodedToken); + + if (parsedToken.problems) { + return null; + } + + return parsedToken.data; } catch (e) { console.error('Failed to decode auth cookie: ', e); - return; + return null; } }), loginGoogle: procedure.input(type('string').assert).mutation(async (context) => { @@ -61,7 +76,10 @@ const authRouter = router({ // Set the auth cookie so the client can be recognized on future requests. context.ctx.res.cookie('auth', authCookie, { maxAge: 1000 * 60 * 60, path: '/' }); - return (await ddbClient.getUserData(context.input)) ?? (await ddbClient.getLegacyUserData(context.input)); + return await ddbClient.getUserData(context.input); // ?? (await ddbClient.getLegacyUserData(context.input)); + }), + logout: procedure.mutation(async (context) => { + context.ctx.res.cookie('auth', '', { maxAge: 0 }); }), }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47c0b2d77..a11c1d6cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -275,7 +275,7 @@ importers: version: 8.8.0(eslint@8.37.0) eslint-plugin-import: specifier: ^2.27.5 - version: 2.27.5(eslint@8.37.0) + version: 2.27.5(@typescript-eslint/parser@5.57.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.38.0) eslint-plugin-jsx-a11y: specifier: ^6.7.1 version: 6.7.1(eslint@8.37.0) @@ -318,6 +318,9 @@ importers: aws-lambda: specifier: ^1.0.7 version: 1.0.7 + cookie-parser: + specifier: ^1.4.6 + version: 1.4.6 cors: specifier: ^2.8.5 version: 2.8.5 @@ -358,6 +361,9 @@ importers: '@types/aws-lambda': specifier: ^8.10.110 version: 8.10.110 + '@types/cookie-parser': + specifier: ^1.4.7 + version: 1.4.7 '@types/cors': specifier: ^2.8.13 version: 2.8.13 @@ -4140,6 +4146,12 @@ packages: '@types/node': 20.11.5 dev: true + /@types/cookie-parser@1.4.7: + resolution: {integrity: sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==} + dependencies: + '@types/express': 4.17.17 + dev: true + /@types/cors@2.8.13: resolution: {integrity: sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==} dependencies: @@ -5224,10 +5236,23 @@ packages: /convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + /cookie-parser@1.4.6: + resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==} + engines: {node: '>= 0.8.0'} + dependencies: + cookie: 0.4.1 + cookie-signature: 1.0.6 + dev: false + /cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} dev: false + /cookie@0.4.1: + resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==} + engines: {node: '>= 0.6'} + dev: false + /cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} @@ -5419,17 +5444,6 @@ packages: ms: 2.0.0 dev: false - /debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.3 - dev: true - /debug@3.2.7(supports-color@5.5.0): resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -5816,7 +5830,7 @@ packages: /eslint-import-resolver-node@0.3.7: resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} dependencies: - debug: 3.2.7 + debug: 3.2.7(supports-color@5.5.0) is-core-module: 2.11.0 resolve: 1.22.1 transitivePeerDependencies: @@ -5876,34 +5890,6 @@ packages: - supports-color dev: true - /eslint-module-utils@2.7.4(eslint-import-resolver-node@0.3.7)(eslint@8.37.0): - resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - dependencies: - debug: 3.2.7 - eslint: 8.37.0 - eslint-import-resolver-node: 0.3.7 - transitivePeerDependencies: - - supports-color - dev: true - /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.57.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.38.0): resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} engines: {node: '>=4'} @@ -5937,38 +5923,6 @@ packages: - supports-color dev: true - /eslint-plugin-import@2.27.5(eslint@8.37.0): - resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - dependencies: - array-includes: 3.1.6 - array.prototype.flat: 1.3.1 - array.prototype.flatmap: 1.3.1 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 8.37.0 - eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.7.4(eslint-import-resolver-node@0.3.7)(eslint@8.37.0) - has: 1.0.3 - is-core-module: 2.11.0 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.values: 1.1.6 - resolve: 1.22.1 - semver: 6.3.0 - tsconfig-paths: 3.14.1 - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - dev: true - /eslint-plugin-jsx-a11y@6.7.1(eslint@8.37.0): resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==} engines: {node: '>=4.0'} From 926cd741f8d44a44b3058a20d1f45f22c6947e48 Mon Sep 17 00:00:00 2001 From: Aponia Date: Sun, 19 May 2024 22:47:33 -0700 Subject: [PATCH 11/18] feat(antalmanac): working account button --- .../src/components/Header/AccountButton.tsx | 75 +++++++++++++++++++ .../src/components/Header/Header.tsx | 14 +--- 2 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 apps/antalmanac/src/components/Header/AccountButton.tsx diff --git a/apps/antalmanac/src/components/Header/AccountButton.tsx b/apps/antalmanac/src/components/Header/AccountButton.tsx new file mode 100644 index 000000000..70eccc394 --- /dev/null +++ b/apps/antalmanac/src/components/Header/AccountButton.tsx @@ -0,0 +1,75 @@ +import { Logout, PersonAdd, Settings } from '@mui/icons-material'; +import { Avatar, IconButton, ListItemIcon, Menu, MenuItem, Tooltip } from '@mui/material'; +import { useState } from 'react'; + +import { trpc } from '$lib/trpc'; + +export function AccountButton() { + const [anchorEl, setAnchorEl] = useState(); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(undefined); + }; + + const logoutMutation = trpc.auth.logout.useMutation(); + + const utils = trpc.useUtils(); + + const open = Boolean(anchorEl); + + const logout = async () => { + await logoutMutation.mutateAsync(); + await utils.auth.status.invalidate(); + handleClose(); + }; + + return ( + <> + + + + + + + + + + + Add another account + + + + + + Settings + + + + + + Logout + + + + ); +} + +export default AccountButton; diff --git a/apps/antalmanac/src/components/Header/Header.tsx b/apps/antalmanac/src/components/Header/Header.tsx index 930f1604d..622719683 100644 --- a/apps/antalmanac/src/components/Header/Header.tsx +++ b/apps/antalmanac/src/components/Header/Header.tsx @@ -1,7 +1,8 @@ -import { AppBar, Button, Toolbar, useMediaQuery } from '@material-ui/core'; +import { AppBar, Toolbar, useMediaQuery } from '@material-ui/core'; import { withStyles } from '@material-ui/core/styles'; import { ClassNameMap } from '@material-ui/core/styles/withStyles'; +import AccountButton from './AccountButton'; import Export from './Export'; import Import from './Import'; import LoadButton from './LoadButton'; @@ -45,15 +46,6 @@ const Header = ({ classes }: CustomAppBarProps) => { const authStatus = trpc.auth.status.useQuery(); - const logoutMutation = trpc.auth.logout.useMutation(); - - const utils = trpc.useUtils(); - - const logout = async () => { - await logoutMutation.mutateAsync(); - await utils.auth.status.invalidate(); - }; - return ( @@ -76,7 +68,7 @@ const Header = ({ classes }: CustomAppBarProps) => { )} - {authStatus.data ? : } + {authStatus.data ? : } From 236fbeb7ea171227b16c599347b20838829a65d2 Mon Sep 17 00:00:00 2001 From: Aponia Date: Mon, 20 May 2024 09:19:52 -0700 Subject: [PATCH 12/18] feat(antalmanac): account button settings set username --- .../src/components/Header/AccountButton.tsx | 86 +++++++++++++++++-- 1 file changed, 77 insertions(+), 9 deletions(-) diff --git a/apps/antalmanac/src/components/Header/AccountButton.tsx b/apps/antalmanac/src/components/Header/AccountButton.tsx index 70eccc394..f3ce59203 100644 --- a/apps/antalmanac/src/components/Header/AccountButton.tsx +++ b/apps/antalmanac/src/components/Header/AccountButton.tsx @@ -1,5 +1,22 @@ -import { Logout, PersonAdd, Settings } from '@mui/icons-material'; -import { Avatar, IconButton, ListItemIcon, Menu, MenuItem, Tooltip } from '@mui/material'; +import { Logout, Settings } from '@mui/icons-material'; +import { + Avatar, + Box, + Button, + Dialog, + DialogContent, + DialogTitle, + Divider, + IconButton, + ListItemIcon, + Menu, + MenuItem, + Stack, + TextField, + Tooltip, + Typography, +} from '@mui/material'; +import { useSnackbar } from 'notistack'; import { useState } from 'react'; import { trpc } from '$lib/trpc'; @@ -7,6 +24,12 @@ import { trpc } from '$lib/trpc'; export function AccountButton() { const [anchorEl, setAnchorEl] = useState(); + const [username, setUsername] = useState(''); + + const [settingsOpen, setSettingsOpen] = useState(false); + + const { enqueueSnackbar } = useSnackbar(); + const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; @@ -27,6 +50,28 @@ export function AccountButton() { handleClose(); }; + const handleOpenSettings = () => { + setSettingsOpen(true); + handleClose(); + }; + + const handleCloseSettings = () => { + setSettingsOpen(false); + }; + + const handleUsernameChange = (e: React.ChangeEvent) => { + setUsername(e.target.value); + }; + + const handleSubmit = async () => { + if (!username) { + enqueueSnackbar('Username must not be empty', { variant: 'error' }); + return; + } + console.log('new username: ', username); + enqueueSnackbar('New username set', { variant: 'success' }); + }; + return ( <> @@ -49,13 +94,7 @@ export function AccountButton() { transformOrigin={{ horizontal: 'right', vertical: 'top' }} anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} > - - - - - Add another account - - + @@ -68,6 +107,35 @@ export function AccountButton() { Logout + + + Account Settings + + + + + + Change Username + + + Enter a unique username. + If the username exists, you will need to claim it. + + + + + Username + + + + + + + + + ); } From 00b48c01a36e3cfb6d828ca32f6073f4d684d8f2 Mon Sep 17 00:00:00 2001 From: Aponia Date: Mon, 20 May 2024 09:39:43 -0700 Subject: [PATCH 13/18] feat(backend): always create user when logging in --- apps/backend/src/db/ddb.ts | 5 +- apps/backend/src/routers/index.ts | 84 +++++++++++++++++++++++++------ 2 files changed, 72 insertions(+), 17 deletions(-) diff --git a/apps/backend/src/db/ddb.ts b/apps/backend/src/db/ddb.ts index b26f6e72a..99e13698e 100644 --- a/apps/backend/src/db/ddb.ts +++ b/apps/backend/src/db/ddb.ts @@ -25,9 +25,9 @@ export const VISIBILITY = { }; class DDBClient>> { - private tableName: string; + tableName: string; - private schema: T; + schema: T; client: DynamoDB; @@ -171,7 +171,6 @@ class DDBClient>> { } else { return parsedUserData.data; } - } } diff --git a/apps/backend/src/routers/index.ts b/apps/backend/src/routers/index.ts index 13528975e..0eeb29556 100644 --- a/apps/backend/src/routers/index.ts +++ b/apps/backend/src/routers/index.ts @@ -25,13 +25,10 @@ export interface GoogleProfile extends Record { sub: string; } -export const AccessTokenSchema = type([ - { - googleId: 'string', - }, - '|', - { username: 'string' }, -]); +export const AccessTokenSchema = type({ + id: 'string', + googleId: 'string', +}); const privateKey = 'secret'; @@ -56,27 +53,86 @@ const authRouter = router({ return null; } }), + /** + * Logs in with Google and returns schedule data. + */ loginGoogle: procedure.input(type('string').assert).mutation(async (context) => { try { const idToken = jwt.decode(context.input) as GoogleProfile; - // Set the auth cookie so the client can be recognized on future requests. - const authCookie = jwt.sign({ googleId: context.input }, privateKey); + const googleId = idToken.sub; + + const existingUser = await ddbClient.get('googleId', googleId); + + if (existingUser != null) { + const authCookie = jwt.sign( + { + id: existingUser.id, + googleId, + }, + privateKey + ); + + // Set the auth cookie so the client can be recognized on future requests. + context.ctx.res.cookie('auth', authCookie); + + return existingUser.userData; + } + + await ddbClient.documentClient.put({ + TableName: ddbClient.tableName, + Item: { + id: googleId, + googleId, + }, + }); + + /** + * By default, a new user logged in with Google will just have their ID set to + * their google ID. + */ + const authCookie = jwt.sign( + { + id: googleId, + googleId, + }, + privateKey + ); context.ctx.res.cookie('auth', authCookie); - return await ddbClient.getGoogleUserData(idToken.sub); - } catch { - return; + return null; + } catch (e) { + console.error('Error ocurred while logging in with google: ', e); + return null; } }), loginUsername: procedure.input(type('string').assert).mutation(async (context) => { - const authCookie = jwt.sign({ username: context.input }, privateKey); + const id = context.input; + + const userData = await ddbClient.getUserData(id); + + if (userData != null) { + const authCookie = jwt.sign({ id }, privateKey); + + // Set the auth cookie so the client can be recognized on future requests. + context.ctx.res.cookie('auth', authCookie, { maxAge: 1000 * 60 * 60, path: '/' }); + + return userData; + } + + await ddbClient.documentClient.put({ + TableName: ddbClient.tableName, + Item: { + id, + }, + }); + const authCookie = jwt.sign({ id }, privateKey); // Set the auth cookie so the client can be recognized on future requests. context.ctx.res.cookie('auth', authCookie, { maxAge: 1000 * 60 * 60, path: '/' }); - return await ddbClient.getUserData(context.input); // ?? (await ddbClient.getLegacyUserData(context.input)); + return null; }), logout: procedure.mutation(async (context) => { context.ctx.res.cookie('auth', '', { maxAge: 0 }); From 44b11fb552f4ef85e2ee404e35c5a1c552f0bcb2 Mon Sep 17 00:00:00 2001 From: Aponia Date: Mon, 20 May 2024 10:00:31 -0700 Subject: [PATCH 14/18] feat(antalamanc): login will save user --- .../src/components/Header/LoginButton.tsx | 46 ++++++++++++++++--- apps/backend/src/routers/index.ts | 15 +++--- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/apps/antalmanac/src/components/Header/LoginButton.tsx b/apps/antalmanac/src/components/Header/LoginButton.tsx index e39a1aa5d..ba5b9dfb6 100644 --- a/apps/antalmanac/src/components/Header/LoginButton.tsx +++ b/apps/antalmanac/src/components/Header/LoginButton.tsx @@ -72,16 +72,18 @@ export function LoginButton() { try { const response = await usernameLoginMutation.mutateAsync(userId); - if (response == null) { + await utils.auth.status.invalidate(); + + if (response?.userData == null) { snackbar.enqueueSnackbar(`Logged in as "${userId}", no schedules found.`); setOpen(false); return; } - const loadedSchedule = await AppStore.loadSchedule(response); + const loadedSchedule = await AppStore.loadSchedule(response.userData); if (loadedSchedule == null) { - AppStore.loadSkeletonSchedule(response); + AppStore.loadSkeletonSchedule(response.userData); snackbar.enqueueSnackbar( `Network error loading course information for "${userId}". If this continues to happen, please submit a feedback form.`, @@ -94,8 +96,6 @@ export function LoginButton() { snackbar.enqueueSnackbar(`Schedule for username "${userId}" loaded.`, { variant: 'success' }); setOpen(false); - - await utils.auth.status.invalidate(); } catch (e) { console.error('Error occurred while loading schedules: ', e); snackbar.enqueueSnackbar( @@ -112,9 +112,41 @@ export function LoginButton() { return; } - const response = await googleLoginMutation.mutateAsync(credential.credential); + try { + const response = await googleLoginMutation.mutateAsync(credential.credential); - console.log('response: ', response); + await utils.auth.status.invalidate(); + + if (response?.userData == null) { + snackbar.enqueueSnackbar(`Logged in as "${response?.id}", no schedules found.`); + setOpen(false); + return; + } + + const loadedSchedule = await AppStore.loadSchedule(response.userData); + + if (loadedSchedule == null) { + AppStore.loadSkeletonSchedule(response.userData); + snackbar.enqueueSnackbar( + `Network error loading course information for "${userId}". + If this continues to happen, please submit a feedback form.`, + { variant: 'error' } + ); + setOpen(false); + return; + } + + snackbar.enqueueSnackbar(`Schedule for username "${userId}" loaded.`, { variant: 'success' }); + + setOpen(false); + } catch (e) { + console.error('Error occurred while loading schedules: ', e); + snackbar.enqueueSnackbar( + `Failed to load schedules. If this continues to happen, please submit a feedback form.`, + { variant: 'error' } + ); + setOpen(false); + } }; const handleGoogleError = async () => { diff --git a/apps/backend/src/routers/index.ts b/apps/backend/src/routers/index.ts index 0eeb29556..ac3c7f91a 100644 --- a/apps/backend/src/routers/index.ts +++ b/apps/backend/src/routers/index.ts @@ -27,7 +27,7 @@ export interface GoogleProfile extends Record { export const AccessTokenSchema = type({ id: 'string', - googleId: 'string', + 'googleId?': 'string', }); const privateKey = 'secret'; @@ -62,7 +62,9 @@ const authRouter = router({ const googleId = idToken.sub; - const existingUser = await ddbClient.get('googleId', googleId); + const existingUser = await ddbClient.get('googleId', googleId).catch((e) => { + console.error('Error while looking up google ID: ', e); + }); if (existingUser != null) { const authCookie = jwt.sign( @@ -76,7 +78,7 @@ const authRouter = router({ // Set the auth cookie so the client can be recognized on future requests. context.ctx.res.cookie('auth', authCookie); - return existingUser.userData; + return existingUser; } await ddbClient.documentClient.put({ @@ -101,7 +103,7 @@ const authRouter = router({ context.ctx.res.cookie('auth', authCookie); - return null; + return { id: googleId, userData: null }; } catch (e) { console.error('Error ocurred while logging in with google: ', e); return null; @@ -118,7 +120,7 @@ const authRouter = router({ // Set the auth cookie so the client can be recognized on future requests. context.ctx.res.cookie('auth', authCookie, { maxAge: 1000 * 60 * 60, path: '/' }); - return userData; + return { id, userData }; } await ddbClient.documentClient.put({ @@ -127,12 +129,13 @@ const authRouter = router({ id, }, }); + const authCookie = jwt.sign({ id }, privateKey); // Set the auth cookie so the client can be recognized on future requests. context.ctx.res.cookie('auth', authCookie, { maxAge: 1000 * 60 * 60, path: '/' }); - return null; + return { id, userData: null }; }), logout: procedure.mutation(async (context) => { context.ctx.res.cookie('auth', '', { maxAge: 0 }); From bea498b6071b15b07fc63fa419f2d48c7328b6b7 Mon Sep 17 00:00:00 2001 From: Aponia Date: Mon, 20 May 2024 10:16:17 -0700 Subject: [PATCH 15/18] feat(antalamanc): load and save interfaces --- .../src/components/Header/Header.tsx | 4 +- .../src/components/Header/LoadButton.tsx | 23 ++- .../src/components/Header/SaveButton.tsx | 138 ++++++++++++++++++ 3 files changed, 155 insertions(+), 10 deletions(-) create mode 100644 apps/antalmanac/src/components/Header/SaveButton.tsx diff --git a/apps/antalmanac/src/components/Header/Header.tsx b/apps/antalmanac/src/components/Header/Header.tsx index 622719683..47133da57 100644 --- a/apps/antalmanac/src/components/Header/Header.tsx +++ b/apps/antalmanac/src/components/Header/Header.tsx @@ -6,8 +6,8 @@ import AccountButton from './AccountButton'; import Export from './Export'; import Import from './Import'; import LoadButton from './LoadButton'; -import LoadSaveScheduleFunctionality from './LoadSaveFunctionality'; import LoginButton from './LoginButton'; +import SaveButton from './SaveButton'; import AppDrawer from './SettingsMenu'; import Logo from '$assets/logo.svg'; @@ -57,7 +57,7 @@ const Header = ({ classes }: CustomAppBarProps) => { />
- + diff --git a/apps/antalmanac/src/components/Header/LoadButton.tsx b/apps/antalmanac/src/components/Header/LoadButton.tsx index 90909bf56..efcc25775 100644 --- a/apps/antalmanac/src/components/Header/LoadButton.tsx +++ b/apps/antalmanac/src/components/Header/LoadButton.tsx @@ -12,9 +12,10 @@ import { Stack, TextField, } from '@mui/material'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import { loadSchedule } from '$actions/AppStoreActions'; +import { trpc } from '$lib/trpc'; import AppStore from '$stores/AppStore'; import { useThemeStore } from '$stores/SettingsStore'; @@ -25,6 +26,8 @@ export function LoadButton() { const [skeletonMode, setSkeletonMode] = useState(AppStore.getSkeletonMode()); + const authStatus = trpc.auth.status.useQuery(); + const [userId, setUserId] = useState(''); const isDark = useThemeStore((store) => store.isDark); @@ -57,12 +60,15 @@ export function LoadButton() { console.log('Submitting'); }; - /** - * TODO: actually generate options. - */ - const options = useMemo(() => { - return ['a', 'b', 'c']; - }, []); + const options: string[] = []; + + if (authStatus.data?.id != null) { + options.push(authStatus.data?.id); + } + + if (userId && userId !== authStatus.data?.id) { + options.push(userId); + } useEffect(() => { const handleSkeletonModeChange = () => { @@ -93,7 +99,7 @@ export function LoadButton() { - Load {userId} + Load @@ -101,6 +107,7 @@ export function LoadButton() { { if (newValue != null) { setUserId(newValue); diff --git a/apps/antalmanac/src/components/Header/SaveButton.tsx b/apps/antalmanac/src/components/Header/SaveButton.tsx new file mode 100644 index 000000000..978eea832 --- /dev/null +++ b/apps/antalmanac/src/components/Header/SaveButton.tsx @@ -0,0 +1,138 @@ +import { Save } from '@material-ui/icons'; +import { LoadingButton } from '@mui/lab'; +import { + Autocomplete, + Button, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Stack, + TextField, +} from '@mui/material'; +import { useEffect, useState } from 'react'; + +import { loadSchedule } from '$actions/AppStoreActions'; +import { trpc } from '$lib/trpc'; +import AppStore from '$stores/AppStore'; +import { useThemeStore } from '$stores/SettingsStore'; + +export function SaveButton() { + const [open, setOpen] = useState(false); + + const [loading, setLoading] = useState(false); + + const [skeletonMode, setSkeletonMode] = useState(AppStore.getSkeletonMode()); + + const authStatus = trpc.auth.status.useQuery(); + + const [userId, setUserId] = useState(''); + + const isDark = useThemeStore((store) => store.isDark); + + const loadScheduleAndSetLoading = async (userID: string, rememberMe: boolean) => { + setLoading(true); + await loadSchedule(userID, rememberMe); + setLoading(false); + }; + + const loadSavedSchedule = async () => { + if (typeof Storage === 'undefined') return; + + const savedUserID = window.localStorage.getItem('userID'); + + if (savedUserID == null) return; + + await loadScheduleAndSetLoading(savedUserID, true); + }; + + const handleOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + const handleSubmit = () => { + console.log('Submitting'); + }; + + const options: string[] = []; + + if (authStatus.data?.id != null) { + options.push(authStatus.data?.id); + } + + if (userId && userId !== authStatus.data?.id) { + options.push(userId); + } + + useEffect(() => { + const handleSkeletonModeChange = () => { + setSkeletonMode(AppStore.getSkeletonMode()); + }; + + AppStore.on('skeletonModeChange', handleSkeletonModeChange); + + return () => { + AppStore.off('skeletonModeChange', handleSkeletonModeChange); + }; + }, []); + + useEffect(() => { + loadSavedSchedule(); + }, []); + + return ( + <> + : } + disabled={skeletonMode} + loading={false} + > + Save + + + + Save + + + + Enter a unique user ID here to save a schedule. + + { + if (newValue != null) { + setUserId(newValue); + } + }} + sx={{ width: 300 }} + options={options} + renderInput={(params) => { + return ; + }} + /> + + + + + + + + + + ); +} + +export default SaveButton; From f77f03f02fa8dd666d7778ff9ca5b94d50e19325 Mon Sep 17 00:00:00 2001 From: Aponia Date: Mon, 20 May 2024 10:35:40 -0700 Subject: [PATCH 16/18] feat(antalmanac): access controls for saving --- .../src/components/Header/SaveButton.tsx | 87 ++++++++++++++----- apps/backend/src/db/ddb.ts | 10 +-- 2 files changed, 69 insertions(+), 28 deletions(-) diff --git a/apps/antalmanac/src/components/Header/SaveButton.tsx b/apps/antalmanac/src/components/Header/SaveButton.tsx index 978eea832..10e6c64d8 100644 --- a/apps/antalmanac/src/components/Header/SaveButton.tsx +++ b/apps/antalmanac/src/components/Header/SaveButton.tsx @@ -12,9 +12,11 @@ import { Stack, TextField, } from '@mui/material'; +import { TRPCError } from '@trpc/server'; +import { useSnackbar } from 'notistack'; import { useEffect, useState } from 'react'; -import { loadSchedule } from '$actions/AppStoreActions'; +import analyticsEnum, { logAnalytics } from '$lib/analytics'; import { trpc } from '$lib/trpc'; import AppStore from '$stores/AppStore'; import { useThemeStore } from '$stores/SettingsStore'; @@ -26,27 +28,15 @@ export function SaveButton() { const [skeletonMode, setSkeletonMode] = useState(AppStore.getSkeletonMode()); - const authStatus = trpc.auth.status.useQuery(); - const [userId, setUserId] = useState(''); - const isDark = useThemeStore((store) => store.isDark); + const { enqueueSnackbar } = useSnackbar(); - const loadScheduleAndSetLoading = async (userID: string, rememberMe: boolean) => { - setLoading(true); - await loadSchedule(userID, rememberMe); - setLoading(false); - }; - - const loadSavedSchedule = async () => { - if (typeof Storage === 'undefined') return; - - const savedUserID = window.localStorage.getItem('userID'); + const isDark = useThemeStore((store) => store.isDark); - if (savedUserID == null) return; + const authStatus = trpc.auth.status.useQuery(); - await loadScheduleAndSetLoading(savedUserID, true); - }; + const saveMutation = trpc.users.saveUserData.useMutation(); const handleOpen = () => { setOpen(true); @@ -56,8 +46,63 @@ export function SaveButton() { setOpen(false); }; - const handleSubmit = () => { - console.log('Submitting'); + const handleSubmit = async () => { + setLoading(true); + + logAnalytics({ + category: analyticsEnum.nav.title, + action: analyticsEnum.nav.actions.SAVE_SCHEDULE, + label: userId, + value: 1, + }); + + const normalizedUserId = userId.replace(/\s+/g, ''); + + if (!normalizedUserId) { + enqueueSnackbar('Invalid user ID.', { variant: 'error' }); + setLoading(false); + return; + } + + const scheduleSaveState = AppStore.schedule.getScheduleAsSaveState(); + + try { + await saveMutation.mutateAsync({ + /** + * Requester ID. + * + * The user may be logged in, and save the schedule under a different account. + * Based on the schedule's visibility settings, this may or may not be permitted. + * + * If the user is not logged in, assume that the requester and requestee are the same. + */ + id: authStatus.data?.id ?? normalizedUserId, + + /** + * Assume that the schedule belongs to whatever the specified userId is. + */ + data: { + id: normalizedUserId, + userData: scheduleSaveState, + }, + }); + + enqueueSnackbar( + `Schedule saved under username "${normalizedUserId}". Don't forget to sign up for classes on WebReg!`, + { variant: 'success' } + ); + AppStore.saveSchedule(); + } catch (e) { + if (e instanceof TRPCError) { + enqueueSnackbar(`Schedule could not be saved under username "${normalizedUserId}`, { + variant: 'error', + }); + } else { + enqueueSnackbar('Network error or server is down.', { variant: 'error' }); + } + } + + setLoading(false); }; const options: string[] = []; @@ -82,10 +127,6 @@ export function SaveButton() { }; }, []); - useEffect(() => { - loadSavedSchedule(); - }, []); - return ( <> >> { const existingUserData = await ddbClient.get('id', requesteeId); if (existingUserData == null) { - return undefined; + return null; } const parsedUserData = UserSchema(existingUserData); if (parsedUserData.problems != null) { - return undefined; + return null; } - parsedUserData.data.visibility ??= VISIBILITY.PRIVATE; + const visibility = parsedUserData.data.visibility ?? VISIBILITY.PRIVATE; // Requester and requestee IDs must match if schedule is private. // Otherwise, return the schedule without any additional processing. @@ -166,8 +166,8 @@ class DDBClient>> { // check the schedule's user ID with the user ID making the request // to fully define the visibility system. - if (parsedUserData.data.visibility === VISIBILITY.PRIVATE) { - return requesterId === requesteeId ? parsedUserData.data : undefined; + if (visibility === VISIBILITY.PRIVATE) { + return requesterId === requesteeId ? parsedUserData.data : null; } else { return parsedUserData.data; } From d5d5f7c92b75dc73dbbc8b2ff85242386bd26f2e Mon Sep 17 00:00:00 2001 From: Aponia Date: Mon, 20 May 2024 11:55:42 -0700 Subject: [PATCH 17/18] feat(antalamanc): save button with visibility specified --- .../src/components/Header/SaveButton.tsx | 36 ++++++++++++++++++- apps/backend/src/routers/users.ts | 15 ++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/apps/antalmanac/src/components/Header/SaveButton.tsx b/apps/antalmanac/src/components/Header/SaveButton.tsx index 10e6c64d8..8f441f5e0 100644 --- a/apps/antalmanac/src/components/Header/SaveButton.tsx +++ b/apps/antalmanac/src/components/Header/SaveButton.tsx @@ -9,8 +9,14 @@ import { DialogContent, DialogContentText, DialogTitle, + FormControl, + FormLabel, + FormControlLabel, + Radio, + RadioGroup, Stack, TextField, + Tooltip, } from '@mui/material'; import { TRPCError } from '@trpc/server'; import { useSnackbar } from 'notistack'; @@ -26,6 +32,8 @@ export function SaveButton() { const [loading, setLoading] = useState(false); + const [visibility, setVisibility] = useState('public'); + const [skeletonMode, setSkeletonMode] = useState(AppStore.getSkeletonMode()); const [userId, setUserId] = useState(''); @@ -46,6 +54,10 @@ export function SaveButton() { setOpen(false); }; + const handlevisibilityChange = (e: React.ChangeEvent) => { + setVisibility(e.target.value); + }; + const handleSubmit = async () => { setLoading(true); @@ -83,6 +95,7 @@ export function SaveButton() { */ data: { id: normalizedUserId, + visibility, userData: scheduleSaveState, }, }); @@ -160,6 +173,27 @@ export function SaveButton() { return ; }} /> + + + Visibility + + + } label="Public" /> + + + } label="Open" /> + + + } label="Private" /> + + + @@ -168,7 +202,7 @@ export function SaveButton() { Cancel diff --git a/apps/backend/src/routers/users.ts b/apps/backend/src/routers/users.ts index 82d09c5c3..6475856f0 100644 --- a/apps/backend/src/routers/users.ts +++ b/apps/backend/src/routers/users.ts @@ -1,8 +1,8 @@ import { type } from 'arktype'; import { UserSchema } from '@packages/antalmanac-types'; +import { TRPCError } from '@trpc/server'; import { router, procedure } from '../trpc'; import { ddbClient, VISIBILITY } from '../db/ddb'; -import { TRPCError } from '@trpc/server'; const userInputSchema = type([{ userId: 'string' }, '|', { googleId: 'string' }]); @@ -39,9 +39,18 @@ const usersRouter = router({ */ getUserData: procedure.input(userInputSchema.assert).query(async ({ input }) => { if ('googleId' in input) { - return await ddbClient.getGoogleUserData(input.googleId); + const user = await ddbClient.get('googleId', input.googleId); + if (user == null) return null; + return { id: user.id, visibility: user.visibility, userData: user.userData }; } - return (await ddbClient.getUserData(input.userId)) ?? (await ddbClient.getLegacyUserData(input.userId)); + + const user = (await ddbClient.get('id', input.userId)) ?? (await ddbClient.getLegacyUserData(input.userId)); + + if (user == null) return null; + + const visibility = 'visibility' in user ? user.visibility : undefined; + + return { id: user.id, visibility, userData: user.userData }; }), /** From e460ca991b5e1eafc2982c3dd23e51caf1aef0f1 Mon Sep 17 00:00:00 2001 From: Aponia Date: Mon, 20 May 2024 12:07:36 -0700 Subject: [PATCH 18/18] feat(antalmanac): save and load schedules with respect to visibility --- .../src/components/Header/LoadButton.tsx | 63 ++++++++++++++++++- .../src/components/Header/SaveButton.tsx | 2 +- apps/backend/src/db/ddb.ts | 4 +- apps/backend/src/routers/users.ts | 8 ++- 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/apps/antalmanac/src/components/Header/LoadButton.tsx b/apps/antalmanac/src/components/Header/LoadButton.tsx index efcc25775..20a45e6b7 100644 --- a/apps/antalmanac/src/components/Header/LoadButton.tsx +++ b/apps/antalmanac/src/components/Header/LoadButton.tsx @@ -12,9 +12,11 @@ import { Stack, TextField, } from '@mui/material'; +import { useSnackbar } from 'notistack'; import { useEffect, useState } from 'react'; import { loadSchedule } from '$actions/AppStoreActions'; +import analyticsEnum, { logAnalytics } from '$lib/analytics'; import { trpc } from '$lib/trpc'; import AppStore from '$stores/AppStore'; import { useThemeStore } from '$stores/SettingsStore'; @@ -28,6 +30,10 @@ export function LoadButton() { const authStatus = trpc.auth.status.useQuery(); + const utils = trpc.useUtils(); + + const { enqueueSnackbar } = useSnackbar(); + const [userId, setUserId] = useState(''); const isDark = useThemeStore((store) => store.isDark); @@ -56,8 +62,61 @@ export function LoadButton() { setOpen(false); }; - const handleSubmit = () => { - console.log('Submitting'); + const handleSubmit = async () => { + logAnalytics({ + category: analyticsEnum.nav.title, + action: analyticsEnum.nav.actions.LOAD_SCHEDULE, + label: userId, + value: 1, // rememberMe ? 1 : 0, + }); + + const normalizedUserId = userId.replace(/\s+/g, ''); + + if (!normalizedUserId) { + enqueueSnackbar('Invalid user ID.', { variant: 'error' }); + setLoading(false); + return; + } + + if ( + AppStore.hasUnsavedChanges() && + !window.confirm(`Are you sure you want to load a different schedule? You have unsaved changes!`) + ) { + return; + } + + try { + const res = await utils.users.viewUserData.fetch({ + requesterId: authStatus.data?.id, + requesteeId: userId, + }); + + const scheduleSaveState = res && 'userData' in res ? res.userData : res; + + if (scheduleSaveState == null) { + enqueueSnackbar(`Couldn't find schedules for username "${userId}".`, { variant: 'error' }); + return; + } + + if (await AppStore.loadSchedule(scheduleSaveState)) { + enqueueSnackbar(`Schedule for username "${userId}" loaded.`, { variant: 'success' }); + return; + } + + AppStore.loadSkeletonSchedule(scheduleSaveState); + enqueueSnackbar( + `Network error loading course information for "${userId}". + If this continues to happen, please submit a feedback form.`, + { variant: 'error' } + ); + } catch (e) { + console.error(e); + enqueueSnackbar(`Failed to load schedules. If this continues to happen, please submit a feedback form.`, { + variant: 'error', + }); + } finally { + setLoading(false); + } }; const options: string[] = []; diff --git a/apps/antalmanac/src/components/Header/SaveButton.tsx b/apps/antalmanac/src/components/Header/SaveButton.tsx index 8f441f5e0..5fd0d6e4a 100644 --- a/apps/antalmanac/src/components/Header/SaveButton.tsx +++ b/apps/antalmanac/src/components/Header/SaveButton.tsx @@ -190,7 +190,7 @@ export function SaveButton() { title="Only the owner can view and edit the schedule (after logging in)." placement="left" > - } label="Private" /> + } label="Private" /> diff --git a/apps/backend/src/db/ddb.ts b/apps/backend/src/db/ddb.ts index 55deb53eb..4a0162751 100644 --- a/apps/backend/src/db/ddb.ts +++ b/apps/backend/src/db/ddb.ts @@ -144,7 +144,7 @@ class DDBClient>> { return (await ddbClient.get('googleId', googleId))?.userData; } - async viewUserData(requesterId: string, requesteeId: string) { + async viewUserData(requesteeId: string, requesterId?: string) { const existingUserData = await ddbClient.get('id', requesteeId); if (existingUserData == null) { @@ -157,7 +157,7 @@ class DDBClient>> { return null; } - const visibility = parsedUserData.data.visibility ?? VISIBILITY.PRIVATE; + const visibility = parsedUserData.data.visibility ?? VISIBILITY.PUBLIC; // Requester and requestee IDs must match if schedule is private. // Otherwise, return the schedule without any additional processing. diff --git a/apps/backend/src/routers/users.ts b/apps/backend/src/routers/users.ts index 6475856f0..dc8453c70 100644 --- a/apps/backend/src/routers/users.ts +++ b/apps/backend/src/routers/users.ts @@ -9,8 +9,10 @@ const userInputSchema = type([{ userId: 'string' }, '|', { googleId: 'string' }] const viewInputSchema = type({ /** * ID of the user who's requesting to view another user's schedule. + * + * Maybe undefined if user is viewing anonymously. */ - requesterId: 'string', + 'requesterId?': 'string | undefined', /** * ID of the user whose schedule is being requested. @@ -60,7 +62,7 @@ const usersRouter = router({ /** * Assign default visility value. */ - input.data.visibility ??= VISIBILITY.PRIVATE; + input.data.visibility ??= VISIBILITY.PUBLIC; // Requester and requestee IDs must match if schedule is private. @@ -95,7 +97,7 @@ const usersRouter = router({ * - open: Anybody can view and edit. */ viewUserData: procedure.input(viewInputSchema.assert).query(async ({ input }) => { - return await ddbClient.viewUserData(input.requesterId, input.requesteeId); + return await ddbClient.viewUserData(input.requesteeId, input.requesterId); }), });