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 */}