From fc3d721317208a1b1ba80a64264a6c9827e81ff3 Mon Sep 17 00:00:00 2001 From: Jacob Owens Date: Fri, 13 Feb 2026 07:29:46 -0800 Subject: [PATCH 1/6] feat: added ownerId to guild --- packages/auth/src/lib/auth.ts | 22 ++- packages/db/src/generated-schema.ts | 255 ++++++++++++++++++++++++++++ packages/db/src/schemas/guilds.ts | 4 + packages/env/src/server.ts | 6 + 4 files changed, 285 insertions(+), 2 deletions(-) diff --git a/packages/auth/src/lib/auth.ts b/packages/auth/src/lib/auth.ts index 9d97d3f..e7f6faf 100644 --- a/packages/auth/src/lib/auth.ts +++ b/packages/auth/src/lib/auth.ts @@ -1,11 +1,12 @@ -import { db } from "@repo/db" +import { db, schema } from "@repo/db" import { env } from "@repo/env/server" import { drizzleAdapter } from "better-auth/adapters/drizzle" import { betterAuth } from "better-auth/minimal" import { admin, organization, twoFactor, username } from "better-auth/plugins" export const auth = betterAuth({ - database: drizzleAdapter(db, { provider: "pg" }), + baseURL: env.NEXT_PUBLIC_API_URL, + database: drizzleAdapter(db, { provider: "pg", schema }), secret: env.BETTER_AUTH_SECRET, emailAndPassword: { enabled: true, @@ -21,6 +22,20 @@ export const auth = betterAuth({ schema: { organization: { modelName: "guild", + additionalFields: { + ownerId: { + type: "string", + fieldName: "ownerId", + references: { + field: "id", + table: "user", + model: "user", + onDelete: "restrict", + }, + required: true, + returned: true, + }, + }, }, member: { modelName: "guildMember", @@ -39,6 +54,9 @@ export const auth = betterAuth({ }, }, }, + dynamicAccessControl: { + enabled: true, + }, }), admin(), username(), diff --git a/packages/db/src/generated-schema.ts b/packages/db/src/generated-schema.ts index e69de29..71237c4 100644 --- a/packages/db/src/generated-schema.ts +++ b/packages/db/src/generated-schema.ts @@ -0,0 +1,255 @@ +import { relations } from "drizzle-orm" +import { + boolean, + index, + pgTable, + text, + timestamp, + uniqueIndex, +} from "drizzle-orm/pg-core" + +export const user = pgTable("user", { + id: text("id").primaryKey(), + name: text("name").notNull(), + email: text("email").notNull().unique(), + emailVerified: boolean("email_verified").default(false).notNull(), + image: text("image"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + role: text("role"), + banned: boolean("banned").default(false), + banReason: text("ban_reason"), + banExpires: timestamp("ban_expires"), + username: text("username").unique(), + displayUsername: text("display_username"), + twoFactorEnabled: boolean("two_factor_enabled").default(false), +}) + +export const session = pgTable( + "session", + { + id: text("id").primaryKey(), + expiresAt: timestamp("expires_at").notNull(), + token: text("token").notNull().unique(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + activeGuildId: text("active_guild_id"), + impersonatedBy: text("impersonated_by"), + }, + (table) => [index("session_userId_idx").on(table.userId)] +) + +export const account = pgTable( + "account", + { + id: text("id").primaryKey(), + accountId: text("account_id").notNull(), + providerId: text("provider_id").notNull(), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + accessToken: text("access_token"), + refreshToken: text("refresh_token"), + idToken: text("id_token"), + accessTokenExpiresAt: timestamp("access_token_expires_at"), + refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), + scope: text("scope"), + password: text("password"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [index("account_userId_idx").on(table.userId)] +) + +export const verification = pgTable( + "verification", + { + id: text("id").primaryKey(), + identifier: text("identifier").notNull(), + value: text("value").notNull(), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [index("verification_identifier_idx").on(table.identifier)] +) + +export const guild = pgTable( + "guild", + { + id: text("id").primaryKey(), + name: text("name").notNull(), + slug: text("slug").notNull().unique(), + logo: text("logo"), + createdAt: timestamp("created_at").notNull(), + metadata: text("metadata"), + ownerId: text("owner_id") + .notNull() + .references(() => user.id, { onDelete: "restrict" }), + }, + (table) => [uniqueIndex("guild_slug_uidx").on(table.slug)] +) + +export const organizationRole = pgTable( + "organization_role", + { + id: text("id").primaryKey(), + organizationId: text("organization_id") + .notNull() + .references(() => guild.id, { onDelete: "cascade" }), + role: text("role").notNull(), + permission: text("permission").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").$onUpdate( + () => /* @__PURE__ */ new Date() + ), + }, + (table) => [ + index("organizationRole_organizationId_idx").on(table.organizationId), + index("organizationRole_role_idx").on(table.role), + ] +) + +export const guildMember = pgTable( + "guild_member", + { + id: text("id").primaryKey(), + guildId: text("guild_id") + .notNull() + .references(() => guild.id, { onDelete: "cascade" }), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + role: text("role").default("member").notNull(), + createdAt: timestamp("created_at").notNull(), + }, + (table) => [ + index("guildMember_guildId_idx").on(table.guildId), + index("guildMember_userId_idx").on(table.userId), + ] +) + +export const invitation = pgTable( + "invitation", + { + id: text("id").primaryKey(), + guildId: text("guild_id") + .notNull() + .references(() => guild.id, { onDelete: "cascade" }), + email: text("email").notNull(), + role: text("role"), + status: text("status").default("pending").notNull(), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + inviterId: text("inviter_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + }, + (table) => [ + index("invitation_guildId_idx").on(table.guildId), + index("invitation_email_idx").on(table.email), + ] +) + +export const twoFactor = pgTable( + "two_factor", + { + id: text("id").primaryKey(), + secret: text("secret").notNull(), + backupCodes: text("backup_codes").notNull(), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + }, + (table) => [ + index("twoFactor_secret_idx").on(table.secret), + index("twoFactor_userId_idx").on(table.userId), + ] +) + +export const userRelations = relations(user, ({ many }) => ({ + sessions: many(session), + accounts: many(account), + guilds: many(guild), + guildMembers: many(guildMember), + invitations: many(invitation), + twoFactors: many(twoFactor), +})) + +export const sessionRelations = relations(session, ({ one }) => ({ + user: one(user, { + fields: [session.userId], + references: [user.id], + }), +})) + +export const accountRelations = relations(account, ({ one }) => ({ + user: one(user, { + fields: [account.userId], + references: [user.id], + }), +})) + +export const guildRelations = relations(guild, ({ one, many }) => ({ + user: one(user, { + fields: [guild.ownerId], + references: [user.id], + }), + organizationRoles: many(organizationRole), + guildMembers: many(guildMember), + invitations: many(invitation), +})) + +export const organizationRoleRelations = relations( + organizationRole, + ({ one }) => ({ + guild: one(guild, { + fields: [organizationRole.organizationId], + references: [guild.id], + }), + }) +) + +export const guildMemberRelations = relations(guildMember, ({ one }) => ({ + guild: one(guild, { + fields: [guildMember.guildId], + references: [guild.id], + }), + user: one(user, { + fields: [guildMember.userId], + references: [user.id], + }), +})) + +export const invitationRelations = relations(invitation, ({ one }) => ({ + guild: one(guild, { + fields: [invitation.guildId], + references: [guild.id], + }), + user: one(user, { + fields: [invitation.inviterId], + references: [user.id], + }), +})) + +export const twoFactorRelations = relations(twoFactor, ({ one }) => ({ + user: one(user, { + fields: [twoFactor.userId], + references: [user.id], + }), +})) diff --git a/packages/db/src/schemas/guilds.ts b/packages/db/src/schemas/guilds.ts index a7a85e2..cfe7dd1 100644 --- a/packages/db/src/schemas/guilds.ts +++ b/packages/db/src/schemas/guilds.ts @@ -8,6 +8,7 @@ import { } from "drizzle-orm/pg-core" import { guildMember } from "./guild-members" import { invitation } from "./invitations" +import { user } from "./users" export const guild = pgTable( "guild", @@ -17,6 +18,9 @@ export const guild = pgTable( slug: text("slug").notNull().unique(), logo: text("logo"), createdAt: timestamp("created_at").notNull(), + ownerId: text("owner_id") // this is the source of truth for the owner of the guild, the guildMember who owns this guild will also have role === "owner" so we will need to keep these in sync + .notNull() + .references(() => user.id, { onDelete: "restrict" }), // don't delete guild if owner deletes account metadata: text("metadata"), }, (table) => [uniqueIndex("guild_slug_uidx").on(table.slug)] diff --git a/packages/env/src/server.ts b/packages/env/src/server.ts index c3d633e..1aaf563 100644 --- a/packages/env/src/server.ts +++ b/packages/env/src/server.ts @@ -8,6 +8,11 @@ if (process.env.NODE_ENV !== "production") { dotenvConfig({ path: resolve(process.cwd(), "../../.env") }) } +const addProtocol = (url: string) => + url.startsWith("http://") || url.startsWith("https://") + ? url + : `https://${url}` + /** 20 MB default — keep in sync with client.ts */ const DEFAULT_MAX_FILE_UPLOAD_SIZE = 20 * 1024 * 1024 @@ -17,6 +22,7 @@ const serverSchema = z.object({ BETTER_AUTH_SECRET: z.string().min(1), SELF_HOSTED: z.coerce.boolean().default(true), MAX_FILE_UPLOAD_SIZE: z.coerce.number().default(DEFAULT_MAX_FILE_UPLOAD_SIZE), + NEXT_PUBLIC_API_URL: z.string().min(1).transform(addProtocol), }) export const env = serverSchema.parse(process.env) From 6c67b2d88bc74dfbc62466a7f09ee18af4cb85e5 Mon Sep 17 00:00:00 2001 From: Jacob Owens Date: Fri, 13 Feb 2026 07:36:28 -0800 Subject: [PATCH 2/6] feat: added guild roles table for dynamic roles --- packages/auth/src/lib/auth.ts | 6 ++++++ packages/db/src/generated-schema.ts | 27 +++++++++++------------ packages/db/src/schemas/guild-roles.ts | 30 ++++++++++++++++++++++++++ packages/db/src/schemas/guilds.ts | 8 ++++++- packages/db/src/schemas/index.ts | 1 + packages/db/src/schemas/users.ts | 2 ++ 6 files changed, 58 insertions(+), 16 deletions(-) create mode 100644 packages/db/src/schemas/guild-roles.ts diff --git a/packages/auth/src/lib/auth.ts b/packages/auth/src/lib/auth.ts index e7f6faf..86c61d6 100644 --- a/packages/auth/src/lib/auth.ts +++ b/packages/auth/src/lib/auth.ts @@ -53,6 +53,12 @@ export const auth = betterAuth({ activeOrganizationId: "activeGuildId", }, }, + organizationRole: { + modelName: "guildRole", + fields: { + organizationId: "guildId", + }, + }, }, dynamicAccessControl: { enabled: true, diff --git a/packages/db/src/generated-schema.ts b/packages/db/src/generated-schema.ts index 71237c4..ea67dfe 100644 --- a/packages/db/src/generated-schema.ts +++ b/packages/db/src/generated-schema.ts @@ -105,11 +105,11 @@ export const guild = pgTable( (table) => [uniqueIndex("guild_slug_uidx").on(table.slug)] ) -export const organizationRole = pgTable( - "organization_role", +export const guildRole = pgTable( + "guild_role", { id: text("id").primaryKey(), - organizationId: text("organization_id") + guildId: text("guild_id") .notNull() .references(() => guild.id, { onDelete: "cascade" }), role: text("role").notNull(), @@ -120,8 +120,8 @@ export const organizationRole = pgTable( ), }, (table) => [ - index("organizationRole_organizationId_idx").on(table.organizationId), - index("organizationRole_role_idx").on(table.role), + index("guildRole_guildId_idx").on(table.guildId), + index("guildRole_role_idx").on(table.role), ] ) @@ -210,20 +210,17 @@ export const guildRelations = relations(guild, ({ one, many }) => ({ fields: [guild.ownerId], references: [user.id], }), - organizationRoles: many(organizationRole), + guildRoles: many(guildRole), guildMembers: many(guildMember), invitations: many(invitation), })) -export const organizationRoleRelations = relations( - organizationRole, - ({ one }) => ({ - guild: one(guild, { - fields: [organizationRole.organizationId], - references: [guild.id], - }), - }) -) +export const guildRoleRelations = relations(guildRole, ({ one }) => ({ + guild: one(guild, { + fields: [guildRole.guildId], + references: [guild.id], + }), +})) export const guildMemberRelations = relations(guildMember, ({ one }) => ({ guild: one(guild, { diff --git a/packages/db/src/schemas/guild-roles.ts b/packages/db/src/schemas/guild-roles.ts new file mode 100644 index 0000000..0bd37c0 --- /dev/null +++ b/packages/db/src/schemas/guild-roles.ts @@ -0,0 +1,30 @@ +import { relations } from "drizzle-orm" +import { index, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core" +import { guild } from "./guilds" + +export const guildRole = pgTable( + "guild_role", + { + id: uuid("id").primaryKey().defaultRandom(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").$onUpdate( + () => /* @__PURE__ */ new Date() + ), + guildId: uuid("guild_id") + .notNull() + .references(() => guild.id, { onDelete: "cascade" }), + role: text("role").notNull(), + permission: text("permission").notNull(), + }, + (table) => [ + index("guildRole_guildId_idx").on(table.guildId), + index("guildRole_role_idx").on(table.role), + ] +) + +export const guildRoleRelations = relations(guildRole, ({ one }) => ({ + guild: one(guild, { + fields: [guildRole.guildId], + references: [guild.id], + }), +})) diff --git a/packages/db/src/schemas/guilds.ts b/packages/db/src/schemas/guilds.ts index cfe7dd1..367c9c2 100644 --- a/packages/db/src/schemas/guilds.ts +++ b/packages/db/src/schemas/guilds.ts @@ -7,6 +7,7 @@ import { uuid, } from "drizzle-orm/pg-core" import { guildMember } from "./guild-members" +import { guildRole } from "./guild-roles" import { invitation } from "./invitations" import { user } from "./users" @@ -26,7 +27,12 @@ export const guild = pgTable( (table) => [uniqueIndex("guild_slug_uidx").on(table.slug)] ) -export const guildRelations = relations(guild, ({ many }) => ({ +export const guildRelations = relations(guild, ({ one, many }) => ({ + user: one(user, { + fields: [guild.ownerId], + references: [user.id], + }), + guildRoles: many(guildRole), guildMembers: many(guildMember), invitations: many(invitation), })) diff --git a/packages/db/src/schemas/index.ts b/packages/db/src/schemas/index.ts index 7965cd2..eaf99bc 100644 --- a/packages/db/src/schemas/index.ts +++ b/packages/db/src/schemas/index.ts @@ -1,5 +1,6 @@ export * from "./accounts" export * from "./guild-members" +export * from "./guild-roles" export * from "./guilds" export * from "./invitations" export * from "./sessions" diff --git a/packages/db/src/schemas/users.ts b/packages/db/src/schemas/users.ts index bc89528..3a0b66a 100644 --- a/packages/db/src/schemas/users.ts +++ b/packages/db/src/schemas/users.ts @@ -2,6 +2,7 @@ import { relations } from "drizzle-orm" import { boolean, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core" import { account } from "./accounts" import { guildMember } from "./guild-members" +import { guild } from "./guilds" import { invitation } from "./invitations" import { session } from "./sessions" import { twoFactor } from "./two-factors" @@ -29,6 +30,7 @@ export const user = pgTable("user", { export const userRelations = relations(user, ({ many }) => ({ sessions: many(session), accounts: many(account), + guilds: many(guild), // can be owners of many guilds guildMembers: many(guildMember), invitations: many(invitation), twoFactors: many(twoFactor), From 1d6eab4dab0a1bf511999a06580a4e9f9371c338 Mon Sep 17 00:00:00 2001 From: Jacob Owens Date: Fri, 13 Feb 2026 23:24:29 -0800 Subject: [PATCH 3/6] feat: migrated app from NextJs -> React + Vite so we can mount this inside a desktop client later down the road --- apps/web/.gitignore | 27 +- apps/web/README.md | 36 -- apps/web/app/components/providers.tsx | 31 -- apps/web/app/layout.tsx | 34 -- apps/web/index.html | 14 + apps/web/next.config.js | 8 - apps/web/package.json | 18 +- apps/web/pnpm-workspace.yaml | 3 - apps/web/{app => public}/favicon.ico | Bin .../assets}/fonts/GeistMonoVF.woff | Bin .../{app => src/assets}/fonts/GeistVF.woff | Bin apps/web/{ => src}/lib/api-client.ts | 0 apps/web/src/main.tsx | 44 ++ apps/web/src/routes/__root.tsx | 21 + .../{app/page.tsx => src/routes/index.tsx} | 8 +- apps/web/src/styles/fonts.css | 24 + apps/web/src/vite-env.d.ts | 1 + apps/web/tsconfig.json | 19 +- apps/web/vite.config.ts | 29 ++ biome.json | 3 +- packages/typescript-config/vite-react.json | 10 + pnpm-lock.yaml | 491 +++++++++++++++++- 22 files changed, 657 insertions(+), 164 deletions(-) delete mode 100644 apps/web/README.md delete mode 100644 apps/web/app/components/providers.tsx delete mode 100644 apps/web/app/layout.tsx create mode 100644 apps/web/index.html delete mode 100644 apps/web/next.config.js delete mode 100644 apps/web/pnpm-workspace.yaml rename apps/web/{app => public}/favicon.ico (100%) rename apps/web/{app => src/assets}/fonts/GeistMonoVF.woff (100%) rename apps/web/{app => src/assets}/fonts/GeistVF.woff (100%) rename apps/web/{ => src}/lib/api-client.ts (100%) create mode 100644 apps/web/src/main.tsx create mode 100644 apps/web/src/routes/__root.tsx rename apps/web/{app/page.tsx => src/routes/index.tsx} (50%) create mode 100644 apps/web/src/styles/fonts.css create mode 100644 apps/web/src/vite-env.d.ts create mode 100644 apps/web/vite.config.ts create mode 100644 packages/typescript-config/vite-react.json diff --git a/apps/web/.gitignore b/apps/web/.gitignore index 5ef6a52..8c3bd87 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -1,24 +1,8 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - # dependencies /node_modules -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions - -# testing -/coverage - -# next.js -/.next/ -/out/ # production -/build +/dist # misc .DS_Store @@ -30,12 +14,11 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# env files (can opt-in for committing if needed) +# env files .env* -# vercel -.vercel - # typescript *.tsbuildinfo -next-env.d.ts + +# tanstack router (auto-generated) +routeTree.gen.ts diff --git a/apps/web/README.md b/apps/web/README.md deleted file mode 100644 index e215bc4..0000000 --- a/apps/web/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/apps/web/app/components/providers.tsx b/apps/web/app/components/providers.tsx deleted file mode 100644 index 14484bd..0000000 --- a/apps/web/app/components/providers.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client" - -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" -import { ThemeProvider } from "next-themes" -import { useState } from "react" - -export function Providers({ children }: { children: React.ReactNode }) { - const [queryClient] = useState( - () => - new QueryClient({ - defaultOptions: { - queries: { - staleTime: 60 * 1000, - }, - }, - }) - ) - - return ( - - - {children} - - - ) -} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx deleted file mode 100644 index fb9443e..0000000 --- a/apps/web/app/layout.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { Metadata } from "next" -import localFont from "next/font/local" -import { Providers } from "./components/providers" -import "@repo/ui/globals.css" - -const geistSans = localFont({ - src: "./fonts/GeistVF.woff", - variable: "--font-geist-sans", -}) -const geistMono = localFont({ - src: "./fonts/GeistMonoVF.woff", - variable: "--font-geist-mono", -}) - -export const metadata: Metadata = { - title: "Townhall", - description: "Community chat. Nothing else.", -} - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode -}>) { - return ( - - - {children} - - - ) -} diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..10e83f5 --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,14 @@ + + + + + + Townhall + + + + +
+ + + diff --git a/apps/web/next.config.js b/apps/web/next.config.js deleted file mode 100644 index 7ff7441..0000000 --- a/apps/web/next.config.js +++ /dev/null @@ -1,8 +0,0 @@ -const { resolve } = await import("node:path") -const dotenv = await import("dotenv") -dotenv.config({ path: resolve(import.meta.dirname, "../../.env") }) - -/** @type {import('next').NextConfig} */ -const nextConfig = {} - -export default nextConfig diff --git a/apps/web/package.json b/apps/web/package.json index 4492862..6d58a8f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -4,10 +4,10 @@ "type": "module", "private": true, "scripts": { - "dev": "next dev --port 3000", - "build": "next build", - "start": "next start -p ${PORT:-3000}", - "check-types": "next typegen && tsc --noEmit" + "dev": "vite --port 3000", + "build": "vite build", + "start": "vite preview --port 3000", + "check-types": "tsc --noEmit" }, "dependencies": { "@repo/api-client": "workspace:*", @@ -16,11 +16,10 @@ "@repo/ui": "workspace:*", "@tailwindcss/postcss": "^4.1.18", "@tanstack/react-query": "^5.90.21", + "@tanstack/react-router": "^1.120.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "dotenv": "^17.2.4", "lucide-react": "^0.563.0", - "next": "16.1.6", "next-themes": "^0.4.6", "postcss": "^8.5.6", "radix-ui": "^1.4.3", @@ -32,8 +31,13 @@ }, "devDependencies": { "@repo/typescript-config": "workspace:*", + "@tanstack/react-query-devtools": "^5.91.3", + "@tanstack/react-router-devtools": "^1.120.3", + "@tanstack/router-plugin": "^1.120.3", "@types/react": "19.2.13", "@types/react-dom": "19.2.3", - "tw-animate-css": "^1.4.0" + "@vitejs/plugin-react": "^4.5.2", + "tw-animate-css": "^1.4.0", + "vite": "^6.3.5" } } diff --git a/apps/web/pnpm-workspace.yaml b/apps/web/pnpm-workspace.yaml deleted file mode 100644 index 581a9d5..0000000 --- a/apps/web/pnpm-workspace.yaml +++ /dev/null @@ -1,3 +0,0 @@ -ignoredBuiltDependencies: - - sharp - - unrs-resolver diff --git a/apps/web/app/favicon.ico b/apps/web/public/favicon.ico similarity index 100% rename from apps/web/app/favicon.ico rename to apps/web/public/favicon.ico diff --git a/apps/web/app/fonts/GeistMonoVF.woff b/apps/web/src/assets/fonts/GeistMonoVF.woff similarity index 100% rename from apps/web/app/fonts/GeistMonoVF.woff rename to apps/web/src/assets/fonts/GeistMonoVF.woff diff --git a/apps/web/app/fonts/GeistVF.woff b/apps/web/src/assets/fonts/GeistVF.woff similarity index 100% rename from apps/web/app/fonts/GeistVF.woff rename to apps/web/src/assets/fonts/GeistVF.woff diff --git a/apps/web/lib/api-client.ts b/apps/web/src/lib/api-client.ts similarity index 100% rename from apps/web/lib/api-client.ts rename to apps/web/src/lib/api-client.ts diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx new file mode 100644 index 0000000..6aead06 --- /dev/null +++ b/apps/web/src/main.tsx @@ -0,0 +1,44 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { ReactQueryDevtools } from "@tanstack/react-query-devtools" +import { createRouter, RouterProvider } from "@tanstack/react-router" +import { ThemeProvider } from "next-themes" +import { StrictMode } from "react" +import { createRoot } from "react-dom/client" +import "@repo/ui/globals.css" +import "./styles/fonts.css" +import { routeTree } from "./routeTree.gen" + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, + }, + }, +}) + +const router = createRouter({ routeTree }) + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById("root") +if (!rootElement) throw new Error("Root element not found") + +createRoot(rootElement).render( + + + + + + + + +) diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx new file mode 100644 index 0000000..2d0b90d --- /dev/null +++ b/apps/web/src/routes/__root.tsx @@ -0,0 +1,21 @@ +import { createRootRoute, Outlet } from "@tanstack/react-router" +import { lazy, Suspense } from "react" + +const TanStackRouterDevtools = import.meta.env.DEV + ? lazy(() => + import("@tanstack/react-router-devtools").then((mod) => ({ + default: mod.TanStackRouterDevtools, + })) + ) + : () => null + +export const Route = createRootRoute({ + component: () => ( + <> + + + + + + ), +}) diff --git a/apps/web/app/page.tsx b/apps/web/src/routes/index.tsx similarity index 50% rename from apps/web/app/page.tsx rename to apps/web/src/routes/index.tsx index 7d23791..4ab109f 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/src/routes/index.tsx @@ -1,4 +1,10 @@ -export default function Home() { +import { createFileRoute } from "@tanstack/react-router" + +export const Route = createFileRoute("/")({ + component: Home, +}) + +function Home() { return (

Townhall

diff --git a/apps/web/src/styles/fonts.css b/apps/web/src/styles/fonts.css new file mode 100644 index 0000000..ea4db11 --- /dev/null +++ b/apps/web/src/styles/fonts.css @@ -0,0 +1,24 @@ +@font-face { + font-family: "Geist Sans"; + src: url("../assets/fonts/GeistVF.woff") format("woff"); + font-weight: 100 900; + font-display: swap; +} + +@font-face { + font-family: "Geist Mono"; + src: url("../assets/fonts/GeistMonoVF.woff") format("woff"); + font-weight: 100 900; + font-display: swap; +} + +:root { + --font-geist-sans: "Geist Sans", ui-sans-serif, system-ui, sans-serif; + --font-geist-mono: "Geist Mono", ui-monospace, monospace; +} + +body { + font-family: var(--font-geist-sans); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/apps/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 84e20cd..9f6b5f4 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -1,21 +1,10 @@ { - "extends": "@repo/typescript-config/nextjs.json", + "extends": "@repo/typescript-config/vite-react.json", "compilerOptions": { - "plugins": [ - { - "name": "next" - } - ], "paths": { - "@/*": ["./*"] + "@/*": ["./src/*"] } }, - "include": [ - "**/*.ts", - "**/*.tsx", - "next-env.d.ts", - "next.config.js", - ".next/types/**/*.ts" - ], - "exclude": ["node_modules"] + "include": ["src", "vite.config.ts"], + "exclude": ["node_modules", "dist"] } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..604aa9e --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,29 @@ +import { resolve } from "node:path" +import { tanstackRouter } from "@tanstack/router-plugin/vite" +import react from "@vitejs/plugin-react" +import { defineConfig, loadEnv } from "vite" + +export default defineConfig(({ mode }) => { + const monorepoRoot = resolve(__dirname, "../..") + const env = { + ...loadEnv(mode, monorepoRoot, "NEXT_PUBLIC_"), + ...loadEnv(mode, __dirname, "NEXT_PUBLIC_"), + } + + return { + plugins: [tanstackRouter(), react()], + resolve: { + alias: { + "@": resolve(__dirname, "./src"), + }, + }, + define: { + "process.env.NEXT_PUBLIC_API_URL": JSON.stringify( + env.NEXT_PUBLIC_API_URL + ), + "process.env.NEXT_PUBLIC_MAX_FILE_UPLOAD_SIZE": JSON.stringify( + env.NEXT_PUBLIC_MAX_FILE_UPLOAD_SIZE + ), + }, + } +}) diff --git a/biome.json b/biome.json index 6635208..e59811a 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,8 @@ "defaultBranch": "main" }, "files": { - "ignoreUnknown": true + "ignoreUnknown": true, + "includes": ["**", "!**/routeTree.gen.ts"] }, "formatter": { "enabled": true, diff --git a/packages/typescript-config/vite-react.json b/packages/typescript-config/vite-react.json new file mode 100644 index 0000000..197b5b4 --- /dev/null +++ b/packages/typescript-config/vite-react.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./base.json", + "compilerOptions": { + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e1159e..434ded5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,21 +87,18 @@ importers: '@tanstack/react-query': specifier: ^5.90.21 version: 5.90.21(react@19.2.4) + '@tanstack/react-router': + specifier: ^1.120.3 + version: 1.159.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 clsx: specifier: ^2.1.1 version: 2.1.1 - dotenv: - specifier: ^17.2.4 - version: 17.2.4 lucide-react: specifier: ^0.563.0 version: 0.563.0(react@19.2.4) - next: - specifier: 16.1.6 - version: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -130,15 +127,30 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:../../packages/typescript-config + '@tanstack/react-query-devtools': + specifier: ^5.91.3 + version: 5.91.3(@tanstack/react-query@5.90.21(react@19.2.4))(react@19.2.4) + '@tanstack/react-router-devtools': + specifier: ^1.120.3 + version: 1.159.10(@tanstack/react-router@1.159.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.159.9)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/router-plugin': + specifier: ^1.120.3 + version: 1.159.11(@tanstack/react-router@1.159.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) '@types/react': specifier: 19.2.13 version: 19.2.13 '@types/react-dom': specifier: 19.2.3 version: 19.2.3(@types/react@19.2.13) + '@vitejs/plugin-react': + specifier: ^4.5.2 + version: 4.7.0(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) tw-animate-css: specifier: ^1.4.0 version: 1.4.0 + vite: + specifier: ^6.3.5 + version: 6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) apps/www: dependencies: @@ -442,6 +454,18 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-typescript@7.28.6': resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} engines: {node: '>=6.9.0'} @@ -2042,6 +2066,9 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rollup/rollup-android-arm-eabi@4.57.1': resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} cpu: [arm] @@ -2271,17 +2298,117 @@ packages: '@tailwindcss/postcss@4.1.18': resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==} + '@tanstack/history@1.154.14': + resolution: {integrity: sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA==} + engines: {node: '>=12'} + '@tanstack/query-core@5.90.20': resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==} + '@tanstack/query-devtools@5.93.0': + resolution: {integrity: sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg==} + + '@tanstack/react-query-devtools@5.91.3': + resolution: {integrity: sha512-nlahjMtd/J1h7IzOOfqeyDh5LNfG0eULwlltPEonYy0QL+nqrBB+nyzJfULV+moL7sZyxc2sHdNJki+vLA9BSA==} + peerDependencies: + '@tanstack/react-query': ^5.90.20 + react: ^18 || ^19 + '@tanstack/react-query@5.90.21': resolution: {integrity: sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==} peerDependencies: react: ^18 || ^19 + '@tanstack/react-router-devtools@1.159.10': + resolution: {integrity: sha512-dfaXh7WBz1HJ639oMix5hJUJWCxrpcINPVXiN/3CBPYuGB2wYsBG2Iw61yufp+KkuFatAy95VTTnyeqGOq8ysw==} + engines: {node: '>=12'} + peerDependencies: + '@tanstack/react-router': ^1.159.10 + '@tanstack/router-core': ^1.159.9 + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + peerDependenciesMeta: + '@tanstack/router-core': + optional: true + + '@tanstack/react-router@1.159.10': + resolution: {integrity: sha512-PQO6hpnqNALmotXasfCafVBWWKpxChmYbXRjwPZQQq8au7m71z4WtAHsmUA2v/uqqhsvE9ySyWVx/Ece/Uq2ZQ==} + engines: {node: '>=12'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-store@0.8.0': + resolution: {integrity: sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/router-core@1.159.9': + resolution: {integrity: sha512-A9B8gvklvMCjSAFG8nDAhfmROI8kjcij8wzznQaw4RfGIOrYXyNe5fCAcbHXGpgNeTE2JnK75b6AjidDPQfrmw==} + engines: {node: '>=12'} + + '@tanstack/router-devtools-core@1.159.9': + resolution: {integrity: sha512-2b1zmN12qOhuxAYq5EEtecDmj1ekA8i7yKKDXc2WYCwc6W2sqz+JMoKDwGzAIrC8rHpe4n0+eU3r1re5VnIPcg==} + engines: {node: '>=12'} + peerDependencies: + '@tanstack/router-core': ^1.159.9 + csstype: ^3.0.10 + peerDependenciesMeta: + csstype: + optional: true + + '@tanstack/router-generator@1.159.9': + resolution: {integrity: sha512-WDn17uYP/Mk//7OP5ZnlYK228ezQ/N+pVA8BrwoF69g3Scq5CkfZUD633UI1+oXIl8Fb1pCt4CU0LkN7niMTmQ==} + engines: {node: '>=12'} + + '@tanstack/router-plugin@1.159.11': + resolution: {integrity: sha512-QrnwUX9XtfOqiNsD/AYmqTvvezuUwv4W7ewWwUgSTe0CEkuyjEa8aiZMLrofB613lRmoHSmjT6ciaV3z2vHdWw==} + engines: {node: '>=12'} + peerDependencies: + '@rsbuild/core': '>=1.0.2' + '@tanstack/react-router': ^1.159.10 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' + vite-plugin-solid: ^2.11.10 + webpack: '>=5.92.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@tanstack/react-router': + optional: true + vite: + optional: true + vite-plugin-solid: + optional: true + webpack: + optional: true + + '@tanstack/router-utils@1.158.0': + resolution: {integrity: sha512-qZ76eaLKU6Ae9iI/mc5zizBX149DXXZkBVVO3/QRIll79uKLJZHQlMKR++2ba7JsciBWz1pgpIBcCJPE9S0LVg==} + engines: {node: '>=12'} + + '@tanstack/store@0.8.0': + resolution: {integrity: sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==} + + '@tanstack/virtual-file-routes@1.154.7': + resolution: {integrity: sha512-cHHDnewHozgjpI+MIVp9tcib6lYEQK5MyUr0ChHpHFGBl8Xei55rohFK0I0ve/GKoHeioaK42Smd8OixPp6CTg==} + engines: {node: '>=12'} + '@ts-morph/common@0.27.0': resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -2302,6 +2429,12 @@ packages: '@types/validate-npm-package-name@4.0.2': resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -2345,6 +2478,10 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -2360,6 +2497,9 @@ packages: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} + babel-dead-code-elimination@1.0.12: + resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} + baseline-browser-mapping@2.9.19: resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true @@ -2434,6 +2574,10 @@ packages: zod: optional: true + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -2487,6 +2631,10 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -2560,6 +2708,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@2.0.0: + resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -2987,6 +3138,11 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + goober@2.1.18: + resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==} + peerDependencies: + csstype: ^3.0.10 + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -3070,6 +3226,10 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3142,6 +3302,10 @@ packages: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} + isbot@5.1.35: + resolution: {integrity: sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg==} + engines: {node: '>=18'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -3418,6 +3582,10 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -3587,6 +3755,11 @@ packages: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -3647,6 +3820,10 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -3681,6 +3858,10 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -3768,6 +3949,16 @@ packages: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} + seroval-plugins@1.5.0: + resolution: {integrity: sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.0: + resolution: {integrity: sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==} + engines: {node: '>=10'} + serve-static@2.2.1: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} @@ -3934,6 +4125,9 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -4073,6 +4267,10 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + until-async@3.0.2: resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} @@ -4118,10 +4316,53 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -4343,6 +4584,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -5727,6 +5978,8 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rollup/rollup-android-arm-eabi@4.57.1': optional: true @@ -5883,19 +6136,150 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.18 + '@tanstack/history@1.154.14': {} + '@tanstack/query-core@5.90.20': {} + '@tanstack/query-devtools@5.93.0': {} + + '@tanstack/react-query-devtools@5.91.3(@tanstack/react-query@5.90.21(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/query-devtools': 5.93.0 + '@tanstack/react-query': 5.90.21(react@19.2.4) + react: 19.2.4 + '@tanstack/react-query@5.90.21(react@19.2.4)': dependencies: '@tanstack/query-core': 5.90.20 react: 19.2.4 + '@tanstack/react-router-devtools@1.159.10(@tanstack/react-router@1.159.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.159.9)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/react-router': 1.159.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/router-devtools-core': 1.159.9(@tanstack/router-core@1.159.9)(csstype@3.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@tanstack/router-core': 1.159.9 + transitivePeerDependencies: + - csstype + + '@tanstack/react-router@1.159.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/history': 1.154.14 + '@tanstack/react-store': 0.8.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/router-core': 1.159.9 + isbot: 5.1.35 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/react-store@0.8.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/store': 0.8.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) + + '@tanstack/router-core@1.159.9': + dependencies: + '@tanstack/history': 1.154.14 + '@tanstack/store': 0.8.0 + cookie-es: 2.0.0 + seroval: 1.5.0 + seroval-plugins: 1.5.0(seroval@1.5.0) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/router-devtools-core@1.159.9(@tanstack/router-core@1.159.9)(csstype@3.2.3)': + dependencies: + '@tanstack/router-core': 1.159.9 + clsx: 2.1.1 + goober: 2.1.18(csstype@3.2.3) + tiny-invariant: 1.3.3 + optionalDependencies: + csstype: 3.2.3 + + '@tanstack/router-generator@1.159.9': + dependencies: + '@tanstack/router-core': 1.159.9 + '@tanstack/router-utils': 1.158.0 + '@tanstack/virtual-file-routes': 1.154.7 + prettier: 3.8.1 + recast: 0.23.11 + source-map: 0.7.6 + tsx: 4.21.0 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-plugin@1.159.11(@tanstack/react-router@1.159.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@tanstack/router-core': 1.159.9 + '@tanstack/router-generator': 1.159.9 + '@tanstack/router-utils': 1.158.0 + '@tanstack/virtual-file-routes': 1.154.7 + chokidar: 3.6.0 + unplugin: 2.3.11 + zod: 3.25.76 + optionalDependencies: + '@tanstack/react-router': 1.159.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + vite: 6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + + '@tanstack/router-utils@1.158.0': + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + ansis: 4.2.0 + babel-dead-code-elimination: 1.0.12 + diff: 8.0.3 + pathe: 2.0.3 + tinyglobby: 0.2.15 + transitivePeerDependencies: + - supports-color + + '@tanstack/store@0.8.0': {} + + '@tanstack/virtual-file-routes@1.154.7': {} + '@ts-morph/common@0.27.0': dependencies: fast-glob: 3.3.3 minimatch: 10.1.2 path-browserify: 1.0.1 + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + '@types/estree@1.0.8': {} '@types/node@25.2.2': @@ -5914,6 +6298,18 @@ snapshots: '@types/validate-npm-package-name@4.0.2': {} + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -5946,6 +6342,11 @@ snapshots: any-promise@1.3.0: {} + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + argparse@2.0.1: {} aria-hidden@1.2.6: @@ -5958,6 +6359,15 @@ snapshots: atomic-sleep@1.0.0: {} + babel-dead-code-elimination@1.0.12: + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + baseline-browser-mapping@2.9.19: {} better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(kysely@0.28.11)(postgres@3.4.8))(next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -5990,6 +6400,8 @@ snapshots: optionalDependencies: zod: 4.3.6 + binary-extensions@2.3.0: {} + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -6047,6 +6459,18 @@ snapshots: chalk@5.6.2: {} + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -6099,6 +6523,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie-es@2.0.0: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} @@ -6511,6 +6937,10 @@ snapshots: dependencies: is-glob: 4.0.3 + goober@2.1.18(csstype@3.2.3): + dependencies: + csstype: 3.2.3 + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -6575,6 +7005,10 @@ snapshots: is-arrayish@0.2.1: {} + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + is-docker@3.0.0: {} is-extglob@2.1.1: {} @@ -6617,6 +7051,8 @@ snapshots: dependencies: is-inside-container: 1.0.0 + isbot@5.1.35: {} + isexe@2.0.0: {} isexe@3.1.5: {} @@ -6845,6 +7281,8 @@ snapshots: node-releases@2.0.27: {} + normalize-path@3.0.0: {} + npm-run-path@4.0.1: dependencies: path-key: 3.1.1 @@ -7020,6 +7458,8 @@ snapshots: powershell-utils@0.1.0: {} + prettier@3.8.1: {} + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -7130,6 +7570,8 @@ snapshots: dependencies: react: 19.2.4 + react-refresh@0.17.0: {} + react-remove-scroll-bar@2.3.8(@types/react@19.2.13)(react@19.2.4): dependencies: react: 19.2.4 @@ -7159,6 +7601,10 @@ snapshots: react@19.2.4: {} + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + readdirp@4.1.2: {} real-require@0.2.0: {} @@ -7268,6 +7714,12 @@ snapshots: transitivePeerDependencies: - supports-color + seroval-plugins@1.5.0(seroval@1.5.0): + dependencies: + seroval: 1.5.0 + + seroval@1.5.0: {} + serve-static@2.2.1: dependencies: encodeurl: 2.0.0 @@ -7493,6 +7945,8 @@ snapshots: tiny-invariant@1.3.3: {} + tiny-warning@1.0.3: {} + tinyexec@0.3.2: {} tinyexec@1.0.2: {} @@ -7621,6 +8075,13 @@ snapshots: unpipe@1.0.0: {} + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.15.0 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + until-async@3.0.2: {} update-browserslist-db@1.2.3(browserslist@4.28.1): @@ -7654,8 +8115,26 @@ snapshots: vary@1.1.2: {} + vite@6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.57.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.2.2 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + tsx: 4.21.0 + yaml: 2.8.2 + web-streams-polyfill@3.3.3: {} + webpack-virtual-modules@0.6.2: {} + which@2.0.2: dependencies: isexe: 2.0.0 From 60d277b6a7b13bd51091d6fee8ab068c8cba2505 Mon Sep 17 00:00:00 2001 From: Jacob Owens Date: Sat, 14 Feb 2026 20:09:01 -0800 Subject: [PATCH 4/6] feat: add auth flow with login/signup pages and protected routes - Mount better-auth handler on Hono API server at /api/auth/** - Configure CORS with credentials for cookie-based sessions - Add baseURL to auth client using type-safe env - Create login and signup pages with useMutation - Add authenticated layout route to gate protected content - Add shadcn label component - Fix all FK columns from text to uuid to match PK types --- apps/api/package.json | 1 + apps/api/src/app.ts | 11 +- apps/web/src/routes/_authenticated.tsx | 32 +++++ apps/web/src/routes/_authenticated/index.tsx | 25 ++++ apps/web/src/routes/index.tsx | 13 -- apps/web/src/routes/login.tsx | 94 ++++++++++++++ apps/web/src/routes/signup.tsx | 123 +++++++++++++++++++ packages/auth/src/lib/auth-client.ts | 2 + packages/db/src/schemas/accounts.ts | 2 +- packages/db/src/schemas/channels.ts | 10 +- packages/db/src/schemas/guild-members.ts | 4 +- packages/db/src/schemas/guilds.ts | 2 +- packages/db/src/schemas/invitations.ts | 4 +- packages/db/src/schemas/messages.ts | 6 +- packages/db/src/schemas/sessions.ts | 4 +- packages/db/src/schemas/two-factors.ts | 2 +- packages/ui/src/components/label.tsx | 23 ++++ pnpm-lock.yaml | 3 + 18 files changed, 330 insertions(+), 31 deletions(-) create mode 100644 apps/web/src/routes/_authenticated.tsx create mode 100644 apps/web/src/routes/_authenticated/index.tsx delete mode 100644 apps/web/src/routes/index.tsx create mode 100644 apps/web/src/routes/login.tsx create mode 100644 apps/web/src/routes/signup.tsx create mode 100644 packages/ui/src/components/label.tsx diff --git a/apps/api/package.json b/apps/api/package.json index d1b337f..50024a1 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -12,6 +12,7 @@ "dependencies": { "@hono/node-server": "^1.19.9", "@hono/zod-openapi": "^0.19.2", + "@repo/auth": "workspace:*", "@repo/db": "workspace:*", "@repo/env": "workspace:*", "dotenv": "^17.2.4", diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 113e848..c2a48c2 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -1,3 +1,4 @@ +import { auth } from "@repo/auth" import { cors } from "hono/cors" import createApp from "@/lib/helpers/app/create-app" import configureOpenAPI from "@/lib/helpers/openapi/configure-openapi" @@ -6,7 +7,15 @@ import waitlistRouter from "@/routes/waitlist/index" const app = createApp() -app.use("*", cors()) +app.use( + "*", + cors({ + origin: "http://localhost:3000", + credentials: true, + }) +) + +app.on(["GET", "POST"], "/api/auth/**", (c) => auth.handler(c.req.raw)) configureOpenAPI(app) diff --git a/apps/web/src/routes/_authenticated.tsx b/apps/web/src/routes/_authenticated.tsx new file mode 100644 index 0000000..4d076ba --- /dev/null +++ b/apps/web/src/routes/_authenticated.tsx @@ -0,0 +1,32 @@ +import { authClient } from "@repo/auth/client" +import { createFileRoute, Outlet, useNavigate } from "@tanstack/react-router" +import { useEffect } from "react" + +export const Route = createFileRoute("/_authenticated")({ + component: AuthenticatedLayout, +}) + +function AuthenticatedLayout() { + const navigate = useNavigate() + const { data: session, isPending } = authClient.useSession() + + useEffect(() => { + if (!isPending && !session) { + navigate({ to: "/login" }) + } + }, [isPending, session, navigate]) + + if (isPending) { + return ( +
+
Loading...
+
+ ) + } + + if (!session) { + return null + } + + return +} diff --git a/apps/web/src/routes/_authenticated/index.tsx b/apps/web/src/routes/_authenticated/index.tsx new file mode 100644 index 0000000..af70582 --- /dev/null +++ b/apps/web/src/routes/_authenticated/index.tsx @@ -0,0 +1,25 @@ +import { authClient } from "@repo/auth/client" +import { Button } from "@repo/ui/components/button" +import { createFileRoute } from "@tanstack/react-router" + +export const Route = createFileRoute("/_authenticated/")({ + component: Home, +}) + +function Home() { + const { data: session } = authClient.useSession() + + return ( +
+

Townhall

+ {session && ( +

+ Welcome, {session.user.name} +

+ )} + +
+ ) +} diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx deleted file mode 100644 index 4ab109f..0000000 --- a/apps/web/src/routes/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router" - -export const Route = createFileRoute("/")({ - component: Home, -}) - -function Home() { - return ( -
-

Townhall

-
- ) -} diff --git a/apps/web/src/routes/login.tsx b/apps/web/src/routes/login.tsx new file mode 100644 index 0000000..c83450a --- /dev/null +++ b/apps/web/src/routes/login.tsx @@ -0,0 +1,94 @@ +import { authClient } from "@repo/auth/client" +import { Button } from "@repo/ui/components/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@repo/ui/components/card" +import { Input } from "@repo/ui/components/input" +import { Label } from "@repo/ui/components/label" +import { useMutation } from "@tanstack/react-query" +import { createFileRoute, Link, useNavigate } from "@tanstack/react-router" +import { type FormEvent, useState } from "react" + +export const Route = createFileRoute("/login")({ + component: LoginPage, +}) + +function LoginPage() { + const navigate = useNavigate() + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + + const { + mutateAsync: signIn, + isPending, + error, + } = useMutation({ + mutationFn: async () => { + const { error } = await authClient.signIn.email({ email, password }) + if (error) throw new Error(error.message ?? "Failed to sign in") + }, + onSuccess: () => navigate({ to: "/" }), + }) + + return ( +
+ + + Login + + Enter your credentials to access your account + + +
{ + e.preventDefault() + signIn() + }} + > + + {error && ( +

{error.message}

+ )} +
+ + setEmail(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+
+ + +

+ Don't have an account?{" "} + + Sign up + +

+
+
+
+
+ ) +} diff --git a/apps/web/src/routes/signup.tsx b/apps/web/src/routes/signup.tsx new file mode 100644 index 0000000..25f3f40 --- /dev/null +++ b/apps/web/src/routes/signup.tsx @@ -0,0 +1,123 @@ +import { authClient } from "@repo/auth/client" +import { Button } from "@repo/ui/components/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@repo/ui/components/card" +import { Input } from "@repo/ui/components/input" +import { Label } from "@repo/ui/components/label" +import { useMutation } from "@tanstack/react-query" +import { createFileRoute, Link, useNavigate } from "@tanstack/react-router" +import { type FormEvent, useState } from "react" + +export const Route = createFileRoute("/signup")({ + component: SignUpPage, +}) + +function SignUpPage() { + const navigate = useNavigate() + const [name, setName] = useState("") + const [username, setUsername] = useState("") + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + + const { + mutateAsync: signUp, + isPending, + error, + } = useMutation({ + mutationFn: async () => { + const { error } = await authClient.signUp.email({ + name, + username, + email, + password, + }) + if (error) throw new Error(error.message ?? "Failed to create account") + }, + onSuccess: () => navigate({ to: "/" }), + }) + + return ( +
+ + + Sign up + Create your Townhall account + +
{ + e.preventDefault() + signUp() + }} + > + + {error && ( +

{error.message}

+ )} +
+ + setName(e.target.value)} + required + /> +
+
+ + setUsername(e.target.value)} + required + /> +
+
+ + setEmail(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + minLength={8} + required + /> +
+
+ + +

+ Already have an account?{" "} + + Sign in + +

+
+
+
+
+ ) +} diff --git a/packages/auth/src/lib/auth-client.ts b/packages/auth/src/lib/auth-client.ts index 5d0c03c..f8f74a5 100644 --- a/packages/auth/src/lib/auth-client.ts +++ b/packages/auth/src/lib/auth-client.ts @@ -1,3 +1,4 @@ +import { env } from "@repo/env/client" import { adminClient, inferAdditionalFields, @@ -10,6 +11,7 @@ import { createAuthClient } from "better-auth/react" import type { auth } from "./auth.js" export const authClient = createAuthClient({ + baseURL: env.NEXT_PUBLIC_API_URL, plugins: [ organizationClient({ schema: inferOrgAdditionalFields(), diff --git a/packages/db/src/schemas/accounts.ts b/packages/db/src/schemas/accounts.ts index c4db6d3..f731645 100644 --- a/packages/db/src/schemas/accounts.ts +++ b/packages/db/src/schemas/accounts.ts @@ -8,7 +8,7 @@ export const account = pgTable( id: uuid("id").defaultRandom().primaryKey(), accountId: text("account_id").notNull(), providerId: text("provider_id").notNull(), - userId: text("user_id") + userId: uuid("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), accessToken: text("access_token"), diff --git a/packages/db/src/schemas/channels.ts b/packages/db/src/schemas/channels.ts index 6ccfbb6..a32ae44 100644 --- a/packages/db/src/schemas/channels.ts +++ b/packages/db/src/schemas/channels.ts @@ -38,12 +38,12 @@ export const channel = pgTable( type: channelTypeEnum("type").notNull().default("text"), // null for DMs/group DMs - guildId: text("guild_id").references(() => guild.id, { + guildId: uuid("guild_id").references(() => guild.id, { onDelete: "cascade", }), // points to a category channel - parentId: text("parent_id").references((): AnyPgColumn => channel.id, { + parentId: uuid("parent_id").references((): AnyPgColumn => channel.id, { onDelete: "set null", }), @@ -51,7 +51,7 @@ export const channel = pgTable( position: integer("position").default(0).notNull(), // group DM owner — null for guild channels (use roles/permissions instead) - ownerId: text("owner_id").references(() => user.id, { + ownerId: uuid("owner_id").references(() => user.id, { onDelete: "set null", }), @@ -91,10 +91,10 @@ export const channelMember = pgTable( "channel_member", { id: uuid("id").defaultRandom().primaryKey(), - channelId: text("channel_id") + channelId: uuid("channel_id") .notNull() .references(() => channel.id, { onDelete: "cascade" }), - userId: text("user_id") + userId: uuid("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), createdAt: timestamp("created_at").defaultNow().notNull(), diff --git a/packages/db/src/schemas/guild-members.ts b/packages/db/src/schemas/guild-members.ts index 69fb9cf..e8805f3 100644 --- a/packages/db/src/schemas/guild-members.ts +++ b/packages/db/src/schemas/guild-members.ts @@ -7,10 +7,10 @@ export const guildMember = pgTable( "guild_member", { id: uuid("id").defaultRandom().primaryKey(), - guildId: text("guild_id") + guildId: uuid("guild_id") .notNull() .references(() => guild.id, { onDelete: "cascade" }), - userId: text("user_id") + userId: uuid("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), role: text("role").default("member").notNull(), diff --git a/packages/db/src/schemas/guilds.ts b/packages/db/src/schemas/guilds.ts index 367c9c2..8572ac8 100644 --- a/packages/db/src/schemas/guilds.ts +++ b/packages/db/src/schemas/guilds.ts @@ -19,7 +19,7 @@ export const guild = pgTable( slug: text("slug").notNull().unique(), logo: text("logo"), createdAt: timestamp("created_at").notNull(), - ownerId: text("owner_id") // this is the source of truth for the owner of the guild, the guildMember who owns this guild will also have role === "owner" so we will need to keep these in sync + ownerId: uuid("owner_id") // this is the source of truth for the owner of the guild, the guildMember who owns this guild will also have role === "owner" so we will need to keep these in sync .notNull() .references(() => user.id, { onDelete: "restrict" }), // don't delete guild if owner deletes account metadata: text("metadata"), diff --git a/packages/db/src/schemas/invitations.ts b/packages/db/src/schemas/invitations.ts index d806fe9..b0b92c8 100644 --- a/packages/db/src/schemas/invitations.ts +++ b/packages/db/src/schemas/invitations.ts @@ -7,7 +7,7 @@ export const invitation = pgTable( "invitation", { id: uuid("id").defaultRandom().primaryKey(), - guildId: text("guild_id") + guildId: uuid("guild_id") .notNull() .references(() => guild.id, { onDelete: "cascade" }), email: text("email").notNull(), @@ -15,7 +15,7 @@ export const invitation = pgTable( status: text("status").default("pending").notNull(), expiresAt: timestamp("expires_at").notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), - inviterId: text("inviter_id") + inviterId: uuid("inviter_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), }, diff --git a/packages/db/src/schemas/messages.ts b/packages/db/src/schemas/messages.ts index 5af0f8a..820f7d0 100644 --- a/packages/db/src/schemas/messages.ts +++ b/packages/db/src/schemas/messages.ts @@ -26,10 +26,10 @@ export const message = pgTable( "message", { id: uuid("id").defaultRandom().primaryKey(), - channelId: text("channel_id") + channelId: uuid("channel_id") .notNull() .references(() => channel.id, { onDelete: "cascade" }), - authorId: text("author_id") // all messages must have an author + authorId: uuid("author_id") // all messages must have an author .notNull() .references(() => user.id, { onDelete: "cascade" }), @@ -37,7 +37,7 @@ export const message = pgTable( type: messageTypeEnum("type").notNull().default("default"), // for replies — points to the message being replied to - referencedMessageId: text("referenced_message_id").references( + referencedMessageId: uuid("referenced_message_id").references( (): AnyPgColumn => message.id, { onDelete: "set null" } ), diff --git a/packages/db/src/schemas/sessions.ts b/packages/db/src/schemas/sessions.ts index 1b2da6a..7958f7a 100644 --- a/packages/db/src/schemas/sessions.ts +++ b/packages/db/src/schemas/sessions.ts @@ -14,10 +14,10 @@ export const session = pgTable( .notNull(), ipAddress: text("ip_address"), userAgent: text("user_agent"), - userId: text("user_id") + userId: uuid("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), - activeGuildId: text("active_guild_id"), + activeGuildId: uuid("active_guild_id"), impersonatedBy: text("impersonated_by"), }, (table) => [index("session_userId_idx").on(table.userId)] diff --git a/packages/db/src/schemas/two-factors.ts b/packages/db/src/schemas/two-factors.ts index ab22d59..11494f9 100644 --- a/packages/db/src/schemas/two-factors.ts +++ b/packages/db/src/schemas/two-factors.ts @@ -8,7 +8,7 @@ export const twoFactor = pgTable( id: uuid("id").defaultRandom().primaryKey(), secret: text("secret").notNull(), backupCodes: text("backup_codes").notNull(), - userId: text("user_id") + userId: uuid("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), }, diff --git a/packages/ui/src/components/label.tsx b/packages/ui/src/components/label.tsx new file mode 100644 index 0000000..2b7831f --- /dev/null +++ b/packages/ui/src/components/label.tsx @@ -0,0 +1,23 @@ +"use client" + +import { cn } from "@repo/ui/lib/utils" +import { Label as LabelPrimitive } from "radix-ui" +import type * as React from "react" + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 434ded5..062f7e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@hono/zod-openapi': specifier: ^0.19.2 version: 0.19.10(hono@4.11.9)(zod@3.25.76) + '@repo/auth': + specifier: workspace:* + version: link:../../packages/auth '@repo/db': specifier: workspace:* version: link:../../packages/db From 8abd964932daabe66032080ff334191711f27be5 Mon Sep 17 00:00:00 2001 From: Jacob Owens Date: Sat, 14 Feb 2026 20:30:26 -0800 Subject: [PATCH 5/6] feat: Add NODE_ENV to environment schemas --- apps/api/src/app.ts | 6 +++++- packages/auth/src/lib/auth.ts | 2 ++ packages/env/src/client.ts | 4 ++++ packages/env/src/server.ts | 3 +++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index c2a48c2..9bd30b2 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -1,4 +1,5 @@ import { auth } from "@repo/auth" +import { env } from "@repo/env/server" import { cors } from "hono/cors" import createApp from "@/lib/helpers/app/create-app" import configureOpenAPI from "@/lib/helpers/openapi/configure-openapi" @@ -10,7 +11,10 @@ const app = createApp() app.use( "*", cors({ - origin: "http://localhost:3000", + origin: + env.NODE_ENV === "development" + ? "http://localhost:3000" + : env.NEXT_PUBLIC_API_URL, credentials: true, }) ) diff --git a/packages/auth/src/lib/auth.ts b/packages/auth/src/lib/auth.ts index 86c61d6..019ed99 100644 --- a/packages/auth/src/lib/auth.ts +++ b/packages/auth/src/lib/auth.ts @@ -8,6 +8,8 @@ export const auth = betterAuth({ baseURL: env.NEXT_PUBLIC_API_URL, database: drizzleAdapter(db, { provider: "pg", schema }), secret: env.BETTER_AUTH_SECRET, + trustedOrigins: + env.NODE_ENV === "development" ? ["http://localhost:3000"] : [], emailAndPassword: { enabled: true, }, diff --git a/packages/env/src/client.ts b/packages/env/src/client.ts index 7cfddf9..435961a 100644 --- a/packages/env/src/client.ts +++ b/packages/env/src/client.ts @@ -9,6 +9,9 @@ const addProtocol = (url: string) => : `https://${url}` const clientSchema = z.object({ + NODE_ENV: z + .enum(["development", "staging", "production", "test"]) + .default("production"), NEXT_PUBLIC_API_URL: z.string().min(1).transform(addProtocol), NEXT_PUBLIC_MAX_FILE_UPLOAD_SIZE: z.coerce .number() @@ -16,6 +19,7 @@ const clientSchema = z.object({ }) export const env = clientSchema.parse({ + NODE_ENV: process.env.NODE_ENV, NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, NEXT_PUBLIC_MAX_FILE_UPLOAD_SIZE: process.env.NEXT_PUBLIC_MAX_FILE_UPLOAD_SIZE, diff --git a/packages/env/src/server.ts b/packages/env/src/server.ts index 1aaf563..060a949 100644 --- a/packages/env/src/server.ts +++ b/packages/env/src/server.ts @@ -17,6 +17,9 @@ const addProtocol = (url: string) => const DEFAULT_MAX_FILE_UPLOAD_SIZE = 20 * 1024 * 1024 const serverSchema = z.object({ + NODE_ENV: z + .enum(["development", "staging", "production", "test"]) + .default("production"), DATABASE_URL: z.string().url(), PORT: z.coerce.number().default(8080), BETTER_AUTH_SECRET: z.string().min(1), From 416e9ceffd6d3c3e5bc90c3780add3b0220153b6 Mon Sep 17 00:00:00 2001 From: Jacob Owens Date: Sat, 14 Feb 2026 20:37:29 -0800 Subject: [PATCH 6/6] fix: update tsconfig, env protocol handling, and switch to sync mutations --- apps/web/src/routes/login.tsx | 2 +- apps/web/src/routes/signup.tsx | 2 +- packages/auth/tsconfig.json | 2 ++ packages/env/src/client.ts | 13 +++++++++---- packages/env/src/server.ts | 13 +++++++++---- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/apps/web/src/routes/login.tsx b/apps/web/src/routes/login.tsx index c83450a..743062c 100644 --- a/apps/web/src/routes/login.tsx +++ b/apps/web/src/routes/login.tsx @@ -24,7 +24,7 @@ function LoginPage() { const [password, setPassword] = useState("") const { - mutateAsync: signIn, + mutate: signIn, isPending, error, } = useMutation({ diff --git a/apps/web/src/routes/signup.tsx b/apps/web/src/routes/signup.tsx index 25f3f40..bc3daaa 100644 --- a/apps/web/src/routes/signup.tsx +++ b/apps/web/src/routes/signup.tsx @@ -26,7 +26,7 @@ function SignUpPage() { const [password, setPassword] = useState("") const { - mutateAsync: signUp, + mutate: signUp, isPending, error, } = useMutation({ diff --git a/packages/auth/tsconfig.json b/packages/auth/tsconfig.json index 8e03c53..cf25c1c 100644 --- a/packages/auth/tsconfig.json +++ b/packages/auth/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "@repo/typescript-config/base.json", "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", "declaration": false, "declarationMap": false }, diff --git a/packages/env/src/client.ts b/packages/env/src/client.ts index 435961a..fec7a20 100644 --- a/packages/env/src/client.ts +++ b/packages/env/src/client.ts @@ -3,10 +3,15 @@ import { z } from "zod" /** 20 MB default — keep in sync with server.ts */ const DEFAULT_MAX_FILE_UPLOAD_SIZE = 20 * 1024 * 1024 -const addProtocol = (url: string) => - url.startsWith("http://") || url.startsWith("https://") - ? url - : `https://${url}` +/** Adds a protocol to a URL if missing. Defaults to http:// for localhost/loopback, https:// otherwise. */ +const addProtocol = (url: string) => { + const trimmed = url.trim() + if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) + return trimmed + const isLocal = + trimmed.startsWith("localhost") || trimmed.startsWith("127.0.0.1") + return isLocal ? `http://${trimmed}` : `https://${trimmed}` +} const clientSchema = z.object({ NODE_ENV: z diff --git a/packages/env/src/server.ts b/packages/env/src/server.ts index 060a949..1670735 100644 --- a/packages/env/src/server.ts +++ b/packages/env/src/server.ts @@ -8,10 +8,15 @@ if (process.env.NODE_ENV !== "production") { dotenvConfig({ path: resolve(process.cwd(), "../../.env") }) } -const addProtocol = (url: string) => - url.startsWith("http://") || url.startsWith("https://") - ? url - : `https://${url}` +/** Adds a protocol to a URL if missing. Defaults to http:// for localhost/loopback, https:// otherwise. */ +const addProtocol = (url: string) => { + const trimmed = url.trim() + if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) + return trimmed + const isLocal = + trimmed.startsWith("localhost") || trimmed.startsWith("127.0.0.1") + return isLocal ? `http://${trimmed}` : `https://${trimmed}` +} /** 20 MB default — keep in sync with client.ts */ const DEFAULT_MAX_FILE_UPLOAD_SIZE = 20 * 1024 * 1024