From 58c941c116076c3fad9563f23f3db348d8e83fdd Mon Sep 17 00:00:00 2001 From: Jared Lucas Amistad Schulz Date: Fri, 13 Dec 2024 15:33:22 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=A5=9Elinked=20frontend=20to=20backen?= =?UTF-8?q?d,=20not=20made=20ui=20elements=20to=20represent=20this=20data?= =?UTF-8?q?=20though?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/index.ts | 6 ++--- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 26 +++++++++++++++---- frontend/src/Calendar/Calendar.tsx | 40 +++++++++++++++++++++++++++--- package.json | 5 ++++ pnpm-lock.yaml | 29 ++++++++++++++++++++++ 6 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 package.json create mode 100644 pnpm-lock.yaml diff --git a/backend/src/index.ts b/backend/src/index.ts index 6f71065..1400d8d 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -720,9 +720,9 @@ app.get("/events", async (req, res: Response) => { skip: page * 10, take: 10, }); - + if (!events || events.length === 0) { - return res.status(404).json({ message: "No events found." }); + return res.status(200).json({ message: "No events found." }); } return res.status(200).json(events); @@ -782,7 +782,7 @@ app.get("/user/events", async (req, res: Response) => { take: 10, }), }); - + if (!events || events.length === 0) { return res.status(404).json({ message: "No events found." }); } diff --git a/frontend/package.json b/frontend/package.json index 266145d..e960ec4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ }, "devDependencies": { "@eslint/js": "^9.16.0", + "@types/node": "^22.10.2", "@types/react": "^18.3.14", "@types/react-dom": "^18.3.3", "@vitejs/plugin-react": "^4.3.4", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 4e1d65e..1faeda4 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -30,6 +30,9 @@ importers: '@eslint/js': specifier: ^9.16.0 version: 9.16.0 + '@types/node': + specifier: ^22.10.2 + version: 22.10.2 '@types/react': specifier: ^18.3.14 version: 18.3.16 @@ -38,7 +41,7 @@ importers: version: 18.3.5(@types/react@18.3.16) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.3.4(vite@5.4.11) + version: 4.3.4(vite@5.4.11(@types/node@22.10.2)) eslint: specifier: ^9.16.0 version: 9.16.0 @@ -56,7 +59,7 @@ importers: version: 8.18.0(eslint@9.16.0)(typescript@5.7.2) vite: specifier: ^5.4.11 - version: 5.4.11 + version: 5.4.11(@types/node@22.10.2) packages: @@ -492,6 +495,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@22.10.2': + resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} + '@types/prop-types@15.7.13': resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} @@ -1053,6 +1059,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + update-browserslist-db@1.1.1: resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} hasBin: true @@ -1473,6 +1482,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/node@22.10.2': + dependencies: + undici-types: 6.20.0 + '@types/prop-types@15.7.13': {} '@types/react-dom@18.3.5(@types/react@18.3.16)': @@ -1561,14 +1574,14 @@ snapshots: '@typescript-eslint/types': 8.18.0 eslint-visitor-keys: 4.2.0 - '@vitejs/plugin-react@4.3.4(vite@5.4.11)': + '@vitejs/plugin-react@4.3.4(vite@5.4.11(@types/node@22.10.2))': dependencies: '@babel/core': 7.26.0 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.11 + vite: 5.4.11(@types/node@22.10.2) transitivePeerDependencies: - supports-color @@ -2057,6 +2070,8 @@ snapshots: typescript@5.7.2: {} + undici-types@6.20.0: {} + update-browserslist-db@1.1.1(browserslist@4.24.2): dependencies: browserslist: 4.24.2 @@ -2067,12 +2082,13 @@ snapshots: dependencies: punycode: 2.3.1 - vite@5.4.11: + vite@5.4.11(@types/node@22.10.2): dependencies: esbuild: 0.21.5 postcss: 8.4.47 rollup: 4.24.0 optionalDependencies: + '@types/node': 22.10.2 fsevents: 2.3.3 which@2.0.2: diff --git a/frontend/src/Calendar/Calendar.tsx b/frontend/src/Calendar/Calendar.tsx index 2c8ae1f..ac83e22 100644 --- a/frontend/src/Calendar/Calendar.tsx +++ b/frontend/src/Calendar/Calendar.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { format, startOfMonth, endOfMonth, eachDayOfInterval, getDay, addMonths, subMonths, getDaysInMonth } from 'date-fns' import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline" import classes from './Calendar.module.css' @@ -12,11 +12,12 @@ function Calendar() { const addMonth = (currentDate:Date) => { setCurrentDate(addMonths(currentDate, 1)); + fetchEvents(); } const subMonth = (currentDate:Date) => { - console.log(subMonth); setCurrentDate(subMonths(currentDate, 1)); + fetchEvents(); } const [currentDate, setCurrentDate] = useState(new Date()); @@ -29,8 +30,41 @@ function Calendar() { const days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; const startDayIndex = mapDayIndex(getDay(firstDay)); const dayBuffer = (42-(getDaysInMonth(currentDate)+startDayIndex)); - //const [eventsInMonth, setEventsInMonth] = useState([]); + const getEventsInMonth = async(firstDay:Date, lastDay:Date) => { + const params = new URLSearchParams({ + after: format(firstDay, 'yyyy-MM-dd'), + before: format(lastDay, 'yyyy-MM-dd'), + }); + + try { + //not gonna use user/events for now as idk if login is working with the frontedn + const res = await fetch(`${import.meta.env.VITE_BACKEND_URL + '/events?' + params}`) + + if (res.ok) { + const data = await res.json(); + return data; + } else { + return []; + } + + } catch(error) { + console.error(error); + return [] + } + } + const [eventsInMonth, setEventsInMonth] = useState([]); + + const fetchEvents = async () => { + const events = await getEventsInMonth(firstDay, lastDay); + setEventsInMonth(events); + }; + + useEffect(() => { + fetchEvents(); + }, [currentDate]); + + console.log(eventsInMonth); return (
diff --git a/package.json b/package.json new file mode 100644 index 0000000..9cdcef6 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "@types/node": "^22.10.2" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..9c1b30e --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,29 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^22.10.2 + version: 22.10.2 + +packages: + + '@types/node@22.10.2': + resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} + + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + +snapshots: + + '@types/node@22.10.2': + dependencies: + undici-types: 6.20.0 + + undici-types@6.20.0: {} From 94ba5e1d4686d16fa9284ec2a736cfbd186caeb2 Mon Sep 17 00:00:00 2001 From: Jared Lucas Amistad Schulz Date: Sat, 14 Dec 2024 01:29:56 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=9A=81created=20dictionary=20to=20map?= =?UTF-8?q?=20events=20to=20dates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/Calendar/Calendar.tsx | 24 +++++++++++++++++++--- frontend/src/CalendarCell/CalendarCell.tsx | 9 ++++++-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/frontend/src/Calendar/Calendar.tsx b/frontend/src/Calendar/Calendar.tsx index ac83e22..3b13816 100644 --- a/frontend/src/Calendar/Calendar.tsx +++ b/frontend/src/Calendar/Calendar.tsx @@ -4,6 +4,10 @@ import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline" import classes from './Calendar.module.css' import CalendarCell from '../CalendarCell/CalendarCell' import CalendarDayName from '../CalendarDayName/CalendarDayName' +interface Event { + startDateTime: Date; + [key: string]: any; +}; function Calendar() { const mapDayIndex = (dayIndex:number) => { @@ -54,17 +58,31 @@ function Calendar() { } } const [eventsInMonth, setEventsInMonth] = useState([]); - + const [eventsByDate, setEventsByDate] = useState>({}); + const fetchEvents = async () => { const events = await getEventsInMonth(firstDay, lastDay); setEventsInMonth(events); }; + const buildEventsDict = async (events: Event[]) => { + const dict: Record = {} + events.forEach((item: Event) => { + const date = item.startDateTime.getTime(); + if(!dict[date]) { + dict[date] = [] + } + dict[date].push(item); + }); + setEventsByDate(dict); + } + useEffect(() => { fetchEvents(); + buildEventsDict(eventsInMonth); }, [currentDate]); - console.log(eventsInMonth); + console.log(eventsByDate); return (
@@ -87,7 +105,7 @@ function Calendar() { })} { daysOfMonth.map((day) => { - return + return })} {Array.from({length:dayBuffer}).map((_, ) => { diff --git a/frontend/src/CalendarCell/CalendarCell.tsx b/frontend/src/CalendarCell/CalendarCell.tsx index b03892a..46e933a 100644 --- a/frontend/src/CalendarCell/CalendarCell.tsx +++ b/frontend/src/CalendarCell/CalendarCell.tsx @@ -1,7 +1,12 @@ import classes from './CalendarCell.module.css'; +interface Event { + startDateTime: Date; + [key: string]: any; +}; type CalendarCellProps = { - date?: string + date?: string, + events?: Event[] }; function CalendarCell(props: CalendarCellProps) { @@ -11,7 +16,7 @@ function CalendarCell(props: CalendarCellProps) {

{props.date ? props.date : ''}

- +
) From ad494c65ee512bdda059dd008a5fec896ad089b8 Mon Sep 17 00:00:00 2001 From: Jared Lucas Amistad Schulz Date: Sat, 14 Dec 2024 03:23:00 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=97=BEpreliminary=20calendar=20event?= =?UTF-8?q?=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/Calendar/Calendar.module.css | 1 - .../src/CalendarCell/CalendarCell.module.css | 7 +++-- frontend/src/CalendarCell/CalendarCell.tsx | 5 ++-- .../CalendarEventElem/CalendarEventElem.tsx | 18 +++++++++++++ .../CalendarEventElement.module.css | 27 +++++++++++++++++++ 5 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 frontend/src/CalendarEventElem/CalendarEventElem.tsx create mode 100644 frontend/src/CalendarEventElem/CalendarEventElement.module.css diff --git a/frontend/src/Calendar/Calendar.module.css b/frontend/src/Calendar/Calendar.module.css index b09be8d..6eca537 100644 --- a/frontend/src/Calendar/Calendar.module.css +++ b/frontend/src/Calendar/Calendar.module.css @@ -52,4 +52,3 @@ .monthYearName { font-size: 1.5rem; } - \ No newline at end of file diff --git a/frontend/src/CalendarCell/CalendarCell.module.css b/frontend/src/CalendarCell/CalendarCell.module.css index 52a3133..f85311a 100644 --- a/frontend/src/CalendarCell/CalendarCell.module.css +++ b/frontend/src/CalendarCell/CalendarCell.module.css @@ -16,6 +16,9 @@ .dateElement { font-size: 0.875rem; } -.eventSpace{ - height: 100%; +.eventSpace { + display: flex; + flex-direction: column; + align-items: center; + padding-top: 3%; } \ No newline at end of file diff --git a/frontend/src/CalendarCell/CalendarCell.tsx b/frontend/src/CalendarCell/CalendarCell.tsx index 46e933a..85a59d4 100644 --- a/frontend/src/CalendarCell/CalendarCell.tsx +++ b/frontend/src/CalendarCell/CalendarCell.tsx @@ -1,12 +1,14 @@ import classes from './CalendarCell.module.css'; +import CalendarEventElem from '../CalendarEventElem/CalendarEventElem'; interface Event { startDateTime: Date; [key: string]: any; }; type CalendarCellProps = { - date?: string, + date: string, events?: Event[] + [key: string]: any; //gonna fix this and fill it out with whatever the backend sends soon }; function CalendarCell(props: CalendarCellProps) { @@ -16,7 +18,6 @@ function CalendarCell(props: CalendarCellProps) {

{props.date ? props.date : ''}

-
) diff --git a/frontend/src/CalendarEventElem/CalendarEventElem.tsx b/frontend/src/CalendarEventElem/CalendarEventElem.tsx new file mode 100644 index 0000000..b2a9260 --- /dev/null +++ b/frontend/src/CalendarEventElem/CalendarEventElem.tsx @@ -0,0 +1,18 @@ +import classes from './CalendarEventElement.module.css'; +import { format } from 'date-fns' +type CalendarEventElemProps = { + eventId?: number + eventTitle: string + eventStartDate: Date +} + +function CalendarEventElem(props: CalendarEventElemProps) { + return ( +
+

{props.eventTitle}

+

{format(props.eventStartDate, 'ha')}

+
+ ) +} + +export default CalendarEventElem \ No newline at end of file diff --git a/frontend/src/CalendarEventElem/CalendarEventElement.module.css b/frontend/src/CalendarEventElem/CalendarEventElement.module.css new file mode 100644 index 0000000..c613119 --- /dev/null +++ b/frontend/src/CalendarEventElem/CalendarEventElement.module.css @@ -0,0 +1,27 @@ +.CalEventElemWrapper{ + border-radius: 20px; + display: flex; + justify-content: space-between; + align-items: center; + width: 90%; + height: 20px; + padding: 2%; + background-color: hsl(201, 39%, 87%); +} + +.title { + margin-left: 2%; + white-space: nowrap; + overflow: hidden; + font-size: 14px; + color: hsl(216.4,78%,24.9%); + font-weight:bold; + max-width: 70%; + text-overflow: ellipsis; +} + +.startTime{ + margin-right: 2%; + font-size: 12px; + color: hsl(216, 50%, 35%); +} From caf5aabec18a13a7a782b89b9fd864c844107eb9 Mon Sep 17 00:00:00 2001 From: Jared Lucas Amistad Schulz Date: Sat, 14 Dec 2024 10:32:09 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=A6=88rendered=20individual=20event?= =?UTF-8?q?=20components=20for=20each=20individual=20day=20in=20the=20cale?= =?UTF-8?q?ndar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/Calendar/Calendar.tsx | 14 +++++++++----- frontend/src/CalendarCell/CalendarCell.tsx | 7 ++++++- .../src/CalendarEventElem/CalendarEventElem.tsx | 15 ++++++++++----- .../CalendarEventElement.module.css | 1 + 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/frontend/src/Calendar/Calendar.tsx b/frontend/src/Calendar/Calendar.tsx index 3b13816..bdd8398 100644 --- a/frontend/src/Calendar/Calendar.tsx +++ b/frontend/src/Calendar/Calendar.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import { format, startOfMonth, endOfMonth, eachDayOfInterval, getDay, addMonths, subMonths, getDaysInMonth } from 'date-fns' +import { format, startOfMonth, endOfMonth, eachDayOfInterval, getDay, addMonths, subMonths, getDaysInMonth, getTime, startOfDay } from 'date-fns' import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline" import classes from './Calendar.module.css' import CalendarCell from '../CalendarCell/CalendarCell' @@ -68,7 +68,7 @@ function Calendar() { const buildEventsDict = async (events: Event[]) => { const dict: Record = {} events.forEach((item: Event) => { - const date = item.startDateTime.getTime(); + const date = getTime(startOfDay(item.startDateTime)); if(!dict[date]) { dict[date] = [] } @@ -79,9 +79,13 @@ function Calendar() { useEffect(() => { fetchEvents(); - buildEventsDict(eventsInMonth); }, [currentDate]); - + + useEffect(() => { + buildEventsDict(eventsInMonth); + }, [eventsInMonth]) + + console.log(eventsInMonth) console.log(eventsByDate); return (
@@ -105,7 +109,7 @@ function Calendar() { })} { daysOfMonth.map((day) => { - return + return })} {Array.from({length:dayBuffer}).map((_, ) => { diff --git a/frontend/src/CalendarCell/CalendarCell.tsx b/frontend/src/CalendarCell/CalendarCell.tsx index 85a59d4..4fa3ff1 100644 --- a/frontend/src/CalendarCell/CalendarCell.tsx +++ b/frontend/src/CalendarCell/CalendarCell.tsx @@ -6,7 +6,7 @@ interface Event { }; type CalendarCellProps = { - date: string, + date?: string, events?: Event[] [key: string]: any; //gonna fix this and fill it out with whatever the backend sends soon }; @@ -18,6 +18,11 @@ function CalendarCell(props: CalendarCellProps) {

{props.date ? props.date : ''}

+ { + props.events ? props.events.map((event) => { + return + }) : '' + }
) diff --git a/frontend/src/CalendarEventElem/CalendarEventElem.tsx b/frontend/src/CalendarEventElem/CalendarEventElem.tsx index b2a9260..3bebcdc 100644 --- a/frontend/src/CalendarEventElem/CalendarEventElem.tsx +++ b/frontend/src/CalendarEventElem/CalendarEventElem.tsx @@ -1,16 +1,21 @@ import classes from './CalendarEventElement.module.css'; import { format } from 'date-fns' + +interface Event { + startDateTime: Date; + [key: string]: any; +}; + type CalendarEventElemProps = { - eventId?: number - eventTitle: string - eventStartDate: Date + event: Event } function CalendarEventElem(props: CalendarEventElemProps) { + console.log(props.event) return (
-

{props.eventTitle}

-

{format(props.eventStartDate, 'ha')}

+

{props.event.name}

+

{format(props.event.startDateTime, 'ha')}

) } diff --git a/frontend/src/CalendarEventElem/CalendarEventElement.module.css b/frontend/src/CalendarEventElem/CalendarEventElement.module.css index c613119..cb33497 100644 --- a/frontend/src/CalendarEventElem/CalendarEventElement.module.css +++ b/frontend/src/CalendarEventElem/CalendarEventElement.module.css @@ -7,6 +7,7 @@ height: 20px; padding: 2%; background-color: hsl(201, 39%, 87%); + margin-bottom: 5px; } .title { From 9770dea6b3e05a8df3612e26a52e580d724d50c2 Mon Sep 17 00:00:00 2001 From: Jared Lucas Amistad Schulz Date: Sun, 15 Dec 2024 00:47:02 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=90=B3merged=20with=20main=20branch?= =?UTF-8?q?=20and=20added=20initial=20functionality=20for=20displaying=20m?= =?UTF-8?q?odal(not=20working=20properly=20yet,=20just=20wanted=20to=20get?= =?UTF-8?q?=20this=20commit=20up=20just=20in=20case)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/docker-compose.yml | 16 - backend/pnpm-lock.yaml | 2792 ----------------- .../20241213074516_init/migration.sql | 2 + .../migration.sql | 8 + backend/scripts/run-integration.sh | 14 +- backend/scripts/wait-for-it.sh | 182 -- backend/src/index.ts | 501 +-- backend/src/requestTypes.ts | 4 + backend/src/routes/OTP/OTPToken.ts | 18 +- backend/src/routes/OTP/generateOTP.ts | 28 +- backend/src/routes/OTP/verifyOTP.ts | 16 +- backend/tests/createKeyword.test.ts | 223 ++ backend/tests/otp.test.ts | 177 +- 13 files changed, 674 insertions(+), 3307 deletions(-) delete mode 100644 backend/docker-compose.yml delete mode 100644 backend/pnpm-lock.yaml create mode 100644 backend/prisma/migrations/20241213074516_init/migration.sql create mode 100644 backend/prisma/migrations/20241213143101_20241213074516/migration.sql delete mode 100755 backend/scripts/wait-for-it.sh create mode 100644 backend/tests/createKeyword.test.ts diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml deleted file mode 100644 index 4f0674b..0000000 --- a/backend/docker-compose.yml +++ /dev/null @@ -1,16 +0,0 @@ -services: - db: - image: postgres:14.1-alpine - restart: always - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=postgres - - POSTGRES_DB=local_db - ports: - - '5432:5432' - volumes: - - db:/var/lib/postgresql/data - -volumes: - db: - driver: local \ No newline at end of file diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml deleted file mode 100644 index add81fc..0000000 --- a/backend/pnpm-lock.yaml +++ /dev/null @@ -1,2792 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@prisma/client': - specifier: ^5.22.0 - version: 5.22.0(prisma@5.22.0) - bcrypt: - specifier: ^5.1.1 - version: 5.1.1 - connect-redis: - specifier: ^7.1.1 - version: 7.1.1(express-session@1.18.1) - cors: - specifier: ^2.8.5 - version: 2.8.5 - dayjs: - specifier: ^1.11.13 - version: 1.11.13 - express: - specifier: ^4.21.2 - version: 4.21.2 - express-session: - specifier: ^1.18.1 - version: 1.18.1 - form-data: - specifier: ^4.0.1 - version: 4.0.1 - mailgun.js: - specifier: ^10.2.3 - version: 10.2.3 - prisma: - specifier: ^5.22.0 - version: 5.22.0 - redis: - specifier: ^4.7.0 - version: 4.7.0 - zod: - specifier: ^3.23.8 - version: 3.23.8 - zod-prisma-types: - specifier: ^3.2.1 - version: 3.2.1(@prisma/client@5.22.0(prisma@5.22.0))(prisma@5.22.0) - devDependencies: - '@types/bcrypt': - specifier: ^5.0.2 - version: 5.0.2 - '@types/cors': - specifier: ^2.8.17 - version: 2.8.17 - '@types/express': - specifier: ^4.17.21 - version: 4.17.21 - '@types/express-session': - specifier: ^1.18.1 - version: 1.18.1 - '@types/node': - specifier: ^22.10.1 - version: 22.10.1 - supertest: - specifier: ^7.0.0 - version: 7.0.0 - ts-node: - specifier: ^10.9.2 - version: 10.9.2(@types/node@22.10.1)(typescript@5.7.2) - tsx: - specifier: ^4.19.2 - version: 4.19.2 - typescript: - specifier: ^5.7.2 - version: 5.7.2 - vitest: - specifier: ^2.1.8 - version: 2.1.8(@types/node@22.10.1) - vitest-mock-extended: - specifier: ^2.0.2 - version: 2.0.2(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.1)) - -packages: - - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - - '@esbuild/aix-ppc64@0.23.1': - resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.23.1': - resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.23.1': - resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.23.1': - resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.23.1': - resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.23.1': - resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.23.1': - resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.23.1': - resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.23.1': - resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.23.1': - resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.23.1': - resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.23.1': - resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.23.1': - resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.23.1': - resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.23.1': - resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.23.1': - resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.23.1': - resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.23.1': - resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.23.1': - resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.23.1': - resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.23.1': - resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.23.1': - resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.23.1': - resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.23.1': - resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - - '@mapbox/node-pre-gyp@1.0.11': - resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} - hasBin: true - - '@prisma/client@5.22.0': - resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==} - engines: {node: '>=16.13'} - peerDependencies: - prisma: '*' - peerDependenciesMeta: - prisma: - optional: true - - '@prisma/debug@5.22.0': - resolution: {integrity: sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==} - - '@prisma/debug@6.0.1': - resolution: {integrity: sha512-jQylgSOf7ibTVxqBacnAlVGvek6fQxJIYCQOeX2KexsfypNzXjJQSS2o5s+Mjj2Np93iSOQUaw6TvPj8syhG4w==} - - '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': - resolution: {integrity: sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==} - - '@prisma/engines@5.22.0': - resolution: {integrity: sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==} - - '@prisma/fetch-engine@5.22.0': - resolution: {integrity: sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==} - - '@prisma/generator-helper@6.0.1': - resolution: {integrity: sha512-Qhli2Rtr0fyRcI490oLgSVlUSXp3cdbnL/yqrnChYyziourszWqqw7U5IEoOsvyWJ9iaOupduf9Sde7knUEA/Q==} - - '@prisma/get-platform@5.22.0': - resolution: {integrity: sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==} - - '@redis/bloom@1.2.0': - resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/client@1.6.0': - resolution: {integrity: sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==} - engines: {node: '>=14'} - - '@redis/graph@1.1.1': - resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/json@1.0.7': - resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/search@1.2.0': - resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/time-series@1.1.0': - resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@rollup/rollup-android-arm-eabi@4.28.1': - resolution: {integrity: sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.28.1': - resolution: {integrity: sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.28.1': - resolution: {integrity: sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.28.1': - resolution: {integrity: sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.28.1': - resolution: {integrity: sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.28.1': - resolution: {integrity: sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.28.1': - resolution: {integrity: sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.28.1': - resolution: {integrity: sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.28.1': - resolution: {integrity: sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.28.1': - resolution: {integrity: sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-loongarch64-gnu@4.28.1': - resolution: {integrity: sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-powerpc64le-gnu@4.28.1': - resolution: {integrity: sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.28.1': - resolution: {integrity: sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.28.1': - resolution: {integrity: sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.28.1': - resolution: {integrity: sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.28.1': - resolution: {integrity: sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-win32-arm64-msvc@4.28.1': - resolution: {integrity: sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.28.1': - resolution: {integrity: sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.28.1': - resolution: {integrity: sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==} - cpu: [x64] - os: [win32] - - '@tsconfig/node10@1.0.11': - resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} - - '@tsconfig/node12@1.0.11': - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} - - '@tsconfig/node14@1.0.3': - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} - - '@tsconfig/node16@1.0.4': - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - - '@types/bcrypt@5.0.2': - resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==} - - '@types/body-parser@1.19.5': - resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} - - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - - '@types/cors@2.8.17': - resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} - - '@types/estree@1.0.6': - resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - - '@types/express-serve-static-core@4.19.6': - resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} - - '@types/express-session@1.18.1': - resolution: {integrity: sha512-S6TkD/lljxDlQ2u/4A70luD8/ZxZcrU5pQwI1rVXCiaVIywoFgbA+PIUNDjPhQpPdK0dGleLtYc/y7XWBfclBg==} - - '@types/express@4.17.21': - resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} - - '@types/http-errors@2.0.4': - resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} - - '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - - '@types/node@22.10.1': - resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==} - - '@types/qs@6.9.17': - resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==} - - '@types/range-parser@1.2.7': - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - - '@types/send@0.17.4': - resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} - - '@types/serve-static@1.15.7': - resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} - - '@vitest/expect@2.1.8': - resolution: {integrity: sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==} - - '@vitest/mocker@2.1.8': - resolution: {integrity: sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@2.1.8': - resolution: {integrity: sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==} - - '@vitest/runner@2.1.8': - resolution: {integrity: sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==} - - '@vitest/snapshot@2.1.8': - resolution: {integrity: sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==} - - '@vitest/spy@2.1.8': - resolution: {integrity: sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==} - - '@vitest/utils@2.1.8': - resolution: {integrity: sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==} - - abbrev@1.1.1: - resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} - - acorn-walk@8.3.4: - resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} - engines: {node: '>=0.4.0'} - - acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} - engines: {node: '>=0.4.0'} - hasBin: true - - agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - aproba@2.0.0: - resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} - - are-we-there-yet@2.0.0: - resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} - engines: {node: '>=10'} - deprecated: This package is no longer supported. - - arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - - array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - - asap@2.0.6: - resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - - axios@1.7.9: - resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - base-64@1.0.0: - resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} - - bcrypt@5.1.1: - resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==} - engines: {node: '>= 10.0.0'} - - body-parser@1.20.3: - resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - call-bind-apply-helpers@1.0.1: - resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} - engines: {node: '>= 0.4'} - - call-bind@1.0.8: - resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} - engines: {node: '>= 0.4'} - - chai@5.1.2: - resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} - engines: {node: '>=12'} - - check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} - engines: {node: '>= 16'} - - chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} - - cluster-key-slot@1.1.2: - resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} - engines: {node: '>=0.10.0'} - - code-block-writer@12.0.0: - resolution: {integrity: sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==} - - color-support@1.1.3: - resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} - hasBin: true - - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - - component-emitter@1.3.1: - resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - connect-redis@7.1.1: - resolution: {integrity: sha512-M+z7alnCJiuzKa8/1qAYdGUXHYfDnLolOGAUjOioB07pP39qxjG+X9ibsud7qUBc4jMV5Mcy3ugGv8eFcgamJQ==} - engines: {node: '>=16'} - peerDependencies: - express-session: '>=1' - - console-control-strings@1.1.0: - resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} - - content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - - cookie-signature@1.0.6: - resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - - cookie-signature@1.0.7: - resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} - - cookie@0.7.1: - resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} - engines: {node: '>= 0.6'} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - - cookiejar@2.1.4: - resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} - - cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} - engines: {node: '>= 0.10'} - - create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - - dayjs@1.11.13: - resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} - - debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - - delegates@1.0.0: - resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} - - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - - destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - - detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} - engines: {node: '>=8'} - - dezalgo@1.0.4: - resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} - - diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} - engines: {node: '>=0.3.1'} - - dunder-proto@1.0.0: - resolution: {integrity: sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==} - engines: {node: '>= 0.4'} - - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} - - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-module-lexer@1.5.4: - resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} - - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - - esbuild@0.23.1: - resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} - engines: {node: '>=18'} - hasBin: true - - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - - expect-type@1.1.0: - resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} - engines: {node: '>=12.0.0'} - - express-session@1.18.1: - resolution: {integrity: sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==} - engines: {node: '>= 0.8.0'} - - express@4.21.2: - resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} - engines: {node: '>= 0.10.0'} - - fast-safe-stringify@2.1.1: - resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - - finalhandler@1.3.1: - resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} - engines: {node: '>= 0.8'} - - follow-redirects@1.15.9: - resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - - form-data@4.0.1: - resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} - engines: {node: '>= 6'} - - formidable@3.5.2: - resolution: {integrity: sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==} - - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} - - fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - gauge@3.0.2: - resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} - engines: {node: '>=10'} - deprecated: This package is no longer supported. - - generic-pool@3.9.0: - resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} - engines: {node: '>= 4'} - - get-intrinsic@1.2.5: - resolution: {integrity: sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg==} - engines: {node: '>= 0.4'} - - get-tsconfig@4.8.1: - resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-unicode@2.0.1: - resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - hexoid@2.0.0: - resolution: {integrity: sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==} - engines: {node: '>=8'} - - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} - - https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - - iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - - loupe@3.1.2: - resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} - - magic-string@0.30.15: - resolution: {integrity: sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==} - - mailgun.js@10.2.3: - resolution: {integrity: sha512-7Mcw5IFtzN21i+qFQoWI+aQFDpLYSMUIWvDUXKLlpGFVVGfYVL8GIiveS+LIXpEJTQcF1hoNhOhDwenFqNSKmw==} - engines: {node: '>=18.0.0'} - - make-dir@3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} - - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - - media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - - merge-descriptors@1.0.3: - resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} - - methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} - - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true - - mime@2.6.0: - resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} - engines: {node: '>=4.0.0'} - hasBin: true - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - - minipass@5.0.0: - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} - engines: {node: '>=8'} - - minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} - - mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true - - ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.8: - resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - - node-addon-api@5.1.0: - resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} - - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - - nopt@5.0.0: - resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} - engines: {node: '>=6'} - hasBin: true - - npmlog@5.0.1: - resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} - deprecated: This package is no longer supported. - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-inspect@1.13.3: - resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} - engines: {node: '>= 0.4'} - - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - - on-headers@1.0.2: - resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} - engines: {node: '>= 0.8'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - path-to-regexp@0.1.12: - resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} - - pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - - pathval@2.0.0: - resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} - engines: {node: '>= 14.16'} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - postcss@8.4.49: - resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} - engines: {node: ^10 || ^12 || >=14} - - prisma@5.22.0: - resolution: {integrity: sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==} - engines: {node: '>=16.13'} - hasBin: true - - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - - qs@6.13.0: - resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} - engines: {node: '>=0.6'} - - qs@6.13.1: - resolution: {integrity: sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==} - engines: {node: '>=0.6'} - - random-bytes@1.0.0: - resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} - engines: {node: '>= 0.8'} - - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@2.5.2: - resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} - engines: {node: '>= 0.8'} - - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - - redis@4.7.0: - resolution: {integrity: sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==} - - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - rollup@4.28.1: - resolution: {integrity: sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} - engines: {node: '>=10'} - hasBin: true - - send@0.19.0: - resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} - engines: {node: '>= 0.8.0'} - - serve-static@1.16.2: - resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} - engines: {node: '>= 0.8.0'} - - set-blocking@2.0.0: - resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - - set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} - - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - - side-channel@1.0.6: - resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} - engines: {node: '>= 0.4'} - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - - std-env@3.8.0: - resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - superagent@9.0.2: - resolution: {integrity: sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==} - engines: {node: '>=14.18.0'} - - supertest@7.0.0: - resolution: {integrity: sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==} - engines: {node: '>=14.18.0'} - - tar@6.2.1: - resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} - engines: {node: '>=10'} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@0.3.1: - resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} - - tinypool@1.0.2: - resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} - engines: {node: ^18.0.0 || >=20.0.0} - - tinyrainbow@1.2.0: - resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} - engines: {node: '>=14.0.0'} - - tinyspy@3.0.2: - resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} - engines: {node: '>=14.0.0'} - - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - - ts-essentials@10.0.3: - resolution: {integrity: sha512-/FrVAZ76JLTWxJOERk04fm8hYENDo0PWSP3YLQKxevLwWtxemGcl5JJEzN4iqfDlRve0ckyfFaOBu4xbNH/wZw==} - peerDependencies: - typescript: '>=4.5.0' - peerDependenciesMeta: - typescript: - optional: true - - ts-node@10.9.2: - resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} - hasBin: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true - - tsx@4.19.2: - resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} - engines: {node: '>=18.0.0'} - hasBin: true - - type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} - - typescript@5.7.2: - resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} - engines: {node: '>=14.17'} - hasBin: true - - uid-safe@2.1.5: - resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} - engines: {node: '>= 0.8'} - - undici-types@6.20.0: - resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} - - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - - url-join@4.0.1: - resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} - - v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - - vite-node@2.1.8: - resolution: {integrity: sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - - vite@5.4.11: - resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - - vitest-mock-extended@2.0.2: - resolution: {integrity: sha512-n3MBqVITKyclZ0n0y66hkT4UiiEYFQn9tteAnIxT0MPz1Z8nFcPUG3Cf0cZOyoPOj/cq6Ab1XFw2lM/qM5EDWQ==} - peerDependencies: - typescript: 3.x || 4.x || 5.x - vitest: '>=2.0.0' - - vitest@2.1.8: - resolution: {integrity: sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.1.8 - '@vitest/ui': 2.1.8 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - - wide-align@1.1.5: - resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - - yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} - - zod-prisma-types@3.2.1: - resolution: {integrity: sha512-qsOD8aMVx3Yg9gHctvhTRpavaJizt8xUda6qjwOcH7suvrirXax38tjs0ilKcY7GKbyY57q2rZ+uoSDnzVXpag==} - hasBin: true - peerDependencies: - '@prisma/client': ^4.x.x || ^5.x.x || ^6.x.x - prisma: ^4.x.x || ^5.x.x || ^6.x.x - - zod@3.23.8: - resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} - -snapshots: - - '@cspotcode/source-map-support@0.8.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - - '@esbuild/aix-ppc64@0.21.5': - optional: true - - '@esbuild/aix-ppc64@0.23.1': - optional: true - - '@esbuild/android-arm64@0.21.5': - optional: true - - '@esbuild/android-arm64@0.23.1': - optional: true - - '@esbuild/android-arm@0.21.5': - optional: true - - '@esbuild/android-arm@0.23.1': - optional: true - - '@esbuild/android-x64@0.21.5': - optional: true - - '@esbuild/android-x64@0.23.1': - optional: true - - '@esbuild/darwin-arm64@0.21.5': - optional: true - - '@esbuild/darwin-arm64@0.23.1': - optional: true - - '@esbuild/darwin-x64@0.21.5': - optional: true - - '@esbuild/darwin-x64@0.23.1': - optional: true - - '@esbuild/freebsd-arm64@0.21.5': - optional: true - - '@esbuild/freebsd-arm64@0.23.1': - optional: true - - '@esbuild/freebsd-x64@0.21.5': - optional: true - - '@esbuild/freebsd-x64@0.23.1': - optional: true - - '@esbuild/linux-arm64@0.21.5': - optional: true - - '@esbuild/linux-arm64@0.23.1': - optional: true - - '@esbuild/linux-arm@0.21.5': - optional: true - - '@esbuild/linux-arm@0.23.1': - optional: true - - '@esbuild/linux-ia32@0.21.5': - optional: true - - '@esbuild/linux-ia32@0.23.1': - optional: true - - '@esbuild/linux-loong64@0.21.5': - optional: true - - '@esbuild/linux-loong64@0.23.1': - optional: true - - '@esbuild/linux-mips64el@0.21.5': - optional: true - - '@esbuild/linux-mips64el@0.23.1': - optional: true - - '@esbuild/linux-ppc64@0.21.5': - optional: true - - '@esbuild/linux-ppc64@0.23.1': - optional: true - - '@esbuild/linux-riscv64@0.21.5': - optional: true - - '@esbuild/linux-riscv64@0.23.1': - optional: true - - '@esbuild/linux-s390x@0.21.5': - optional: true - - '@esbuild/linux-s390x@0.23.1': - optional: true - - '@esbuild/linux-x64@0.21.5': - optional: true - - '@esbuild/linux-x64@0.23.1': - optional: true - - '@esbuild/netbsd-x64@0.21.5': - optional: true - - '@esbuild/netbsd-x64@0.23.1': - optional: true - - '@esbuild/openbsd-arm64@0.23.1': - optional: true - - '@esbuild/openbsd-x64@0.21.5': - optional: true - - '@esbuild/openbsd-x64@0.23.1': - optional: true - - '@esbuild/sunos-x64@0.21.5': - optional: true - - '@esbuild/sunos-x64@0.23.1': - optional: true - - '@esbuild/win32-arm64@0.21.5': - optional: true - - '@esbuild/win32-arm64@0.23.1': - optional: true - - '@esbuild/win32-ia32@0.21.5': - optional: true - - '@esbuild/win32-ia32@0.23.1': - optional: true - - '@esbuild/win32-x64@0.21.5': - optional: true - - '@esbuild/win32-x64@0.23.1': - optional: true - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.0': {} - - '@jridgewell/trace-mapping@0.3.9': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 - - '@mapbox/node-pre-gyp@1.0.11': - dependencies: - detect-libc: 2.0.3 - https-proxy-agent: 5.0.1 - make-dir: 3.1.0 - node-fetch: 2.7.0 - nopt: 5.0.0 - npmlog: 5.0.1 - rimraf: 3.0.2 - semver: 7.6.3 - tar: 6.2.1 - transitivePeerDependencies: - - encoding - - supports-color - - '@prisma/client@5.22.0(prisma@5.22.0)': - optionalDependencies: - prisma: 5.22.0 - - '@prisma/debug@5.22.0': {} - - '@prisma/debug@6.0.1': {} - - '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': {} - - '@prisma/engines@5.22.0': - dependencies: - '@prisma/debug': 5.22.0 - '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 - '@prisma/fetch-engine': 5.22.0 - '@prisma/get-platform': 5.22.0 - - '@prisma/fetch-engine@5.22.0': - dependencies: - '@prisma/debug': 5.22.0 - '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 - '@prisma/get-platform': 5.22.0 - - '@prisma/generator-helper@6.0.1': - dependencies: - '@prisma/debug': 6.0.1 - - '@prisma/get-platform@5.22.0': - dependencies: - '@prisma/debug': 5.22.0 - - '@redis/bloom@1.2.0(@redis/client@1.6.0)': - dependencies: - '@redis/client': 1.6.0 - - '@redis/client@1.6.0': - dependencies: - cluster-key-slot: 1.1.2 - generic-pool: 3.9.0 - yallist: 4.0.0 - - '@redis/graph@1.1.1(@redis/client@1.6.0)': - dependencies: - '@redis/client': 1.6.0 - - '@redis/json@1.0.7(@redis/client@1.6.0)': - dependencies: - '@redis/client': 1.6.0 - - '@redis/search@1.2.0(@redis/client@1.6.0)': - dependencies: - '@redis/client': 1.6.0 - - '@redis/time-series@1.1.0(@redis/client@1.6.0)': - dependencies: - '@redis/client': 1.6.0 - - '@rollup/rollup-android-arm-eabi@4.28.1': - optional: true - - '@rollup/rollup-android-arm64@4.28.1': - optional: true - - '@rollup/rollup-darwin-arm64@4.28.1': - optional: true - - '@rollup/rollup-darwin-x64@4.28.1': - optional: true - - '@rollup/rollup-freebsd-arm64@4.28.1': - optional: true - - '@rollup/rollup-freebsd-x64@4.28.1': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.28.1': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.28.1': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.28.1': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.28.1': - optional: true - - '@rollup/rollup-linux-loongarch64-gnu@4.28.1': - optional: true - - '@rollup/rollup-linux-powerpc64le-gnu@4.28.1': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.28.1': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.28.1': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.28.1': - optional: true - - '@rollup/rollup-linux-x64-musl@4.28.1': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.28.1': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.28.1': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.28.1': - optional: true - - '@tsconfig/node10@1.0.11': {} - - '@tsconfig/node12@1.0.11': {} - - '@tsconfig/node14@1.0.3': {} - - '@tsconfig/node16@1.0.4': {} - - '@types/bcrypt@5.0.2': - dependencies: - '@types/node': 22.10.1 - - '@types/body-parser@1.19.5': - dependencies: - '@types/connect': 3.4.38 - '@types/node': 22.10.1 - - '@types/connect@3.4.38': - dependencies: - '@types/node': 22.10.1 - - '@types/cors@2.8.17': - dependencies: - '@types/node': 22.10.1 - - '@types/estree@1.0.6': {} - - '@types/express-serve-static-core@4.19.6': - dependencies: - '@types/node': 22.10.1 - '@types/qs': 6.9.17 - '@types/range-parser': 1.2.7 - '@types/send': 0.17.4 - - '@types/express-session@1.18.1': - dependencies: - '@types/express': 4.17.21 - - '@types/express@4.17.21': - dependencies: - '@types/body-parser': 1.19.5 - '@types/express-serve-static-core': 4.19.6 - '@types/qs': 6.9.17 - '@types/serve-static': 1.15.7 - - '@types/http-errors@2.0.4': {} - - '@types/mime@1.3.5': {} - - '@types/node@22.10.1': - dependencies: - undici-types: 6.20.0 - - '@types/qs@6.9.17': {} - - '@types/range-parser@1.2.7': {} - - '@types/send@0.17.4': - dependencies: - '@types/mime': 1.3.5 - '@types/node': 22.10.1 - - '@types/serve-static@1.15.7': - dependencies: - '@types/http-errors': 2.0.4 - '@types/node': 22.10.1 - '@types/send': 0.17.4 - - '@vitest/expect@2.1.8': - dependencies: - '@vitest/spy': 2.1.8 - '@vitest/utils': 2.1.8 - chai: 5.1.2 - tinyrainbow: 1.2.0 - - '@vitest/mocker@2.1.8(vite@5.4.11(@types/node@22.10.1))': - dependencies: - '@vitest/spy': 2.1.8 - estree-walker: 3.0.3 - magic-string: 0.30.15 - optionalDependencies: - vite: 5.4.11(@types/node@22.10.1) - - '@vitest/pretty-format@2.1.8': - dependencies: - tinyrainbow: 1.2.0 - - '@vitest/runner@2.1.8': - dependencies: - '@vitest/utils': 2.1.8 - pathe: 1.1.2 - - '@vitest/snapshot@2.1.8': - dependencies: - '@vitest/pretty-format': 2.1.8 - magic-string: 0.30.15 - pathe: 1.1.2 - - '@vitest/spy@2.1.8': - dependencies: - tinyspy: 3.0.2 - - '@vitest/utils@2.1.8': - dependencies: - '@vitest/pretty-format': 2.1.8 - loupe: 3.1.2 - tinyrainbow: 1.2.0 - - abbrev@1.1.1: {} - - accepts@1.3.8: - dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - - acorn-walk@8.3.4: - dependencies: - acorn: 8.14.0 - - acorn@8.14.0: {} - - agent-base@6.0.2: - dependencies: - debug: 4.4.0 - transitivePeerDependencies: - - supports-color - - ansi-regex@5.0.1: {} - - aproba@2.0.0: {} - - are-we-there-yet@2.0.0: - dependencies: - delegates: 1.0.0 - readable-stream: 3.6.2 - - arg@4.1.3: {} - - array-flatten@1.1.1: {} - - asap@2.0.6: {} - - assertion-error@2.0.1: {} - - asynckit@0.4.0: {} - - axios@1.7.9: - dependencies: - follow-redirects: 1.15.9 - form-data: 4.0.1 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - - balanced-match@1.0.2: {} - - base-64@1.0.0: {} - - bcrypt@5.1.1: - dependencies: - '@mapbox/node-pre-gyp': 1.0.11 - node-addon-api: 5.1.0 - transitivePeerDependencies: - - encoding - - supports-color - - body-parser@1.20.3: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.13.0 - raw-body: 2.5.2 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - - brace-expansion@1.1.11: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - bytes@3.1.2: {} - - cac@6.7.14: {} - - call-bind-apply-helpers@1.0.1: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bind@1.0.8: - dependencies: - call-bind-apply-helpers: 1.0.1 - es-define-property: 1.0.1 - get-intrinsic: 1.2.5 - set-function-length: 1.2.2 - - chai@5.1.2: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.1 - deep-eql: 5.0.2 - loupe: 3.1.2 - pathval: 2.0.0 - - check-error@2.1.1: {} - - chownr@2.0.0: {} - - cluster-key-slot@1.1.2: {} - - code-block-writer@12.0.0: {} - - color-support@1.1.3: {} - - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - - component-emitter@1.3.1: {} - - concat-map@0.0.1: {} - - connect-redis@7.1.1(express-session@1.18.1): - dependencies: - express-session: 1.18.1 - - console-control-strings@1.1.0: {} - - content-disposition@0.5.4: - dependencies: - safe-buffer: 5.2.1 - - content-type@1.0.5: {} - - cookie-signature@1.0.6: {} - - cookie-signature@1.0.7: {} - - cookie@0.7.1: {} - - cookie@0.7.2: {} - - cookiejar@2.1.4: {} - - cors@2.8.5: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - - create-require@1.1.1: {} - - dayjs@1.11.13: {} - - debug@2.6.9: - dependencies: - ms: 2.0.0 - - debug@4.4.0: - dependencies: - ms: 2.1.3 - - deep-eql@5.0.2: {} - - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - - delayed-stream@1.0.0: {} - - delegates@1.0.0: {} - - depd@2.0.0: {} - - destroy@1.2.0: {} - - detect-libc@2.0.3: {} - - dezalgo@1.0.4: - dependencies: - asap: 2.0.6 - wrappy: 1.0.2 - - diff@4.0.2: {} - - dunder-proto@1.0.0: - dependencies: - call-bind-apply-helpers: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - - ee-first@1.1.1: {} - - emoji-regex@8.0.0: {} - - encodeurl@1.0.2: {} - - encodeurl@2.0.0: {} - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-module-lexer@1.5.4: {} - - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - - esbuild@0.23.1: - optionalDependencies: - '@esbuild/aix-ppc64': 0.23.1 - '@esbuild/android-arm': 0.23.1 - '@esbuild/android-arm64': 0.23.1 - '@esbuild/android-x64': 0.23.1 - '@esbuild/darwin-arm64': 0.23.1 - '@esbuild/darwin-x64': 0.23.1 - '@esbuild/freebsd-arm64': 0.23.1 - '@esbuild/freebsd-x64': 0.23.1 - '@esbuild/linux-arm': 0.23.1 - '@esbuild/linux-arm64': 0.23.1 - '@esbuild/linux-ia32': 0.23.1 - '@esbuild/linux-loong64': 0.23.1 - '@esbuild/linux-mips64el': 0.23.1 - '@esbuild/linux-ppc64': 0.23.1 - '@esbuild/linux-riscv64': 0.23.1 - '@esbuild/linux-s390x': 0.23.1 - '@esbuild/linux-x64': 0.23.1 - '@esbuild/netbsd-x64': 0.23.1 - '@esbuild/openbsd-arm64': 0.23.1 - '@esbuild/openbsd-x64': 0.23.1 - '@esbuild/sunos-x64': 0.23.1 - '@esbuild/win32-arm64': 0.23.1 - '@esbuild/win32-ia32': 0.23.1 - '@esbuild/win32-x64': 0.23.1 - - escape-html@1.0.3: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.6 - - etag@1.8.1: {} - - expect-type@1.1.0: {} - - express-session@1.18.1: - dependencies: - cookie: 0.7.2 - cookie-signature: 1.0.7 - debug: 2.6.9 - depd: 2.0.0 - on-headers: 1.0.2 - parseurl: 1.3.3 - safe-buffer: 5.2.1 - uid-safe: 2.1.5 - transitivePeerDependencies: - - supports-color - - express@4.21.2: - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.3 - content-disposition: 0.5.4 - content-type: 1.0.5 - cookie: 0.7.1 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.3.1 - fresh: 0.5.2 - http-errors: 2.0.0 - merge-descriptors: 1.0.3 - methods: 1.1.2 - on-finished: 2.4.1 - parseurl: 1.3.3 - path-to-regexp: 0.1.12 - proxy-addr: 2.0.7 - qs: 6.13.0 - range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.19.0 - serve-static: 1.16.2 - setprototypeof: 1.2.0 - statuses: 2.0.1 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - - fast-safe-stringify@2.1.1: {} - - finalhandler@1.3.1: - dependencies: - debug: 2.6.9 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.1 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - - follow-redirects@1.15.9: {} - - form-data@4.0.1: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - - formidable@3.5.2: - dependencies: - dezalgo: 1.0.4 - hexoid: 2.0.0 - once: 1.4.0 - - forwarded@0.2.0: {} - - fresh@0.5.2: {} - - fs-minipass@2.1.0: - dependencies: - minipass: 3.3.6 - - fs.realpath@1.0.0: {} - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - gauge@3.0.2: - dependencies: - aproba: 2.0.0 - color-support: 1.1.3 - console-control-strings: 1.1.0 - has-unicode: 2.0.1 - object-assign: 4.1.1 - signal-exit: 3.0.7 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wide-align: 1.1.5 - - generic-pool@3.9.0: {} - - get-intrinsic@1.2.5: - dependencies: - call-bind-apply-helpers: 1.0.1 - dunder-proto: 1.0.0 - es-define-property: 1.0.1 - es-errors: 1.3.0 - function-bind: 1.1.2 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - - get-tsconfig@4.8.1: - dependencies: - resolve-pkg-maps: 1.0.0 - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - - gopd@1.2.0: {} - - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - - has-symbols@1.1.0: {} - - has-unicode@2.0.1: {} - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - hexoid@2.0.0: {} - - http-errors@2.0.0: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - - https-proxy-agent@5.0.1: - dependencies: - agent-base: 6.0.2 - debug: 4.4.0 - transitivePeerDependencies: - - supports-color - - iconv-lite@0.4.24: - dependencies: - safer-buffer: 2.1.2 - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - - ipaddr.js@1.9.1: {} - - is-fullwidth-code-point@3.0.0: {} - - lodash@4.17.21: {} - - loupe@3.1.2: {} - - magic-string@0.30.15: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - - mailgun.js@10.2.3: - dependencies: - axios: 1.7.9 - base-64: 1.0.0 - url-join: 4.0.1 - transitivePeerDependencies: - - debug - - make-dir@3.1.0: - dependencies: - semver: 6.3.1 - - make-error@1.3.6: {} - - media-typer@0.3.0: {} - - merge-descriptors@1.0.3: {} - - methods@1.1.2: {} - - mime-db@1.52.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - - mime@1.6.0: {} - - mime@2.6.0: {} - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.11 - - minipass@3.3.6: - dependencies: - yallist: 4.0.0 - - minipass@5.0.0: {} - - minizlib@2.1.2: - dependencies: - minipass: 3.3.6 - yallist: 4.0.0 - - mkdirp@1.0.4: {} - - ms@2.0.0: {} - - ms@2.1.3: {} - - nanoid@3.3.8: {} - - negotiator@0.6.3: {} - - node-addon-api@5.1.0: {} - - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - - nopt@5.0.0: - dependencies: - abbrev: 1.1.1 - - npmlog@5.0.1: - dependencies: - are-we-there-yet: 2.0.0 - console-control-strings: 1.1.0 - gauge: 3.0.2 - set-blocking: 2.0.0 - - object-assign@4.1.1: {} - - object-inspect@1.13.3: {} - - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - - on-headers@1.0.2: {} - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - parseurl@1.3.3: {} - - path-is-absolute@1.0.1: {} - - path-to-regexp@0.1.12: {} - - pathe@1.1.2: {} - - pathval@2.0.0: {} - - picocolors@1.1.1: {} - - postcss@8.4.49: - dependencies: - nanoid: 3.3.8 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prisma@5.22.0: - dependencies: - '@prisma/engines': 5.22.0 - optionalDependencies: - fsevents: 2.3.3 - - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - - proxy-from-env@1.1.0: {} - - qs@6.13.0: - dependencies: - side-channel: 1.0.6 - - qs@6.13.1: - dependencies: - side-channel: 1.0.6 - - random-bytes@1.0.0: {} - - range-parser@1.2.1: {} - - raw-body@2.5.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - - redis@4.7.0: - dependencies: - '@redis/bloom': 1.2.0(@redis/client@1.6.0) - '@redis/client': 1.6.0 - '@redis/graph': 1.1.1(@redis/client@1.6.0) - '@redis/json': 1.0.7(@redis/client@1.6.0) - '@redis/search': 1.2.0(@redis/client@1.6.0) - '@redis/time-series': 1.1.0(@redis/client@1.6.0) - - resolve-pkg-maps@1.0.0: {} - - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - - rollup@4.28.1: - dependencies: - '@types/estree': 1.0.6 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.28.1 - '@rollup/rollup-android-arm64': 4.28.1 - '@rollup/rollup-darwin-arm64': 4.28.1 - '@rollup/rollup-darwin-x64': 4.28.1 - '@rollup/rollup-freebsd-arm64': 4.28.1 - '@rollup/rollup-freebsd-x64': 4.28.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.28.1 - '@rollup/rollup-linux-arm-musleabihf': 4.28.1 - '@rollup/rollup-linux-arm64-gnu': 4.28.1 - '@rollup/rollup-linux-arm64-musl': 4.28.1 - '@rollup/rollup-linux-loongarch64-gnu': 4.28.1 - '@rollup/rollup-linux-powerpc64le-gnu': 4.28.1 - '@rollup/rollup-linux-riscv64-gnu': 4.28.1 - '@rollup/rollup-linux-s390x-gnu': 4.28.1 - '@rollup/rollup-linux-x64-gnu': 4.28.1 - '@rollup/rollup-linux-x64-musl': 4.28.1 - '@rollup/rollup-win32-arm64-msvc': 4.28.1 - '@rollup/rollup-win32-ia32-msvc': 4.28.1 - '@rollup/rollup-win32-x64-msvc': 4.28.1 - fsevents: 2.3.3 - - safe-buffer@5.2.1: {} - - safer-buffer@2.1.2: {} - - semver@6.3.1: {} - - semver@7.6.3: {} - - send@0.19.0: - dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.0 - mime: 1.6.0 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - - serve-static@1.16.2: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 0.19.0 - transitivePeerDependencies: - - supports-color - - set-blocking@2.0.0: {} - - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.2.5 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - - setprototypeof@1.2.0: {} - - side-channel@1.0.6: - dependencies: - call-bind: 1.0.8 - es-errors: 1.3.0 - get-intrinsic: 1.2.5 - object-inspect: 1.13.3 - - siginfo@2.0.0: {} - - signal-exit@3.0.7: {} - - source-map-js@1.2.1: {} - - stackback@0.0.2: {} - - statuses@2.0.1: {} - - std-env@3.8.0: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - superagent@9.0.2: - dependencies: - component-emitter: 1.3.1 - cookiejar: 2.1.4 - debug: 4.4.0 - fast-safe-stringify: 2.1.1 - form-data: 4.0.1 - formidable: 3.5.2 - methods: 1.1.2 - mime: 2.6.0 - qs: 6.13.1 - transitivePeerDependencies: - - supports-color - - supertest@7.0.0: - dependencies: - methods: 1.1.2 - superagent: 9.0.2 - transitivePeerDependencies: - - supports-color - - tar@6.2.1: - dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 5.0.0 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 - - tinybench@2.9.0: {} - - tinyexec@0.3.1: {} - - tinypool@1.0.2: {} - - tinyrainbow@1.2.0: {} - - tinyspy@3.0.2: {} - - toidentifier@1.0.1: {} - - tr46@0.0.3: {} - - ts-essentials@10.0.3(typescript@5.7.2): - optionalDependencies: - typescript: 5.7.2 - - ts-node@10.9.2(@types/node@22.10.1)(typescript@5.7.2): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 22.10.1 - acorn: 8.14.0 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.7.2 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - - tsx@4.19.2: - dependencies: - esbuild: 0.23.1 - get-tsconfig: 4.8.1 - optionalDependencies: - fsevents: 2.3.3 - - type-is@1.6.18: - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - - typescript@5.7.2: {} - - uid-safe@2.1.5: - dependencies: - random-bytes: 1.0.0 - - undici-types@6.20.0: {} - - unpipe@1.0.0: {} - - url-join@4.0.1: {} - - util-deprecate@1.0.2: {} - - utils-merge@1.0.1: {} - - v8-compile-cache-lib@3.0.1: {} - - vary@1.1.2: {} - - vite-node@2.1.8(@types/node@22.10.1): - dependencies: - cac: 6.7.14 - debug: 4.4.0 - es-module-lexer: 1.5.4 - pathe: 1.1.2 - vite: 5.4.11(@types/node@22.10.1) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - vite@5.4.11(@types/node@22.10.1): - dependencies: - esbuild: 0.21.5 - postcss: 8.4.49 - rollup: 4.28.1 - optionalDependencies: - '@types/node': 22.10.1 - fsevents: 2.3.3 - - vitest-mock-extended@2.0.2(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.1)): - dependencies: - ts-essentials: 10.0.3(typescript@5.7.2) - typescript: 5.7.2 - vitest: 2.1.8(@types/node@22.10.1) - - vitest@2.1.8(@types/node@22.10.1): - dependencies: - '@vitest/expect': 2.1.8 - '@vitest/mocker': 2.1.8(vite@5.4.11(@types/node@22.10.1)) - '@vitest/pretty-format': 2.1.8 - '@vitest/runner': 2.1.8 - '@vitest/snapshot': 2.1.8 - '@vitest/spy': 2.1.8 - '@vitest/utils': 2.1.8 - chai: 5.1.2 - debug: 4.4.0 - expect-type: 1.1.0 - magic-string: 0.30.15 - pathe: 1.1.2 - std-env: 3.8.0 - tinybench: 2.9.0 - tinyexec: 0.3.1 - tinypool: 1.0.2 - tinyrainbow: 1.2.0 - vite: 5.4.11(@types/node@22.10.1) - vite-node: 2.1.8(@types/node@22.10.1) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 22.10.1 - transitivePeerDependencies: - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - webidl-conversions@3.0.1: {} - - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - wide-align@1.1.5: - dependencies: - string-width: 4.2.3 - - wrappy@1.0.2: {} - - yallist@4.0.0: {} - - yn@3.1.1: {} - - zod-prisma-types@3.2.1(@prisma/client@5.22.0(prisma@5.22.0))(prisma@5.22.0): - dependencies: - '@prisma/client': 5.22.0(prisma@5.22.0) - '@prisma/generator-helper': 6.0.1 - code-block-writer: 12.0.0 - lodash: 4.17.21 - prisma: 5.22.0 - zod: 3.23.8 - - zod@3.23.8: {} diff --git a/backend/prisma/migrations/20241213074516_init/migration.sql b/backend/prisma/migrations/20241213074516_init/migration.sql new file mode 100644 index 0000000..ddecac5 --- /dev/null +++ b/backend/prisma/migrations/20241213074516_init/migration.sql @@ -0,0 +1,2 @@ +-- DropIndex +DROP INDEX "Keyword_text_key"; diff --git a/backend/prisma/migrations/20241213143101_20241213074516/migration.sql b/backend/prisma/migrations/20241213143101_20241213074516/migration.sql new file mode 100644 index 0000000..6bd9da6 --- /dev/null +++ b/backend/prisma/migrations/20241213143101_20241213074516/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[text]` on the table `Keyword` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "Keyword_text_key" ON "Keyword"("text"); diff --git a/backend/scripts/run-integration.sh b/backend/scripts/run-integration.sh index b7385fe..19c540a 100755 --- a/backend/scripts/run-integration.sh +++ b/backend/scripts/run-integration.sh @@ -1,17 +1,19 @@ #This script only works for integration testing. DIR="$(cd "$(dirname "$0")" && pwd)" export $(grep -v '^#' .env.test | xargs) -docker-compose up -d -docker rm -f redis-stack-test 2>/dev/null || true && docker run -d --name redis-stack-test -p 6380:6379 -p 8002:8001 redis/redis-stack:latest -echo '🟡 - Waiting for database to be ready...' +docker rm -f postgres-test-pyramids 2>/dev/null && docker run --name postgres-test-pyramids -e POSTGRES_PASSWORD=postgres -d -p 5432:5432 postgres +docker rm -f redis-stack-test-pyramids 2>/dev/null || true && docker run -d --name redis-stack-test-pyramids -p 6380:6379 -p 8002:8001 redis/redis-stack:latest + echo "node_env is ${NODE_ENV}" echo "Using database: ${DATABASE_URL}" +echo "And direct URL: ${DIRECT_URL}" echo "redis port is ${REDIS_PORT}" -$DIR/wait-for-it.sh "${DATABASE_URL}" -- echo '🟢 - Database is ready!' -npx prisma migrate dev --name init + +pnpm prisma migrate deploy + if [ "$#" -eq "0" ] then vitest -c ./vitest.config.integration.ts else vitest -c ./vitest.config.integration.ts --ui -fi \ No newline at end of file +fi diff --git a/backend/scripts/wait-for-it.sh b/backend/scripts/wait-for-it.sh deleted file mode 100755 index d990e0d..0000000 --- a/backend/scripts/wait-for-it.sh +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env bash -# Use this script to test if a given TCP host/port are available - -WAITFORIT_cmdname=${0##*/} - -echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } - -usage() -{ - cat << USAGE >&2 -Usage: - $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] - -h HOST | --host=HOST Host or IP under test - -p PORT | --port=PORT TCP port under test - Alternatively, you specify the host and port as host:port - -s | --strict Only execute subcommand if the test succeeds - -q | --quiet Don't output any status messages - -t TIMEOUT | --timeout=TIMEOUT - Timeout in seconds, zero for no timeout - -- COMMAND ARGS Execute command with args after the test finishes -USAGE - exit 1 -} - -wait_for() -{ - if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then - echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" - else - echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" - fi - WAITFORIT_start_ts=$(date +%s) - while : - do - if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then - nc -z $WAITFORIT_HOST $WAITFORIT_PORT - WAITFORIT_result=$? - else - (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 - WAITFORIT_result=$? - fi - if [[ $WAITFORIT_result -eq 0 ]]; then - WAITFORIT_end_ts=$(date +%s) - echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" - break - fi - sleep 1 - done - return $WAITFORIT_result -} - -wait_for_wrapper() -{ - # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 - if [[ $WAITFORIT_QUIET -eq 1 ]]; then - timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & - else - timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & - fi - WAITFORIT_PID=$! - trap "kill -INT -$WAITFORIT_PID" INT - wait $WAITFORIT_PID - WAITFORIT_RESULT=$? - if [[ $WAITFORIT_RESULT -ne 0 ]]; then - echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" - fi - return $WAITFORIT_RESULT -} - -# process arguments -while [[ $# -gt 0 ]] -do - case "$1" in - *:* ) - WAITFORIT_hostport=(${1//:/ }) - WAITFORIT_HOST=${WAITFORIT_hostport[0]} - WAITFORIT_PORT=${WAITFORIT_hostport[1]} - shift 1 - ;; - --child) - WAITFORIT_CHILD=1 - shift 1 - ;; - -q | --quiet) - WAITFORIT_QUIET=1 - shift 1 - ;; - -s | --strict) - WAITFORIT_STRICT=1 - shift 1 - ;; - -h) - WAITFORIT_HOST="$2" - if [[ $WAITFORIT_HOST == "" ]]; then break; fi - shift 2 - ;; - --host=*) - WAITFORIT_HOST="${1#*=}" - shift 1 - ;; - -p) - WAITFORIT_PORT="$2" - if [[ $WAITFORIT_PORT == "" ]]; then break; fi - shift 2 - ;; - --port=*) - WAITFORIT_PORT="${1#*=}" - shift 1 - ;; - -t) - WAITFORIT_TIMEOUT="$2" - if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi - shift 2 - ;; - --timeout=*) - WAITFORIT_TIMEOUT="${1#*=}" - shift 1 - ;; - --) - shift - WAITFORIT_CLI=("$@") - break - ;; - --help) - usage - ;; - *) - echoerr "Unknown argument: $1" - usage - ;; - esac -done - -if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then - echoerr "Error: you need to provide a host and port to test." - usage -fi - -WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} -WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} -WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} -WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} - -# Check to see if timeout is from busybox? -WAITFORIT_TIMEOUT_PATH=$(type -p timeout) -WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) - -WAITFORIT_BUSYTIMEFLAG="" -if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then - WAITFORIT_ISBUSY=1 - # Check if busybox timeout uses -t flag - # (recent Alpine versions don't support -t anymore) - if timeout &>/dev/stdout | grep -q -e '-t '; then - WAITFORIT_BUSYTIMEFLAG="-t" - fi -else - WAITFORIT_ISBUSY=0 -fi - -if [[ $WAITFORIT_CHILD -gt 0 ]]; then - wait_for - WAITFORIT_RESULT=$? - exit $WAITFORIT_RESULT -else - if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then - wait_for_wrapper - WAITFORIT_RESULT=$? - else - wait_for - WAITFORIT_RESULT=$? - fi -fi - -if [[ $WAITFORIT_CLI != "" ]]; then - if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then - echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" - exit $WAITFORIT_RESULT - fi - exec "${WAITFORIT_CLI[@]}" -else - exit $WAITFORIT_RESULT -fi diff --git a/backend/src/index.ts b/backend/src/index.ts index 1400d8d..ffb2620 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,6 +1,6 @@ -import express, { Request, Response } from "express"; -import session, { Session } from "express-session"; -import cors from "cors"; +import express, { Request, Response } from 'express'; +import session, { Session } from 'express-session'; +import cors from 'cors'; import { DiscordLoginBody, LoginBody, @@ -11,36 +11,52 @@ import { eventIdBody, RegisterBody, UpdateEventBody, -} from "./requestTypes"; -import bcrypt from "bcrypt"; -import { LoginErrors, SanitisedUser } from "./interfaces"; -import { PrismaClient, Prisma, User } from "@prisma/client"; -import prisma from "./prisma"; -import RedisStore from "connect-redis"; -import { createClient } from "redis"; -import dayjs, { Dayjs } from "dayjs"; -import { generateOTP } from "./routes/OTP/generateOTP"; -import assert from "assert"; -import { verifyOTP } from "./routes/OTP/verifyOTP"; + CreateKeywordBody, +} from './requestTypes'; +import bcrypt from 'bcrypt'; +import { LoginErrors, SanitisedUser } from './interfaces'; +import { PrismaClient, Prisma, User } from '@prisma/client'; +import prisma from './prisma'; +import RedisStore from 'connect-redis'; +import { createClient } from 'redis'; +import dayjs, { Dayjs } from 'dayjs'; +import { generateOTP } from './routes/OTP/generateOTP'; +import assert from 'assert'; +import { verifyOTP } from './routes/OTP/verifyOTP'; import { findUserFromId, updateUserPasswordFromEmail, -} from "./routes/User/user"; +} from './routes/User/user'; -declare module "express-session" { +declare module 'express-session' { interface SessionData { userId: number; } } // Initialize client. -if (process.env["REDIS_PORT"] === undefined) { +if ( + process.env['REDIS_PORT'] === undefined || + process.env['REDIS_PORT'] === '' +) { console.log(process.env); - console.error("Redis port not provided in .env file"); + console.error('Redis port not provided in .env file'); process.exit(1); } + +let allowed_origins; +if ( + process.env['ALLOWED_ORIGINS'] === undefined || + process.env['ALLOWED_ORIGINS'] === '' +) { + console.log('Warning: ALLOWED_ORIGINS not specified. Using wildcard *.'); + allowed_origins = ['*']; +} else { + allowed_origins = process.env['ALLOWED_ORIGINS']?.split(','); +} + let redisClient = createClient({ - url: `redis://localhost:${process.env["REDIS_PORT"]}`, + url: `redis://localhost:${process.env['REDIS_PORT']}`, }); redisClient.connect().catch(console.error); @@ -48,26 +64,40 @@ redisClient.connect().catch(console.error); // Initialize store. let redisStore = new RedisStore({ client: redisClient, - prefix: "session:", + prefix: 'session:', }); const app = express(); const SERVER_PORT = 5180; const SALT_ROUNDS = 10; -app.use(cors()); +app.use( + cors({ + origin: allowed_origins, + credentials: true, + }) +); app.use(express.json()); -if (process.env["SESSION_SECRET"] === undefined) { - console.error("Session secret not provided in .env file"); +if (process.env['SESSION_SECRET'] === undefined) { + console.error('Session secret not provided in .env file'); + process.exit(1); +} + +if ( + process.env['DATABASE_URL'] === undefined || + process.env['DIRECT_URL'] === undefined +) { + console.error('Database URL or Direct URL not provided in .env file.'); process.exit(1); } if ( - process.env["DATABASE_URL"] === undefined || - process.env["DIRECT_URL"] === undefined + (process.env['NODE_ENV'] !== 'test' && + process.env['EMAIL_KEY'] === undefined) || + process.env['EMAIL_KEY'] === '' ) { - console.error("Database URL or Direct URL not provided in .env file."); + console.log('`EMAIL_KEY` not provided in .env file.'); process.exit(1); } @@ -77,22 +107,22 @@ app.use( store: redisStore, resave: false, // required: force lightweight session keep alive (touch) saveUninitialized: false, // recommended: only save session when data exists - secret: process.env["SESSION_SECRET"], + secret: process.env['SESSION_SECRET'], }) ); -app.get("/", (req: Request, res: Response) => { - console.log("Hello, TypeScript with Express :)))!"); - res.send("Hello, TypeScript with Express :)))!"); +app.get('/', (req: Request, res: Response) => { + console.log('Hello, TypeScript with Express :)))!'); + res.send('Hello, TypeScript with Express :)))!'); }); app.post( - "/auth/register", + '/auth/register', async (req: TypedRequest, res: Response) => { const { username, email, password } = req.body; if (!username || !email || !password) { - return res.status(400).json({ error: "Missing required fields" }); + return res.status(400).json({ error: 'Missing required fields' }); } const results = await prisma.user.findFirst({ @@ -104,7 +134,7 @@ app.post( if (results) { return res .status(400) - .json({ error: "Account with same credentials already exists" }); + .json({ error: 'Account with same credentials already exists' }); } const saltRounds: number = SALT_ROUNDS; @@ -133,96 +163,99 @@ app.post( } ); -app.post("/auth/otp/generate", async (req: Request, res: Response) => { - try { - const { email }: { email: string } = req.body; +app.post('/auth/otp/generate', async (req: Request, res: Response) => { + const { email }: { email: string } = req.body; - if (!email) { - throw new Error("Email address expected."); - } + if (!email) { + return res.status(400).json({ message: 'Missing required fields' }); + } - const token = await generateOTP(email); + let token; + try { + token = await generateOTP(email); + } catch (e) { + return res.status(500).json({ message: (e as Error).message }); + } - if (token) { - const expiryTime = process.env["OTP_EXPIRES"]; + if (token) { + const expiryTime = process.env['OTP_EXPIRES']; - try { - await redisClient.set(email, token, { - EX: parseInt(expiryTime as string), - }); - } catch { - console.error("OTP expiration time not set in environment variable."); - } + try { + await redisClient.set(email, token, { + EX: parseInt(expiryTime as string), + }); + } catch { + return res.status(500).json({ + message: 'OTP expiration time not set in environment variable.', + }); + } - if (process.env["CI"]) { - return res.status(200).json({ message: token }); - } - return res.status(200).json({ message: "ok" }); - } else { - return res - .status(400) - .json({ message: "Unexpected error while generating OTP." }); + if (process.env['CI']) { + return res.status(200).json({ message: token }); } - } catch (error) { - return res.status(400).json({ message: (error as Error).message }); + return res.status(200).json({ message: 'ok' }); + } else { + return res + .status(400) + .json({ message: 'Unexpected error while generating OTP.' }); } }); -app.post("/auth/otp/verify", async (req: Request, res: Response) => { +app.post('/auth/otp/verify', async (req: Request, res: Response) => { try { const { email, token } = req.body; if (!email) { - throw new Error("Email address expected."); + throw new Error('Email address expected.'); } if (!token) { - throw new Error("One time code expected."); + throw new Error('One time code expected.'); } const otp = await redisClient.get(email); verifyOTP(token, otp); - const expiryTime = process.env["OTP_EXPIRES"]; + const expiryTime = process.env['OTP_EXPIRES']; try { await redisClient.set(email, token, { EX: parseInt(expiryTime as string), }); } catch { - console.error("OTP expiration time not set in environment variable."); + console.error('OTP expiration time not set in environment variable.'); } - return res.status(200).json({ message: "ok" }); + return res.status(200).json({ message: 'ok' }); } catch (error) { return res.status(400).json({ message: (error as Error).message }); } }); -app.post("/auth/password/forgot", async (req: Request, res: Response) => { +app.post('/auth/password/forgot', async (req: Request, res: Response) => { try { - const { email, token, newPassword } = req.body; + const { email, token, password } = req.body; if (!email) { - throw new Error("Email is expected."); + throw new Error('Email is expected.'); } if (!token) { - throw new Error("One time code required to reset password."); + throw new Error('One time code required to reset password.'); } - if (!newPassword) { - throw new Error("New password is invalid."); + if (!password) { + throw new Error('New password is invalid.'); } const otp = await redisClient.get(email); verifyOTP(token, otp); - await updateUserPasswordFromEmail(email, newPassword, SALT_ROUNDS); + await updateUserPasswordFromEmail(email, password, SALT_ROUNDS); - return res.status(200).json({ message: "ok" }); + return res.status(200).json({ message: 'ok' }); } catch (error) { return res.status(400).json({ message: `Unable to update password. ${(error as Error).message}`, @@ -230,22 +263,22 @@ app.post("/auth/password/forgot", async (req: Request, res: Response) => { } }); -app.post("/auth/password/update", async (req: Request, res: Response) => { +app.post('/auth/password/update', async (req: Request, res: Response) => { try { const { oldPassword, newPassword } = req.body; if (!oldPassword) { - throw new Error("Unable to validate existing password."); + throw new Error('Unable to validate existing password.'); } if (!newPassword) { - throw new Error("Invalid new password."); + throw new Error('Invalid new password.'); } const currentId = req.session.userId; if (!currentId) { - throw new Error("Invalid session."); + throw new Error('Invalid session.'); } // validate old password @@ -253,17 +286,17 @@ app.post("/auth/password/update", async (req: Request, res: Response) => { const validPassword = await bcrypt.compare(oldPassword, user.password); if (!validPassword) { - throw new Error("Current password is incorrect."); + throw new Error('Current password is incorrect.'); } // save new password await updateUserPasswordFromEmail(user.email, newPassword, SALT_ROUNDS); // refresh session - req.session.cookie.expires = dayjs().add(1, "week").toDate(); + req.session.cookie.expires = dayjs().add(1, 'week').toDate(); req.session.save(); - return res.status(200).json({ message: "ok" }); + return res.status(200).json({ message: 'ok' }); } catch (error) { return res.status(400).json({ message: `Unable to update password. ${(error as Error).message}`, @@ -271,12 +304,12 @@ app.post("/auth/password/update", async (req: Request, res: Response) => { } }); -app.post("/auth/login", async (req: TypedRequest, res: Response) => { +app.post('/auth/login', async (req: TypedRequest, res: Response) => { try { const { username, password } = req.body; if (!username || !password) { - return res.status(400).json({ error: "Missing required fields" }); + return res.status(400).json({ error: 'Missing required fields' }); } // Find user @@ -287,60 +320,72 @@ app.post("/auth/login", async (req: TypedRequest, res: Response) => { }); if (!user) { - return res.status(400).json({ error: "User doesnt exist!" }); + return res.status(400).json({ error: 'User doesnt exist!' }); } // Check password const validPassword = await bcrypt.compare(password, user.password); if (!validPassword) { - return res.status(400).json({ error: "Invalid password" }); + return res.status(400).json({ error: 'Invalid password' }); } // Set user session req.session.userId = user.id; - req.session.cookie.expires = dayjs().add(1, "week").toDate(); + req.session.cookie.expires = dayjs().add(1, 'week').toDate(); req.session.save(); // Explicitly save session to Redis - return res.status(200).json({ message: "ok" }); + return res.status(200).json({ + id: user.id, + username: user.username, + email: user.email, + dateJoined: user.dateJoined, + profilePicture: user.profilePicture, + } as SanitisedUser); } catch (error) { - return res.status(500).json({ error: "Error logging in" }); + return res.status(500).json({ error: 'Error logging in' }); } }); +app.post('/auth/logout', async (req: Request, res: Response) => { + req.session.destroy(() => { + return res.status(200).json({ message: 'ok' }); + }); +}); + app.post( - "/discord/login", + '/discord/login', async (req: TypedRequest, res: Response) => { const sessionFromDB = await validateSession( req.session ? req.session : null ); if (!sessionFromDB) { - return res.status(401).json({ message: "Invalid session provided." }); + return res.status(401).json({ message: 'Invalid session provided.' }); } - if (!req.body.discordID || typeof req.body.discordID !== "number") { + if (!req.body.discordID || typeof req.body.discordID !== 'number') { return res .status(400) - .json({ error: "Body is missing discordID, or it is not a number." }); + .json({ error: 'Body is missing discordID, or it is not a number.' }); } await redisClient.set(`discord:${req.body.discordID}`, req.session.id); - return res.status(200).json({ message: "ok" }); + return res.status(200).json({ message: 'ok' }); } ); app.post( - "/discord/logout", + '/discord/logout', async (req: TypedRequest, res: Response) => { const providedSession = await validateSession( req.session ? req.session : null ); if (!providedSession) { - return res.status(401).json({ message: "Invalid session provided." }); + return res.status(401).json({ message: 'Invalid session provided.' }); } - if (!req.body.discordID || typeof req.body.discordID !== "number") { + if (!req.body.discordID || typeof req.body.discordID !== 'number') { return res .status(400) - .json({ error: "Body is missing discordID, or it is not a number." }); + .json({ error: 'Body is missing discordID, or it is not a number.' }); } const sessionString = await redisClient.get( @@ -350,19 +395,19 @@ app.post( if (!sessionString) { return res .status(400) - .json({ error: "No session is associated with this Discord account." }); + .json({ error: 'No session is associated with this Discord account.' }); } const session = JSON.parse(sessionString); if (!providedSession.id !== session.id) { - return res.status(401).json({ message: "Invalid session provided." }); + return res.status(401).json({ message: 'Invalid session provided.' }); } await redisClient.del(`discord_${req.body.discordID}`); return res.status(200).json({ message: - "The association between a Pyramids session and this Discord account has been removed.", + 'The association between a Pyramids session and this Discord account has been removed.', }); } ); @@ -416,10 +461,10 @@ const getUserFromID = async (userID: number): Promise => { } }; -app.get("/user", async (req, res: Response) => { +app.get('/user', async (req, res: Response) => { const sessionFromDB = await validateSession(req.session ? req.session : null); if (!sessionFromDB) { - return res.status(401).json({ message: "Invalid session provided." }); + return res.status(401).json({ message: 'Invalid session provided.' }); } const userID = sessionFromDB.userId; @@ -447,12 +492,12 @@ app.get("/user", async (req, res: Response) => { }); }); -app.get("/society", async (req, res: Response) => { - if (!req.query["id"]) { - return res.status(400).json({ message: "Missing `id` query parameter." }); +app.get('/society', async (req, res: Response) => { + if (!req.query['id']) { + return res.status(400).json({ message: 'Missing `id` query parameter.' }); } - const societyID = Number(req.query["id"]); + const societyID = Number(req.query['id']); const society = await prisma.society.findFirst({ where: { @@ -461,25 +506,25 @@ app.get("/society", async (req, res: Response) => { }); if (!society) { - return res.status(404).json({ message: "Society not found." }); + return res.status(404).json({ message: 'Society not found.' }); } return res.status(200).json(society); }); -app.get("/society/events", async (req, res: Response) => { - if (!req.query["id"]) { - return res.status(400).json({ message: "Missing `id` query parameter." }); +app.get('/society/events', async (req, res: Response) => { + if (!req.query['id']) { + return res.status(400).json({ message: 'Missing `id` query parameter.' }); } - const before = req.query["before"] - ? new Date(req.query["before"] as string) + const before = req.query['before'] + ? new Date(req.query['before'] as string) : undefined; - const after = req.query["after"] - ? new Date(req.query["after"] as string) + const after = req.query['after'] + ? new Date(req.query['after'] as string) : undefined; - const societyID = Number(req.query["id"]); + const societyID = Number(req.query['id']); const society = await prisma.society.findFirst({ where: { @@ -488,7 +533,7 @@ app.get("/society/events", async (req, res: Response) => { }); if (!society) { - return res.status(404).json({ message: "Society not found." }); + return res.status(404).json({ message: 'Society not found.' }); } // Find events that pertain to the society @@ -509,14 +554,14 @@ app.get("/society/events", async (req, res: Response) => { }), }, orderBy: { - startDateTime: "asc", + startDateTime: 'asc', }, }); if (!events || events.length === 0) { return res.status(404).json({ message: - "The society does not have any events, or none exist within the provided filters.", + 'The society does not have any events, or none exist within the provided filters.', }); } @@ -524,11 +569,11 @@ app.get("/society/events", async (req, res: Response) => { }); app.post( - "/society/create", + '/society/create', async (req: TypedRequest, res: Response) => { const society = req.body; if (!society.name) { - return res.status(400).json({ message: "Invalid input." }); + return res.status(400).json({ message: 'Invalid input.' }); } if (!society.profilePicture) { @@ -540,7 +585,7 @@ app.post( ); if (!sessionFromDB) { - return res.status(401).json({ message: "Invalid session provided." }); + return res.status(401).json({ message: 'Invalid session provided.' }); } try { @@ -557,13 +602,13 @@ app.post( return res.status(200).json(newSociety); } catch (e) { - return res.status(400).json({ message: "invalid society input" }); + return res.status(400).json({ message: 'invalid society input' }); } } ); app.post( - "/event", + '/event', async (req: TypedRequest, res: Response) => { //Session validation const event = req.body; @@ -572,12 +617,12 @@ app.post( req.session ? req.session : null ); if (!sessionFromDB) { - return res.status(401).json({ message: "Invalid session provided." }); + return res.status(401).json({ message: 'Invalid session provided.' }); } //Sanitize Inputs/Check Validity if (!isValidDate(event.startDateTime, event.endDateTime)) { - return res.status(400).json({ message: "Invalid date" }); + return res.status(400).json({ message: 'Invalid date' }); } try { @@ -598,22 +643,22 @@ app.post( }); return res.status(200).json(eventRes); } catch (e) { - return res.status(400).json({ message: "Invalid event input" }); + return res.status(400).json({ message: 'Invalid event input' }); } } ); -app.put("/event", async (req: TypedRequest, res: Response) => { +app.put('/event', async (req: TypedRequest, res: Response) => { const sessionFromDB = await validateSession(req.session ? req.session : null); if (!sessionFromDB) { - return res.status(401).json({ message: "Invalid session provided." }); + return res.status(401).json({ message: 'Invalid session provided.' }); } - if (!req.body["id"]) { - return res.status(400).json({ message: "Missing `id` query parameter." }); + if (!req.body['id']) { + return res.status(400).json({ message: 'Missing `id` query parameter.' }); } - const eventID = Number(req.body["id"]); + const eventID = Number(req.body['id']); const event = await prisma.event.findFirst({ where: { @@ -622,11 +667,11 @@ app.put("/event", async (req: TypedRequest, res: Response) => { }); if (!event) { - return res.status(404).json({ message: "Event not found." }); + return res.status(404).json({ message: 'Event not found.' }); } if (!isValidDate(req.body.startDateTime, req.body.endDateTime)) { - return res.status(400).json({ message: "Invalid date" }); + return res.status(400).json({ message: 'Invalid date' }); } try { @@ -645,21 +690,21 @@ app.put("/event", async (req: TypedRequest, res: Response) => { }); return res.status(200).json(eventRes); } catch (e) { - return res.status(400).json({ message: "Invalid event input" }); + return res.status(400).json({ message: 'Invalid event input' }); } }); -app.get("/event", async (req: Request, res: Response) => { +app.get('/event', async (req: Request, res: Response) => { const sessionFromDB = await validateSession(req.session ? req.session : null); if (!sessionFromDB) { - return res.status(401).json({ message: "Invalid session provided." }); + return res.status(401).json({ message: 'Invalid session provided.' }); } - if (!req.query["id"]) { - return res.status(400).json({ message: "Missing `id` query parameter." }); + if (!req.query['id']) { + return res.status(400).json({ message: 'Missing `id` query parameter.' }); } - const eventID = Number(req.query["id"]); + const eventID = Number(req.query['id']); const event = await prisma.event.findFirst({ where: { @@ -668,7 +713,7 @@ app.get("/event", async (req: Request, res: Response) => { }); if (!event) { - return res.status(404).json({ message: "Event not found." }); + return res.status(404).json({ message: 'Event not found.' }); } return res.status(200).json(event); @@ -681,24 +726,24 @@ function isValidDate(startDate: Date, endDate: Date): boolean { return !( parsedStartDate.isAfter(parsedEndDate) || parsedStartDate.isSame(parsedEndDate) || - parsedStartDate.isBefore(dayjs(), "day") + parsedStartDate.isBefore(dayjs(), 'day') ); } -app.get("/events", async (req, res: Response) => { - const page = Number(req.query["page"]) - 1 || 0; +app.get('/events', async (req, res: Response) => { + const page = Number(req.query['page']) - 1 || 0; if (page < 0 || isNaN(page)) { return res.status(400).json({ - message: "Invalid page specified. Note that a page must be 1 or greater.", + message: 'Invalid page specified. Note that a page must be 1 or greater.', }); } - const before = req.query["before"] - ? new Date(req.query["before"] as string) + const before = req.query['before'] + ? new Date(req.query['before'] as string) : undefined; - const after = req.query["after"] - ? new Date(req.query["after"] as string) + const after = req.query['after'] + ? new Date(req.query['after'] as string) : undefined; const events = await prisma.event.findMany({ @@ -715,23 +760,23 @@ app.get("/events", async (req, res: Response) => { }), }, orderBy: { - startDateTime: "asc", + startDateTime: 'asc', }, skip: page * 10, take: 10, }); if (!events || events.length === 0) { - return res.status(200).json({ message: "No events found." }); + return res.status(404).json({ message: 'No events found.' }); } return res.status(200).json(events); }); -app.get("/user/events", async (req, res: Response) => { +app.get('/user/events', async (req, res: Response) => { const sessionFromDB = await validateSession(req.session ? req.session : null); if (!sessionFromDB) { - return res.status(401).json({ message: "Invalid session provided." }); + return res.status(401).json({ message: 'Invalid session provided.' }); } const userID = sessionFromDB.userId; @@ -739,21 +784,21 @@ app.get("/user/events", async (req, res: Response) => { // pagination is optional for /user/events let page = undefined; - if (req.query["page"]) { - page = Number(req.query["page"]) - 1; + if (req.query['page']) { + page = Number(req.query['page']) - 1; if (page < 0 || isNaN(page)) { return res.status(400).json({ message: - "Invalid page specified. Note that a page must be 1 or greater.", + 'Invalid page specified. Note that a page must be 1 or greater.', }); } } - const before = req.query["before"] - ? new Date(req.query["before"] as string) + const before = req.query['before'] + ? new Date(req.query['before'] as string) : undefined; - const after = req.query["after"] - ? new Date(req.query["after"] as string) + const after = req.query['after'] + ? new Date(req.query['after'] as string) : undefined; const events = await prisma.event.findMany({ @@ -775,7 +820,7 @@ app.get("/user/events", async (req, res: Response) => { }, }, orderBy: { - startDateTime: "asc", + startDateTime: 'asc', }, ...(page !== undefined && { skip: page * 10, @@ -784,16 +829,16 @@ app.get("/user/events", async (req, res: Response) => { }); if (!events || events.length === 0) { - return res.status(404).json({ message: "No events found." }); + return res.status(404).json({ message: 'No events found.' }); } return res.status(200).json(events); }); -app.get("/user/societies", async (req, res: Response) => { +app.get('/user/societies', async (req, res: Response) => { const sessionFromDB = await validateSession(req.session ? req.session : null); if (!sessionFromDB) { - return res.status(401).json({ message: "Invalid session provided." }); + return res.status(401).json({ message: 'Invalid session provided.' }); } const userID = sessionFromDB.userId; @@ -824,10 +869,10 @@ app.get("/user/societies", async (req, res: Response) => { return res.status(200).json(societies); }); -app.get("/societies", async (req, res: Response) => { +app.get('/societies', async (req, res: Response) => { const societies = await prisma.society.findMany({ orderBy: { - id: "asc", + id: 'asc', }, }); return res.status(200).json(societies); @@ -835,14 +880,14 @@ app.get("/societies", async (req, res: Response) => { //Lets a user join a society app.post( - "/user/society/join", + '/user/society/join', async (req: TypedRequest, res: Response) => { //get userid from session const sessionFromDB = await validateSession( req.session ? req.session : null ); if (!sessionFromDB) { - return res.status(401).json({ message: "Invalid session provided." }); + return res.status(401).json({ message: 'Invalid session provided.' }); } const userID = sessionFromDB.userId; @@ -858,7 +903,7 @@ app.post( }); if (!societyId) { - return res.status(400).json({ message: "Invalid society" }); + return res.status(400).json({ message: 'Invalid society' }); } //Connect society and user @@ -875,18 +920,18 @@ app.post( }, }); - return res.status(200).json({ message: "ok" }); + return res.status(200).json({ message: 'ok' }); } ); app.delete( - "/user/society", + '/user/society', async (req: TypedRequest, res: Response) => { const sessionFromDB = await validateSession( req.session ? req.session : null ); if (!sessionFromDB) { - return res.status(401).json({ message: "Invalid session provided." }); + return res.status(401).json({ message: 'Invalid session provided.' }); } const userID = sessionFromDB.userId; @@ -901,7 +946,7 @@ app.delete( }); if (!societyId) { - return res.status(400).json({ message: "Invalid society" }); + return res.status(400).json({ message: 'Invalid society' }); } const result = await prisma.society.update({ @@ -917,18 +962,18 @@ app.delete( }, }); - return res.status(200).json({ message: "ok" }); + return res.status(200).json({ message: 'ok' }); } ); app.post( - "/user/event", + '/user/event', async (req: TypedRequest, res: Response) => { const sessionFromDB = await validateSession( req.session ? req.session : null ); if (!sessionFromDB) { - return res.status(401).json({ message: "Invalid session provided." }); + return res.status(401).json({ message: 'Invalid session provided.' }); } const userID = sessionFromDB.userId; @@ -943,7 +988,7 @@ app.post( }); if (!event) { - return res.status(400).json({ message: "Invalid Event" }); + return res.status(400).json({ message: 'Invalid Event' }); } const result = await prisma.event.update({ @@ -959,18 +1004,18 @@ app.post( }, }); - return res.status(200).json({ message: "ok" }); + return res.status(200).json({ message: 'ok' }); } ); app.delete( - "/user/event", + '/user/event', async (req: TypedRequest, res: Response) => { const sessionFromDB = await validateSession( req.session ? req.session : null ); if (!sessionFromDB) { - return res.status(401).json({ message: "Invalid session provided." }); + return res.status(401).json({ message: 'Invalid session provided.' }); } const userID = sessionFromDB.userId; @@ -985,7 +1030,7 @@ app.delete( }); if (!event) { - return res.status(400).json({ message: "Invalid Event" }); + return res.status(400).json({ message: 'Invalid Event' }); } const result = await prisma.event.update({ @@ -1001,13 +1046,13 @@ app.delete( }, }); - return res.status(200).json({ message: "ok" }); + return res.status(200).json({ message: 'ok' }); } ); //For retrieving the data in the individual event view card app.get( - "/event/details", + '/event/details', async (req: TypedRequest, res: Response) => { const event = await prisma.event.findFirst({ where: { @@ -1016,7 +1061,7 @@ app.get( }); if (!event) { - return res.status(400).json({ message: "invalid society" }); + return res.status(400).json({ message: 'invalid society' }); } return res.status(200).json(event); @@ -1024,10 +1069,10 @@ app.get( ); //this is a bit messy -app.delete("/event", async (req: TypedRequest, res: Response) => { +app.delete('/event', async (req: TypedRequest, res: Response) => { const sessionFromDB = await validateSession(req.session ? req.session : null); if (!sessionFromDB) { - return res.status(401).json({ message: "Invalid session provided." }); + return res.status(401).json({ message: 'Invalid session provided.' }); } const userID = sessionFromDB.userId; @@ -1061,7 +1106,7 @@ app.delete("/event", async (req: TypedRequest, res: Response) => { }); if (!society) { - return res.status(401).json({ message: "User is not an admin!" }); + return res.status(401).json({ message: 'User is not an admin!' }); } //200 if deletion is successful @@ -1072,20 +1117,20 @@ app.delete("/event", async (req: TypedRequest, res: Response) => { }, }); } catch (e) { - return res.status(400).json({ message: "Deletion failed" }); + return res.status(400).json({ message: 'Deletion failed' }); } - return res.status(200).json({ message: "ok" }); + return res.status(200).json({ message: 'ok' }); }); app.delete( - "/society", + '/society', async (req: TypedRequest, res: Response) => { const sessionFromDB = await validateSession( req.session ? req.session : null ); if (!sessionFromDB) { - return res.status(401).json({ message: "Invalid session provided." }); + return res.status(401).json({ message: 'Invalid session provided.' }); } const userID = sessionFromDB.userId; @@ -1102,7 +1147,7 @@ app.delete( }); if (!society) { - return res.status(401).json({ message: "User is not an admin!" }); + return res.status(401).json({ message: 'User is not an admin!' }); } //200 if deletion is successful @@ -1113,18 +1158,84 @@ app.delete( }, }); } catch (e) { - return res.status(400).json({ message: "Deletion failed" }); + return res.status(400).json({ message: 'Deletion failed' }); } - return res.status(200).json({ message: "ok" }); + return res.status(200).json({ message: 'ok' }); } ); -app.get("/hello", () => { - console.log("Hello World!"); +// gets keywords a user is associated with +app.get('/user/keywords', async (req, res: Response) => { + const sessionFromDB = await validateSession(req.session ? req.session : null); + if (!sessionFromDB) { + return res.status(401).json({ message: 'Invalid session provided.' }); + } + + const userID = sessionFromDB.userId; + + const userKeywords = await prisma.user.findFirst({ + where: { + id: userID, + }, + select: { + keywords: { + select: { text: true }, + }, + }, + }); + + return res.status(200).json(userKeywords); +}); + +// creates a keyword +app.post( + '/keyword', + async (req: TypedRequest, res: Response) => { + const sessionFromDB = await validateSession( + req.session ? req.session : null + ); + + if (!sessionFromDB) { + return res.status(401).json({ message: 'Invalid session provided.' }); + } + + const { text } = req.body; + if (!text) { + return res.status(400).json({ message: 'Invalid input.' }); + } + + const keywordExists = await prisma.keyword.findFirst({ + where: { + text: text, + }, + }); + + if (keywordExists) { + return res.status(400).json({ message: 'Keyword already exists.' }); + } + + try { + const newKeyword = await prisma.keyword.create({ + data: { + text: text, + }, + }); + return res.status(200).json(newKeyword); + } catch (e) { + return res.status(400).json({ message: 'invalid keyword input' }); + } + } +); + +// - app.post("/user/keyword") - Associates a user with a keyword +// - app.delete("/user/keyword") - Disassociates a user with a keyword + +app.get('/hello', () => { + console.log('Hello World!'); }); -if (process.env["NODE_ENV"] !== "test") { +if (process.env['NODE_ENV'] !== 'test') { app.listen(SERVER_PORT, () => { console.log(`Server running on port http://localhost:${SERVER_PORT}`); }); diff --git a/backend/src/requestTypes.ts b/backend/src/requestTypes.ts index 90ae943..1d3c637 100644 --- a/backend/src/requestTypes.ts +++ b/backend/src/requestTypes.ts @@ -43,6 +43,10 @@ export interface CreateSocietyBody { profilePicture: string | null; } +export interface CreateKeywordBody { + text: string; +} + export interface DiscordLoginBody { discordID: number; } diff --git a/backend/src/routes/OTP/OTPToken.ts b/backend/src/routes/OTP/OTPToken.ts index bb9757b..7f9fad8 100644 --- a/backend/src/routes/OTP/OTPToken.ts +++ b/backend/src/routes/OTP/OTPToken.ts @@ -1,14 +1,13 @@ -import { User } from "@prisma/client"; -import prisma from "../../prisma"; +import { User } from '@prisma/client'; +import prisma from '../../prisma'; export interface OTPToken { - user: User, - token: string, - timeCreated: Date, - expiryTime: Date, + user: User; + token: string; + timeCreated: Date; + expiryTime: Date; } - export const getUserFromEmail = async (emailAddress: string) => { try { const result = await prisma.user.findUnique({ @@ -18,7 +17,6 @@ export const getUserFromEmail = async (emailAddress: string) => { }); return result; } catch (error) { - throw new Error("Error finding user"); + throw new Error('Error finding user'); } - -} \ No newline at end of file +}; diff --git a/backend/src/routes/OTP/generateOTP.ts b/backend/src/routes/OTP/generateOTP.ts index b2da255..5dd5f67 100644 --- a/backend/src/routes/OTP/generateOTP.ts +++ b/backend/src/routes/OTP/generateOTP.ts @@ -3,17 +3,21 @@ import { getUserFromEmail } from './OTPToken'; import { sendEmail } from './sendEmail'; export const generateOTP = async (emailAddress: string) => { - const user = await getUserFromEmail(emailAddress); - - if(!user) { - throw new Error("User not found."); - } - - const rand = crypto.randomBytes(4).readUint32BE(0); - const sixDigits = (rand % 900000) + 100000; - const otpCode = sixDigits.toString(); - + const user = await getUserFromEmail(emailAddress); + + if (!user) { + throw new Error("User doesn't exist!"); + } + + const rand = crypto.randomBytes(4).readUint32BE(0); + const sixDigits = (rand % 900000) + 100000; + const otpCode = sixDigits.toString(); + + try { await sendEmail(emailAddress, user.username, otpCode); + } catch (error) { + throw new Error('Error sending OTP email'); + } - return otpCode; -} + return otpCode; +}; diff --git a/backend/src/routes/OTP/verifyOTP.ts b/backend/src/routes/OTP/verifyOTP.ts index 8aca308..4f9fa01 100644 --- a/backend/src/routes/OTP/verifyOTP.ts +++ b/backend/src/routes/OTP/verifyOTP.ts @@ -1,9 +1,9 @@ export const verifyOTP = (token: string, otp: string | null) => { - if(!otp) { - throw new Error("One time code is invalid or expired."); - } - - if(otp !== token) { - throw new Error("Incorrect code."); - } -} \ No newline at end of file + if (!otp) { + throw new Error('One time code is invalid or expired.'); + } + + if (otp !== token) { + throw new Error('Incorrect code.'); + } +}; diff --git a/backend/tests/createKeyword.test.ts b/backend/tests/createKeyword.test.ts new file mode 100644 index 0000000..f9dd1c8 --- /dev/null +++ b/backend/tests/createKeyword.test.ts @@ -0,0 +1,223 @@ +import { expect, test, vi, describe } from "vitest"; // 👈🏻 Added the `vi` import +import prisma from "../src/prisma"; +import request from "supertest"; +import app from "../src/index"; +import { beforeEach } from "node:test"; +import dayjs from "dayjs"; + +describe('/keyword endpoint', () => { + test("Invalid session", async () => { + const { body, status } = await request(app).post("/keyword").send({ + text: "skibidi", + }); + + expect(status).toBe(401); + expect(body.message).toBe("Invalid session provided."); + }); + + test('Keyword successfully created', async () => { + const { status, body } = await request(app).post("/auth/register").send({ + username: "shinjisatoo", + password: "testpassword", + email: "longseason1996@gmail.com", + }); + + const newUser = await prisma.user.findFirst({ + where: { + id: body.newUser.id, + }, + }); + + if (newUser == null) return; + expect(status).toBe(201); + expect(newUser).not.toBeNull(); + + const loginres = await request(app).post("/auth/login").send({ + username: "shinjisatoo", + password: "testpassword", + }); + let sessionID = loginres.headers["set-cookie"]; + + const response = await request(app) + .post("/keyword") + .set("Cookie", sessionID) + .send({ + text: "skibidi toilet yes yes ghvsbdkakbhf", + }); + + expect(response.status).toBe(200); + }); + +// test('Event invalid', async () => { +// const { status, body } = await request(app).post("/auth/register").send({ +// username: "shinjisatoo", +// password: "testpassword", +// email: "longseason1996@gmail.com", +// userType: "ATTENDEE", +// }); + +// const newUser = await prisma.user.findFirst({ +// where: { +// id: body.newUser.id, +// }, +// }); + +// if (newUser == null) return; +// expect(status).toBe(201); +// expect(newUser).not.toBeNull(); + +// const loginres = await request(app).post("/auth/login").send({ +// username: "shinjisatoo", +// password: "testpassword", +// }); +// let sessionID = loginres.headers["set-cookie"]; + +// const societyRes = await request(app).post("/society/create") +// .set("Cookie", sessionID) +// .send({ +// name: "Rizzsoc", +// userId: newUser.id, +// }); + +// const socId = societyRes.body.id +// expect(societyRes.status).toBe(200); +// const start = new Date() + +// const eventRes = await request(app) +// .post("/event/create") +// .set("Cookie", sessionID) +// .send({ +// banner: "https://img-cdn.inc.com/image/upload/f_webp,q_auto,c_fit/images/panoramic/Island-Entertainment-viral-tiktok-inc_539684_hnvnix.jpg", +// name: "tiktokrizzparty", +// startDateTime: new Date(), +// endDateTime: new Date(start.getTime() + 86400000), +// location: "tampa, florida", +// description: "fein! fein! fein! fein! fein so good she honor roll", +// societyId: socId +// }); + +// const attendRes = await request(app).post("/user/event/attend") +// .set("Cookie", sessionID) +// .send({ +// eventId: -123 +// }) + +// expect(attendRes.status).toBe(400); +// }) +// }) + +// describe('/unattend endpoint', () => { +// test('Unattend successful', async () => { +// const { status, body } = await request(app).post("/auth/register").send({ +// username: "shinjisatoo", +// password: "testpassword", +// email: "longseason1996@gmail.com", +// userType: "ATTENDEE", +// }); + +// const newUser = await prisma.user.findFirst({ +// where: { +// id: body.newUser.id, +// }, +// }); + +// if (newUser == null) return; +// expect(status).toBe(201); +// expect(newUser).not.toBeNull(); + +// const loginres = await request(app).post("/auth/login").send({ +// username: "shinjisatoo", +// password: "testpassword", +// }); +// let sessionID = loginres.headers["set-cookie"]; + +// const societyRes = await request(app).post("/society/create") +// .set("Cookie", sessionID) +// .send({ +// name: "Rizzsoc", +// userId: newUser.id, +// }); + +// const socId = societyRes.body.id +// expect(societyRes.status).toBe(200); +// const start = new Date() + +// const eventRes = await request(app) +// .post("/event/create") +// .set("Cookie", sessionID) +// .send({ +// banner: "https://img-cdn.inc.com/image/upload/f_webp,q_auto,c_fit/images/panoramic/Island-Entertainment-viral-tiktok-inc_539684_hnvnix.jpg", +// name: "tiktokrizzparty", +// startDateTime: new Date(), +// endDateTime: new Date(start.getTime() + 86400000), +// location: "tampa, florida", +// description: "fein! fein! fein! fein! fein so good she honor roll", +// societyId: socId +// }); + +// const attendRes = await request(app).delete("/user/event") +// .set("Cookie", sessionID) +// .send({ +// eventId: eventRes.body.numId +// }) + +// expect(attendRes.status).toBe(200); +// }) + +// test('Event invalid', async () => { +// const { status, body } = await request(app).post("/auth/register").send({ +// username: "shinjisatoo", +// password: "testpassword", +// email: "longseason1996@gmail.com", +// userType: "ATTENDEE", +// }); + +// const newUser = await prisma.user.findFirst({ +// where: { +// id: body.newUser.id, +// }, +// }); + +// if (newUser == null) return; +// expect(status).toBe(201); +// expect(newUser).not.toBeNull(); + +// const loginres = await request(app).post("/auth/login").send({ +// username: "shinjisatoo", +// password: "testpassword", +// }); +// let sessionID = loginres.headers["set-cookie"]; + +// const societyRes = await request(app).post("/society/create") +// .set("Cookie", sessionID) +// .send({ +// name: "Rizzsoc", +// userId: newUser.id, +// }); + +// const socId = societyRes.body.id +// expect(societyRes.status).toBe(200); +// const start = new Date() + +// const eventRes = await request(app) +// .post("/event/create") +// .set("Cookie", sessionID) +// .send({ +// banner: "https://img-cdn.inc.com/image/upload/f_webp,q_auto,c_fit/images/panoramic/Island-Entertainment-viral-tiktok-inc_539684_hnvnix.jpg", +// name: "tiktokrizzparty", +// startDateTime: new Date(), +// endDateTime: new Date(start.getTime() + 86400000), +// location: "tampa, florida", +// description: "fein! fein! fein! fein! fein so good she honor roll", +// societyId: socId +// }); + +// const attendRes = await request(app).delete("/user/event") +// .set("Cookie", sessionID) +// .send({ +// eventId: -123 +// }) + +// expect(attendRes.status).toBe(400); +// }) +}) \ No newline at end of file diff --git a/backend/tests/otp.test.ts b/backend/tests/otp.test.ts index 744fb14..7f2a5f8 100644 --- a/backend/tests/otp.test.ts +++ b/backend/tests/otp.test.ts @@ -1,23 +1,23 @@ //test/sample.test.ts -import { expect, test, vi, describe } from "vitest"; // 👈🏻 Added the `vi` import -import request from "supertest"; -import app from "../src/index"; -import { createClient } from "redis"; -import prisma from "../src/prisma"; - -describe("Password change", () => { - test("Forgot password OTP", async () => { +import { expect, test, vi, describe } from 'vitest'; // 👈🏻 Added the `vi` import +import request from 'supertest'; +import app from '../src/index'; +import { createClient } from 'redis'; +import prisma from '../src/prisma'; + +describe.skip('Password change', () => { + test('Forgot password OTP', async () => { const redisClient = createClient({ - url: `redis://localhost:${process.env["REDIS_PORT"]}`, + url: `redis://localhost:${process.env['REDIS_PORT']}`, }); await redisClient.connect().catch(console.error); expect(redisClient).not.toBeUndefined(); - - const { status, body } = await request(app).post("/auth/register").send({ - username: "richard grayson", - password: "iheartkori", - email: "pyramidstestdump@gmail.com" + + const { status, body } = await request(app).post('/auth/register').send({ + username: 'richard grayson', + password: 'iheartkori', + email: 'pyramidstestdump@gmail.com', }); const newUser = await prisma.user.findFirst({ @@ -25,127 +25,126 @@ describe("Password change", () => { id: body.newUser.id, }, }); - + expect(status).toBe(201); expect(newUser).not.toBeNull(); expect(body.newUser).toStrictEqual({ - username: "richard grayson", + username: 'richard grayson', id: newUser?.id, }); - if(newUser) { - + if (newUser) { //testing token expiration - const expRes = await request(app).post("/auth/otp/generate").send({ - email: "pyramidstestdump@gmail.com" + const expRes = await request(app).post('/auth/otp/generate').send({ + email: 'pyramidstestdump@gmail.com', }); const expResToken = expRes.body.message; expect(expRes.status).toBe(200); expect(expResToken).not.toBeUndefined(); - const expToken = await redisClient.get(newUser.email); + const expToken = await redisClient.get(newUser.email); expect(expToken).not.toBeNull(); expect(expToken).toEqual(expResToken); - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 1000)); const checkGenTokens = await redisClient.get(newUser.email); expect(checkGenTokens).toBeNull(); //verification - const genRes = await request(app).post("/auth/otp/generate").send({ - email: "pyramidstestdump@gmail.com" + const genRes = await request(app).post('/auth/otp/generate').send({ + email: 'pyramidstestdump@gmail.com', }); const resToken = genRes.body.message; - let verToken = await redisClient.get(newUser.email); + let verToken = await redisClient.get(newUser.email); expect(verToken).not.toBeNull(); expect(verToken).toEqual(resToken); - - const verRes = await request(app).post("/auth/otp/verify").send({ - email: "pyramidstestdump@gmail.com", - token: resToken + + const verRes = await request(app).post('/auth/otp/verify').send({ + email: 'pyramidstestdump@gmail.com', + token: resToken, }); expect(verRes.status).toBe(200); - verToken = await redisClient.get(newUser.email); + verToken = await redisClient.get(newUser.email); expect(verToken).not.toBeNull(); - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 1000)); const checkVerTokens = await redisClient.get(newUser.email); expect(checkVerTokens).toBeNull(); //forgot password - const oldPassRes = await request(app).post("/auth/login").send({ - username: "richard grayson", - password: "iheartkori" + const oldPassRes = await request(app).post('/auth/login').send({ + username: 'richard grayson', + password: 'iheartkori', }); expect(oldPassRes.status).toBe(200); - const fGenRes = await request(app).post("/auth/otp/generate").send({ - email: "pyramidstestdump@gmail.com" + const fGenRes = await request(app).post('/auth/otp/generate').send({ + email: 'pyramidstestdump@gmail.com', }); const fResToken = fGenRes.body.message; - let fVerToken = await redisClient.get(newUser.email); + let fVerToken = await redisClient.get(newUser.email); expect(fVerToken).not.toBeNull(); expect(fVerToken).toEqual(fResToken); - - const fVerRes = await request(app).post("/auth/otp/verify").send({ - email: "pyramidstestdump@gmail.com", - token: fResToken + + const fVerRes = await request(app).post('/auth/otp/verify').send({ + email: 'pyramidstestdump@gmail.com', + token: fResToken, }); expect(fVerRes.status).toBe(200); - fVerToken = await redisClient.get(newUser.email); + fVerToken = await redisClient.get(newUser.email); expect(fVerToken).not.toBeNull(); - const forgotRes = await request(app).post("/auth/password/forgot").send({ - email: "pyramidstestdump@gmail.com", + const forgotRes = await request(app).post('/auth/password/forgot').send({ + email: 'pyramidstestdump@gmail.com', token: fResToken, - newPassword: "oraclefan1" + newPassword: 'oraclefan1', }); //console.log(forgotRes.error.text); expect(forgotRes.status).toBe(200); - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 1000)); const fCheckVerTokens = await redisClient.get(newUser.email); expect(fCheckVerTokens).toBeNull(); - const oldPassFailRes = await request(app).post("/auth/login").send({ - username: "richard grayson", - password: "iheartkori" + const oldPassFailRes = await request(app).post('/auth/login').send({ + username: 'richard grayson', + password: 'iheartkori', }); expect(oldPassFailRes.status).toBe(400); - expect(oldPassFailRes.body.error).toBe("Invalid password"); + expect(oldPassFailRes.body.error).toBe('Invalid password'); - const loginRes = await request(app).post("/auth/login").send({ - username: "richard grayson", - password: "oraclefan1", + const loginRes = await request(app).post('/auth/login').send({ + username: 'richard grayson', + password: 'oraclefan1', }); - + expect(loginRes.status).toBe(200); } }, 200000); - test("Update password", async () => { + test('Update password', async () => { const redisClient = createClient({ - url: `redis://localhost:${process.env["REDIS_PORT"]}`, + url: `redis://localhost:${process.env['REDIS_PORT']}`, }); await redisClient.connect().catch(console.error); expect(redisClient).not.toBeUndefined(); - - const { status, body } = await request(app).post("/auth/register").send({ - username: "richard grayson", - password: "iheartkori", - email: "pyramidstestdump@gmail.com" + + const { status, body } = await request(app).post('/auth/register').send({ + username: 'richard grayson', + password: 'iheartkori', + email: 'pyramidstestdump@gmail.com', }); const newUser = await prisma.user.findFirst({ @@ -153,47 +152,53 @@ describe("Password change", () => { id: body.newUser.id, }, }); - + expect(status).toBe(201); expect(newUser).not.toBeNull(); expect(body.newUser).toStrictEqual({ - username: "richard grayson", + username: 'richard grayson', id: newUser?.id, }); - if(newUser) { - const oldPassRes = await request(app).post("/auth/login").send({ - username: "richard grayson", - password: "iheartkori" + if (newUser) { + const oldPassRes = await request(app).post('/auth/login').send({ + username: 'richard grayson', + password: 'iheartkori', }); expect(oldPassRes.status).toBe(200); - const sessionID = oldPassRes.headers["set-cookie"]; + const sessionID = oldPassRes.headers['set-cookie']; - const updateResFail = await request(app).post("/auth/password/update") - .set("Cookie", sessionID).send({ - oldPassword: "1234", - newPassword: "newPassword" - }); + const updateResFail = await request(app) + .post('/auth/password/update') + .set('Cookie', sessionID) + .send({ + oldPassword: '1234', + newPassword: 'newPassword', + }); expect(updateResFail.status).toBe(400); - expect(updateResFail.body.message).toBe("Unable to update password. Current password is incorrect."); - - const updateRes = await request(app).post("/auth/password/update") - .set("Cookie", sessionID).send({ - oldPassword: "iheartkori", - newPassword: "batgirlfan123" - }); + expect(updateResFail.body.message).toBe( + 'Unable to update password. Current password is incorrect.' + ); + + const updateRes = await request(app) + .post('/auth/password/update') + .set('Cookie', sessionID) + .send({ + oldPassword: 'iheartkori', + newPassword: 'batgirlfan123', + }); expect(updateRes.status).toBe(200); - const oldPassResFail = await request(app).post("/auth/login").send({ - username: "richard grayson", - password: "iheartkori" + const oldPassResFail = await request(app).post('/auth/login').send({ + username: 'richard grayson', + password: 'iheartkori', }); expect(updateResFail.status).toBe(400); - const newPassRes = await request(app).post("/auth/login").send({ - username: "richard grayson", - password: "batgirlfan123" + const newPassRes = await request(app).post('/auth/login').send({ + username: 'richard grayson', + password: 'batgirlfan123', }); expect(newPassRes.status).toBe(200); } From f66bffaac1a521b3cfec0a76fdd4e7d621c88ea7 Mon Sep 17 00:00:00 2001 From: Jared Lucas Amistad Schulz Date: Sun, 15 Dec 2024 00:48:42 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=90=B3merged=20with=20main=20branch?= =?UTF-8?q?=20and=20added=20initial=20functionality=20for=20displaying=20m?= =?UTF-8?q?odal(not=20working=20properly=20yet,=20just=20wanted=20to=20get?= =?UTF-8?q?=20this=20commit=20up=20just=20in=20case)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- frontend/pnpm-lock.yaml | 2 +- frontend/src/App.tsx | 118 +++++++++++++----- frontend/src/AuthScreen/AuthScreen.module.css | 60 +++++---- frontend/src/AuthScreen/AuthScreen.tsx | 12 +- frontend/src/Button/Button.tsx | 1 + .../CalendarEventElem/CalendarEventElem.tsx | 21 +++- frontend/src/EventDetails/EventDetails.tsx | 6 +- .../src/GenerateOTP/GenerateOTP.module.css | 7 ++ frontend/src/GenerateOTP/GenerateOTP.tsx | 59 +++++++++ frontend/src/Login/Login.tsx | 48 ++++--- frontend/src/NavBar/NavBar.tsx | 4 +- .../src/ProtectedRoute/ProtectedRoute.tsx | 20 +++ .../SettingsNavbar/SettingsNavbar.tsx | 27 +++- .../Unauthenticated.module.css | 10 ++ .../src/Unauthenticated/Unauthenticated.tsx | 18 +++ frontend/src/UserContext/UserContext.ts | 19 +++ frontend/src/VerifyOTP/VerifyOTP.module.css | 7 ++ frontend/src/VerifyOTP/VerifyOTP.tsx | 112 +++++++++++++++++ frontend/src/errorHandler.ts | 85 ++++++++++--- 20 files changed, 536 insertions(+), 104 deletions(-) create mode 100644 frontend/src/GenerateOTP/GenerateOTP.module.css create mode 100644 frontend/src/GenerateOTP/GenerateOTP.tsx create mode 100644 frontend/src/ProtectedRoute/ProtectedRoute.tsx create mode 100644 frontend/src/Unauthenticated/Unauthenticated.module.css create mode 100644 frontend/src/Unauthenticated/Unauthenticated.tsx create mode 100644 frontend/src/UserContext/UserContext.ts create mode 100644 frontend/src/VerifyOTP/VerifyOTP.module.css create mode 100644 frontend/src/VerifyOTP/VerifyOTP.tsx diff --git a/README.md b/README.md index a9dd2a5..7060aaa 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,12 @@ ```bash SESSION_SECRET= NODE_ENV=development -ALLOWED_ORIGINS=commaseparated,regexes,slashesnotrequired +ALLOWED_ORIGINS=commaseparated,urls,or,regexes DATABASE_URL="postgresql://.../postgres?pgbouncer=true" DIRECT_URL="postgresql://.../postgres" REDIS_PORT=6379 +EMAIL_KEY="mailgun_api_key" +OTP_EXPIRES=600 ``` `NODE_ENV` may be either 'development' or 'production'. diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 1faeda4..646a6e6 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -1,7 +1,7 @@ lockfileVersion: '9.0' settings: - autoInstallPeers: true + autoInstallPeers: false excludeLinksFromLockfile: false importers: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 695a58c..fd4eb30 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,36 +1,96 @@ -import "./App.css"; -import NavBar from "./NavBar/NavBar"; -import { BrowserRouter, Route, Routes } from "react-router"; -import HomePage from "./HomePage/HomePage"; -import AboutPage from "./About/About"; -import Calendar from "./Calendar/Calendar"; -import LoginPage from "./Login/Login"; -import RegisterPage from "./Register/Register"; -import { Settings } from "./Settings/Settings"; -import { ProfilePage } from "./Settings/SettingsPage/ProfilePage/ProfilePage"; -import { EventManagementPage } from "./Settings/SettingsPage/EventManagementPage/EventManagementPage"; -import { CreateNewEventPage } from "./Settings/SettingsPage/EventManagementPage/CreateNewEvent/CreateNewEvent"; -import { DiscordPage } from "./Settings/SettingsPage/DiscordPage/DiscordPage"; +import './App.css'; +import NavBar from './NavBar/NavBar'; +import { BrowserRouter, Navigate, Route, Routes } from 'react-router'; +import HomePage from './HomePage/HomePage'; +import AboutPage from './About/About'; +import Calendar from './Calendar/Calendar'; +import LoginPage from './Login/Login'; +import RegisterPage from './Register/Register'; +import { Settings } from './Settings/Settings'; +import { ProfilePage } from './Settings/SettingsPage/ProfilePage/ProfilePage'; +import { EventManagementPage } from './Settings/SettingsPage/EventManagementPage/EventManagementPage'; +import { CreateNewEventPage } from './Settings/SettingsPage/EventManagementPage/CreateNewEvent/CreateNewEvent'; +import { DiscordPage } from './Settings/SettingsPage/DiscordPage/DiscordPage'; +import { Unauthenticated } from './Unauthenticated/Unauthenticated'; +import { ProtectedRoute } from './ProtectedRoute/ProtectedRoute'; +import { useEffect, useState } from 'react'; +import { User, UserContext } from './UserContext/UserContext'; +import GenerateOTP from './GenerateOTP/GenerateOTP'; +import VerifyOTP from './VerifyOTP/VerifyOTP'; function App() { + const [user, setUser] = useState(null); + + useEffect(() => { + fetch('http://localhost:5180/user', { + method: 'GET', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }).then((res) => { + if (res.ok) { + res.json().then((data) => { + setUser(data); + }); + } + }); + }, []); + return ( -
- - } /> - } /> - } /> // - } /> - } /> - }> - } /> - } /> - } /> - } /> - - -
- + +
+ + } /> + } /> + } /> // + } + > + + + } + /> + } + > + + + } + /> + } + > + + + } + > + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + +
+ +
); } diff --git a/frontend/src/AuthScreen/AuthScreen.module.css b/frontend/src/AuthScreen/AuthScreen.module.css index e9b2533..9306c9b 100644 --- a/frontend/src/AuthScreen/AuthScreen.module.css +++ b/frontend/src/AuthScreen/AuthScreen.module.css @@ -1,37 +1,45 @@ -.container{ - width: 20%; - height: 50%; - background-color: hsl(0, 0%, 100%); - border-radius: 10px; - margin: auto; - padding: 30px; - min-width: 500px; - min-height: 300px; +.container { + width: 20%; + height: 50%; + background-color: hsl(0, 0%, 100%); + border-radius: 10px; + margin: auto; + padding: 30px; + min-width: 500px; + min-height: 300px; } -.form{ - display: flex; - flex-direction: column; - align-items: center; - width: 100%; +.form { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; } -.headerText{ - padding-top: 34px; - padding-bottom: 46px; +.headerText { + padding-top: 34px; + padding-bottom: 46px; } -.footer{ - text-align: center; - color: hsl(0, 0%, 62%); +.footer { + text-align: center; + color: hsl(0, 0%, 62%); } .footer div:first-child { - padding-top: 50px; + padding-top: 50px; } -.error{ - padding: 10px; - color: hsl(0, 100%, 64%); - font-weight: bold; -} \ No newline at end of file +.error, +.success { + padding: 10px; + font-weight: bold; +} + +.error { + color: hsl(0, 100%, 64%); +} + +.success { + color: hsl(119, 100%, 30%); +} diff --git a/frontend/src/AuthScreen/AuthScreen.tsx b/frontend/src/AuthScreen/AuthScreen.tsx index dc6a2c3..56bc9ff 100644 --- a/frontend/src/AuthScreen/AuthScreen.tsx +++ b/frontend/src/AuthScreen/AuthScreen.tsx @@ -1,4 +1,4 @@ -import { ReactNode, FormEvent } from "react"; +import { ReactNode, FormEvent, Fragment } from "react"; import classes from "./AuthScreen.module.css"; import Button from "../Button/Button"; import { AuthError } from "../errorHandler"; @@ -12,6 +12,7 @@ type AuthScreenProp = { footer?: ReactNode; onSubmit: (event: FormEvent) => void; error?: AuthError; + success?: string; }; export function AuthScreen(props: AuthScreenProp) { @@ -23,7 +24,9 @@ export function AuthScreen(props: AuthScreenProp) {
- {props.inputs.map((input: ReactNode) => input)} + {props.inputs.map((input: ReactNode, index) => ( + {input} + ))} @@ -53,7 +55,7 @@ function EventDetails(props: EventDetailsProps) {
-

{props.date}

+

{props.eventDate}

diff --git a/frontend/src/GenerateOTP/GenerateOTP.module.css b/frontend/src/GenerateOTP/GenerateOTP.module.css new file mode 100644 index 0000000..7dee2d9 --- /dev/null +++ b/frontend/src/GenerateOTP/GenerateOTP.module.css @@ -0,0 +1,7 @@ +.container{ + background-color: hsl(0, 0%, 96%); + min-height: 100%; + min-width: 100%; + height: fit-content; + padding: 2%; +} \ No newline at end of file diff --git a/frontend/src/GenerateOTP/GenerateOTP.tsx b/frontend/src/GenerateOTP/GenerateOTP.tsx new file mode 100644 index 0000000..45ea021 --- /dev/null +++ b/frontend/src/GenerateOTP/GenerateOTP.tsx @@ -0,0 +1,59 @@ +import classes from './GenerateOTP.module.css'; +import { AuthScreen } from '../AuthScreen/AuthScreen'; +import { TextInput, TextOptions } from '../TextInput/TextInput'; +import { useState, FormEvent } from 'react'; +import { AtSymbolIcon } from '@heroicons/react/24/outline'; +import { errorHandler, AuthError } from '../errorHandler'; +import { useNavigate } from 'react-router'; + +export default function GenerateOTP() { + const [email, setEmail] = useState(''); + const [error, setError] = useState(undefined); + const navigate = useNavigate(); + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + const res = await fetch('http://localhost:5180/auth/otp/generate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email, + }), + }); + const json = await res.json(); + + if (!res.ok) { + setError(errorHandler(json.message)); + } else { + setError(undefined); + navigate('/changepassword/verify', { + state: { + email, + }, + }); + } + } + return ( +
+ Please enter the email associated with your account.} + inputs={[ + } + onChange={setEmail} + type={TextOptions.Email} + error={(error && error.emailError) || false} + />, + ]} + buttonText="Submit" + onSubmit={handleSubmit} + error={error} + > +
+ ); +} diff --git a/frontend/src/Login/Login.tsx b/frontend/src/Login/Login.tsx index 0aac57e..65b4789 100644 --- a/frontend/src/Login/Login.tsx +++ b/frontend/src/Login/Login.tsx @@ -1,23 +1,28 @@ -import classes from "./Login.module.css"; -import { AuthScreen } from "../AuthScreen/AuthScreen"; -import { TextInput, TextOptions } from "../TextInput/TextInput"; -import { UserCircleIcon } from "@heroicons/react/24/outline"; -import { LockClosedIcon } from "@heroicons/react/24/outline"; -import { useState, FormEvent } from "react"; -import { Link } from "react-router"; -import { errorHandler, AuthError } from "../errorHandler"; +import classes from './Login.module.css'; +import { AuthScreen } from '../AuthScreen/AuthScreen'; +import { TextInput, TextOptions } from '../TextInput/TextInput'; +import { UserCircleIcon } from '@heroicons/react/24/outline'; +import { LockClosedIcon } from '@heroicons/react/24/outline'; +import { useState, FormEvent, useContext } from 'react'; +import { Link } from 'react-router'; +import { errorHandler, AuthError } from '../errorHandler'; +import { UserContext, User } from '../UserContext/UserContext'; export default function LoginPage() { - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); const [error, setError] = useState(undefined); + const [success, setSuccess] = useState(undefined); + const { setUser } = useContext(UserContext); + async function handleSubmit(event: FormEvent) { event.preventDefault(); - const res = await fetch("http://localhost:5180/auth/login", { - method: "POST", + const res = await fetch('http://localhost:5180/auth/login', { + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, + credentials: 'include', body: JSON.stringify({ username, password, @@ -27,8 +32,14 @@ export default function LoginPage() { if (!res.ok) { setError(errorHandler(json.error)); - } else { + } else if (setUser) { setError(undefined); + setSuccess('Logged in successfully! Redirecting...'); + setTimeout(() => { + setUser(json as User); + }, 1000); + } else { + setError(errorHandler("Couldn't update user object.")); } } @@ -64,9 +75,16 @@ export default function LoginPage() { />, ]} buttonText="Log in" - footer={

Forgot Password

} + footer={ +

+ + Forgot Password + +

+ } onSubmit={handleSubmit} error={error} + success={success} />
diff --git a/frontend/src/NavBar/NavBar.tsx b/frontend/src/NavBar/NavBar.tsx index d04398b..da4de4b 100644 --- a/frontend/src/NavBar/NavBar.tsx +++ b/frontend/src/NavBar/NavBar.tsx @@ -31,7 +31,7 @@ function NavBar(props: NavBarProps) { className={({ isActive }) => isActive && view === "calendar" ? classes.active : "" } - to="/timeline?view=calendar" + to="/timeline?view=calendar" > Calendar @@ -52,7 +52,7 @@ function NavBar(props: NavBarProps) { style={{ backgroundImage: `url(${props.profileImage})`, }} - to="/login" + to="/settings" > ); diff --git a/frontend/src/ProtectedRoute/ProtectedRoute.tsx b/frontend/src/ProtectedRoute/ProtectedRoute.tsx new file mode 100644 index 0000000..d9004c3 --- /dev/null +++ b/frontend/src/ProtectedRoute/ProtectedRoute.tsx @@ -0,0 +1,20 @@ +import { ReactNode } from "react"; +import { Navigate } from "react-router"; + +interface ProtectedRouteProps { + isAuthenticated: boolean; + children: ReactNode; + fallback?: ReactNode; +} + +export const ProtectedRoute = (props: ProtectedRouteProps) => { + if (!props.isAuthenticated) { + if (props.fallback) { + return props.fallback; + } else { + return ; + } + } + + return props.children; +}; diff --git a/frontend/src/Settings/SettingsNavbar/SettingsNavbar.tsx b/frontend/src/Settings/SettingsNavbar/SettingsNavbar.tsx index b9ddc15..7c5fbd8 100644 --- a/frontend/src/Settings/SettingsNavbar/SettingsNavbar.tsx +++ b/frontend/src/Settings/SettingsNavbar/SettingsNavbar.tsx @@ -7,12 +7,14 @@ import { } from '@heroicons/react/24/outline'; import classes from './SettingsNavbar.module.css'; import { NavLink, useLocation } from 'react-router'; +import { useContext } from 'react'; +import { User, UserContext } from '../../UserContext/UserContext'; interface Row { icon: React.ReactNode; name: string; to?: string; - onClick?: () => void; + onClick?: (() => void) | ((state: any) => void); } const rows: Row[][] = [ @@ -46,8 +48,18 @@ const rows: Row[][] = [ { icon: , name: 'Log out', - onClick: () => { - alert('hello'); + onClick: async (state: { + setUser: React.Dispatch>; + }) => { + const logout = await fetch('http://localhost:5180/auth/logout', { + method: 'POST', + credentials: 'include', + }); + if (logout.ok) { + state.setUser(null); + } else { + alert('Failed to logout.'); + } }, }, ], @@ -55,6 +67,7 @@ const rows: Row[][] = [ export function SettingsNavbar() { const { pathname } = useLocation(); + const { setUser } = useContext(UserContext); return (