const express = require('express'); const crypto = require('crypto'); const authMiddleware = require('../middleware/auth'); const { buildPublicUrl, getDefaultS3Config, getObjectFromDefaultS3, } = require('../services/objectStorage'); const { compareVersions, getDesktopReleaseById, getLatestDesktopRelease, mapRelease, normalizeVersion, } = require('../services/desktopReleaseService'); const { DesktopSessionError, createDesktopSession, createSsoTicket, exchangeSsoTicket, getDesktopBootstrap, revokeDesktopSession, } = require('../services/desktopSessionService'); const router = express.Router(); function publicRelease(release) { const mapped = mapRelease(release); if (!mapped) return null; return { id: mapped.id, version: mapped.version, releaseNotes: mapped.releaseNotes, platform: mapped.platform, architecture: mapped.architecture, fileName: mapped.fileName, fileSize: mapped.fileSize, sha256: mapped.sha256, publishedAt: mapped.publishedAt, downloadUrl: `/api/desktop-client/releases/${mapped.id}/download`, }; } function requestUserId(req) { return req.user?.sub ?? req.user?.id; } function sendDesktopSessionError(res, error, fallbackMessage) { if (error instanceof DesktopSessionError) { return res.status(error.status).json({ success: false, error: error.message, code: error.code, }); } console.error(fallbackMessage, error); return res.status(500).json({ success: false, error: fallbackMessage }); } function safeScriptJson(value) { return JSON.stringify(value) .replace(//g, '\\u003e') .replace(/&/g, '\\u0026') .replace(/\u2028/g, '\\u2028') .replace(/\u2029/g, '\\u2029'); } function sendSsoExchangeHtml(res, result) { const nonce = crypto.randomBytes(18).toString('base64'); res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, private'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Referrer-Policy', 'no-referrer'); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader( 'Content-Security-Policy', `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'` ); res.type('html').send(` 正在进入总控 正在进入总控... `); } router.post('/session', authMiddleware, async (req, res) => { try { if (req.user?.tokenType) { throw new DesktopSessionError('请使用用户登录令牌创建桌面会话', 401, 'DESKTOP_USER_TOKEN_REQUIRED'); } const data = await createDesktopSession({ userId: requestUserId(req), payload: req.body || {}, }); res.setHeader('Cache-Control', 'no-store'); return res.json({ success: true, data }); } catch (error) { return sendDesktopSessionError(res, error, '创建桌面会话失败'); } }); router.get('/bootstrap', authMiddleware, async (req, res) => { try { const data = await getDesktopBootstrap({ claims: req.user, clientId: req.query.clientId, }); res.setHeader('Cache-Control', 'no-store'); return res.json({ success: true, data }); } catch (error) { return sendDesktopSessionError(res, error, '读取桌面启动配置失败'); } }); router.post('/sso-ticket', authMiddleware, async (req, res) => { try { const data = await createSsoTicket({ claims: req.user, clientId: req.body?.clientId, redirectPath: req.body?.redirectPath, }); res.setHeader('Cache-Control', 'no-store'); return res.json({ success: true, data }); } catch (error) { return sendDesktopSessionError(res, error, '创建总控登录票据失败'); } }); router.get('/sso/exchange', async (req, res) => { try { const result = await exchangeSsoTicket(req.query.ticket); return sendSsoExchangeHtml(res, result); } catch (error) { if (error instanceof DesktopSessionError) { res.setHeader('Cache-Control', 'no-store'); return res.status(error.status).type('html').send('登录失效登录票据无效或已过期,请返回桌面客户端重试。'); } console.error('交换总控登录票据失败', error); return res.status(500).type('html').send('登录失败总控登录失败,请稍后重试。'); } }); router.post('/session/revoke', authMiddleware, async (req, res) => { try { const data = await revokeDesktopSession(req.user); res.setHeader('Cache-Control', 'no-store'); return res.json({ success: true, data }); } catch (error) { return sendDesktopSessionError(res, error, '退出桌面会话失败'); } }); router.get('/latest', async (req, res) => { try { const platform = req.query.platform || 'win32'; const architecture = req.query.architecture || ''; const release = await getLatestDesktopRelease(platform, architecture); const currentVersion = normalizeVersion(req.query.currentVersion); const data = publicRelease(release); res.setHeader('Cache-Control', 'no-store, max-age=0'); res.json({ success: true, data: data ? { ...data, currentVersion: currentVersion || null, updateAvailable: currentVersion ? compareVersions(data.version, currentVersion) > 0 : null, } : null, }); } catch (error) { console.error('获取桌面客户端最新版本失败:', error); res.status(500).json({ success: false, error: '获取桌面客户端最新版本失败' }); } }); router.get('/releases/:id/download', async (req, res) => { try { const release = await getDesktopReleaseById(Number(req.params.id)); if (!release) { return res.status(404).json({ success: false, error: '安装包不存在或已下线' }); } const storageConfig = await getDefaultS3Config(); if (storageConfig?.customDomain) { return res.redirect(302, buildPublicUrl(storageConfig, release.objectKey)); } const object = await getObjectFromDefaultS3(release.objectKey, { range: req.headers.range, }); const encodedName = encodeURIComponent(release.fileName).replace(/%20/g, '+'); res.status(object.ContentRange ? 206 : 200); const fallbackContentType = release.platform === 'darwin' ? 'application/x-apple-diskimage' : 'application/vnd.microsoft.portable-executable'; res.setHeader('Content-Type', object.ContentType || fallbackContentType); res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodedName}`); res.setHeader('Accept-Ranges', object.AcceptRanges || 'bytes'); res.setHeader('Cache-Control', 'private, max-age=3600'); if (object.ContentLength != null) res.setHeader('Content-Length', String(object.ContentLength)); if (object.ContentRange) res.setHeader('Content-Range', object.ContentRange); if (object.ETag) res.setHeader('ETag', object.ETag); object.Body.on('error', (error) => { console.error('桌面客户端安装包下载流中断:', error); if (!res.headersSent) res.status(502).end(); else res.destroy(error); }); object.Body.pipe(res); } catch (error) { console.error('下载桌面客户端安装包失败:', error); if (!res.headersSent) { res.status(502).json({ success: false, error: '安装包下载失败,请稍后重试' }); } } }); module.exports = router;