feat: Gitea Webhook, IMAP, Settings & Deployment docs
- Webhook route and Gitea integration - IMAP service and Nextcloud/Porkbun setup docs - Settings UI improvements and API updates - SSH/Webhook fix prompt for emailsorter.webklar.com - Bootstrap, config and AI sorter updates
This commit is contained in:
@@ -11,7 +11,7 @@ import { dirname, join } from 'path'
|
||||
|
||||
// Config & Middleware
|
||||
import { config, validateConfig } from './config/index.mjs'
|
||||
import { errorHandler, asyncHandler, NotFoundError, ValidationError } from './middleware/errorHandler.mjs'
|
||||
import { errorHandler, asyncHandler, NotFoundError, ValidationError, AuthorizationError } from './middleware/errorHandler.mjs'
|
||||
import { respond } from './utils/response.mjs'
|
||||
import { logger, log } from './middleware/logger.mjs'
|
||||
import { limiters } from './middleware/rateLimit.mjs'
|
||||
@@ -22,6 +22,7 @@ import emailRoutes from './routes/email.mjs'
|
||||
import stripeRoutes from './routes/stripe.mjs'
|
||||
import apiRoutes from './routes/api.mjs'
|
||||
import analyticsRoutes from './routes/analytics.mjs'
|
||||
import webhookRoutes from './routes/webhook.mjs'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
@@ -56,6 +57,11 @@ app.use('/api', limiters.api)
|
||||
// Static files
|
||||
app.use(express.static(join(__dirname, '..', 'public')))
|
||||
|
||||
// Gitea webhook: raw body for X-Gitea-Signature verification (must be before JSON parser)
|
||||
// Limit 2mb so large Gitea payloads (full repo JSON) don't get rejected and cause 502
|
||||
app.use('/api/webhook', express.raw({ type: 'application/json', limit: '2mb' }))
|
||||
app.use('/api/webhook', webhookRoutes)
|
||||
|
||||
// Body parsing (BEFORE routes, AFTER static)
|
||||
// Note: Stripe webhook needs raw body, handled in stripe routes
|
||||
app.use('/api', express.json({ limit: '1mb' }))
|
||||
@@ -84,6 +90,19 @@ app.use('/api', apiRoutes)
|
||||
|
||||
// Preferences endpoints (inline for simplicity)
|
||||
import { userPreferences } from './services/database.mjs'
|
||||
import { isAdmin } from './config/index.mjs'
|
||||
|
||||
/**
|
||||
* GET /api/me?email=xxx
|
||||
* Returns current user context (e.g. isAdmin) for the given email.
|
||||
*/
|
||||
app.get('/api/me', asyncHandler(async (req, res) => {
|
||||
const { email } = req.query
|
||||
if (!email || typeof email !== 'string') {
|
||||
throw new ValidationError('email is required')
|
||||
}
|
||||
respond.success(res, { isAdmin: isAdmin(email) })
|
||||
}))
|
||||
|
||||
app.get('/api/preferences', asyncHandler(async (req, res) => {
|
||||
const { userId } = req.query
|
||||
@@ -207,6 +226,69 @@ app.delete('/api/preferences/company-labels/:id', asyncHandler(async (req, res)
|
||||
respond.success(res, null, 'Company label deleted')
|
||||
}))
|
||||
|
||||
/**
|
||||
* GET /api/preferences/name-labels
|
||||
* Get name labels (worker labels). Admin only.
|
||||
*/
|
||||
app.get('/api/preferences/name-labels', asyncHandler(async (req, res) => {
|
||||
const { userId, email } = req.query
|
||||
if (!userId) throw new ValidationError('userId is required')
|
||||
if (!email || typeof email !== 'string') throw new ValidationError('email is required')
|
||||
if (!isAdmin(email)) throw new AuthorizationError('Admin access required for name labels')
|
||||
|
||||
const prefs = await userPreferences.getByUser(userId)
|
||||
const preferences = prefs?.preferences || userPreferences.getDefaults()
|
||||
respond.success(res, preferences.nameLabels || [])
|
||||
}))
|
||||
|
||||
/**
|
||||
* POST /api/preferences/name-labels
|
||||
* Save/Update name label (worker). Admin only.
|
||||
*/
|
||||
app.post('/api/preferences/name-labels', asyncHandler(async (req, res) => {
|
||||
const { userId, email, nameLabel } = req.body
|
||||
if (!userId) throw new ValidationError('userId is required')
|
||||
if (!email || typeof email !== 'string') throw new ValidationError('email is required')
|
||||
if (!isAdmin(email)) throw new AuthorizationError('Admin access required for name labels')
|
||||
if (!nameLabel) throw new ValidationError('nameLabel is required')
|
||||
|
||||
const prefs = await userPreferences.getByUser(userId)
|
||||
const preferences = prefs?.preferences || userPreferences.getDefaults()
|
||||
const nameLabels = preferences.nameLabels || []
|
||||
|
||||
if (!nameLabel.id) {
|
||||
nameLabel.id = `namelabel_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
const existingIndex = nameLabels.findIndex(l => l.id === nameLabel.id)
|
||||
if (existingIndex >= 0) {
|
||||
nameLabels[existingIndex] = nameLabel
|
||||
} else {
|
||||
nameLabels.push(nameLabel)
|
||||
}
|
||||
|
||||
await userPreferences.upsert(userId, { nameLabels })
|
||||
respond.success(res, nameLabel, 'Name label saved')
|
||||
}))
|
||||
|
||||
/**
|
||||
* DELETE /api/preferences/name-labels/:id
|
||||
* Delete name label. Admin only.
|
||||
*/
|
||||
app.delete('/api/preferences/name-labels/:id', asyncHandler(async (req, res) => {
|
||||
const { userId, email } = req.query
|
||||
const { id } = req.params
|
||||
if (!userId) throw new ValidationError('userId is required')
|
||||
if (!email || typeof email !== 'string') throw new ValidationError('email is required')
|
||||
if (!isAdmin(email)) throw new AuthorizationError('Admin access required for name labels')
|
||||
if (!id) throw new ValidationError('label id is required')
|
||||
|
||||
const prefs = await userPreferences.getByUser(userId)
|
||||
const preferences = prefs?.preferences || userPreferences.getDefaults()
|
||||
const nameLabels = (preferences.nameLabels || []).filter(l => l.id !== id)
|
||||
await userPreferences.upsert(userId, { nameLabels })
|
||||
respond.success(res, null, 'Name label deleted')
|
||||
}))
|
||||
|
||||
// Legacy Stripe webhook endpoint
|
||||
app.use('/stripe', stripeRoutes)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user