From 996978ea9eca5282a2753779c9e691bff9d23693 Mon Sep 17 00:00:00 2001 From: itsmegood Date: Fri, 23 Feb 2024 08:21:05 +0530 Subject: [PATCH 1/3] init --- app/routes/_auth+/auth.$provider.callback.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/routes/_auth+/auth.$provider.callback.test.ts b/app/routes/_auth+/auth.$provider.callback.test.ts index d21486e..c6b4480 100644 --- a/app/routes/_auth+/auth.$provider.callback.test.ts +++ b/app/routes/_auth+/auth.$provider.callback.test.ts @@ -142,7 +142,6 @@ test('gives an error if the account is already connected to another user', async await prisma.user.create({ data: { ...createUser(), - platformStatusKey: 'ACTIVE', connections: { create: { providerName: GITHUB_PROVIDER_NAME, @@ -247,7 +246,6 @@ async function setupUser(userData = createUser()) { user: { create: { ...userData, - platformStatusKey: 'active', }, }, }, From c4c116b22ff49eb7f28cbc7d7fad6d6af9bd7063 Mon Sep 17 00:00:00 2001 From: itsmegood Date: Fri, 23 Feb 2024 08:25:36 +0530 Subject: [PATCH 2/3] final --- README.md | 50 +- app/entry.server.tsx | 2 +- app/root.tsx | 16 +- app/routes/_auth+/auth.$provider.callback.ts | 9 +- app/routes/_auth+/forgot-password.tsx | 6 +- app/routes/_auth+/login.server.ts | 158 ++++++ app/routes/_auth+/login.tsx | 177 +----- app/routes/_auth+/onboarding.server.ts | 19 + app/routes/_auth+/onboarding.tsx | 19 +- .../_auth+/onboarding_.$provider.server.ts | 19 + app/routes/_auth+/onboarding_.$provider.tsx | 28 +- app/routes/_auth+/reset-password.server.ts | 34 ++ app/routes/_auth+/reset-password.tsx | 62 +-- app/routes/_auth+/signup.tsx | 2 +- app/routes/_auth+/verify.server.ts | 205 +++++++ app/routes/_auth+/verify.tsx | 206 +------ app/routes/_seo+/sitemap[.]xml.ts | 8 +- app/routes/admin+/cache_.lru.$cacheKey.ts | 7 +- app/routes/admin+/cache_.sqlite.$cacheKey.ts | 7 +- app/routes/admin+/cache_.sqlite.server.ts | 29 + app/routes/admin+/cache_.sqlite.tsx | 29 +- .../settings+/profile.change-email.server.tsx | 124 +++++ app/routes/settings+/profile.change-email.tsx | 131 +---- .../settings+/profile.two-factor.disable.tsx | 2 +- .../settings+/profile.two-factor.verify.tsx | 2 +- app/routes/users+/$username.test.tsx | 12 +- .../$username_+/__note-editor.server.tsx | 131 +++++ .../users+/$username_+/__note-editor.tsx | 143 +---- .../$username_+/notes.$noteId_.edit.tsx | 6 +- app/routes/users+/$username_+/notes.new.tsx | 5 +- app/utils/cache.server.ts | 2 +- app/utils/litefs.server.ts | 9 +- package-lock.json | 505 +++++++++--------- package.json | 41 +- .../migrations/20240130113044_/migration.sql | 89 --- server/dev-server.js | 2 +- server/index.ts | 117 ++-- tests/e2e/notes.test.ts | 74 --- tests/e2e/search.test.ts | 26 - tests/playwright-utils.ts | 1 - tsconfig.json | 12 +- types/env.env.d.ts | 2 + types/remix.env.d.ts | 2 - vite.config.ts | 36 ++ 44 files changed, 1202 insertions(+), 1364 deletions(-) create mode 100644 app/routes/_auth+/login.server.ts create mode 100644 app/routes/_auth+/onboarding.server.ts create mode 100644 app/routes/_auth+/onboarding_.$provider.server.ts create mode 100644 app/routes/_auth+/reset-password.server.ts create mode 100644 app/routes/_auth+/verify.server.ts create mode 100644 app/routes/admin+/cache_.sqlite.server.ts create mode 100644 app/routes/settings+/profile.change-email.server.tsx create mode 100644 app/routes/users+/$username_+/__note-editor.server.tsx delete mode 100644 prisma/migrations/20240130113044_/migration.sql delete mode 100644 tests/e2e/notes.test.ts delete mode 100644 tests/e2e/search.test.ts create mode 100644 types/env.env.d.ts delete mode 100644 types/remix.env.d.ts create mode 100644 vite.config.ts diff --git a/README.md b/README.md index 1500cdb..0eeaf96 100644 --- a/README.md +++ b/README.md @@ -29,41 +29,43 @@ To run Book Breeze, you'll need the following: To set up the Book Breeze on your local machine, execute the following steps: ```sh -# Clone the repository -git clone https://github.com/itsmegood/BookBreeze.git -cd BookBreeze - -# Install dependencies -npm install - -# Set up your environment variables -cp .env.local.example .env.local -# Edit .env.local with your database credentials and any other configurations +npx create-epic-app@latest +``` -# Run the development server -npm run dev +[![The Epic Stack](https://github-production-user-asset-6210df.s3.amazonaws.com/1500684/246885449-1b00286c-aa3d-44b2-9ef2-04f694eb3592.png)](https://www.epicweb.dev/epic-stack) -# Open http://localhost:3000 with your browser to see the result. The app should be up and running! -``` +[The Epic Stack](https://www.epicweb.dev/epic-stack) - +## Watch Kent's Introduction to The Epic Stack - +["The Epic Stack" by Kent C. Dodds](https://www.epicweb.dev/talks/the-epic-stack) -## Acknowledgements +## Docs -Built on top of Epic Stack by Kent C. Dodds. +[Read the docs](https://github.com/epicweb-dev/epic-stack/blob/main/docs) +(please 🙏). -## Contact +## Support -For help and support, please join [discord](https://discord.com/invite/rswhkuujJB). For any additional queries, open an issue in the repository, or if you're looking to collaborate, feel free to reach out! +- 🆘 Join the + [discussion on GitHub](https://github.com/epicweb-dev/epic-stack/discussions) + and the [KCD Community on Discord](https://kcd.im/discord). +- 💡 Create an + [idea discussion](https://github.com/epicweb-dev/epic-stack/discussions/new?category=ideas) + for suggestions. +- 🐛 Open a [GitHub issue](https://github.com/epicweb-dev/epic-stack/issues) to + report a bug. -## License +## Branding -Book Breeze is released under the [MIT License](LICENSE). +Want to talk about the Epic Stack in a blog post or talk? Great! Here are some +assets you can use in your material: +[EpicWeb.dev/brand](https://epicweb.dev/brand) +## Thanks +You rock 🪨 diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 827d5f5..4095110 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -8,9 +8,9 @@ import { import { RemixServer } from '@remix-run/react' import * as Sentry from '@sentry/remix' import { isbot } from 'isbot' -import { getInstanceInfo } from 'litefs-js' import { renderToPipeableStream } from 'react-dom/server' import { getEnv, init } from './utils/env.server.ts' +import { getInstanceInfo } from './utils/litefs.server.ts' import { NonceProvider } from './utils/nonce-provider.ts' import { makeTimings } from './utils/timing.server.ts' diff --git a/app/root.tsx b/app/root.tsx index 5b7b5fb..dbcc6bb 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -10,13 +10,12 @@ import { type MetaFunction, } from '@remix-run/node' import { - Links, - LiveReload, + Links, Meta, Outlet, Scripts, ScrollRestoration, - useFetchers, + useFetchers, useLoaderData, useRouteLoaderData, } from '@remix-run/react' @@ -28,7 +27,7 @@ import { EpicProgress } from './components/progress-bar.tsx' import { useToast } from './components/toaster.tsx' import { href as iconsHref } from './components/ui/icon.tsx' import { EpicToaster } from './components/ui/sonner.tsx' -import tailwindStyleSheetUrl from './styles/tailwind.css' +import tailwindStyleSheetUrl from './styles/tailwind.css?url' import { getUserId, logout } from './utils/auth.server.ts' import { ClientHintCheck, getHints, useHints } from './utils/client-hints.tsx' import { prisma } from './utils/db.server.ts' @@ -195,7 +194,6 @@ function Document({ /> - ) @@ -204,8 +202,8 @@ function Document({ function App() { const data = useLoaderData() const nonce = useNonce() - const theme = useTheme() - + const theme = useTheme() + useToast(data.toast) return ( @@ -218,8 +216,8 @@ function App() {
-
- */} + + */} { const user = await prisma.user.findFirst({ @@ -46,7 +45,6 @@ export async function action({ request }: ActionFunctionArgs) { }), async: true, }) - if (submission.status !== 'success') { return json( { result: submission.reply() }, diff --git a/app/routes/_auth+/login.server.ts b/app/routes/_auth+/login.server.ts new file mode 100644 index 0000000..4d4249a --- /dev/null +++ b/app/routes/_auth+/login.server.ts @@ -0,0 +1,158 @@ +import { invariant } from '@epic-web/invariant' +import { redirect } from '@remix-run/node' +import { safeRedirect } from 'remix-utils/safe-redirect' +import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx' +import { getUserId, sessionKey } from '#app/utils/auth.server.ts' +import { prisma } from '#app/utils/db.server.ts' +import { combineResponseInits } from '#app/utils/misc.tsx' +import { authSessionStorage } from '#app/utils/session.server.ts' +import { redirectWithToast } from '#app/utils/toast.server.ts' +import { verifySessionStorage } from '#app/utils/verification.server.ts' +import { getRedirectToUrl, type VerifyFunctionArgs } from './verify.server.ts' + +const verifiedTimeKey = 'verified-time' +const unverifiedSessionIdKey = 'unverified-session-id' +const rememberKey = 'remember' + +export async function handleNewSession( + { + request, + session, + redirectTo, + remember, + }: { + request: Request + session: { userId: string; id: string; expirationDate: Date } + redirectTo?: string + remember: boolean + }, + responseInit?: ResponseInit, +) { + const verification = await prisma.verification.findUnique({ + select: { id: true }, + where: { + target_type: { target: session.userId, type: twoFAVerificationType }, + }, + }) + const userHasTwoFactor = Boolean(verification) + + if (userHasTwoFactor) { + const verifySession = await verifySessionStorage.getSession() + verifySession.set(unverifiedSessionIdKey, session.id) + verifySession.set(rememberKey, remember) + const redirectUrl = getRedirectToUrl({ + request, + type: twoFAVerificationType, + target: session.userId, + redirectTo, + }) + return redirect( + `${redirectUrl.pathname}?${redirectUrl.searchParams}`, + combineResponseInits( + { + headers: { + 'set-cookie': + await verifySessionStorage.commitSession(verifySession), + }, + }, + responseInit, + ), + ) + } else { + const authSession = await authSessionStorage.getSession( + request.headers.get('cookie'), + ) + authSession.set(sessionKey, session.id) + + return redirect( + safeRedirect(redirectTo), + combineResponseInits( + { + headers: { + 'set-cookie': await authSessionStorage.commitSession(authSession, { + expires: remember ? session.expirationDate : undefined, + }), + }, + }, + responseInit, + ), + ) + } +} + +export async function handleVerification({ + request, + submission, +}: VerifyFunctionArgs) { + invariant( + submission.status === 'success', + 'Submission should be successful by now', + ) + const authSession = await authSessionStorage.getSession( + request.headers.get('cookie'), + ) + const verifySession = await verifySessionStorage.getSession( + request.headers.get('cookie'), + ) + + const remember = verifySession.get(rememberKey) + const { redirectTo } = submission.value + const headers = new Headers() + authSession.set(verifiedTimeKey, Date.now()) + + const unverifiedSessionId = verifySession.get(unverifiedSessionIdKey) + if (unverifiedSessionId) { + const session = await prisma.session.findUnique({ + select: { expirationDate: true }, + where: { id: unverifiedSessionId }, + }) + if (!session) { + throw await redirectWithToast('/login', { + type: 'error', + title: 'Invalid session', + description: 'Could not find session to verify. Please try again.', + }) + } + authSession.set(sessionKey, unverifiedSessionId) + + headers.append( + 'set-cookie', + await authSessionStorage.commitSession(authSession, { + expires: remember ? session.expirationDate : undefined, + }), + ) + } else { + headers.append( + 'set-cookie', + await authSessionStorage.commitSession(authSession), + ) + } + + headers.append( + 'set-cookie', + await verifySessionStorage.destroySession(verifySession), + ) + + return redirect(safeRedirect(redirectTo), { headers }) +} + +export async function shouldRequestTwoFA(request: Request) { + const authSession = await authSessionStorage.getSession( + request.headers.get('cookie'), + ) + const verifySession = await verifySessionStorage.getSession( + request.headers.get('cookie'), + ) + if (verifySession.has(unverifiedSessionIdKey)) return true + const userId = await getUserId(request) + if (!userId) return false + // if it's over two hours since they last verified, we should request 2FA again + const userHasTwoFA = await prisma.verification.findUnique({ + select: { id: true }, + where: { target_type: { target: userId, type: twoFAVerificationType } }, + }) + if (!userHasTwoFA) return false + const verifiedTime = authSession.get(verifiedTimeKey) ?? new Date(0) + const twoHours = 1000 * 60 * 2 + return Date.now() - verifiedTime > twoHours +} diff --git a/app/routes/_auth+/login.tsx b/app/routes/_auth+/login.tsx index a800f39..adf5c69 100644 --- a/app/routes/_auth+/login.tsx +++ b/app/routes/_auth+/login.tsx @@ -1,191 +1,27 @@ -import { useForm, getFormProps, getInputProps } from '@conform-to/react' +import { getFormProps, getInputProps, useForm } from '@conform-to/react' import { getZodConstraint, parseWithZod } from '@conform-to/zod' -import { invariant } from '@epic-web/invariant' import { json, - redirect, - type LoaderFunctionArgs, type ActionFunctionArgs, + type LoaderFunctionArgs, type MetaFunction, } from '@remix-run/node' import { Form, Link, useActionData, useSearchParams } from '@remix-run/react' import { HoneypotInputs } from 'remix-utils/honeypot/react' -import { safeRedirect } from 'remix-utils/safe-redirect' import { z } from 'zod' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' import { CheckboxField, ErrorList, Field } from '#app/components/forms.tsx' import { Spacer } from '#app/components/spacer.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx' -import { - getUserId, - login, - requireAnonymous, - sessionKey, -} from '#app/utils/auth.server.ts' +import { login, requireAnonymous } from '#app/utils/auth.server.ts' import { ProviderConnectionForm, providerNames, } from '#app/utils/connections.tsx' -import { prisma } from '#app/utils/db.server.ts' import { checkHoneypot } from '#app/utils/honeypot.server.ts' -import { combineResponseInits, useIsPending } from '#app/utils/misc.tsx' -import { authSessionStorage } from '#app/utils/session.server.ts' -import { redirectWithToast } from '#app/utils/toast.server.ts' -import { - EmailSchema, - PasswordSchema, - // UsernameSchema, -} from '#app/utils/user-validation.ts' -import { verifySessionStorage } from '#app/utils/verification.server.ts' -import { getRedirectToUrl, type VerifyFunctionArgs } from './verify.tsx' - -const verifiedTimeKey = 'verified-time' -const unverifiedSessionIdKey = 'unverified-session-id' -const rememberKey = 'remember' - -export async function handleNewSession( - { - request, - session, - redirectTo, - remember, - }: { - request: Request - session: { userId: string; id: string; expirationDate: Date } - redirectTo?: string - remember: boolean - }, - responseInit?: ResponseInit, -) { - const verification = await prisma.verification.findUnique({ - select: { id: true }, - where: { - target_type: { target: session.userId, type: twoFAVerificationType }, - }, - }) - const userHasTwoFactor = Boolean(verification) - - if (userHasTwoFactor) { - const verifySession = await verifySessionStorage.getSession() - verifySession.set(unverifiedSessionIdKey, session.id) - verifySession.set(rememberKey, remember) - const redirectUrl = getRedirectToUrl({ - request, - type: twoFAVerificationType, - target: session.userId, - redirectTo, - }) - return redirect( - `${redirectUrl.pathname}?${redirectUrl.searchParams}`, - combineResponseInits( - { - headers: { - 'set-cookie': - await verifySessionStorage.commitSession(verifySession), - }, - }, - responseInit, - ), - ) - } else { - const authSession = await authSessionStorage.getSession( - request.headers.get('cookie'), - ) - authSession.set(sessionKey, session.id) - - return redirect( - safeRedirect(redirectTo), - combineResponseInits( - { - headers: { - 'set-cookie': await authSessionStorage.commitSession(authSession, { - expires: remember ? session.expirationDate : undefined, - }), - }, - }, - responseInit, - ), - ) - } -} - -export async function handleVerification({ - request, - submission, -}: VerifyFunctionArgs) { - invariant( - submission.status === 'success', - 'Submission should be successful by now', - ) - const authSession = await authSessionStorage.getSession( - request.headers.get('cookie'), - ) - const verifySession = await verifySessionStorage.getSession( - request.headers.get('cookie'), - ) - - const remember = verifySession.get(rememberKey) - const { redirectTo } = submission.value - const headers = new Headers() - authSession.set(verifiedTimeKey, Date.now()) - - const unverifiedSessionId = verifySession.get(unverifiedSessionIdKey) - if (unverifiedSessionId) { - const session = await prisma.session.findUnique({ - select: { expirationDate: true }, - where: { id: unverifiedSessionId }, - }) - if (!session) { - throw await redirectWithToast('/login', { - type: 'error', - title: 'Invalid session', - description: 'Could not find session to verify. Please try again.', - }) - } - authSession.set(sessionKey, unverifiedSessionId) - - headers.append( - 'set-cookie', - await authSessionStorage.commitSession(authSession, { - expires: remember ? session.expirationDate : undefined, - }), - ) - } else { - headers.append( - 'set-cookie', - await authSessionStorage.commitSession(authSession), - ) - } - - headers.append( - 'set-cookie', - await verifySessionStorage.destroySession(verifySession), - ) - - return redirect(safeRedirect(redirectTo), { headers }) -} - -export async function shouldRequestTwoFA(request: Request) { - const authSession = await authSessionStorage.getSession( - request.headers.get('cookie'), - ) - const verifySession = await verifySessionStorage.getSession( - request.headers.get('cookie'), - ) - if (verifySession.has(unverifiedSessionIdKey)) return true - const userId = await getUserId(request) - if (!userId) return false - // if it's over two hours since they last verified, we should request 2FA again - const userHasTwoFA = await prisma.verification.findUnique({ - select: { id: true }, - where: { target_type: { target: userId, type: twoFAVerificationType } }, - }) - if (!userHasTwoFA) return false - const verifiedTime = authSession.get(verifiedTimeKey) ?? new Date(0) - const twoHours = 1000 * 60 * 2 - return Date.now() - verifiedTime > twoHours -} +import { useIsPending } from '#app/utils/misc.tsx' +import { EmailSchema, PasswordSchema} from '#app/utils/user-validation.ts' +import { handleNewSession } from './login.server.ts' const LoginFormSchema = z.object({ // username: UsernameSchema, @@ -204,7 +40,6 @@ export async function action({ request }: ActionFunctionArgs) { await requireAnonymous(request) const formData = await request.formData() checkHoneypot(formData) - const submission = await parseWithZod(formData, { schema: intent => LoginFormSchema.transform(async (data, ctx) => { diff --git a/app/routes/_auth+/onboarding.server.ts b/app/routes/_auth+/onboarding.server.ts new file mode 100644 index 0000000..826a72a --- /dev/null +++ b/app/routes/_auth+/onboarding.server.ts @@ -0,0 +1,19 @@ +import { invariant } from '@epic-web/invariant' +import { redirect } from '@remix-run/node' +import { verifySessionStorage } from '#app/utils/verification.server.ts' +import { onboardingEmailSessionKey } from './onboarding.tsx' +import { type VerifyFunctionArgs } from './verify.server.ts' + +export async function handleVerification({ submission }: VerifyFunctionArgs) { + invariant( + submission.status === 'success', + 'Submission should be successful by now', + ) + const verifySession = await verifySessionStorage.getSession() + verifySession.set(onboardingEmailSessionKey, submission.value.target) + return redirect('/onboarding', { + headers: { + 'set-cookie': await verifySessionStorage.commitSession(verifySession), + }, + }) +} diff --git a/app/routes/_auth+/onboarding.tsx b/app/routes/_auth+/onboarding.tsx index 6752145..786cec5 100644 --- a/app/routes/_auth+/onboarding.tsx +++ b/app/routes/_auth+/onboarding.tsx @@ -1,6 +1,5 @@ import { getFormProps, getInputProps, useForm } from '@conform-to/react' import { getZodConstraint, parseWithZod } from '@conform-to/zod' -import { invariant } from '@epic-web/invariant' import { json, redirect, @@ -32,9 +31,8 @@ import { UsernameSchema, } from '#app/utils/user-validation.ts' import { verifySessionStorage } from '#app/utils/verification.server.ts' -import { type VerifyFunctionArgs } from './verify.tsx' -const onboardingEmailSessionKey = 'onboardingEmail' +export const onboardingEmailSessionKey = 'onboardingEmail' const SignupFormSchema = z .object({ @@ -60,6 +58,7 @@ async function requireOnboardingEmail(request: Request) { } return email } + export async function loader({ request }: LoaderFunctionArgs) { const email = await requireOnboardingEmail(request) return json({ email }) @@ -126,20 +125,6 @@ export async function action({ request }: ActionFunctionArgs) { ) } -export async function handleVerification({ submission }: VerifyFunctionArgs) { - invariant( - submission.status === 'success', - 'Submission should be successful by now', - ) - const verifySession = await verifySessionStorage.getSession() - verifySession.set(onboardingEmailSessionKey, submission.value.target) - return redirect('/onboarding', { - headers: { - 'set-cookie': await verifySessionStorage.commitSession(verifySession), - }, - }) -} - export const meta: MetaFunction = () => { return [{ title: 'Setup Epic Notes Account' }] } diff --git a/app/routes/_auth+/onboarding_.$provider.server.ts b/app/routes/_auth+/onboarding_.$provider.server.ts new file mode 100644 index 0000000..826a72a --- /dev/null +++ b/app/routes/_auth+/onboarding_.$provider.server.ts @@ -0,0 +1,19 @@ +import { invariant } from '@epic-web/invariant' +import { redirect } from '@remix-run/node' +import { verifySessionStorage } from '#app/utils/verification.server.ts' +import { onboardingEmailSessionKey } from './onboarding.tsx' +import { type VerifyFunctionArgs } from './verify.server.ts' + +export async function handleVerification({ submission }: VerifyFunctionArgs) { + invariant( + submission.status === 'success', + 'Submission should be successful by now', + ) + const verifySession = await verifySessionStorage.getSession() + verifySession.set(onboardingEmailSessionKey, submission.value.target) + return redirect('/onboarding', { + headers: { + 'set-cookie': await verifySessionStorage.commitSession(verifySession), + }, + }) +} diff --git a/app/routes/_auth+/onboarding_.$provider.tsx b/app/routes/_auth+/onboarding_.$provider.tsx index be4df71..411ba8a 100644 --- a/app/routes/_auth+/onboarding_.$provider.tsx +++ b/app/routes/_auth+/onboarding_.$provider.tsx @@ -1,24 +1,23 @@ import { - type SubmissionResult, getFormProps, getInputProps, useForm, + type SubmissionResult, } from '@conform-to/react' import { getZodConstraint, parseWithZod } from '@conform-to/zod' -import { invariant } from '@epic-web/invariant' import { - json, redirect, - type LoaderFunctionArgs, + json, type ActionFunctionArgs, + type LoaderFunctionArgs, type MetaFunction, } from '@remix-run/node' import { + type Params, Form, useActionData, useLoaderData, useSearchParams, - type Params, } from '@remix-run/react' import { safeRedirect } from 'remix-utils/safe-redirect' import { z } from 'zod' @@ -27,9 +26,9 @@ import { Spacer } from '#app/components/spacer.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' import { authenticator, - requireAnonymous, sessionKey, signupWithConnection, + requireAnonymous, } from '#app/utils/auth.server.ts' import { ProviderNameSchema } from '#app/utils/connections.tsx' import { prisma } from '#app/utils/db.server.ts' @@ -38,9 +37,8 @@ import { authSessionStorage } from '#app/utils/session.server.ts' import { redirectWithToast } from '#app/utils/toast.server.ts' import { NameSchema, UsernameSchema } from '#app/utils/user-validation.ts' import { verifySessionStorage } from '#app/utils/verification.server.ts' -import { type VerifyFunctionArgs } from './verify.tsx' +import { onboardingEmailSessionKey } from './onboarding' -export const onboardingEmailSessionKey = 'onboardingEmail' export const providerIdKey = 'providerId' export const prefilledProfileKey = 'prefilledProfile' @@ -176,20 +174,6 @@ export async function action({ request, params }: ActionFunctionArgs) { ) } -export async function handleVerification({ submission }: VerifyFunctionArgs) { - invariant( - submission.status === 'success', - 'Submission should be successful by now', - ) - const verifySession = await verifySessionStorage.getSession() - verifySession.set(onboardingEmailSessionKey, submission.value.target) - return redirect('/onboarding', { - headers: { - 'set-cookie': await verifySessionStorage.commitSession(verifySession), - }, - }) -} - export const meta: MetaFunction = () => { return [{ title: 'Setup Epic Notes Account' }] } diff --git a/app/routes/_auth+/reset-password.server.ts b/app/routes/_auth+/reset-password.server.ts new file mode 100644 index 0000000..1c25b4d --- /dev/null +++ b/app/routes/_auth+/reset-password.server.ts @@ -0,0 +1,34 @@ +import { invariant } from '@epic-web/invariant' +import { json, redirect } from '@remix-run/node' +import { prisma } from '#app/utils/db.server.ts' +import { verifySessionStorage } from '#app/utils/verification.server.ts' +import { resetPasswordUsernameSessionKey } from './reset-password.tsx' +import { type VerifyFunctionArgs } from './verify.server.ts' + +export async function handleVerification({ submission }: VerifyFunctionArgs) { + invariant( + submission.status === 'success', + 'Submission should be successful by now', + ) + const target = submission.value.target + const user = await prisma.user.findFirst({ + where: { OR: [{ email: target }, { username: target }] }, + select: { email: true, username: true }, + }) + // we don't want to say the user is not found if the email is not found + // because that would allow an attacker to check if an email is registered + if (!user) { + return json( + { result: submission.reply({ fieldErrors: { code: ['Invalid code'] } }) }, + { status: 400 }, + ) + } + + const verifySession = await verifySessionStorage.getSession() + verifySession.set(resetPasswordUsernameSessionKey, user.username) + return redirect('/reset-password', { + headers: { + 'set-cookie': await verifySessionStorage.commitSession(verifySession), + }, + }) +} diff --git a/app/routes/_auth+/reset-password.tsx b/app/routes/_auth+/reset-password.tsx index 932eb55..9a9b425 100644 --- a/app/routes/_auth+/reset-password.tsx +++ b/app/routes/_auth+/reset-password.tsx @@ -1,80 +1,46 @@ import { getFormProps, getInputProps, useForm } from '@conform-to/react' import { getZodConstraint, parseWithZod } from '@conform-to/zod' -import { invariant } from '@epic-web/invariant' import { json, redirect, - type LoaderFunctionArgs, type ActionFunctionArgs, + type LoaderFunctionArgs, type MetaFunction, } from '@remix-run/node' import { Form, useActionData, useLoaderData } from '@remix-run/react' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' import { ErrorList, Field } from '#app/components/forms.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { requireAnonymous, resetEmailPassword } from '#app/utils/auth.server.ts' -import { prisma } from '#app/utils/db.server.ts' +import { requireAnonymous, resetUserPassword } from '#app/utils/auth.server.ts' import { useIsPending } from '#app/utils/misc.tsx' import { PasswordAndConfirmPasswordSchema } from '#app/utils/user-validation.ts' import { verifySessionStorage } from '#app/utils/verification.server.ts' -import { type VerifyFunctionArgs } from './verify.tsx' -const resetPasswordEmailSessionKey = 'resetPasswordEmail' - -export async function handleVerification({ submission }: VerifyFunctionArgs) { - invariant( - submission.status === 'success', - 'Submission should be successful by now', - ) - const target = submission.value.target - const user = await prisma.user.findFirst({ - // where: { OR: [{ email: target }, { username: target }] }, - where: { email: target }, - select: { email: true }, - }) - // we don't want to say the user is not found if the email is not found - // because that would allow an attacker to check if an email is registered - if (!user) { - return json( - { - result: submission.reply({ fieldErrors: { code: ['Invalid code'] } }), - }, - { - status: 400, - }, - ) - } - - const verifySession = await verifySessionStorage.getSession() - verifySession.set(resetPasswordEmailSessionKey, user.email) - return redirect('/reset-password', { - headers: { - 'set-cookie': await verifySessionStorage.commitSession(verifySession), - }, - }) -} +export const resetPasswordUsernameSessionKey = 'resetPasswordUsername' const ResetPasswordSchema = PasswordAndConfirmPasswordSchema -async function requireResetPasswordEmail(request: Request) { +async function requireResetPasswordUsername(request: Request) { await requireAnonymous(request) const verifySession = await verifySessionStorage.getSession( request.headers.get('cookie'), ) - const resetPasswordEmail = verifySession.get(resetPasswordEmailSessionKey) - if (typeof resetPasswordEmail !== 'string' || !resetPasswordEmail) { + const resetPasswordUsername = verifySession.get( + resetPasswordUsernameSessionKey, + ) + if (typeof resetPasswordUsername !== 'string' || !resetPasswordUsername) { throw redirect('/login') } - return resetPasswordEmail + return resetPasswordUsername } export async function loader({ request }: LoaderFunctionArgs) { - const resetPasswordEmail = await requireResetPasswordEmail(request) - return json({ resetPasswordEmail }) + const resetPasswordUsername = await requireResetPasswordUsername(request) + return json({ resetPasswordUsername }) } export async function action({ request }: ActionFunctionArgs) { - const resetPasswordEmail = await requireResetPasswordEmail(request) + const resetPasswordUsername = await requireResetPasswordUsername(request) const formData = await request.formData() const submission = parseWithZod(formData, { schema: ResetPasswordSchema, @@ -87,7 +53,7 @@ export async function action({ request }: ActionFunctionArgs) { } const { password } = submission.value - await resetEmailPassword({ email: resetPasswordEmail, password }) + await resetUserPassword({ username: resetPasswordUsername, password }) const verifySession = await verifySessionStorage.getSession() return redirect('/login', { headers: { @@ -120,7 +86,7 @@ export default function ResetPasswordPage() {

Password Reset

- Hi, {data.resetPasswordEmail}. No worries. It happens all the time. + Hi, {data.resetPasswordUsername}. No worries. It happens all the time.

diff --git a/app/routes/_auth+/signup.tsx b/app/routes/_auth+/signup.tsx index 0303877..14c71c3 100644 --- a/app/routes/_auth+/signup.tsx +++ b/app/routes/_auth+/signup.tsx @@ -22,7 +22,7 @@ import { sendEmail } from '#app/utils/email.server.ts' import { checkHoneypot } from '#app/utils/honeypot.server.ts' import { useIsPending } from '#app/utils/misc.tsx' import { EmailSchema } from '#app/utils/user-validation.ts' -import { prepareVerification } from './verify.tsx' +import { prepareVerification } from './verify.server.ts' const SignupSchema = z.object({ email: EmailSchema, diff --git a/app/routes/_auth+/verify.server.ts b/app/routes/_auth+/verify.server.ts new file mode 100644 index 0000000..c7f53fa --- /dev/null +++ b/app/routes/_auth+/verify.server.ts @@ -0,0 +1,205 @@ +import { type Submission } from '@conform-to/react' +import { parseWithZod } from '@conform-to/zod' +import { json } from '@remix-run/node' +import { z } from 'zod' +import { handleVerification as handleChangeEmailVerification } from '#app/routes/settings+/profile.change-email.server.tsx' +import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx' +import { requireUserId } from '#app/utils/auth.server.ts' +import { prisma } from '#app/utils/db.server.ts' +import { ensurePrimary } from '#app/utils/litefs.server.ts' +import { getDomainUrl } from '#app/utils/misc.tsx' +import { redirectWithToast } from '#app/utils/toast.server.ts' +import { generateTOTP, verifyTOTP } from '#app/utils/totp.server.ts' +import { type twoFAVerifyVerificationType } from '../settings+/profile.two-factor.verify.tsx' +import { + handleVerification as handleLoginTwoFactorVerification, + shouldRequestTwoFA, +} from './login.server.ts' +import { handleVerification as handleOnboardingVerification } from './onboarding.server.ts' +import { handleVerification as handleResetPasswordVerification } from './reset-password.server.ts' +import { + VerifySchema, + codeQueryParam, + redirectToQueryParam, + targetQueryParam, + typeQueryParam, + type VerificationTypes, +} from './verify.tsx' + +export type VerifyFunctionArgs = { + request: Request + submission: Submission< + z.input, + string[], + z.output + > + body: FormData | URLSearchParams +} + +export function getRedirectToUrl({ + request, + type, + target, + redirectTo, +}: { + request: Request + type: VerificationTypes + target: string + redirectTo?: string +}) { + const redirectToUrl = new URL(`${getDomainUrl(request)}/verify`) + redirectToUrl.searchParams.set(typeQueryParam, type) + redirectToUrl.searchParams.set(targetQueryParam, target) + if (redirectTo) { + redirectToUrl.searchParams.set(redirectToQueryParam, redirectTo) + } + return redirectToUrl +} + +export async function requireRecentVerification(request: Request) { + const userId = await requireUserId(request) + const shouldReverify = await shouldRequestTwoFA(request) + if (shouldReverify) { + const reqUrl = new URL(request.url) + const redirectUrl = getRedirectToUrl({ + request, + target: userId, + type: twoFAVerificationType, + redirectTo: reqUrl.pathname + reqUrl.search, + }) + throw await redirectWithToast(redirectUrl.toString(), { + title: 'Please Reverify', + description: 'Please reverify your account before proceeding', + }) + } +} + +export async function prepareVerification({ + period, + request, + type, + target, +}: { + period: number + request: Request + type: VerificationTypes + target: string +}) { + const verifyUrl = getRedirectToUrl({ request, type, target }) + const redirectTo = new URL(verifyUrl.toString()) + + const { otp, ...verificationConfig } = generateTOTP({ + algorithm: 'SHA256', + // Leaving off 0 and O on purpose to avoid confusing users. + charSet: 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789', + period, + }) + const verificationData = { + type, + target, + ...verificationConfig, + expiresAt: new Date(Date.now() + verificationConfig.period * 1000), + } + await prisma.verification.upsert({ + where: { target_type: { target, type } }, + create: verificationData, + update: verificationData, + }) + + // add the otp to the url we'll email the user. + verifyUrl.searchParams.set(codeQueryParam, otp) + + return { otp, redirectTo, verifyUrl } +} + +export async function isCodeValid({ + code, + type, + target, +}: { + code: string + type: VerificationTypes | typeof twoFAVerifyVerificationType + target: string +}) { + const verification = await prisma.verification.findUnique({ + where: { + target_type: { target, type }, + OR: [{ expiresAt: { gt: new Date() } }, { expiresAt: null }], + }, + select: { algorithm: true, secret: true, period: true, charSet: true }, + }) + if (!verification) return false + const result = verifyTOTP({ + otp: code, + ...verification, + }) + if (!result) return false + + return true +} + +export async function validateRequest( + request: Request, + body: URLSearchParams | FormData, +) { + const submission = await parseWithZod(body, { + schema: VerifySchema.superRefine(async (data, ctx) => { + const codeIsValid = await isCodeValid({ + code: data[codeQueryParam], + type: data[typeQueryParam], + target: data[targetQueryParam], + }) + if (!codeIsValid) { + ctx.addIssue({ + path: ['code'], + code: z.ZodIssueCode.custom, + message: `Invalid code`, + }) + return + } + }), + async: true, + }) + + if (submission.status !== 'success') { + return json( + { result: submission.reply() }, + { status: submission.status === 'error' ? 400 : 200 }, + ) + } + + // this code path could be part of a loader (GET request), so we need to make + // sure we're running on primary because we're about to make writes. + await ensurePrimary() + + const { value: submissionValue } = submission + + async function deleteVerification() { + await prisma.verification.delete({ + where: { + target_type: { + type: submissionValue[typeQueryParam], + target: submissionValue[targetQueryParam], + }, + }, + }) + } + + switch (submissionValue[typeQueryParam]) { + case 'reset-password': { + await deleteVerification() + return handleResetPasswordVerification({ request, body, submission }) + } + case 'onboarding': { + await deleteVerification() + return handleOnboardingVerification({ request, body, submission }) + } + case 'change-email': { + await deleteVerification() + return handleChangeEmailVerification({ request, body, submission }) + } + case '2fa': { + return handleLoginTwoFactorVerification({ request, body, submission }) + } + } +} diff --git a/app/routes/_auth+/verify.tsx b/app/routes/_auth+/verify.tsx index 0612822..287a01c 100644 --- a/app/routes/_auth+/verify.tsx +++ b/app/routes/_auth+/verify.tsx @@ -1,11 +1,6 @@ -import { - useForm, - type Submission, - getFormProps, - getInputProps, -} from '@conform-to/react' +import { getFormProps, getInputProps, useForm } from '@conform-to/react' import { getZodConstraint, parseWithZod } from '@conform-to/zod' -import { json, type ActionFunctionArgs } from '@remix-run/node' +import { type ActionFunctionArgs } from '@remix-run/node' import { Form, useActionData, useSearchParams } from '@remix-run/react' import { HoneypotInputs } from 'remix-utils/honeypot/react' import { z } from 'zod' @@ -13,22 +8,9 @@ import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' import { ErrorList, Field } from '#app/components/forms.tsx' import { Spacer } from '#app/components/spacer.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { handleVerification as handleChangeEmailVerification } from '#app/routes/settings+/profile.change-email.tsx' -import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx' -import { type twoFAVerifyVerificationType } from '#app/routes/settings+/profile.two-factor.verify.tsx' -import { requireUserId } from '#app/utils/auth.server.ts' -import { prisma } from '#app/utils/db.server.ts' import { checkHoneypot } from '#app/utils/honeypot.server.ts' -import { ensurePrimary } from '#app/utils/litefs.server.ts' -import { getDomainUrl, useIsPending } from '#app/utils/misc.tsx' -import { redirectWithToast } from '#app/utils/toast.server.ts' -import { generateTOTP, verifyTOTP } from '#app/utils/totp.server.ts' -import { - handleVerification as handleLoginTwoFactorVerification, - shouldRequestTwoFA, -} from './login.tsx' -import { handleVerification as handleOnboardingVerification } from './onboarding.tsx' -import { handleVerification as handleResetPasswordVerification } from './reset-password.tsx' +import { useIsPending } from '#app/utils/misc.tsx' +import { validateRequest } from './verify.server.ts' export const codeQueryParam = 'code' export const targetQueryParam = 'target' @@ -38,7 +20,7 @@ const types = ['onboarding', 'reset-password', 'change-email', '2fa'] as const const VerificationTypeSchema = z.enum(types) export type VerificationTypes = z.infer -const VerifySchema = z.object({ +export const VerifySchema = z.object({ [codeQueryParam]: z.string().min(6).max(6), [typeQueryParam]: VerificationTypeSchema, [targetQueryParam]: z.string(), @@ -51,184 +33,6 @@ export async function action({ request }: ActionFunctionArgs) { return validateRequest(request, formData) } -export function getRedirectToUrl({ - request, - type, - target, - redirectTo, -}: { - request: Request - type: VerificationTypes - target: string - redirectTo?: string -}) { - const redirectToUrl = new URL(`${getDomainUrl(request)}/verify`) - redirectToUrl.searchParams.set(typeQueryParam, type) - redirectToUrl.searchParams.set(targetQueryParam, target) - if (redirectTo) { - redirectToUrl.searchParams.set(redirectToQueryParam, redirectTo) - } - return redirectToUrl -} - -export async function requireRecentVerification(request: Request) { - const userId = await requireUserId(request) - const shouldReverify = await shouldRequestTwoFA(request) - if (shouldReverify) { - const reqUrl = new URL(request.url) - const redirectUrl = getRedirectToUrl({ - request, - target: userId, - type: twoFAVerificationType, - redirectTo: reqUrl.pathname + reqUrl.search, - }) - throw await redirectWithToast(redirectUrl.toString(), { - title: 'Please Reverify', - description: 'Please reverify your account before proceeding', - }) - } -} - -export async function prepareVerification({ - period, - request, - type, - target, -}: { - period: number - request: Request - type: VerificationTypes - target: string -}) { - const verifyUrl = getRedirectToUrl({ request, type, target }) - const redirectTo = new URL(verifyUrl.toString()) - - const { otp, ...verificationConfig } = generateTOTP({ - algorithm: 'SHA256', - // Leaving off 0 and O on purpose to avoid confusing users. - charSet: 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789', - period, - }) - const verificationData = { - type, - target, - ...verificationConfig, - expiresAt: new Date(Date.now() + verificationConfig.period * 1000), - } - await prisma.verification.upsert({ - where: { target_type: { target, type } }, - create: verificationData, - update: verificationData, - }) - - // add the otp to the url we'll email the user. - verifyUrl.searchParams.set(codeQueryParam, otp) - - return { otp, redirectTo, verifyUrl } -} - -export type VerifyFunctionArgs = { - request: Request - submission: Submission< - z.input, - string[], - z.output - > - body: FormData | URLSearchParams -} - -export async function isCodeValid({ - code, - type, - target, -}: { - code: string - type: VerificationTypes | typeof twoFAVerifyVerificationType - target: string -}) { - const verification = await prisma.verification.findUnique({ - where: { - target_type: { target, type }, - OR: [{ expiresAt: { gt: new Date() } }, { expiresAt: null }], - }, - select: { algorithm: true, secret: true, period: true, charSet: true }, - }) - if (!verification) return false - const result = verifyTOTP({ - otp: code, - ...verification, - }) - if (!result) return false - - return true -} - -async function validateRequest( - request: Request, - body: URLSearchParams | FormData, -) { - const submission = await parseWithZod(body, { - schema: VerifySchema.superRefine(async (data, ctx) => { - const codeIsValid = await isCodeValid({ - code: data[codeQueryParam], - type: data[typeQueryParam], - target: data[targetQueryParam], - }) - if (!codeIsValid) { - ctx.addIssue({ - path: ['code'], - code: z.ZodIssueCode.custom, - message: `Invalid code`, - }) - return - } - }), - async: true, - }) - - if (submission.status !== 'success') { - return json( - { result: submission.reply() }, - { status: submission.status === 'error' ? 400 : 200 }, - ) - } - - // this code path could be part of a loader (GET request), so we need to make - // sure we're running on primary because we're about to make writes. - await ensurePrimary() - - const { value: submissionValue } = submission - - async function deleteVerification() { - await prisma.verification.delete({ - where: { - target_type: { - type: submissionValue[typeQueryParam], - target: submissionValue[targetQueryParam], - }, - }, - }) - } - - switch (submissionValue[typeQueryParam]) { - case 'reset-password': { - await deleteVerification() - return handleResetPasswordVerification({ request, body, submission }) - } - case 'onboarding': { - await deleteVerification() - return handleOnboardingVerification({ request, body, submission }) - } - case 'change-email': { - await deleteVerification() - return handleChangeEmailVerification({ request, body, submission }) - } - case '2fa': { - return handleLoginTwoFactorVerification({ request, body, submission }) - } - } -} - export default function VerifyRoute() { const [searchParams] = useSearchParams() const isPending = useIsPending() diff --git a/app/routes/_seo+/sitemap[.]xml.ts b/app/routes/_seo+/sitemap[.]xml.ts index bd553e0..22721c3 100644 --- a/app/routes/_seo+/sitemap[.]xml.ts +++ b/app/routes/_seo+/sitemap[.]xml.ts @@ -1,10 +1,10 @@ import { generateSitemap } from '@nasa-gcn/remix-seo' -import { routes } from '@remix-run/dev/server-build' -import { type LoaderFunctionArgs } from '@remix-run/node' +import { type ServerBuild, type LoaderFunctionArgs } from '@remix-run/node' import { getDomainUrl } from '#app/utils/misc.tsx' -export function loader({ request }: LoaderFunctionArgs) { - return generateSitemap(request, routes, { +export async function loader({ request, context }: LoaderFunctionArgs) { + const serverBuild = (await context.serverBuild) as ServerBuild + return generateSitemap(request, serverBuild.routes, { siteUrl: getDomainUrl(request), headers: { 'Cache-Control': `public, max-age=${60 * 5}`, diff --git a/app/routes/admin+/cache_.lru.$cacheKey.ts b/app/routes/admin+/cache_.lru.$cacheKey.ts index 5083fd9..2b793d0 100644 --- a/app/routes/admin+/cache_.lru.$cacheKey.ts +++ b/app/routes/admin+/cache_.lru.$cacheKey.ts @@ -1,8 +1,11 @@ import { invariantResponse } from '@epic-web/invariant' import { json, type LoaderFunctionArgs } from '@remix-run/node' -import { getAllInstances, getInstanceInfo } from 'litefs-js' -import { ensureInstance } from 'litefs-js/remix.js' import { lruCache } from '#app/utils/cache.server.ts' +import { + getAllInstances, + getInstanceInfo, + ensureInstance, +} from '#app/utils/litefs.server.ts' import { requireUserWithRole } from '#app/utils/permissions.server.ts' export async function loader({ request, params }: LoaderFunctionArgs) { diff --git a/app/routes/admin+/cache_.sqlite.$cacheKey.ts b/app/routes/admin+/cache_.sqlite.$cacheKey.ts index bd1ae37..39bcb07 100644 --- a/app/routes/admin+/cache_.sqlite.$cacheKey.ts +++ b/app/routes/admin+/cache_.sqlite.$cacheKey.ts @@ -1,8 +1,11 @@ import { invariantResponse } from '@epic-web/invariant' import { json, type LoaderFunctionArgs } from '@remix-run/node' -import { getAllInstances, getInstanceInfo } from 'litefs-js' -import { ensureInstance } from 'litefs-js/remix.js' import { cache } from '#app/utils/cache.server.ts' +import { + getAllInstances, + getInstanceInfo, + ensureInstance, +} from '#app/utils/litefs.server.ts' import { requireUserWithRole } from '#app/utils/permissions.server.ts' export async function loader({ request, params }: LoaderFunctionArgs) { diff --git a/app/routes/admin+/cache_.sqlite.server.ts b/app/routes/admin+/cache_.sqlite.server.ts new file mode 100644 index 0000000..314f930 --- /dev/null +++ b/app/routes/admin+/cache_.sqlite.server.ts @@ -0,0 +1,29 @@ +import { + getInstanceInfo, + getInternalInstanceDomain, +} from '#app/utils/litefs.server' + +export async function updatePrimaryCacheValue({ + key, + cacheValue, +}: { + key: string + cacheValue: any +}) { + const { currentIsPrimary, primaryInstance } = await getInstanceInfo() + if (currentIsPrimary) { + throw new Error( + `updatePrimaryCacheValue should not be called on the primary instance (${primaryInstance})}`, + ) + } + const domain = getInternalInstanceDomain(primaryInstance) + const token = process.env.INTERNAL_COMMAND_TOKEN + return fetch(`${domain}/admin/cache/sqlite`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ key, cacheValue }), + }) +} diff --git a/app/routes/admin+/cache_.sqlite.tsx b/app/routes/admin+/cache_.sqlite.tsx index 6eef013..68b48ac 100644 --- a/app/routes/admin+/cache_.sqlite.tsx +++ b/app/routes/admin+/cache_.sqlite.tsx @@ -1,7 +1,7 @@ -import { type ActionFunctionArgs, json, redirect } from '@remix-run/node' -import { getInstanceInfo, getInternalInstanceDomain } from 'litefs-js' +import { json, redirect, type ActionFunctionArgs } from '@remix-run/node' import { z } from 'zod' import { cache } from '#app/utils/cache.server.ts' +import { getInstanceInfo } from '#app/utils/litefs.server' export async function action({ request }: ActionFunctionArgs) { const { currentIsPrimary, primaryInstance } = await getInstanceInfo() @@ -28,28 +28,3 @@ export async function action({ request }: ActionFunctionArgs) { } return json({ success: true }) } - -export async function updatePrimaryCacheValue({ - key, - cacheValue, -}: { - key: string - cacheValue: any -}) { - const { currentIsPrimary, primaryInstance } = await getInstanceInfo() - if (currentIsPrimary) { - throw new Error( - `updatePrimaryCacheValue should not be called on the primary instance (${primaryInstance})}`, - ) - } - const domain = getInternalInstanceDomain(primaryInstance) - const token = process.env.INTERNAL_COMMAND_TOKEN - return fetch(`${domain}/admin/cache/sqlite`, { - method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ key, cacheValue }), - }) -} diff --git a/app/routes/settings+/profile.change-email.server.tsx b/app/routes/settings+/profile.change-email.server.tsx new file mode 100644 index 0000000..0a0eebc --- /dev/null +++ b/app/routes/settings+/profile.change-email.server.tsx @@ -0,0 +1,124 @@ +import { invariant } from '@epic-web/invariant' +import * as E from '@react-email/components' +import { json } from '@remix-run/node' +import { + requireRecentVerification, + type VerifyFunctionArgs, +} from '#app/routes/_auth+/verify.server.ts' +import { prisma } from '#app/utils/db.server.ts' +import { sendEmail } from '#app/utils/email.server.ts' +import { redirectWithToast } from '#app/utils/toast.server.ts' +import { verifySessionStorage } from '#app/utils/verification.server.ts' +import { newEmailAddressSessionKey } from './profile.change-email' + +export async function handleVerification({ + request, + submission, +}: VerifyFunctionArgs) { + await requireRecentVerification(request) + invariant( + submission.status === 'success', + 'Submission should be successful by now', + ) + + const verifySession = await verifySessionStorage.getSession( + request.headers.get('cookie'), + ) + const newEmail = verifySession.get(newEmailAddressSessionKey) + if (!newEmail) { + return json( + { + result: submission.reply({ + formErrors: [ + 'You must submit the code on the same device that requested the email change.', + ], + }), + }, + { status: 400 }, + ) + } + const preUpdateUser = await prisma.user.findFirstOrThrow({ + select: { email: true }, + where: { id: submission.value.target }, + }) + const user = await prisma.user.update({ + where: { id: submission.value.target }, + select: { id: true, email: true, username: true }, + data: { email: newEmail }, + }) + + void sendEmail({ + to: preUpdateUser.email, + subject: 'Epic Stack email changed', + react: , + }) + + return redirectWithToast( + '/settings/profile', + { + title: 'Email Changed', + type: 'success', + description: `Your email has been changed to ${user.email}`, + }, + { + headers: { + 'set-cookie': await verifySessionStorage.destroySession(verifySession), + }, + }, + ) +} + +export function EmailChangeEmail({ + verifyUrl, + otp, +}: { + verifyUrl: string + otp: string +}) { + return ( + + +

+ Epic Notes Email Change +

+

+ + Here's your verification code: {otp} + +

+

+ Or click the link: +

+ {verifyUrl} +
+
+ ) +} + +function EmailChangeNoticeEmail({ userId }: { userId: string }) { + return ( + + +

+ Your Epic Notes email has been changed +

+

+ + We're writing to let you know that your Epic Notes email has been + changed. + +

+

+ + If you changed your email address, then you can safely ignore this. + But if you did not change your email address, then please contact + support immediately. + +

+

+ Your Account ID: {userId} +

+
+
+ ) +} diff --git a/app/routes/settings+/profile.change-email.tsx b/app/routes/settings+/profile.change-email.tsx index 03db9f4..b666fd9 100644 --- a/app/routes/settings+/profile.change-email.tsx +++ b/app/routes/settings+/profile.change-email.tsx @@ -1,13 +1,11 @@ import { getFormProps, getInputProps, useForm } from '@conform-to/react' import { getZodConstraint, parseWithZod } from '@conform-to/zod' -import { invariant } from '@epic-web/invariant' import { type SEOHandle } from '@nasa-gcn/remix-seo' -import * as E from '@react-email/components' import { json, redirect, - type LoaderFunctionArgs, type ActionFunctionArgs, + type LoaderFunctionArgs, } from '@remix-run/node' import { Form, useActionData, useLoaderData } from '@remix-run/react' import { z } from 'zod' @@ -17,15 +15,14 @@ import { StatusButton } from '#app/components/ui/status-button.tsx' import { prepareVerification, requireRecentVerification, - type VerifyFunctionArgs, -} from '#app/routes/_auth+/verify.tsx' +} from '#app/routes/_auth+/verify.server.ts' import { requireUserId } from '#app/utils/auth.server.ts' import { prisma } from '#app/utils/db.server.ts' import { sendEmail } from '#app/utils/email.server.ts' import { useIsPending } from '#app/utils/misc.tsx' -import { redirectWithToast } from '#app/utils/toast.server.ts' import { EmailSchema } from '#app/utils/user-validation.ts' import { verifySessionStorage } from '#app/utils/verification.server.ts' +import { EmailChangeEmail } from './profile.change-email.server.tsx' import { type BreadcrumbHandle } from './profile.tsx' export const handle: BreadcrumbHandle & SEOHandle = { @@ -33,64 +30,7 @@ export const handle: BreadcrumbHandle & SEOHandle = { getSitemapEntries: () => null, } -const newEmailAddressSessionKey = 'new-email-address' - -export async function handleVerification({ - request, - submission, -}: VerifyFunctionArgs) { - await requireRecentVerification(request) - invariant( - submission.status === 'success', - 'Submission should be successful by now', - ) - - const verifySession = await verifySessionStorage.getSession( - request.headers.get('cookie'), - ) - const newEmail = verifySession.get(newEmailAddressSessionKey) - if (!newEmail) { - return json( - { - result: submission.reply({ - formErrors: [ - 'You must submit the code on the same device that requested the email change.', - ], - }), - }, - { status: 400 }, - ) - } - const preUpdateUser = await prisma.user.findFirstOrThrow({ - select: { email: true }, - where: { id: submission.value.target }, - }) - const user = await prisma.user.update({ - where: { id: submission.value.target }, - select: { id: true, email: true, username: true }, - data: { email: newEmail }, - }) - - void sendEmail({ - to: preUpdateUser.email, - subject: 'Epic Stack email changed', - react: , - }) - - return redirectWithToast( - '/settings/profile', - { - title: 'Email Changed', - type: 'success', - description: `Your email has been changed to ${user.email}`, - }, - { - headers: { - 'set-cookie': await verifySessionStorage.destroySession(verifySession), - }, - }, - ) -} +export const newEmailAddressSessionKey = 'new-email-address' const ChangeEmailSchema = z.object({ email: EmailSchema, @@ -158,71 +98,12 @@ export async function action({ request }: ActionFunctionArgs) { }) } else { return json( - { - result: submission.reply({ formErrors: [response.error.message] }), - }, - { - status: 500, - }, + { result: submission.reply({ formErrors: [response.error.message] }) }, + { status: 500 }, ) } } -export function EmailChangeEmail({ - verifyUrl, - otp, -}: { - verifyUrl: string - otp: string -}) { - return ( - - -

- Epic Notes Email Change -

-

- - Here's your verification code: {otp} - -

-

- Or click the link: -

- {verifyUrl} -
-
- ) -} - -export function EmailChangeNoticeEmail({ userId }: { userId: string }) { - return ( - - -

- Your Epic Notes email has been changed -

-

- - We're writing to let you know that your Epic Notes email has been - changed. - -

-

- - If you changed your email address, then you can safely ignore this. - But if you did not change your email address, then please contact - support immediately. - -

-

- Your Account ID: {userId} -

-
-
- ) -} - export default function ChangeEmailIndex() { const data = useLoaderData() const actionData = useActionData() diff --git a/app/routes/settings+/profile.two-factor.disable.tsx b/app/routes/settings+/profile.two-factor.disable.tsx index 8805b58..f153099 100644 --- a/app/routes/settings+/profile.two-factor.disable.tsx +++ b/app/routes/settings+/profile.two-factor.disable.tsx @@ -7,7 +7,7 @@ import { import { useFetcher } from '@remix-run/react' import { Icon } from '#app/components/ui/icon.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { requireRecentVerification } from '#app/routes/_auth+/verify.tsx' +import { requireRecentVerification } from '#app/routes/_auth+/verify.server.ts' import { requireUserId } from '#app/utils/auth.server.ts' import { prisma } from '#app/utils/db.server.ts' import { useDoubleCheck } from '#app/utils/misc.tsx' diff --git a/app/routes/settings+/profile.two-factor.verify.tsx b/app/routes/settings+/profile.two-factor.verify.tsx index ba3adf9..30742e4 100644 --- a/app/routes/settings+/profile.two-factor.verify.tsx +++ b/app/routes/settings+/profile.two-factor.verify.tsx @@ -18,7 +18,7 @@ import { z } from 'zod' import { ErrorList, Field } from '#app/components/forms.tsx' import { Icon } from '#app/components/ui/icon.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { isCodeValid } from '#app/routes/_auth+/verify.tsx' +import { isCodeValid } from '#app/routes/_auth+/verify.server.ts' import { requireUserId } from '#app/utils/auth.server.ts' import { prisma } from '#app/utils/db.server.ts' import { getDomainUrl, useIsPending } from '#app/utils/misc.tsx' diff --git a/app/routes/users+/$username.test.tsx b/app/routes/users+/$username.test.tsx index 66e2cd1..d0c47a6 100644 --- a/app/routes/users+/$username.test.tsx +++ b/app/routes/users+/$username.test.tsx @@ -19,11 +19,7 @@ test('The user profile when not logged in as self', async () => { userImages[faker.number.int({ min: 0, max: userImages.length - 1 })] const user = await prisma.user.create({ select: { id: true, username: true, name: true }, - data: { - ...createUser(), - image: { create: userImage }, - platformStatusKey: 'ACTIVE', - }, + data: { ...createUser(), image: { create: userImage } }, }) const App = createRemixStub([ { @@ -47,11 +43,7 @@ test('The user profile when logged in as self', async () => { userImages[faker.number.int({ min: 0, max: userImages.length - 1 })] const user = await prisma.user.create({ select: { id: true, username: true, name: true }, - data: { - ...createUser(), - image: { create: userImage }, - platformStatusKey: 'ACTIVE', - }, + data: { ...createUser(), image: { create: userImage } }, }) const session = await prisma.session.create({ select: { id: true }, diff --git a/app/routes/users+/$username_+/__note-editor.server.tsx b/app/routes/users+/$username_+/__note-editor.server.tsx new file mode 100644 index 0000000..6b16253 --- /dev/null +++ b/app/routes/users+/$username_+/__note-editor.server.tsx @@ -0,0 +1,131 @@ +import { parseWithZod } from '@conform-to/zod' +import { createId as cuid } from '@paralleldrive/cuid2' +import { + unstable_createMemoryUploadHandler as createMemoryUploadHandler, + json, + unstable_parseMultipartFormData as parseMultipartFormData, + redirect, + type ActionFunctionArgs, +} from '@remix-run/node' +import { z } from 'zod' +import { requireUserId } from '#app/utils/auth.server.ts' +import { prisma } from '#app/utils/db.server.ts' +import { + MAX_UPLOAD_SIZE, + NoteEditorSchema, + type ImageFieldset, +} from './__note-editor' + +function imageHasFile( + image: ImageFieldset, +): image is ImageFieldset & { file: NonNullable } { + return Boolean(image.file?.size && image.file?.size > 0) +} + +function imageHasId( + image: ImageFieldset, +): image is ImageFieldset & { id: NonNullable } { + return image.id != null +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + + const formData = await parseMultipartFormData( + request, + createMemoryUploadHandler({ maxPartSize: MAX_UPLOAD_SIZE }), + ) + + const submission = await parseWithZod(formData, { + schema: NoteEditorSchema.superRefine(async (data, ctx) => { + if (!data.id) return + + const note = await prisma.note.findUnique({ + select: { id: true }, + where: { id: data.id, ownerId: userId }, + }) + if (!note) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Note not found', + }) + } + }).transform(async ({ images = [], ...data }) => { + return { + ...data, + imageUpdates: await Promise.all( + images.filter(imageHasId).map(async i => { + if (imageHasFile(i)) { + return { + id: i.id, + altText: i.altText, + contentType: i.file.type, + blob: Buffer.from(await i.file.arrayBuffer()), + } + } else { + return { + id: i.id, + altText: i.altText, + } + } + }), + ), + newImages: await Promise.all( + images + .filter(imageHasFile) + .filter(i => !i.id) + .map(async image => { + return { + altText: image.altText, + contentType: image.file.type, + blob: Buffer.from(await image.file.arrayBuffer()), + } + }), + ), + } + }), + async: true, + }) + + if (submission.status !== 'success') { + return json( + { result: submission.reply() }, + { status: submission.status === 'error' ? 400 : 200 }, + ) + } + + const { + id: noteId, + title, + content, + imageUpdates = [], + newImages = [], + } = submission.value + + const updatedNote = await prisma.note.upsert({ + select: { id: true, owner: { select: { username: true } } }, + where: { id: noteId ?? '__new_note__' }, + create: { + ownerId: userId, + title, + content, + images: { create: newImages }, + }, + update: { + title, + content, + images: { + deleteMany: { id: { notIn: imageUpdates.map(i => i.id) } }, + updateMany: imageUpdates.map(updates => ({ + where: { id: updates.id }, + data: { ...updates, id: updates.blob ? cuid() : updates.id }, + })), + create: newImages, + }, + }, + }) + + return redirect( + `/users/${updatedNote.owner.username}/notes/${updatedNote.id}`, + ) +} diff --git a/app/routes/users+/$username_+/__note-editor.tsx b/app/routes/users+/$username_+/__note-editor.tsx index 7d31658..f5ce8b3 100644 --- a/app/routes/users+/$username_+/__note-editor.tsx +++ b/app/routes/users+/$username_+/__note-editor.tsx @@ -1,23 +1,15 @@ import { - type FieldMetadata, - useForm, + FormProvider, + getFieldsetProps, + getFormProps, getInputProps, getTextareaProps, - getFormProps, - getFieldsetProps, - FormProvider, + useForm, + type FieldMetadata, } from '@conform-to/react' import { getZodConstraint, parseWithZod } from '@conform-to/zod' -import { createId as cuid } from '@paralleldrive/cuid2' import { type Note, type NoteImage } from '@prisma/client' -import { - unstable_createMemoryUploadHandler as createMemoryUploadHandler, - json, - unstable_parseMultipartFormData as parseMultipartFormData, - redirect, - type ActionFunctionArgs, - type SerializeFrom, -} from '@remix-run/node' +import { type SerializeFrom } from '@remix-run/node' import { Form, useActionData } from '@remix-run/react' import { useState } from 'react' import { z } from 'zod' @@ -29,16 +21,15 @@ import { Icon } from '#app/components/ui/icon.tsx' import { Label } from '#app/components/ui/label.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' import { Textarea } from '#app/components/ui/textarea.tsx' -import { requireUserId } from '#app/utils/auth.server.ts' -import { prisma } from '#app/utils/db.server.ts' import { cn, getNoteImgSrc, useIsPending } from '#app/utils/misc.tsx' +import { type action } from './__note-editor.server' const titleMinLength = 1 const titleMaxLength = 100 const contentMinLength = 1 const contentMaxLength = 10000 -const MAX_UPLOAD_SIZE = 1024 * 1024 * 3 // 3MB +export const MAX_UPLOAD_SIZE = 1024 * 1024 * 3 // 3MB const ImageFieldsetSchema = z.object({ id: z.string().optional(), @@ -51,129 +42,15 @@ const ImageFieldsetSchema = z.object({ altText: z.string().optional(), }) -type ImageFieldset = z.infer - -function imageHasFile( - image: ImageFieldset, -): image is ImageFieldset & { file: NonNullable } { - return Boolean(image.file?.size && image.file?.size > 0) -} - -function imageHasId( - image: ImageFieldset, -): image is ImageFieldset & { id: NonNullable } { - return image.id != null -} +export type ImageFieldset = z.infer -const NoteEditorSchema = z.object({ +export const NoteEditorSchema = z.object({ id: z.string().optional(), title: z.string().min(titleMinLength).max(titleMaxLength), content: z.string().min(contentMinLength).max(contentMaxLength), images: z.array(ImageFieldsetSchema).max(5).optional(), }) -export async function action({ request }: ActionFunctionArgs) { - const userId = await requireUserId(request) - - const formData = await parseMultipartFormData( - request, - createMemoryUploadHandler({ maxPartSize: MAX_UPLOAD_SIZE }), - ) - - const submission = await parseWithZod(formData, { - schema: NoteEditorSchema.superRefine(async (data, ctx) => { - if (!data.id) return - - const note = await prisma.note.findUnique({ - select: { id: true }, - where: { id: data.id, ownerId: userId }, - }) - if (!note) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'Note not found', - }) - } - }).transform(async ({ images = [], ...data }) => { - return { - ...data, - imageUpdates: await Promise.all( - images.filter(imageHasId).map(async i => { - if (imageHasFile(i)) { - return { - id: i.id, - altText: i.altText, - contentType: i.file.type, - blob: Buffer.from(await i.file.arrayBuffer()), - } - } else { - return { - id: i.id, - altText: i.altText, - } - } - }), - ), - newImages: await Promise.all( - images - .filter(imageHasFile) - .filter(i => !i.id) - .map(async image => { - return { - altText: image.altText, - contentType: image.file.type, - blob: Buffer.from(await image.file.arrayBuffer()), - } - }), - ), - } - }), - async: true, - }) - - if (submission.status !== 'success') { - return json( - { result: submission.reply() }, - { status: submission.status === 'error' ? 400 : 200 }, - ) - } - - const { - id: noteId, - title, - content, - imageUpdates = [], - newImages = [], - } = submission.value - - const updatedNote = await prisma.note.upsert({ - select: { id: true, owner: { select: { username: true } } }, - where: { id: noteId ?? '__new_note__' }, - create: { - ownerId: userId, - title, - content, - images: { create: newImages }, - }, - update: { - title, - content, - images: { - deleteMany: { id: { notIn: imageUpdates.map(i => i.id) } }, - updateMany: imageUpdates.map(updates => ({ - where: { id: updates.id }, - data: { ...updates, id: updates.blob ? cuid() : updates.id }, - })), - create: newImages, - }, - }, - }) - - return redirect( - `/users/${updatedNote.owner.username}/notes/${updatedNote.id}`, - ) -} - export function NoteEditor({ note, }: { diff --git a/app/routes/users+/$username_+/notes.$noteId_.edit.tsx b/app/routes/users+/$username_+/notes.$noteId_.edit.tsx index 6ef57ae..0d14817 100644 --- a/app/routes/users+/$username_+/notes.$noteId_.edit.tsx +++ b/app/routes/users+/$username_+/notes.$noteId_.edit.tsx @@ -4,9 +4,9 @@ import { useLoaderData } from '@remix-run/react' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' import { requireUserId } from '#app/utils/auth.server.ts' import { prisma } from '#app/utils/db.server.ts' -import { NoteEditor, action } from './__note-editor.tsx' +import { NoteEditor } from './__note-editor.tsx' -export { action } +export { action } from './__note-editor.server.tsx' export async function loader({ params, request }: LoaderFunctionArgs) { const userId = await requireUserId(request) @@ -42,7 +42,7 @@ export function ErrorBoundary() { ( -

No note with the id == 0 g "{params.noteId}" exists

+

No note with the id "{params.noteId}" exists

), }} /> diff --git a/app/routes/users+/$username_+/notes.new.tsx b/app/routes/users+/$username_+/notes.new.tsx index 14a097f..ecc8580 100644 --- a/app/routes/users+/$username_+/notes.new.tsx +++ b/app/routes/users+/$username_+/notes.new.tsx @@ -1,11 +1,12 @@ import { json, type LoaderFunctionArgs } from '@remix-run/node' import { requireUserId } from '#app/utils/auth.server.ts' -import { NoteEditor, action } from './__note-editor.tsx' +import { NoteEditor } from './__note-editor.tsx' + +export { action } from './__note-editor.server.tsx' export async function loader({ request }: LoaderFunctionArgs) { await requireUserId(request) return json({}) } -export { action } export default NoteEditor diff --git a/app/utils/cache.server.ts b/app/utils/cache.server.ts index d37db2c..eaee619 100644 --- a/app/utils/cache.server.ts +++ b/app/utils/cache.server.ts @@ -14,7 +14,7 @@ import { remember } from '@epic-web/remember' import Database from 'better-sqlite3' import { LRUCache } from 'lru-cache' import { z } from 'zod' -import { updatePrimaryCacheValue } from '#app/routes/admin+/cache_.sqlite.tsx' +import { updatePrimaryCacheValue } from '#app/routes/admin+/cache_.sqlite.server.ts' import { getInstanceInfo, getInstanceInfoSync } from './litefs.server.ts' import { cachifiedTimingReporter, type Timings } from './timing.server.ts' diff --git a/app/utils/litefs.server.ts b/app/utils/litefs.server.ts index 805a984..0565a5b 100644 --- a/app/utils/litefs.server.ts +++ b/app/utils/litefs.server.ts @@ -1,5 +1,10 @@ // litefs-js should be used server-side only. It imports `fs` which results in Remix // including a big polyfill. So we put the import in a `.server.ts` file to avoid that // polyfill from being included. https://github.com/epicweb-dev/epic-stack/pull/331 -export * from 'litefs-js' -export * from 'litefs-js/remix.js' +export { + getInstanceInfo, + getAllInstances, + getInternalInstanceDomain, + getInstanceInfoSync, +} from 'litefs-js' +export { ensurePrimary, ensureInstance } from 'litefs-js/remix.js' diff --git a/package-lock.json b/package-lock.json index 2d29c74..3076add 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,20 +17,20 @@ "@paralleldrive/cuid2": "^2.2.2", "@prisma/client": "^5.10.2", "@radix-ui/react-checkbox": "^1.0.4", - "@radix-ui/react-dialog": "^1.0.5", +"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.0.7", - "@react-email/components": "0.0.14", - "@remix-run/css-bundle": "^2.7.2", - "@remix-run/express": "^2.7.2", - "@remix-run/node": "^2.7.2", - "@remix-run/react": "^2.7.2", - "@remix-run/server-runtime": "^2.7.2", - "@sentry/profiling-node": "^7.102.0", - "@sentry/remix": "^7.102.0", + "@react-email/components": "0.0.15", + "@remix-run/css-bundle": "2.7.2", + "@remix-run/express": "2.7.2", + "@remix-run/node": "2.7.2", + "@remix-run/react": "2.7.2", + "@remix-run/server-runtime": "2.7.2", + "@sentry/profiling-node": "^7.102.1", + "@sentry/remix": "^7.102.1", "address": "^2.0.1", "bcryptjs": "^2.4.3", "better-sqlite3": "^9.4.3", @@ -56,7 +56,7 @@ "litefs-js": "^1.1.2", "lru-cache": "^10.2.0", "morgan": "^1.10.0", - "qrcode": "^1.5.3", + "qrcode": "^1.5.3", "react": "^18.2.0", "react-dom": "^18.2.0", "remix-auth": "^3.6.0", @@ -76,10 +76,10 @@ "devDependencies": { "@faker-js/faker": "^8.4.1", "@playwright/test": "^1.41.2", - "@remix-run/dev": "^2.7.2", - "@remix-run/eslint-config": "^2.7.2", - "@remix-run/serve": "^2.7.2", - "@remix-run/testing": "^2.7.2", + "@remix-run/dev": "2.7.2", + "@remix-run/eslint-config": "2.7.2", + "@remix-run/serve": "2.7.2", + "@remix-run/testing": "2.7.2", "@sly-cli/sly": "^1.8.0", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.1", @@ -89,12 +89,12 @@ "@types/better-sqlite3": "^7.6.9", "@types/compression": "^1.7.5", "@types/cookie": "^0.6.0", - "@types/eslint": "^8.56.2", + "@types/eslint": "^8.56.3", "@types/express": "^4.17.21", "@types/fs-extra": "^11.0.4", "@types/glob": "^8.1.0", "@types/morgan": "^1.9.9", - "@types/node": "^20.11.19", + "@types/node": "^20.11.20", "@types/qrcode": "^1.5.5", "@types/react": "^18.2.57", "@types/react-dom": "^18.2.19", @@ -103,7 +103,6 @@ "@vitejs/plugin-react": "^4.2.1", "@vitest/coverage-v8": "^1.3.1", "autoprefixer": "^10.4.17", - "chokidar": "^3.6.0", "enforce-unique": "^1.2.0", "esbuild": "^0.20.1", "eslint": "^8.56.0", @@ -116,9 +115,8 @@ "prettier": "^3.2.5", "prettier-plugin-sql": "^0.18.0", "prettier-plugin-tailwindcss": "^0.5.11", - "prisma": "^5.10.2", +"prisma": "^5.10.2", "remix-flat-routes": "^0.6.4", - "rimraf": "^5.0.5", "tsx": "^4.7.1", "typescript": "^5.3.3", "vite": "^5.1.4", @@ -2151,14 +2149,12 @@ "node_modules/@prisma/debug": { "version": "5.10.2", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.10.2.tgz", - "integrity": "sha512-bkBOmH9dpEBbMKFJj8V+Zp8IZHIBjy3fSyhLhxj4FmKGb/UBSt9doyfA6k1UeUREsMJft7xgPYBbHSOYBr8XCA==", - "dev": true + "integrity": "sha512-bkBOmH9dpEBbMKFJj8V+Zp8IZHIBjy3fSyhLhxj4FmKGb/UBSt9doyfA6k1UeUREsMJft7xgPYBbHSOYBr8XCA==" }, "node_modules/@prisma/engines": { "version": "5.10.2", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.10.2.tgz", "integrity": "sha512-HkSJvix6PW8YqEEt3zHfCYYJY69CXsNdhU+wna+4Y7EZ+AwzeupMnUThmvaDA7uqswiHkgm5/SZ6/4CStjaGmw==", - "dev": true, "hasInstallScript": true, "dependencies": { "@prisma/debug": "5.10.2", @@ -2170,14 +2166,12 @@ "node_modules/@prisma/engines-version": { "version": "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9.tgz", - "integrity": "sha512-uCy/++3Jx/O3ufM+qv2H1L4tOemTNqcP/gyEVOlZqTpBvYJUe0tWtW0y3o2Ueq04mll4aM5X3f6ugQftOSLdFQ==", - "dev": true + "integrity": "sha512-uCy/++3Jx/O3ufM+qv2H1L4tOemTNqcP/gyEVOlZqTpBvYJUe0tWtW0y3o2Ueq04mll4aM5X3f6ugQftOSLdFQ==" }, "node_modules/@prisma/fetch-engine": { "version": "5.10.2", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.10.2.tgz", "integrity": "sha512-dSmXcqSt6DpTmMaLQ9K8ZKzVAMH3qwGCmYEZr/uVnzVhxRJ1EbT/w2MMwIdBNq1zT69Rvh0h75WMIi0mrIw7Hg==", - "dev": true, "dependencies": { "@prisma/debug": "5.10.2", "@prisma/engines-version": "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9", @@ -2188,7 +2182,6 @@ "version": "5.10.2", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.10.2.tgz", "integrity": "sha512-nqXP6vHiY2PIsebBAuDeWiUYg8h8mfjBckHh6Jezuwej0QJNnjDiOq30uesmg+JXxGk99nqyG3B7wpcOODzXvg==", - "dev": true, "dependencies": { "@prisma/debug": "5.10.2" } @@ -2314,42 +2307,6 @@ } } }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", - "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-direction": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", @@ -2928,9 +2885,9 @@ } }, "node_modules/@react-email/button": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.13.tgz", - "integrity": "sha512-e/y8u2odJ8fF83B+wvL2FXzVcbQSUh2Cn2JH2Ez4L6AuPELsh8s2JYo081IDsXc16IyFiYpObn0blOt7s/qp8g==", + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.14.tgz", + "integrity": "sha512-SMk40moGcAvkHIALX4XercQlK0PNeeEIam6OXHw68ea9WtzzqVwiK4pzLY0iiMI9B4xWHcaS2lCPf3cKbQBf1Q==", "engines": { "node": ">=18.0.0" }, @@ -2939,9 +2896,9 @@ } }, "node_modules/@react-email/code-block": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.2.tgz", - "integrity": "sha512-bQApEmpsvIcVYXdPCXhJB9CGCyShhn/c1JdctE/6R1uIosLbWt40evvVfp2X9STdi02Dhsjxw/AcGuQE6zGZqw==", + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.3.tgz", + "integrity": "sha512-nxhl7WjjM2cOYtl0boBZfSObTrUCz2LbarcMyHkTVAsA9rbjbtWAQF7jmlefXJusk3Uol5l2c8hTh2lHLlHTRQ==", "dependencies": { "prismjs": "1.29.0" }, @@ -2975,13 +2932,13 @@ } }, "node_modules/@react-email/components": { - "version": "0.0.14", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.14.tgz", - "integrity": "sha512-t/sNj0R9Mx9Sx5degPQcSBeWotNs7eUwiv72KN8v6fxaf87XlnMo0CPcKI/1by2DHZr5S0258ZQOO7vEFrbcLw==", + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.15.tgz", + "integrity": "sha512-jXfKiuyi94JBYfPVptEUwF57nRCvhEZIfyl2LqbL53fKsMrGlcjlN921iNnx1z41GAJOqZ8LPogeix3Iid23zw==", "dependencies": { "@react-email/body": "0.0.7", - "@react-email/button": "0.0.13", - "@react-email/code-block": "0.0.2", + "@react-email/button": "0.0.14", + "@react-email/code-block": "0.0.3", "@react-email/code-inline": "0.0.1", "@react-email/column": "0.0.9", "@react-email/container": "0.0.11", @@ -2992,6 +2949,7 @@ "@react-email/html": "0.0.7", "@react-email/img": "0.0.7", "@react-email/link": "0.0.7", + "@react-email/markdown": "0.0.8", "@react-email/preview": "0.0.8", "@react-email/render": "0.0.12", "@react-email/row": "0.0.7", @@ -3094,6 +3052,20 @@ "react": "18.2.0" } }, + "node_modules/@react-email/markdown": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.8.tgz", + "integrity": "sha512-x/2iTWskE0XoM13Rx80ckwbWaWdS6koYvxW6PHkOJ/k88NPnDIm+TaYvvg2DYSFJAUI0gK/LmIwenbebgNDS+w==", + "dependencies": { + "md-to-react-email": "4.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "18.2.0" + } + }, "node_modules/@react-email/preview": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.8.tgz", @@ -4310,66 +4282,66 @@ } }, "node_modules/@sentry-internal/feedback": { - "version": "7.102.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.102.0.tgz", - "integrity": "sha512-GxHdzbOF4tg6TtyQzFqb/8c/p07n68qZC5KYwzs7AuW5ey0IPmdC58pOh3Kk52JA0P69/RZy39+r1p1Swr6C+Q==", + "version": "7.102.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.102.1.tgz", + "integrity": "sha512-vY4hpLLMNLjICtWiizc7KeGbWOTUMGrF7C+9dPCztZww3CLgzWy9A7DvPj5hodRiYzpdRnAMl8yQnMFbYXh7bA==", "dependencies": { - "@sentry/core": "7.102.0", - "@sentry/types": "7.102.0", - "@sentry/utils": "7.102.0" + "@sentry/core": "7.102.1", + "@sentry/types": "7.102.1", + "@sentry/utils": "7.102.1" }, "engines": { "node": ">=12" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "7.102.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.102.0.tgz", - "integrity": "sha512-rgNO4PdFv0AYflBsCNbSIwpQuOOJQTqyu8i8U0PupjveNjkm0CUJhber/ZOcaGmbyjdvwikGwgWY2O0Oj0USCA==", + "version": "7.102.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.102.1.tgz", + "integrity": "sha512-GUX4RWI10uRjdjeyvCLtAAhWRVqnAnG6+yNxWfqUQ3qMA7B7XxG43KT2UhSnulmErNzODQ6hA68rGPwwYeRIww==", "dependencies": { - "@sentry/core": "7.102.0", - "@sentry/replay": "7.102.0", - "@sentry/types": "7.102.0", - "@sentry/utils": "7.102.0" + "@sentry/core": "7.102.1", + "@sentry/replay": "7.102.1", + "@sentry/types": "7.102.1", + "@sentry/utils": "7.102.1" }, "engines": { "node": ">=12" } }, "node_modules/@sentry-internal/tracing": { - "version": "7.102.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.102.0.tgz", - "integrity": "sha512-BlE33HWL1IzkGa0W+pwTiyu01MUIfYf+WnO9UC8qkDW3jxVvg2zhoSjXSxikT+KPCOgoZpQHspaTzwjnI1LCvw==", + "version": "7.102.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.102.1.tgz", + "integrity": "sha512-RkFlFyAC0fQOvBbBqnq0CLmFW5m3JJz9pKbZd5vXPraWAlniKSb1bC/4DF9SlNx0FN1LWG+IU3ISdpzwwTeAGg==", "dependencies": { - "@sentry/core": "7.102.0", - "@sentry/types": "7.102.0", - "@sentry/utils": "7.102.0" + "@sentry/core": "7.102.1", + "@sentry/types": "7.102.1", + "@sentry/utils": "7.102.1" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/browser": { - "version": "7.102.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.102.0.tgz", - "integrity": "sha512-hIggcMnojIbWhbmlRfkykHmy6n7pjug0AHfF19HRUQxAx9KJfMH5YdWvohov0Hb9fS+jdvqgE+/4AWbEeXQrHw==", + "version": "7.102.1", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.102.1.tgz", + "integrity": "sha512-7BOfPBiM7Kp6q/iy0JIbsBTxIASV+zWXByqqjuEMWGj3X2u4oRIfm3gv4erPU/l+CORQUVQZLSPGoIoM1gbB/A==", "dependencies": { - "@sentry-internal/feedback": "7.102.0", - "@sentry-internal/replay-canvas": "7.102.0", - "@sentry-internal/tracing": "7.102.0", - "@sentry/core": "7.102.0", - "@sentry/replay": "7.102.0", - "@sentry/types": "7.102.0", - "@sentry/utils": "7.102.0" + "@sentry-internal/feedback": "7.102.1", + "@sentry-internal/replay-canvas": "7.102.1", + "@sentry-internal/tracing": "7.102.1", + "@sentry/core": "7.102.1", + "@sentry/replay": "7.102.1", + "@sentry/types": "7.102.1", + "@sentry/utils": "7.102.1" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/cli": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.28.0.tgz", - "integrity": "sha512-0vdMTeN3Ip1wI9T7F6GupuaOocIrfyHpAN3iUztsO7PY2j7e/+m69DRkU99aPTlmUgQikZjtVaHkTsEMLt3lgA==", + "version": "2.28.6", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.28.6.tgz", + "integrity": "sha512-o2Ngz7xXuhwHxMi+4BFgZ4qjkX0tdZeOSIZkFAGnTbRhQe5T8bxq6CcQRLdPhqMgqvDn7XuJ3YlFtD3ZjHvD7g==", "hasInstallScript": true, "dependencies": { "https-proxy-agent": "^5.0.0", @@ -4385,19 +4357,19 @@ "node": ">= 10" }, "optionalDependencies": { - "@sentry/cli-darwin": "2.28.0", - "@sentry/cli-linux-arm": "2.28.0", - "@sentry/cli-linux-arm64": "2.28.0", - "@sentry/cli-linux-i686": "2.28.0", - "@sentry/cli-linux-x64": "2.28.0", - "@sentry/cli-win32-i686": "2.28.0", - "@sentry/cli-win32-x64": "2.28.0" + "@sentry/cli-darwin": "2.28.6", + "@sentry/cli-linux-arm": "2.28.6", + "@sentry/cli-linux-arm64": "2.28.6", + "@sentry/cli-linux-i686": "2.28.6", + "@sentry/cli-linux-x64": "2.28.6", + "@sentry/cli-win32-i686": "2.28.6", + "@sentry/cli-win32-x64": "2.28.6" } }, "node_modules/@sentry/cli-darwin": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.28.0.tgz", - "integrity": "sha512-GgpayUQcGjT55Dc7oojjbqIYIUaBAr4za7D9yU5foMTJ6QjMTovmtE1bVj4bVKzK+0aIiZvZ2dg2g6jF0iGqfg==", + "version": "2.28.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.28.6.tgz", + "integrity": "sha512-KRf0VvTltHQ5gA7CdbUkaIp222LAk/f1+KqpDzO6nB/jC/tL4sfiy6YyM4uiH6IbVEudB8WpHCECiatmyAqMBA==", "optional": true, "os": [ "darwin" @@ -4407,9 +4379,9 @@ } }, "node_modules/@sentry/cli-linux-arm": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.28.0.tgz", - "integrity": "sha512-hjCRyZBNri+gNoMO22g2qevKcUOnDGhTjmyq14q2rXT0KHb4LjyMpebSgE63YTLDj/qxq4MSq8kcjD/jDzSpLw==", + "version": "2.28.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.28.6.tgz", + "integrity": "sha512-ANG7U47yEHD1g3JrfhpT4/MclEvmDZhctWgSP5gVw5X4AlcI87E6dTqccnLgvZjiIAQTaJJAZuSHVVF3Jk403w==", "cpu": [ "arm" ], @@ -4423,9 +4395,9 @@ } }, "node_modules/@sentry/cli-linux-arm64": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.28.0.tgz", - "integrity": "sha512-QZtl4dyVMrsWEuRCN8h3RMQSjekM6LmdAWiEIxCgVMvTueau31EQz1jokGpaYotAsWK2GyzFALiCA3QwMCTtnA==", + "version": "2.28.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.28.6.tgz", + "integrity": "sha512-caMDt37FI752n4/3pVltDjlrRlPFCOxK4PHvoZGQ3KFMsai0ZhE/0CLBUMQqfZf0M0r8KB2x7wqLm7xSELjefQ==", "cpu": [ "arm64" ], @@ -4439,9 +4411,9 @@ } }, "node_modules/@sentry/cli-linux-i686": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.28.0.tgz", - "integrity": "sha512-fgT0G6b1OCBHtrIClNrFfO8w5pVw7yIqtVsq4Bf+FJOwkD2buaPx1Qt66aGP+3+AexXO5pXfagN4+ykSsKqKZA==", + "version": "2.28.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.28.6.tgz", + "integrity": "sha512-Tj1+GMc6lFsDRquOqaGKXFpW9QbmNK4TSfynkWKiJxdTEn5jSMlXXfr0r9OQrxu3dCCqEHkhEyU63NYVpgxIPw==", "cpu": [ "x86", "ia32" @@ -4456,9 +4428,9 @@ } }, "node_modules/@sentry/cli-linux-x64": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.28.0.tgz", - "integrity": "sha512-mrqbxpo6dF8iC4nz0+TS8ymIeNKy6gngcmlRVfOBuVEP9+Ry8HAeIzuKwbt4QAA6lwKCbPsEwK5ZLsrJEJIC6A==", + "version": "2.28.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.28.6.tgz", + "integrity": "sha512-Dt/Xz784w/z3tEObfyJEMmRIzn0D5qoK53H9kZ6e0yNvJOSKNCSOq5cQk4n1/qeG0K/6SU9dirmvHwFUiVNyYg==", "cpu": [ "x64" ], @@ -4472,9 +4444,9 @@ } }, "node_modules/@sentry/cli-win32-i686": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.28.0.tgz", - "integrity": "sha512-yzji557eqz4XW7z8k0LF4LiIwFAqxPlpVnoeN8ntk8hi/ehXm9AdvPqA+bw7cRK5iu4/Tqr4OJeGPbcI5iKpgQ==", + "version": "2.28.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.28.6.tgz", + "integrity": "sha512-zkpWtvY3kt+ogVaAbfFr2MEkgMMHJNJUnNMO8Ixce9gh38sybIkDkZNFnVPBXMClJV0APa4QH0EwumYBFZUMuQ==", "cpu": [ "x86", "ia32" @@ -4488,9 +4460,9 @@ } }, "node_modules/@sentry/cli-win32-x64": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.28.0.tgz", - "integrity": "sha512-5frag3uV+niuMVYQ3ME5Nwlv5uftV88xDUyaCe1UD9jfM8WqJPgvQYUNPgBQKynxwLAUp5zXII+47Vnn8mriOA==", + "version": "2.28.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.28.6.tgz", + "integrity": "sha512-TG2YzZ9JMeNFzbicdr5fbtsusVGACbrEfHmPgzWGDeLUP90mZxiMTjkXsE1X/5jQEQjB2+fyfXloba/Ugo51hA==", "cpu": [ "x64" ], @@ -4517,35 +4489,35 @@ } }, "node_modules/@sentry/core": { - "version": "7.102.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.102.0.tgz", - "integrity": "sha512-GO9eLOSBK1waW4AD0wDXAreaNqXFQ1MPQZrkKcN+GJYEFhJK1+u+MSV7vO5Fs/rIfaTZIZ2jtEkxSSAOucE8EQ==", + "version": "7.102.1", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.102.1.tgz", + "integrity": "sha512-QjY+LSP3du3J/C8x/FfEbRxgZgsWd0jfTJ4P7s9f219I1csK4OeBMC3UA1HwEa0pY/9OF6H/egW2CjOcMM5Pdg==", "dependencies": { - "@sentry/types": "7.102.0", - "@sentry/utils": "7.102.0" + "@sentry/types": "7.102.1", + "@sentry/utils": "7.102.1" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/node": { - "version": "7.102.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.102.0.tgz", - "integrity": "sha512-ZS1s2uO/+K4rHkmWjyqm5Jtl6dT7klbZSMvn4tfIpkfWuqrs7pP0jaATyvmF+96z3lpq6fRAJliV5tRqPy7w5Q==", + "version": "7.102.1", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.102.1.tgz", + "integrity": "sha512-mb3vmM3SGuCruckPiv/Vafeh89UQavTfpPFoU6Jwe6dSpQ39BO8fO8k8Zev+/nP6r/FKLtX17mJobErHECXsYw==", "dependencies": { - "@sentry-internal/tracing": "7.102.0", - "@sentry/core": "7.102.0", - "@sentry/types": "7.102.0", - "@sentry/utils": "7.102.0" + "@sentry-internal/tracing": "7.102.1", + "@sentry/core": "7.102.1", + "@sentry/types": "7.102.1", + "@sentry/utils": "7.102.1" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/profiling-node": { - "version": "7.102.0", - "resolved": "https://registry.npmjs.org/@sentry/profiling-node/-/profiling-node-7.102.0.tgz", - "integrity": "sha512-prw0PveisR6zjAkz0zC9xZHELXLpqofa9vbj6xqnwFQkw3pES/pZAgzRnGjs5xzVsIkXZzVA0vWzOJDGOa0kFg==", + "version": "7.102.1", + "resolved": "https://registry.npmjs.org/@sentry/profiling-node/-/profiling-node-7.102.1.tgz", + "integrity": "sha512-eqOUdu04eI4ODqeh/nHvC/mdwm3tWkqm02anR2ITEjKVJxHliHH6+jr+3M2X56e1hIxOibtL+JrR89Du9HEl9w==", "hasInstallScript": true, "dependencies": { "detect-libc": "^2.0.2", @@ -4559,14 +4531,14 @@ } }, "node_modules/@sentry/react": { - "version": "7.102.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.102.0.tgz", - "integrity": "sha512-Dz2JZwQMU/gpAVRHz6usMGgDF5Y0QcPUAnRoNpewEanZW7nChN8FsIYjOkvEbbsgk8bAlAjWErNlKGfl0B3YoA==", - "dependencies": { - "@sentry/browser": "7.102.0", - "@sentry/core": "7.102.0", - "@sentry/types": "7.102.0", - "@sentry/utils": "7.102.0", + "version": "7.102.1", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.102.1.tgz", + "integrity": "sha512-X4j2DgbktlEifnd21YJKCayAmff5hnaS+9MNz9OonEwD0ARi0ks7bo0wtWHMjPK20992MO+JwczVg/1BXJYDdQ==", + "dependencies": { + "@sentry/browser": "7.102.1", + "@sentry/core": "7.102.1", + "@sentry/types": "7.102.1", + "@sentry/utils": "7.102.1", "hoist-non-react-statics": "^3.3.2" }, "engines": { @@ -4577,16 +4549,17 @@ } }, "node_modules/@sentry/remix": { - "version": "7.102.0", - "resolved": "https://registry.npmjs.org/@sentry/remix/-/remix-7.102.0.tgz", - "integrity": "sha512-AIDaeHBOLfCLcEqBuwsQSV9f9ZNw/FJ0upjKKJpuZSTTsFQCJRsP2INHP4ER4bjVxfY37l6Ber+qCwMTkUNxDA==", + "version": "7.102.1", + "resolved": "https://registry.npmjs.org/@sentry/remix/-/remix-7.102.1.tgz", + "integrity": "sha512-NQOB3/ZthPfXmG8e0954UeRnoCyJ6ZVl0Q76MAJKydx795NutrZbaFZAtLzV0PCIeR9PampjUmd6VnEW0YHysw==", "dependencies": { + "@remix-run/router": "1.x", "@sentry/cli": "^2.28.0", - "@sentry/core": "7.102.0", - "@sentry/node": "7.102.0", - "@sentry/react": "7.102.0", - "@sentry/types": "7.102.0", - "@sentry/utils": "7.102.0", + "@sentry/core": "7.102.1", + "@sentry/node": "7.102.1", + "@sentry/react": "7.102.1", + "@sentry/types": "7.102.1", + "@sentry/utils": "7.102.1", "glob": "^10.3.4", "yargs": "^17.6.0" }, @@ -4603,33 +4576,33 @@ } }, "node_modules/@sentry/replay": { - "version": "7.102.0", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.102.0.tgz", - "integrity": "sha512-sUIBN4ZY0J5/dQS3KOe5VLykm856KZkTrhV8kmBEylzQhw1BBc8i2ehTILy5ZYh9Ra8uXPTAmtwpvYf/dRDfAg==", + "version": "7.102.1", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.102.1.tgz", + "integrity": "sha512-HR/j9dGIvbrId8fh8mQlODx7JrhRmawEd9e9P3laPtogWCg/5TI+XPb2VGSaXOX9VWtb/6Z2UjHsaGjgg6YcuA==", "dependencies": { - "@sentry-internal/tracing": "7.102.0", - "@sentry/core": "7.102.0", - "@sentry/types": "7.102.0", - "@sentry/utils": "7.102.0" + "@sentry-internal/tracing": "7.102.1", + "@sentry/core": "7.102.1", + "@sentry/types": "7.102.1", + "@sentry/utils": "7.102.1" }, "engines": { "node": ">=12" } }, "node_modules/@sentry/types": { - "version": "7.102.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.102.0.tgz", - "integrity": "sha512-FPfFBP0x3LkPARw1/6cWySLq1djIo8ao3Qo2KNBeE9CHdq8bsS1a8zzjJLuWG4Ww+wieLP8/lY3WTgrCz4jowg==", + "version": "7.102.1", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.102.1.tgz", + "integrity": "sha512-htKorf3t/D0XYtM7foTcmG+rM47rDP6XdbvCcX5gBCuCYlzpM1vqCt2rl3FLktZC6TaIpFRJw1TLfx6m+x5jdA==", "engines": { "node": ">=8" } }, "node_modules/@sentry/utils": { - "version": "7.102.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.102.0.tgz", - "integrity": "sha512-cp5KCRe0slOVMwG4iP2Z4UajQkjryRTiFskZ5H7Q3X9R5voM8+DAhiDcIW88GL9NxqyUrAJOjmKdeLK2vM+bdA==", + "version": "7.102.1", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.102.1.tgz", + "integrity": "sha512-+8WcFjHVV/HROXSAwMuUzveElBFC43EiTG7SNEBNgOUeQzQVTmbUZXyTVgLrUmtoWqvnIxCacoLxtZo1o67kdg==", "dependencies": { - "@sentry/types": "7.102.0" + "@sentry/types": "7.102.1" }, "engines": { "node": ">=8" @@ -5589,9 +5562,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.56.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", - "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==", + "version": "8.56.3", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.3.tgz", + "integrity": "sha512-PvSf1wfv2wJpVIFUMSb+i4PvqNYkB9Rkp9ZDO3oaWzq4SKhsQk4mrMBr3ZH06I0hKrVGLBacmgl8JM4WVjb9dg==", "dev": true, "dependencies": { "@types/estree": "*", @@ -5750,9 +5723,9 @@ } }, "node_modules/@types/node": { - "version": "20.11.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", - "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", + "version": "20.11.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", + "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -8142,9 +8115,15 @@ } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -8157,9 +8136,6 @@ "engines": { "node": ">= 8.10.0" }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -10750,63 +10726,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/flat-cache/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/flat-cache/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/flat-cache/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/flat-cache/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/flatted": { "version": "3.2.9", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", @@ -12262,13 +12181,14 @@ } }, "node_modules/js-beautify": { - "version": "1.14.11", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.11.tgz", - "integrity": "sha512-rPogWqAfoYh1Ryqqh2agUpVfbxAhbjuN1SmU86dskQUKouRiggUTCO4+2ym9UPXllc2WAp0J+T5qxn7Um3lCdw==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", + "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", "dependencies": { "config-chain": "^1.1.13", - "editorconfig": "^1.0.3", + "editorconfig": "^1.0.4", "glob": "^10.3.3", + "js-cookie": "^3.0.5", "nopt": "^7.2.0" }, "bin": { @@ -12280,6 +12200,14 @@ "node": ">=14" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -12814,6 +12742,29 @@ "node": ">=0.10.0" } }, + "node_modules/marked": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-7.0.4.tgz", + "integrity": "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/md-to-react-email": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/md-to-react-email/-/md-to-react-email-4.1.0.tgz", + "integrity": "sha512-aQvj4dCuy0wmBVvSB377qTErlpjN5Pl61+5v+B8Z76KoxOgKhbzvK3qnO94eOsuGSWwI+9n4zb3xD3/MypxM4w==", + "dependencies": { + "marked": "7.0.4" + }, + "peerDependencies": { + "react": "18.x", + "react-email": ">1.9.3" + } + }, "node_modules/mdast-util-definitions": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", @@ -15616,7 +15567,6 @@ "version": "5.10.2", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.10.2.tgz", "integrity": "sha512-hqb/JMz9/kymRE25pMWCxkdyhbnIWrq+h7S6WysJpdnCvhstbJSNP/S6mScEcqiB8Qv2F+0R3yG+osRaWqZacQ==", - "dev": true, "hasInstallScript": true, "dependencies": { "@prisma/engines": "5.10.2" @@ -16723,23 +16673,62 @@ } }, "node_modules/rimraf": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", - "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "dependencies": { - "glob": "^10.3.7" + "glob": "^7.1.3" }, "bin": { - "rimraf": "dist/esm/bin.mjs" + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=14" + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/rollup": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.5.tgz", diff --git a/package.json b/package.json index e21a2da..1ec3245 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,10 @@ "scripts": { "build": "run-s build:*", "build:icons": "tsx ./other/build-icons.ts", - "build:remix": "remix build --sourcemap", + "build:remix": "remix vite:build --sourcemapClient", "build:server": "tsx ./other/build-server.ts", "predev": "npm run build:icons --silent", - "dev": "remix dev -c \"node ./server/dev-server.js\" --manual", + "dev": "node ./server/dev-server.js", "prisma:studio": "prisma studio", "format": "prettier --write .", "lint": "eslint .", @@ -49,20 +49,20 @@ "@paralleldrive/cuid2": "^2.2.2", "@prisma/client": "^5.10.2", "@radix-ui/react-checkbox": "^1.0.4", - "@radix-ui/react-dialog": "^1.0.5", +"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.0.7", - "@react-email/components": "0.0.14", - "@remix-run/css-bundle": "^2.7.2", - "@remix-run/express": "^2.7.2", - "@remix-run/node": "^2.7.2", - "@remix-run/react": "^2.7.2", - "@remix-run/server-runtime": "^2.7.2", - "@sentry/profiling-node": "^7.102.0", - "@sentry/remix": "^7.102.0", + "@react-email/components": "0.0.15", + "@remix-run/css-bundle": "2.7.2", + "@remix-run/express": "2.7.2", + "@remix-run/node": "2.7.2", + "@remix-run/react": "2.7.2", + "@remix-run/server-runtime": "2.7.2", + "@sentry/profiling-node": "^7.102.1", + "@sentry/remix": "^7.102.1", "address": "^2.0.1", "bcryptjs": "^2.4.3", "better-sqlite3": "^9.4.3", @@ -88,7 +88,7 @@ "litefs-js": "^1.1.2", "lru-cache": "^10.2.0", "morgan": "^1.10.0", - "qrcode": "^1.5.3", + "qrcode": "^1.5.3", "react": "^18.2.0", "react-dom": "^18.2.0", "remix-auth": "^3.6.0", @@ -108,10 +108,10 @@ "devDependencies": { "@faker-js/faker": "^8.4.1", "@playwright/test": "^1.41.2", - "@remix-run/dev": "^2.7.2", - "@remix-run/eslint-config": "^2.7.2", - "@remix-run/serve": "^2.7.2", - "@remix-run/testing": "^2.7.2", + "@remix-run/dev": "2.7.2", + "@remix-run/eslint-config": "2.7.2", + "@remix-run/serve": "2.7.2", + "@remix-run/testing": "2.7.2", "@sly-cli/sly": "^1.8.0", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.1", @@ -121,12 +121,12 @@ "@types/better-sqlite3": "^7.6.9", "@types/compression": "^1.7.5", "@types/cookie": "^0.6.0", - "@types/eslint": "^8.56.2", + "@types/eslint": "^8.56.3", "@types/express": "^4.17.21", "@types/fs-extra": "^11.0.4", "@types/glob": "^8.1.0", "@types/morgan": "^1.9.9", - "@types/node": "^20.11.19", + "@types/node": "^20.11.20", "@types/qrcode": "^1.5.5", "@types/react": "^18.2.57", "@types/react-dom": "^18.2.19", @@ -135,7 +135,6 @@ "@vitejs/plugin-react": "^4.2.1", "@vitest/coverage-v8": "^1.3.1", "autoprefixer": "^10.4.17", - "chokidar": "^3.6.0", "enforce-unique": "^1.2.0", "esbuild": "^0.20.1", "eslint": "^8.56.0", @@ -148,9 +147,9 @@ "prettier": "^3.2.5", "prettier-plugin-sql": "^0.18.0", "prettier-plugin-tailwindcss": "^0.5.11", - "prisma": "^5.10.2", +"prisma": "^5.10.2", "remix-flat-routes": "^0.6.4", - "rimraf": "^5.0.5", +"rimraf": "^5.0.5", "tsx": "^4.7.1", "typescript": "^5.3.3", "vite": "^5.1.4", diff --git a/prisma/migrations/20240130113044_/migration.sql b/prisma/migrations/20240130113044_/migration.sql deleted file mode 100644 index b274935..0000000 --- a/prisma/migrations/20240130113044_/migration.sql +++ /dev/null @@ -1,89 +0,0 @@ --- CreateTable -CREATE TABLE "Company" ( - "id" TEXT NOT NULL PRIMARY KEY, - "name" TEXT NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL -); - --- CreateTable -CREATE TABLE "UserCompany" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "userId" TEXT NOT NULL, - "companyId" TEXT NOT NULL, - "isOwner" BOOLEAN NOT NULL DEFAULT false, - CONSTRAINT "UserCompany_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT "UserCompany_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company" ("id") ON DELETE RESTRICT ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "Account" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "companyId" TEXT NOT NULL, - "name" TEXT NOT NULL, - "balance" REAL NOT NULL, - "type" TEXT NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL, - CONSTRAINT "Account_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company" ("id") ON DELETE RESTRICT ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "Transaction" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "companyId" TEXT NOT NULL, - "date" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "accountId" INTEGER NOT NULL, - "description" TEXT NOT NULL, - "amount" REAL NOT NULL, - "type" TEXT NOT NULL, - "category" TEXT NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL, - "userId" TEXT, - CONSTRAINT "Transaction_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT "Transaction_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT "Transaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "Invoice" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "companyId" TEXT NOT NULL, - "dateIssued" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "dueDate" DATETIME NOT NULL, - "totalAmount" REAL NOT NULL, - "status" TEXT NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL, - "userId" TEXT, - CONSTRAINT "Invoice_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT "Invoice_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "InvoiceItem" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "invoiceId" INTEGER NOT NULL, - "description" TEXT NOT NULL, - "quantity" INTEGER NOT NULL, - "price" REAL NOT NULL, - "total" REAL NOT NULL DEFAULT 0, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL, - CONSTRAINT "InvoiceItem_invoiceId_fkey" FOREIGN KEY ("invoiceId") REFERENCES "Invoice" ("id") ON DELETE RESTRICT ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "_RoleToUserCompany" ( - "A" TEXT NOT NULL, - "B" INTEGER NOT NULL, - CONSTRAINT "_RoleToUserCompany_A_fkey" FOREIGN KEY ("A") REFERENCES "Role" ("id") ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT "_RoleToUserCompany_B_fkey" FOREIGN KEY ("B") REFERENCES "UserCompany" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); - --- CreateIndex -CREATE UNIQUE INDEX "_RoleToUserCompany_AB_unique" ON "_RoleToUserCompany"("A", "B"); - --- CreateIndex -CREATE INDEX "_RoleToUserCompany_B_index" ON "_RoleToUserCompany"("B"); diff --git a/server/dev-server.js b/server/dev-server.js index fb61456..2b8bdae 100644 --- a/server/dev-server.js +++ b/server/dev-server.js @@ -4,7 +4,7 @@ if (process.env.NODE_ENV === 'production') { await import('./index.js') } else { const command = - 'tsx watch --clear-screen=false --ignore "app/**" --ignore "build/**" --ignore "node_modules/**" --inspect ./index.js' + 'tsx watch --clear-screen=false --ignore ".cache/**" --ignore "app/**" --ignore "vite.config.ts.timestamp-*" --ignore "build/**" --ignore "node_modules/**" --inspect ./index.js' execa(command, { stdio: ['ignore', 'inherit', 'inherit'], shell: true, diff --git a/server/index.ts b/server/index.ts index 6a5b135..4b668ee 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,15 +1,6 @@ import crypto from 'crypto' -import path from 'path' -import { fileURLToPath } from 'url' -import { - createRequestHandler as _createRequestHandler, - type RequestHandler, -} from '@remix-run/express' -import { - broadcastDevReady, - installGlobals, - type ServerBuild, -} from '@remix-run/node' +import { createRequestHandler as _createRequestHandler } from '@remix-run/express' +import { type ServerBuild, installGlobals } from '@remix-run/node' import * as Sentry from '@sentry/remix' import { ip as ipAddress } from 'address' import chalk from 'chalk' @@ -23,21 +14,21 @@ import morgan from 'morgan' installGlobals() -const MODE = process.env.NODE_ENV +const MODE = process.env.NODE_ENV ?? 'development' -const createRequestHandler = Sentry.wrapExpressCreateRequestHandler( - _createRequestHandler, -) - -const BUILD_PATH = '../build/index.js' -const WATCH_PATH = '../build/version.txt' +const createRequestHandler = + MODE === 'production' + ? Sentry.wrapExpressCreateRequestHandler(_createRequestHandler) + : _createRequestHandler -/** - * Initial build - * @type {ServerBuild} - */ -const build = await import(BUILD_PATH) -let devBuild = build +const viteDevServer = + MODE === 'production' + ? undefined + : await import('vite').then(vite => + vite.createServer({ + server: { middlewareMode: true }, + }), + ) const app = express() @@ -79,23 +70,27 @@ app.disable('x-powered-by') app.use(Sentry.Handlers.requestHandler()) app.use(Sentry.Handlers.tracingHandler()) -// Remix fingerprints its assets so we can cache forever. -app.use( - '/build', - express.static('public/build', { immutable: true, maxAge: '1y' }), -) +if (viteDevServer) { + app.use(viteDevServer.middlewares) +} else { + // Remix fingerprints its assets so we can cache forever. + app.use( + '/assets', + express.static('build/client/assets', { immutable: true, maxAge: '1y' }), + ) -// Everything else (like favicon.ico) is cached for an hour. You may want to be -// more aggressive with this caching. -app.use(express.static('public', { maxAge: '1h' })) + // Everything else (like favicon.ico) is cached for an hour. You may want to be + // more aggressive with this caching. + app.use(express.static('build/client', { maxAge: '1h' })) +} -app.get(['/build/*', '/img/*', '/fonts/*', '/favicons/*'], (req, res) => { +app.get(['/img/*', '/favicons/*'], (req, res) => { // if we made it past the express.static for these, then we're missing something. // So we'll just send a 404 and won't bother calling other middleware. return res.status(404).send('Not found') }) -morgan.token('url', (req, res) => decodeURIComponent(req.url ?? '')) +morgan.token('url', req => decodeURIComponent(req.url ?? '')) app.use( morgan('tiny', { skip: (req, res) => @@ -199,18 +194,27 @@ app.use((req, res, next) => { return generalRateLimit(req, res, next) }) -function getRequestHandler(build: ServerBuild): RequestHandler { - function getLoadContext(_: any, res: any) { - return { cspNonce: res.locals.cspNonce } - } - return createRequestHandler({ build, mode: MODE, getLoadContext }) +async function getBuild() { + const build = viteDevServer + ? viteDevServer.ssrLoadModule('virtual:remix/server-build') + : // @ts-ignore this should exist before running the server + // but it may not exist just yet. + await import('#build/server/index.js') + // not sure how to make this happy 🤷‍♂️ + return build as unknown as ServerBuild } app.all( '*', - MODE === 'development' - ? (...args) => getRequestHandler(devBuild)(...args) - : getRequestHandler(build), + createRequestHandler({ + getLoadContext: (_: any, res: any) => ({ + cspNonce: res.locals.cspNonce, + serverBuild: getBuild(), + }), + mode: MODE, + // @sentry/remix needs to be updated to handle the function signature + build: MODE === 'production' ? await getBuild() : getBuild, + }), ) const desiredPort = Number(process.env.PORT || 3000) @@ -224,8 +228,8 @@ const server = app.listen(portToUse, () => { desiredPort === portToUse ? desiredPort : addy && typeof addy === 'object' - ? addy.port - : 0 + ? addy.port + : 0 if (portUsed !== desiredPort) { console.warn( @@ -252,10 +256,6 @@ ${lanUrl ? `${chalk.bold('On Your Network:')} ${chalk.cyan(lanUrl)}` : ''} ${chalk.bold('Press Ctrl+C to stop')} `.trim(), ) - - if (MODE === 'development') { - broadcastDevReady(build) - } }) closeWithGrace(async () => { @@ -263,24 +263,3 @@ closeWithGrace(async () => { server.close(e => (e ? reject(e) : resolve('ok'))) }) }) - -// during dev, we'll keep the build module up to date with the changes -if (MODE === 'development') { - async function reloadBuild() { - devBuild = await import(`${BUILD_PATH}?update=${Date.now()}`) - broadcastDevReady(devBuild) - } - - // We'll import chokidar here so doesn't get bundled in production. - const chokidar = await import('chokidar') - - const dirname = path.dirname(fileURLToPath(import.meta.url)) - const watchPath = path.join(dirname, WATCH_PATH).replace(/\\/g, '/') - - const buildWatcher = chokidar - .watch(watchPath, { ignoreInitial: true }) - .on('add', reloadBuild) - .on('change', reloadBuild) - - closeWithGrace(() => buildWatcher.close()) -} diff --git a/tests/e2e/notes.test.ts b/tests/e2e/notes.test.ts deleted file mode 100644 index 449a224..0000000 --- a/tests/e2e/notes.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { faker } from '@faker-js/faker' -import { prisma } from '#app/utils/db.server.ts' -import { expect, test } from '#tests/playwright-utils.ts' - -test('Users can create notes', async ({ page, login }) => { - const user = await login() - await page.goto(`/users/${user.username}/notes`) - - const newNote = createNote() - await page.getByRole('link', { name: /New Note/i }).click() - - // fill in form and submit - await page.getByRole('textbox', { name: /title/i }).fill(newNote.title) - await page.getByRole('textbox', { name: /content/i }).fill(newNote.content) - - await page.getByRole('button', { name: /submit/i }).click() - await expect(page).toHaveURL(new RegExp(`/users/${user.username}/notes/.*`)) -}) - -test('Users can edit notes', async ({ page, login }) => { - const user = await login() - - const note = await prisma.note.create({ - select: { id: true }, - data: { ...createNote(), ownerId: user.id }, - }) - await page.goto(`/users/${user.username}/notes/${note.id}`) - - // edit the note - await page.getByRole('link', { name: 'Edit', exact: true }).click() - const updatedNote = createNote() - await page.getByRole('textbox', { name: /title/i }).fill(updatedNote.title) - await page - .getByRole('textbox', { name: /content/i }) - .fill(updatedNote.content) - await page.getByRole('button', { name: /submit/i }).click() - - await expect(page).toHaveURL(new RegExp(`/users/${user.username}/notes/.*`)) - await expect( - page.getByRole('heading', { name: updatedNote.title }), - ).toBeVisible() -}) - -test('Users can delete notes', async ({ page, login }) => { - const user = await login() - - const note = await prisma.note.create({ - select: { id: true }, - data: { ...createNote(), ownerId: user.id }, - }) - await page.goto(`/users/${user.username}/notes/${note.id}`) - - // find links with href prefix - const noteLinks = page - .getByRole('main') - .getByRole('list') - .getByRole('listitem') - .getByRole('link') - const countBefore = await noteLinks.count() - await page.getByRole('button', { name: /delete/i }).click() - await expect( - page.getByText('Your note has been deleted.', { exact: true }), - ).toBeVisible() - await expect(page).toHaveURL(`/users/${user.username}/notes`) - const countAfter = await noteLinks.count() - expect(countAfter).toEqual(countBefore - 1) -}) - -function createNote() { - return { - title: faker.lorem.words(3), - content: faker.lorem.paragraphs(3), - } -} diff --git a/tests/e2e/search.test.ts b/tests/e2e/search.test.ts deleted file mode 100644 index 270eb4e..0000000 --- a/tests/e2e/search.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { invariant } from '@epic-web/invariant' -import { expect, test } from '#tests/playwright-utils.ts' - -test('Search from home page', async ({ page, insertNewUser }) => { - const newUser = await insertNewUser() - await page.goto('/') - - await page.getByRole('searchbox', { name: /search/i }).fill(newUser.username) - await page.getByRole('button', { name: /search/i }).click() - - await page.waitForURL( - `/users?${new URLSearchParams({ search: newUser.username })}`, - ) - await expect(page.getByText('Epic Notes Users')).toBeVisible() - const userList = page.getByRole('main').getByRole('list') - await expect(userList.getByRole('listitem')).toHaveCount(1) - invariant(newUser.name, 'User name not found') - await expect(page.getByAltText(newUser.name)).toBeVisible() - - await page.getByRole('searchbox', { name: /search/i }).fill('__nonexistent__') - await page.getByRole('button', { name: /search/i }).click() - await page.waitForURL(`/users?search=__nonexistent__`) - - await expect(userList.getByRole('listitem')).not.toBeVisible() - await expect(page.getByText(/no users found/i)).toBeVisible() -}) diff --git a/tests/playwright-utils.ts b/tests/playwright-utils.ts index 7384365..e163142 100644 --- a/tests/playwright-utils.ts +++ b/tests/playwright-utils.ts @@ -50,7 +50,6 @@ async function getOrInsertUser({ email, username, roles: { connect: { name: 'user' } }, - platformStatusKey: 'ACTIVE', password: { create: { hash: await getPasswordHash(password) } }, }, }) diff --git a/tsconfig.json b/tsconfig.json index c99639a..b92f566 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "isolatedModules": true, "esModuleInterop": true, "jsx": "react-jsx", - "module": "ES2022", + "module": "ESNext", "target": "ES2022", "moduleResolution": "bundler", "resolveJsonModule": true, @@ -17,11 +17,11 @@ "#*": ["./*"], "@/icon-name": [ "./app/components/ui/icons/name.d.ts", - "./types/icon-name.d.ts" - ] + "./types/icon-name.d.ts", + ], }, "skipLibCheck": true, "allowImportingTsExtensions": true, - "noEmit": true - } -} \ No newline at end of file + "noEmit": true, + }, +} diff --git a/types/env.env.d.ts b/types/env.env.d.ts new file mode 100644 index 0000000..8d2f951 --- /dev/null +++ b/types/env.env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/types/remix.env.d.ts b/types/remix.env.d.ts deleted file mode 100644 index 72e2aff..0000000 --- a/types/remix.env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..b4e95c3 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,36 @@ +import { vitePlugin as remix } from '@remix-run/dev' +import { flatRoutes } from 'remix-flat-routes' +import { defineConfig } from 'vite' + +const MODE = process.env.NODE_ENV + +export default defineConfig({ + build: { + cssMinify: MODE === 'production', + rollupOptions: { + external: [/node:.*/, 'stream', 'crypto', 'fsevents'], + }, + }, + plugins: [ + remix({ + ignoredRouteFiles: ['**/*'], + serverModuleFormat: 'esm', + routes: async defineRoutes => { + return flatRoutes('routes', defineRoutes, { + ignoredRouteFiles: [ + '.*', + '**/*.css', + '**/*.test.{js,jsx,ts,tsx}', + '**/__*.*', + // This is for server-side utilities you want to colocate next to + // your routes without making an additional directory. + // If you need a route that includes "server" or "client" in the + // filename, use the escape brackets like: my-route.[server].tsx + '**/*.server.*', + '**/*.client.*', + ], + }) + }, + }), + ], +}) From 49fdb021636f9e9a47549a722a7a8cde5475f6d6 Mon Sep 17 00:00:00 2001 From: itsmegood Date: Fri, 23 Feb 2024 08:32:54 +0530 Subject: [PATCH 3/3] minor update --- package-lock.json | 161 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 111 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3076add..e368937 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@paralleldrive/cuid2": "^2.2.2", "@prisma/client": "^5.10.2", "@radix-ui/react-checkbox": "^1.0.4", -"@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", @@ -56,7 +56,7 @@ "litefs-js": "^1.1.2", "lru-cache": "^10.2.0", "morgan": "^1.10.0", - "qrcode": "^1.5.3", + "qrcode": "^1.5.3", "react": "^18.2.0", "react-dom": "^18.2.0", "remix-auth": "^3.6.0", @@ -115,8 +115,9 @@ "prettier": "^3.2.5", "prettier-plugin-sql": "^0.18.0", "prettier-plugin-tailwindcss": "^0.5.11", -"prisma": "^5.10.2", + "prisma": "^5.10.2", "remix-flat-routes": "^0.6.4", + "rimraf": "^5.0.5", "tsx": "^4.7.1", "typescript": "^5.3.3", "vite": "^5.1.4", @@ -2149,12 +2150,14 @@ "node_modules/@prisma/debug": { "version": "5.10.2", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.10.2.tgz", - "integrity": "sha512-bkBOmH9dpEBbMKFJj8V+Zp8IZHIBjy3fSyhLhxj4FmKGb/UBSt9doyfA6k1UeUREsMJft7xgPYBbHSOYBr8XCA==" + "integrity": "sha512-bkBOmH9dpEBbMKFJj8V+Zp8IZHIBjy3fSyhLhxj4FmKGb/UBSt9doyfA6k1UeUREsMJft7xgPYBbHSOYBr8XCA==", + "dev": true }, "node_modules/@prisma/engines": { "version": "5.10.2", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.10.2.tgz", "integrity": "sha512-HkSJvix6PW8YqEEt3zHfCYYJY69CXsNdhU+wna+4Y7EZ+AwzeupMnUThmvaDA7uqswiHkgm5/SZ6/4CStjaGmw==", + "dev": true, "hasInstallScript": true, "dependencies": { "@prisma/debug": "5.10.2", @@ -2166,12 +2169,14 @@ "node_modules/@prisma/engines-version": { "version": "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9.tgz", - "integrity": "sha512-uCy/++3Jx/O3ufM+qv2H1L4tOemTNqcP/gyEVOlZqTpBvYJUe0tWtW0y3o2Ueq04mll4aM5X3f6ugQftOSLdFQ==" + "integrity": "sha512-uCy/++3Jx/O3ufM+qv2H1L4tOemTNqcP/gyEVOlZqTpBvYJUe0tWtW0y3o2Ueq04mll4aM5X3f6ugQftOSLdFQ==", + "dev": true }, "node_modules/@prisma/fetch-engine": { "version": "5.10.2", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.10.2.tgz", "integrity": "sha512-dSmXcqSt6DpTmMaLQ9K8ZKzVAMH3qwGCmYEZr/uVnzVhxRJ1EbT/w2MMwIdBNq1zT69Rvh0h75WMIi0mrIw7Hg==", + "dev": true, "dependencies": { "@prisma/debug": "5.10.2", "@prisma/engines-version": "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9", @@ -2182,6 +2187,7 @@ "version": "5.10.2", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.10.2.tgz", "integrity": "sha512-nqXP6vHiY2PIsebBAuDeWiUYg8h8mfjBckHh6Jezuwej0QJNnjDiOq30uesmg+JXxGk99nqyG3B7wpcOODzXvg==", + "dev": true, "dependencies": { "@prisma/debug": "5.10.2" } @@ -2307,6 +2313,42 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", + "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", @@ -10726,6 +10768,63 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/flat-cache/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/flat-cache/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flat-cache/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/flatted": { "version": "3.2.9", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", @@ -15567,6 +15666,7 @@ "version": "5.10.2", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.10.2.tgz", "integrity": "sha512-hqb/JMz9/kymRE25pMWCxkdyhbnIWrq+h7S6WysJpdnCvhstbJSNP/S6mScEcqiB8Qv2F+0R3yG+osRaWqZacQ==", + "dev": true, "hasInstallScript": true, "dependencies": { "@prisma/engines": "5.10.2" @@ -16673,62 +16773,23 @@ } }, "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", "dev": true, "dependencies": { - "glob": "^7.1.3" + "glob": "^10.3.7" }, "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "rimraf": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": ">=14" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/rollup": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.5.tgz",