mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-25 11:20:07 +08:00
* style(linter): enable consistent-type-imports rule in typescript * chore: add biome to lint script for improved code formatting * chore: add oxlint-specific lint script for faster linting * refactor: use type-only imports for better type safety and clarity
63 lines
1.9 KiB
TypeScript
63 lines
1.9 KiB
TypeScript
import crypto from 'crypto'
|
||
import type { NextFunction, Request, Response } from 'express'
|
||
|
||
import { config } from '../config'
|
||
|
||
export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => {
|
||
const auth = req.header('Authorization') || ''
|
||
const xApiKey = req.header('x-api-key') || ''
|
||
|
||
// Fast rejection if neither credential header provided
|
||
if (!auth && !xApiKey) {
|
||
return res.status(401).json({ error: 'Unauthorized: missing credentials' })
|
||
}
|
||
|
||
let token: string | undefined
|
||
|
||
// Prefer Bearer if well‑formed
|
||
if (auth) {
|
||
const trimmed = auth.trim()
|
||
const bearerPrefix = /^Bearer\s+/i
|
||
if (bearerPrefix.test(trimmed)) {
|
||
const candidate = trimmed.replace(bearerPrefix, '').trim()
|
||
if (!candidate) {
|
||
return res.status(401).json({ error: 'Unauthorized: empty bearer token' })
|
||
}
|
||
token = candidate
|
||
}
|
||
}
|
||
|
||
// Fallback to x-api-key if token still not resolved
|
||
if (!token && xApiKey) {
|
||
if (!xApiKey.trim()) {
|
||
return res.status(401).json({ error: 'Unauthorized: empty x-api-key' })
|
||
}
|
||
token = xApiKey.trim()
|
||
}
|
||
|
||
if (!token) {
|
||
// At this point we had at least one header, but none yielded a usable token
|
||
return res.status(401).json({ error: 'Unauthorized: invalid credentials format' })
|
||
}
|
||
|
||
const { apiKey } = await config.get()
|
||
|
||
if (!apiKey) {
|
||
// If server not configured, treat as forbidden (or could be 500). Choose 403 to avoid leaking config state.
|
||
return res.status(403).json({ error: 'Forbidden' })
|
||
}
|
||
|
||
// Timing-safe compare when lengths match, else immediate forbidden
|
||
if (token.length !== apiKey.length) {
|
||
return res.status(403).json({ error: 'Forbidden' })
|
||
}
|
||
|
||
const tokenBuf = Buffer.from(token)
|
||
const keyBuf = Buffer.from(apiKey)
|
||
if (!crypto.timingSafeEqual(tokenBuf, keyBuf)) {
|
||
return res.status(403).json({ error: 'Forbidden' })
|
||
}
|
||
|
||
return next()
|
||
}
|