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 (
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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);