feat: 新增短链接工具,优化日历提醒功能
1. 新增完整短链接工具服务,包括前端页面、后端API与数据库支持 2. 新增移动端日历提醒工具与鉴权配置 3. 优化日历事件编辑与展示: - 添加事件循环重复设置 - 新增全天事件支持 - 时间选择器自动对齐15分钟间隔 - 保存状态加载动画 - 事件卡片显示重复规则 4. 更新API基础地址与路由配置 5. 新增SSL证书文件与相关工具静态资源
This commit is contained in:
+146
-1
@@ -78,6 +78,7 @@ const plantHome = require('./plant_home')
|
||||
const globalNews = require('./global_news')
|
||||
const boxBackup = require('./box_backup')
|
||||
const yuanzhupai = require('./yuanzhupai')
|
||||
const shortLink = require('./short_link')
|
||||
const dataGateway = require('./data_gateway')
|
||||
const ossFileCabinet = require('./oss_file_cabinet')
|
||||
yuanzhupai.initDb()
|
||||
@@ -1579,7 +1580,9 @@ const mapSystemIdToUrl = (systemId) => {
|
||||
'Tools-plant_home': '/tools/plant_home',
|
||||
'Tools-apple_watch': '/tools/apple_watch',
|
||||
'Tools-private_clipboard': '/tools/private_clipboard',
|
||||
'Tools-style_check': '/tools/style_check/index.html'
|
||||
'Tools-style_check': '/tools/style_check/index.html',
|
||||
'Tools-short_link': '/tools/short_link',
|
||||
'Tools-web_mobile_calendar_reminder': '/tools/web_mobile_calendar_reminder'
|
||||
}
|
||||
if (m[systemId]) return m[systemId]
|
||||
if (systemId.startsWith('Tools-')) {
|
||||
@@ -5721,6 +5724,133 @@ app.use('/tools/yuanzhupai', (req, res, next) => {
|
||||
return res.status(401).send('未授权')
|
||||
})
|
||||
|
||||
// Short Link: 鉴权基础函数 + 路由守卫(必须在 express.static 之前)
|
||||
const SHORT_LINK_AUTH_COOKIE = 'short_link_gate'
|
||||
const readShortLinkAuthConfig = () => {
|
||||
try {
|
||||
const p = path.join(process.cwd(), 'public', 'tools', 'short_link', 'auth_config.json')
|
||||
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
|
||||
} catch {}
|
||||
return {}
|
||||
}
|
||||
const readShortLinkJwk = () => {
|
||||
try {
|
||||
const p = path.join(process.cwd(), 'config', 'short_link.jwk.json')
|
||||
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
|
||||
} catch {}
|
||||
return { kty: 'RSA', n: '', e: '' }
|
||||
}
|
||||
const getShortLinkCookieName = () => {
|
||||
try { const n = String(readShortLinkAuthConfig().cookieName || ''); if (n) return n } catch {}
|
||||
return SHORT_LINK_AUTH_COOKIE
|
||||
}
|
||||
const getShortLinkMaxAge = () => {
|
||||
try { const days = Number(readShortLinkAuthConfig().max_age_days || 0); if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000 } catch {}
|
||||
return 30 * 24 * 3600 * 1000
|
||||
}
|
||||
const getShortLinkGuidConfig = () => {
|
||||
try {
|
||||
const cfg = readShortLinkAuthConfig()
|
||||
const keyRaw = String(cfg.guidParam || cfg.guid_param || 'guid').trim()
|
||||
const valueRaw = String(cfg.guidValue || cfg.guid_value || '').trim()
|
||||
return { key: keyRaw || 'guid', value: valueRaw }
|
||||
} catch { return { key: 'guid', value: '' } }
|
||||
}
|
||||
const hasValidShortLinkGuid = req => {
|
||||
try {
|
||||
const cfg = readShortLinkAuthConfig()
|
||||
if (cfg.enable_auth === false) return false
|
||||
const { key, value } = getShortLinkGuidConfig()
|
||||
if (!value) return false
|
||||
const q = req && req.query ? req.query : {}
|
||||
const raw = q[key]
|
||||
if (raw === undefined || raw === null) return false
|
||||
const incoming = Array.isArray(raw) ? String(raw[0] || '') : String(raw)
|
||||
return incoming === value
|
||||
} catch { return false }
|
||||
}
|
||||
const isShortLinkForceGuid = () => {
|
||||
try {
|
||||
const cfg = readShortLinkAuthConfig()
|
||||
if (cfg.enable_auth === false) return false
|
||||
if (Object.prototype.hasOwnProperty.call(cfg, 'requireGuid')) return !!cfg.requireGuid
|
||||
if (Object.prototype.hasOwnProperty.call(cfg, 'require_guid')) return !!cfg.require_guid
|
||||
return false
|
||||
} catch { return false }
|
||||
}
|
||||
const hasShortLinkAuth = req => {
|
||||
try {
|
||||
const config = readShortLinkAuthConfig()
|
||||
if (config.enable_auth === false) return true
|
||||
const cookies = parseCookie(req.headers.cookie || '')
|
||||
return cookies[getShortLinkCookieName()] === '1'
|
||||
} catch { return false }
|
||||
}
|
||||
|
||||
// 第1层:精确入口守卫
|
||||
app.get('/tools/short_link', (req, res, next) => {
|
||||
setNoCache(res)
|
||||
try {
|
||||
const forceGuid = isShortLinkForceGuid()
|
||||
if (hasShortLinkAuth(req)) return next()
|
||||
const guidKey = getShortLinkGuidConfig().key
|
||||
const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
|
||||
if (hasValidShortLinkGuid(req)) {
|
||||
const maxAge = getShortLinkMaxAge()
|
||||
const cookieName = getShortLinkCookieName()
|
||||
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
|
||||
return res.redirect('/tools/short_link/index.html')
|
||||
}
|
||||
if (hasGuidInput) return res.status(401).send('未授权')
|
||||
const token = String(req.query.token || '')
|
||||
if (!token) {
|
||||
if (forceGuid) return res.status(401).send('未授权')
|
||||
return next()
|
||||
}
|
||||
const payload = verifyJwtWithKeys(token, jwkKeys(readShortLinkJwk()))
|
||||
if (!payload) return res.status(401).send('未授权')
|
||||
{
|
||||
const iss = String(payload.iss || '')
|
||||
const navIss = getNavIssFromFlags()
|
||||
if (iss === navIss && !audMatch(payload, 'Tools-short_link')) return res.status(401).send('未授权')
|
||||
}
|
||||
const maxAge = getShortLinkMaxAge()
|
||||
const cookieName = getShortLinkCookieName()
|
||||
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
|
||||
return res.redirect('/tools/short_link/index.html')
|
||||
} catch (e) { return res.status(401).send('未授权') }
|
||||
})
|
||||
|
||||
// 第2层:子路径守卫(拦截静态资源绕过)
|
||||
app.use('/tools/short_link', (req, res, next) => {
|
||||
const forceGuid = isShortLinkForceGuid()
|
||||
if (hasShortLinkAuth(req)) return next()
|
||||
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '' || req.path === '/index.html')
|
||||
const guidKey = getShortLinkGuidConfig().key
|
||||
const hasGuidInput = isHandshake && req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
|
||||
if (isHandshake && hasValidShortLinkGuid(req)) {
|
||||
const maxAge = getShortLinkMaxAge()
|
||||
const cookieName = getShortLinkCookieName()
|
||||
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
|
||||
return res.redirect('/tools/short_link/index.html')
|
||||
}
|
||||
if (hasGuidInput) return res.status(401).send('未授权')
|
||||
if (isHandshake && String(req.query.token || '')) {
|
||||
const payload = verifyJwtWithKeys(String(req.query.token), jwkKeys(readShortLinkJwk()))
|
||||
if (payload) {
|
||||
const iss = String(payload.iss || '')
|
||||
const navIss = getNavIssFromFlags()
|
||||
if (iss === navIss && !audMatch(payload, 'Tools-short_link')) return res.status(401).send('未授权')
|
||||
const maxAge = getShortLinkMaxAge()
|
||||
const cookieName = getShortLinkCookieName()
|
||||
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
|
||||
return res.redirect('/tools/short_link/index.html')
|
||||
}
|
||||
}
|
||||
if (isHandshake && forceGuid) return res.status(401).send('未授权')
|
||||
return res.status(401).send('未授权')
|
||||
})
|
||||
|
||||
const BOX_BACKUP_AUTH_COOKIE = 'box_disaster_sentinel_gate'
|
||||
const readBoxBackupAuthConfig = () => {
|
||||
try {
|
||||
@@ -6395,6 +6525,21 @@ globalNews.bindRoutes(app)
|
||||
dataGateway.bindRoutes(app)
|
||||
ossFileCabinet.bindRoutes(app)
|
||||
|
||||
// 第3层:Short Link API 鉴权(必须在 bindRoutes 之前注册)
|
||||
app.use('/api/short_link', (req, res, next) => {
|
||||
if (req.path === '/auth/hint') return next()
|
||||
if (hasShortLinkAuth(req)) return next()
|
||||
return res.status(401).json({ ok: false, error: 'unauthorized' })
|
||||
})
|
||||
|
||||
app.get('/api/short_link/auth/hint', (req, res) => {
|
||||
setNoCache(res)
|
||||
res.json({ ok: true, message: '请通过首页九宫格点击「短链接」进入,或使用正确的访问链接' })
|
||||
})
|
||||
|
||||
// Short Link 业务路由(/s/:code 公开跳转 + /api/short_link/* 管理接口)
|
||||
shortLink.bindRoutes(app)
|
||||
|
||||
const marketsCfgAtBoot = getMarketsCfg()
|
||||
const currentPort = Number(process.env.PORT || '8976') || 8976
|
||||
const marketsAutomationPort = Number(marketsCfgAtBoot.automation_port || 8977) || 8977
|
||||
|
||||
Reference in New Issue
Block a user