Skip to content

Commit 225a0bd

Browse files
committed
feat: add email notifications for new document creation
1 parent a73c094 commit 225a0bd

File tree

17 files changed

+579
-112
lines changed

17 files changed

+579
-112
lines changed

.env.example

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,31 @@ WEBAPP_REPLICAS=2
231231
BUILD_ID=development-build
232232
GIT_HASH=dev
233233

234+
# =============================================================================
235+
# EMAIL - Notification Service (SMTP via Nodemailer)- optional and will be removed soon
236+
# =============================================================================
237+
# Sends email notifications when new documents are created.
238+
# Requires: bun add nodemailer
239+
240+
# Common settings
241+
242+
NEW_DOCUMENT_NOTIFICATION_EMAILS=[email protected],[email protected]
243+
APP_URL=https://yourdomain.com
244+
EMAIL_LOG_LEVEL=
245+
246+
# SMTP Configuration
247+
# Gmail: Use App Password (not your regular password)
248+
# Setup guide: https://support.google.com/accounts/answer/185833
249+
# 1. Enable 2FA on Gmail
250+
# 2. Go to: https://myaccount.google.com/apppasswords
251+
# 3. Generate app password for "Mail"
252+
# 4. Use that 16-char password below
253+
SMTP_HOST=smtp.gmail.com
254+
SMTP_PORT=587
255+
256+
SMTP_PASS=your-16-char-app-password
257+
SMTP_SECURE=false
258+
234259
# =============================================================================
235260
# ANALYTICS & EXTERNAL SERVICES
236261
# =============================================================================

bun.lock

Lines changed: 165 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/hocuspocus.server/docker/config-templates/env.development

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,27 @@ STORAGE_S3_LOG_LEVEL=
164164
# Security & Auth
165165
JWT_LOG_LEVEL=
166166

167+
# Email
168+
EMAIL_LOG_LEVEL=debug
169+
170+
# ============================================
171+
# Email Notifications (SMTP via Nodemailer)
172+
# ============================================
173+
174+
# Common settings
175+
EMAIL_FROM=dev@localhost
176+
NEW_DOCUMENT_NOTIFICATION_EMAILS=
177+
APP_URL=http://localhost:3000
178+
179+
# SMTP Configuration
180+
# For local testing, use Mailpit: docker run -p 1025:1025 -p 8025:8025 axllent/mailpit
181+
# View emails at: http://localhost:8025
182+
SMTP_HOST=host.docker.internal
183+
SMTP_PORT=1025
184+
SMTP_USER=
185+
SMTP_PASS=
186+
SMTP_SECURE=false
187+
167188
# ============================================
168189
# Development Settings
169190
# ============================================

packages/hocuspocus.server/docker/config-templates/env.production

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,24 @@ STORAGE_S3_LOG_LEVEL=
176176
# Security & Auth
177177
JWT_LOG_LEVEL=
178178

179+
# Email
180+
EMAIL_LOG_LEVEL=
181+
182+
# ============================================
183+
# Email Notifications (SMTP via Nodemailer)
184+
# ============================================
185+
186+
# Common settings
187+
188+
189+
APP_URL=https://yourdomain.com
190+
191+
# SMTP Configuration
192+
# Gmail: Use App Password (not your regular password)
193+
# Setup: https://support.google.com/accounts/answer/185833
194+
SMTP_HOST=smtp.gmail.com
195+
SMTP_PORT=587
196+
197+
SMTP_PASS=your-16-char-app-password
198+
SMTP_SECURE=false
199+

packages/hocuspocus.server/docker/config-templates/env.staging

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,22 @@ STORAGE_S3_LOG_LEVEL=
160160
# Security & Auth
161161
JWT_LOG_LEVEL=
162162

163+
# Email
164+
EMAIL_LOG_LEVEL=
165+
166+
# ============================================
167+
# Email Notifications (SMTP via Nodemailer)
168+
# ============================================
169+
170+
# Common settings
171+
172+
173+
APP_URL=https://staging.yourdomain.com
174+
175+
# SMTP Configuration
176+
SMTP_HOST=smtp.example.com
177+
SMTP_PORT=587
178+
179+
SMTP_PASS=password
180+
SMTP_SECURE=false
181+

packages/hocuspocus.server/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"type": "module",
2626
"devDependencies": {
2727
"@types/jsonwebtoken": "^9.0.10",
28+
"@types/nodemailer": "^6.4.14",
2829
"@types/pg": "^8.15.6",
2930
"bun-types": "latest",
3031
"prisma": "^6.19.0"
@@ -51,6 +52,7 @@
5152
"ioredis": "^5.8.2",
5253
"jsonwebtoken": "^9.0.2",
5354
"mime": "^4.1.0",
55+
"nodemailer": "^6.9.15",
5456
"pg": "^8.16.3",
5557
"pino": "^10.1.0",
5658
"pino-http": "^11.0.0",

packages/hocuspocus.server/src/api/controllers/documents.controller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { PrismaClient } from '@prisma/client'
2-
import { extractUserFromToken } from '../../utils'
2+
import { extractUserFromToken } from '../utils/auth'
33
import * as documentsService from '../services/documents.service'
44
import * as mediaService from '../services/media.service'
55
import { documentsControllerLogger } from '../../lib/logger'
@@ -11,7 +11,7 @@ export const getDocumentBySlug = async (c: any) => {
1111
const { userId } = c.req.valid('query')
1212
const token = c.req.header('token')
1313

14-
const user = extractUserFromToken(token, userId)
14+
const user = await extractUserFromToken(token, userId)
1515

1616
try {
1717
const doc = await documentsService.getDocumentBySlug(prisma, docName)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { verifySupabaseToken } from '../../utils/jwt'
2+
3+
/**
4+
* Extract user from token for REST API controllers
5+
* Used when token and userId are provided separately
6+
*/
7+
export const extractUserFromToken = async (
8+
token?: string,
9+
userId?: string
10+
): Promise<{ sub: string; email?: string; user_metadata?: any } | null> => {
11+
if (!token || !userId) return null
12+
13+
const user = await verifySupabaseToken(token)
14+
15+
if (!user || user.sub !== userId) {
16+
return null
17+
}
18+
19+
return user
20+
}

packages/hocuspocus.server/src/config/env.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,23 @@ export const config = {
7171
.map((origin) => origin.trim())
7272
.filter(Boolean),
7373
rateLimitMax: parseInt(process.env.RATE_LIMIT_MAX || '100', 10)
74+
},
75+
76+
// Email (SMTP via Nodemailer)
77+
email: {
78+
fromEmail: process.env.EMAIL_FROM || process.env.SMTP_USER || '[email protected]',
79+
notificationEmails: (process.env.NEW_DOCUMENT_NOTIFICATION_EMAILS || '')
80+
.split(',')
81+
.map((email) => email.trim())
82+
.filter((email) => email.length > 0 && email.includes('@')),
83+
appUrl: process.env.APP_URL || 'https://docs.plus',
84+
smtp: {
85+
host: process.env.SMTP_HOST || '',
86+
port: parseInt(process.env.SMTP_PORT || '587', 10),
87+
secure: process.env.SMTP_SECURE === 'true',
88+
user: process.env.SMTP_USER || '',
89+
pass: process.env.SMTP_PASS || ''
90+
}
7491
}
7592
} as const
7693

packages/hocuspocus.server/src/config/hocuspocus.config.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,31 +90,30 @@ const configureExtensions = () => {
9090
const ydoc = new Y.Doc()
9191
Y.applyUpdate(ydoc, state)
9292
const meta = ydoc.getMap('metadata')
93-
const commitMessage = meta.get('commitMessage') || ''
93+
const commitMessageValue = meta.get('commitMessage')
94+
const commitMessage = typeof commitMessageValue === 'string' ? commitMessageValue : ''
9495
const isDraft = meta.get('isDraft') || false
95-
let firstCreation = false
9696

9797
meta.delete('commitMessage')
9898

9999
// If the document is draft, don't store the data
100100
if (isDraft) return
101101

102+
// Clean up draft flag after first save
102103
if (meta.has('isDraft')) {
103104
meta.delete('isDraft')
104-
firstCreation = true
105105
}
106106

107107
// Create a new Y.Doc to store the updated state
108108
Y.applyUpdate(ydoc, state)
109109
const newState = Y.encodeStateAsUpdate(ydoc)
110110

111-
// Add job to queue
111+
// Add job to queue (firstCreation is determined in worker by checking DB)
112112
await StoreDocumentQueue.add('store-document', {
113113
documentName,
114114
state: Buffer.from(newState).toString('base64'),
115115
context,
116-
commitMessage,
117-
firstCreation
116+
commitMessage
118117
})
119118
}
120119
})

0 commit comments

Comments
 (0)