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 uuid = require('uuid') 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/device/bind') { return await bindDevice(event, body) } if (method === 'POST' && path === '/api/v1/device/unbind') { return await unbindDevice(event, body) } if (method === 'GET' && path === '/api/v1/device/list') { return await listDevices(event) } if (method === 'GET' && pathParams.device_id) { return await getDeviceDetail(event, pathParams.device_id) } return RESPONSE.error(ERROR_CODES.PARAM_ERROR.code, 'unknown route') } catch (err) { console.error('device error:', err) return RESPONSE.error(ERROR_CODES.SERVER_ERROR.code, err.message) } } async function bindDevice(event, body) { var user = auth.extractUser(event) if (!user) return RESPONSE.error(ERROR_CODES.INVALID_TOKEN.code, ERROR_CODES.INVALID_TOKEN.message) if (!body.device_id) { return RESPONSE.error(ERROR_CODES.PARAM_ERROR.code, 'missing device_id') } var devices = await db.query( 'SELECT id, device_id, device_name, product_id, status FROM devices WHERE device_id = ? AND deleted_at IS NULL LIMIT 1', [body.device_id] ) if (devices.length === 0) { return RESPONSE.error(ERROR_CODES.DEVICE_NOT_FOUND.code, ERROR_CODES.DEVICE_NOT_FOUND.message) } var device = devices[0] var existing = await db.query( 'SELECT id FROM bindings WHERE device_id = ? AND status = 1', [device.id] ) if (existing.length > 0) { var existingUser = await db.query('SELECT id FROM users WHERE id = ? AND deleted_at IS NULL', [existing[0].user_id]) if (existingUser.length > 0 && String(existing[0].user_id) !== String(user.user_id)) { return RESPONSE.error(ERROR_CODES.PERMISSION_DENIED.code, 'device already bound') } if (existingUser.length > 0 && String(existing[0].user_id) === String(user.user_id)) { var bindToken = uuid.v4().replace(/-/g, '').substring(0, 16) await db.update( 'UPDATE bindings SET bind_token = ?, updated_at = NOW() WHERE id = ?', [bindToken, existing[0].id] ) return RESPONSE.success({ bind_token: bindToken, bind_expires: 300 }) } } var bindToken = uuid.v4().replace(/-/g, '').substring(0, 16) await db.insert( 'INSERT INTO bindings (user_id, device_id, bind_token, status, bound_at) VALUES (?, ?, ?, 1, NOW())', [user.user_id, device.id, bindToken] ) await db.update( 'UPDATE devices SET status = 2, activated_at = COALESCE(activated_at, NOW()) WHERE id = ?', [device.id] ) await grantTrialIfNeeded(user.user_id) await log.writeLog({ operator_type: 2, operator_id: user.user_id, target_type: 'device', target_id: device.device_id, action: 'bind', ip_address: getClientIp(event) }) return RESPONSE.success({ bind_token: bindToken, bind_expires: 300 }) } async function unbindDevice(event, body) { var user = auth.extractUser(event) if (!user) return RESPONSE.error(ERROR_CODES.INVALID_TOKEN.code, ERROR_CODES.INVALID_TOKEN.message) if (!body.device_id) { return RESPONSE.error(ERROR_CODES.PARAM_ERROR.code, 'missing device_id') } var devices = await db.query( 'SELECT id, device_id FROM devices WHERE device_id = ? AND deleted_at IS NULL LIMIT 1', [body.device_id] ) if (devices.length === 0) { return RESPONSE.error(ERROR_CODES.DEVICE_NOT_FOUND.code, ERROR_CODES.DEVICE_NOT_FOUND.message) } var result = await db.update( 'UPDATE bindings SET status = 2, unbound_at = NOW(), updated_at = NOW() WHERE user_id = ? AND device_id = ? AND status = 1', [user.user_id, devices[0].id] ) if (result.affectedRows === 0) { return RESPONSE.error(ERROR_CODES.DEVICE_NOT_BOUND.code, ERROR_CODES.DEVICE_NOT_BOUND.message) } await log.writeLog({ operator_type: 2, operator_id: user.user_id, target_type: 'device', target_id: body.device_id, action: 'unbind', ip_address: getClientIp(event) }) return RESPONSE.success({ message: 'success' }) } async function listDevices(event) { var user = auth.extractUser(event) if (!user) return RESPONSE.error(ERROR_CODES.INVALID_TOKEN.code, ERROR_CODES.INVALID_TOKEN.message) var bindings = await db.query( 'SELECT b.bound_at AS bind_time, b.bind_token, d.device_id, d.device_name, d.fw_version AS firmware_version, d.hw_version, d.status ' + 'FROM bindings b ' + 'JOIN devices d ON b.device_id = d.id ' + 'WHERE b.user_id = ? AND b.status = 1 AND d.deleted_at IS NULL ' + 'ORDER BY b.bound_at DESC', [user.user_id] ) return RESPONSE.success({ devices: bindings }) } async function getDeviceDetail(event, deviceId) { var user = auth.extractUser(event) if (!user) return RESPONSE.error(ERROR_CODES.INVALID_TOKEN.code, ERROR_CODES.INVALID_TOKEN.message) var bindings = await db.query( 'SELECT b.id, b.bound_at AS bind_time FROM bindings b ' + 'JOIN devices d ON b.device_id = d.id ' + 'WHERE b.user_id = ? AND b.status = 1 AND d.device_id = ? AND d.deleted_at IS NULL LIMIT 1', [user.user_id, deviceId] ) if (bindings.length === 0) { return RESPONSE.error(ERROR_CODES.DEVICE_NOT_FOUND.code, ERROR_CODES.DEVICE_NOT_FOUND.message) } var devices = await db.query( 'SELECT device_id, device_name, device_type, hw_version, fw_version AS firmware_version, status, activated_at FROM devices WHERE device_id = ? LIMIT 1', [deviceId] ) return RESPONSE.success(Object.assign({}, devices[0], { bind_time: bindings[0].bind_time })) } async function grantTrialIfNeeded(userId) { var existing = await db.query( 'SELECT id FROM subscriptions WHERE user_id = ? AND type = 1 LIMIT 1', [userId] ) if (existing.length > 0) return await db.insert( 'INSERT INTO subscriptions (user_id, type, plan, status, trial_used, started_at, expired_at, source) ' + 'VALUES (?, 1, NULL, 1, 1, NOW(), DATE_ADD(NOW(), INTERVAL 7 DAY), ?)', [userId, 'trial_grant'] ) } 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'] || '' }