Files
Syco fdf616d5e8
ci/woodpecker/push/woodpecker Pipeline was successful
Fix Vaultwarden 429: add 5min server cache, slow widget poll
2026-05-16 17:22:36 +02:00

96 lines
3.0 KiB
TypeScript

import { Router } from 'express'
import axios from 'axios'
const router = Router()
let vwCookie: string | null = null
let vwCookieExpiry = 0
interface CacheEntry { data: unknown; ts: number }
let statsCache: CacheEntry | null = null
const CACHE_TTL = 5 * 60 * 1000 // 5 minutes
async function getCookie(): Promise<string> {
if (vwCookie && Date.now() < vwCookieExpiry) return vwCookie
const host = process.env.VAULTWARDEN_HOST
const token = process.env.VAULTWARDEN_ADMIN_TOKEN
if (!host || !token) throw new Error('VAULTWARDEN_HOST / VAULTWARDEN_ADMIN_TOKEN not configured')
const res = await axios.post(
`${host}/admin`,
new URLSearchParams({ token: token.trim() }),
{
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
maxRedirects: 0,
validateStatus: s => s === 303 || s === 200,
}
)
const setCookieHeader = res.headers['set-cookie']
const match = (Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader ?? ''])
.find(c => c.startsWith('VW_ADMIN='))
if (!match) throw new Error('Admin login failed — check VAULTWARDEN_ADMIN_TOKEN')
vwCookie = match.split(';')[0]
vwCookieExpiry = Date.now() + 18 * 60 * 1000
return vwCookie
}
router.get('/stats', async (_req, res) => {
try {
const host = process.env.VAULTWARDEN_HOST
if (!host) { res.status(503).json({ error: 'VAULTWARDEN_HOST not configured' }); return }
if (statsCache && Date.now() - statsCache.ts < CACHE_TTL) {
res.json(statsCache.data)
return
}
const cookie = await getCookie()
const [usersRes, diagRes] = await Promise.all([
axios.get(`${host}/admin/users`, { headers: { Cookie: cookie, Accept: 'application/json' } }),
axios.get(`${host}/admin/diagnostics`, { headers: { Cookie: cookie, Accept: 'application/json' } }),
])
type VwUser = {
id: string
email: string
name?: string
lastActive?: string
creationDate?: string
userEnabled?: boolean
twoFactorEnabled?: boolean
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const users: any[] = usersRes.data ?? []
const diag = diagRes.data ?? {}
const result = {
version: diag.version ?? null,
signupsAllowed: diag.config?.signups_allowed ?? null,
userCount: users.length,
users: users.map(u => ({
email: u.email,
name: u.name || u.email,
enabled: u.userEnabled ?? true,
lastActive: u.lastActive ?? null,
created: u.creationDate ?? null,
twoFa: u.twoFactorEnabled ?? false,
cipherCount: u.cipherCount ?? u.cipher_count ?? null,
attachCount: u.attachmentCount ?? u.attachment_count ?? null,
})),
}
statsCache = { data: result, ts: Date.now() }
res.json(result)
} catch (err: unknown) {
if (err instanceof Error && err.message.includes('login failed')) vwCookie = null
const msg = err instanceof Error ? err.message : 'Unknown error'
res.status(500).json({ error: msg })
}
})
export default router