diff --git a/apps/antalmanac/package.json b/apps/antalmanac/package.json
index ee1079e93..04e7f9751 100644
--- a/apps/antalmanac/package.json
+++ b/apps/antalmanac/package.json
@@ -34,10 +34,13 @@
"@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",
+ "@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/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/AccountButton.tsx b/apps/antalmanac/src/components/Header/AccountButton.tsx
new file mode 100644
index 000000000..f3ce59203
--- /dev/null
+++ b/apps/antalmanac/src/components/Header/AccountButton.tsx
@@ -0,0 +1,143 @@
+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';
+
+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);
+ };
+
+ 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();
+ };
+
+ 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 (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default AccountButton;
diff --git a/apps/antalmanac/src/components/Header/Header.tsx b/apps/antalmanac/src/components/Header/Header.tsx
index 7da5e8a31..a27800f05 100644
--- a/apps/antalmanac/src/components/Header/Header.tsx
+++ b/apps/antalmanac/src/components/Header/Header.tsx
@@ -2,12 +2,16 @@ 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 Import from './Import';
-import LoadSaveScheduleFunctionality from './LoadSaveFunctionality';
+import LoadButton from './LoadButton';
+import LoginButton from './LoginButton';
+import SaveButton from './SaveButton';
import AppDrawer from './SettingsMenu';
import Logo from '$assets/logo.svg';
import MobileLogo from '$assets/mobile-logo.svg';
+import { trpc } from '$lib/trpc';
const styles = {
appBar: {
@@ -39,6 +43,8 @@ interface CustomAppBarProps {
const Header = ({ classes }: CustomAppBarProps) => {
const isMobileScreen = useMediaQuery('(max-width:750px)');
+ const authStatus = trpc.auth.status.useQuery();
+
return (
@@ -50,8 +56,14 @@ const Header = ({ classes }: CustomAppBarProps) => {
/>
-
+
+
+
+
+
+ {authStatus.data ?
:
}
+
diff --git a/apps/antalmanac/src/components/Header/LoadButton.tsx b/apps/antalmanac/src/components/Header/LoadButton.tsx
new file mode 100644
index 000000000..20a45e6b7
--- /dev/null
+++ b/apps/antalmanac/src/components/Header/LoadButton.tsx
@@ -0,0 +1,197 @@
+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 { 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';
+
+export function LoadButton() {
+ const [open, setOpen] = useState(false);
+
+ const [loading, setLoading] = useState(false);
+
+ const [skeletonMode, setSkeletonMode] = useState(AppStore.getSkeletonMode());
+
+ const authStatus = trpc.auth.status.useQuery();
+
+ const utils = trpc.useUtils();
+
+ const { enqueueSnackbar } = useSnackbar();
+
+ 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 = 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[] = [];
+
+ 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}
+ >
+ Load
+
+
+
+ >
+ );
+}
+
+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/apps/antalmanac/src/components/Header/LoginButton.tsx b/apps/antalmanac/src/components/Header/LoginButton.tsx
new file mode 100644
index 000000000..ba5b9dfb6
--- /dev/null
+++ b/apps/antalmanac/src/components/Header/LoginButton.tsx
@@ -0,0 +1,221 @@
+import LoginIcon from '@mui/icons-material/Login';
+import {
+ Box,
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Divider,
+ 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 [open, setOpen] = useState(false);
+
+ const snackbar = useSnackbar();
+
+ const usernameLoginMutation = trpc.auth.loginUsername.useMutation();
+
+ const googleLoginMutation = trpc.auth.loginGoogle.useMutation();
+
+ const utils = trpc.useUtils();
+
+ const handleOpen = () => {
+ setOpen(true);
+ };
+
+ const handleClose = () => {
+ setOpen(false);
+ };
+
+ const handleUserIdChange = (event: React.ChangeEvent) => {
+ setUserId(event.target.value);
+ };
+
+ 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;
+ }
+
+ try {
+ const response = await usernameLoginMutation.mutateAsync(userId);
+
+ 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.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 handleGoogleLogin = async (credential: CredentialResponse) => {
+ if (credential.credential == null) {
+ console.error('Did not receive google credential');
+ return;
+ }
+
+ try {
+ const response = await googleLoginMutation.mutateAsync(credential.credential);
+
+ 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 () => {
+ console.log('Login Failed');
+ };
+
+ return (
+ <>
+ }>
+ Login
+
+
+ >
+ );
+}
+
+export default LoginButton;
diff --git a/apps/antalmanac/src/components/Header/SaveButton.tsx b/apps/antalmanac/src/components/Header/SaveButton.tsx
new file mode 100644
index 000000000..5fd0d6e4a
--- /dev/null
+++ b/apps/antalmanac/src/components/Header/SaveButton.tsx
@@ -0,0 +1,213 @@
+import { Save } from '@material-ui/icons';
+import { LoadingButton } from '@mui/lab';
+import {
+ Autocomplete,
+ Button,
+ CircularProgress,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ DialogTitle,
+ FormControl,
+ FormLabel,
+ FormControlLabel,
+ Radio,
+ RadioGroup,
+ Stack,
+ TextField,
+ Tooltip,
+} from '@mui/material';
+import { TRPCError } from '@trpc/server';
+import { useSnackbar } from 'notistack';
+import { useEffect, useState } from 'react';
+
+import analyticsEnum, { logAnalytics } from '$lib/analytics';
+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 [visibility, setVisibility] = useState('public');
+
+ const [skeletonMode, setSkeletonMode] = useState(AppStore.getSkeletonMode());
+
+ const [userId, setUserId] = useState('');
+
+ const { enqueueSnackbar } = useSnackbar();
+
+ const isDark = useThemeStore((store) => store.isDark);
+
+ const authStatus = trpc.auth.status.useQuery();
+
+ const saveMutation = trpc.users.saveUserData.useMutation();
+
+ const handleOpen = () => {
+ setOpen(true);
+ };
+
+ const handleClose = () => {
+ setOpen(false);
+ };
+
+ const handlevisibilityChange = (e: React.ChangeEvent) => {
+ setVisibility(e.target.value);
+ };
+
+ 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,
+ visibility,
+ 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[] = [];
+
+ 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);
+ };
+ }, []);
+
+ return (
+ <>
+ : }
+ disabled={skeletonMode}
+ loading={false}
+ >
+ Save
+
+
+
+ >
+ );
+}
+
+export default SaveButton;
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..1055339ff 100644
--- a/apps/antalmanac/src/providers/Query.tsx
+++ b/apps/antalmanac/src/providers/Query.tsx
@@ -1,11 +1,37 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { httpLink } 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: [
+ httpLink({
+ fetch(url, options) {
+ return fetch(url, {
+ ...options,
+ credentials: 'include',
+ });
+ },
+ url: getEndpoint() + '/trpc',
+ }),
+ ],
+ })
+ );
- return {props.children};
+ return (
+
+ {props.children};
+
+ );
}
diff --git a/apps/backend/package.json b/apps/backend/package.json
index 6f1d36e3e..67d142b0b 100644
--- a/apps/backend/package.json
+++ b/apps/backend/package.json
@@ -15,10 +15,13 @@
"@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",
"express": "^4.18.2",
+ "jose": "^5.2.4",
+ "jsonwebtoken": "^9.0.2",
"mongodb": "^5.0.1",
"mongoose": "^7.1.0j",
"superjson": "^1.12.3",
@@ -28,8 +31,10 @@
"@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",
"@typescript-eslint/eslint-plugin": "^5.52.0",
"@typescript-eslint/parser": "^5.52.0",
"concurrently": "^8.0.1",
@@ -43,11 +48,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/db/ddb.ts b/apps/backend/src/db/ddb.ts
index b26f6e72a..4a0162751 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;
@@ -144,20 +144,20 @@ 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) {
- 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.PUBLIC;
// Requester and requestee IDs must match if schedule is private.
// Otherwise, return the schedule without any additional processing.
@@ -166,12 +166,11 @@ 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;
}
-
}
}
diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts
index bb1e2a356..9ed243bef 100644
--- a/apps/backend/src/index.ts
+++ b/apps/backend/src/index.ts
@@ -2,26 +2,31 @@ 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 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) {
- await connectToMongoDB();
+ const corsOptions: CorsOptions = {
+ credentials: true,
+ origin: corsEnabled ? origins : true,
+ };
const app = express();
- app.use(cors(corsEnabled ? corsOptions : undefined));
+
+ 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);
@@ -33,16 +38,14 @@ 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.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)
+ res.type('image/png');
+ res.send(Buffer.from(buffer));
});
-
+
app.use(
'/trpc',
createExpressMiddleware({
diff --git a/apps/backend/src/routers/index.ts b/apps/backend/src/routers/index.ts
index 2611cca4a..ac3c7f91a 100644
--- a/apps/backend/src/routers/index.ts
+++ b/apps/backend/src/routers/index.ts
@@ -1,12 +1,152 @@
-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;
+}
+
+export const AccessTokenSchema = type({
+ id: 'string',
+ 'googleId?': 'string',
+});
+
+const privateKey = 'secret';
+
+const authRouter = router({
+ status: procedure.query(async (context) => {
+ const authCookie = context.ctx.req.cookies['auth'];
+
+ if (authCookie == null) return null;
+
+ try {
+ 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 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;
+
+ const googleId = idToken.sub;
+
+ 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(
+ {
+ 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;
+ }
+
+ 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 { id: googleId, userData: 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 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 { id, 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 { id, userData: null };
+ }),
+ logout: procedure.mutation(async (context) => {
+ context.ctx.res.cookie('auth', '', { maxAge: 0 });
+ }),
+});
const appRouter = router({
+ auth: authRouter,
news: newsRouter,
users: usersRouter,
- zotcourse: zotcourseRouter
+ zotcourse: zotcourseRouter,
});
export type AppRouter = typeof appRouter;
diff --git a/apps/backend/src/routers/users.ts b/apps/backend/src/routers/users.ts
index 82d09c5c3..dc8453c70 100644
--- a/apps/backend/src/routers/users.ts
+++ b/apps/backend/src/routers/users.ts
@@ -1,16 +1,18 @@
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' }]);
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.
@@ -39,9 +41,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 };
}),
/**
@@ -51,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.
@@ -86,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);
}),
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fe1cbba64..a11c1d6cb 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)
@@ -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)
@@ -107,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
@@ -266,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)
@@ -309,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
@@ -321,6 +333,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
@@ -343,12 +361,18 @@ 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
'@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)
@@ -3390,6 +3414,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:
@@ -4061,6 +4095,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
@@ -4096,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:
@@ -4193,6 +4249,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:
@@ -4941,6 +5003,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
@@ -5170,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'}
@@ -5520,6 +5599,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
@@ -5763,7 +5848,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
@@ -5805,35 +5890,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'}
@@ -5867,70 +5923,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'}
@@ -6935,6 +6927,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
@@ -7029,6 +7025,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:
@@ -7099,6 +7111,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'}
@@ -7204,10 +7231,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==}
@@ -7244,7 +7299,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==}
@@ -8407,6 +8461,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'}
@@ -8867,64 +8929,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:
@@ -9511,7 +9573,6 @@ packages:
/yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
- dev: true
/yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}