diff --git a/apps/journal/app/lib/auth.server.ts b/apps/journal/app/lib/auth.server.ts index 6d93276..c68961d 100644 --- a/apps/journal/app/lib/auth.server.ts +++ b/apps/journal/app/lib/auth.server.ts @@ -249,6 +249,31 @@ export async function createMagicToken(email: string): Promise { return token; } +export async function initiateEmailChange(userId: string, newEmail: string): Promise { + const db = getDb(); + + const [existing] = await db.select().from(users).where(eq(users.email, newEmail)); + if (existing) throw new Error("Email already in use"); + + const [user] = await db.select().from(users).where(eq(users.id, userId)); + if (!user) throw new Error("User not found"); + if (user.email === newEmail) throw new Error("This is already your email"); + + const token = randomBytes(32).toString("base64url"); + const expiresAt = new Date(Date.now() + 15 * 60 * 1000); + + // Store new email in the token — verifyMagicToken will detect the mismatch + // and update the user's email + await db.insert(magicTokens).values({ + id: randomUUID(), + email: newEmail, + token, + expiresAt, + }); + + return token; +} + export async function verifyMagicToken(token: string): Promise { const db = getDb(); @@ -271,13 +296,46 @@ export async function verifyMagicToken(token: string): Promise { .set({ usedAt: new Date() }) .where(eq(magicTokens.id, record.id)); - // Find user + // Find user — for login/registration tokens, email matches an existing user. + // For email-change tokens, the email is the NEW email (no user yet). const [user] = await db.select().from(users).where(eq(users.email, record.email)); if (!user) throw new Error("User not found"); return user.id; } +export async function verifyEmailChange(token: string, userId: string): Promise { + const db = getDb(); + + const [record] = await db + .select() + .from(magicTokens) + .where( + and( + eq(magicTokens.token, token), + gt(magicTokens.expiresAt, new Date()), + isNull(magicTokens.usedAt), + ), + ); + + if (!record) throw new Error("Invalid or expired verification link"); + + await db + .update(magicTokens) + .set({ usedAt: new Date() }) + .where(eq(magicTokens.id, record.id)); + + const newEmail = record.email; + + // Update user email + await db + .update(users) + .set({ email: newEmail }) + .where(eq(users.id, userId)); + + return newEmail; +} + // --- Sessions --- import { createCookieSessionStorage } from "react-router"; diff --git a/apps/journal/app/root.tsx b/apps/journal/app/root.tsx index cdd85c1..9f35ab8 100644 --- a/apps/journal/app/root.tsx +++ b/apps/journal/app/root.tsx @@ -76,6 +76,9 @@ function NavBar({ user }: { user: { id: string; username: string } | null }) { > {user.username} + + {t("nav.settings")} +