文件
jw-beauty/server/src/routes/subscription.js
T
Guoguo 2eb38195f1 fix: address critical security and data integrity issues from cross-audit
- Add expire_time > NOW() filter to findActive() preventing stale subscriptions
- Add express-rate-limit on login endpoints (user: 10/15min, admin: 5/15min)
- Add production guard for default admin credentials
- Fix BLE bindDevice userId encoding (uint32 instead of hexToBytes on numeric)
- Wrap adminCreate in transaction to prevent race condition
- Add settings cache invalidation after admin saves
- Read trial_days from settings instead of hardcoding 7
- Fix double JSON.stringify in commandDao.finish call
- Cancel stale pending bindings before creating new ones
- Reduce token refresh grace period from 3 days to 1 day
- Fix subscribe-success to fetch expiry from server (correct for renewals)
- Add keep-alive name property to DashboardView and SettingsView
- Fix BLE disconnect() to preserve listener registrations across reconnects
2026-05-05 02:33:25 -07:00

88 行
4.1 KiB
JavaScript

const router = require('express').Router()
const { ok, fail } = require('../lib/response')
const { requireUser } = require('../middleware/auth')
const { requireAdmin } = require('../middleware/auth')
const { getSettings } = require('../lib/settings-cache')
const subscriptionDao = require('../dao/subscription.dao')
const logDao = require('../dao/log.dao')
const wrap = fn => (req, res, next) => fn(req, res, next).catch(next)
const PLANS = {
trial: { amount: 0, days: 7 },
monthly: { amount: 99, days: 30 },
yearly: { amount: 899, days: 365 }
}
router.get('/subscription/plans', wrap(async (req, res) => {
const settingsDao = require('../dao/settings.dao')
const settings = await settingsDao.getAll()
res.json(ok({
plans: [
{ key: 'monthly', name: '月卡', price: Number(settings.monthly_price) || PLANS.monthly.amount, days: 30 },
{ key: 'yearly', name: '年卡', price: Number(settings.yearly_price) || PLANS.yearly.amount, days: 365 },
{ key: 'trial', name: '试用', price: 0, days: Number(settings.trial_days) || 7 }
]
}))
}))
router.get('/subscription', requireUser, wrap(async (req, res) => {
const sub = await subscriptionDao.findActive(req.user.user_id)
const trialUsed = await subscriptionDao.findTrial(req.user.user_id)
if (!sub) return res.json(ok({ status: 'inactive', plan: 'none', remaining_days: 0, trial_used: !!trialUsed }))
res.json(ok({
status: sub.remaining_days > 0 ? 'active' : 'expired',
plan: sub.plan,
start_time: sub.start_time,
expire_time: sub.expire_time,
remaining_days: sub.remaining_days,
trial_used: !!trialUsed
}))
}))
router.post('/subscription/purchase', requireUser, wrap(async (req, res) => {
const plan = req.body.plan || req.body.plan_type
if (!PLANS[plan]) return res.json(fail(2001, 'invalid plan'))
const orderId = 'ORD' + Date.now()
res.json(ok({ order_id: orderId, payment_params: {}, plan, amount: PLANS[plan].amount }))
}))
router.post('/subscription/mock-purchase', requireUser, wrap(async (req, res) => {
const config = require('../config')
if (config.nodeEnv === 'production') return res.json(fail(2001, 'not available in production'))
const plan = req.body.plan || req.body.plan_type
if (!PLANS[plan] || plan === 'trial') return res.json(fail(2001, 'invalid plan'))
const p = PLANS[plan]
const orderId = 'MOCK' + Date.now()
await subscriptionDao.purchase(req.user.user_id, plan, p.amount, orderId, p.days)
await logDao.write({ user_id: req.user.user_id, action: 'subscription_mock_purchase', detail: '模拟购买: ' + plan, ip: req.ip })
const sub = await subscriptionDao.findActive(req.user.user_id)
res.json(ok({ status: 'active', plan, remaining_days: sub ? sub.remaining_days : p.days }))
}))
router.post('/subscription/trial', requireUser, wrap(async (req, res) => {
const usedTrial = await subscriptionDao.findTrial(req.user.user_id)
if (usedTrial) return res.json(fail(2001, '已使用过试用'))
const activeSub = await subscriptionDao.findActive(req.user.user_id)
if (activeSub) return res.json(fail(2001, '已有有效订阅'))
const settings = await getSettings()
const trialDays = Number(settings.trial_days) || 7
await subscriptionDao.createTrial(req.user.user_id, undefined, trialDays)
res.json(ok({ status: 'active', plan: 'trial', remaining_days: trialDays }))
}))
// Temporary: admin-only until payment integration
router.post('/subscription/verify', requireAdmin, wrap(async (req, res) => {
const userId = req.body.user_id
if (!userId) return res.json(fail(2001, 'user_id required'))
const plan = req.body.plan || req.body.plan_type || 'monthly'
if (!PLANS[plan]) return res.json(fail(2001, 'invalid plan'))
const p = PLANS[plan]
await subscriptionDao.purchase(userId, plan, p.amount, req.body.order_id || 'ORD' + Date.now(), p.days)
await logDao.write({ admin_id: req.admin.admin_id, action: 'subscription_verify', detail: '订阅生效: ' + plan + ' user:' + userId, ip: req.ip })
const sub = await subscriptionDao.findActive(userId)
res.json(ok({ status: 'active', plan, remaining_days: sub ? sub.remaining_days : p.days }))
}))
module.exports = router