fix: CORS-Origins, Sync-Body-Limit und geteilte Logbuch-Rolle
Erlaubt mehrere/normalisierte CORS-Origins mit Dev-Fallbacks für Session-Cookies, stellt express.json wieder auf 50mb für große Sync-Payloads und setzt die Zugriffsrolle beim Wechsel in geteilte Logbücher ohne Cache korrekt. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+3
-1
@@ -4,8 +4,10 @@ OpenWeatherMapAPIKey=<owm_api_key>
|
|||||||
# For local dev: localhost and http://localhost
|
# For local dev: localhost and http://localhost
|
||||||
# For production: e.g. kapteins-daagbok.eu and https://kapteins-daagbok.eu
|
# For production: e.g. kapteins-daagbok.eu and https://kapteins-daagbok.eu
|
||||||
RP_ID=localhost
|
RP_ID=localhost
|
||||||
# Must match the frontend URL (Vite dev: http://localhost:5173)
|
# Must match the frontend URL (Vite dev: http://localhost:5173; Docker: http://localhost)
|
||||||
ORIGIN=http://localhost:5173
|
ORIGIN=http://localhost:5173
|
||||||
|
# Optional: comma-separated CORS origins (defaults to ORIGIN; dev also allows 127.0.0.1:5173)
|
||||||
|
# CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
||||||
|
|
||||||
# API session signing (min. 32 chars; required in production)
|
# API session signing (min. 32 chars; required in production)
|
||||||
# Generate: openssl rand -base64 48
|
# Generate: openssl rand -base64 48
|
||||||
|
|||||||
+9
-5
@@ -40,19 +40,20 @@ import {
|
|||||||
getStoredDemoFirstEntryId,
|
getStoredDemoFirstEntryId,
|
||||||
seedDemoLogbookIfNeeded
|
seedDemoLogbookIfNeeded
|
||||||
} from './services/demoLogbook.js'
|
} from './services/demoLogbook.js'
|
||||||
import { fetchLogbooks } from './services/logbook.js'
|
import { fetchLogbooks, parseCollaborationRole } from './services/logbook.js'
|
||||||
import { ensurePushSubscriptionIfEnabled } from './services/pushNotifications.js'
|
import { ensurePushSubscriptionIfEnabled } from './services/pushNotifications.js'
|
||||||
|
|
||||||
const PENDING_PUSH_LOGBOOK_KEY = 'pending_push_logbook_id'
|
const PENDING_PUSH_LOGBOOK_KEY = 'pending_push_logbook_id'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { t, i18n } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
const { registerNavigation, requestStartAfterLogin } = useAppTour()
|
const { registerNavigation, requestStartAfterLogin, isActive, currentStepId } = useAppTour()
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||||
const [activeLogbookId, setActiveLogbookId] = useState<string | null>(null)
|
const [activeLogbookId, setActiveLogbookId] = useState<string | null>(null)
|
||||||
const [activeLogbookTitle, setActiveLogbookTitle] = useState<string | null>(null)
|
const [activeLogbookTitle, setActiveLogbookTitle] = useState<string | null>(null)
|
||||||
const [activeTab, setActiveTab] = useState<AppTab>('logs')
|
const [activeTab, setActiveTab] = useState<AppTab>('logs')
|
||||||
const [tourSelectedEntryId, setTourSelectedEntryId] = useState<string | null>(null)
|
const [tourSelectedEntryId, setTourSelectedEntryId] = useState<string | null>(null)
|
||||||
|
const [tourFeedbackOpen, setTourFeedbackOpen] = useState(false)
|
||||||
const [demoHighlightEntryId, setDemoHighlightEntryId] = useState<string | null>(null)
|
const [demoHighlightEntryId, setDemoHighlightEntryId] = useState<string | null>(null)
|
||||||
const [online, setOnline] = useState(navigator.onLine)
|
const [online, setOnline] = useState(navigator.onLine)
|
||||||
const [isSyncing, setIsSyncing] = useState(false)
|
const [isSyncing, setIsSyncing] = useState(false)
|
||||||
@@ -90,9 +91,11 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cachedRole = activeLogbookRecord.collaborationRole
|
const cachedRole = activeLogbookRecord.collaborationRole
|
||||||
if (cachedRole) {
|
setActiveAccessRole(
|
||||||
setActiveAccessRole(cachedRole)
|
cachedRole
|
||||||
}
|
? parseCollaborationRole(cachedRole, `logbook ${activeLogbookId}`)
|
||||||
|
: 'WRITE'
|
||||||
|
)
|
||||||
|
|
||||||
getLogbookAccess(activeLogbookId).then((access) => {
|
getLogbookAccess(activeLogbookId).then((access) => {
|
||||||
if (access) setActiveAccessRole(access.role)
|
if (access) setActiveAccessRole(access.role)
|
||||||
@@ -503,6 +506,7 @@ function App() {
|
|||||||
<button
|
<button
|
||||||
className={`sidebar-btn ${activeTab === 'stats' ? 'active' : ''}`}
|
className={`sidebar-btn ${activeTab === 'stats' ? 'active' : ''}`}
|
||||||
onClick={() => setActiveTab('stats')}
|
onClick={() => setActiveTab('stats')}
|
||||||
|
data-tour="nav-stats"
|
||||||
>
|
>
|
||||||
<BarChart2 size={18} />
|
<BarChart2 size={18} />
|
||||||
{t('nav.stats')}
|
{t('nav.stats')}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import type { CorsOptions } from 'cors'
|
||||||
|
|
||||||
|
function normalizeOrigin(origin: string): string {
|
||||||
|
return origin.trim().replace(/\/$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Origins allowed for credentialed CORS (must match the browser frontend URL exactly). */
|
||||||
|
export function getAllowedCorsOrigins(): Set<string> {
|
||||||
|
const raw =
|
||||||
|
process.env.CORS_ORIGINS?.trim() ||
|
||||||
|
process.env.ORIGIN?.trim() ||
|
||||||
|
'http://localhost:5173'
|
||||||
|
|
||||||
|
const origins = raw
|
||||||
|
.split(',')
|
||||||
|
.map(normalizeOrigin)
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
const allowed = new Set(origins)
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
for (const dev of [
|
||||||
|
'http://localhost:5173',
|
||||||
|
'http://127.0.0.1:5173',
|
||||||
|
'http://localhost:4173'
|
||||||
|
]) {
|
||||||
|
allowed.add(dev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCorsOptions(): CorsOptions {
|
||||||
|
const allowed = getAllowedCorsOrigins()
|
||||||
|
|
||||||
|
return {
|
||||||
|
origin(origin, callback) {
|
||||||
|
// Non-browser clients, same-origin via reverse proxy (no Origin header)
|
||||||
|
if (!origin) {
|
||||||
|
callback(null, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = normalizeOrigin(origin)
|
||||||
|
if (allowed.has(normalized)) {
|
||||||
|
callback(null, normalized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
`[cors] Rejected origin "${origin}". Allowed: ${[...allowed].join(', ')}`
|
||||||
|
)
|
||||||
|
callback(new Error('Not allowed by CORS'))
|
||||||
|
},
|
||||||
|
credentials: true
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-9
@@ -15,6 +15,7 @@ import pushRouter from './routes/push.js'
|
|||||||
import weatherRouter from './routes/weather.js'
|
import weatherRouter from './routes/weather.js'
|
||||||
import feedbackRouter from './routes/feedback.js'
|
import feedbackRouter from './routes/feedback.js'
|
||||||
import { prisma } from './db.js'
|
import { prisma } from './db.js'
|
||||||
|
import { buildCorsOptions } from './cors.js'
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
@@ -24,22 +25,16 @@ dotenv.config({ path: resolve(__dirname, '../.env') })
|
|||||||
const app = express()
|
const app = express()
|
||||||
const PORT = process.env.PORT || 5000
|
const PORT = process.env.PORT || 5000
|
||||||
|
|
||||||
const allowedOrigin = process.env.ORIGIN || 'http://localhost:5173'
|
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
helmet({
|
helmet({
|
||||||
contentSecurityPolicy: false,
|
contentSecurityPolicy: false,
|
||||||
crossOriginEmbedderPolicy: false
|
crossOriginEmbedderPolicy: false
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
app.use(
|
app.use(cors(buildCorsOptions()))
|
||||||
cors({
|
|
||||||
origin: allowedOrigin,
|
|
||||||
credentials: true
|
|
||||||
})
|
|
||||||
)
|
|
||||||
app.use(cookieParser())
|
app.use(cookieParser())
|
||||||
app.use(express.json({ limit: '10mb' }))
|
// Encrypted sync payloads (photos, GPS tracks) can be large — align with nginx client_max_body_size
|
||||||
|
app.use(express.json({ limit: '50mb' }))
|
||||||
|
|
||||||
const authLimiter = rateLimit({
|
const authLimiter = rateLimit({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
|
|||||||
Reference in New Issue
Block a user