mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-10 13:30:46 +08:00
Require browser login for scheduled-task HTTP APIs.
List/run/pause/resume/delete now return 401 without UDS cookies; hide sidebar chrome when logged out. uds-auth still validates the session store on the same routes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
67238d746e
commit
a38f3dcec3
4 changed files with 122 additions and 17 deletions
71
lib/host.js
71
lib/host.js
|
|
@ -52,11 +52,50 @@ function parseUrl(req) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function parseCookieHeader(header, name) {
|
||||
if (!header || typeof header !== 'string') return null
|
||||
for (const part of header.split(';')) {
|
||||
const idx = part.indexOf('=')
|
||||
if (idx < 0) continue
|
||||
if (part.slice(0, idx).trim() !== name) continue
|
||||
try {
|
||||
return decodeURIComponent(part.slice(idx + 1).trim())
|
||||
} catch {
|
||||
return part.slice(idx + 1).trim()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** Browser UDS / fallback login cookies (validated more strictly by uds-auth when present). */
|
||||
function browserEmpNo(request) {
|
||||
const cookie = request?.headers?.cookie || ''
|
||||
return parseCookieHeader(cookie, 'PORTALSSOUser')
|
||||
|| parseCookieHeader(cookie, 'ZTEDPGSSOUser')
|
||||
|| parseCookieHeader(cookie, 'UDS_FALLBACK_USER')
|
||||
}
|
||||
|
||||
function requireBrowserLogin(request, write) {
|
||||
if (!isTrustedApiRequest(request)) {
|
||||
write(403, { ok: false, error: 'forbidden' })
|
||||
return false
|
||||
}
|
||||
if (!browserEmpNo(request)) {
|
||||
write(401, { ok: false, error: 'login_required', message: '登录后才能使用定时任务' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function isTrustedApiRequest(request) {
|
||||
const host = request.headers.host ?? ''
|
||||
if (!host) return false
|
||||
const hostname = host.split(':')[0].replace(/^\[|\]$/g, '')
|
||||
if ((request.headers['sec-fetch-site'] ?? '') === 'cross-site') return false
|
||||
// Allow same-origin LAN / non-loopback Host (login still required separately).
|
||||
const site = request.headers['sec-fetch-site'] ?? ''
|
||||
if (site === 'same-origin' || site === 'same-site') return true
|
||||
const origin = request.headers.origin
|
||||
if (origin !== undefined && origin !== 'null') {
|
||||
try {
|
||||
|
|
@ -391,14 +430,14 @@ export function createHostService(options = {}) {
|
|||
}
|
||||
|
||||
if (path === `${API_PREFIX}/settings` && method === 'GET') {
|
||||
if (!isTrustedApiRequest(req)) return write(403, { ok: false, error: 'forbidden' })
|
||||
if (!requireBrowserLogin(req, write)) return
|
||||
const state = await snapshot()
|
||||
write(200, { ok: true, settings: state.settings })
|
||||
return
|
||||
}
|
||||
|
||||
if (path === `${API_PREFIX}/settings` && method === 'PUT') {
|
||||
if (!isTrustedApiRequest(req)) return write(403, { ok: false, error: 'forbidden' })
|
||||
if (!requireBrowserLogin(req, write)) return
|
||||
const body = await readJsonBody(req)
|
||||
const settings = await updateSettings(body)
|
||||
write(200, { ok: true, settings })
|
||||
|
|
@ -406,7 +445,7 @@ export function createHostService(options = {}) {
|
|||
}
|
||||
|
||||
if (path === `${API_PREFIX}/models` && method === 'GET') {
|
||||
if (!isTrustedApiRequest(req)) return write(403, { ok: false, error: 'forbidden' })
|
||||
if (!requireBrowserLogin(req, write)) return
|
||||
const catalog = typeof sessionPort?.listModels === 'function'
|
||||
? await sessionPort.listModels()
|
||||
: { groups: [], current: null }
|
||||
|
|
@ -415,7 +454,7 @@ export function createHostService(options = {}) {
|
|||
}
|
||||
|
||||
if (path === `${API_PREFIX}/presets` && method === 'GET') {
|
||||
if (!isTrustedApiRequest(req)) return write(403, { ok: false, error: 'forbidden' })
|
||||
if (!requireBrowserLogin(req, write)) return
|
||||
const catalog = typeof sessionPort?.listPresets === 'function'
|
||||
? await sessionPort.listPresets()
|
||||
: { items: [], current: null }
|
||||
|
|
@ -424,7 +463,7 @@ export function createHostService(options = {}) {
|
|||
}
|
||||
|
||||
if (path === `${API_PREFIX}/workspaces` && method === 'GET') {
|
||||
if (!isTrustedApiRequest(req)) return write(403, { ok: false, error: 'forbidden' })
|
||||
if (!requireBrowserLogin(req, write)) return
|
||||
const workspaces = typeof sessionPort?.listWorkspaces === 'function'
|
||||
? await sessionPort.listWorkspaces()
|
||||
: []
|
||||
|
|
@ -433,7 +472,7 @@ export function createHostService(options = {}) {
|
|||
}
|
||||
|
||||
if (path === `${API_PREFIX}/im-catalog` && method === 'GET') {
|
||||
if (!isTrustedApiRequest(req)) return write(403, { ok: false, error: 'forbidden' })
|
||||
if (!requireBrowserLogin(req, write)) return
|
||||
const dshIm = getDshIm()
|
||||
if (!dshIm || typeof dshIm.listDeliveryCatalog !== 'function') {
|
||||
write(200, {
|
||||
|
|
@ -463,14 +502,14 @@ export function createHostService(options = {}) {
|
|||
}
|
||||
|
||||
if (path === `${API_PREFIX}/jobs` && method === 'GET') {
|
||||
if (!isTrustedApiRequest(req)) return write(403, { ok: false, error: 'forbidden' })
|
||||
if (!requireBrowserLogin(req, write)) return
|
||||
const state = await snapshot()
|
||||
write(200, { ok: true, jobs: listJobs(state).map(jobView) })
|
||||
return
|
||||
}
|
||||
|
||||
if (path === `${API_PREFIX}/jobs` && method === 'POST') {
|
||||
if (!isTrustedApiRequest(req)) return write(403, { ok: false, error: 'forbidden' })
|
||||
if (!requireBrowserLogin(req, write)) return
|
||||
const body = await readJsonBody(req)
|
||||
const job = await createJob(body)
|
||||
write(200, { ok: true, job })
|
||||
|
|
@ -482,14 +521,14 @@ export function createHostService(options = {}) {
|
|||
const jobId = decodeURIComponent(jobMatch[1])
|
||||
const rest = jobMatch[2] || ''
|
||||
if (method === 'GET' && !rest) {
|
||||
if (!isTrustedApiRequest(req)) return write(403, { ok: false, error: 'forbidden' })
|
||||
if (!requireBrowserLogin(req, write)) return
|
||||
const state = await snapshot()
|
||||
const job = getJob(state, jobId)
|
||||
if (!job) return write(404, { ok: false, error: 'job not found' })
|
||||
write(200, { ok: true, job: jobView(job) })
|
||||
return
|
||||
}
|
||||
if (!isTrustedApiRequest(req)) return write(403, { ok: false, error: 'forbidden' })
|
||||
if (!requireBrowserLogin(req, write)) return
|
||||
if (method === 'PATCH' && !rest) {
|
||||
const body = await readJsonBody(req)
|
||||
const job = await updateJob(jobId, body)
|
||||
|
|
@ -520,7 +559,7 @@ export function createHostService(options = {}) {
|
|||
|
||||
const openMatch = path.match(new RegExp(`^${API_PREFIX}/runs/([^/]+)/open$`))
|
||||
if (openMatch && method === 'POST') {
|
||||
if (!isTrustedApiRequest(req)) return write(403, { ok: false, error: 'forbidden' })
|
||||
if (!requireBrowserLogin(req, write)) return
|
||||
const runId = decodeURIComponent(openMatch[1])
|
||||
const state = await snapshot()
|
||||
const run = (state.runs || []).find((row) => row.id === runId)
|
||||
|
|
@ -535,7 +574,7 @@ export function createHostService(options = {}) {
|
|||
|
||||
const adoptMatch = path.match(new RegExp(`^${API_PREFIX}/sessions/([^/]+)/adopt$`))
|
||||
if (adoptMatch && method === 'POST') {
|
||||
if (!isTrustedApiRequest(req)) return write(403, { ok: false, error: 'forbidden' })
|
||||
if (!requireBrowserLogin(req, write)) return
|
||||
const sessionId = decodeURIComponent(adoptMatch[1])
|
||||
if (!sessionId) return write(400, { ok: false, error: 'sessionId required' })
|
||||
if (typeof sessionPort?.adoptSession !== 'function') {
|
||||
|
|
@ -547,14 +586,14 @@ export function createHostService(options = {}) {
|
|||
}
|
||||
|
||||
if (path === `${API_PREFIX}/conceal` && method === 'POST') {
|
||||
if (!isTrustedApiRequest(req)) return write(403, { ok: false, error: 'forbidden' })
|
||||
if (!requireBrowserLogin(req, write)) return
|
||||
const hidden = await concealKnownSessions()
|
||||
write(200, { ok: true, hidden })
|
||||
return
|
||||
}
|
||||
|
||||
if (path === `${API_PREFIX}/history` && method === 'GET') {
|
||||
if (!isTrustedApiRequest(req)) return write(403, { ok: false, error: 'forbidden' })
|
||||
if (!requireBrowserLogin(req, write)) return
|
||||
const state = await snapshot()
|
||||
const jobId = url.searchParams.get('jobId') || undefined
|
||||
write(200, { ok: true, runs: listHistory(state, jobId).map(runView) })
|
||||
|
|
@ -562,7 +601,7 @@ export function createHostService(options = {}) {
|
|||
}
|
||||
|
||||
if (path === `${API_PREFIX}/preview` && method === 'POST') {
|
||||
if (!isTrustedApiRequest(req)) return write(403, { ok: false, error: 'forbidden' })
|
||||
if (!requireBrowserLogin(req, write)) return
|
||||
const body = await readJsonBody(req)
|
||||
const settings = (await snapshot()).settings
|
||||
const schedule = validateSchedule(body.schedule || body, body.timezone || settings.timezone)
|
||||
|
|
@ -572,7 +611,7 @@ export function createHostService(options = {}) {
|
|||
}
|
||||
|
||||
if (path === `${API_PREFIX}/workspace-visible` && method === 'GET') {
|
||||
if (!isTrustedApiRequest(req)) return write(403, { ok: false, error: 'forbidden' })
|
||||
if (!requireBrowserLogin(req, write)) return
|
||||
const state = await snapshot()
|
||||
const listed = url.searchParams.getAll('id')
|
||||
write(200, {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue