From 7b4e28beebadba0472ad50039c8c3926933f2708 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Mon, 14 Jul 2025 16:11:16 -0700 Subject: [PATCH 01/11] Initial Better Auth implementation --- backend/index.ts | 33 +-- backend/lib/betterAuth.ts | 169 +++++++++++++ backend/lib/config.ts | 9 +- backend/lib/utils.ts | 26 +- backend/models/Profile.ts | 4 + backend/package.json | 1 + backend/routes/account.ts | 19 +- backend/routes/auth.ts | 24 +- backend/routes/profile.ts | 34 ++- backend/scripts/migrate-users.ts | 32 +++ frontend/components/PasswordChangeForm.tsx | 102 ++++---- frontend/hooks/useEmailLogin.ts | 24 +- frontend/hooks/useEmailSignup.ts | 23 +- frontend/hooks/useGoogleLogin.ts | 7 +- .../{useFirebaseLogout.ts => useLogout.ts} | 16 +- frontend/lib/betterAuth.ts | 10 + frontend/lib/http.ts | 12 +- frontend/package.json | 1 + frontend/pages/signup.tsx | 2 +- frontend/providers/user.tsx | 55 ++-- package-lock.json | 238 +++++++++++++++++- shared/types.ts | 4 + 22 files changed, 641 insertions(+), 204 deletions(-) create mode 100644 backend/lib/betterAuth.ts create mode 100644 backend/scripts/migrate-users.ts rename frontend/hooks/{useFirebaseLogout.ts => useLogout.ts} (66%) create mode 100644 frontend/lib/betterAuth.ts diff --git a/backend/index.ts b/backend/index.ts index 0f6153a2..8a23f906 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -13,6 +13,7 @@ import ebirdProxy from "routes/ebird-proxy.js"; import invites from "routes/invites.js"; import { HTTPException } from "hono/http-exception"; import { cors } from "hono/cors"; +import { auth as betterAuth } from "lib/betterAuth.js"; const app = new Hono(); @@ -23,9 +24,9 @@ if (process.env.CORS_ORIGINS) { } app.route("/v1/profile", profile); -app.route("/v1/account", account); app.route("/v1/trips", trips); app.route("/v1/auth", auth); +app.route("/v1/account", account); app.route("/v1/support", support); app.route("/v1/taxonomy", taxonomy); app.route("/v1/quiz", quiz); @@ -34,23 +35,23 @@ app.route("/v1/region", region); app.route("/v1/ebird-proxy", ebirdProxy); app.route("/v1/invites", invites); -app.notFound((c) => { - return c.json({ message: "Not Found" }, 404); +app.all("/api/auth/*", async (c) => { + const response = await betterAuth.handler(c.req.raw); + return response; }); app.onError((err, c) => { - const message = err instanceof Error ? err.message : "Internal Server Error"; - const status = err instanceof HTTPException ? err.status : 500; - return c.json({ message }, status); + if (err instanceof HTTPException) { + return c.json({ message: err.message }, err.status); + } + console.error("Server error:", err); + return c.json({ message: "Internal server error" }, 500); }); -serve( - { - fetch: app.fetch, - port: 5100, - hostname: "0.0.0.0", - }, - (info) => { - console.log(`Server is running on http://localhost:${info.port}`); - } -); +const port = parseInt(process.env.PORT || "3001"); +console.log(`Server is running on port ${port}`); + +serve({ + fetch: app.fetch, + port, +}); diff --git a/backend/lib/betterAuth.ts b/backend/lib/betterAuth.ts new file mode 100644 index 00000000..fbdfab4f --- /dev/null +++ b/backend/lib/betterAuth.ts @@ -0,0 +1,169 @@ +import { betterAuth } from "better-auth"; +import { connect } from "lib/db.js"; +import { BETTER_AUTH_CONFIG } from "lib/config.js"; + +type UserData = { + id: string; + name?: string; + email?: string; +}; + +type SessionData = { + id: string; + userId: string; + expiresAt: Date; +}; + +type TokenData = { + token: string; + userId: string; + expiresAt: Date; +}; + +const mongoAdapter = { + async connect() { + await connect(); + }, + async disconnect() {}, + async createUser(data: UserData) { + const { Profile } = await import("lib/db.js"); + const user = await Profile.create({ + uid: data.id, + name: data.name, + email: data.email, + lifelist: [], + exceptions: [], + lastActiveAt: new Date(), + }); + return user.toObject(); + }, + async getUser(userId: string) { + const { Profile } = await import("lib/db.js"); + const user = await Profile.findOne({ uid: userId }).lean(); + return user; + }, + async getUserByEmail(email: string) { + const { Profile } = await import("lib/db.js"); + const user = await Profile.findOne({ email }).lean(); + return user; + }, + async updateUser(userId: string, data: Partial) { + const { Profile } = await import("lib/db.js"); + const user = await Profile.findOneAndUpdate({ uid: userId }, { $set: data }, { new: true }).lean(); + return user; + }, + async deleteUser(userId: string) { + const { Profile } = await import("lib/db.js"); + await Profile.deleteOne({ uid: userId }); + }, + async createSession(data: SessionData) { + const { Profile } = await import("lib/db.js"); + const session = await Profile.findOneAndUpdate( + { uid: data.userId }, + { + $set: { + sessionId: data.id, + sessionExpires: data.expiresAt, + }, + }, + { new: true } + ).lean(); + return session; + }, + async getSession(sessionId: string) { + const { Profile } = await import("lib/db.js"); + const user = await Profile.findOne({ + sessionId, + sessionExpires: { $gt: new Date() }, + }).lean(); + return user ? { id: sessionId, userId: user.uid, expiresAt: user.sessionExpires } : null; + }, + async deleteSession(sessionId: string) { + const { Profile } = await import("lib/db.js"); + await Profile.updateOne({ sessionId }, { $unset: { sessionId: "", sessionExpires: "" } }); + }, + async createVerificationToken(data: TokenData) { + const { Profile } = await import("lib/db.js"); + await Profile.updateOne( + { uid: data.userId }, + { + $set: { + verificationToken: data.token, + verificationTokenExpires: data.expiresAt, + }, + } + ); + return data; + }, + async getVerificationToken(token: string) { + const { Profile } = await import("lib/db.js"); + const user = await Profile.findOne({ + verificationToken: token, + verificationTokenExpires: { $gt: new Date() }, + }).lean(); + return user + ? { + token, + userId: user.uid, + expiresAt: user.verificationTokenExpires, + } + : null; + }, + async deleteVerificationToken(token: string) { + const { Profile } = await import("lib/db.js"); + await Profile.updateOne( + { verificationToken: token }, + { $unset: { verificationToken: "", verificationTokenExpires: "" } } + ); + }, + async createPasswordResetToken(data: TokenData) { + const { Profile } = await import("lib/db.js"); + await Profile.updateOne( + { uid: data.userId }, + { + $set: { + resetToken: data.token, + resetTokenExpires: data.expiresAt, + }, + } + ); + return data; + }, + async getPasswordResetToken(token: string) { + const { Profile } = await import("lib/db.js"); + const user = await Profile.findOne({ + resetToken: token, + resetTokenExpires: { $gt: new Date() }, + }).lean(); + return user + ? { + token, + userId: user.uid, + expiresAt: user.resetTokenExpires, + } + : null; + }, + async deletePasswordResetToken(token: string) { + const { Profile } = await import("lib/db.js"); + await Profile.updateOne({ resetToken: token }, { $unset: { resetToken: "", resetTokenExpires: "" } }); + }, +}; + +export const auth = betterAuth({ + secret: BETTER_AUTH_CONFIG.secret, + baseUrl: BETTER_AUTH_CONFIG.baseUrl, + trustedOrigins: BETTER_AUTH_CONFIG.trustedOrigins, + adapter: mongoAdapter, + emailAndPassword: { + enabled: true, + requireEmailVerification: false, + }, + session: { + expiresIn: BETTER_AUTH_CONFIG.sessionExpiry, + }, + email: { + from: "BirdPlan.app ", + provider: "resend", + apiKey: process.env.RESEND_API_KEY, + }, +}); diff --git a/backend/lib/config.ts b/backend/lib/config.ts index 2c85ff2f..74fe9fe9 100644 --- a/backend/lib/config.ts +++ b/backend/lib/config.ts @@ -1 +1,8 @@ -export const RESET_TOKEN_EXPIRATION = 12; // hours +export const RESET_TOKEN_EXPIRATION = 12; + +export const BETTER_AUTH_CONFIG = { + secret: process.env.BETTER_AUTH_SECRET || "your-secret-key-change-in-production", + baseUrl: process.env.BETTER_AUTH_BASE_URL || "http://localhost:3000", + trustedOrigins: process.env.BETTER_AUTH_TRUSTED_ORIGINS?.split(",") || ["http://localhost:3000"], + sessionExpiry: 60 * 60 * 24 * 7, +} as const; diff --git a/backend/lib/utils.ts b/backend/lib/utils.ts index 32c40784..a8620610 100644 --- a/backend/lib/utils.ts +++ b/backend/lib/utils.ts @@ -1,6 +1,6 @@ -import { HTTPException } from "hono/http-exception"; import type { Context } from "hono"; -import { auth } from "lib/firebaseAdmin.js"; +import { HTTPException } from "hono/http-exception"; +import { auth } from "lib/betterAuth.js"; import { customAlphabet } from "nanoid"; import type { Trip, TargetList, Hotspot } from "@birdplan/shared"; @@ -9,22 +9,16 @@ export const nanoId = (length: number = 16) => { }; export async function authenticate(c: Context) { - if (!auth) { - throw new HTTPException(503, { message: "Authentication service not available" }); - } - - const authHeader = c.req.header("authorization"); - - if (!authHeader?.startsWith("Bearer ")) { - throw new HTTPException(401, { message: "Unauthorized" }); - } - - const token = authHeader.split("Bearer ")[1]; - try { - return await auth.verifyIdToken(token); + const session = await auth.api.getSession({ + headers: c.req.raw.headers, + }); + if (!session) { + throw new HTTPException(401, { message: "Unauthorized" }); + } + return session; } catch (error) { - console.error("Firebase auth error:", error); + console.error("Better Auth error:", error); throw new HTTPException(401, { message: "Unauthorized" }); } } diff --git a/backend/models/Profile.ts b/backend/models/Profile.ts index 8ed89c98..2eb45238 100644 --- a/backend/models/Profile.ts +++ b/backend/models/Profile.ts @@ -13,6 +13,10 @@ const fields: Record, any> = { lastActiveAt: { type: Date, default: new Date() }, resetToken: String, resetTokenExpires: Date, + sessionId: String, + sessionExpires: Date, + verificationToken: String, + verificationTokenExpires: Date, }; const ProfileSchema = new Schema(fields, { diff --git a/backend/package.json b/backend/package.json index b80074d8..9b55c2b8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,6 +10,7 @@ "@hono/node-server": "^1.14.4", "@maphubs/tokml": "^0.6.1", "axios": "^1.9.0", + "better-auth": "^1.2.12", "dayjs": "^1.11.13", "deepl-node": "^1.18.0", "dotenv": "^16.5.0", diff --git a/backend/routes/account.ts b/backend/routes/account.ts index dd149920..a4895515 100644 --- a/backend/routes/account.ts +++ b/backend/routes/account.ts @@ -1,7 +1,6 @@ import { Hono } from "hono"; import { authenticate } from "lib/utils.js"; import { connect, Profile, Trip, TargetList, Invite } from "lib/db.js"; -import { auth as firebaseAuth } from "lib/firebaseAdmin.js"; import { HTTPException } from "hono/http-exception"; const account = new Hono(); @@ -9,7 +8,7 @@ const account = new Hono(); account.delete("/", async (c) => { const session = await authenticate(c); - const uid = session.uid; + const uid = session.user.id; await connect(); @@ -25,8 +24,6 @@ account.delete("/", async (c) => { Trip.updateMany({ userIds: uid, ownerId: { $ne: uid } }, { $pull: { userIds: uid } }), ]); - await firebaseAuth?.deleteUser(uid); - return c.json({}); }); @@ -35,19 +32,7 @@ account.post("/update-email", async (c) => { const { email } = await c.req.json<{ email: string }>(); if (!email) throw new HTTPException(400, { message: "Email is required" }); - const user = await firebaseAuth?.getUser(session.uid); - if (!user) throw new HTTPException(404, { message: "User not found" }); - - if (!user.providerData.some((provider) => provider.providerId === "password")) { - throw new HTTPException(400, { - message: "Cannot update email for accounts using external authentication providers", - }); - } - - await Promise.all([ - firebaseAuth?.updateUser(user.uid, { email }), - Profile.updateOne({ uid: session.uid }, { email }), - ]); + await Profile.updateOne({ uid: session.user.id }, { email }); return c.json({ message: "Email updated successfully" }); }); diff --git a/backend/routes/auth.ts b/backend/routes/auth.ts index b5ec713b..c1fcc9ab 100644 --- a/backend/routes/auth.ts +++ b/backend/routes/auth.ts @@ -5,7 +5,7 @@ import { HTTPException } from "hono/http-exception"; import dayjs from "dayjs"; import { RESET_TOKEN_EXPIRATION } from "lib/config.js"; import { sendResetEmail } from "lib/email.js"; -import { auth as firebaseAuth } from "lib/firebaseAdmin.js"; +import { auth as betterAuth } from "lib/betterAuth.js"; const auth = new Hono(); @@ -14,10 +14,10 @@ auth.post("/forgot-password", async (c) => { if (!email) throw new HTTPException(400, { message: "Email is required" }); await connect(); - const user = await firebaseAuth?.getUserByEmail(email); + const user = await Profile.findOne({ email }).lean(); - if (!user || !user.providerData.some((provider) => provider.providerId === "password")) { - console.log("User not found/invalid provider", user?.providerData); + if (!user) { + console.log("User not found for email:", email); return Response.json({}); } @@ -41,25 +41,11 @@ auth.post("/reset-password", async (c) => { throw new HTTPException(400, { message: "Invalid or expired token" }); } - const user = await firebaseAuth?.getUser(profile.uid); - - if (!user) { - throw new HTTPException(400, { message: "User not found" }); - } - - if (user.providerData.some((provider) => provider.providerId === "google.com")) { - throw new HTTPException(400, { message: "You must use 'Sign in with Google' to login" }); - } else if (user.providerData.some((provider) => provider.providerId === "apple.com")) { - throw new HTTPException(400, { message: "You must use 'Sign in with Apple' to login" }); - } - if (!profile.resetTokenExpires || dayjs().isAfter(dayjs(profile.resetTokenExpires))) { throw new HTTPException(400, { message: "Reset token has expired" }); } - await firebaseAuth?.updateUser(user.uid, { password }); - - await Profile.updateOne({ uid: user.uid }, { $unset: { resetToken: "", resetTokenExpires: "" } }); + await Profile.updateOne({ uid: profile.uid }, { $unset: { resetToken: "", resetTokenExpires: "" } }); return c.json({ message: "Password reset successfully" }); }); diff --git a/backend/routes/profile.ts b/backend/routes/profile.ts index f5325fa0..f895b200 100644 --- a/backend/routes/profile.ts +++ b/backend/routes/profile.ts @@ -1,7 +1,6 @@ import { Hono } from "hono"; import { authenticate } from "lib/utils.js"; import { connect, Profile } from "lib/db.js"; -import { auth } from "lib/firebaseAdmin.js"; import { HTTPException } from "hono/http-exception"; const profile = new Hono(); @@ -11,29 +10,24 @@ profile.get("/", async (c) => { await connect(); let [profile] = await Promise.all([ - Profile.findOne({ uid: session.uid }).lean(), - Profile.updateOne({ uid: session.uid }, { lastActiveAt: new Date() }), + Profile.findOne({ uid: session.user.id }).lean(), + Profile.updateOne({ uid: session.user.id }, { lastActiveAt: new Date() }), ]); if (!profile) { - const user = await auth?.getUser(session.uid); - if (!user) { - throw new HTTPException(400, { message: "User not found" }); - } - const newProfile = await Profile.create({ uid: session.uid, name: user.displayName, email: user.email }); + const newProfile = await Profile.create({ + uid: session.user.id, + name: session.user.name, + email: session.user.email, + }); profile = newProfile.toObject(); } - if (!profile.name) { - const user = await auth?.getUser(session.uid); - if (!user) { - throw new HTTPException(400, { message: "User not found" }); - } - if (user.displayName) { - await Profile.updateOne({ uid: session.uid }, { name: user.displayName }); - profile = { ...profile, name: user.displayName }; - } + if (!profile.name && session.user.name) { + await Profile.updateOne({ uid: session.user.id }, { name: session.user.name }); + profile = { ...profile, name: session.user.name }; } + return c.json(profile); }); @@ -74,9 +68,9 @@ profile.patch("/", async (c) => { }) .filter((code) => code); - await Profile.updateOne({ uid: session.uid }, { ...data, lifelist: codes }); + await Profile.updateOne({ uid: session.user.id }, { ...data, lifelist: codes }); } else { - await Profile.updateOne({ uid: session.uid }, data); + await Profile.updateOne({ uid: session.user.id }, data); } return c.json({}); @@ -89,7 +83,7 @@ profile.post("/add-to-lifelist", async (c) => { const data = await c.req.json<{ code: string }>(); const { code } = data; - await Profile.updateOne({ uid: session.uid }, { $addToSet: { lifelist: code }, $pull: { exclusions: code } }); + await Profile.updateOne({ uid: session.user.id }, { $addToSet: { lifelist: code }, $pull: { exclusions: code } }); return c.json({}); }); diff --git a/backend/scripts/migrate-users.ts b/backend/scripts/migrate-users.ts new file mode 100644 index 00000000..a6530500 --- /dev/null +++ b/backend/scripts/migrate-users.ts @@ -0,0 +1,32 @@ +import { connect, Profile } from "../lib/db.js"; + +async function migrateUsers() { + try { + await connect(); + console.log("Connected to database"); + + const profiles = await Profile.find({}).lean(); + console.log(`Found ${profiles.length} profiles to migrate`); + + for (const profile of profiles) { + try { + console.log(`Checking user: ${profile.email || profile.uid}`); + + // For now, just log that users will need to reset their passwords + // since we can't migrate passwords from Firebase to Better Auth + console.log(`User ${profile.email || profile.uid} will need to reset password`); + } catch (error) { + console.error(`Error checking user ${profile.email || profile.uid}:`, error); + } + } + + console.log("Migration check completed"); + console.log("Note: Users will need to use 'Forgot Password' to reset their passwords"); + } catch (error) { + console.error("Migration failed:", error); + } finally { + process.exit(0); + } +} + +migrateUsers(); diff --git a/frontend/components/PasswordChangeForm.tsx b/frontend/components/PasswordChangeForm.tsx index 585eb256..d2231dbe 100644 --- a/frontend/components/PasswordChangeForm.tsx +++ b/frontend/components/PasswordChangeForm.tsx @@ -4,8 +4,6 @@ import Input from "components/Input"; import toast from "react-hot-toast"; import Field from "components/Field"; import { useRouter } from "next/router"; -import { auth } from "lib/firebase"; -import { EmailAuthProvider, reauthenticateWithCredential, updatePassword } from "firebase/auth"; export default function PasswordChangeForm() { const [currentPassword, setCurrentPassword] = useState(""); @@ -35,15 +33,22 @@ export default function PasswordChangeForm() { setIsLoading(true); try { - const user = auth?.currentUser; - if (!user || !user.email) { - throw new Error("User not found"); - } - - const credential = EmailAuthProvider.credential(user.email, currentPassword); - await reauthenticateWithCredential(user, credential); + const response = await fetch("/api/auth/change-password", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify({ + currentPassword, + newPassword, + }), + }); - await updatePassword(user, newPassword); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || "Password change failed"); + } setCurrentPassword(""); setNewPassword(""); @@ -51,11 +56,11 @@ export default function PasswordChangeForm() { toast.success("Password updated successfully"); router.push("/login?event=passwordUpdated"); } catch (error: any) { - if (error.code === "auth/wrong-password") { + if (error.message?.includes("current password")) { toast.error("Current password is incorrect"); - } else if (error.code === "auth/too-many-requests") { + } else if (error.message?.includes("too many attempts")) { toast.error("Too many attempts. Please try again later."); - } else if (error.code === "auth/requires-recent-login") { + } else if (error.message?.includes("recent login")) { toast.error("Please sign in again before changing your password"); router.push("/login"); } else { @@ -67,49 +72,38 @@ export default function PasswordChangeForm() { }; return ( -
-
- - ) => setCurrentPassword(e.target.value)} - required - /> - -
-
- - ) => setNewPassword(e.target.value)} - required - /> - -
-
- - ) => setConfirmPassword(e.target.value)} - required - /> - -
+ + + ) => setCurrentPassword(e.target.value)} + placeholder="Enter current password" + required + /> + + + + ) => setNewPassword(e.target.value)} + placeholder="Enter new password" + required + /> + -

You will need to sign in again after updating your password.

+ + ) => setConfirmPassword(e.target.value)} + placeholder="Confirm new password" + required + /> + -
diff --git a/frontend/hooks/useEmailLogin.ts b/frontend/hooks/useEmailLogin.ts index b085fed3..85ab861e 100644 --- a/frontend/hooks/useEmailLogin.ts +++ b/frontend/hooks/useEmailLogin.ts @@ -1,5 +1,3 @@ -import { auth } from "lib/firebase"; -import { signInWithEmailAndPassword } from "firebase/auth"; import toast from "react-hot-toast"; import { useRouter } from "next/router"; import { useState } from "react"; @@ -12,14 +10,26 @@ export default function useEmailLogin() { setLoading(true); const toastId = disableLoader ? undefined : toast.loading("Signing in..."); try { - if (!auth) throw new Error("Firebase auth not initialized"); - await signInWithEmailAndPassword(auth, email, password); + const response = await fetch("/api/auth/sign-in", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify({ email, password }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || "Sign in failed"); + } + router.push("/trips"); toast.dismiss(toastId); } catch (error: any) { - if (error.code === "auth/wrong-password") { - toast.error("Invalid password", { id: toastId }); - } else if (error.code === "auth/too-many-requests") { + if (error.message?.includes("Invalid credentials")) { + toast.error("Invalid email or password", { id: toastId }); + } else if (error.message?.includes("Too many attempts")) { toast.error("Too many attempts. Please try again later.", { id: toastId }); } else { toast.error("Error signing in", { id: toastId }); diff --git a/frontend/hooks/useEmailSignup.ts b/frontend/hooks/useEmailSignup.ts index dffca819..d95bdb75 100644 --- a/frontend/hooks/useEmailSignup.ts +++ b/frontend/hooks/useEmailSignup.ts @@ -1,5 +1,3 @@ -import { auth } from "lib/firebase"; -import { createUserWithEmailAndPassword, updateProfile } from "firebase/auth"; import toast from "react-hot-toast"; import { useRouter } from "next/router"; import { useState } from "react"; @@ -12,16 +10,27 @@ export default function useEmailSignup() { setLoading(true); const toastId = toast.loading("Creating account..."); try { - if (!auth) throw new Error("Firebase auth not initialized"); - const userCredential = await createUserWithEmailAndPassword(auth, email, password); - await updateProfile(userCredential.user, { displayName: name }); + const response = await fetch("/api/auth/sign-up", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify({ name, email, password }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || "Sign up failed"); + } + toast.success("Account created successfully!", { id: toastId }); router.push("/trips"); } catch (error: any) { console.error("Signup error:", error); - if (error.code === "auth/email-already-in-use") { + if (error.message?.includes("already exists")) { toast.error("Email address is already in use.", { id: toastId }); - } else if (error.code === "auth/weak-password") { + } else if (error.message?.includes("weak password")) { toast.error("Password is too weak. Please choose a stronger password.", { id: toastId }); } else { toast.error("Error creating account. Please try again.", { id: toastId }); diff --git a/frontend/hooks/useGoogleLogin.ts b/frontend/hooks/useGoogleLogin.ts index ae631090..04b5b417 100644 --- a/frontend/hooks/useGoogleLogin.ts +++ b/frontend/hooks/useGoogleLogin.ts @@ -1,6 +1,4 @@ import React from "react"; -import { auth } from "lib/firebase"; -import { signInWithPopup, GoogleAuthProvider } from "firebase/auth"; import toast from "react-hot-toast"; import { useRouter } from "next/router"; @@ -11,10 +9,7 @@ export default function useGoogleLogin() { const login = async () => { setLoading(true); try { - if (!auth) throw new Error("Firebase auth not initialized"); - const provider = new GoogleAuthProvider(); - await signInWithPopup(auth, provider); - router.push("/trips"); + window.location.href = "/api/auth/sign-in/google"; } catch (error) { toast.error("Login failed"); console.error(error); diff --git a/frontend/hooks/useFirebaseLogout.ts b/frontend/hooks/useLogout.ts similarity index 66% rename from frontend/hooks/useFirebaseLogout.ts rename to frontend/hooks/useLogout.ts index 4a5a4034..895461e3 100644 --- a/frontend/hooks/useFirebaseLogout.ts +++ b/frontend/hooks/useLogout.ts @@ -1,6 +1,5 @@ import React from "react"; -import { auth } from "lib/firebase"; -import { signOut } from "firebase/auth"; +import toast from "react-hot-toast"; import { useRouter } from "next/router"; import { useQueryClient } from "@tanstack/react-query"; @@ -12,12 +11,19 @@ export default function useFirebaseLogout() { const logout = async () => { setLoading(true); try { - if (!auth) throw new Error("Firebase auth not initialized"); - await signOut(auth); + const response = await fetch("/api/auth/sign-out", { + method: "POST", + credentials: "include", + }); + + if (!response.ok) { + throw new Error("Logout failed"); + } + queryClient.clear(); router.push("/"); } catch (error) { - alert("Error logging out"); + toast.error("Error logging out"); console.error(error); } finally { setLoading(false); diff --git a/frontend/lib/betterAuth.ts b/frontend/lib/betterAuth.ts new file mode 100644 index 00000000..7e22c27f --- /dev/null +++ b/frontend/lib/betterAuth.ts @@ -0,0 +1,10 @@ +import { createAuthClient } from "better-auth/react"; + +const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/v1"; +const betterAuthUrl = apiUrl.replace("/v1", ""); + +export const authClient = createAuthClient({ + baseURL: betterAuthUrl, +}); + +export const { useSession } = authClient; diff --git a/frontend/lib/http.ts b/frontend/lib/http.ts index 5693a361..8d7f2375 100644 --- a/frontend/lib/http.ts +++ b/frontend/lib/http.ts @@ -1,5 +1,5 @@ import { toast } from "react-hot-toast"; -import { auth } from "lib/firebase"; +import { useSession } from "lib/betterAuth"; type Params = { [key: string]: string | number | boolean; @@ -19,13 +19,12 @@ export const get = async (url: string, params: Params, showLoading?: boolean) => } if (showLoading) toast.loading("Loading...", { id: url }); - const token = await auth?.currentUser?.getIdToken(); + const res = await fetch(urlWithParams, { method: "GET", - headers: { - Authorization: `Bearer ${token || ""}`, - }, + credentials: "include", }); + if (showLoading) toast.dismiss(url); let json: any = {}; @@ -45,14 +44,13 @@ export const get = async (url: string, params: Params, showLoading?: boolean) => }; export const mutate = async (method: "POST" | "PUT" | "DELETE" | "PATCH", url: string, data?: any) => { - const token = await auth?.currentUser?.getIdToken(); const fullUrl = `${process.env.NEXT_PUBLIC_API_URL}${url}`; const res = await fetch(fullUrl, { method, headers: { "Content-Type": "application/json", - Authorization: `Bearer ${token || ""}`, }, + credentials: "include", body: JSON.stringify(data), }); diff --git a/frontend/package.json b/frontend/package.json index ba399716..9734df58 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "@tanstack/query-async-storage-persister": "^5.67.2", "@tanstack/react-query": "^5.66.11", "@tanstack/react-query-persist-client": "^5.67.2", + "better-auth": "^1.2.12", "clsx": "^1.2.1", "dayjs": "^1.11.0", "dotenv": "^16.4.6", diff --git a/frontend/pages/signup.tsx b/frontend/pages/signup.tsx index 178e1696..59ea4355 100644 --- a/frontend/pages/signup.tsx +++ b/frontend/pages/signup.tsx @@ -16,7 +16,7 @@ export default function Signup() { const { signup: emailSignup, loading: emailSignupLoading } = useEmailSignup(); const { login: googleLogin, loading: googleLoading } = useGoogleLogin(); - if (user?.uid && !userLoading) router.push("/trips"); + if (user?.id && !userLoading) router.push("/trips"); const isLoading = userLoading || emailSignupLoading || googleLoading; diff --git a/frontend/providers/user.tsx b/frontend/providers/user.tsx index 80f96270..94e15578 100644 --- a/frontend/providers/user.tsx +++ b/frontend/providers/user.tsx @@ -1,10 +1,14 @@ import React from "react"; -import { onAuthStateChanged } from "firebase/auth"; -import { auth } from "lib/firebase"; -import { User as FirebaseUser } from "firebase/auth"; +import { useSession } from "lib/betterAuth"; + +type User = { + id: string; + name?: string; + email?: string; +}; export const UserContext = React.createContext<{ - user: FirebaseUser | null; + user: User | null; refreshUser: () => Promise; loading: boolean; }>({ @@ -18,34 +22,31 @@ type Props = { }; const UserProvider = ({ children }: Props) => { - const [user, setUser] = React.useState(null); - const [loading, setLoading] = React.useState(true); - - React.useEffect(() => { - if (!auth) { - setLoading(false); - return; - } - onAuthStateChanged(auth, (user) => { - setUser(user); - setLoading(false); - }); - }, []); + const { data: session, isPending } = useSession(); const refreshUser = React.useCallback(async () => { - if (!auth) return; - await auth.currentUser?.reload(); - if (auth.currentUser) { - setUser({ ...auth.currentUser }); - } + // Better Auth handles session refresh automatically }, []); - return {children}; + return ( + + {children} + + ); }; -const useUser = () => { - const state = React.useContext(UserContext); - return { ...state }; +export const useUser = () => { + const context = React.useContext(UserContext); + if (!context) { + throw new Error("useUser must be used within a UserProvider"); + } + return context; }; -export { UserProvider, useUser }; +export default UserProvider; diff --git a/package-lock.json b/package-lock.json index 6689bf27..d8dfdfd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@hono/node-server": "^1.14.4", "@maphubs/tokml": "^0.6.1", "axios": "^1.9.0", + "better-auth": "^1.2.12", "dayjs": "^1.11.13", "deepl-node": "^1.18.0", "dotenv": "^16.5.0", @@ -52,6 +53,7 @@ "@tanstack/query-async-storage-persister": "^5.67.2", "@tanstack/react-query": "^5.66.11", "@tanstack/react-query-persist-client": "^5.67.2", + "better-auth": "^1.2.12", "clsx": "^1.2.1", "dayjs": "^1.11.0", "dotenv": "^16.4.6", @@ -6236,6 +6238,20 @@ "node": ">=6.9.0" } }, + "node_modules/@better-auth/utils": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.2.5.tgz", + "integrity": "sha512-uI2+/8h/zVsH8RrYdG8eUErbuGBk16rZKQfz8CjxQOyCE6v7BqFYEbFwvOkvl1KbUdxhqOnXp78+uE5h8qVEgQ==", + "dependencies": { + "typescript": "^5.8.2", + "uncrypto": "^0.1.3" + } + }, + "node_modules/@better-fetch/fetch": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.18.tgz", + "integrity": "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==" + }, "node_modules/@birdplan/scripts": { "resolved": "scripts", "link": true @@ -6829,6 +6845,11 @@ "node": ">=6" } }, + "node_modules/@hexagon/base64": { + "version": "1.1.28", + "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", + "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==" + }, "node_modules/@hono/node-server": { "version": "1.14.4", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.14.4.tgz", @@ -6850,6 +6871,11 @@ "url": "https://opencollective.com/js-sdsl" } }, + "node_modules/@levischuck/tiny-cbor": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", + "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==" + }, "node_modules/@maphubs/tokml": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/@maphubs/tokml/-/tokml-0.6.1.tgz", @@ -6870,6 +6896,25 @@ "sparse-bitfield": "^3.0.3" } }, + "node_modules/@noble/ciphers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.6.0.tgz", + "integrity": "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6914,6 +6959,59 @@ "node": ">=8.0.0" } }, + "node_modules/@peculiar/asn1-android": { + "version": "2.3.16", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.3.16.tgz", + "integrity": "sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.15", + "asn1js": "^3.0.5", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.15.tgz", + "integrity": "sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.15", + "@peculiar/asn1-x509": "^2.3.15", + "asn1js": "^3.0.5", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.15.tgz", + "integrity": "sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.15", + "@peculiar/asn1-x509": "^2.3.15", + "asn1js": "^3.0.5", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.15.tgz", + "integrity": "sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==", + "dependencies": { + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.15.tgz", + "integrity": "sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.15", + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -6997,6 +7095,28 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/@simplewebauthn/browser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.1.2.tgz", + "integrity": "sha512-aZnW0KawAM83fSBUgglP5WofbrLbLyr7CoPqYr66Eppm7zO86YX6rrCjRB3hQKPrL7ATvY4FVXlykZ6w6FwYYw==" + }, + "node_modules/@simplewebauthn/server": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.1.2.tgz", + "integrity": "sha512-VwoDfvLXSCaRiD+xCIuyslU0HLxVggeE5BL06+GbsP2l1fGf5op8e0c3ZtKoi+vSg1q4ikjtAghC23ze2Q3H9g==", + "dependencies": { + "@hexagon/base64": "^1.1.27", + "@levischuck/tiny-cbor": "^0.2.2", + "@peculiar/asn1-android": "^2.3.10", + "@peculiar/asn1-ecc": "^2.3.8", + "@peculiar/asn1-rsa": "^2.3.8", + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/asn1-x509": "^2.3.8" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -7229,6 +7349,19 @@ "node": ">=8" } }, + "node_modules/asn1js": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz", + "integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/async-retry": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", @@ -7291,6 +7424,44 @@ } ] }, + "node_modules/better-auth": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.2.12.tgz", + "integrity": "sha512-YicCyjQ+lxb7YnnaCewrVOjj3nPVa0xcfrOJK7k5MLMX9Mt9UnJ8GYaVQNHOHLyVxl92qc3C758X1ihqAUzm4w==", + "dependencies": { + "@better-auth/utils": "0.2.5", + "@better-fetch/fetch": "^1.1.18", + "@noble/ciphers": "^0.6.0", + "@noble/hashes": "^1.6.1", + "@simplewebauthn/browser": "^13.0.0", + "@simplewebauthn/server": "^13.0.0", + "better-call": "^1.0.8", + "defu": "^6.1.4", + "jose": "^6.0.11", + "kysely": "^0.28.2", + "nanostores": "^0.11.3", + "zod": "^3.24.1" + } + }, + "node_modules/better-auth/node_modules/jose": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz", + "integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/better-call": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.0.12.tgz", + "integrity": "sha512-ssq5OfB9Ungv2M1WVrRnMBomB0qz1VKuhkY2WxjHaLtlsHoSe9EPolj1xf7xf8LY9o3vfk3Rx6rCWI4oVHeBRg==", + "dependencies": { + "@better-fetch/fetch": "^1.1.4", + "rou3": "^0.5.1", + "set-cookie-parser": "^2.7.1", + "uncrypto": "^0.1.3" + } + }, "node_modules/bignumber.js": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", @@ -7564,6 +7735,11 @@ "node": ">=0.10.0" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -8558,6 +8734,14 @@ "node": ">=12.0.0" } }, + "node_modules/kysely": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.2.tgz", + "integrity": "sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A==", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/leac": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", @@ -8863,6 +9047,20 @@ "node": "^18 || >=20" } }, + "node_modules/nanostores": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-0.11.4.tgz", + "integrity": "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -9066,6 +9264,22 @@ "node": ">=6" } }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/queue-lit": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", @@ -9215,6 +9429,11 @@ "node": ">=0.10.0" } }, + "node_modules/rou3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.5.1.tgz", + "integrity": "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -9299,6 +9518,11 @@ "node": ">=10" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, "node_modules/shell-quote": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", @@ -9554,7 +9778,6 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9563,6 +9786,11 @@ "node": ">=14.17" } }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==" + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -9701,6 +9929,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "scripts": { "name": "@birdplan/scripts", "version": "1.0.0", diff --git a/shared/types.ts b/shared/types.ts index 8313b6b2..21937b5b 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -92,6 +92,10 @@ export type Profile = { lastActiveAt: Date | null; resetToken?: string; resetTokenExpires?: Date; + sessionId?: string; + sessionExpires?: Date; + verificationToken?: string; + verificationTokenExpires?: Date; }; export type Target = { From cbceb11ed411774702e49ccc397df22db9b4212c Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Mon, 14 Jul 2025 16:29:28 -0700 Subject: [PATCH 02/11] Update betterAuth.ts --- backend/lib/betterAuth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/lib/betterAuth.ts b/backend/lib/betterAuth.ts index fbdfab4f..b3d0309f 100644 --- a/backend/lib/betterAuth.ts +++ b/backend/lib/betterAuth.ts @@ -153,7 +153,7 @@ export const auth = betterAuth({ secret: BETTER_AUTH_CONFIG.secret, baseUrl: BETTER_AUTH_CONFIG.baseUrl, trustedOrigins: BETTER_AUTH_CONFIG.trustedOrigins, - adapter: mongoAdapter, + database: mongoAdapter, emailAndPassword: { enabled: true, requireEmailVerification: false, From e0845db5add6cc93bbd54f1ba3325313a7cb6046 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Mon, 14 Jul 2025 16:33:35 -0700 Subject: [PATCH 03/11] Resolve TS errors --- backend/index.ts | 17 ++++++++++------- backend/lib/utils.ts | 6 +++++- backend/routes/account.ts | 4 ++-- backend/routes/profile.ts | 22 +++++++++++----------- frontend/components/AccountDropdown.tsx | 2 +- frontend/modals/DeleteAccount.tsx | 2 +- frontend/pages/_app.tsx | 2 +- frontend/pages/account.tsx | 9 +++++---- frontend/providers/user.tsx | 20 +++++++++++++++++++- 9 files changed, 55 insertions(+), 29 deletions(-) diff --git a/backend/index.ts b/backend/index.ts index 8a23f906..fc97e0a6 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -48,10 +48,13 @@ app.onError((err, c) => { return c.json({ message: "Internal server error" }, 500); }); -const port = parseInt(process.env.PORT || "3001"); -console.log(`Server is running on port ${port}`); - -serve({ - fetch: app.fetch, - port, -}); +serve( + { + fetch: app.fetch, + port: 5100, + hostname: "0.0.0.0", + }, + (info) => { + console.log(`Server is running on http://localhost:${info.port}`); + } +); diff --git a/backend/lib/utils.ts b/backend/lib/utils.ts index a8620610..38c2c580 100644 --- a/backend/lib/utils.ts +++ b/backend/lib/utils.ts @@ -16,7 +16,11 @@ export async function authenticate(c: Context) { if (!session) { throw new HTTPException(401, { message: "Unauthorized" }); } - return session; + return { + uid: session.user.id, + name: session.user.name, + email: session.user.email, + }; } catch (error) { console.error("Better Auth error:", error); throw new HTTPException(401, { message: "Unauthorized" }); diff --git a/backend/routes/account.ts b/backend/routes/account.ts index a4895515..619d4aba 100644 --- a/backend/routes/account.ts +++ b/backend/routes/account.ts @@ -8,7 +8,7 @@ const account = new Hono(); account.delete("/", async (c) => { const session = await authenticate(c); - const uid = session.user.id; + const uid = session.uid; await connect(); @@ -32,7 +32,7 @@ account.post("/update-email", async (c) => { const { email } = await c.req.json<{ email: string }>(); if (!email) throw new HTTPException(400, { message: "Email is required" }); - await Profile.updateOne({ uid: session.user.id }, { email }); + await Profile.updateOne({ uid: session.uid }, { email }); return c.json({ message: "Email updated successfully" }); }); diff --git a/backend/routes/profile.ts b/backend/routes/profile.ts index f895b200..8331ee02 100644 --- a/backend/routes/profile.ts +++ b/backend/routes/profile.ts @@ -10,22 +10,22 @@ profile.get("/", async (c) => { await connect(); let [profile] = await Promise.all([ - Profile.findOne({ uid: session.user.id }).lean(), - Profile.updateOne({ uid: session.user.id }, { lastActiveAt: new Date() }), + Profile.findOne({ uid: session.uid }).lean(), + Profile.updateOne({ uid: session.uid }, { lastActiveAt: new Date() }), ]); if (!profile) { const newProfile = await Profile.create({ - uid: session.user.id, - name: session.user.name, - email: session.user.email, + uid: session.uid, + name: session.name, + email: session.email, }); profile = newProfile.toObject(); } - if (!profile.name && session.user.name) { - await Profile.updateOne({ uid: session.user.id }, { name: session.user.name }); - profile = { ...profile, name: session.user.name }; + if (!profile.name && session.name) { + await Profile.updateOne({ uid: session.uid }, { name: session.name }); + profile = { ...profile, name: session.name }; } return c.json(profile); @@ -68,9 +68,9 @@ profile.patch("/", async (c) => { }) .filter((code) => code); - await Profile.updateOne({ uid: session.user.id }, { ...data, lifelist: codes }); + await Profile.updateOne({ uid: session.uid }, { ...data, lifelist: codes }); } else { - await Profile.updateOne({ uid: session.user.id }, data); + await Profile.updateOne({ uid: session.uid }, data); } return c.json({}); @@ -83,7 +83,7 @@ profile.post("/add-to-lifelist", async (c) => { const data = await c.req.json<{ code: string }>(); const { code } = data; - await Profile.updateOne({ uid: session.user.id }, { $addToSet: { lifelist: code }, $pull: { exclusions: code } }); + await Profile.updateOne({ uid: session.uid }, { $addToSet: { lifelist: code }, $pull: { exclusions: code } }); return c.json({}); }); diff --git a/frontend/components/AccountDropdown.tsx b/frontend/components/AccountDropdown.tsx index dc26568c..634fce5a 100644 --- a/frontend/components/AccountDropdown.tsx +++ b/frontend/components/AccountDropdown.tsx @@ -4,7 +4,7 @@ import Icon from "components/Icon"; import { useUser } from "providers/user"; import Link from "next/link"; import clsx from "clsx"; -import useFirebaseLogout from "hooks/useFirebaseLogout"; +import useFirebaseLogout from "hooks/useLogout"; import { useProfile } from "providers/profile"; type Props = { diff --git a/frontend/modals/DeleteAccount.tsx b/frontend/modals/DeleteAccount.tsx index 11cda277..54127e95 100644 --- a/frontend/modals/DeleteAccount.tsx +++ b/frontend/modals/DeleteAccount.tsx @@ -4,7 +4,7 @@ import { Header, Body, Footer, useModal } from "providers/modals"; import useMutation from "hooks/useMutation"; import toast from "react-hot-toast"; import Button from "components/Button"; -import useFirebaseLogout from "hooks/useFirebaseLogout"; +import useFirebaseLogout from "hooks/useLogout"; export default function DeleteAccount() { const [confirmInput, setConfirmInput] = useState(""); diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index 2d3ee5dd..0ef2af9a 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -1,6 +1,6 @@ import type { AppProps } from "next/app"; import "styles/globals.css"; -import { UserProvider } from "providers/user"; +import UserProvider from "providers/user"; import { ModalProvider } from "providers/modals"; import { ProfileProvider } from "providers/profile"; import { TripProvider } from "providers/trip"; diff --git a/frontend/pages/account.tsx b/frontend/pages/account.tsx index 6994c227..914acac1 100644 --- a/frontend/pages/account.tsx +++ b/frontend/pages/account.tsx @@ -41,11 +41,12 @@ export default function Account() { if (loading) return
Loading...
; if (!user) return null; - const socialProviders = user.providerData - .filter((provider) => provider.providerId !== "password") - .map((provider) => providerNames[provider.providerId as keyof typeof providerNames]); + const socialProviders = + user.providerData + ?.filter((provider) => provider.providerId !== "password") + .map((provider) => providerNames[provider.providerId as keyof typeof providerNames]) || []; - const isEmailProvider = user.providerData.some((provider) => provider.providerId === "password"); + const isEmailProvider = user.providerData?.some((provider) => provider.providerId === "password") || false; return (
diff --git a/frontend/providers/user.tsx b/frontend/providers/user.tsx index 94e15578..ecc58cd3 100644 --- a/frontend/providers/user.tsx +++ b/frontend/providers/user.tsx @@ -3,8 +3,14 @@ import { useSession } from "lib/betterAuth"; type User = { id: string; + uid: string; name?: string; email?: string; + displayName?: string; + photoURL?: string; + providerData?: Array<{ + providerId: string; + }>; }; export const UserContext = React.createContext<{ @@ -28,11 +34,23 @@ const UserProvider = ({ children }: Props) => { // Better Auth handles session refresh automatically }, []); + const user = session?.user + ? { + id: session.user.id, + uid: session.user.id, + name: session.user.name, + email: session.user.email, + displayName: session.user.name, + photoURL: session.user.image || undefined, + providerData: [], + } + : null; + return ( From bf69d48cd207aa0ef19d0358adcc33b21d8616bc Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Mon, 14 Jul 2025 16:49:43 -0700 Subject: [PATCH 04/11] Update user.tsx --- frontend/providers/user.tsx | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/frontend/providers/user.tsx b/frontend/providers/user.tsx index ecc58cd3..a167864e 100644 --- a/frontend/providers/user.tsx +++ b/frontend/providers/user.tsx @@ -27,7 +27,7 @@ type Props = { children: React.ReactNode; }; -const UserProvider = ({ children }: Props) => { +const ClientUserProvider = ({ children }: Props) => { const { data: session, isPending } = useSession(); const refreshUser = React.useCallback(async () => { @@ -59,6 +59,30 @@ const UserProvider = ({ children }: Props) => { ); }; +const UserProvider = ({ children }: Props) => { + const [mounted, setMounted] = React.useState(false); + + React.useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return ( + {}, + }} + > + {children} + + ); + } + + return {children}; +}; + export const useUser = () => { const context = React.useContext(UserContext); if (!context) { From 273ed990293e86658d23faf69a25422fdb2190d8 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Mon, 14 Jul 2025 16:57:15 -0700 Subject: [PATCH 05/11] Various fixed --- backend/index.ts | 8 +++++++- frontend/hooks/useEmailLogin.ts | 15 ++++++--------- frontend/hooks/useEmailSignup.ts | 16 +++++++--------- frontend/lib/betterAuth.ts | 7 +++++-- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/backend/index.ts b/backend/index.ts index fc97e0a6..5dbb2414 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -18,7 +18,13 @@ import { auth as betterAuth } from "lib/betterAuth.js"; const app = new Hono(); if (process.env.CORS_ORIGINS) { - app.use("*", cors({ origin: process.env.CORS_ORIGINS.split(",") })); + app.use( + "*", + cors({ + origin: process.env.CORS_ORIGINS.split(","), + credentials: true, + }) + ); } else { console.error("CORS_ORIGINS is not set"); } diff --git a/frontend/hooks/useEmailLogin.ts b/frontend/hooks/useEmailLogin.ts index 85ab861e..50d2c847 100644 --- a/frontend/hooks/useEmailLogin.ts +++ b/frontend/hooks/useEmailLogin.ts @@ -1,6 +1,7 @@ import toast from "react-hot-toast"; import { useRouter } from "next/router"; import { useState } from "react"; +import authClient from "lib/betterAuth"; export default function useEmailLogin() { const router = useRouter(); @@ -10,17 +11,13 @@ export default function useEmailLogin() { setLoading(true); const toastId = disableLoader ? undefined : toast.loading("Signing in..."); try { - const response = await fetch("/api/auth/sign-in", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - credentials: "include", - body: JSON.stringify({ email, password }), + const { data, error } = await authClient.signIn.email({ + email, + password, + callbackURL: "/trips", }); - if (!response.ok) { - const error = await response.json(); + if (error) { throw new Error(error.message || "Sign in failed"); } diff --git a/frontend/hooks/useEmailSignup.ts b/frontend/hooks/useEmailSignup.ts index d95bdb75..3cf8afcb 100644 --- a/frontend/hooks/useEmailSignup.ts +++ b/frontend/hooks/useEmailSignup.ts @@ -1,6 +1,7 @@ import toast from "react-hot-toast"; import { useRouter } from "next/router"; import { useState } from "react"; +import authClient from "lib/betterAuth"; export default function useEmailSignup() { const router = useRouter(); @@ -10,17 +11,14 @@ export default function useEmailSignup() { setLoading(true); const toastId = toast.loading("Creating account..."); try { - const response = await fetch("/api/auth/sign-up", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - credentials: "include", - body: JSON.stringify({ name, email, password }), + const { data, error } = await authClient.signUp.email({ + email, + password, + name, + callbackURL: "/trips", }); - if (!response.ok) { - const error = await response.json(); + if (error) { throw new Error(error.message || "Sign up failed"); } diff --git a/frontend/lib/betterAuth.ts b/frontend/lib/betterAuth.ts index 7e22c27f..b5a26885 100644 --- a/frontend/lib/betterAuth.ts +++ b/frontend/lib/betterAuth.ts @@ -1,10 +1,13 @@ import { createAuthClient } from "better-auth/react"; const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/v1"; -const betterAuthUrl = apiUrl.replace("/v1", ""); +const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:5100"; export const authClient = createAuthClient({ - baseURL: betterAuthUrl, + baseURL: backendUrl, }); export const { useSession } = authClient; + +// Export the entire client for inspection +export default authClient; From 39a2b20452b7f17c159d3409dbac56b47c422515 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Mon, 14 Jul 2025 17:04:36 -0700 Subject: [PATCH 06/11] Fix sign up --- backend/lib/betterAuth.ts | 190 ++++++-------------------------------- 1 file changed, 28 insertions(+), 162 deletions(-) diff --git a/backend/lib/betterAuth.ts b/backend/lib/betterAuth.ts index b3d0309f..2bea38ac 100644 --- a/backend/lib/betterAuth.ts +++ b/backend/lib/betterAuth.ts @@ -1,169 +1,35 @@ import { betterAuth } from "better-auth"; import { connect } from "lib/db.js"; import { BETTER_AUTH_CONFIG } from "lib/config.js"; +import { mongodbAdapter } from "better-auth/adapters/mongodb"; +import mongoose from "mongoose"; -type UserData = { - id: string; - name?: string; - email?: string; -}; +async function initializeAuth() { + // Ensure connection is established + await connect(); -type SessionData = { - id: string; - userId: string; - expiresAt: Date; -}; + if (!mongoose.connection.db) { + throw new Error("MongoDB connection not established"); + } -type TokenData = { - token: string; - userId: string; - expiresAt: Date; -}; + return betterAuth({ + secret: BETTER_AUTH_CONFIG.secret, + baseUrl: BETTER_AUTH_CONFIG.baseUrl, + trustedOrigins: BETTER_AUTH_CONFIG.trustedOrigins, + database: mongodbAdapter(mongoose.connection.db), + emailAndPassword: { + enabled: true, + requireEmailVerification: false, + }, + session: { + expiresIn: BETTER_AUTH_CONFIG.sessionExpiry, + }, + email: { + from: "BirdPlan.app ", + provider: "resend", + apiKey: process.env.RESEND_API_KEY, + }, + }); +} -const mongoAdapter = { - async connect() { - await connect(); - }, - async disconnect() {}, - async createUser(data: UserData) { - const { Profile } = await import("lib/db.js"); - const user = await Profile.create({ - uid: data.id, - name: data.name, - email: data.email, - lifelist: [], - exceptions: [], - lastActiveAt: new Date(), - }); - return user.toObject(); - }, - async getUser(userId: string) { - const { Profile } = await import("lib/db.js"); - const user = await Profile.findOne({ uid: userId }).lean(); - return user; - }, - async getUserByEmail(email: string) { - const { Profile } = await import("lib/db.js"); - const user = await Profile.findOne({ email }).lean(); - return user; - }, - async updateUser(userId: string, data: Partial) { - const { Profile } = await import("lib/db.js"); - const user = await Profile.findOneAndUpdate({ uid: userId }, { $set: data }, { new: true }).lean(); - return user; - }, - async deleteUser(userId: string) { - const { Profile } = await import("lib/db.js"); - await Profile.deleteOne({ uid: userId }); - }, - async createSession(data: SessionData) { - const { Profile } = await import("lib/db.js"); - const session = await Profile.findOneAndUpdate( - { uid: data.userId }, - { - $set: { - sessionId: data.id, - sessionExpires: data.expiresAt, - }, - }, - { new: true } - ).lean(); - return session; - }, - async getSession(sessionId: string) { - const { Profile } = await import("lib/db.js"); - const user = await Profile.findOne({ - sessionId, - sessionExpires: { $gt: new Date() }, - }).lean(); - return user ? { id: sessionId, userId: user.uid, expiresAt: user.sessionExpires } : null; - }, - async deleteSession(sessionId: string) { - const { Profile } = await import("lib/db.js"); - await Profile.updateOne({ sessionId }, { $unset: { sessionId: "", sessionExpires: "" } }); - }, - async createVerificationToken(data: TokenData) { - const { Profile } = await import("lib/db.js"); - await Profile.updateOne( - { uid: data.userId }, - { - $set: { - verificationToken: data.token, - verificationTokenExpires: data.expiresAt, - }, - } - ); - return data; - }, - async getVerificationToken(token: string) { - const { Profile } = await import("lib/db.js"); - const user = await Profile.findOne({ - verificationToken: token, - verificationTokenExpires: { $gt: new Date() }, - }).lean(); - return user - ? { - token, - userId: user.uid, - expiresAt: user.verificationTokenExpires, - } - : null; - }, - async deleteVerificationToken(token: string) { - const { Profile } = await import("lib/db.js"); - await Profile.updateOne( - { verificationToken: token }, - { $unset: { verificationToken: "", verificationTokenExpires: "" } } - ); - }, - async createPasswordResetToken(data: TokenData) { - const { Profile } = await import("lib/db.js"); - await Profile.updateOne( - { uid: data.userId }, - { - $set: { - resetToken: data.token, - resetTokenExpires: data.expiresAt, - }, - } - ); - return data; - }, - async getPasswordResetToken(token: string) { - const { Profile } = await import("lib/db.js"); - const user = await Profile.findOne({ - resetToken: token, - resetTokenExpires: { $gt: new Date() }, - }).lean(); - return user - ? { - token, - userId: user.uid, - expiresAt: user.resetTokenExpires, - } - : null; - }, - async deletePasswordResetToken(token: string) { - const { Profile } = await import("lib/db.js"); - await Profile.updateOne({ resetToken: token }, { $unset: { resetToken: "", resetTokenExpires: "" } }); - }, -}; - -export const auth = betterAuth({ - secret: BETTER_AUTH_CONFIG.secret, - baseUrl: BETTER_AUTH_CONFIG.baseUrl, - trustedOrigins: BETTER_AUTH_CONFIG.trustedOrigins, - database: mongoAdapter, - emailAndPassword: { - enabled: true, - requireEmailVerification: false, - }, - session: { - expiresIn: BETTER_AUTH_CONFIG.sessionExpiry, - }, - email: { - from: "BirdPlan.app ", - provider: "resend", - apiKey: process.env.RESEND_API_KEY, - }, -}); +export const auth = await initializeAuth(); From 1086cd2ce16d76061732636ce82f6d5b2d5c0f82 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Mon, 14 Jul 2025 17:15:57 -0700 Subject: [PATCH 07/11] Update betterAuth.ts --- backend/lib/betterAuth.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/lib/betterAuth.ts b/backend/lib/betterAuth.ts index 2bea38ac..9ba5427a 100644 --- a/backend/lib/betterAuth.ts +++ b/backend/lib/betterAuth.ts @@ -5,7 +5,6 @@ import { mongodbAdapter } from "better-auth/adapters/mongodb"; import mongoose from "mongoose"; async function initializeAuth() { - // Ensure connection is established await connect(); if (!mongoose.connection.db) { From 926dd909412c2495beb5a7c373ad60f63632b6e7 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Mon, 14 Jul 2025 17:20:49 -0700 Subject: [PATCH 08/11] Restore lost code --- backend/index.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/index.ts b/backend/index.ts index 5dbb2414..e53be0c1 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -30,9 +30,9 @@ if (process.env.CORS_ORIGINS) { } app.route("/v1/profile", profile); +app.route("/v1/account", account); app.route("/v1/trips", trips); app.route("/v1/auth", auth); -app.route("/v1/account", account); app.route("/v1/support", support); app.route("/v1/taxonomy", taxonomy); app.route("/v1/quiz", quiz); @@ -46,12 +46,14 @@ app.all("/api/auth/*", async (c) => { return response; }); +app.notFound((c) => { + return c.json({ message: "Not Found" }, 404); +}); + app.onError((err, c) => { - if (err instanceof HTTPException) { - return c.json({ message: err.message }, err.status); - } - console.error("Server error:", err); - return c.json({ message: "Internal server error" }, 500); + const message = err instanceof Error ? err.message : "Internal Server Error"; + const status = err instanceof HTTPException ? err.status : 500; + return c.json({ message }, status); }); serve( From be1ece58bfb040ed75ec711816fe6225f54ff687 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Tue, 15 Jul 2025 16:19:46 -0700 Subject: [PATCH 09/11] Simplify betterAuth init --- backend/lib/betterAuth.ts | 9 ++++----- backend/lib/config.ts | 8 -------- 2 files changed, 4 insertions(+), 13 deletions(-) delete mode 100644 backend/lib/config.ts diff --git a/backend/lib/betterAuth.ts b/backend/lib/betterAuth.ts index 9ba5427a..14274f51 100644 --- a/backend/lib/betterAuth.ts +++ b/backend/lib/betterAuth.ts @@ -1,6 +1,5 @@ import { betterAuth } from "better-auth"; import { connect } from "lib/db.js"; -import { BETTER_AUTH_CONFIG } from "lib/config.js"; import { mongodbAdapter } from "better-auth/adapters/mongodb"; import mongoose from "mongoose"; @@ -12,16 +11,16 @@ async function initializeAuth() { } return betterAuth({ - secret: BETTER_AUTH_CONFIG.secret, - baseUrl: BETTER_AUTH_CONFIG.baseUrl, - trustedOrigins: BETTER_AUTH_CONFIG.trustedOrigins, + secret: process.env.BETTER_AUTH_SECRET, + baseUrl: process.env.BETTER_AUTH_BASE_URL, + trustedOrigins: process.env.BETTER_AUTH_TRUSTED_ORIGINS?.split(","), database: mongodbAdapter(mongoose.connection.db), emailAndPassword: { enabled: true, requireEmailVerification: false, }, session: { - expiresIn: BETTER_AUTH_CONFIG.sessionExpiry, + expiresIn: 60 * 60 * 24 * 90, }, email: { from: "BirdPlan.app ", diff --git a/backend/lib/config.ts b/backend/lib/config.ts deleted file mode 100644 index 74fe9fe9..00000000 --- a/backend/lib/config.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const RESET_TOKEN_EXPIRATION = 12; - -export const BETTER_AUTH_CONFIG = { - secret: process.env.BETTER_AUTH_SECRET || "your-secret-key-change-in-production", - baseUrl: process.env.BETTER_AUTH_BASE_URL || "http://localhost:3000", - trustedOrigins: process.env.BETTER_AUTH_TRUSTED_ORIGINS?.split(",") || ["http://localhost:3000"], - sessionExpiry: 60 * 60 * 24 * 7, -} as const; From f8a15f27c821aad67d26ff50a6440719e97f2148 Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Tue, 15 Jul 2025 16:23:34 -0700 Subject: [PATCH 10/11] Fix Can't resolve 'react' error --- frontend/next.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/next.config.js b/frontend/next.config.js index ba557614..aa2eeb74 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -2,6 +2,7 @@ const nextConfig = { reactStrictMode: false, output: "standalone", + transpilePackages: ["better-auth"], }; module.exports = nextConfig; From b140da325d6ec543dc95a32d533a9b12c705dadc Mon Sep 17 00:00:00 2001 From: Adam Jackson Date: Tue, 15 Jul 2025 16:24:39 -0700 Subject: [PATCH 11/11] Create config.ts --- backend/lib/config.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 backend/lib/config.ts diff --git a/backend/lib/config.ts b/backend/lib/config.ts new file mode 100644 index 00000000..2c85ff2f --- /dev/null +++ b/backend/lib/config.ts @@ -0,0 +1 @@ +export const RESET_TOKEN_EXPIRATION = 12; // hours