From 9749fc8b778246cb7b94ed13997f4075c32a8022 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Sat, 11 Oct 2025 00:53:39 -0700 Subject: [PATCH] feat: add user model, routes, and Clerk webhook integration --- app/models/index.ts | 1 + app/models/user.ts | 108 +++++++++ app/routes.ts | 2 + app/routes/api/webhooks/clerk.ts | 94 +++++++ app/routes/user-profile.tsx | 22 ++ database/schema.ts | 13 + drizzle/0003_public_shadowcat.sql | 11 + drizzle/0004_spicy_zarda.sql | 2 + drizzle/meta/0003_snapshot.json | 385 +++++++++++++++++++++++++++++ drizzle/meta/0004_snapshot.json | 391 ++++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 14 ++ package-lock.json | 25 ++ package.json | 1 + 13 files changed, 1069 insertions(+) create mode 100644 app/models/user.ts create mode 100644 app/routes/api/webhooks/clerk.ts create mode 100644 app/routes/user-profile.tsx create mode 100644 drizzle/0003_public_shadowcat.sql create mode 100644 drizzle/0004_spicy_zarda.sql create mode 100644 drizzle/meta/0003_snapshot.json create mode 100644 drizzle/meta/0004_snapshot.json diff --git a/app/models/index.ts b/app/models/index.ts index cf1405e..761a8db 100644 --- a/app/models/index.ts +++ b/app/models/index.ts @@ -1,4 +1,5 @@ // Re-export all model functions and types +export * from "./user"; export * from "./league"; export * from "./season"; export * from "./team"; diff --git a/app/models/user.ts b/app/models/user.ts new file mode 100644 index 0000000..4265fe9 --- /dev/null +++ b/app/models/user.ts @@ -0,0 +1,108 @@ +import { eq } from "drizzle-orm"; +import { database } from "~/database/context"; +import * as schema from "~/database/schema"; + +export type User = typeof schema.users.$inferSelect; +export type NewUser = typeof schema.users.$inferInsert; + +export async function createUser(data: NewUser): Promise { + const db = database(); + const [user] = await db.insert(schema.users).values(data).returning(); + return user; +} + +export async function findUserById(id: string): Promise { + const db = database(); + return await db.query.users.findFirst({ + where: eq(schema.users.id, id), + }); +} + +export async function findUserByClerkId( + clerkId: string +): Promise { + const db = database(); + return await db.query.users.findFirst({ + where: eq(schema.users.clerkId, clerkId), + }); +} + +export async function updateUser( + id: string, + data: Partial +): Promise { + const db = database(); + const [user] = await db + .update(schema.users) + .set({ ...data, updatedAt: new Date() }) + .where(eq(schema.users.id, id)) + .returning(); + return user; +} + +export async function updateUserByClerkId( + clerkId: string, + data: Partial +): Promise { + const db = database(); + const [user] = await db + .update(schema.users) + .set({ ...data, updatedAt: new Date() }) + .where(eq(schema.users.clerkId, clerkId)) + .returning(); + return user; +} + +export async function findOrCreateUser(clerkUser: { + id: string; + emailAddresses?: Array<{ emailAddress: string }>; + firstName?: string | null; + lastName?: string | null; + imageUrl?: string; +}): Promise { + // Try to find existing user + const existingUser = await findUserByClerkId(clerkUser.id); + + const email = clerkUser.emailAddresses?.[0]?.emailAddress; + if (!email) { + throw new Error("User must have an email address"); + } + + // Generate display name: firstName lastName, or email username + let displayName = ""; + if (clerkUser.firstName || clerkUser.lastName) { + displayName = [clerkUser.firstName, clerkUser.lastName] + .filter(Boolean) + .join(" ") + .trim(); + } else { + // Use email username as fallback + displayName = email.split("@")[0]; + } + + if (existingUser) { + // Update user info in case it changed in Clerk + return await updateUserByClerkId(clerkUser.id, { + email, + displayName, + firstName: clerkUser.firstName || undefined, + lastName: clerkUser.lastName || undefined, + imageUrl: clerkUser.imageUrl, + }); + } + + // Create new user + return await createUser({ + clerkId: clerkUser.id, + email, + displayName, + firstName: clerkUser.firstName || undefined, + lastName: clerkUser.lastName || undefined, + imageUrl: clerkUser.imageUrl, + }); +} + +export async function deleteUser(id: string): Promise { + const db = database(); + await db.delete(schema.users).where(eq(schema.users.id, id)); +} diff --git a/app/routes.ts b/app/routes.ts index 4bbd149..239c752 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -5,4 +5,6 @@ export default [ route("leagues/new", "routes/leagues/new.tsx"), route("leagues/:leagueId", "routes/leagues/$leagueId.tsx"), route("leagues/:leagueId/settings", "routes/leagues/$leagueId.settings.tsx"), + route("api/webhooks/clerk", "routes/api/webhooks/clerk.ts"), + route("user-profile", "routes/user-profile.tsx"), ] satisfies RouteConfig; diff --git a/app/routes/api/webhooks/clerk.ts b/app/routes/api/webhooks/clerk.ts new file mode 100644 index 0000000..9801ab2 --- /dev/null +++ b/app/routes/api/webhooks/clerk.ts @@ -0,0 +1,94 @@ +import { Webhook } from "svix"; +import type { Route } from "./+types/clerk"; +import { findOrCreateUser, updateUserByClerkId } from "~/models/user"; + +export async function action({ request }: Route.ActionArgs) { + // Get the webhook secret from environment + const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET; + + if (!WEBHOOK_SECRET) { + throw new Error("Please add CLERK_WEBHOOK_SECRET to your .env file"); + } + + // Get the headers + const svix_id = request.headers.get("svix-id"); + const svix_timestamp = request.headers.get("svix-timestamp"); + const svix_signature = request.headers.get("svix-signature"); + + // If there are no headers, error out + if (!svix_id || !svix_timestamp || !svix_signature) { + return new Response("Error: Missing svix headers", { status: 400 }); + } + + // Get the body + const payload = await request.text(); + + // Create a new Svix instance with your webhook secret + const wh = new Webhook(WEBHOOK_SECRET); + + let evt: any; + + // Verify the webhook + try { + evt = wh.verify(payload, { + "svix-id": svix_id, + "svix-timestamp": svix_timestamp, + "svix-signature": svix_signature, + }); + } catch (err) { + console.error("Error verifying webhook:", err); + return new Response("Error: Verification failed", { status: 400 }); + } + + // Handle the webhook + const eventType = evt.type; + console.log(`Webhook received: ${eventType}`); + + if (eventType === "user.created" || eventType === "user.updated") { + const { id, email_addresses, first_name, last_name, image_url } = evt.data; + + try { + if (eventType === "user.created") { + // Create new user + const user = await findOrCreateUser({ + id, + emailAddresses: email_addresses, + firstName: first_name, + lastName: last_name, + imageUrl: image_url, + }); + console.log(`User created in database: ${user.id} (${user.displayName})`); + } else { + // Update existing user + const email = email_addresses?.[0]?.email_address; + if (!email) { + throw new Error("User must have an email address"); + } + + // Generate display name + let displayName = ""; + if (first_name || last_name) { + displayName = [first_name, last_name].filter(Boolean).join(" ").trim(); + } else { + displayName = email.split("@")[0]; + } + + const user = await updateUserByClerkId(id, { + email, + displayName, + firstName: first_name || undefined, + lastName: last_name || undefined, + imageUrl: image_url, + }); + console.log(`User updated in database: ${user.id} (${user.displayName})`); + } + } catch (error) { + console.error(`Error handling ${eventType}:`, error); + return new Response(`Error: Failed to ${eventType === "user.created" ? "create" : "update"} user`, { + status: 500, + }); + } + } + + return new Response("Webhook processed successfully", { status: 200 }); +} diff --git a/app/routes/user-profile.tsx b/app/routes/user-profile.tsx new file mode 100644 index 0000000..16868fc --- /dev/null +++ b/app/routes/user-profile.tsx @@ -0,0 +1,22 @@ +import { UserProfile } from "@clerk/react-router"; +import { getAuth } from "@clerk/react-router/server"; +import { redirect } from "react-router"; +import type { Route } from "./+types/user-profile"; + +export async function loader(args: Route.LoaderArgs) { + const { userId } = await getAuth(args); + + if (!userId) { + return redirect("/"); + } + + return {}; +} + +export default function UserProfilePage() { + return ( +
+ +
+ ); +} diff --git a/database/schema.ts b/database/schema.ts index e469138..adf29c4 100644 --- a/database/schema.ts +++ b/database/schema.ts @@ -6,6 +6,19 @@ export const guestBook = pgTable("guestBook", { email: varchar({ length: 255 }).notNull().unique(), }); +// Users table - synced from Clerk +export const users = pgTable("users", { + id: uuid("id").primaryKey().defaultRandom(), + clerkId: varchar("clerk_id", { length: 255 }).notNull().unique(), + email: varchar("email", { length: 255 }).notNull(), + displayName: varchar("display_name", { length: 255 }), + firstName: varchar("first_name", { length: 255 }), + lastName: varchar("last_name", { length: 255 }), + imageUrl: varchar("image_url", { length: 512 }), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + // Fantasy League Tables export const seasonStatusEnum = pgEnum("season_status", [ "pre_draft", diff --git a/drizzle/0003_public_shadowcat.sql b/drizzle/0003_public_shadowcat.sql new file mode 100644 index 0000000..c66e1e9 --- /dev/null +++ b/drizzle/0003_public_shadowcat.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS "users" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "clerk_id" varchar(255) NOT NULL, + "email" varchar(255), + "first_name" varchar(255), + "last_name" varchar(255), + "image_url" varchar(512), + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "users_clerk_id_unique" UNIQUE("clerk_id") +); diff --git a/drizzle/0004_spicy_zarda.sql b/drizzle/0004_spicy_zarda.sql new file mode 100644 index 0000000..3e4204a --- /dev/null +++ b/drizzle/0004_spicy_zarda.sql @@ -0,0 +1,2 @@ +ALTER TABLE "users" ALTER COLUMN "email" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "display_name" varchar(255); \ No newline at end of file diff --git a/drizzle/meta/0003_snapshot.json b/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..596acbc --- /dev/null +++ b/drizzle/meta/0003_snapshot.json @@ -0,0 +1,385 @@ +{ + "id": "db985c81-11e6-4c1b-9cf0-0fffec7bea16", + "prevId": "34297d0b-9216-46c7-9d1d-5b81f1402786", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.commissioners": { + "name": "commissioners", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "commissioners_league_id_leagues_id_fk": { + "name": "commissioners_league_id_leagues_id_fk", + "tableFrom": "commissioners", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.guestBook": { + "name": "guestBook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "guestBook_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "guestBook_email_unique": { + "name": "guestBook_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.leagues": { + "name": "leagues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.seasons": { + "name": "seasons", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "season_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pre_draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "seasons_league_id_leagues_id_fk": { + "name": "seasons_league_id_leagues_id_fk", + "tableFrom": "seasons", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "teams_season_id_seasons_id_fk": { + "name": "teams_season_id_seasons_id_fk", + "tableFrom": "teams", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "clerk_id": { + "name": "clerk_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_clerk_id_unique": { + "name": "users_clerk_id_unique", + "nullsNotDistinct": false, + "columns": [ + "clerk_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.season_status": { + "name": "season_status", + "schema": "public", + "values": [ + "pre_draft", + "draft", + "active", + "completed" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0004_snapshot.json b/drizzle/meta/0004_snapshot.json new file mode 100644 index 0000000..304861a --- /dev/null +++ b/drizzle/meta/0004_snapshot.json @@ -0,0 +1,391 @@ +{ + "id": "5e858241-1709-4099-90d2-58c7e3bf0d23", + "prevId": "db985c81-11e6-4c1b-9cf0-0fffec7bea16", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.commissioners": { + "name": "commissioners", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "commissioners_league_id_leagues_id_fk": { + "name": "commissioners_league_id_leagues_id_fk", + "tableFrom": "commissioners", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.guestBook": { + "name": "guestBook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "guestBook_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "guestBook_email_unique": { + "name": "guestBook_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.leagues": { + "name": "leagues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.seasons": { + "name": "seasons", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "season_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pre_draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "seasons_league_id_leagues_id_fk": { + "name": "seasons_league_id_leagues_id_fk", + "tableFrom": "seasons", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "teams_season_id_seasons_id_fk": { + "name": "teams_season_id_seasons_id_fk", + "tableFrom": "teams", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "clerk_id": { + "name": "clerk_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_clerk_id_unique": { + "name": "users_clerk_id_unique", + "nullsNotDistinct": false, + "columns": [ + "clerk_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.season_status": { + "name": "season_status", + "schema": "public", + "values": [ + "pre_draft", + "draft", + "active", + "completed" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 915f823..001f81d 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -22,6 +22,20 @@ "when": 1760166775427, "tag": "0002_tired_iron_man", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1760167848365, + "tag": "0003_public_shadowcat", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1760168383812, + "tag": "0004_spicy_zarda", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 37ad408..3a9cb41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "react-dom": "^19.1.0", "react-router": "^7.7.1", "sonner": "^2.0.7", + "svix": "^1.77.0", "tailwind-merge": "^3.3.1" }, "devDependencies": { @@ -6147,6 +6148,17 @@ "node": ">=8" } }, + "node_modules/svix": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.77.0.tgz", + "integrity": "sha512-rqyvcFHMq1eGIjYwZEEsW5MkeLH4FRr23TuSsLLhH+/wilK4sjdJSYmALTke3kyMqab7lqWTc9jyKFw6o0/oKg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0", + "uuid": "^10.0.0" + } + }, "node_modules/swr": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.4.tgz", @@ -6861,6 +6873,19 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/valibot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.1.0.tgz", diff --git a/package.json b/package.json index dca557f..e9ef630 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "react-dom": "^19.1.0", "react-router": "^7.7.1", "sonner": "^2.0.7", + "svix": "^1.77.0", "tailwind-merge": "^3.3.1" }, "devDependencies": {