-
Notifications
You must be signed in to change notification settings - Fork 97
feat: email integration for BetterAuth callbacks #2021
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
270a9dd
8ecdfc9
f638d53
2efad53
8396dc8
1b67e91
4de4e65
71c67db
0300ab6
4afddc8
1bc303b
df804f4
b62217e
26b622f
18177da
89335b1
000be7c
713029d
98201e2
11195d3
3ae03d8
ca08349
bf9b104
09090ba
12bb8c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "@inkeep/agents-core": patch | ||
| "@inkeep/agents-api": patch | ||
| --- | ||
|
|
||
| Add email integration for BetterAuth callbacks (invitation and password reset emails via SMTP) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,8 @@ | ||
| import { createApiError, getPendingInvitationsByEmail } from '@inkeep/agents-core'; | ||
| import { | ||
| createApiError, | ||
| getEmailSendStatus, | ||
| getPendingInvitationsByEmail, | ||
| } from '@inkeep/agents-core'; | ||
| import { Hono } from 'hono'; | ||
| import { HTTPException } from 'hono/http-exception'; | ||
| import runDbClient from '../../../data/db/runDbClient'; | ||
|
|
@@ -104,6 +108,40 @@ invitationsRoutes.get('/verify', async (c) => { | |
| // Require authentication for remaining routes | ||
| invitationsRoutes.use('*', sessionAuth()); | ||
|
|
||
| // Internal route - not exposed in OpenAPI spec | ||
| invitationsRoutes.get('/:id/email-status', async (c) => { | ||
| const invitationId = c.req.param('id'); | ||
| const session = c.get('session'); | ||
| const auth = c.get('auth'); | ||
|
|
||
| if (!auth || !session) { | ||
| return c.json({ emailSent: false }); | ||
| } | ||
|
|
||
| const activeMember = await auth.api.getActiveMember({ | ||
| headers: c.req.raw.headers, | ||
| }); | ||
|
|
||
| if (!activeMember || (activeMember.role !== 'admin' && activeMember.role !== 'owner')) { | ||
| throw createApiError({ | ||
| code: 'forbidden', | ||
| message: 'Not authorized to view invitation email status', | ||
| }); | ||
| } | ||
|
|
||
| const status = getEmailSendStatus(invitationId); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Minor: Cross-tenant invitation status disclosure Issue: The endpoint verifies the caller is admin/owner of their active organization (line 121-130), but does not verify the invitation ID belongs to that organization. An admin from Org A can query email status for invitations belonging to Org B. Why: While the exposed data is limited ( Fix: Before calling // Option 1: Look up invitation (adds DB query)
const invitation = await auth.api.getInvitation?.({ query: { id: invitationId } });
if (!invitation || invitation.organizationId !== activeMember.organizationId) {
return c.json({ emailSent: false });
}
// Option 2: Store orgId in status bridge (requires email-send-status-store change)Refs:
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 4567c8d. Added |
||
|
|
||
| if (!status) { | ||
| return c.json({ emailSent: false }); | ||
| } | ||
|
|
||
| if (status.organizationId && status.organizationId !== activeMember.organizationId) { | ||
| return c.json({ emailSent: false }); | ||
| } | ||
|
|
||
| return c.json({ emailSent: status.emailSent, error: status.error }); | ||
| }); | ||
|
|
||
| // GET /api/invitations/pending?email=user@example.com - Get pending invitations for an email | ||
| // Internal route - not exposed in OpenAPI spec | ||
| invitationsRoutes.get('/pending', async (c) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| --- | ||
| title: Configure Email | ||
| sidebarTitle: Email | ||
| description: Set up SMTP email for team invitations and password resets | ||
| icon: LuMail | ||
| --- | ||
|
|
||
| Configure email delivery so the platform can send team invitation emails and password reset links. Without email configured, these features degrade gracefully — invitation links are shown directly in the UI and password reset is disabled. | ||
|
|
||
| <Note> | ||
| For a feature overview of invitations and password reset, see [Access Control](/visual-builder/access-control). | ||
| </Note> | ||
|
|
||
| ## How It Works | ||
|
|
||
| The email system uses a priority-based transport selection: | ||
|
|
||
| | Priority | Transport | When used | | ||
| |----------|-----------|-----------| | ||
| | 1 | **Resend** | `RESEND_API_KEY` is set | | ||
| | 2 | **Generic SMTP** | `SMTP_HOST` is set (and no Resend key) | | ||
| | 3 | **Disabled** | Neither is set — email features are turned off | | ||
|
|
||
| Both transports require `SMTP_FROM_ADDRESS` to be set. If the from address is missing, email is disabled even when a transport is configured. | ||
|
|
||
| ## Environment Variables | ||
|
|
||
| | Variable | Required | Description | | ||
| |----------|----------|-------------| | ||
| | `SMTP_FROM_ADDRESS` | Yes | Sender email address (e.g., `noreply@example.com`) | | ||
| | `SMTP_FROM_NAME` | No | Sender display name (e.g., `Inkeep`). Defaults to the address. | | ||
| | `SMTP_REPLY_TO` | No | Reply-to address. Defaults to the from address. | | ||
|
|
||
| ### Option A: Resend (Recommended for Production) | ||
|
|
||
| [Resend](https://resend.com) provides a managed email delivery service with high deliverability. | ||
|
|
||
| | Variable | Required | Description | | ||
| |----------|----------|-------------| | ||
| | `RESEND_API_KEY` | Yes | Your Resend API key | | ||
|
|
||
| ```dotenv title=".env" | ||
| RESEND_API_KEY=re_your_api_key | ||
| SMTP_FROM_ADDRESS=notifications@yourdomain.com | ||
| SMTP_FROM_NAME=Inkeep | ||
| ``` | ||
|
|
||
| <Tip> | ||
| When using Resend, you don't need to configure any SMTP host or port variables — the system connects to Resend's SMTP relay automatically. | ||
| </Tip> | ||
|
|
||
| ### Option B: Generic SMTP | ||
|
|
||
| Use any SMTP server (Amazon SES, SendGrid, Gmail, self-hosted Postfix, etc.). | ||
|
|
||
| | Variable | Required | Description | | ||
| |----------|----------|-------------| | ||
| | `SMTP_HOST` | Yes | SMTP server hostname | | ||
| | `SMTP_PORT` | No | Server port. Default: `587` | | ||
| | `SMTP_SECURE` | No | Use TLS/SSL. Auto-detected from port if not set (`true` for port 465). | | ||
| | `SMTP_USER` | No | SMTP authentication username | | ||
| | `SMTP_PASSWORD` | No | SMTP authentication password | | ||
|
|
||
| ```dotenv title=".env" | ||
| SMTP_HOST=smtp.example.com | ||
| SMTP_PORT=587 | ||
| SMTP_SECURE=false | ||
| SMTP_USER=apikey | ||
| SMTP_PASSWORD=your-smtp-password | ||
| SMTP_FROM_ADDRESS=noreply@example.com | ||
| SMTP_FROM_NAME=Inkeep | ||
| ``` | ||
|
|
||
| ## Local Development with Mailpit | ||
|
|
||
| [Mailpit](https://mailpit.axigen.com/) captures outgoing email locally without sending it to real recipients. This is the recommended setup for development and testing. | ||
|
|
||
| Mailpit is included in the default Docker Compose configuration. Configure the SMTP environment variables to point to it: | ||
|
|
||
| <Steps> | ||
| <Step> | ||
| ### Configure environment variables | ||
|
|
||
| ```dotenv title=".env" | ||
| SMTP_HOST=localhost | ||
| SMTP_PORT=1025 | ||
| SMTP_SECURE=false | ||
| SMTP_FROM_ADDRESS=noreply@inkeep.local | ||
| SMTP_FROM_NAME=Inkeep | ||
| ``` | ||
| </Step> | ||
| <Step> | ||
| ### Start services and verify | ||
|
|
||
| ```bash | ||
| docker compose up -d | ||
| ``` | ||
|
|
||
| Open [http://localhost:8025](http://localhost:8025) to view the Mailpit inbox. Any emails sent by the platform (invitations, password resets) will appear here. | ||
| </Step> | ||
| </Steps> | ||
|
|
||
| ## What Happens Without Email | ||
|
|
||
| When email is not configured, the platform adjusts its behavior: | ||
|
|
||
| | Feature | With email | Without email | | ||
| |---------|-----------|---------------| | ||
| | **Team invitations** | Invitation email sent to the recipient | Invitation link shown directly in the UI | | ||
| | **Password reset** | Reset link emailed to the user | "Forgot password?" link hidden from the sign-in page | | ||
|
|
||
| No errors are thrown — the UI adapts automatically based on whether email is available. | ||
|
|
||
| ## Verifying Email Configuration | ||
|
|
||
| After configuring your environment variables, restart services and test: | ||
|
|
||
| 1. Go to **Settings** in the sidebar | ||
| 2. Click **Invite** and send a test invitation | ||
| 3. Verify the email arrives (in Mailpit for local dev, or the real inbox for production) | ||
|
|
||
| If using Mailpit, open [http://localhost:8025](http://localhost:8025) to see the captured email. | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| ### Emails not being sent | ||
|
|
||
| - Verify `SMTP_FROM_ADDRESS` is set — this is required for all transports | ||
| - Check the API server logs for `[email]` warnings about missing configuration | ||
| - For Resend: confirm your API key is valid and the from address domain is verified in Resend | ||
| - For generic SMTP: verify the host, port, and credentials are correct | ||
|
|
||
| ### Invitation link shown instead of email | ||
|
|
||
| This is expected behavior when email is not configured. Set up SMTP or Resend to enable email delivery. | ||
|
|
||
| ### "Password reset unavailable" message | ||
|
|
||
| The forgot-password page shows this when email is not configured. Configure email to enable self-service password resets. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Minor: Add internal route comment for consistency
Issue: This endpoint follows the internal route pattern (plain Hono routing, no OpenAPI schema) but lacks the comment that peer endpoints have.
Why: Other internal routes in this file (like
/pendingat line 125) have a comment indicating they're internal. This helps maintainers understand the route is not part of the public API contract.Fix:
Refs:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 37e4fe4.