Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,18 @@ 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();

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");
}
Expand All @@ -34,6 +41,11 @@ app.route("/v1/region", region);
app.route("/v1/ebird-proxy", ebirdProxy);
app.route("/v1/invites", invites);

app.all("/api/auth/*", async (c) => {
const response = await betterAuth.handler(c.req.raw);
return response;
});

app.notFound((c) => {
return c.json({ message: "Not Found" }, 404);
});
Expand Down
33 changes: 33 additions & 0 deletions backend/lib/betterAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { betterAuth } from "better-auth";
import { connect } from "lib/db.js";
import { mongodbAdapter } from "better-auth/adapters/mongodb";
import mongoose from "mongoose";

async function initializeAuth() {
await connect();

if (!mongoose.connection.db) {
throw new Error("MongoDB connection not established");
}

return betterAuth({
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: 60 * 60 * 24 * 90,
},
email: {
from: "BirdPlan.app <support@birdplan.app>",
provider: "resend",
apiKey: process.env.RESEND_API_KEY,
},
});
}

export const auth = await initializeAuth();
30 changes: 14 additions & 16 deletions backend/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -9,22 +9,20 @@ 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 {
uid: session.user.id,
name: session.user.name,
email: session.user.email,
};
} catch (error) {
console.error("Firebase auth error:", error);
console.error("Better Auth error:", error);
throw new HTTPException(401, { message: "Unauthorized" });
}
}
Expand Down
4 changes: 4 additions & 0 deletions backend/models/Profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const fields: Record<keyof Omit<Profile, "createdAt" | "updatedAt">, any> = {
lastActiveAt: { type: Date, default: new Date() },
resetToken: String,
resetTokenExpires: Date,
sessionId: String,
sessionExpires: Date,
verificationToken: String,
verificationTokenExpires: Date,
};

const ProfileSchema = new Schema(fields, {
Expand Down
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 1 addition & 16 deletions backend/routes/account.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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({});
});

Expand All @@ -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.uid }, { email });
return c.json({ message: "Email updated successfully" });
});

Expand Down
24 changes: 5 additions & 19 deletions backend/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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({});
}

Expand All @@ -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" });
});
Expand Down
24 changes: 9 additions & 15 deletions backend/routes/profile.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -16,24 +15,19 @@ profile.get("/", async (c) => {
]);

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.uid,
name: session.name,
email: session.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.name) {
await Profile.updateOne({ uid: session.uid }, { name: session.name });
profile = { ...profile, name: session.name };
}

return c.json(profile);
});

Expand Down
32 changes: 32 additions & 0 deletions backend/scripts/migrate-users.ts
Original file line number Diff line number Diff line change
@@ -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();
2 changes: 1 addition & 1 deletion frontend/components/AccountDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading