diff --git a/apps/web/src/auth.ts b/apps/web/src/auth.ts
index 41a0fa4..5469aec 100644
--- a/apps/web/src/auth.ts
+++ b/apps/web/src/auth.ts
@@ -12,6 +12,7 @@ declare module "next-auth" {
email: string;
name?: string | null;
image?: string | null;
+ isEmailVerified: boolean;
};
}
}
@@ -19,6 +20,7 @@ declare module "next-auth" {
declare module "@auth/core/jwt" {
interface JWT {
id: string;
+ isEmailVerified: boolean;
}
}
@@ -58,14 +60,24 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
}),
],
callbacks: {
- jwt({ token, user }) {
+ async jwt({ token, user, trigger }) {
if (user) {
token.id = user.id as string;
}
+ if (trigger === "update" || user) {
+ const dbUser = await prisma.user.findUnique({
+ where: { id: token.id },
+ select: { emailVerified: true },
+ });
+ if (dbUser) {
+ token.isEmailVerified = dbUser.emailVerified;
+ }
+ }
return token;
},
session({ session, token }) {
session.user.id = token.id;
+ session.user.isEmailVerified = token.isEmailVerified;
return session;
},
},
diff --git a/apps/web/src/lib/email.ts b/apps/web/src/lib/email.ts
new file mode 100644
index 0000000..8ecb169
--- /dev/null
+++ b/apps/web/src/lib/email.ts
@@ -0,0 +1,36 @@
+import nodemailer from "nodemailer";
+
+interface SendEmailOptions {
+ to: string;
+ subject: string;
+ html: string;
+}
+
+export async function sendEmail({ to, subject, html }: SendEmailOptions) {
+ const password = process.env.EMAIL_PASSWORD;
+
+ if (!password) {
+ console.warn(
+ "[email] EMAIL_PASSWORD not set — skipping email send to:",
+ to
+ );
+ return;
+ }
+
+ const transporter = nodemailer.createTransport({
+ host: "smtp.migadu.com",
+ port: 465,
+ secure: true,
+ auth: {
+ user: "hunter@repi.fun",
+ pass: password,
+ },
+ });
+
+ await transporter.sendMail({
+ from: "AgentLens ",
+ to,
+ subject,
+ html,
+ });
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index a2f0c64..39d67f1 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,6 +15,7 @@ services:
- STRIPE_WEBHOOK_SECRET=whsec_ZGT3JCrEK6GWP3cIMvYfrfLplZ3rMn0m
- STRIPE_STARTER_PRICE_ID=price_1SzJUlR8i0An4Wz7gZeYgzBY
- STRIPE_PRO_PRICE_ID=price_1SzJVWR8i0An4Wz755hBrxzn
+ - EMAIL_PASSWORD=${EMAIL_PASSWORD:-}
depends_on:
redis:
condition: service_started
diff --git a/package-lock.json b/package-lock.json
index f4bf21e..ca4eabf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -28,6 +28,7 @@
"lucide-react": "^0.469.0",
"next": "^15.1.0",
"next-auth": "^5.0.0-beta.30",
+ "nodemailer": "^6.10.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"shiki": "^3.22.0",
@@ -39,6 +40,7 @@
"@types/bcryptjs": "^2.4.6",
"@types/dagre": "^0.7.53",
"@types/node": "^22.0.0",
+ "@types/nodemailer": "^7.0.9",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"postcss": "^8.5.0",
@@ -2136,6 +2138,16 @@
"undici-types": "~6.21.0"
}
},
+ "node_modules/@types/nodemailer": {
+ "version": "7.0.9",
+ "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.9.tgz",
+ "integrity": "sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/react": {
"version": "19.2.13",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz",
@@ -3467,6 +3479,15 @@
"devOptional": true,
"license": "MIT"
},
+ "node_modules/nodemailer": {
+ "version": "6.10.1",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
+ "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/nypm": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz",
@@ -4449,7 +4470,7 @@
},
"packages/opencode-plugin": {
"name": "opencode-agentlens",
- "version": "0.1.6",
+ "version": "0.1.7",
"license": "MIT",
"dependencies": {
"agentlens-sdk": "*"
@@ -4525,7 +4546,7 @@
},
"packages/sdk-ts": {
"name": "agentlens-sdk",
- "version": "0.1.3",
+ "version": "0.1.4",
"license": "MIT",
"devDependencies": {
"tsup": "^8.3.0",
diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma
index 0c15eac..1bc965a 100644
--- a/packages/database/prisma/schema.prisma
+++ b/packages/database/prisma/schema.prisma
@@ -14,6 +14,7 @@ model User {
email String @unique
passwordHash String
name String?
+ emailVerified Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -21,10 +22,42 @@ model User {
subscription Subscription?
apiKeys ApiKey[]
traces Trace[]
+ passwordResetTokens PasswordResetToken[]
+ emailVerificationTokens EmailVerificationToken[]
@@index([email])
}
+model PasswordResetToken {
+ id String @id @default(cuid())
+ userId String
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ token String @unique // SHA-256 hash of the raw token
+ expiresAt DateTime
+ used Boolean @default(false)
+
+ createdAt DateTime @default(now())
+
+ @@index([token])
+ @@index([userId])
+}
+
+model EmailVerificationToken {
+ id String @id @default(cuid())
+ userId String
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ token String @unique // SHA-256 hash of the raw token
+ expiresAt DateTime
+ used Boolean @default(false)
+
+ createdAt DateTime @default(now())
+
+ @@index([token])
+ @@index([userId])
+}
+
model ApiKey {
id String @id @default(cuid())
userId String