feat(security): Session-Cookies statt X-User-Id und API-Härtung

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>
This commit is contained in:
2026-05-30 13:47:24 +02:00
parent 4f3f530f1f
commit dea33e3f00
33 changed files with 657 additions and 397 deletions
+11 -15
View File
@@ -1,19 +1,10 @@
import { Router } from 'express'
import { prisma } from '../db.js'
import { notifyOwnerOfCollaboratorChanges } from '../services/pushNotify.js'
import { requireUser } from '../middleware/auth.js'
const router = Router()
// Middleware to extract user ID from headers
const requireUser = (req: any, res: any, next: any) => {
const userId = req.headers['x-user-id']
if (!userId) {
return res.status(401).json({ error: 'Unauthorized: X-User-Id header missing' })
}
req.userId = userId
next()
}
router.use(requireUser)
// 1. Push local changes to the server
@@ -99,7 +90,7 @@ router.post('/push', async (req: any, res) => {
}
const isOwner = logbook.userId === req.userId
const isCollaborator = await prisma.collaboration.findUnique({
const collaboration = await prisma.collaboration.findUnique({
where: {
logbookId_userId: {
logbookId,
@@ -108,11 +99,16 @@ router.post('/push', async (req: any, res) => {
}
})
if (!isOwner && !isCollaborator) {
if (!isOwner && !collaboration) {
results.push({ payloadId, status: 'error', error: 'Forbidden: Access denied' })
continue
}
if (!isOwner && (!collaboration || collaboration.role !== 'WRITE')) {
results.push({ payloadId, status: 'error', error: 'Forbidden: WRITE access required' })
continue
}
if (type === 'logbook' && action === 'delete') {
if (!isOwner) {
results.push({ payloadId, status: 'error', error: 'Forbidden: Only owner can delete logbook' })
@@ -244,7 +240,7 @@ router.post('/push', async (req: any, res) => {
logbook.userId,
logbookId,
isOwner,
isCollaborator,
collaboration,
action,
type
)
@@ -284,7 +280,7 @@ router.get('/pull', async (req: any, res) => {
}
const isOwner = logbook.userId === req.userId
const isCollaborator = await prisma.collaboration.findUnique({
const collaboration = await prisma.collaboration.findUnique({
where: {
logbookId_userId: {
logbookId,
@@ -293,7 +289,7 @@ router.get('/pull', async (req: any, res) => {
}
})
if (!isOwner && !isCollaborator) {
if (!isOwner && !collaboration) {
return res.status(403).json({ error: 'Forbidden: Access denied' })
}