270 行
11 KiB
JavaScript
270 行
11 KiB
JavaScript
const router = require('express').Router()
|
|
const { ok, fail } = require('../lib/response')
|
|
const { hashPassword, hashPasswordLegacy, verifyPassword, signAdmin } = require('../lib/auth')
|
|
const { requireAdmin } = require('../middleware/auth')
|
|
const { invalidateCache } = require('../lib/settings-cache')
|
|
const adminDao = require('../dao/admin.dao')
|
|
const deviceDao = require('../dao/device.dao')
|
|
const bindingDao = require('../dao/binding.dao')
|
|
const commandDao = require('../dao/command.dao')
|
|
const userDao = require('../dao/user.dao')
|
|
const subscriptionDao = require('../dao/subscription.dao')
|
|
const treatmentDao = require('../dao/treatment.dao')
|
|
const logDao = require('../dao/log.dao')
|
|
const settingsDao = require('../dao/settings.dao')
|
|
|
|
const wrap = fn => (req, res, next) => fn(req, res, next).catch(next)
|
|
|
|
function pageParams(query) {
|
|
const page = Math.max(1, parseInt(query.page, 10) || 1)
|
|
const pageSize = Math.min(Math.max(1, parseInt(query.page_size, 10) || 20), 100)
|
|
return { page, pageSize, offset: (page - 1) * pageSize }
|
|
}
|
|
|
|
// --- Auth ---
|
|
|
|
router.post('/login', wrap(async (req, res) => {
|
|
const username = req.body.username || ''
|
|
const password = req.body.password || ''
|
|
const admin = await adminDao.findByUsername(username)
|
|
if (!admin) return res.json(fail(1001, '用户名或密码错误'))
|
|
let matched = verifyPassword(password, admin.password_hash)
|
|
if (!matched) {
|
|
if (admin.password_salt && hashPasswordLegacy(password, admin.password_salt) === admin.password_hash) {
|
|
const newHash = hashPassword(password)
|
|
await adminDao.updatePassword(admin.admin_id, newHash)
|
|
matched = true
|
|
}
|
|
}
|
|
if (!matched) return res.json(fail(1001, '用户名或密码错误'))
|
|
const token = signAdmin(admin)
|
|
await logDao.write({ admin_id: admin.admin_id, action: 'admin_login', detail: '管理员登录: ' + username, ip: req.ip })
|
|
res.json(ok({ token, admin_id: String(admin.admin_id), username: admin.username, real_name: admin.real_name, role: admin.role }))
|
|
}))
|
|
|
|
router.post('/password', requireAdmin, wrap(async (req, res) => {
|
|
const oldPassword = req.body.old_password || ''
|
|
const newPassword = req.body.new_password || ''
|
|
if (newPassword.length < 6) return res.json(fail(2001, 'password too short'))
|
|
const current = await adminDao.findById(req.admin.admin_id)
|
|
if (!current) return res.json(fail(1002, '未授权,请重新登录'))
|
|
let matched = verifyPassword(oldPassword, current.password_hash)
|
|
if (!matched && current.password_salt && hashPasswordLegacy(oldPassword, current.password_salt) === current.password_hash) {
|
|
matched = true
|
|
}
|
|
if (!matched) return res.json(fail(1001, '原密码错误'))
|
|
const newHash = hashPassword(newPassword)
|
|
await adminDao.updatePassword(req.admin.admin_id, newHash)
|
|
await logDao.write({ admin_id: req.admin.admin_id, action: 'admin_change_password', detail: '管理员修改密码', ip: req.ip })
|
|
res.json(ok({ message: 'success' }))
|
|
}))
|
|
|
|
// --- Dashboard ---
|
|
|
|
router.get('/dashboard', requireAdmin, wrap(async (req, res) => {
|
|
const counts = await adminDao.getDashboardCounts()
|
|
const subStats = await adminDao.getSubscriptionStats()
|
|
res.json(ok({
|
|
device_count: counts.device_count,
|
|
user_count: counts.user_count,
|
|
treatment_count: counts.treatment_count,
|
|
subscription_count: counts.subscription_count,
|
|
sub_stats: subStats
|
|
}))
|
|
}))
|
|
|
|
// --- Devices ---
|
|
|
|
router.get('/devices', requireAdmin, wrap(async (req, res) => {
|
|
const { page, pageSize, offset } = pageParams(req.query)
|
|
const { records, total } = await deviceDao.list({
|
|
keyword: req.query.keyword,
|
|
pageSize,
|
|
offset
|
|
})
|
|
res.json(ok({ records, total }))
|
|
}))
|
|
|
|
router.post('/devices', requireAdmin, wrap(async (req, res) => {
|
|
const deviceId = String(req.body.device_id || '').trim()
|
|
if (!deviceId) return res.json(fail(2001, 'device_id required'))
|
|
await deviceDao.create({
|
|
device_id: deviceId,
|
|
product_id: req.body.product_id || 'HOX_LIGHT_MASK',
|
|
device_secret: req.body.device_secret || '',
|
|
device_name: req.body.device_name || 'LumiFlow',
|
|
firmware_version: req.body.firmware_version || '1.0.0'
|
|
})
|
|
await logDao.write({ admin_id: req.admin.admin_id, action: 'admin_device_create', detail: '预生成产品码: ' + deviceId, ip: req.ip })
|
|
res.json(ok({ device_id: deviceId }))
|
|
}))
|
|
|
|
router.post('/devices/batch', requireAdmin, wrap(async (req, res) => {
|
|
const deviceIds = req.body.device_ids
|
|
if (!Array.isArray(deviceIds) || deviceIds.length === 0 || deviceIds.length > 500) {
|
|
return res.json(fail(2001, 'device_ids must be an array with 1-500 items'))
|
|
}
|
|
const { created, failed } = await deviceDao.createBatch(deviceIds)
|
|
await logDao.write({ admin_id: req.admin.admin_id, action: 'admin_device_batch_create', detail: '批量预生成产品码: ' + created + '/' + deviceIds.length, ip: req.ip })
|
|
res.json(ok({ created, failed }))
|
|
}))
|
|
|
|
router.get('/devices/:device_id', requireAdmin, wrap(async (req, res) => {
|
|
const result = await deviceDao.findByIdWithHistory(req.params.device_id)
|
|
if (!result || !result.device) return res.json(fail(1005, 'DEVICE_NOT_FOUND'))
|
|
res.json(ok(Object.assign({}, result.device, { binding_history: result.binding_history, recent_treatments: result.recent_treatments })))
|
|
}))
|
|
|
|
router.post('/devices/:device_id/unbind', requireAdmin, wrap(async (req, res) => {
|
|
await deviceDao.unbind(req.params.device_id)
|
|
await logDao.write({ admin_id: req.admin.admin_id, action: 'admin_device_unbind', detail: '后台解绑设备: ' + req.params.device_id, ip: req.ip })
|
|
res.json(ok({ message: 'success' }))
|
|
}))
|
|
|
|
router.post('/devices/:device_id/command', requireAdmin, wrap(async (req, res) => {
|
|
const opcode = parseInt(req.body.opcode, 10)
|
|
if (!opcode) return res.json(fail(2001, 'opcode required'))
|
|
await commandDao.create(req.params.device_id, req.admin.admin_id, opcode, req.body)
|
|
await logDao.write({ admin_id: req.admin.admin_id, action: 'admin_device_command', detail: '记录远程指令: ' + req.params.device_id, ip: req.ip })
|
|
res.json(ok({ message: 'queued', command: req.body }))
|
|
}))
|
|
|
|
router.get('/devices/:device_id/commands', requireAdmin, wrap(async (req, res) => {
|
|
const { page, pageSize, offset } = pageParams(req.query)
|
|
const { records, total } = await commandDao.listByDevice(req.params.device_id, { pageSize, offset })
|
|
res.json(ok({ records, total }))
|
|
}))
|
|
|
|
// --- Users ---
|
|
|
|
router.get('/users', requireAdmin, wrap(async (req, res) => {
|
|
const { page, pageSize, offset } = pageParams(req.query)
|
|
const { records, total } = await userDao.listAdmin({
|
|
keyword: req.query.keyword,
|
|
pageSize,
|
|
offset
|
|
})
|
|
res.json(ok({ records, total }))
|
|
}))
|
|
|
|
router.get('/users/:user_id', requireAdmin, wrap(async (req, res) => {
|
|
const result = await userDao.findByIdAdmin(req.params.user_id)
|
|
if (!result) return res.json(fail(1004, 'USER_NOT_FOUND'))
|
|
res.json(ok(result))
|
|
}))
|
|
|
|
router.post('/users/:user_id/unbind', requireAdmin, wrap(async (req, res) => {
|
|
const userId = req.params.user_id
|
|
const targetUser = await userDao.findById(userId)
|
|
if (!targetUser) return res.json(fail(1004, 'USER_NOT_FOUND'))
|
|
const deviceId = req.body.device_id ? String(req.body.device_id).trim() : null
|
|
const result = await bindingDao.unbindByUser(userId, deviceId)
|
|
await logDao.write({
|
|
admin_id: req.admin.admin_id,
|
|
user_id: userId,
|
|
action: 'admin_user_unbind',
|
|
detail: '后台解绑用户设备: user=' + userId + ', device=' + (deviceId || 'all'),
|
|
ip: req.ip
|
|
})
|
|
res.json(ok({ message: 'success', affected_rows: result.affectedRows || 0 }))
|
|
}))
|
|
|
|
router.post('/users/:user_id/deactivate', requireAdmin, wrap(async (req, res) => {
|
|
const userId = req.params.user_id
|
|
const targetUser = await userDao.findById(userId)
|
|
if (!targetUser) return res.json(fail(1004, 'USER_NOT_FOUND'))
|
|
const result = await userDao.deactivate(userId)
|
|
await logDao.write({
|
|
admin_id: req.admin.admin_id,
|
|
user_id: userId,
|
|
action: 'admin_user_deactivate',
|
|
detail: '后台注销用户: user=' + userId + ', unbound=' + result.unboundRows,
|
|
ip: req.ip
|
|
})
|
|
res.json(ok({ message: 'success', unbound_rows: result.unboundRows }))
|
|
}))
|
|
|
|
// --- Subscriptions ---
|
|
|
|
router.get('/subscriptions', requireAdmin, wrap(async (req, res) => {
|
|
const { page, pageSize, offset } = pageParams(req.query)
|
|
const { records, total } = await subscriptionDao.list({
|
|
tab: req.query.tab,
|
|
pageSize,
|
|
offset
|
|
})
|
|
const stats = await subscriptionDao.getStats()
|
|
res.json(ok({ records, total, stats }))
|
|
}))
|
|
|
|
router.post('/subscriptions', requireAdmin, wrap(async (req, res) => {
|
|
const userId = req.body.user_id
|
|
if (!userId) return res.json(fail(2001, 'user_id required'))
|
|
const targetUser = await userDao.findById(userId)
|
|
if (!targetUser) return res.json(fail(1004, 'user_not_found'))
|
|
await subscriptionDao.adminCreate(
|
|
userId,
|
|
req.body.plan || 'monthly',
|
|
req.body.amount || 0,
|
|
req.body.order_id || 'ADMIN' + Date.now(),
|
|
req.body.days || 30
|
|
)
|
|
res.json(ok({ message: 'success' }))
|
|
}))
|
|
|
|
router.post('/subscriptions/cancel', requireAdmin, wrap(async (req, res) => {
|
|
const subscriptionId = req.body.subscription_id
|
|
if (!subscriptionId) return res.json(fail(2001, 'subscription_id required'))
|
|
const result = await subscriptionDao.cancel(subscriptionId)
|
|
if (result.affectedRows === 0) return res.json(fail(2001, '未找到有效订阅'))
|
|
await logDao.write({ admin_id: req.admin.admin_id, action: 'subscription_cancel', detail: '取消订阅 #' + subscriptionId, ip: req.ip })
|
|
res.json(ok({ message: 'success' }))
|
|
}))
|
|
|
|
// --- Treatment Records ---
|
|
|
|
router.get('/records', requireAdmin, wrap(async (req, res) => {
|
|
const { page, pageSize, offset } = pageParams(req.query)
|
|
const { records, total } = await treatmentDao.listAdmin({
|
|
keyword: req.query.keyword,
|
|
userId: req.query.user_id,
|
|
dateFrom: req.query.date_from,
|
|
dateTo: req.query.date_to,
|
|
pageSize,
|
|
offset
|
|
})
|
|
res.json(ok({ records, total }))
|
|
}))
|
|
|
|
// --- Logs ---
|
|
|
|
router.get('/logs', requireAdmin, wrap(async (req, res) => {
|
|
const { page, pageSize, offset } = pageParams(req.query)
|
|
const { records, total } = await logDao.list({
|
|
type: req.query.type,
|
|
deviceId: req.query.device_id,
|
|
pageSize,
|
|
offset
|
|
})
|
|
res.json(ok({ records, total }))
|
|
}))
|
|
|
|
// --- Settings ---
|
|
|
|
router.get('/settings', requireAdmin, wrap(async (req, res) => {
|
|
const settings = await settingsDao.getAll()
|
|
res.json(ok(settings))
|
|
}))
|
|
|
|
router.post('/settings', requireAdmin, wrap(async (req, res) => {
|
|
const ALLOWED_KEYS = ['system_name', 'admin_email', 'timezone', 'monthly_price', 'yearly_price', 'trial_days', 'enable_register', 'enable_binding', 'enable_free_mode', 'enable_smart_mode', 'maintenance_mode']
|
|
for (const key of Object.keys(req.body || {})) {
|
|
if (!ALLOWED_KEYS.includes(key)) continue
|
|
await settingsDao.update(key, req.body[key])
|
|
}
|
|
invalidateCache()
|
|
res.json(ok({ message: 'success' }))
|
|
}))
|
|
|
|
module.exports = router
|