var db = require('../../common/db') var auth = require('../../common/auth') var log = require('../../common/log') var RESPONSE = require('../../common/response').RESPONSE var ERROR_CODES = require('../../common/response').ERROR_CODES var bcrypt = require('bcryptjs') exports.main_handler = async function (event) { try { var method = event.httpMethod || 'GET' var path = event.path || '' var pathParams = event.pathParameters || {} var body = parseBody(event) if (method === 'POST' && path === '/api/v1/admin/login') { return await adminLogin(body, event) } var admin = auth.extractAdmin(event) if (!admin) return RESPONSE.error(ERROR_CODES.INVALID_TOKEN.code, ERROR_CODES.INVALID_TOKEN.message) if (method === 'GET' && path === '/api/v1/admin/dashboard') { return await getDashboard() } if (method === 'GET' && path === '/api/v1/admin/devices' && !pathParams.device_id) { return await listDevices(body) } if (method === 'GET' && pathParams.device_id && !path.match(/\/command$/)) { return await getDeviceDetail(pathParams.device_id) } if (method === 'POST' && path.match(/\/command$/)) { return await sendDeviceCommand(pathParams.device_id, body) } if (method === 'GET' && path === '/api/v1/admin/users' && !pathParams.user_id) { return await listUsers(body) } if (method === 'GET' && pathParams.user_id && path.indexOf('/admin/users/') !== -1) { return await getUserDetail(pathParams.user_id) } if (method === 'GET' && path === '/api/v1/admin/subscriptions') { return await listSubscriptions(body) } if (method === 'POST' && path === '/api/v1/admin/subscriptions') { return await createSubscription(event, body) } if (method === 'GET' && path === '/api/v1/admin/logs') { return await getLogs(body) } return RESPONSE.error(ERROR_CODES.PARAM_ERROR.code, 'unknown route') } catch (err) { console.error('admin error:', err) return RESPONSE.error(ERROR_CODES.SERVER_ERROR.code, err.message) } } async function adminLogin(body, event) { if (!body.username || !body.password) { return RESPONSE.error(ERROR_CODES.PARAM_ERROR.code, 'missing username or password') } var admins = await db.query( 'SELECT id, username, password_hash, role, real_name FROM admins WHERE username = ? AND status = 1 AND deleted_at IS NULL LIMIT 1', [body.username] ) if (admins.length === 0) { return RESPONSE.error(ERROR_CODES.PERMISSION_DENIED.code, 'invalid credentials') } var admin = admins[0] var valid = bcrypt.compareSync(body.password, admin.password_hash) if (!valid) { return RESPONSE.error(ERROR_CODES.PERMISSION_DENIED.code, 'invalid credentials') } await db.update( 'UPDATE admins SET last_login_at = NOW() WHERE id = ?', [admin.id] ) var token = auth.generateAdminToken(admin.id, admin.username, admin.role) await log.writeLog({ operator_type: 1, operator_id: admin.id, target_type: 'admin', target_id: String(admin.id), action: 'login', ip_address: getClientIp(event) }) return RESPONSE.success({ token: token, admin_id: String(admin.id), username: admin.username, real_name: admin.real_name, role: admin.role }) } async function getDashboard() { var userCount = await db.query('SELECT COUNT(*) AS cnt FROM users WHERE deleted_at IS NULL') var deviceCount = await db.query('SELECT COUNT(*) AS cnt FROM devices WHERE deleted_at IS NULL') var activeDevices = await db.query('SELECT COUNT(*) AS cnt FROM devices WHERE status = 2 AND deleted_at IS NULL') var subCount = await db.query('SELECT COUNT(*) AS cnt FROM subscriptions WHERE status = 1 AND expired_at > NOW()') var trialCount = await db.query('SELECT COUNT(*) AS cnt FROM subscriptions WHERE type = 1') var treatmentCount = await db.query('SELECT COUNT(*) AS cnt FROM treatment_records') var todayTreatments = await db.query( "SELECT COUNT(*) AS cnt FROM treatment_records WHERE DATE(created_at) = CURDATE()" ) return RESPONSE.success({ user_count: userCount[0].cnt, device_count: deviceCount[0].cnt, active_device_count: activeDevices[0].cnt, subscription_count: subCount[0].cnt, trial_count: trialCount[0].cnt, treatment_count: treatmentCount[0].cnt, today_treatment_count: todayTreatments[0].cnt }) } async function listDevices(body) { var page = parseInt(body.page) || 1 var pageSize = parseInt(body.page_size) || 20 var offset = (page - 1) * pageSize var total = await db.query('SELECT COUNT(*) AS cnt FROM devices WHERE deleted_at IS NULL') var devices = await db.query( 'SELECT d.id, d.device_id, d.device_name, d.device_type, d.hw_version, d.fw_version, d.status, d.activated_at, d.created_at, ' + 'u.nickname AS bound_user, b.bound_at ' + 'FROM devices d ' + 'LEFT JOIN bindings b ON d.id = b.device_id AND b.status = 1 ' + 'LEFT JOIN users u ON b.user_id = u.id AND u.deleted_at IS NULL ' + 'WHERE d.deleted_at IS NULL ' + 'ORDER BY d.created_at DESC LIMIT ? OFFSET ?', [pageSize, offset] ) return RESPONSE.paginate(devices, total[0].cnt, page, pageSize) } async function getDeviceDetail(deviceId) { var devices = await db.query( 'SELECT * FROM devices WHERE device_id = ? AND deleted_at IS NULL LIMIT 1', [deviceId] ) if (devices.length === 0) { return RESPONSE.error(ERROR_CODES.DEVICE_NOT_FOUND.code, ERROR_CODES.DEVICE_NOT_FOUND.message) } var bindings = await db.query( 'SELECT b.*, u.nickname, u.avatar_url FROM bindings b JOIN users u ON b.user_id = u.id WHERE b.device_id = ? ORDER BY b.bound_at DESC', [devices[0].id] ) return RESPONSE.success(Object.assign({}, devices[0], { binding_history: bindings })) } async function sendDeviceCommand(deviceId, body) { var devices = await db.query( 'SELECT id, device_id, product_id, device_name FROM devices WHERE device_id = ? AND deleted_at IS NULL LIMIT 1', [deviceId] ) if (devices.length === 0) { return RESPONSE.error(ERROR_CODES.DEVICE_NOT_FOUND.code, ERROR_CODES.DEVICE_NOT_FOUND.message) } return RESPONSE.success({ message: 'command queued', device_id: deviceId, command: body }) } async function listUsers(body) { var page = parseInt(body.page) || 1 var pageSize = parseInt(body.page_size) || 20 var offset = (page - 1) * pageSize var total = await db.query('SELECT COUNT(*) AS cnt FROM users WHERE deleted_at IS NULL') var users = await db.query( 'SELECT u.id AS user_id, u.nickname, u.avatar_url, u.phone, u.status, u.created_at, ' + 'COUNT(DISTINCT b.id) AS device_count, ' + 'MAX(s.expired_at) AS subscription_expire ' + 'FROM users u ' + 'LEFT JOIN bindings b ON u.id = b.user_id AND b.status = 1 ' + 'LEFT JOIN subscriptions s ON u.id = s.user_id AND s.status = 1 AND s.expired_at > NOW() ' + 'WHERE u.deleted_at IS NULL ' + 'GROUP BY u.id ' + 'ORDER BY u.created_at DESC LIMIT ? OFFSET ?', [pageSize, offset] ) return RESPONSE.paginate(users, total[0].cnt, page, pageSize) } async function getUserDetail(userId) { var users = await db.query( 'SELECT id AS user_id, openid, nickname, avatar_url, phone, status, created_at FROM users WHERE id = ? AND deleted_at IS NULL LIMIT 1', [userId] ) if (users.length === 0) { return RESPONSE.error(ERROR_CODES.USER_NOT_FOUND.code, ERROR_CODES.USER_NOT_FOUND.message) } var bindings = await db.query( 'SELECT b.bound_at, d.device_id, d.device_name, d.fw_version FROM bindings b JOIN devices d ON b.device_id = d.id WHERE b.user_id = ? AND b.status = 1', [userId] ) var subs = await db.query( 'SELECT type, plan, status, started_at, expired_at, source FROM subscriptions WHERE user_id = ? ORDER BY created_at DESC LIMIT 5', [userId] ) var treatments = await db.query( 'SELECT session_id, regions, total_duration_ms, mode, started_at, ended_at FROM treatment_records WHERE user_id = ? ORDER BY started_at DESC LIMIT 5', [userId] ) return RESPONSE.success(Object.assign({}, users[0], { devices: bindings, subscriptions: subs, recent_treatments: treatments })) } async function listSubscriptions(body) { var page = parseInt(body.page) || 1 var pageSize = parseInt(body.page_size) || 20 var offset = (page - 1) * pageSize var total = await db.query('SELECT COUNT(*) AS cnt FROM subscriptions') var subs = await db.query( 'SELECT s.id, s.user_id, u.nickname, s.type, s.plan, s.status, s.trial_used, s.started_at, s.expired_at, s.source ' + 'FROM subscriptions s ' + 'JOIN users u ON s.user_id = u.id ' + 'ORDER BY s.created_at DESC LIMIT ? OFFSET ?', [pageSize, offset] ) return RESPONSE.paginate(subs, total[0].cnt, page, pageSize) } async function createSubscription(event, body) { if (!body.user_id || !body.plan || !body.days) { return RESPONSE.error(ERROR_CODES.PARAM_ERROR.code, 'missing user_id, plan or days') } await db.insert( 'INSERT INTO subscriptions (user_id, type, plan, status, started_at, expired_at, source) VALUES (?, 2, ?, 1, NOW(), DATE_ADD(NOW(), INTERVAL ? DAY), ?)', [body.user_id, body.plan, body.days, body.source || 'manual_create'] ) await log.writeLog({ operator_type: 1, operator_id: event.admin_id, target_type: 'subscription', target_id: String(body.user_id), action: 'create', remark: 'plan=' + body.plan + ',days=' + body.days }) return RESPONSE.success({ message: 'success' }) } async function getLogs(body) { var page = parseInt(body.page) || 1 var pageSize = parseInt(body.page_size) || 20 var offset = (page - 1) * pageSize var where = '1=1' var params = [] if (body.type) { where += ' AND target_type = ?' params.push(body.type) } if (body.device_id) { where += ' AND target_id = ?' params.push(body.device_id) } if (body.start_time) { where += ' AND created_at >= ?' params.push(body.start_time) } if (body.end_time) { where += ' AND created_at <= ?' params.push(body.end_time) } var total = await db.query('SELECT COUNT(*) AS cnt FROM operation_logs WHERE ' + where, params) var logs = await db.query( 'SELECT * FROM operation_logs WHERE ' + where + ' ORDER BY created_at DESC LIMIT ? OFFSET ?', params.concat([pageSize, offset]) ) return RESPONSE.paginate(logs, total[0].cnt, page, pageSize) } function parseBody(event) { if (event.body) { try { return JSON.parse(event.body) } catch (e) { return {} } } return event.queryStringParameters || {} } function getClientIp(event) { var headers = event.headers || {} return headers['x-forwarded-for'] || headers['X-Forwarded-For'] || '' }