diff --git a/apps/web/src/auth/adapters/prisma.ts b/apps/web/src/auth/adapters/prisma.ts index 5f97940ca..f3436b0b8 100644 --- a/apps/web/src/auth/adapters/prisma.ts +++ b/apps/web/src/auth/adapters/prisma.ts @@ -9,6 +9,7 @@ * * See: https://github.com/lukevella/rallly/issues/949 */ +import { createUser } from "@/features/user/mutations"; import { PrismaAdapter } from "@auth/prisma-adapter"; import { prisma } from "@rallly/database"; import type { Adapter } from "next-auth/adapters"; @@ -38,26 +39,15 @@ export function CustomPrismaAdapter(options: { }); }, createUser: async (user) => { - const newUser = await prisma.user.create({ - data: { - name: user.name ?? "Unknown", - email: user.email, - emailVerified: user.emailVerified, - image: user.image, - timeZone: user.timeZone, - weekStart: user.weekStart, - timeFormat: user.timeFormat, - locale: user.locale, - role: "user", - spaces: { - create: { - name: "Personal", - }, - }, - }, + return await createUser({ + name: user.name ?? "Unknown", + email: user.email, + emailVerified: user.emailVerified ?? undefined, + image: user.image ?? undefined, + timeZone: user.timeZone ?? undefined, + timeFormat: user.timeFormat ?? undefined, + locale: user.locale ?? undefined, }); - - return newUser; }, } as Adapter; } diff --git a/apps/web/src/features/spaces/mutations.ts b/apps/web/src/features/spaces/mutations.ts new file mode 100644 index 000000000..e6842e4ad --- /dev/null +++ b/apps/web/src/features/spaces/mutations.ts @@ -0,0 +1,22 @@ +import { prisma } from "@rallly/database"; + +export async function createSpace({ + ownerId, + name, +}: { + ownerId: string; + name: string; +}) { + return await prisma.space.create({ + data: { + ownerId, + name, + members: { + create: { + userId: ownerId, + role: "OWNER", + }, + }, + }, + }); +} diff --git a/apps/web/src/features/user/mutations.ts b/apps/web/src/features/user/mutations.ts new file mode 100644 index 000000000..d4d323d3a --- /dev/null +++ b/apps/web/src/features/user/mutations.ts @@ -0,0 +1,54 @@ +import { type TimeFormat, prisma } from "@rallly/database"; + +export async function createUser({ + name, + email, + emailVerified, + image, + timeZone, + timeFormat, + locale, + weekStart, +}: { + name: string; + email: string; + emailVerified?: Date; + image?: string; + timeZone?: string; + timeFormat?: TimeFormat; + locale?: string; + weekStart?: number; +}) { + return await prisma.$transaction(async (tx) => { + const user = await tx.user.create({ + data: { + name, + email, + emailVerified, + image, + timeZone, + timeFormat, + locale, + weekStart, + role: "user", + }, + }); + + const space = await tx.space.create({ + data: { + ownerId: user.id, + name: "Personal", + }, + }); + + await tx.spaceMember.create({ + data: { + spaceId: space.id, + userId: user.id, + role: "OWNER", + }, + }); + + return user; + }); +} diff --git a/apps/web/src/trpc/routers/auth.ts b/apps/web/src/trpc/routers/auth.ts index 988b55faa..bda4f4917 100644 --- a/apps/web/src/trpc/routers/auth.ts +++ b/apps/web/src/trpc/routers/auth.ts @@ -12,6 +12,7 @@ import { isValidName } from "@/utils/is-valid-name"; import { createToken, decryptToken } from "@/utils/session"; import { getInstanceSettings } from "@/features/instance-settings/queries"; +import { createUser } from "@/features/user/mutations"; import { TRPCError } from "@trpc/server"; import { createRateLimitMiddleware, publicProcedure, router } from "../trpc"; import type { RegistrationTokenPayload } from "../types"; @@ -124,21 +125,14 @@ export const auth = router({ return { ok: false }; } - const user = await prisma.user.create({ - select: { id: true, name: true, email: true }, - data: { - name, - email, - timeZone: input.timeZone, - timeFormat: input.timeFormat, - weekStart: input.weekStart, - locale: input.locale, - spaces: { - create: { - name: "Personal", - }, - }, - }, + const user = await createUser({ + name, + email, + emailVerified: new Date(), + timeZone: input.timeZone, + timeFormat: input.timeFormat, + weekStart: input.weekStart, + locale: input.locale, }); if (ctx.user?.isGuest) { @@ -166,7 +160,14 @@ export const auth = router({ }, }); - return { ok: true, user }; + return { + ok: true, + user: { + id: user.id, + name: user.name, + email: user.email, + }, + }; }), getUserPermission: publicProcedure .input(z.object({ token: z.string() })) diff --git a/packages/database/prisma/migrations/20250616140319_create_space_members/migration.sql b/packages/database/prisma/migrations/20250616140319_create_space_members/migration.sql new file mode 100644 index 000000000..6e82eb1a9 --- /dev/null +++ b/packages/database/prisma/migrations/20250616140319_create_space_members/migration.sql @@ -0,0 +1,26 @@ +-- CreateEnum +CREATE TYPE "SpaceMemberRole" AS ENUM ('OWNER', 'ADMIN', 'MEMBER'); + +-- CreateTable +CREATE TABLE "space_members" ( + "id" TEXT NOT NULL, + "space_id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "role" "SpaceMemberRole" NOT NULL DEFAULT 'MEMBER', + + CONSTRAINT "space_members_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "space_members_space_id_idx" ON "space_members"("space_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "space_members_space_id_user_id_key" ON "space_members"("space_id", "user_id"); + +-- AddForeignKey +ALTER TABLE "space_members" ADD CONSTRAINT "space_members_space_id_fkey" FOREIGN KEY ("space_id") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "space_members" ADD CONSTRAINT "space_members_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/database/prisma/migrations/20250616140359_space_members_migration/migration.sql b/packages/database/prisma/migrations/20250616140359_space_members_migration/migration.sql new file mode 100644 index 000000000..773538e50 --- /dev/null +++ b/packages/database/prisma/migrations/20250616140359_space_members_migration/migration.sql @@ -0,0 +1,15 @@ +-- Create space members with OWNER role for existing spaces +INSERT INTO "space_members" ("id", "space_id", "user_id", "created_at", "updated_at", "role") +SELECT + gen_random_uuid(), + id as space_id, + owner_id as user_id, + NOW() as created_at, + NOW() as updated_at, + 'OWNER' as role +FROM "spaces" +WHERE NOT EXISTS ( + SELECT 1 FROM "space_members" + WHERE "space_members"."space_id" = "spaces"."id" + AND "space_members"."user_id" = "spaces"."owner_id" +); \ No newline at end of file diff --git a/packages/database/prisma/models/space.prisma b/packages/database/prisma/models/space.prisma new file mode 100644 index 000000000..647a66571 --- /dev/null +++ b/packages/database/prisma/models/space.prisma @@ -0,0 +1,39 @@ +model Space { + id String @id @default(uuid()) + name String + ownerId String @map("owner_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + owner User @relation("UserSpaces", fields: [ownerId], references: [id], onDelete: Cascade) + polls Poll[] + scheduledEvents ScheduledEvent[] + subscription Subscription? @relation("SpaceToSubscription") + + members SpaceMember[] + + @@index([ownerId], type: Hash) + @@map("spaces") +} + +enum SpaceMemberRole { + OWNER + ADMIN + MEMBER +} + +model SpaceMember { + id String @id @default(uuid()) + spaceId String @map("space_id") + userId String @map("user_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + role SpaceMemberRole @default(MEMBER) + + space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([spaceId, userId]) + @@index([spaceId]) + @@map("space_members") +} diff --git a/packages/database/prisma/models/user.prisma b/packages/database/prisma/models/user.prisma index 14d86acc5..7892fcd83 100644 --- a/packages/database/prisma/models/user.prisma +++ b/packages/database/prisma/models/user.prisma @@ -33,22 +33,22 @@ enum UserRole { } model User { - id String @id @default(cuid()) - name String - email String @unique() @db.Citext - emailVerified DateTime? @map("email_verified") - image String? - timeZone String? @map("time_zone") - weekStart Int? @map("week_start") - timeFormat TimeFormat? @map("time_format") - locale String? - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt @map("updated_at") - customerId String? @map("customer_id") - banned Boolean @default(false) - bannedAt DateTime? @map("banned_at") - banReason String? @map("ban_reason") - role UserRole @default(user) + id String @id @default(cuid()) + name String + email String @unique() @db.Citext + emailVerified DateTime? @map("email_verified") + image String? + timeZone String? @map("time_zone") + weekStart Int? @map("week_start") + timeFormat TimeFormat? @map("time_format") + locale String? + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime? @updatedAt @map("updated_at") + customerId String? @map("customer_id") + banned Boolean @default(false) + bannedAt DateTime? @map("banned_at") + banReason String? @map("ban_reason") + role UserRole @default(user) comments Comment[] polls Poll[] @@ -58,7 +58,8 @@ model User { paymentMethods PaymentMethod[] subscription Subscription? @relation("UserToSubscription") - spaces Space[] @relation("UserSpaces") + spaces Space[] @relation("UserSpaces") + memberOf SpaceMember[] pollViews PollView[] scheduledEvents ScheduledEvent[] @@ -67,22 +68,6 @@ model User { @@map("users") } -model Space { - id String @id @default(uuid()) - name String - ownerId String @map("owner_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - - owner User @relation("UserSpaces", fields: [ownerId], references: [id], onDelete: Cascade) - polls Poll[] - scheduledEvents ScheduledEvent[] - subscription Subscription? @relation("SpaceToSubscription") - - @@index([ownerId], type: Hash) - @@map("spaces") -} - model VerificationToken { identifier String @db.Citext token String @unique