diff --git a/actions/auth.ts b/actions/auth.ts new file mode 100644 index 00000000..9a1aaf27 --- /dev/null +++ b/actions/auth.ts @@ -0,0 +1,178 @@ +'use server'; + +import { hash, verify } from '@node-rs/argon2'; +import { eq } from 'drizzle-orm'; +import { generateIdFromEntropySize } from 'lucia'; +import { revalidatePath } from 'next/cache'; +import { cookies } from 'next/headers'; +import { db } from '~/drizzle/db'; +import { user } from '~/drizzle/schema'; +import { lucia, validateRequest } from '~/utils/auth'; +import { + createUserFormDataSchema, + getUserFormDataSchema, +} from '~/utils/authSchema'; + +export async function signup( + currentState: { + success: boolean; + error: null | string; + }, + formData: FormData, +) { + const parsedFormData = createUserFormDataSchema.safeParse(formData); + + if (!parsedFormData.success) { + return { + success: false, + error: parsedFormData.error.message, + }; + } + + try { + const { username, password } = parsedFormData.data; + + const passwordHash = await hash(password, { + // recommended minimum parameters + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1, + }); + + const userId = generateIdFromEntropySize(10); + + // check if username is taken + const [existingUser] = await db + .select() + .from(user) + .where(eq(user.username, username)) + .limit(1); + + if (existingUser) throw new Error('Username already taken!'); + + // create user + await db.insert(user).values({ + id: userId, + username, + passwordHash, + }); + + const session = await lucia.createSession(userId, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + cookies().set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes, + ); + return { error: null, success: true }; + } catch (error) { + return { + success: false, + error: 'Username already taken!', + }; + } +} + +export async function signin( + currentState: { + success: boolean; + error: null | string; + }, + formData: FormData, +) { + const parsedFormData = getUserFormDataSchema.safeParse(formData); + + if (!parsedFormData.success) { + return { + success: false, + error: parsedFormData.error.message, + }; + } + + try { + const { username, password } = parsedFormData.data; + + const [existingUser] = await db + .select() + .from(user) + .where(eq(user.username, username)) + .limit(1); + + if (!existingUser) { + // NOTE: + // Returning immediately allows malicious actors to figure out valid usernames from response times, + // allowing them to only focus on guessing passwords in brute-force attacks. + // As a preventive measure, you may want to hash passwords even for invalid usernames. + // However, valid usernames can be already be revealed with the signup page among other methods. + // It will also be much more resource intensive. + // Since protecting against this is non-trivial, + // it is crucial your implementation is protected against brute-force attacks with login throttling etc. + // If usernames are public, you may outright tell the user that the username is invalid. + // eslint-disable-next-line no-console + console.log('invalid username'); + return { + success: false, + error: 'Incorrect username or password!', + }; + } + + const validPassword = await verify(existingUser.passwordHash, password, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1, + }); + if (!validPassword) { + return { + success: false, + error: 'Incorrect username or password!', + }; + } + + const session = await lucia.createSession(existingUser.id, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + cookies().set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes, + ); + + return { + success: true, + error: null, + }; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error while signing in', error); + return { + success: false, + error: 'Something went wrong! Please try again.', + }; + } +} + +export async function signout() { + const { session } = await validateRequest(); + if (!session) { + return { + success: false, + error: 'Unauthorized', + }; + } + + await lucia.invalidateSession(session.id); + + const sessionCookie = lucia.createBlankSessionCookie(); + cookies().set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes, + ); + + revalidatePath('/'); + return { + success: true, + error: null, + }; +} diff --git a/app/_components/SignOutBtn.tsx b/app/_components/SignOutBtn.tsx new file mode 100644 index 00000000..aa9a9cae --- /dev/null +++ b/app/_components/SignOutBtn.tsx @@ -0,0 +1,11 @@ +"use client"; + +import React from "react"; +import { signout } from "~/actions/auth"; +import { Button } from "~/components/ui/button"; + +const SignOutBtn = () => { + return ; +}; + +export default SignOutBtn; diff --git a/app/globals.css b/app/globals.css index d4f7a570..46f918d6 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,25 +2,81 @@ @tailwind components; @tailwind utilities; -:root { - --foreground-rgb: 0, 0, 0; - --background-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { +@layer base { :root { - --foreground-rgb: 255, 255, 255; - --background-rgb: 0, 0, 0; + --platinum-hue: 210; + --platinum-saturation: 43%; + --platinum-lightness: 95%; + --platinum: var(--platinum-hue) var(--platinum-saturation) + var(--platinum-lightness); + + --background: var(--platinum); + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; } -} -body { - color: rgb(var(--foreground-rgb)); - background-color: rgb(var(--background-rgb)); + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } } -@layer utilities { - .text-balance { - text-wrap: balance; +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; } } diff --git a/app/page.tsx b/app/page.tsx index d167eb45..a2e06d1f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,11 @@ import { getOrganizations } from '~/actions/organizations'; import CreateOrgForm from '~/app/[org]/_components/CreateOrgForm'; +import { requirePageAuth } from "~/utils/auth"; +import SignOutBtn from "./_components/SignOutBtn"; export default async function Home() { + await requirePageAuth(); + const allOrgs = await getOrganizations(); return (
@@ -15,6 +19,8 @@ export default async function Home() { ))} + +
); } diff --git a/app/signin/_components/SignInForm.tsx b/app/signin/_components/SignInForm.tsx new file mode 100644 index 00000000..34a50cf0 --- /dev/null +++ b/app/signin/_components/SignInForm.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { useFormState } from "react-dom"; +import { signin } from "~/actions/auth"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; + +const SignInForm = () => { + const initialState = { error: null, success: false }; + const [formState, formAction] = useFormState(signin, initialState); + + return ( +
+ {formState.error && ( +
+ {formState.error} +
+ )} + +
+ + +
+
+
+ + +
+
+ +
+ ); +}; + +export default SignInForm; diff --git a/app/signin/page.tsx b/app/signin/page.tsx new file mode 100644 index 00000000..3343ce13 --- /dev/null +++ b/app/signin/page.tsx @@ -0,0 +1,39 @@ +import Link from "next/link"; +import { redirect } from "next/navigation"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { validateRequest } from "~/utils/auth"; +import SignInForm from "./_components/SignInForm"; + +export default async function Page() { + const { session, user } = await validateRequest(); + + if (session && user) { + // If the user is already signed in, redirect to the home page + redirect("/"); + } + + return ( +
+ + + Sign in to Studio + + Don't have an account?{" "} + + Sign Up + + + + + + + +
+ ); +} diff --git a/app/signup/_components/SignUpForm.tsx b/app/signup/_components/SignUpForm.tsx new file mode 100644 index 00000000..3e9b5a13 --- /dev/null +++ b/app/signup/_components/SignUpForm.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { useFormState } from "react-dom"; +import { signup } from "~/actions/auth"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; + +const SignUpForm = () => { + const initialState = { error: null, success: false }; + const [formState, formAction] = useFormState(signup, initialState); + + return ( +
+ {formState.error && ( +
+ {formState.error} +
+ )} + +
+ + +
+
+
+ + +
+
+ +
+ ); +}; + +export default SignUpForm; diff --git a/app/signup/page.tsx b/app/signup/page.tsx new file mode 100644 index 00000000..cc9e5c40 --- /dev/null +++ b/app/signup/page.tsx @@ -0,0 +1,39 @@ +import Link from "next/link"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import SignUpForm from "./_components/SignUpForm"; +import { validateRequest } from "~/utils/auth"; +import { redirect } from "next/navigation"; + +export default async function Page() { + const { session, user } = await validateRequest(); + + if (session && user) { + // If the user is already signed in, redirect to the home page + redirect("/"); + } + + return ( +
+ + + Create an account + + Already have an account?{" "} + + Sign In + + + + + + + +
+ ); +} diff --git a/components.json b/components.json new file mode 100644 index 00000000..57e19038 --- /dev/null +++ b/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "~/components", + "utils": "~/lib/utils" + } +} \ No newline at end of file diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 00000000..61dc9df8 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '~/lib/utils'; + +const buttonVariants = cva( + 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: + 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + outline: + 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + secondary: + 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + icon: 'h-10 w-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +); + +type ButtonProps = { + asChild?: boolean; +} & React.ButtonHTMLAttributes & + VariantProps; + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; + return ( + + ); + }, +); +Button.displayName = 'Button'; + +export { Button }; diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 00000000..6267c6b0 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from 'react'; + +import { cn } from '~/lib/utils'; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = 'Card'; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = 'CardHeader'; + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardTitle.displayName = 'CardTitle'; + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardDescription.displayName = 'CardDescription'; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardContent.displayName = 'CardContent'; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = 'CardFooter'; + +export { Card, CardHeader, CardTitle, CardDescription, CardContent }; diff --git a/components/ui/input.tsx b/components/ui/input.tsx new file mode 100644 index 00000000..53b6f7ed --- /dev/null +++ b/components/ui/input.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; + +import { cn } from '~/lib/utils'; + +type InputProps = object & React.InputHTMLAttributes; + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = 'Input'; + +export { Input }; diff --git a/components/ui/label.tsx b/components/ui/label.tsx new file mode 100644 index 00000000..8f407389 --- /dev/null +++ b/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "~/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/drizzle/db.ts b/drizzle/db.ts index 880be7e8..7e6a26e6 100644 --- a/drizzle/db.ts +++ b/drizzle/db.ts @@ -1,16 +1,18 @@ -import { env } from "~/env"; -import * as schema from "./schema"; -import { PostgresJsDatabase, drizzle } from "drizzle-orm/postgres-js"; -import postgres from "postgres"; +import { env } from '~/env'; +import * as schema from './schema'; +import { type PostgresJsDatabase, drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; declare global { + // Todo: figure out how to fix linting here + // eslint-disable-next-line no-var var db: PostgresJsDatabase | undefined; } let db: PostgresJsDatabase; let pg: ReturnType; -if (env.NODE_ENV === "production") { +if (env.NODE_ENV === 'production') { pg = postgres(env.DATABASE_URL); db = drizzle(pg, { schema }); } else { diff --git a/drizzle/schema.ts b/drizzle/schema.ts index b236eb7f..f5ec35ba 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -3,7 +3,8 @@ import { pgTable, serial, text, -} from 'drizzle-orm/pg-core'; + timestamp, +} from "drizzle-orm/pg-core"; export const organizations = pgTable('organizations', { id: serial('id').primaryKey(), @@ -31,3 +32,22 @@ export const protocols = pgTable('protocols', { }); export type Project = typeof projects.$inferSelect; + +export const user = pgTable("user", { + id: text("id").primaryKey(), + username: text("username").notNull().unique(), + passwordHash: text("password_hash").notNull(), +}); + +export const session = pgTable("session", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => user.id), + expiresAt: timestamp("expires_at", { + withTimezone: true, + mode: "date", + }).notNull(), +}); + +export type UserType = typeof user.$inferSelect; diff --git a/env.ts b/env.ts index a01e7d3a..ff9858fb 100644 --- a/env.ts +++ b/env.ts @@ -1,5 +1,6 @@ -import { createEnv } from "@t3-oss/env-nextjs"; -import { z } from "zod"; +/* eslint-disable no-process-env */ +import { createEnv } from '@t3-oss/env-nextjs'; +import { z } from 'zod'; export const env = createEnv({ server: { @@ -8,8 +9,8 @@ export const env = createEnv({ client: {}, shared: { NODE_ENV: z - .enum(["development", "test", "production"]) - .default("development"), + .enum(['development', 'test', 'production']) + .default('development'), }, runtimeEnv: { DATABASE_URL: process.env.DATABASE_URL, diff --git a/knip.json b/knip.json index 26fe4090..10dd0c9e 100644 --- a/knip.json +++ b/knip.json @@ -1,5 +1,5 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", - "ignoreDependencies": ["server-only", "sharp"], + "ignoreDependencies": ["sharp"], "ignoreBinaries": ["docker-compose"] } diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 00000000..d084ccad --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/next.config.js b/next.config.js index 4678774e..d37307a9 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,9 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + // Todo: remove this config once we upgrade to Next 15 + experimental: { + serverComponentsExternalPackages: ['@node-rs/argon2'], + }, +}; export default nextConfig; diff --git a/package.json b/package.json index c055ef48..24a9e4e9 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,19 @@ "knip": "SKIP_ENV_VALIDATION=true knip" }, "dependencies": { + "@lucia-auth/adapter-drizzle": "^1.0.7", + "@node-rs/argon2": "^1.8.3", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-slot": "^1.0.2", "@t3-oss/env-nextjs": "^0.10.1", "@typescript-eslint/eslint-plugin": "^7.12.0", "@typescript-eslint/parser": "^7.12.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", "drizzle-orm": "^0.31.1", - "knip": "^5.17.4", "eslint-config-prettier": "^9.1.0", + "knip": "^5.17.4", + "lucia": "^3.2.0", "next": "14.2.3", "postgres": "^3.4.4", "prettier": "^3.3.1", @@ -27,12 +34,17 @@ "react": "^18", "react-dom": "^18", "sharp": "^0.33.4", - "zod": "^3.23.8" + "tailwind-merge": "^2.3.0", + "tailwindcss-animate": "^1.0.7", + "validator": "^13.12.0", + "zod": "^3.23.8", + "zod-form-data": "^2.0.2" }, "devDependencies": { "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@types/validator": "^13.11.10", "autoprefixer": "^10.4.19", "drizzle-kit": "^0.22.2", "eslint": "^8.57.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87b34d24..17a2eba6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,18 @@ importers: .: dependencies: + '@lucia-auth/adapter-drizzle': + specifier: ^1.0.7 + version: 1.0.7(lucia@3.2.0) + '@node-rs/argon2': + specifier: ^1.8.3 + version: 1.8.3 + '@radix-ui/react-label': + specifier: ^2.0.2 + version: 2.0.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': + specifier: ^1.0.2 + version: 1.0.2(@types/react@18.3.3)(react@18.3.1) '@t3-oss/env-nextjs': specifier: ^0.10.1 version: 0.10.1(typescript@5.4.5)(zod@3.23.8) @@ -17,15 +29,24 @@ importers: '@typescript-eslint/parser': specifier: ^7.12.0 version: 7.12.0(eslint@8.57.0)(typescript@5.4.5) + class-variance-authority: + specifier: ^0.7.0 + version: 0.7.0 + clsx: + specifier: ^2.1.1 + version: 2.1.1 drizzle-orm: specifier: ^0.31.1 version: 0.31.1(@types/react@18.3.3)(postgres@3.4.4)(react@18.3.1) - knip: - specifier: ^5.17.4 - version: 5.17.4(@types/node@20.14.1)(typescript@5.4.5) eslint-config-prettier: specifier: ^9.1.0 version: 9.1.0(eslint@8.57.0) + knip: + specifier: ^5.17.4 + version: 5.17.4(@types/node@20.14.2)(typescript@5.4.5) + lucia: + specifier: ^3.2.0 + version: 3.2.0 next: specifier: 14.2.3 version: 14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -47,25 +68,40 @@ importers: sharp: specifier: ^0.33.4 version: 0.33.4 + tailwind-merge: + specifier: ^2.3.0 + version: 2.3.0 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.4) + validator: + specifier: ^13.12.0 + version: 13.12.0 zod: specifier: ^3.23.8 version: 3.23.8 + zod-form-data: + specifier: ^2.0.2 + version: 2.0.2(zod@3.23.8) devDependencies: '@types/node': specifier: ^20 - version: 20.14.1 + version: 20.14.2 '@types/react': specifier: ^18 version: 18.3.3 '@types/react-dom': specifier: ^18 version: 18.3.0 + '@types/validator': + specifier: ^13.11.10 + version: 13.11.10 autoprefixer: specifier: ^10.4.19 version: 10.4.19(postcss@8.4.38) drizzle-kit: specifier: ^0.22.2 - version: 0.22.2 + version: 0.22.4 eslint: specifier: ^8.57.0 version: 8.57.0 @@ -77,7 +113,7 @@ importers: version: 8.4.38 tailwindcss: specifier: ^3.4.1 - version: 3.4.3 + version: 3.4.4 typescript: specifier: ^5 version: 5.4.5 @@ -88,13 +124,25 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@babel/runtime@7.24.6': - resolution: {integrity: sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==} + '@babel/runtime@7.24.7': + resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==} engines: {node: '>=6.9.0'} + '@emnapi/core@0.45.0': + resolution: {integrity: sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==} + + '@emnapi/core@1.2.0': + resolution: {integrity: sha512-E7Vgw78I93we4ZWdYCb4DGAwRROGkMIXk7/y87UmANR+J6qsWusmC3gLt0H+O0KOt5e6O38U8oJamgbudrES/w==} + + '@emnapi/runtime@0.45.0': + resolution: {integrity: sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==} + '@emnapi/runtime@1.2.0': resolution: {integrity: sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==} + '@emnapi/wasi-threads@1.0.1': + resolution: {integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==} + '@ericcornelissen/bash-parser@0.5.2': resolution: {integrity: sha512-4pIMTa1nEFfMXitv7oaNEWOdM+zpOZavesa5GaiWTgda6Zk32CFGxjUp/iIaN0PwgUW1yTq/fztSjbpE8SLGZQ==} engines: {node: '>=4'} @@ -539,6 +587,14 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@lucia-auth/adapter-drizzle@1.0.7': + resolution: {integrity: sha512-X/V7fLBca8EC/gPXCntwbQpb0+F9oEuRoHElvsi9rCrdnGhCMNxHgwAvgiQ6pes+rIYpyvx4n3hvjqo/fPo03A==} + peerDependencies: + lucia: 3.x + + '@napi-rs/wasm-runtime@0.2.4': + resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} + '@next/env@14.2.3': resolution: {integrity: sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==} @@ -599,6 +655,267 @@ packages: cpu: [x64] os: [win32] + '@node-rs/argon2-android-arm-eabi@1.7.0': + resolution: {integrity: sha512-udDqkr5P9E+wYX1SZwAVPdyfYvaF4ry9Tm+R9LkfSHbzWH0uhU6zjIwNRp7m+n4gx691rk+lqqDAIP8RLKwbhg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@node-rs/argon2-android-arm-eabi@1.8.3': + resolution: {integrity: sha512-JFZPlNM0A8Og+Tncb8UZsQrhEMlbHBXPsT3hRoKImzVmTmq28Os0ucFWow0AACp2coLHBSydXH3Dh0lZup3rWw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@node-rs/argon2-android-arm64@1.7.0': + resolution: {integrity: sha512-s9j/G30xKUx8WU50WIhF0fIl1EdhBGq0RQ06lEhZ0Gi0ap8lhqbE2Bn5h3/G2D1k0Dx+yjeVVNmt/xOQIRG38A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@node-rs/argon2-android-arm64@1.8.3': + resolution: {integrity: sha512-zaf8P3T92caeW2xnMA7P1QvRA4pIt/04oilYP44XlTCtMye//vwXDMeK53sl7dvYiJKnzAWDRx41k8vZvpZazg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@node-rs/argon2-darwin-arm64@1.7.0': + resolution: {integrity: sha512-ZIz4L6HGOB9U1kW23g+m7anGNuTZ0RuTw0vNp3o+2DWpb8u8rODq6A8tH4JRL79S+Co/Nq608m9uackN2pe0Rw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@node-rs/argon2-darwin-arm64@1.8.3': + resolution: {integrity: sha512-DV/IbmLGdNXBtXb5o2UI5ba6kvqXqPAJgmMOTUCuHeBSp992GlLHdfU4rzGu0dNrxudBnunNZv+crd0YdEQSUA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@node-rs/argon2-darwin-x64@1.7.0': + resolution: {integrity: sha512-5oi/pxqVhODW/pj1+3zElMTn/YukQeywPHHYDbcAW3KsojFjKySfhcJMd1DjKTc+CHQI+4lOxZzSUzK7mI14Hw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@node-rs/argon2-darwin-x64@1.8.3': + resolution: {integrity: sha512-YMjmBGFZhLfYjfQ2gll9A+BZu/zAMV7lWZIbKxb7ZgEofILQwuGmExjDtY3Jplido/6leCEdpmlk2oIsME00LA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@node-rs/argon2-freebsd-x64@1.7.0': + resolution: {integrity: sha512-Ify08683hA4QVXYoIm5SUWOY5DPIT/CMB0CQT+IdxQAg/F+qp342+lUkeAtD5bvStQuCx/dFO3bnnzoe2clMhA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@node-rs/argon2-freebsd-x64@1.8.3': + resolution: {integrity: sha512-Hq3Rj5Yb2RolTG/luRPnv+XiGCbi5nAK25Pc8ou/tVapwX+iktEm/NXbxc5zsMxraYVkCvfdwBjweC5O+KqCGw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@node-rs/argon2-linux-arm-gnueabihf@1.7.0': + resolution: {integrity: sha512-7DjDZ1h5AUHAtRNjD19RnQatbhL+uuxBASuuXIBu4/w6Dx8n7YPxwTP4MXfsvuRgKuMWiOb/Ub/HJ3kXVCXRkg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@node-rs/argon2-linux-arm-gnueabihf@1.8.3': + resolution: {integrity: sha512-x49l8RgzKoG0/V0IXa5rrEl1TcJEc936ctlYFvqcunSOyowZ6kiWtrp1qrbOR8gbaNILl11KTF52vF6+h8UlEQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@node-rs/argon2-linux-arm64-gnu@1.7.0': + resolution: {integrity: sha512-nJDoMP4Y3YcqGswE4DvP080w6O24RmnFEDnL0emdI8Nou17kNYBzP2546Nasx9GCyLzRcYQwZOUjrtUuQ+od2g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@node-rs/argon2-linux-arm64-gnu@1.8.3': + resolution: {integrity: sha512-gJesam/qA63reGkb9qJ2TjFSLBtY41zQh2oei7nfnYsmVQPuHHWItJxEa1Bm21SPW53gZex4jFJbDIgj0+PxIw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@node-rs/argon2-linux-arm64-musl@1.7.0': + resolution: {integrity: sha512-BKWS8iVconhE3jrb9mj6t1J9vwUqQPpzCbUKxfTGJfc+kNL58F1SXHBoe2cDYGnHrFEHTY0YochzXoAfm4Dm/A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@node-rs/argon2-linux-arm64-musl@1.8.3': + resolution: {integrity: sha512-7O6kQdSKzB4Tjx/EBa8zKIxnmLkQE8VdJgPm6Ksrpn+ueo0mx2xf76fIDnbbTCtm3UbB+y+FkTo2wLA7tOqIKg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@node-rs/argon2-linux-x64-gnu@1.7.0': + resolution: {integrity: sha512-EmgqZOlf4Jurk/szW1iTsVISx25bKksVC5uttJDUloTgsAgIGReCpUUO1R24pBhu9ESJa47iv8NSf3yAfGv6jQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@node-rs/argon2-linux-x64-gnu@1.8.3': + resolution: {integrity: sha512-OBH+EFG7BGjFyldaao2H2gSCLmjtrrwf420B1L+lFn7JLW9UAjsIPFKAcWsYwPa/PwYzIge9Y7SGcpqlsSEX0w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@node-rs/argon2-linux-x64-musl@1.7.0': + resolution: {integrity: sha512-/o1efYCYIxjfuoRYyBTi2Iy+1iFfhqHCvvVsnjNSgO1xWiWrX0Rrt/xXW5Zsl7vS2Y+yu8PL8KFWRzZhaVxfKA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@node-rs/argon2-linux-x64-musl@1.8.3': + resolution: {integrity: sha512-bDbMuyekIxZaN7NaX+gHVkOyABB8bcMEJYeRPW1vCXKHj3brJns1wiUFSxqeUXreupifNVJlQfPt1Y5B/vFXgQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@node-rs/argon2-wasm32-wasi@1.7.0': + resolution: {integrity: sha512-Evmk9VcxqnuwQftfAfYEr6YZYSPLzmKUsbFIMep5nTt9PT4XYRFAERj7wNYp+rOcBenF3X4xoB+LhwcOMTNE5w==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@node-rs/argon2-wasm32-wasi@1.8.3': + resolution: {integrity: sha512-NBf2cMCDbNKMzp13Pog8ZPmI0M9U4Ak5b95EUjkp17kdKZFds12dwW67EMnj7Zy+pRqby2QLECaWebDYfNENTg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@node-rs/argon2-win32-arm64-msvc@1.7.0': + resolution: {integrity: sha512-qgsU7T004COWWpSA0tppDqDxbPLgg8FaU09krIJ7FBl71Sz8SFO40h7fDIjfbTT5w7u6mcaINMQ5bSHu75PCaA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@node-rs/argon2-win32-arm64-msvc@1.8.3': + resolution: {integrity: sha512-AHpPo7UbdW5WWjwreVpgFSY0o1RY4A7cUFaqDXZB2OqEuyrhMxBdZct9PX7PQKI18D85pLsODnR+gvVuTwJ6rQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@node-rs/argon2-win32-ia32-msvc@1.7.0': + resolution: {integrity: sha512-JGafwWYQ/HpZ3XSwP4adQ6W41pRvhcdXvpzIWtKvX+17+xEXAe2nmGWM6s27pVkg1iV2ZtoYLRDkOUoGqZkCcg==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@node-rs/argon2-win32-ia32-msvc@1.8.3': + resolution: {integrity: sha512-bqzn2rcQkEwCINefhm69ttBVVkgHJb/V03DdBKsPFtiX6H47axXKz62d1imi26zFXhOEYxhKbu3js03GobJOLw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@node-rs/argon2-win32-x64-msvc@1.7.0': + resolution: {integrity: sha512-9oq4ShyFakw8AG3mRls0AoCpxBFcimYx7+jvXeAf2OqKNO+mSA6eZ9z7KQeVCi0+SOEUYxMGf5UiGiDb9R6+9Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@node-rs/argon2-win32-x64-msvc@1.8.3': + resolution: {integrity: sha512-ILlrRThdbp5xNR5gwYM2ic1n/vG5rJ8dQZ+YMRqksl+lnTJ/6FDe5BOyIhiPtiDwlCiCtUA+1NxpDB9KlUCAIA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@node-rs/argon2@1.7.0': + resolution: {integrity: sha512-zfULc+/tmcWcxn+nHkbyY8vP3+MpEqKORbszt4UkpqZgBgDAAIYvuDN/zukfTgdmo6tmJKKVfzigZOPk4LlIog==} + engines: {node: '>= 10'} + + '@node-rs/argon2@1.8.3': + resolution: {integrity: sha512-sf/QAEI59hsMEEE2J8vO4hKrXrv4Oplte3KI2N4MhMDYpytH0drkVfErmHBfWFZxxIEK03fX1WsBNswS2nIZKg==} + engines: {node: '>= 10'} + + '@node-rs/bcrypt-android-arm-eabi@1.9.0': + resolution: {integrity: sha512-nOCFISGtnodGHNiLrG0WYLWr81qQzZKYfmwHc7muUeq+KY0sQXyHOwZk9OuNQAWv/lnntmtbwkwT0QNEmOyLvA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@node-rs/bcrypt-android-arm64@1.9.0': + resolution: {integrity: sha512-+ZrIAtigVmjYkqZQTThHVlz0+TG6D+GDHWhVKvR2DifjtqJ0i+mb9gjo++hN+fWEQdWNGxKCiBBjwgT4EcXd6A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@node-rs/bcrypt-darwin-arm64@1.9.0': + resolution: {integrity: sha512-CQiS+F9Pa0XozvkXR1g7uXE9QvBOPOplDg0iCCPRYTN9PqA5qYxhwe48G3o+v2UeQceNRrbnEtWuANm7JRqIhw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@node-rs/bcrypt-darwin-x64@1.9.0': + resolution: {integrity: sha512-4pTKGawYd7sNEjdJ7R/R67uwQH1VvwPZ0SSUMmeNHbxD5QlwAPXdDH11q22uzVXsvNFZ6nGQBg8No5OUGpx6Ug==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@node-rs/bcrypt-freebsd-x64@1.9.0': + resolution: {integrity: sha512-UmWzySX4BJhT/B8xmTru6iFif3h0Rpx3TqxRLCcbgmH43r7k5/9QuhpiyzpvKGpKHJCFNm4F3rC2wghvw5FCIg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@node-rs/bcrypt-linux-arm-gnueabihf@1.9.0': + resolution: {integrity: sha512-8qoX4PgBND2cVwsbajoAWo3NwdfJPEXgpCsZQZURz42oMjbGyhhSYbovBCskGU3EBLoC8RA2B1jFWooeYVn5BA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@node-rs/bcrypt-linux-arm64-gnu@1.9.0': + resolution: {integrity: sha512-TuAC6kx0SbcIA4mSEWPi+OCcDjTQUMl213v5gMNlttF+D4ieIZx6pPDGTaMO6M2PDHTeCG0CBzZl0Lu+9b0c7Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@node-rs/bcrypt-linux-arm64-musl@1.9.0': + resolution: {integrity: sha512-/sIvKDABOI8QOEnLD7hIj02BVaNOuCIWBKvxcJOt8+TuwJ6zmY1UI5kSv9d99WbiHjTp97wtAUbZQwauU4b9ew==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@node-rs/bcrypt-linux-x64-gnu@1.9.0': + resolution: {integrity: sha512-DyyhDHDsLBsCKz1tZ1hLvUZSc1DK0FU0v52jK6IBQxrj24WscSU9zZe7ie/V9kdmA4Ep57BfpWX8Dsa2JxGdgQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@node-rs/bcrypt-linux-x64-musl@1.9.0': + resolution: {integrity: sha512-duIiuqQ+Lew8ASSAYm6ZRqcmfBGWwsi81XLUwz86a2HR7Qv6V4yc3ZAUQovAikhjCsIqe8C11JlAZSK6+PlXYg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@node-rs/bcrypt-wasm32-wasi@1.9.0': + resolution: {integrity: sha512-ylaGmn9Wjwv/D5lxtawttx3H6Uu2WTTR7lWlRHGT6Ga/MB1Vj4OjSGUW8G8zIVnKuXpGbZ92pgHlt4HUpSLctw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@node-rs/bcrypt-win32-arm64-msvc@1.9.0': + resolution: {integrity: sha512-2h86gF7QFyEzODuDFml/Dp1MSJoZjxJ4yyT2Erf4NkwsiA5MqowUhUsorRwZhX6+2CtlGa7orbwi13AKMsYndw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@node-rs/bcrypt-win32-ia32-msvc@1.9.0': + resolution: {integrity: sha512-kqxalCvhs4FkN0+gWWfa4Bdy2NQAkfiqq/CEf6mNXC13RSV673Ev9V8sRlQyNpCHCNkeXfOT9pgoBdJmMs9muA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@node-rs/bcrypt-win32-x64-msvc@1.9.0': + resolution: {integrity: sha512-2y0Tuo6ZAT2Cz8V7DHulSlv1Bip3zbzeXyeur+uR25IRNYXKvI/P99Zl85Fbuu/zzYAZRLLlGTRe6/9IHofe/w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@node-rs/bcrypt@1.9.0': + resolution: {integrity: sha512-u2OlIxW264bFUfvbFqDz9HZKFjwe8FHFtn7T/U8mYjPZ7DWYpbUB+/dkW/QgYfMSfR0ejkyuWaBBe0coW7/7ig==} + engines: {node: '>= 10'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -627,6 +944,50 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@radix-ui/react-compose-refs@1.0.1': + resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.0.2': + resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==} + 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 + + '@radix-ui/react-primitive@1.0.3': + resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} + 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 + + '@radix-ui/react-slot@1.0.2': + resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@rushstack/eslint-patch@1.10.3': resolution: {integrity: sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==} @@ -659,11 +1020,17 @@ packages: typescript: optional: true + '@tybys/wasm-util@0.8.3': + resolution: {integrity: sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==} + + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/node@20.14.1': - resolution: {integrity: sha512-T2MzSGEu+ysB/FkWfqmhV3PLyQlowdptmmgD20C6QxsS8Fmv5SjpZ1ayXaEC0S21/h5UJ9iA6W/5vSNU5l00OA==} + '@types/node@20.14.2': + resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==} '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} @@ -674,6 +1041,9 @@ packages: '@types/react@18.3.3': resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} + '@types/validator@13.11.10': + resolution: {integrity: sha512-e2PNXoXLr6Z+dbfx5zSh9TRlXJrELycxiaXznp4S5+D2M3b9bqJEitNHA5923jhnB2zzFiZHa2f0SI1HoIahpg==} + '@typescript-eslint/eslint-plugin@7.12.0': resolution: {integrity: sha512-7F91fcbuDf/d3S8o21+r3ZncGIke/+eWk0EpO21LXhDfLahriZF9CGj4fbAetEjlaBdjdSm9a6VeXbpbT6Z40Q==} engines: {node: ^18.18.0 || >=20.0.0} @@ -927,8 +1297,8 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001627: - resolution: {integrity: sha512-4zgNiB8nTyV/tHhwZrFs88ryjls/lHiqFhrxCW4qSTeuRByBVnPYpDInchOIySWknznucaf31Z4KYqjfbrecVw==} + caniuse-lite@1.0.30001629: + resolution: {integrity: sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -938,6 +1308,9 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + class-variance-authority@0.7.0: + resolution: {integrity: sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==} + clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -949,6 +1322,14 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clsx@2.0.0: + resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} + engines: {node: '>=6'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1060,8 +1441,8 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} - drizzle-kit@0.22.2: - resolution: {integrity: sha512-dxluXlhT1N9bpj2eZR3N/z3u4H0PbbrBYgyouIobFF25tOt2Buy1abx26Jii96qcYV0JgxjhnuV+FBcgR0Xa0w==} + drizzle-kit@0.22.4: + resolution: {integrity: sha512-jsiYGqHsbsP/GtM26y/bGK7je1ja+1H/RniCt1ovg2E7tMNraw6XdqKcjxHhb8FonCfDDjvwFgouRsZS46vrmA==} hasBin: true drizzle-orm@0.31.1: @@ -1159,8 +1540,8 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - enhanced-resolve@5.16.1: - resolution: {integrity: sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==} + enhanced-resolve@5.17.0: + resolution: {integrity: sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==} engines: {node: '>=10.13.0'} es-abstract@1.23.3: @@ -1384,6 +1765,9 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fs-monkey@1.0.6: + resolution: {integrity: sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -1651,14 +2035,18 @@ packages: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} - jackspeak@3.2.3: - resolution: {integrity: sha512-htOzIMPbpLid/Gq9/zaz9SfExABxqRe1sSCdxntlO/aMD6u0issZQiY25n2GKQUtJ02j7z5sfptlAOMpWWOmvw==} + jackspeak@3.4.0: + resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==} engines: {node: '>=14'} jiti@1.21.0: resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} hasBin: true + jiti@1.21.3: + resolution: {integrity: sha512-uy2bNX5zQ+tESe+TiC7ilGRz8AtRGmnJH55NC5S0nSUjvvvM2hJHmefHErugGXN4pNv4Qx7vLsnNw9qJ9mtIsw==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1734,6 +2122,9 @@ packages: resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} engines: {node: 14 || >=16.14} + lucia@3.2.0: + resolution: {integrity: sha512-eXMxXwk6hqtjRTj4W/x3EnTUtAztLPm0p2N2TEBMDEbakDLXiYnDQ9z/qahjPdPdhPguQc+vwO0/88zIWxlpuw==} + magic-string@0.16.0: resolution: {integrity: sha512-c4BEos3y6G2qO0B9X7K0FVLOPT9uGrjYwYRLFmDqyl5YMboUviyecnXWp94fJTSMwPw2/sf+CEYt5AGpmklkkQ==} @@ -1741,6 +2132,13 @@ packages: resolution: {integrity: sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ==} engines: {node: '>=4'} + memfs-browser@3.5.10302: + resolution: {integrity: sha512-JJTc/nh3ig05O0gBBGZjTCPOyydaTxNF0uHYBrcc1gHNnO+KIHIvo0Y1FKCJsaei6FCl8C6xfQomXqu+cuzkIw==} + + memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1866,6 +2264,9 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + oslo@1.2.0: + resolution: {integrity: sha512-OoFX6rDsNcOQVAD2gQD/z03u4vEjWZLzJtwkmgfRF+KpQUXwdgEXErD7zNhyowmHwHefP+PM9Pw13pgpHMRlzw==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -1988,9 +2389,6 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - pretty-ms@9.0.0: - resolution: {integrity: sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==} - engines: {node: '>=18'} prettier-plugin-tailwindcss@0.6.1: resolution: {integrity: sha512-AnbeYZu0WGj+QgKciUgdMnRxrqcxltleZPgdwfA5104BHM3siBLONN/HLW1yS2HvzSNkzpQ/JAj+LN0jcJO+0w==} engines: {node: '>=14.21.3'} @@ -2048,6 +2446,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-ms@9.0.0: + resolution: {integrity: sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==} + engines: {node: '>=18'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -2270,8 +2672,16 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - tailwindcss@3.4.3: - resolution: {integrity: sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==} + tailwind-merge@2.3.0: + resolution: {integrity: sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==} + + tailwindcss-animate@1.0.7: + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + + tailwindcss@3.4.4: + resolution: {integrity: sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==} engines: {node: '>=14.0.0'} hasBin: true @@ -2314,8 +2724,8 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + tslib@2.6.3: + resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} @@ -2367,6 +2777,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + validator@13.12.0: + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} + engines: {node: '>= 0.10'} + vlq@0.2.3: resolution: {integrity: sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==} @@ -2417,6 +2831,11 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod-form-data@2.0.2: + resolution: {integrity: sha512-sKTi+k0fvkxdakD0V5rq+9WVJA3cuTQUfEmNqvHrTzPLvjfLmkkBLfR0ed3qOi9MScJXTHIDH/jUNnEJ3CBX4g==} + peerDependencies: + zod: '>= 3.11.0' + zod-validation-error@3.3.0: resolution: {integrity: sha512-Syib9oumw1NTqEv4LT0e6U83Td9aVRk9iTXPUQr1otyV1PuXQKOvOwhMNqZIq5hluzHP2pMgnOmHEo7kPdI2mw==} engines: {node: '>=18.0.0'} @@ -2430,13 +2849,34 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@babel/runtime@7.24.6': + '@babel/runtime@7.24.7': dependencies: regenerator-runtime: 0.14.1 + '@emnapi/core@0.45.0': + dependencies: + tslib: 2.6.3 + optional: true + + '@emnapi/core@1.2.0': + dependencies: + '@emnapi/wasi-threads': 1.0.1 + tslib: 2.6.3 + optional: true + + '@emnapi/runtime@0.45.0': + dependencies: + tslib: 2.6.3 + optional: true + '@emnapi/runtime@1.2.0': dependencies: - tslib: 2.6.2 + tslib: 2.6.3 + optional: true + + '@emnapi/wasi-threads@1.0.1': + dependencies: + tslib: 2.6.3 optional: true '@ericcornelissen/bash-parser@0.5.2': @@ -2742,6 +3182,17 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 + '@lucia-auth/adapter-drizzle@1.0.7(lucia@3.2.0)': + dependencies: + lucia: 3.2.0 + + '@napi-rs/wasm-runtime@0.2.4': + dependencies: + '@emnapi/core': 1.2.0 + '@emnapi/runtime': 1.2.0 + '@tybys/wasm-util': 0.9.0 + optional: true + '@next/env@14.2.3': {} '@next/eslint-plugin-next@14.2.3': @@ -2775,6 +3226,195 @@ snapshots: '@next/swc-win32-x64-msvc@14.2.3': optional: true + '@node-rs/argon2-android-arm-eabi@1.7.0': + optional: true + + '@node-rs/argon2-android-arm-eabi@1.8.3': + optional: true + + '@node-rs/argon2-android-arm64@1.7.0': + optional: true + + '@node-rs/argon2-android-arm64@1.8.3': + optional: true + + '@node-rs/argon2-darwin-arm64@1.7.0': + optional: true + + '@node-rs/argon2-darwin-arm64@1.8.3': + optional: true + + '@node-rs/argon2-darwin-x64@1.7.0': + optional: true + + '@node-rs/argon2-darwin-x64@1.8.3': + optional: true + + '@node-rs/argon2-freebsd-x64@1.7.0': + optional: true + + '@node-rs/argon2-freebsd-x64@1.8.3': + optional: true + + '@node-rs/argon2-linux-arm-gnueabihf@1.7.0': + optional: true + + '@node-rs/argon2-linux-arm-gnueabihf@1.8.3': + optional: true + + '@node-rs/argon2-linux-arm64-gnu@1.7.0': + optional: true + + '@node-rs/argon2-linux-arm64-gnu@1.8.3': + optional: true + + '@node-rs/argon2-linux-arm64-musl@1.7.0': + optional: true + + '@node-rs/argon2-linux-arm64-musl@1.8.3': + optional: true + + '@node-rs/argon2-linux-x64-gnu@1.7.0': + optional: true + + '@node-rs/argon2-linux-x64-gnu@1.8.3': + optional: true + + '@node-rs/argon2-linux-x64-musl@1.7.0': + optional: true + + '@node-rs/argon2-linux-x64-musl@1.8.3': + optional: true + + '@node-rs/argon2-wasm32-wasi@1.7.0': + dependencies: + '@emnapi/core': 0.45.0 + '@emnapi/runtime': 0.45.0 + '@tybys/wasm-util': 0.8.3 + memfs-browser: 3.5.10302 + optional: true + + '@node-rs/argon2-wasm32-wasi@1.8.3': + dependencies: + '@napi-rs/wasm-runtime': 0.2.4 + optional: true + + '@node-rs/argon2-win32-arm64-msvc@1.7.0': + optional: true + + '@node-rs/argon2-win32-arm64-msvc@1.8.3': + optional: true + + '@node-rs/argon2-win32-ia32-msvc@1.7.0': + optional: true + + '@node-rs/argon2-win32-ia32-msvc@1.8.3': + optional: true + + '@node-rs/argon2-win32-x64-msvc@1.7.0': + optional: true + + '@node-rs/argon2-win32-x64-msvc@1.8.3': + optional: true + + '@node-rs/argon2@1.7.0': + optionalDependencies: + '@node-rs/argon2-android-arm-eabi': 1.7.0 + '@node-rs/argon2-android-arm64': 1.7.0 + '@node-rs/argon2-darwin-arm64': 1.7.0 + '@node-rs/argon2-darwin-x64': 1.7.0 + '@node-rs/argon2-freebsd-x64': 1.7.0 + '@node-rs/argon2-linux-arm-gnueabihf': 1.7.0 + '@node-rs/argon2-linux-arm64-gnu': 1.7.0 + '@node-rs/argon2-linux-arm64-musl': 1.7.0 + '@node-rs/argon2-linux-x64-gnu': 1.7.0 + '@node-rs/argon2-linux-x64-musl': 1.7.0 + '@node-rs/argon2-wasm32-wasi': 1.7.0 + '@node-rs/argon2-win32-arm64-msvc': 1.7.0 + '@node-rs/argon2-win32-ia32-msvc': 1.7.0 + '@node-rs/argon2-win32-x64-msvc': 1.7.0 + + '@node-rs/argon2@1.8.3': + optionalDependencies: + '@node-rs/argon2-android-arm-eabi': 1.8.3 + '@node-rs/argon2-android-arm64': 1.8.3 + '@node-rs/argon2-darwin-arm64': 1.8.3 + '@node-rs/argon2-darwin-x64': 1.8.3 + '@node-rs/argon2-freebsd-x64': 1.8.3 + '@node-rs/argon2-linux-arm-gnueabihf': 1.8.3 + '@node-rs/argon2-linux-arm64-gnu': 1.8.3 + '@node-rs/argon2-linux-arm64-musl': 1.8.3 + '@node-rs/argon2-linux-x64-gnu': 1.8.3 + '@node-rs/argon2-linux-x64-musl': 1.8.3 + '@node-rs/argon2-wasm32-wasi': 1.8.3 + '@node-rs/argon2-win32-arm64-msvc': 1.8.3 + '@node-rs/argon2-win32-ia32-msvc': 1.8.3 + '@node-rs/argon2-win32-x64-msvc': 1.8.3 + + '@node-rs/bcrypt-android-arm-eabi@1.9.0': + optional: true + + '@node-rs/bcrypt-android-arm64@1.9.0': + optional: true + + '@node-rs/bcrypt-darwin-arm64@1.9.0': + optional: true + + '@node-rs/bcrypt-darwin-x64@1.9.0': + optional: true + + '@node-rs/bcrypt-freebsd-x64@1.9.0': + optional: true + + '@node-rs/bcrypt-linux-arm-gnueabihf@1.9.0': + optional: true + + '@node-rs/bcrypt-linux-arm64-gnu@1.9.0': + optional: true + + '@node-rs/bcrypt-linux-arm64-musl@1.9.0': + optional: true + + '@node-rs/bcrypt-linux-x64-gnu@1.9.0': + optional: true + + '@node-rs/bcrypt-linux-x64-musl@1.9.0': + optional: true + + '@node-rs/bcrypt-wasm32-wasi@1.9.0': + dependencies: + '@emnapi/core': 0.45.0 + '@emnapi/runtime': 0.45.0 + '@tybys/wasm-util': 0.8.3 + memfs-browser: 3.5.10302 + optional: true + + '@node-rs/bcrypt-win32-arm64-msvc@1.9.0': + optional: true + + '@node-rs/bcrypt-win32-ia32-msvc@1.9.0': + optional: true + + '@node-rs/bcrypt-win32-x64-msvc@1.9.0': + optional: true + + '@node-rs/bcrypt@1.9.0': + optionalDependencies: + '@node-rs/bcrypt-android-arm-eabi': 1.9.0 + '@node-rs/bcrypt-android-arm64': 1.9.0 + '@node-rs/bcrypt-darwin-arm64': 1.9.0 + '@node-rs/bcrypt-darwin-x64': 1.9.0 + '@node-rs/bcrypt-freebsd-x64': 1.9.0 + '@node-rs/bcrypt-linux-arm-gnueabihf': 1.9.0 + '@node-rs/bcrypt-linux-arm64-gnu': 1.9.0 + '@node-rs/bcrypt-linux-arm64-musl': 1.9.0 + '@node-rs/bcrypt-linux-x64-gnu': 1.9.0 + '@node-rs/bcrypt-linux-x64-musl': 1.9.0 + '@node-rs/bcrypt-wasm32-wasi': 1.9.0 + '@node-rs/bcrypt-win32-arm64-msvc': 1.9.0 + '@node-rs/bcrypt-win32-ia32-msvc': 1.9.0 + '@node-rs/bcrypt-win32-x64-msvc': 1.9.0 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2802,6 +3442,41 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.7 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-label@2.0.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.7 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.7 + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-slot@1.0.2(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.7 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + '@rushstack/eslint-patch@1.10.3': {} '@snyk/github-codeowners@1.1.0': @@ -2815,7 +3490,7 @@ snapshots: '@swc/helpers@0.5.5': dependencies: '@swc/counter': 0.1.3 - tslib: 2.6.2 + tslib: 2.6.3 '@t3-oss/env-core@0.10.1(typescript@5.4.5)(zod@3.23.8)': dependencies: @@ -2830,9 +3505,19 @@ snapshots: optionalDependencies: typescript: 5.4.5 + '@tybys/wasm-util@0.8.3': + dependencies: + tslib: 2.6.3 + optional: true + + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.6.3 + optional: true + '@types/json5@0.0.29': {} - '@types/node@20.14.1': + '@types/node@20.14.2': dependencies: undici-types: 5.26.5 @@ -2847,6 +3532,8 @@ snapshots: '@types/prop-types': 15.7.12 csstype: 3.1.3 + '@types/validator@13.11.10': {} + '@typescript-eslint/eslint-plugin@7.12.0(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)': dependencies: '@eslint-community/regexpp': 4.10.1 @@ -3098,7 +3785,7 @@ snapshots: autoprefixer@10.4.19(postcss@8.4.38): dependencies: browserslist: 4.23.0 - caniuse-lite: 1.0.30001627 + caniuse-lite: 1.0.30001629 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.0.1 @@ -3136,7 +3823,7 @@ snapshots: browserslist@4.23.0: dependencies: - caniuse-lite: 1.0.30001627 + caniuse-lite: 1.0.30001629 electron-to-chromium: 1.4.792 node-releases: 2.0.14 update-browserslist-db: 1.0.16(browserslist@4.23.0) @@ -3159,7 +3846,7 @@ snapshots: camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001627: {} + caniuse-lite@1.0.30001629: {} chalk@4.1.2: dependencies: @@ -3178,6 +3865,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + class-variance-authority@0.7.0: + dependencies: + clsx: 2.0.0 + clean-stack@2.2.0: {} client-only@0.0.1: {} @@ -3185,6 +3876,10 @@ snapshots: clone@1.0.4: optional: true + clsx@2.0.0: {} + + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3288,7 +3983,7 @@ snapshots: dependencies: esutils: 2.0.3 - drizzle-kit@0.22.2: + drizzle-kit@0.22.4: dependencies: '@esbuild-kit/esm-loader': 2.6.5 esbuild: 0.19.12 @@ -3316,7 +4011,7 @@ snapshots: emoji-regex@9.2.2: {} - enhanced-resolve@5.16.1: + enhanced-resolve@5.17.0: dependencies: graceful-fs: 4.2.11 tapable: 2.2.1 @@ -3508,7 +4203,7 @@ snapshots: eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0): dependencies: debug: 4.3.5 - enhanced-resolve: 5.16.1 + enhanced-resolve: 5.17.0 eslint: 8.57.0 eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0) @@ -3572,7 +4267,7 @@ snapshots: eslint-plugin-jsx-a11y@6.8.0(eslint@8.57.0): dependencies: - '@babel/runtime': 7.24.6 + '@babel/runtime': 7.24.7 aria-query: 5.3.0 array-includes: 3.1.8 array.prototype.flatmap: 1.3.2 @@ -3747,6 +4442,9 @@ snapshots: fraction.js@4.3.7: {} + fs-monkey@1.0.6: + optional: true + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -3800,7 +4498,7 @@ snapshots: glob@10.4.1: dependencies: foreground-child: 3.1.1 - jackspeak: 3.2.3 + jackspeak: 3.4.0 minimatch: 9.0.4 minipass: 7.1.2 path-scurry: 1.11.1 @@ -4012,7 +4710,7 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jackspeak@3.2.3: + jackspeak@3.4.0: dependencies: '@isaacs/cliui': 8.0.2 optionalDependencies: @@ -4020,6 +4718,8 @@ snapshots: jiti@1.21.0: {} + jiti@1.21.3: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -4047,12 +4747,12 @@ snapshots: dependencies: json-buffer: 3.0.1 - knip@5.17.4(@types/node@20.14.1)(typescript@5.4.5): + knip@5.17.4(@types/node@20.14.2)(typescript@5.4.5): dependencies: '@ericcornelissen/bash-parser': 0.5.2 '@nodelib/fs.walk': 2.0.0 '@snyk/github-codeowners': 1.1.0 - '@types/node': 20.14.1 + '@types/node': 20.14.2 easy-table: 1.2.0 fast-glob: 3.3.2 file-entry-cache: 8.0.0 @@ -4101,12 +4801,26 @@ snapshots: lru-cache@10.2.2: {} + lucia@3.2.0: + dependencies: + oslo: 1.2.0 + magic-string@0.16.0: dependencies: vlq: 0.2.3 map-obj@2.0.0: {} + memfs-browser@3.5.10302: + dependencies: + memfs: 3.5.3 + optional: true + + memfs@3.5.3: + dependencies: + fs-monkey: 1.0.6 + optional: true + merge2@1.4.1: {} micromatch@4.0.7: @@ -4149,7 +4863,7 @@ snapshots: '@next/env': 14.2.3 '@swc/helpers': 0.5.5 busboy: 1.6.0 - caniuse-lite: 1.0.30001627 + caniuse-lite: 1.0.30001629 graceful-fs: 4.2.11 postcss: 8.4.31 react: 18.3.1 @@ -4238,6 +4952,11 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + oslo@1.2.0: + dependencies: + '@node-rs/argon2': 1.7.0 + '@node-rs/bcrypt': 1.9.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -4332,15 +5051,16 @@ snapshots: prelude-ls@1.2.1: {} - pretty-ms@9.0.0: - dependencies: - parse-ms: 4.0.0 prettier-plugin-tailwindcss@0.6.1(prettier@3.3.1): dependencies: prettier: 3.3.1 prettier@3.3.1: {} + pretty-ms@9.0.0: + dependencies: + parse-ms: 4.0.0 + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -4602,7 +5322,15 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - tailwindcss@3.4.3: + tailwind-merge@2.3.0: + dependencies: + '@babel/runtime': 7.24.7 + + tailwindcss-animate@1.0.7(tailwindcss@3.4.4): + dependencies: + tailwindcss: 3.4.4 + + tailwindcss@3.4.4: dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -4612,7 +5340,7 @@ snapshots: fast-glob: 3.3.2 glob-parent: 6.0.2 is-glob: 4.0.3 - jiti: 1.21.0 + jiti: 1.21.3 lilconfig: 2.1.0 micromatch: 4.0.7 normalize-path: 3.0.0 @@ -4668,7 +5396,7 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tslib@2.6.2: {} + tslib@2.6.3: {} type-check@0.4.0: dependencies: @@ -4735,6 +5463,8 @@ snapshots: util-deprecate@1.0.2: {} + validator@13.12.0: {} + vlq@0.2.3: {} wcwidth@1.0.1: @@ -4804,6 +5534,10 @@ snapshots: yocto-queue@0.1.0: {} + zod-form-data@2.0.2(zod@3.23.8): + dependencies: + zod: 3.23.8 + zod-validation-error@3.3.0(zod@3.23.8): dependencies: zod: 3.23.8 diff --git a/tailwind.config.ts b/tailwind.config.ts index 5dbfc1df..84287e82 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,12 +1,80 @@ -import type { Config } from "tailwindcss"; +import type { Config } from "tailwindcss" -const config: Config = { +const config = { + darkMode: ["class"], content: [ - "./pages/**/*.{js,ts,jsx,tsx,mdx}", - "./components/**/*.{js,ts,jsx,tsx,mdx}", - "./app/**/*.{js,ts,jsx,tsx,mdx}", - ], + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + './src/**/*.{ts,tsx}', + ], + prefix: "", + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +} satisfies Config - plugins: [], -}; -export default config; +export default config \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 8606067e..e7d41a20 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,12 @@ "~/*": ["./*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": [ + "next-env.d.ts", + "next.config.js", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], "exclude": ["node_modules"] } diff --git a/utils/auth.ts b/utils/auth.ts new file mode 100644 index 00000000..f86c7a7c --- /dev/null +++ b/utils/auth.ts @@ -0,0 +1,92 @@ +import { DrizzlePostgreSQLAdapter } from '@lucia-auth/adapter-drizzle'; +import type { Session, User } from 'lucia'; +import { Lucia } from 'lucia'; +import { cookies } from 'next/headers'; +import { redirect, RedirectType } from 'next/navigation'; +import { cache } from 'react'; +import { db } from '~/drizzle/db'; +import { + session as sessionTable, + user as userTable, + type UserType, +} from '~/drizzle/schema'; +import { env } from '~/env'; + +const adapter = new DrizzlePostgreSQLAdapter(db, sessionTable, userTable); + +export const lucia = new Lucia(adapter, { + sessionCookie: { + // this sets cookies with super long expiration + // since Next.js doesn't allow Lucia to extend cookie expiration when rendering pages + expires: false, + attributes: { + // set to `true` when using HTTPS + secure: env.NODE_ENV === 'production', + }, + }, + getUserAttributes: (attributes) => { + return { + // attributes has the type of DatabaseUserAttributes + username: attributes.username, + }; + }, +}); + +// IMPORTANT! +declare module 'lucia' { + // Todo: figure out how to fix linting here + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: UserType; + } +} + +export const validateRequest = cache( + async (): Promise< + { user: User; session: Session } | { user: null; session: null } + > => { + const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null; + if (!sessionId) { + return { + user: null, + session: null, + }; + } + + const result = await lucia.validateSession(sessionId); + // next.js throws when you attempt to set cookie when rendering page + try { + if (result.session?.fresh) { + const sessionCookie = lucia.createSessionCookie(result.session.id); + cookies().set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes, + ); + } + if (!result.session) { + const sessionCookie = lucia.createBlankSessionCookie(); + cookies().set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes, + ); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + } + return result; + }, +); + +export async function requirePageAuth() { + const { session, user } = await validateRequest(); + + if (!session || !user) { + redirect('/signin', RedirectType.replace); + } + + return { session, user }; +} diff --git a/utils/authSchema.ts b/utils/authSchema.ts new file mode 100644 index 00000000..6c1c8ed4 --- /dev/null +++ b/utils/authSchema.ts @@ -0,0 +1,32 @@ +import { isStrongPassword } from 'validator'; +import { z } from 'zod'; +import { zfd } from 'zod-form-data'; + +const createUserSchema = z.object({ + username: z + .string() + .min(4, { message: 'Username must be at least 4 characters' }) + .refine((s) => !s.includes(' '), 'Username cannot contain spaces'), + password: z.string().refine( + (password) => + isStrongPassword(password, { + minLowercase: 1, + minUppercase: 1, + minNumbers: 1, + minSymbols: 1, + }), + { + message: + 'Password must contain at least 1 lowercase, 1 uppercase, 1 number, and 1 symbol', + }, + ), +}); + +const loginSchema = z.object({ + username: z.string().min(1, { message: 'Username cannot be empty' }), + password: z.string().min(1, { message: 'Password cannot be empty' }), +}); + +export const createUserFormDataSchema = zfd.formData(createUserSchema); + +export const getUserFormDataSchema = zfd.formData(loginSchema);