dea33e3f00
Ersetzt die spoofbare X-User-Id-Auth durch signierte HttpOnly-Sessions nach WebAuthn, erzwingt WRITE-only Sync, speichert den Master-Key nur im RAM und ergänzt CORS, Rate-Limits, Helmet sowie Passkey-Reauth für sensible Aktionen. Co-authored-by: Cursor <cursoragent@cursor.com>
132 lines
3.8 KiB
TypeScript
132 lines
3.8 KiB
TypeScript
import { Router } from 'express'
|
|
import { prisma } from '../db.js'
|
|
import { requireUser } from '../middleware/auth.js'
|
|
|
|
const router = Router()
|
|
|
|
function isValidHttpsEndpoint(endpoint: unknown): endpoint is string {
|
|
if (typeof endpoint !== 'string' || endpoint.length > 2048) return false
|
|
try {
|
|
const url = new URL(endpoint)
|
|
return url.protocol === 'https:'
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
router.get('/vapid-public-key', (_req, res) => {
|
|
const publicKey = process.env.VAPID_PUBLIC_KEY
|
|
if (!publicKey) {
|
|
return res.status(503).json({ error: 'Push notifications are not configured on this server' })
|
|
}
|
|
return res.json({ publicKey })
|
|
})
|
|
|
|
router.use(requireUser)
|
|
|
|
router.get('/prefs', async (req: any, res) => {
|
|
try {
|
|
const prefs = await prisma.userNotificationPrefs.findUnique({
|
|
where: { userId: req.userId }
|
|
})
|
|
return res.json({
|
|
collaboratorChangesEnabled: prefs?.collaboratorChangesEnabled ?? false
|
|
})
|
|
} catch (error: any) {
|
|
console.error('Error reading push prefs:', error)
|
|
return res.status(500).json({ error: error.message || 'Internal server error' })
|
|
}
|
|
})
|
|
|
|
router.put('/prefs', async (req: any, res) => {
|
|
try {
|
|
const { collaboratorChangesEnabled } = req.body
|
|
if (typeof collaboratorChangesEnabled !== 'boolean') {
|
|
return res.status(400).json({ error: 'collaboratorChangesEnabled must be a boolean' })
|
|
}
|
|
|
|
const prefs = await prisma.userNotificationPrefs.upsert({
|
|
where: { userId: req.userId },
|
|
create: {
|
|
userId: req.userId,
|
|
collaboratorChangesEnabled,
|
|
updatedAt: new Date()
|
|
},
|
|
update: {
|
|
collaboratorChangesEnabled,
|
|
updatedAt: new Date()
|
|
}
|
|
})
|
|
|
|
return res.json({
|
|
collaboratorChangesEnabled: prefs.collaboratorChangesEnabled
|
|
})
|
|
} catch (error: any) {
|
|
console.error('Error updating push prefs:', error)
|
|
return res.status(500).json({ error: error.message || 'Internal server error' })
|
|
}
|
|
})
|
|
|
|
router.put('/subscription', async (req: any, res) => {
|
|
try {
|
|
const { endpoint, keys, locale, userAgent } = req.body
|
|
if (!isValidHttpsEndpoint(endpoint)) {
|
|
return res.status(400).json({ error: 'Invalid push subscription endpoint' })
|
|
}
|
|
if (!keys?.p256dh || !keys?.auth || typeof keys.p256dh !== 'string' || typeof keys.auth !== 'string') {
|
|
return res.status(400).json({ error: 'Invalid subscription keys' })
|
|
}
|
|
|
|
const normalizedLocale =
|
|
typeof locale === 'string' && (locale === 'de' || locale === 'en') ? locale : null
|
|
|
|
await prisma.pushSubscription.upsert({
|
|
where: { endpoint },
|
|
create: {
|
|
userId: req.userId,
|
|
endpoint,
|
|
p256dh: keys.p256dh,
|
|
auth: keys.auth,
|
|
locale: normalizedLocale,
|
|
userAgent: typeof userAgent === 'string' ? userAgent.slice(0, 512) : null
|
|
},
|
|
update: {
|
|
userId: req.userId,
|
|
p256dh: keys.p256dh,
|
|
auth: keys.auth,
|
|
locale: normalizedLocale,
|
|
userAgent: typeof userAgent === 'string' ? userAgent.slice(0, 512) : null,
|
|
updatedAt: new Date()
|
|
}
|
|
})
|
|
|
|
return res.json({ success: true })
|
|
} catch (error: any) {
|
|
console.error('Error saving push subscription:', error)
|
|
return res.status(500).json({ error: error.message || 'Internal server error' })
|
|
}
|
|
})
|
|
|
|
router.delete('/subscription', async (req: any, res) => {
|
|
try {
|
|
const { endpoint } = req.body
|
|
if (!isValidHttpsEndpoint(endpoint)) {
|
|
return res.status(400).json({ error: 'Invalid push subscription endpoint' })
|
|
}
|
|
|
|
await prisma.pushSubscription.deleteMany({
|
|
where: {
|
|
endpoint,
|
|
userId: req.userId
|
|
}
|
|
})
|
|
|
|
return res.json({ success: true })
|
|
} catch (error: any) {
|
|
console.error('Error deleting push subscription:', error)
|
|
return res.status(500).json({ error: error.message || 'Internal server error' })
|
|
}
|
|
})
|
|
|
|
export default router
|