diff --git a/web/app/features/translate/functions/mutations.server.ts b/web/app/features/translate/functions/mutations.server.ts index 03079b3b..201192b7 100644 --- a/web/app/features/translate/functions/mutations.server.ts +++ b/web/app/features/translate/functions/mutations.server.ts @@ -2,17 +2,20 @@ import { prisma } from "~/utils/prisma"; export async function getOrCreateAIUser(name: string): Promise { const user = await prisma.user.upsert({ - where: { email: `${name}@ai.com` }, + where: { userName: name }, update: {}, create: { - email: `${name}@ai.com`, - isAI: true, - icon: "", userName: name, displayName: name, + isAI: true, + icon: "", + userEmail: { + create: { + email: `${name}@ai.com`, + }, + }, }, }); - return user.id; } diff --git a/web/app/features/translate/lib/translate.server.test.ts b/web/app/features/translate/lib/translate.server.test.ts index 2ae656a0..06b851f1 100644 --- a/web/app/features/translate/lib/translate.server.test.ts +++ b/web/app/features/translate/lib/translate.server.test.ts @@ -24,7 +24,6 @@ describe("translate関数テスト (geminiのみモック)", () => { const user = await prisma.user.create({ data: { userName: "testuser", - email: "testuser@example.com", displayName: "testuser", icon: "testuser", }, diff --git a/web/app/routes/$locale+/user.$userName+/index.test.tsx b/web/app/routes/$locale+/user.$userName+/index.test.tsx index 329605ad..a45ed9ae 100644 --- a/web/app/routes/$locale+/user.$userName+/index.test.tsx +++ b/web/app/routes/$locale+/user.$userName+/index.test.tsx @@ -21,7 +21,6 @@ describe("UserProfile", () => { data: { userName: "testuser", displayName: "Test User", - email: "testuser@example.com", icon: "https://example.com/icon.jpg", profile: "This is a test profile", pages: { @@ -71,7 +70,6 @@ describe("UserProfile", () => { data: { userName: "testuser2", displayName: "Test User2", - email: "testuser2@example.com", icon: "https://example.com/icon2.jpg", profile: "This is a test profile2", }, diff --git a/web/app/routes/$locale+/user.$userName+/page+/$slug+/edit/_edit.test.tsx b/web/app/routes/$locale+/user.$userName+/page+/$slug+/edit/_edit.test.tsx index bdb221f1..50e7e6ed 100644 --- a/web/app/routes/$locale+/user.$userName+/page+/$slug+/edit/_edit.test.tsx +++ b/web/app/routes/$locale+/user.$userName+/page+/$slug+/edit/_edit.test.tsx @@ -55,7 +55,6 @@ describe("EditPage", () => { data: { userName: "testuser", displayName: "Test User", - email: "testuser@example.com", icon: "https://example.com/icon.jpg", profile: "This is a test profile", pages: { diff --git a/web/app/routes/$locale+/user.$userName+/page+/$slug+/edit/utils/processHtmlContent.test.ts b/web/app/routes/$locale+/user.$userName+/page+/$slug+/edit/utils/processHtmlContent.test.ts index db6a1ae2..c4f6a2db 100644 --- a/web/app/routes/$locale+/user.$userName+/page+/$slug+/edit/utils/processHtmlContent.test.ts +++ b/web/app/routes/$locale+/user.$userName+/page+/$slug+/edit/utils/processHtmlContent.test.ts @@ -16,7 +16,6 @@ describe("processHtmlContent", () => { create: { id: 10, userName: "htmltester", - email: "htmltester@example.com", displayName: "htmltester", icon: "htmltester", }, @@ -86,7 +85,6 @@ describe("processHtmlContent", () => { create: { id: 11, userName: "htmleditor", - email: "htmleditor@example.com", displayName: "htmleditor", icon: "htmleditor", }, @@ -182,7 +180,6 @@ describe("processHtmlContent", () => { create: { id: 12, userName: "titleduplicateuser", - email: "titleduplicateuser@example.com", displayName: "titleduplicateuser", icon: "titleduplicateuser", }, @@ -268,7 +265,6 @@ describe("processHtmlContent", () => { create: { id: 13, userName: "noedit", - email: "noedit@example.com", displayName: "noedit", icon: "noedit", }, @@ -345,7 +341,6 @@ describe("processHtmlContent", () => { create: { id: 14, userName: "imagetester", - email: "imagetester@example.com", displayName: "imagetester", icon: "imagetester", }, diff --git a/web/app/routes/resources+/functions/mutations.server.test.ts b/web/app/routes/resources+/functions/mutations.server.test.ts index 58b93885..d0dc4614 100644 --- a/web/app/routes/resources+/functions/mutations.server.test.ts +++ b/web/app/routes/resources+/functions/mutations.server.test.ts @@ -12,7 +12,6 @@ describe("toggleLike 実際のDB統合テスト", () => { data: { userName: "testuser", displayName: "Test User", - email: "testuser@example.com", icon: "https://example.com/icon.jpg", profile: "This is a test profile", pages: { diff --git a/web/app/utils/auth.server.ts b/web/app/utils/auth.server.ts index db9d3cf8..8b07f810 100644 --- a/web/app/utils/auth.server.ts +++ b/web/app/utils/auth.server.ts @@ -31,10 +31,11 @@ const formStrategy = new FormStrategy(async ({ form }) => { throw new AuthorizationError("Email and password are required"); } - const existingUser = await prisma.user.findUnique({ + const existingUserEmail = await prisma.userEmail.findUnique({ where: { email: String(email) }, - include: { credential: true }, + include: { user: { include: { credential: true } } }, }); + const existingUser = existingUserEmail?.user; if ( !existingUser || !existingUser.credential?.password || @@ -62,16 +63,22 @@ const googleStrategy = new GoogleStrategy( callbackURL: `${process.env.CLIENT_URL}/api/auth/callback/google`, }, async ({ profile }) => { - const user = await prisma.user.findUnique({ + const userEmail = await prisma.userEmail.findUnique({ where: { email: profile.emails[0].value }, + include: { user: true }, }); + const user = userEmail?.user; if (user) { return user; } const newUser = await prisma.user.create({ data: { - email: profile.emails[0].value || "", + userEmail: { + create: { + email: profile.emails[0].value || "", + }, + }, userName: generateTemporaryUserName(), displayName: profile.displayName || "New User", icon: profile.photos[0].value || "", @@ -91,9 +98,11 @@ const magicLinkStrategy = new EmailLinkStrategy( sessionMagicLinkKey: "auth:magicLink", }, async ({ email, form, magicLinkVerify }) => { - const user = await prisma.user.findUnique({ + const userEmail = await prisma.userEmail.findUnique({ where: { email: String(email) }, + include: { user: true }, }); + const user = userEmail?.user; if (user) { return user; @@ -101,7 +110,11 @@ const magicLinkStrategy = new EmailLinkStrategy( const newUser = await prisma.user.create({ data: { - email: String(email), + userEmail: { + create: { + email: String(email), + }, + }, icon: `${process.env.CLIENT_URL}/avatar.png`, userName: generateTemporaryUserName(), displayName: String(email).split("@")[0], diff --git a/web/prisma/migrations/20250119135311_/migration.sql b/web/prisma/migrations/20250119135311_/migration.sql new file mode 100644 index 00000000..fec1d3ee --- /dev/null +++ b/web/prisma/migrations/20250119135311_/migration.sql @@ -0,0 +1,42 @@ +/* + Warnings: + + - You are about to drop the column `email` on the `users` table. All the data in the column will be lost. + +*/ + + +CREATE TABLE "user_emails" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "user_id" INTEGER NOT NULL, + + CONSTRAINT "user_emails_pkey" PRIMARY KEY ("id") +); + +INSERT INTO "user_emails" ("email", "user_id") + SELECT "email", "id" + FROM "users" + WHERE "email" IS NOT NULL; + +-- DropIndex +DROP INDEX "users_email_idx"; + +-- DropIndex +DROP INDEX "users_email_key"; + +-- AlterTable +ALTER TABLE "users" DROP COLUMN "email"; + +-- CreateTable +-- CreateIndex +CREATE UNIQUE INDEX "user_emails_email_key" ON "user_emails"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "user_emails_user_id_key" ON "user_emails"("user_id"); + +-- CreateIndex +CREATE INDEX "user_emails_user_id_idx" ON "user_emails"("user_id"); + +-- AddForeignKey +ALTER TABLE "user_emails" ADD CONSTRAINT "user_emails_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/web/prisma/schema.prisma b/web/prisma/schema.prisma index ff2c66f9..14d00532 100644 --- a/web/prisma/schema.prisma +++ b/web/prisma/schema.prisma @@ -8,8 +8,7 @@ datasource db { } model User { - id Int @id @default(autoincrement()) - email String @unique + id Int @id @default(autoincrement()) userName String @unique @map("user_name") displayName String @map("display_name") icon String @@ -20,7 +19,8 @@ model User { provider String @default("Credentials") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") - credential UserCredential? + credential UserCredential? + userEmail UserEmail? geminiApiKey GeminiApiKey? pages Page[] userReadHistory UserReadHistory[] @@ -29,14 +29,23 @@ model User { votes Vote[] userAITranslationInfo UserAITranslationInfo[] customAIModels CustomAIModel[] - likePages LikePage[] - followers Follow[] @relation("following") - following Follow[] @relation("follower") - comments Comment[] + likePages LikePage[] + followers Follow[] @relation("following") + following Follow[] @relation("follower") + comments Comment[] - @@map("users") @@index([userName]) - @@index([email]) + @@map("users") +} + +model UserEmail { + id Int @id @default(autoincrement()) + email String @unique + userId Int @unique @map("user_id") + user User @relation(fields: [userId], references: [id]) + + @@index([userId]) + @@map("user_emails") } model UserCredential { @@ -45,18 +54,18 @@ model UserCredential { userId Int @unique user User @relation(fields: [userId], references: [id]) - @@map("user_credentials") @@index([userId]) + @@map("user_credentials") } model GeminiApiKey { - id Int @id @default(autoincrement()) - apiKey String @default("") @map("api_key") - userId Int @unique - user User @relation(fields: [userId], references: [id]) + id Int @id @default(autoincrement()) + apiKey String @default("") @map("api_key") + userId Int @unique + user User @relation(fields: [userId], references: [id]) - @@map("gemini_api_keys") @@index([userId]) + @@map("gemini_api_keys") } model Follow { @@ -68,9 +77,9 @@ model Follow { following User @relation("following", fields: [followingId], references: [id], onDelete: Cascade) @@unique([followerId, followingId]) - @@map("follows") @@index([followerId]) @@index([followingId]) + @@map("follows") } model UserAITranslationInfo { @@ -101,9 +110,9 @@ model UserReadHistory { page Page @relation(fields: [pageId], references: [id], onDelete: Cascade) @@unique([userId, pageId]) - @@map("user_read_history") @@index([userId]) @@index([pageId]) + @@map("user_read_history") } enum PageStatus { @@ -130,65 +139,64 @@ model Page { likePages LikePage[] comments Comment[] - @@map("pages") @@index([createdAt]) @@index([userId]) @@index([slug]) + @@map("pages") } model SourceText { - id Int @id @default(autoincrement()) - text String - number Int - textAndOccurrenceHash String @map("text_and_occurrence_hash") - translateTexts TranslateText[] - page Page @relation(fields: [pageId], references: [id], onDelete: Cascade) - pageId Int @map("page_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + id Int @id @default(autoincrement()) + text String + number Int + textAndOccurrenceHash String @map("text_and_occurrence_hash") + translateTexts TranslateText[] + page Page @relation(fields: [pageId], references: [id], onDelete: Cascade) + pageId Int @map("page_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") @@unique([pageId, number]) @@unique([pageId, textAndOccurrenceHash]) - @@map("source_texts") @@index([pageId]) @@index([number]) @@index([textAndOccurrenceHash]) + @@map("source_texts") } model TranslateText { - id Int @id @default(autoincrement()) - locale String - text String - sourceTextId Int @map("source_text_id") - userId Int @map("user_id") - point Int @default(0) - isArchived Boolean @default(false) @map("is_archived") - createdAt DateTime @default(now()) @map("created_at") - sourceText SourceText @relation(fields: [sourceTextId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - votes Vote[] + id Int @id @default(autoincrement()) + locale String + text String + sourceTextId Int @map("source_text_id") + userId Int @map("user_id") + point Int @default(0) + isArchived Boolean @default(false) @map("is_archived") + createdAt DateTime @default(now()) @map("created_at") + sourceText SourceText @relation(fields: [sourceTextId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + votes Vote[] - @@map("translate_texts") @@index([sourceTextId]) @@index([userId]) @@index([locale]) + @@map("translate_texts") } - model LikePage { id Int @id @default(autoincrement()) - userId Int? @map("user_id") - guestId String? @map("guest_id") + userId Int? @map("user_id") + guestId String? @map("guest_id") pageId Int @map("page_id") createdAt DateTime @default(now()) @map("created_at") - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) page Page @relation(fields: [pageId], references: [id], onDelete: Cascade) @@unique([userId, pageId]) @@unique([guestId, pageId]) - @@map("like_pages") @@index([userId]) @@index([pageId]) + @@map("like_pages") } model Genre { @@ -196,8 +204,8 @@ model Genre { name String @unique pages GenrePage[] - @@map("genres") @@index([name]) + @@map("genres") } model GenrePage { @@ -207,9 +215,9 @@ model GenrePage { page Page @relation(fields: [pageId], references: [id]) @@id([genreId, pageId]) - @@map("genre_pages") @@index([genreId]) @@index([pageId]) + @@map("genre_pages") } model Tag { @@ -217,8 +225,8 @@ model Tag { name String @unique pages TagPage[] - @@map("tags") @@index([name]) + @@map("tags") } model TagPage { @@ -228,9 +236,9 @@ model TagPage { page Page @relation(fields: [pageId], references: [id], onDelete: Cascade) @@id([tagId, pageId]) - @@map("tag_pages") @@index([tagId]) @@index([pageId]) + @@map("tag_pages") } model Vote { @@ -244,9 +252,9 @@ model Vote { user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([translateTextId, userId]) - @@map("votes") @@index([translateTextId]) @@index([userId]) + @@map("votes") } model ApiUsage { @@ -256,9 +264,9 @@ model ApiUsage { amountUsed Int @map("amount_used") user User @relation(fields: [userId], references: [id]) - @@map("api_usage") @@index([userId]) @@index([dateTime]) + @@map("api_usage") } model CustomAIModel { @@ -278,16 +286,16 @@ model CustomAIModel { model Comment { id Int @id @default(autoincrement()) - text String - userId Int @map("user_id") + text String + userId Int @map("user_id") pageId Int @map("page_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) page Page @relation(fields: [pageId], references: [id], onDelete: Cascade) - @@map("comments") @@index([userId]) @@index([pageId]) @@index([createdAt]) + @@map("comments") } diff --git a/web/prisma/seed.ts b/web/prisma/seed.ts index b3887492..a715152b 100644 --- a/web/prisma/seed.ts +++ b/web/prisma/seed.ts @@ -105,9 +105,13 @@ async function createUserAndPages() { create: { userName: "evame", displayName: "evame", - email: "evame@example.com", provider: "Admin", icon: "https://evame.tech/favicon.svg", + userEmail: { + create: { + email: "evame@example.com", + }, + }, }, }); @@ -175,28 +179,40 @@ async function upsertSourceTextWithTranslations( ); } -async function addDevelopmentData() { +export async function addDevelopmentData() { const email = "dev@example.com"; - const devUser = await prisma.user.upsert({ + const devEmail = await prisma.userEmail.upsert({ where: { email }, - update: {}, + update: { + user: { + update: {}, + }, + }, create: { email, - userName: "dev", - displayName: "Dev User", - icon: "", - credential: { + user: { create: { - password: await bcrypt.hash("devpassword", 10), + userName: "dev", + displayName: "Dev User", + icon: "", + credential: { + create: { + password: await bcrypt.hash("devpassword", 10), + }, + }, }, }, }, + include: { + user: true, + }, }); - console.log(`Created/Updated dev user with email: ${devUser.email}`); + console.log( + `Created/Updated dev user with ID=${devEmail.userId}, email=${devEmail.email}`, + ); } - seed() .catch((e) => { console.error(e); diff --git a/web/scripts/processMarkdownContent.test.ts b/web/scripts/processMarkdownContent.test.ts index 0c878ec3..880faa2e 100644 --- a/web/scripts/processMarkdownContent.test.ts +++ b/web/scripts/processMarkdownContent.test.ts @@ -19,7 +19,6 @@ This is another test. create: { id: 1, userName: "test", - email: "test@example.com", displayName: "test", icon: "test", }, @@ -94,7 +93,6 @@ This is another line. create: { id: 2, userName: "editor", - email: "editor@example.com", displayName: "editor", icon: "editor", }, @@ -210,7 +208,6 @@ new line create: { id: 3, userName: "variety", - email: "variety@example.com", displayName: "variety", icon: "variety", }, @@ -282,7 +279,6 @@ new line create: { id: 3, userName: "variety", - email: "variety@example.com", displayName: "variety", icon: "variety", },