From 507487c7d9544cffbddd5b6fa463e43b9f759211 Mon Sep 17 00:00:00 2001 From: Mannu Date: Sun, 10 May 2026 04:18:08 +0530 Subject: [PATCH] feat: add email provider for magic links --- CLAUDE.md | 62 ++++++++++++++++++++++++++++++++++++----- src/app/login/page.tsx | 38 ++++++++++++++++++++----- src/app/verify/page.tsx | 18 ++++++++++++ src/auth.ts | 19 +++++++++++-- 4 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 src/app/verify/page.tsx diff --git a/CLAUDE.md b/CLAUDE.md index 1008a2a..2e970df 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,23 +9,71 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Common Commands ```bash +# Development pnpm dev # Start local dev server -pnpm build # Production build -pnpm start # Start production server +pnpm build # Production build +npm ci # Clean install for Docker + +# Database (Drizzle) +npx drizzle-kit generate # Generate migration from schema +npx drizzle-kit push # Push schema to DB ``` ## Environment Variables -See `.env.example` for required variables including DATABASE_URL, AUTH_SECRET, R2 credentials, LiteLLM config. +Required in `.env.local`: + +```env +DATABASE_URL=postgresql://tia:tia_local_dev@localhost:5433/tia_dev +AUTH_SECRET= +AUTH_URL=http://localhost:3000 +RESEND_API_KEY= +``` + +## Hits & Learned Lessons + +### Build Issues Fixed +1. **pnpm 11 + Node 20 incompatibility**: pnpm 11 requires Node 22+. Fixed by using Node 22 in Dockerfile. +2. **pnpm ignored builds in Docker**: Had to switch from pnpm to npm in Dockerfile (`npm ci` instead of `pnpm install`). +3. **Drizzle adapter type errors**: Using simplified DrizzleAdapter without passing table configs directly fixes type errors. +4. **nodemailer missing**: NextAuth email provider requires nodemailer as a dependency. +5. **Middleware warning**: Next.js 16 shows deprecation warning for middleware file convention. + +### Database Setup +- Local: Docker Compose with pgvector on port 5433, Redis on 6380 +- Schema: 8 tables (users, accounts, sessions, verification_tokens, families, family_members, children, family_invites) +- Migrations: Run via `npx drizzle-kit push` or docker exec with SQL files + +### Docker Build +- Use `npm ci` instead of pnpm in Docker for reliability +- Node 22 alpine image +- Multi-stage build (deps → builder → runner) +- Need to install nodemailer separately for email auth + +## Architecture + +- **Framework**: Next.js 16 (App Router) with TypeScript +- **Database**: PostgreSQL 16 with pgvector (Drizzle ORM) +- **Auth**: NextAuth v5 beta +- **Deployment**: Dokploy (Docker) ## Sprint Plan -Current: Sprint 0 (Foundation Infrastructure) -- Sprint 0: Dev environment, Docker, Dokploy deploy -- Sprint 1: Auth + Database with RLS +Current: Sprint 1 (Database + Auth) + +- Sprint 0: Dev environment, Docker, Dokploy deploy ✅ +- Sprint 1: Auth + Database with RLS (in progress) - Sprint 2: Fast-log engine - Sprint 3: Medical vault - Sprint 4: Media pipeline - Sprint 5: AI Brain - Sprint 6: UI/UX polish -- Sprint 7: Telegram alerts + launch \ No newline at end of file +- Sprint 7: Telegram alerts + launch + +## Key Files + +- `src/auth.ts` - NextAuth configuration +- `src/db/schema/auth.ts` - Auth tables (users, sessions, accounts) +- `src/db/schema/family.ts` - Family, members, children tables +- `src/app/login/page.tsx` - Login page +- `Dockerfile` - Production build (uses npm, not pnpm) \ No newline at end of file diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 06bd0a6..7f64608 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -5,15 +5,36 @@ import { useState } from "react"; export default function LoginPage() { const [email, setEmail] = useState(""); - const [loading, setLoading] = useState(false); + const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle"); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - setLoading(true); - await signIn("email", { email }); - setLoading(false); + setStatus("loading"); + + const result = await signIn("email", { email, redirect: false }); + + if (result?.ok) { + setStatus("success"); + } else { + setStatus("error"); + setTimeout(() => setStatus("idle"), 3000); + } }; + if (status === "success") { + return ( +
+
+
+

Check your email!

+

+ We sent a magic link to {email} +

+
+
+ ); + } + return (
@@ -23,7 +44,7 @@ export default function LoginPage() {
setEmail(e.target.value)} className="w-full p-4 border rounded-2xl bg-white shadow-sm focus:ring-2 focus:ring-rose-200 outline-none" @@ -31,11 +52,14 @@ export default function LoginPage() { /> + {status === "error" && ( +

Something went wrong. Try again.

+ )}
diff --git a/src/app/verify/page.tsx b/src/app/verify/page.tsx new file mode 100644 index 0000000..b4b0d35 --- /dev/null +++ b/src/app/verify/page.tsx @@ -0,0 +1,18 @@ +"use client"; + +export default function VerifyPage() { + return ( +
+
+
✉️
+

Check your email

+

+ We sent you a magic link to sign in. +

+

+ Click the link in the email to sign in to Tia. +

+
+
+ ); +} \ No newline at end of file diff --git a/src/auth.ts b/src/auth.ts index addcc39..52a5434 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,16 +1,29 @@ import NextAuth from "next-auth"; import { DrizzleAdapter } from "@auth/drizzle-adapter"; +import Email from "next-auth/providers/email"; import { db } from "@/db"; -import { users } from "@/db/schema/auth"; export const { handlers, auth, signIn, signOut } = NextAuth({ adapter: DrizzleAdapter(db), - providers: [], + providers: [ + Email({ + server: { + host: process.env.EMAIL_SERVER_HOST || "smtp.resend.com", + port: Number(process.env.EMAIL_SERVER_PORT) || 587, + auth: { + user: process.env.EMAIL_SERVER_USER || "resend", + pass: process.env.RESEND_API_KEY || "", + }, + }, + from: process.env.EMAIL_FROM || "Tia ", + }), + ], pages: { signIn: "/login", + verifyRequest: "/verify", }, session: { strategy: "database", - maxAge: 30 * 24 * 60 * 60, + maxAge: 30 * 24 * 60 * 60, // 30 days }, }); \ No newline at end of file