Compare commits
12 Commits
v0.1.0.98
...
v0.1.0.104
| Author | SHA1 | Date | |
|---|---|---|---|
| 85e641ed39 | |||
| 9bf59280b2 | |||
| aee8f4f3db | |||
| 2b029a26f0 | |||
| 2156aa4bbd | |||
| 5eb4543255 | |||
| fb9bb6754c | |||
| 959afd5a63 | |||
| e3ea45f717 | |||
| 8f57b6ff22 | |||
| 60e1b714b7 | |||
| 1e203bfec1 |
+2
-2
@@ -13,8 +13,8 @@ RP_ID=localhost
|
|||||||
# Must match the frontend URL exactly (Vite dev: http://localhost:5173; Docker: http://localhost)
|
# Must match the frontend URL exactly (Vite dev: http://localhost:5173; Docker: http://localhost)
|
||||||
ORIGIN=http://localhost:5173
|
ORIGIN=http://localhost:5173
|
||||||
|
|
||||||
# Behind Nginx Proxy Manager — see docs/deployment/npm-security.md
|
# Behind reverse proxy — see docs/deployment/npm-security.md
|
||||||
# TRUST_PROXY=172.16.10.10
|
# Docker Compose (NPM → frontend nginx → backend): TRUST_PROXY=1
|
||||||
# TRUST_PROXY=1
|
# TRUST_PROXY=1
|
||||||
|
|
||||||
# Docker Compose database (required for production deploy)
|
# Docker Compose database (required for production deploy)
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ server {
|
|||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection 'upgrade';
|
proxy_set_header Connection 'upgrade';
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_cache_bypass $http_upgrade;
|
proxy_cache_bypass $http_upgrade;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-16
@@ -148,7 +148,8 @@ select.input-text {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-input-24h__select {
|
.time-input-24h__select,
|
||||||
|
.time-input-24h__native {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
@@ -157,6 +158,11 @@ select.input-text {
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type='time'].time-input-24h__native {
|
||||||
|
color-scheme: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.time-input-24h__sep {
|
.time-input-24h__sep {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@@ -2647,27 +2653,13 @@ html.scheme-dark .themed-select-option.is-selected {
|
|||||||
.events-scroll-container {
|
.events-scroll-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
background: rgba(11, 12, 16, 0.4);
|
background: rgba(11, 12, 16, 0.4);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Scrollbar for events container */
|
|
||||||
.events-scroll-container::-webkit-scrollbar {
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
.events-scroll-container::-webkit-scrollbar-track {
|
|
||||||
background: rgba(11, 12, 16, 0.2);
|
|
||||||
}
|
|
||||||
.events-scroll-container::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
.events-scroll-container::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.events-table {
|
.events-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useId, useMemo } from 'react'
|
import { useId, useMemo } from 'react'
|
||||||
import { joinTimeHHMM, splitTimeHHMM } from '../utils/logEntryPayload.js'
|
import { joinTimeHHMM, splitTimeHHMM } from '../utils/logEntryPayload.js'
|
||||||
|
import { preferNativeCameraPicker } from '../utils/captureVideoFrame.js'
|
||||||
|
|
||||||
const HOURS = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'))
|
const HOURS = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'))
|
||||||
const MINUTES = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0'))
|
const MINUTES = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0'))
|
||||||
@@ -18,7 +19,29 @@ export default function EventTimeInput24h({
|
|||||||
'aria-label': ariaLabel
|
'aria-label': ariaLabel
|
||||||
}: EventTimeInput24hProps) {
|
}: EventTimeInput24hProps) {
|
||||||
const baseId = useId()
|
const baseId = useId()
|
||||||
|
const useNativePicker = preferNativeCameraPicker()
|
||||||
const { hours, minutes } = useMemo(() => splitTimeHHMM(value), [value])
|
const { hours, minutes } = useMemo(() => splitTimeHHMM(value), [value])
|
||||||
|
const timeValue = useMemo(() => joinTimeHHMM(hours, minutes), [hours, minutes])
|
||||||
|
|
||||||
|
if (useNativePicker) {
|
||||||
|
return (
|
||||||
|
<div className="time-input-24h">
|
||||||
|
<input
|
||||||
|
id={baseId}
|
||||||
|
type="time"
|
||||||
|
step={60}
|
||||||
|
className="input-text time-input-24h__native"
|
||||||
|
value={timeValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
const next = e.target.value
|
||||||
|
if (next) onChange(next.slice(0, 5))
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="time-input-24h">
|
<div className="time-input-24h">
|
||||||
|
|||||||
@@ -18,3 +18,62 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Scrollbars — auf Touch-Geräten breiter und besser sichtbar */
|
||||||
|
:root {
|
||||||
|
--app-scrollbar-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: none), (pointer: coarse), (max-width: 768px) {
|
||||||
|
:root {
|
||||||
|
--app-scrollbar-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scrollbar-width: auto;
|
||||||
|
scrollbar-color: var(--app-accent-light) var(--app-surface-inset);
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
html::-webkit-scrollbar,
|
||||||
|
body::-webkit-scrollbar,
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: var(--app-scrollbar-size);
|
||||||
|
height: var(--app-scrollbar-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
html::-webkit-scrollbar-track,
|
||||||
|
body::-webkit-scrollbar-track,
|
||||||
|
*::-webkit-scrollbar-track {
|
||||||
|
background: var(--app-surface-inset);
|
||||||
|
border-radius: calc(var(--app-scrollbar-size) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
html::-webkit-scrollbar-thumb,
|
||||||
|
body::-webkit-scrollbar-thumb,
|
||||||
|
*::-webkit-scrollbar-thumb {
|
||||||
|
background: color-mix(in srgb, var(--app-accent-light) 55%, transparent);
|
||||||
|
border-radius: calc(var(--app-scrollbar-size) / 2);
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html::-webkit-scrollbar-thumb:hover,
|
||||||
|
body::-webkit-scrollbar-thumb:hover,
|
||||||
|
*::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: color-mix(in srgb, var(--app-accent-light) 80%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: none), (pointer: coarse), (max-width: 768px) {
|
||||||
|
html::-webkit-scrollbar-thumb,
|
||||||
|
body::-webkit-scrollbar-thumb,
|
||||||
|
*::-webkit-scrollbar-thumb {
|
||||||
|
background: color-mix(in srgb, var(--app-accent-light) 70%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
html::-webkit-scrollbar-thumb:active,
|
||||||
|
body::-webkit-scrollbar-thumb:active,
|
||||||
|
*::-webkit-scrollbar-thumb:active {
|
||||||
|
background: var(--app-accent-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
reconcileVersionOnStartup
|
reconcileVersionOnStartup
|
||||||
} from './services/pwaStartup.ts'
|
} from './services/pwaStartup.ts'
|
||||||
import { redirectToPasskeyCompatibleHostIfNeeded } from './utils/passkeyHost.ts'
|
import { redirectToPasskeyCompatibleHostIfNeeded } from './utils/passkeyHost.ts'
|
||||||
import { logToBackend } from './services/pushNotifications.ts'
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -75,16 +74,13 @@ async function bootstrap(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ('serviceWorker' in navigator && !import.meta.env.DEV) {
|
if ('serviceWorker' in navigator && !import.meta.env.DEV) {
|
||||||
logToBackend('Attempting manual Service Worker registration...')
|
|
||||||
navigator.serviceWorker
|
navigator.serviceWorker
|
||||||
.register('/sw.js', { scope: '/' })
|
.register('/sw.js', { scope: '/' })
|
||||||
.then((reg) => {
|
.then((reg) => {
|
||||||
console.log('Service Worker registered successfully with scope:', reg.scope)
|
console.log('Service Worker registered successfully with scope:', reg.scope)
|
||||||
logToBackend('Service Worker registered successfully with scope: ' + reg.scope)
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('Service Worker registration failed:', err)
|
console.error('Service Worker registration failed:', err)
|
||||||
logToBackend('Service Worker registration failed', err)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,29 +2,6 @@ import { apiFetch, apiJson } from './api.js'
|
|||||||
|
|
||||||
const API_BASE = '/api/push'
|
const API_BASE = '/api/push'
|
||||||
|
|
||||||
export async function logToBackend(message: string, error?: any): Promise<void> {
|
|
||||||
try {
|
|
||||||
await fetch(`${API_BASE}/debug-log`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
message,
|
|
||||||
error: error ? {
|
|
||||||
name: error.name,
|
|
||||||
message: error.message,
|
|
||||||
stack: error.stack,
|
|
||||||
...error
|
|
||||||
} : undefined,
|
|
||||||
userAgent: navigator.userAgent,
|
|
||||||
href: window.location.href,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('Failed to send debug log:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||||
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
|
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
|
||||||
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
|
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
|
||||||
@@ -212,35 +189,23 @@ async function saveSubscriptionToServer(subscription: PushSubscription): Promise
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function subscribeToPush(): Promise<void> {
|
export async function subscribeToPush(): Promise<void> {
|
||||||
logToBackend('subscribeToPush called')
|
|
||||||
if (!isPushSupported()) {
|
if (!isPushSupported()) {
|
||||||
logToBackend('subscribeToPush: push not supported')
|
|
||||||
throw new Error('Push notifications are not supported on this device')
|
throw new Error('Push notifications are not supported on this device')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-resolve registration using getRegistrationCompat to prevent ready state hangs
|
// Pre-resolve registration using getRegistrationCompat to prevent ready state hangs
|
||||||
let registration = cachedRegistration
|
let registration = cachedRegistration
|
||||||
if (!registration) {
|
if (!registration) {
|
||||||
try {
|
registration = await getRegistrationCompat()
|
||||||
logToBackend('subscribeToPush: getting registration...')
|
cachedRegistration = registration
|
||||||
registration = await getRegistrationCompat()
|
|
||||||
cachedRegistration = registration
|
|
||||||
logToBackend('subscribeToPush: got registration successfully')
|
|
||||||
} catch (err) {
|
|
||||||
logToBackend('subscribeToPush: failed to get registration', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicKey = cachedVapidKey || await fetchVapidPublicKey()
|
const publicKey = cachedVapidKey || await fetchVapidPublicKey()
|
||||||
if (!publicKey) {
|
if (!publicKey) {
|
||||||
logToBackend('subscribeToPush: no public key available')
|
|
||||||
throw new Error('Push notifications are not configured on this server')
|
throw new Error('Push notifications are not configured on this server')
|
||||||
}
|
}
|
||||||
|
|
||||||
logToBackend('subscribeToPush: requesting permission...')
|
|
||||||
const permission = await requestNotificationPermission()
|
const permission = await requestNotificationPermission()
|
||||||
logToBackend(`subscribeToPush: permission result: ${permission}`)
|
|
||||||
if (permission !== 'granted') {
|
if (permission !== 'granted') {
|
||||||
throw new Error('Notification permission denied')
|
throw new Error('Notification permission denied')
|
||||||
}
|
}
|
||||||
@@ -249,7 +214,6 @@ export async function subscribeToPush(): Promise<void> {
|
|||||||
const applicationServerKey = new Uint8Array(keyBytes)
|
const applicationServerKey = new Uint8Array(keyBytes)
|
||||||
|
|
||||||
// Always call subscribe with timeout to prevent silent hangs on push network errors
|
// Always call subscribe with timeout to prevent silent hangs on push network errors
|
||||||
logToBackend('subscribeToPush: subscribing via pushManager...')
|
|
||||||
const subscribePromise = registration.pushManager.subscribe({
|
const subscribePromise = registration.pushManager.subscribe({
|
||||||
userVisibleOnly: true,
|
userVisibleOnly: true,
|
||||||
applicationServerKey
|
applicationServerKey
|
||||||
@@ -257,15 +221,9 @@ export async function subscribeToPush(): Promise<void> {
|
|||||||
const subscribeTimeout = new Promise<never>((_, reject) =>
|
const subscribeTimeout = new Promise<never>((_, reject) =>
|
||||||
setTimeout(() => reject(new Error('Timeout establishing subscription with push service (FCM/APNs)')), 12000)
|
setTimeout(() => reject(new Error('Timeout establishing subscription with push service (FCM/APNs)')), 12000)
|
||||||
)
|
)
|
||||||
try {
|
const subscription = await Promise.race([subscribePromise, subscribeTimeout])
|
||||||
const subscription = await Promise.race([subscribePromise, subscribeTimeout])
|
|
||||||
logToBackend('subscribeToPush: subscribed successfully, saving to server...')
|
await saveSubscriptionToServer(subscription)
|
||||||
await saveSubscriptionToServer(subscription)
|
|
||||||
logToBackend('subscribeToPush: saved to server successfully')
|
|
||||||
} catch (err) {
|
|
||||||
logToBackend('subscribeToPush: subscription or save failed', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unsubscribeFromPush(): Promise<void> {
|
export async function unsubscribeFromPush(): Promise<void> {
|
||||||
|
|||||||
+4
-4
@@ -6,6 +6,10 @@ import { NetworkFirst, NetworkOnly } from 'workbox-strategies'
|
|||||||
|
|
||||||
declare let self: ServiceWorkerGlobalScope
|
declare let self: ServiceWorkerGlobalScope
|
||||||
|
|
||||||
|
precacheAndRoute(self.__WB_MANIFEST)
|
||||||
|
cleanupOutdatedCaches()
|
||||||
|
clientsClaim()
|
||||||
|
|
||||||
const appShellFallback = createHandlerBoundToURL('/index.html')
|
const appShellFallback = createHandlerBoundToURL('/index.html')
|
||||||
const navigationStrategy = new NetworkFirst({
|
const navigationStrategy = new NetworkFirst({
|
||||||
cacheName: 'app-shell',
|
cacheName: 'app-shell',
|
||||||
@@ -20,10 +24,6 @@ registerRoute(({ request }) => request.mode === 'navigate', async (context) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
precacheAndRoute(self.__WB_MANIFEST)
|
|
||||||
cleanupOutdatedCaches()
|
|
||||||
clientsClaim()
|
|
||||||
|
|
||||||
// Always fetch the live deploy version, even under an older precache.
|
// Always fetch the live deploy version, even under an older precache.
|
||||||
registerRoute(({ url }) => url.pathname === '/version.json', new NetworkOnly())
|
registerRoute(({ url }) => url.pathname === '/version.json', new NetworkOnly())
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ proxy_set_header X-Real-IP $remote_addr;
|
|||||||
ORIGIN=https://kapteins-daagbok.eu
|
ORIGIN=https://kapteins-daagbok.eu
|
||||||
RP_ID=kapteins-daagbok.eu
|
RP_ID=kapteins-daagbok.eu
|
||||||
SESSION_SECRET=<min. 32 Zeichen, openssl rand -base64 48>
|
SESSION_SECRET=<min. 32 Zeichen, openssl rand -base64 48>
|
||||||
TRUST_PROXY=172.16.10.10
|
# Docker Compose: Frontend-Nginx ist der direkte Proxy zum Backend → 1 Hop
|
||||||
# oder TRUST_PROXY=1 für genau einen Proxy-Hop
|
TRUST_PROXY=1
|
||||||
|
# Nur bei direktem Backend-Zugriff ohne Frontend-Nginx: NPM-IP, z. B. TRUST_PROXY=172.16.10.10
|
||||||
```
|
```
|
||||||
|
|
||||||
`ORIGIN` muss **exakt** der Browser-URL entsprechen (ohne trailing slash).
|
`ORIGIN` muss **exakt** der Browser-URL entsprechen (ohne trailing slash).
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ if ! grep -q "^POSTGRES_PASSWORD=" "$ENV_FILE" || grep -q "^POSTGRES_PASSWORD=$"
|
|||||||
else
|
else
|
||||||
echo " keep POSTGRES_PASSWORD (already set)"
|
echo " keep POSTGRES_PASSWORD (already set)"
|
||||||
fi
|
fi
|
||||||
# NPM on 172.16.10.10 → app on this host
|
# Frontend-Nginx → Backend (one hop); NPM is in front of Nginx, not Backend directly
|
||||||
ensure_var TRUST_PROXY "172.16.10.10"
|
ensure_var TRUST_PROXY "1"
|
||||||
|
|
||||||
echo "Done. Verify with: docker exec daagbox-prod-db psql -U postgres -d daagbox -c 'SELECT 1'"
|
echo "Done. Verify with: docker exec daagbox-prod-db psql -U postgres -d daagbox -c 'SELECT 1'"
|
||||||
|
|||||||
+42
-2
@@ -45,13 +45,45 @@ export function createApp(): express.Express {
|
|||||||
app.use(cookieParser())
|
app.use(cookieParser())
|
||||||
app.use(express.json({ limit: '50mb' }))
|
app.use(express.json({ limit: '50mb' }))
|
||||||
|
|
||||||
const authLimiter = rateLimit({
|
/** WebAuthn login/register/session — strict per IP; excludes high-volume sync routes. */
|
||||||
|
const authFlowPaths = new Set([
|
||||||
|
'/register-options',
|
||||||
|
'/register-verify',
|
||||||
|
'/login-options',
|
||||||
|
'/login-verify',
|
||||||
|
'/reauth-options',
|
||||||
|
'/reauth-verify',
|
||||||
|
'/logout',
|
||||||
|
'/session'
|
||||||
|
])
|
||||||
|
|
||||||
|
/** Account/key/credential mutations — also strict; separate bucket from login flow. */
|
||||||
|
const sensitiveAuthExactPaths = new Set([
|
||||||
|
'/delete-account',
|
||||||
|
'/enroll-prf',
|
||||||
|
'/rotate-recovery',
|
||||||
|
'/add-credential-options',
|
||||||
|
'/add-credential-verify'
|
||||||
|
])
|
||||||
|
|
||||||
|
function isSensitiveAuthPath(path: string): boolean {
|
||||||
|
return sensitiveAuthExactPaths.has(path) || path.startsWith('/credentials/')
|
||||||
|
}
|
||||||
|
|
||||||
|
const authFlowLimiter = rateLimit({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 60,
|
max: 60,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false
|
legacyHeaders: false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const sensitiveAuthLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000,
|
||||||
|
max: 30,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false
|
||||||
|
})
|
||||||
|
|
||||||
const apiLimiter = rateLimit({
|
const apiLimiter = rateLimit({
|
||||||
windowMs: 1 * 60 * 1000,
|
windowMs: 1 * 60 * 1000,
|
||||||
max: 300,
|
max: 300,
|
||||||
@@ -66,7 +98,15 @@ export function createApp(): express.Express {
|
|||||||
legacyHeaders: false
|
legacyHeaders: false
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use('/api/auth', authLimiter)
|
app.use('/api/auth', (req, res, next) => {
|
||||||
|
if (authFlowPaths.has(req.path)) {
|
||||||
|
return authFlowLimiter(req, res, next)
|
||||||
|
}
|
||||||
|
if (isSensitiveAuthPath(req.path)) {
|
||||||
|
return sensitiveAuthLimiter(req, res, next)
|
||||||
|
}
|
||||||
|
return next()
|
||||||
|
})
|
||||||
app.use('/api/collaboration/invite-details', publicCollaborationLimiter)
|
app.use('/api/collaboration/invite-details', publicCollaborationLimiter)
|
||||||
app.use('/api/collaboration/share-pull', publicCollaborationLimiter)
|
app.use('/api/collaboration/share-pull', publicCollaborationLimiter)
|
||||||
app.use('/api', apiLimiter)
|
app.use('/api', apiLimiter)
|
||||||
|
|||||||
@@ -22,11 +22,6 @@ router.get('/vapid-public-key', (_req, res) => {
|
|||||||
return res.json({ publicKey })
|
return res.json({ publicKey })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.post('/debug-log', (req, res) => {
|
|
||||||
console.log('[CLIENT_DEBUG]', req.body)
|
|
||||||
return res.json({ success: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
router.use(requireUser)
|
router.use(requireUser)
|
||||||
|
|
||||||
router.get('/prefs', async (req: any, res) => {
|
router.get('/prefs', async (req: any, res) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user