diff --git a/.env.example b/.env.example
index 8c5a4bf2..24c40703 100644
--- a/.env.example
+++ b/.env.example
@@ -13,6 +13,25 @@ GIT_USEREMAIL=snomiao+comfy-pr@gmail.com
# Use docker compose up to get this mongodb before test
# MONGODB_URI=mongodb://localhost:27017
+# Better Auth Configuration
+# BETTER_AUTH_SECRET=your-secret-key-here
+#
+# Base URL for auth callbacks (auto-detected on Vercel via VERCEL_URL)
+# BETTER_AUTH_URL=http://localhost:3000
+# NEXTAUTH_URL=http://localhost:3000 # Backward compatible with NextAuth
+#
+# GitHub OAuth (supports both AUTH_GITHUB_* and GITHUB_* for backward compatibility)
+# AUTH_GITHUB_ID=your-github-client-id
+# AUTH_GITHUB_SECRET=your-github-client-secret
+# GITHUB_ID=your-github-client-id # NextAuth backward compatibility
+# GITHUB_SECRET=your-github-client-secret # NextAuth backward compatibility
+#
+# Google OAuth (supports both AUTH_GOOGLE_* and GOOGLE_* for backward compatibility)
+# AUTH_GOOGLE_ID=your-google-client-id
+# AUTH_GOOGLE_SECRET=your-google-client-secret
+# GOOGLE_ID=your-google-client-id # NextAuth backward compatibility
+# GOOGLE_SECRET=your-google-client-secret # NextAuth backward compatibility
+
AUTH_ADMINS=snomiao@gmail.com
SLACK_BOT_TOKEN="FILL_THIS_INTO_ .env.local"
diff --git a/MIGRATION.md b/MIGRATION.md
new file mode 100644
index 00000000..f927b809
--- /dev/null
+++ b/MIGRATION.md
@@ -0,0 +1,122 @@
+# NextAuth to Better Auth Migration
+
+## Overview
+
+This PR migrates the authentication system from NextAuth v5 to Better Auth.
+
+## Changes Made
+
+### New Files Created
+
+1. **`lib/auth.ts`** - Better Auth server configuration
+ - Configured MongoDB adapter
+ - Set up GitHub and Google OAuth providers
+ - Disabled email/password authentication (not used in original setup)
+
+2. **`lib/auth-client.ts`** - Better Auth client exports
+ - Exports `signIn`, `signOut`, and `useSession` for client components
+
+3. **`lib/getAuthUser.ts`** - Migrated auth user utility
+ - Moved from `app/api/auth/[...nextauth]/getAuthUser.tsx`
+ - Updated to use Better Auth session API
+
+4. **`app/api/auth/[...all]/route.ts`** - New Better Auth API route
+ - Replaces `app/api/auth/[...nextauth]/route.ts`
+
+### Modified Files
+
+1. **`app/auth/login/page.tsx`**
+ - Updated imports from `next-auth/react` to `@/lib/auth-client`
+ - Updated `signIn()` calls to use Better Auth's `signIn.social({ provider })` syntax
+
+2. **`package.json`**
+ - Added `better-auth@^1.3.28`
+ - Kept `next-auth` for now (can be removed after testing)
+
+3. **`.env.example`**
+ - Added Better Auth environment variable documentation
+
+### Files to Deprecate (After Testing)
+
+- `app/api/auth/[...nextauth]/auth.ts`
+- `app/api/auth/[...nextauth]/route.ts`
+- `app/api/auth/[...nextauth]/getAuthUser.tsx`
+- `app/api/auth/[...nextauth]/Users.tsx` (if not used elsewhere)
+
+## Environment Variables
+
+Better Auth uses the same environment variables as NextAuth for OAuth providers:
+
+- `AUTH_GITHUB_ID` - GitHub OAuth client ID
+- `AUTH_GITHUB_SECRET` - GitHub OAuth client secret
+- `AUTH_GOOGLE_ID` - Google OAuth client ID
+- `AUTH_GOOGLE_SECRET` - Google OAuth client secret
+
+Additional Better Auth-specific variables:
+
+- `BETTER_AUTH_SECRET` - Secret key for session encryption (optional in dev)
+- `BETTER_AUTH_URL` - Base URL for the application (optional, defaults to localhost:3000)
+- `NEXT_PUBLIC_APP_URL` - Public URL for client-side auth (optional)
+
+## Testing Checklist
+
+- [ ] GitHub OAuth login works
+- [ ] Google OAuth login works
+- [ ] Session persistence across page refreshes
+- [ ] Admin role assignment (@comfy.org and @drip.art emails)
+- [ ] Sign out functionality
+- [ ] Protected routes/pages still work
+- [ ] User data in MongoDB is correctly associated
+
+## Breaking Changes
+
+### API Changes
+
+1. **Session Object Structure**: Better Auth may have a different session object structure. Review all places where `session.user` is accessed.
+
+2. **Server-side Session Access**: Changed from:
+
+ ```ts
+ const session = await auth();
+ ```
+
+ to:
+
+ ```ts
+ const session = await auth.api.getSession({ headers });
+ ```
+
+3. **Client-side Sign In**: Changed from:
+ ```ts
+ signIn("google");
+ ```
+ to:
+ ```ts
+ signIn.social({ provider: "google" });
+ ```
+
+## Migration Steps
+
+1. Install Better Auth: ✅
+2. Create Better Auth configuration: ✅
+3. Update API routes: ✅
+4. Update client components: ✅
+5. Test authentication flows: ⏳
+6. Remove old NextAuth files: ⏳
+7. Update documentation: ⏳
+
+## Rollback Plan
+
+If issues are encountered:
+
+1. Revert changes to `app/auth/login/page.tsx`
+2. Remove `app/api/auth/[...all]/` directory
+3. Remove `lib/auth.ts` and `lib/auth-client.ts`
+4. Restore imports to use NextAuth
+5. Remove `better-auth` from package.json
+
+## Notes
+
+- The MongoDB adapter connection is shared with the existing setup
+- Admin role logic remains unchanged
+- Better Auth provides a more modern and actively maintained alternative to NextAuth v5
diff --git a/app/(dashboard)/followup/actions/send-gmail/page.tsx b/app/(dashboard)/followup/actions/send-gmail/page.tsx
index 853a03b5..08254a74 100644
--- a/app/(dashboard)/followup/actions/send-gmail/page.tsx
+++ b/app/(dashboard)/followup/actions/send-gmail/page.tsx
@@ -1,16 +1,8 @@
// /followup/actions/email
-import { getAuthUser } from "@/app/api/auth/[...nextauth]/getAuthUser";
-import {
- TaskDataOrNull,
- TaskError,
- TaskErrorOrNull,
- TaskOK,
-} from "@/packages/mongodb-pipeline-ts/Task";
-import {
- GCloudOAuth2Credentials,
- getGCloudOAuth2Client,
-} from "@/src/gcloud/GCloudOAuth2Credentials";
+import { getAuthUser } from "@/lib/getAuthUser";
+import { TaskDataOrNull, TaskError, TaskErrorOrNull, TaskOK } from "@/packages/mongodb-pipeline-ts/Task";
+import { GCloudOAuth2Credentials, getGCloudOAuth2Client } from "@/src/gcloud/GCloudOAuth2Credentials";
import { sendGmail } from "@/src/sendGmail";
import { yaml } from "@/src/utils/yaml";
import DIE from "@snomiao/die";
@@ -26,6 +18,10 @@ export const dynamic = "force-dynamic";
*/
export default async function GmailPage() {
const user = await getAuthUser();
+ if (!user) {
+ return
Please log in to continue
;
+ }
+
let authorizeUrl = "";
const getOAuth2Client = async () =>
await getGCloudOAuth2Client({
diff --git a/app/(dashboard)/rules/layout.tsx b/app/(dashboard)/rules/layout.tsx
index d10fb028..cb89c29a 100644
--- a/app/(dashboard)/rules/layout.tsx
+++ b/app/(dashboard)/rules/layout.tsx
@@ -1,10 +1,10 @@
+import { getAuthUser } from "@/lib/getAuthUser";
import { forbidden } from "next/navigation";
import type { ReactNode } from "react";
-import { getAuthUser } from "../../api/auth/[...nextauth]/getAuthUser";
export default async function RulesLayout({ children }: { children: ReactNode }) {
const user = await getAuthUser();
- const isAdmin = user.admin;
+ const isAdmin = user?.admin;
if (!isAdmin) return forbidden();
return (
diff --git a/app/api/auth/[...all]/route.ts b/app/api/auth/[...all]/route.ts
new file mode 100644
index 00000000..e11351a1
--- /dev/null
+++ b/app/api/auth/[...all]/route.ts
@@ -0,0 +1,4 @@
+import { auth } from "@/lib/auth";
+import { toNextJsHandler } from "better-auth/next-js";
+
+export const { GET, POST } = toNextJsHandler(auth.handler);
diff --git a/app/api/auth/[...nextauth]/Users.tsx b/app/api/auth/[...nextauth]/Users.tsx
deleted file mode 100644
index dec32ef9..00000000
--- a/app/api/auth/[...nextauth]/Users.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { db } from "@/src/db";
-import sflow from "sflow";
-
-export const Users = db.collection<{
- email: string;
- admin: boolean;
-}>("Users");
-
-// setup admins
-await sflow(process.env.AUTH_ADMINS?.split(",").map((e) => e.toLowerCase()) ?? [])
- .pMap((email) => Users.updateOne({ email }, { $set: { email, admin: true } }, { upsert: true }))
- .toCount();
diff --git a/app/api/auth/[...nextauth]/auth.ts b/app/api/auth/[...nextauth]/auth.ts
deleted file mode 100644
index fcd70ee5..00000000
--- a/app/api/auth/[...nextauth]/auth.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { mongo } from "@/src/db";
-import { MongoDBAdapter } from "@auth/mongodb-adapter";
-import NextAuth from "next-auth";
-import GitHub from "next-auth/providers/github";
-import Google from "next-auth/providers/google";
-import Nodemailer from "next-auth/providers/nodemailer";
-import "nodemailer";
-export const { handlers, auth, signIn, signOut } = NextAuth({
- adapter: MongoDBAdapter(Promise.resolve(mongo)),
- providers: [
- ...(process.env.AUTH_EMAIL_SERVER
- ? [
- Nodemailer({
- name: "Email Magic Link",
- server: process.env.AUTH_EMAIL_SERVER,
- from: process.env.AUTH_EMAIL_FROM,
- }),
- ]
- : []),
- ...(process.env.AUTH_GITHUB_SECRET ? [GitHub] : []),
- ...(process.env.AUTH_GOOGLE_SECRET ? [Google] : []),
- ],
-});
diff --git a/app/api/auth/[...nextauth]/getAuthUser.tsx b/app/api/auth/[...nextauth]/getAuthUser.tsx
deleted file mode 100644
index 62857fa2..00000000
--- a/app/api/auth/[...nextauth]/getAuthUser.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { auth, signIn } from "@/app/api/auth/[...nextauth]/auth";
-import { Users } from "./Users";
-
-export async function getAuthUser() {
- const session = await auth();
- const authUser = session?.user ?? (await signIn());
- const email = authUser.email || (await signIn()); // must have email
- const user = {
- ...authUser,
- email,
- ...(await Users.findOne({ email })),
- };
-
- // TODO: move this into .env file, it's public anyway
- user.admin ||= user.email.endsWith("@comfy.org");
- user.admin ||= user.email.endsWith("@drip.art"); // legacy domain
-
- return user;
-}
diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts
deleted file mode 100644
index 47124b4f..00000000
--- a/app/api/auth/[...nextauth]/route.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-import { handlers } from "./auth";
-export const { GET, POST } = handlers;
diff --git a/app/auth/login/page-bak.tsx b/app/auth/login/page-bak.tsx
deleted file mode 100644
index ad19fa88..00000000
--- a/app/auth/login/page-bak.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-"use server";
-
-import { signIn } from "@/app/api/auth/[...nextauth]/auth";
-import { FcGoogle } from "react-icons/fc";
-
-/**
- *
- * @author: snomiao
- */
-export default async function LoginPage() {
- return (
-
-
-
Login
-
-
-
-
-
- );
-}
diff --git a/app/auth/login/page.tsx b/app/auth/login/page.tsx
index 1d783f7e..c94b7ab1 100644
--- a/app/auth/login/page.tsx
+++ b/app/auth/login/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { signIn } from "next-auth/react";
+import { signIn } from "@/lib/auth-client";
import { FaGithub } from "react-icons/fa";
import { FcGoogle } from "react-icons/fc";
@@ -24,18 +24,16 @@ export default function LoginPage() {
{/* Google OAuth Button */}
{/* GitHub OAuth Button */}