2026-03-09 02:30:38 -05:00
const admin = require ( 'firebase-admin' ) ;
2025-06-28 19:11:25 -05:00
const { verify , hash } = require ( 'argon2' ) ;
const express = require ( 'express' ) ;
const { json } = require ( 'express' ) ;
const { Pool } = require ( 'pg' ) ;
require ( 'dotenv' ) . config ( ) ;
const jwt = require ( 'jsonwebtoken' ) ;
const http = require ( 'http' ) ;
const socketIo = require ( 'socket.io' ) ;
2025-12-31 17:10:14 -06:00
const connectDB = require ( './db' ) ;
2026-01-04 21:45:27 -06:00
const { initializeAgenda } = require ( './agenda' ) ; // Agenda setup (CHANGE THIS FOR BOSS)
2025-07-10 18:31:11 -05:00
const format = require ( 'pg-format' ) ;
2025-12-23 17:22:33 -06:00
const cronParser = require ( 'cron-parser' ) ;
2026-01-01 12:55:34 -06:00
const { ObjectId } = require ( 'mongodb' ) ;
2026-01-05 20:36:21 -06:00
const { RateLimiterMemory } = require ( 'rate-limiter-flexible' ) ;
2026-01-07 17:29:00 -06:00
const crypto = require ( 'crypto' ) ;
2026-01-08 17:28:46 -06:00
const { sendVerificationEmail , sendPasswordResetEmail , generateVerificationPageHTML } = require ( './mailer' ) ;
2026-01-05 20:36:21 -06:00
// HTTP rate limiter: 5 requests per second per IP
const httpRateLimiter = new RateLimiterMemory ( {
points : 5 ,
duration : 1 ,
} ) ;
2026-01-05 20:42:54 -06:00
// Auth endpoints rate limiter: 10 attempts per hour per IP
const authRateLimiter = new RateLimiterMemory ( {
points : 10 ,
duration : 3600 , // 1 hour in seconds
} ) ;
2026-01-07 17:29:00 -06:00
// Resend verification email rate limiter: 1 request per 20 seconds per IP
const resendVerificationRateLimiter = new RateLimiterMemory ( {
points : 1 ,
duration : 20 ,
} ) ;
2026-01-08 13:06:20 -06:00
// Password reset rate limiter: 10 attempts per 15 minutes per IP
const passwordResetRateLimiter = new RateLimiterMemory ( {
points : 10 ,
duration : 900 , // 15 minutes in seconds
} ) ;
2026-01-05 20:36:21 -06:00
// WebSocket connection rate limiter: 1 connection per second per IP
const wsConnectionRateLimiter = new RateLimiterMemory ( {
points : 5 ,
duration : 1 ,
} ) ;
// WebSocket message rate limiter: 5 messages per second per socket
const wsMessageRateLimiter = new RateLimiterMemory ( {
points : 5 ,
duration : 1 ,
} ) ;
2025-06-28 19:11:25 -05:00
const app = express ( ) ;
const port = 3000 ;
app . use ( json ( ) ) ;
2026-01-05 20:36:21 -06:00
// Rate limiting middleware for HTTP requests
app . use ( async ( req , res , next ) => {
const ip = req . ip || req . connection . remoteAddress ;
try {
await httpRateLimiter . consume ( ip ) ;
next ( ) ;
} catch ( rejRes ) {
res . status ( 429 ) . json ( { error : 'Too many requests' } ) ;
}
} ) ;
2025-06-28 19:11:25 -05:00
const pool = new Pool ( ) ;
const server = http . createServer ( app ) ;
2025-07-10 18:31:11 -05:00
const io = socketIo ( server , {
pingInterval : 15000 ,
pingTimeout : 10000 ,
} ) ;
let agenda ;
( async ( ) => {
2025-12-31 17:10:14 -06:00
// 1. Connect to MongoDB
2025-07-10 18:31:11 -05:00
await connectDB ( ) ;
agenda = await initializeAgenda ( 'mongodb://localhost:27017/myScheduledApp' , pool , io ) ;
} ) ( ) ;
2025-06-28 19:11:25 -05:00
2025-07-10 18:31:11 -05:00
( async ( ) => {
await pool . query ( "update user_tokens set connected=FALSE where connected=TRUE" ) ;
await pool . query ( "update device_tokens set connected=FALSE where connected=TRUE" ) ;
2026-01-08 13:06:20 -06:00
// Clear all expired password reset tokens on startup
await pool . query ( "DELETE FROM password_reset_tokens WHERE expires_at < NOW()" ) ;
console . log ( "Cleared expired password reset tokens" ) ;
2026-01-08 16:28:12 -06:00
// Clear all expired pending email changes on startup
await pool . query ( "DELETE FROM user_pending_emails WHERE expires_at < NOW()" ) ;
console . log ( "Cleared expired pending email changes" ) ;
2026-03-09 02:30:38 -05:00
// Add battery_soc column if this is the first deploy with battery support
await pool . query ( "ALTER TABLE devices ADD COLUMN IF NOT EXISTS battery_soc SMALLINT" ) ;
// Add fcm_token column for push notification delivery
await pool . query ( "ALTER TABLE users ADD COLUMN IF NOT EXISTS fcm_token TEXT" ) ;
// Initialise Firebase Admin SDK for push notifications
if ( process . env . FIREBASE _SERVICE _ACCOUNT _JSON ) {
admin . initializeApp ( {
credential : admin . credential . cert ( JSON . parse ( process . env . FIREBASE _SERVICE _ACCOUNT _JSON ) ) ,
} ) ;
} else {
console . warn ( "FIREBASE_SERVICE_ACCOUNT_JSON not set — push notifications disabled" ) ;
}
2025-07-10 18:31:11 -05:00
} ) ( ) ;
2025-06-28 19:11:25 -05:00
const JWT _SECRET = process . env . JWT _SECRET ;
const TOKEN _EXPIRY = '5d' ;
2026-01-05 20:36:21 -06:00
// Helper function to rate limit WebSocket messages
async function rateLimitSocketMessage ( socket , eventName ) {
try {
await wsMessageRateLimiter . consume ( socket . id ) ;
return true ;
} catch ( rejRes ) {
socket . emit ( 'error' , {
type : 'error' ,
code : 429 ,
message : 'Too many messages. Please slow down.' ,
event : eventName
} ) ;
return false ;
}
}
2025-07-10 18:31:11 -05:00
io . on ( 'connection' , async ( socket ) => {
2025-06-28 19:11:25 -05:00
console . log ( "periph connected" ) ;
2026-01-05 20:36:21 -06:00
// Rate limit WebSocket connections by IP
const ip = socket . handshake . address ;
try {
await wsConnectionRateLimiter . consume ( ip ) ;
} catch ( rejRes ) {
socket . emit ( 'error' , {
type : 'error' ,
code : 429 ,
message : 'Too many connection attempts. Please wait.'
} ) ;
socket . disconnect ( true ) ;
return ;
}
2025-07-10 18:31:11 -05:00
const token = socket . handshake . auth . token ? ? socket . handshake . headers [ 'authorization' ] ? . split ( ' ' ) [ 1 ] ;
try {
if ( ! token ) throw new Error ( "no token!" ) ;
const payload = jwt . verify ( token , JWT _SECRET ) ;
let table ;
let idCol ;
let id ;
if ( payload . type === "user" ) {
table = "user_tokens" ;
idCol = "user_id" ;
socket . user = true ;
id = payload . userId ;
}
else if ( payload . type === "peripheral" ) {
table = "device_tokens" ;
idCol = "device_id" ;
socket . user = false ;
id = payload . peripheralId ;
}
else {
throw new Error ( "Unauthorized" ) ;
}
2025-06-28 19:11:25 -05:00
2025-07-10 18:31:11 -05:00
const query = format ( ` update %I set connected=TRUE, socket= $ 1 where %I= $ 2 and token= $ 3 and connected=FALSE ` ,
table , idCol
) ;
2025-06-28 19:11:25 -05:00
2025-07-10 18:31:11 -05:00
const result = await pool . query ( query , [ socket . id , id , token ] ) ;
if ( result . rowCount != 1 ) {
const errorResponse = {
type : 'error' ,
code : 404 ,
message : 'Device not found or already connected'
} ;
socket . emit ( "error" , errorResponse ) ;
socket . disconnect ( true ) ;
}
else {
2025-12-24 18:39:48 -06:00
console . log ( "success - sending device state" ) ;
// For peripheral devices, send current device state
if ( payload . type === "peripheral" ) {
try {
2025-12-31 17:10:14 -06:00
// Get peripheral states for this device (device will report calibration status back)
2025-12-24 18:39:48 -06:00
const { rows : periphRows } = await pool . query (
2025-12-31 17:10:14 -06:00
"select last_pos, peripheral_number from peripherals where device_id=$1" ,
2025-12-24 18:39:48 -06:00
[ id ]
) ;
const successResponse = {
type : 'success' ,
code : 200 ,
message : 'Device authenticated' ,
deviceState : periphRows . map ( row => ( {
port : row . peripheral _number ,
2025-12-31 17:10:14 -06:00
lastPos : row . last _pos
2025-12-24 18:39:48 -06:00
} ) )
} ;
socket . emit ( "device_init" , successResponse ) ;
2025-12-28 14:06:52 -06:00
// Notify user app that device is now connected
const { rows : deviceUserRows } = await pool . query ( "select user_id from devices where id=$1" , [ id ] ) ;
if ( deviceUserRows . length === 1 ) {
const { rows : userRows } = await pool . query ( "select socket from user_tokens where user_id=$1 and connected=TRUE" , [ deviceUserRows [ 0 ] . user _id ] ) ;
if ( userRows . length === 1 && userRows [ 0 ] ) {
io . to ( userRows [ 0 ] . socket ) . emit ( "device_connected" , { deviceID : id } ) ;
}
}
2026-02-22 23:11:21 -06:00
// Check for pending calibrations (await_calib=true)
const { rows : pendingCalibRows } = await pool . query (
"SELECT id FROM peripherals WHERE device_id=$1 AND await_calib=TRUE" ,
[ id ]
) ;
if ( pendingCalibRows . length > 0 && deviceUserRows . length === 1 ) {
console . log ( ` Found ${ pendingCalibRows . length } pending calibration(s) for device ${ id } ` ) ;
// Notify user app that device is ready - app will trigger calibration
const { rows : userRows } = await pool . query (
"SELECT socket FROM user_tokens WHERE user_id=$1 AND connected=TRUE" ,
[ deviceUserRows [ 0 ] . user _id ]
) ;
if ( userRows . length === 1 && userRows [ 0 ] ) {
for ( const periph of pendingCalibRows ) {
io . to ( userRows [ 0 ] . socket ) . emit ( "calib_device_ready" , { periphID : periph . id } ) ;
}
}
}
2025-12-24 18:39:48 -06:00
} catch ( err ) {
console . error ( "Error fetching device state:" , err ) ;
socket . emit ( "device_init" , {
type : 'success' ,
code : 200 ,
message : 'Device authenticated' ,
deviceState : [ ]
} ) ;
}
} else {
// User connection
const successResponse = {
type : 'success' ,
code : 200 ,
message : 'User connected'
} ;
socket . emit ( "success" , successResponse ) ;
}
2025-07-10 18:31:11 -05:00
}
} catch ( error ) {
console . error ( 'Error during periph authentication:' , error ) ;
// Send an error message to the client
socket . emit ( 'error' , { type : 'error' , code : 500 } ) ;
// Disconnect the client
socket . disconnect ( true ) ;
}
2025-12-31 17:10:14 -06:00
// Device reports its calibration status after connection
socket . on ( 'report_calib_status' , async ( data ) => {
2026-01-05 20:36:21 -06:00
if ( ! await rateLimitSocketMessage ( socket , 'report_calib_status' ) ) return ;
2025-12-31 17:10:14 -06:00
console . log ( ` Device reporting calibration status: ${ socket . id } ` ) ;
console . log ( data ) ;
try {
const { rows } = await pool . query ( "select device_id from device_tokens where socket=$1 and connected=TRUE" , [ socket . id ] ) ;
if ( rows . length != 1 ) throw new Error ( "No device with that ID connected to Socket." ) ;
// Update calibration status in database based on device's actual state
if ( data . port && typeof data . calibrated === 'boolean' ) {
2026-03-18 01:41:36 -05:00
// When the device declares itself uncalibrated, also clear await_calib so
// any stale pending-calibration state is gone. The device is already connected
// via socket, so the next calib_start from the agenda job will land correctly.
const updateQuery = data . calibrated
? "update peripherals set calibrated=TRUE where device_id=$1 and peripheral_number=$2 returning id, user_id"
: "update peripherals set calibrated=FALSE, await_calib=FALSE where device_id=$1 and peripheral_number=$2 returning id, user_id" ;
const result = await pool . query ( updateQuery , [ rows [ 0 ] . device _id , data . port ] ) ;
2025-12-31 17:10:14 -06:00
console . log ( ` Updated port ${ data . port } calibrated status to ${ data . calibrated } ` ) ;
2026-02-22 23:11:21 -06:00
// Notify user app of calibration status change
if ( result . rowCount === 1 ) {
const { rows : userRows } = await pool . query (
"select socket from user_tokens where user_id=$1 and connected=TRUE" ,
[ result . rows [ 0 ] . user _id ]
) ;
if ( userRows . length === 1 && userRows [ 0 ] ) {
io . to ( userRows [ 0 ] . socket ) . emit ( "calib_status_changed" , {
periphID : result . rows [ 0 ] . id ,
calibrated : data . calibrated
} ) ;
}
}
2025-12-31 17:10:14 -06:00
}
} catch ( error ) {
console . error ( ` Error in report_calib_status: ` , error ) ;
}
} ) ;
// Device reports calibration error (motor stall, sensor failure, etc.)
socket . on ( 'device_calib_error' , async ( data ) => {
2026-01-05 20:36:21 -06:00
if ( ! await rateLimitSocketMessage ( socket , 'device_calib_error' ) ) return ;
2025-12-31 17:10:14 -06:00
console . log ( ` Device reporting calibration error: ${ socket . id } ` ) ;
console . log ( data ) ;
try {
const { rows } = await pool . query ( "select device_id from device_tokens where socket=$1 and connected=TRUE" , [ socket . id ] ) ;
if ( rows . length != 1 ) throw new Error ( "No device with that ID connected to Socket." ) ;
const result = await pool . query (
"update peripherals set await_calib=FALSE where device_id=$1 and peripheral_number=$2 returning id, user_id" ,
[ rows [ 0 ] . device _id , data . port ]
) ;
if ( result . rowCount != 1 ) throw new Error ( "No such peripheral in database" ) ;
// Notify user app about the error
const { rows : userRows } = await pool . query ( "select socket from user_tokens where user_id=$1 and connected=TRUE" , [ result . rows [ 0 ] . user _id ] ) ;
if ( userRows . length === 1 && userRows [ 0 ] ) {
io . to ( userRows [ 0 ] . socket ) . emit ( "calib_error" , {
periphID : result . rows [ 0 ] . id ,
message : data . message || "Device error during calibration"
} ) ;
}
console . log ( ` Calibration cancelled for port ${ data . port } due to device error ` ) ;
} catch ( error ) {
console . error ( ` Error in device_calib_error: ` , error ) ;
}
} ) ;
2025-12-28 14:06:52 -06:00
// Device acknowledges ready for stage 1 (tilt up)
socket . on ( 'calib_stage1_ready' , async ( data ) => {
2026-01-05 20:36:21 -06:00
if ( ! await rateLimitSocketMessage ( socket , 'calib_stage1_ready' ) ) return ;
2025-12-28 14:06:52 -06:00
console . log ( ` Device ready for stage 1 (tilt up): ${ socket . id } ` ) ;
console . log ( data ) ;
try {
const { rows } = await pool . query ( "select device_id from device_tokens where socket=$1 and connected=TRUE" , [ socket . id ] ) ;
if ( rows . length != 1 ) throw new Error ( "No device with that ID connected to Socket." ) ;
const result = await pool . query (
"select id, user_id from peripherals where device_id=$1 and peripheral_number=$2" ,
[ rows [ 0 ] . device _id , data . port ]
) ;
if ( result . rowCount != 1 ) throw new Error ( "No such peripheral in database" ) ;
const { rows : userRows } = await pool . query ( "select socket from user_tokens where user_id=$1" , [ result . rows [ 0 ] . user _id ] ) ;
if ( userRows . length == 1 && userRows [ 0 ] ) {
const userSocket = userRows [ 0 ] . socket ;
io . to ( userSocket ) . emit ( "calib_stage1_ready" , { periphID : result . rows [ 0 ] . id } ) ;
}
} catch ( error ) {
console . error ( ` Error in calib_stage1_ready: ` , error ) ;
}
} ) ;
// Device acknowledges ready for stage 2 (tilt down)
socket . on ( 'calib_stage2_ready' , async ( data ) => {
2026-01-05 20:36:21 -06:00
if ( ! await rateLimitSocketMessage ( socket , 'calib_stage2_ready' ) ) return ;
2025-12-28 14:06:52 -06:00
console . log ( ` Device ready for stage 2 (tilt down): ${ socket . id } ` ) ;
console . log ( data ) ;
try {
const { rows } = await pool . query ( "select device_id from device_tokens where socket=$1 and connected=TRUE" , [ socket . id ] ) ;
if ( rows . length != 1 ) throw new Error ( "No device with that ID connected to Socket." ) ;
const result = await pool . query (
"select id, user_id from peripherals where device_id=$1 and peripheral_number=$2" ,
[ rows [ 0 ] . device _id , data . port ]
) ;
if ( result . rowCount != 1 ) throw new Error ( "No such peripheral in database" ) ;
const { rows : userRows } = await pool . query ( "select socket from user_tokens where user_id=$1" , [ result . rows [ 0 ] . user _id ] ) ;
if ( userRows . length == 1 && userRows [ 0 ] ) {
const userSocket = userRows [ 0 ] . socket ;
io . to ( userSocket ) . emit ( "calib_stage2_ready" , { periphID : result . rows [ 0 ] . id } ) ;
}
} catch ( error ) {
console . error ( ` Error in calib_stage2_ready: ` , error ) ;
}
} ) ;
// User confirms stage 1 complete (tilt up done)
socket . on ( 'user_stage1_complete' , async ( data ) => {
2026-01-05 20:36:21 -06:00
if ( ! await rateLimitSocketMessage ( socket , 'user_stage1_complete' ) ) return ;
2025-12-28 14:06:52 -06:00
console . log ( ` User confirms stage 1 complete: ${ socket . id } ` ) ;
console . log ( data ) ;
try {
const { rows } = await pool . query ( "select socket from device_tokens where device_id=$1 and connected=TRUE" , [ data . deviceID ] ) ;
if ( rows . length != 1 ) {
// Device not connected - notify app
socket . emit ( "calib_error" , {
periphID : data . periphID ,
message : "Device disconnected during calibration"
} ) ;
// Reset peripheral state
await pool . query ( "update peripherals set await_calib=FALSE where id=$1" , [ data . periphID ] ) ;
return ;
}
const deviceSocket = rows [ 0 ] . socket ;
io . to ( deviceSocket ) . emit ( "user_stage1_complete" , { port : data . periphNum } ) ;
} catch ( error ) {
console . error ( ` Error in user_stage1_complete: ` , error ) ;
socket . emit ( "calib_error" , {
periphID : data . periphID ,
message : "Error during calibration"
} ) ;
}
} ) ;
// User confirms stage 2 complete (tilt down done)
socket . on ( 'user_stage2_complete' , async ( data ) => {
2026-01-05 20:36:21 -06:00
if ( ! await rateLimitSocketMessage ( socket , 'user_stage2_complete' ) ) return ;
2025-12-28 14:06:52 -06:00
console . log ( ` User confirms stage 2 complete: ${ socket . id } ` ) ;
console . log ( data ) ;
try {
const { rows } = await pool . query ( "select socket from device_tokens where device_id=$1 and connected=TRUE" , [ data . deviceID ] ) ;
if ( rows . length != 1 ) {
// Device not connected - notify app
socket . emit ( "calib_error" , {
periphID : data . periphID ,
message : "Device disconnected during calibration"
} ) ;
// Reset peripheral state
await pool . query ( "update peripherals set await_calib=FALSE where id=$1" , [ data . periphID ] ) ;
return ;
}
const deviceSocket = rows [ 0 ] . socket ;
io . to ( deviceSocket ) . emit ( "user_stage2_complete" , { port : data . periphNum } ) ;
} catch ( error ) {
console . error ( ` Error in user_stage2_complete: ` , error ) ;
socket . emit ( "calib_error" , {
periphID : data . periphID ,
message : "Error during calibration"
} ) ;
}
} ) ;
// Device acknowledges calibration complete
2025-07-10 18:31:11 -05:00
socket . on ( 'calib_done' , async ( data ) => {
2026-01-05 20:36:21 -06:00
if ( ! await rateLimitSocketMessage ( socket , 'calib_done' ) ) return ;
2025-07-10 18:31:11 -05:00
console . log ( ` Received 'calib_done' event from client ${ socket . id } : ` ) ;
console . log ( data ) ;
try {
const { rows } = await pool . query ( "select device_id from device_tokens where socket=$1 and connected=TRUE" , [ socket . id ] ) ;
if ( rows . length != 1 ) throw new Error ( "No device with that ID connected to Socket." ) ;
const result = await pool . query ( "update peripherals set await_calib=FALSE, calibrated=TRUE where device_id=$1 and peripheral_number=$2 returning id, user_id" ,
[ rows [ 0 ] . device _id , data . port ]
) ;
if ( result . rowCount != 1 ) throw new Error ( "No such peripheral in database" ) ;
const { rows : userRows } = await pool . query ( "select socket from user_tokens where user_id=$1" , [ result . rows [ 0 ] . user _id ] ) ;
if ( userRows . length == 1 ) {
if ( userRows [ 0 ] ) {
const userSocket = userRows [ 0 ] . socket ;
io . to ( userSocket ) . emit ( "calib_done" , { periphID : result . rows [ 0 ] . id } ) ;
}
2025-06-28 19:11:25 -05:00
}
2025-07-10 18:31:11 -05:00
else console . log ( "No App connected" ) ;
2026-02-22 23:11:21 -06:00
// Send acknowledgment back to device
socket . emit ( "calib_done_ack" , { port : data . port } ) ;
2025-06-28 19:11:25 -05:00
} catch ( error ) {
2025-07-10 18:31:11 -05:00
console . error ( ` Error processing job: ` , error ) ;
throw error ;
}
} ) ;
2025-06-28 19:11:25 -05:00
2025-07-10 18:31:11 -05:00
socket . on ( 'pos_hit' , async ( data ) => {
2026-01-05 20:36:21 -06:00
if ( ! await rateLimitSocketMessage ( socket , 'pos_hit' ) ) return ;
2025-07-10 18:31:11 -05:00
console . log ( ` Received 'pos_hit' event from client ${ socket . id } : ` ) ;
console . log ( data ) ;
const dateTime = new Date ( ) ;
try {
const { rows } = await pool . query ( "select device_id from device_tokens where socket=$1 and connected=TRUE" , [ socket . id ] ) ;
if ( rows . length != 1 ) throw new Error ( "No device with that ID connected to Socket." ) ;
const result = await pool . query ( "update peripherals set last_pos=$1, last_set=$2 where device_id=$3 and peripheral_number=$4 returning id, user_id" ,
[ data . pos , dateTime , rows [ 0 ] . device _id , data . port ]
) ;
if ( result . rowCount != 1 ) throw new Error ( "No such peripheral in database" ) ;
const { rows : userRows } = await pool . query ( "select socket from user_tokens where user_id=$1" , [ result . rows [ 0 ] . user _id ] ) ;
if ( userRows . length == 1 ) {
if ( userRows [ 0 ] ) {
const userSocket = userRows [ 0 ] . socket ;
io . to ( userSocket ) . emit ( "pos_hit" , { periphID : result . rows [ 0 ] . id } ) ;
}
}
else console . log ( "No App connected" ) ;
2025-06-28 19:11:25 -05:00
2025-07-10 18:31:11 -05:00
} catch ( error ) {
console . error ( ` Error processing job: ` , error ) ;
throw error ;
2025-06-28 19:11:25 -05:00
}
2025-07-10 18:31:11 -05:00
} ) ;
2026-02-22 23:11:21 -06:00
// User requests calibration
socket . on ( 'recalibrate' , async ( data ) => {
if ( ! await rateLimitSocketMessage ( socket , 'recalibrate' ) ) return ;
console . log ( ` User requesting calibration: ${ socket . id } ` ) ;
console . log ( data ) ;
try {
const { rows : userRows } = await pool . query ( "select user_id from user_tokens where socket=$1 and connected=TRUE" , [ socket . id ] ) ;
if ( userRows . length != 1 ) throw new Error ( "No user with that socket connected." ) ;
const userId = userRows [ 0 ] . user _id ;
const periphId = data . periphID ;
// Get peripheral info and verify ownership
const { rows : periphRows } = await pool . query (
"SELECT device_id, peripheral_number FROM peripherals WHERE id=$1 AND user_id=$2" ,
[ periphId , userId ]
) ;
if ( periphRows . length !== 1 ) {
socket . emit ( "calib_error" , {
periphID : periphId ,
message : "Peripheral not found"
} ) ;
return ;
}
const deviceId = periphRows [ 0 ] . device _id ;
const periphNum = periphRows [ 0 ] . peripheral _number ;
// Set await_calib=true
await pool . query (
"UPDATE peripherals SET await_calib=TRUE WHERE id=$1" ,
[ periphId ]
) ;
// Check if device is currently connected
const { rows : deviceRows } = await pool . query (
"SELECT socket FROM device_tokens WHERE device_id=$1 AND connected=TRUE" ,
[ deviceId ]
) ;
if ( deviceRows . length === 1 ) {
// Device is online - notify app to start calibration
socket . emit ( "calib_device_ready" , { periphID : periphId } ) ;
console . log ( ` Device online, notified app for peripheral ${ periphId } ` ) ;
} else {
console . log ( ` Device offline, waiting for wake for peripheral ${ periphId } ` ) ;
// Device offline - app will receive calib_device_ready when device connects
}
} catch ( error ) {
console . error ( ` Error in recalibrate: ` , error ) ;
socket . emit ( "calib_error" , {
periphID : data . periphID ,
message : "Error requesting calibration"
} ) ;
}
} ) ;
2025-07-10 18:31:11 -05:00
socket . on ( "disconnect" , async ( ) => {
if ( socket . user ) {
console . log ( "user disconnect" ) ;
await pool . query ( "update user_tokens set connected=FALSE where socket=$1" , [ socket . id ] ) ;
}
else {
console . log ( "device disconnect" ) ;
2025-12-28 14:06:52 -06:00
const { rows } = await pool . query (
"update device_tokens set connected=FALSE where socket=$1 returning device_id" ,
[ socket . id ]
) ;
// Notify user app that device disconnected
if ( rows . length === 1 ) {
const { rows : deviceRows } = await pool . query ( "select user_id from devices where id=$1" , [ rows [ 0 ] . device _id ] ) ;
if ( deviceRows . length === 1 ) {
const { rows : userRows } = await pool . query ( "select socket from user_tokens where user_id=$1 and connected=TRUE" , [ deviceRows [ 0 ] . user _id ] ) ;
if ( userRows . length === 1 && userRows [ 0 ] ) {
io . to ( userRows [ 0 ] . socket ) . emit ( "device_disconnected" , { deviceID : rows [ 0 ] . device _id } ) ;
}
}
}
2025-07-10 18:31:11 -05:00
}
} ) ;
} ) ;
2025-06-28 19:11:25 -05:00
async function createToken ( userId ) {
const token = jwt . sign ( { type : 'user' , userId } , JWT _SECRET , { expiresIn : TOKEN _EXPIRY } ) ;
await pool . query ( "delete from user_tokens where user_id=$1" , [ userId ] ) ;
await pool . query ( "insert into user_tokens (user_id, token) values ($1, $2)" , [ userId , token ] ) ;
return token ;
}
async function createPeripheralToken ( peripheralId ) {
const token = jwt . sign ( { type : 'peripheral' , peripheralId } , JWT _SECRET ) ;
await pool . query ( "insert into device_tokens (device_id, token) values ($1, $2)" , [ peripheralId , token ] ) ;
return token ;
}
async function createTempPeriphToken ( peripheralId ) {
const token = jwt . sign ( { type : 'peripheral' , peripheralId } , JWT _SECRET , { expiresIn : '2m' } ) ;
await pool . query ( "insert into device_tokens (device_id, token) values ($1, $2)" , [ peripheralId , token ] ) ;
return token ;
}
async function authenticateToken ( req , res , next ) {
const authHeader = req . headers [ 'authorization' ] ;
const token = authHeader ? . split ( ' ' ) [ 1 ] ;
if ( ! token ) return res . sendStatus ( 401 ) ;
try {
const payload = jwt . verify ( token , JWT _SECRET ) ;
if ( payload . type === 'user' ) {
const { rows } = await pool . query ( "select user_id from user_tokens where token=$1" , [ token ] ) ;
if ( rows . length != 1 ) throw new Error ( "Invalid/Expired Token" ) ;
req . user = payload . userId ; // make Id accessible in route handlers
}
else if ( payload . type === 'peripheral' ) {
const { rows } = await pool . query ( "select device_id from device_tokens where token=$1" , [ token ] ) ;
if ( rows . length != 1 ) throw new Error ( "Invalid/Expired Token" ) ;
req . peripheral = payload . peripheralId ;
2025-07-10 18:31:11 -05:00
}
else {
throw new Error ( "Invalid/Expired Token" ) ;
}
2025-06-28 19:11:25 -05:00
next ( ) ;
} catch {
return res . sendStatus ( 403 ) ; // invalid/expired token
}
}
app . get ( '/' , ( req , res ) => {
res . send ( 'Hello World!' ) ;
} ) ;
app . post ( '/login' , async ( req , res ) => {
const { email , password } = req . body ;
console . log ( 'login' ) ;
2026-01-05 20:42:54 -06:00
// Rate limit login attempts
const ip = req . ip || req . connection . remoteAddress ;
try {
await authRateLimiter . consume ( ip ) ;
} catch ( rejRes ) {
return res . status ( 429 ) . json ( {
error : 'Too many login attempts. Please try again later.' ,
retryAfter : Math . ceil ( rejRes . msBeforeNext / 1000 / 60 ) // minutes
} ) ;
}
2025-06-28 19:11:25 -05:00
if ( ! email || ! password ) return res . status ( 400 ) . json ( { error : 'email and password required' } ) ;
try {
2026-01-07 17:29:00 -06:00
const { rows } = await pool . query ( 'select id, password_hash_string, is_verified from users where email = $1' , [ email ] ) ;
2025-06-28 19:11:25 -05:00
if ( rows . length === 0 ) return res . status ( 401 ) . json ( { error : 'Invalid Credentials' } ) ;
const user = rows [ 0 ]
console . log ( 'user found' ) ;
const verified = await verify ( user . password _hash _string , password ) ;
if ( ! verified ) return res . status ( 401 ) . json ( { error : 'Invalid credentials' } ) ;
console . log ( "password correct" ) ;
2026-01-07 17:29:00 -06:00
if ( ! user . is _verified ) {
return res . status ( 403 ) . json ( {
error : 'Email not verified. Please check your email and verify your account before logging in.'
} ) ;
}
2025-06-28 19:11:25 -05:00
const token = await createToken ( user . id ) ; // token is now tied to ID
res . status ( 200 ) . json ( { token } ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
app . post ( '/create_user' , async ( req , res ) => {
console . log ( "got post req" ) ;
const { name , email , password } = req . body
2026-01-05 20:42:54 -06:00
// Rate limit account creation attempts
const ip = req . ip || req . connection . remoteAddress ;
try {
await authRateLimiter . consume ( ip ) ;
} catch ( rejRes ) {
return res . status ( 429 ) . json ( {
error : 'Too many account creation attempts. Please try again later.' ,
retryAfter : Math . ceil ( rejRes . msBeforeNext / 1000 / 60 ) // minutes
} ) ;
}
2026-01-08 11:51:31 -06:00
// Validate password length
if ( ! password || password . length < 8 ) {
return res . status ( 400 ) . json ( { error : 'Password must be at least 8 characters' } ) ;
}
2026-01-07 17:29:00 -06:00
// Compute hash and token once for reuse
const hashedPass = await hash ( password ) ;
const token = crypto . randomBytes ( 32 ) . toString ( 'hex' ) ;
2025-06-28 19:11:25 -05:00
try {
2026-01-07 17:29:00 -06:00
const newUser = await pool . query (
` insert into users (name, email, password_hash_string, verification_token, is_verified)
values ( nullif ( $1 , '' ) , $2 , $3 , $4 , false )
RETURNING id , email ` ,
[ name , email , hashedPass , token ]
) ;
2026-01-08 16:28:12 -06:00
// Schedule deletion of unverified user after 15 minutes
const expiryTime = new Date ( Date . now ( ) + 15 * 60 * 1000 ) ;
await agenda . schedule ( expiryTime , 'deleteUnverifiedUser' , { userId : newUser . rows [ 0 ] . id } ) ;
2026-01-07 17:29:00 -06:00
await sendVerificationEmail ( email , token , name ) ;
2025-06-28 19:11:25 -05:00
2026-01-07 17:29:00 -06:00
// Create temporary token for verification checking
const tempToken = await createToken ( newUser . rows [ 0 ] . id ) ;
2025-06-28 19:11:25 -05:00
2026-01-07 17:29:00 -06:00
res . status ( 201 ) . json ( { message : "User registered. Please check your email." , token : tempToken } ) ;
2025-06-28 19:11:25 -05:00
} catch ( err ) {
console . error ( err ) ;
if ( err . code === '23505' ) {
2026-01-07 17:29:00 -06:00
// Email already exists - check if verified
try {
const { rows } = await pool . query ( 'SELECT is_verified FROM users WHERE email = $1' , [ email ] ) ;
if ( rows . length === 1 && ! rows [ 0 ] . is _verified ) {
// User exists but not verified - replace their record (reuse hashedPass and token)
await pool . query (
` UPDATE users
SET name = nullif ( $1 , '' ) , password _hash _string = $2 , verification _token = $3
WHERE email = $4 ` ,
[ name , hashedPass , token , email ]
) ;
await sendVerificationEmail ( email , token , name ) ;
// Get user ID and create temp token
const { rows : userRows } = await pool . query ( 'SELECT id FROM users WHERE email = $1' , [ email ] ) ;
const tempToken = await createToken ( userRows [ 0 ] . id ) ;
return res . status ( 201 ) . json ( { message : "User registered. Please check your email." , token : tempToken } ) ;
}
} catch ( updateErr ) {
console . error ( 'Error updating unverified user:' , updateErr ) ;
}
// User is verified or something went wrong - email is truly in use
2025-06-28 19:11:25 -05:00
return res . status ( 409 ) . json ( { error : 'Email already in use' } ) ;
}
return res . sendStatus ( 500 ) ;
}
} ) ;
2026-01-07 17:29:00 -06:00
app . get ( '/verify-email' , async ( req , res ) => {
const { token } = req . query ;
if ( ! token ) {
2026-01-08 17:28:46 -06:00
return res . status ( 400 ) . send (
generateVerificationPageHTML (
'Missing Token' ,
'The verification link is incomplete. Please check your email and try again.' ,
false
)
) ;
2026-01-07 17:29:00 -06:00
}
try {
2026-01-08 17:28:46 -06:00
// Check if token exists at all
2026-01-07 17:29:00 -06:00
const result = await pool . query (
` SELECT * FROM users WHERE verification_token = $ 1 ` ,
[ token ]
) ;
if ( result . rows . length === 0 ) {
2026-01-08 17:28:46 -06:00
// Token not found - check if already verified
return res . send (
generateVerificationPageHTML (
2026-01-08 17:45:53 -06:00
'Already Verified or Expired' ,
'This verification link has already been used, or the link has expired. Check your app for updated status.' ,
2026-01-08 17:28:46 -06:00
true
)
) ;
2026-01-07 17:29:00 -06:00
}
const user = result . rows [ 0 ] ;
2026-01-08 17:28:46 -06:00
// Verify them and clear the token
2026-01-07 17:29:00 -06:00
await pool . query (
` UPDATE users
SET is _verified = true , verification _token = NULL
WHERE id = $1 ` ,
[ user . id ]
) ;
2026-01-08 16:28:12 -06:00
// Cancel any scheduled deletion job for this user
await agenda . cancel ( { name : 'deleteUnverifiedUser' , 'data.userId' : user . id } ) ;
2026-01-08 17:28:46 -06:00
res . send (
generateVerificationPageHTML (
'Email Verified!' ,
'Your email has been successfully verified. You can now log in to the app and start using BlindMaster.' ,
true
)
) ;
2026-01-07 17:29:00 -06:00
} catch ( err ) {
console . error ( err ) ;
2026-01-08 17:28:46 -06:00
res . status ( 500 ) . send (
generateVerificationPageHTML (
'Server Error' ,
'An unexpected error occurred. Please try again or contact support if the problem persists.' ,
false
)
) ;
2026-01-07 17:29:00 -06:00
}
} ) ;
app . get ( '/verification_status' , authenticateToken , async ( req , res ) => {
try {
const { rows } = await pool . query ( 'SELECT is_verified FROM users WHERE id = $1' , [ req . user ] ) ;
if ( rows . length === 0 ) return res . status ( 404 ) . json ( { error : 'User not found' } ) ;
res . status ( 200 ) . json ( { is _verified : rows [ 0 ] . is _verified } ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
app . post ( '/resend_verification' , authenticateToken , async ( req , res ) => {
const ip = req . ip || req . connection . remoteAddress ;
try {
await resendVerificationRateLimiter . consume ( ip ) ;
} catch ( rejRes ) {
return res . status ( 429 ) . json ( {
error : 'Please wait before requesting another verification email.' ,
retryAfter : Math . ceil ( rejRes . msBeforeNext / 1000 ) // seconds
} ) ;
}
try {
2026-01-08 16:28:12 -06:00
const { localHour } = req . body ;
2026-01-07 17:29:00 -06:00
const { rows } = await pool . query ( 'SELECT email, name, is_verified FROM users WHERE id = $1' , [ req . user ] ) ;
if ( rows . length === 0 ) return res . status ( 404 ) . json ( { error : 'User not found' } ) ;
const user = rows [ 0 ] ;
if ( user . is _verified ) {
return res . status ( 400 ) . json ( { error : 'Email already verified' } ) ;
}
const token = crypto . randomBytes ( 32 ) . toString ( 'hex' ) ;
await pool . query ( 'UPDATE users SET verification_token = $1 WHERE id = $2' , [ token , req . user ] ) ;
2026-01-08 16:28:12 -06:00
await sendVerificationEmail ( user . email , token , user . name , localHour ) ;
2026-01-07 17:29:00 -06:00
res . status ( 200 ) . json ( { message : 'Verification email sent' } ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
2025-06-28 19:11:25 -05:00
app . get ( '/verify' , authenticateToken , async ( req , res ) => {
try {
// Issue a new token to extend session
const newToken = await createToken ( req . user ) ;
res . status ( 200 ) . json ( { token : newToken } ) ;
} catch {
res . status ( 500 ) . json ( { error : 'server error' } ) ;
}
} ) ;
2026-01-08 10:08:12 -06:00
app . post ( '/logout' , authenticateToken , async ( req , res ) => {
try {
// Delete all tokens for this user
await pool . query ( "DELETE FROM user_tokens WHERE user_id=$1" , [ req . user ] ) ;
res . status ( 200 ) . json ( { message : 'Logged out successfully' } ) ;
} catch ( error ) {
console . error ( 'Error during logout:' , error ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
2026-01-08 11:12:46 -06:00
app . get ( '/account_info' , authenticateToken , async ( req , res ) => {
try {
const { rows } = await pool . query (
'SELECT name, email, created_at FROM users WHERE id = $1' ,
[ req . user ]
) ;
if ( rows . length === 0 ) {
return res . status ( 404 ) . json ( { error : 'User not found' } ) ;
}
res . status ( 200 ) . json ( {
name : rows [ 0 ] . name ,
email : rows [ 0 ] . email ,
created _at : rows [ 0 ] . created _at
} ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
2026-01-08 11:51:31 -06:00
app . post ( '/change_password' , authenticateToken , async ( req , res ) => {
try {
const { oldPassword , newPassword } = req . body ;
// Validate that both passwords are provided
if ( ! oldPassword || ! newPassword ) {
return res . status ( 400 ) . json ( { error : 'Old password and new password are required' } ) ;
}
// Validate new password length
if ( newPassword . length < 8 ) {
return res . status ( 400 ) . json ( { error : 'New password must be at least 8 characters' } ) ;
}
// Get current password hash from database
const { rows } = await pool . query (
'SELECT password_hash_string FROM users WHERE id = $1' ,
[ req . user ]
) ;
if ( rows . length === 0 ) {
return res . status ( 404 ) . json ( { error : 'User not found' } ) ;
}
// Verify old password
const verified = await verify ( rows [ 0 ] . password _hash _string , oldPassword ) ;
if ( ! verified ) {
return res . status ( 401 ) . json ( { error : 'Current password is incorrect' } ) ;
}
// Hash the new password
const hashedNewPassword = await hash ( newPassword ) ;
// Update password in database
await pool . query (
'UPDATE users SET password_hash_string = $1 WHERE id = $2' ,
[ hashedNewPassword , req . user ]
) ;
res . status ( 200 ) . json ( { message : 'Password changed successfully' } ) ;
} catch ( err ) {
2026-01-08 16:28:12 -06:00
console . error ( err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
2026-01-08 17:07:12 -06:00
app . delete ( '/delete_account' , authenticateToken , async ( req , res ) => {
try {
// Delete the user - CASCADE DELETE will handle related records
const result = await pool . query (
'DELETE FROM users WHERE id = $1' ,
[ req . user ]
) ;
if ( result . rowCount === 0 ) {
return res . status ( 404 ) . json ( { error : 'User not found' } ) ;
}
res . status ( 200 ) . json ( { message : 'Account deleted successfully' } ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
2026-01-08 16:28:12 -06:00
app . post ( '/request-email-change' , authenticateToken , async ( req , res ) => {
2026-01-08 21:31:42 -06:00
const { password , newEmail , localHour } = req . body ;
if ( ! password ) {
return res . status ( 400 ) . json ( { error : 'Password is required' } ) ;
}
2026-01-08 16:28:12 -06:00
if ( ! newEmail ) {
return res . status ( 400 ) . json ( { error : 'New email is required' } ) ;
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ ;
if ( ! emailRegex . test ( newEmail ) ) {
return res . status ( 400 ) . json ( { error : 'Invalid email format' } ) ;
}
try {
2026-01-08 21:31:42 -06:00
// Get user info and verify password
const { rows : userRows } = await pool . query (
'SELECT name, email, password_hash_string FROM users WHERE id = $1' ,
[ req . user ]
) ;
if ( userRows . length === 0 ) {
return res . status ( 404 ) . json ( { error : 'User not found' } ) ;
}
const user = userRows [ 0 ] ;
// Verify password
const passwordVerified = await verify ( user . password _hash _string , password ) ;
if ( ! passwordVerified ) {
return res . status ( 401 ) . json ( { error : 'Password is incorrect' } ) ;
}
2026-01-08 16:28:12 -06:00
// Check if new email is already in use
const { rows : emailCheck } = await pool . query ( 'SELECT id FROM users WHERE email = $1' , [ newEmail ] ) ;
if ( emailCheck . length > 0 ) {
return res . status ( 409 ) . json ( { error : 'Email already in use' } ) ;
}
const token = crypto . randomBytes ( 32 ) . toString ( 'hex' ) ;
const expiresAt = new Date ( Date . now ( ) + 15 * 60 * 1000 ) ; // 15 minutes
// Insert or update pending email with ON CONFLICT
await pool . query (
` INSERT INTO user_pending_emails (user_id, pending_email, token, expires_at)
VALUES ( $1 , $2 , $3 , $4 )
ON CONFLICT ( user _id )
DO UPDATE SET pending _email = $2 , token = $3 , expires _at = $4 ` ,
[ req . user , newEmail , token , expiresAt ]
) ;
// Cancel any existing job for this user
await agenda . cancel ( { name : 'deleteUserPendingEmail' , 'data.userId' : req . user } ) ;
// Schedule job to delete pending email after 15 minutes
await agenda . schedule ( expiresAt , 'deleteUserPendingEmail' , { userId : req . user } ) ;
// Send verification email
const { sendEmailChangeVerification } = require ( './mailer' ) ;
await sendEmailChangeVerification ( newEmail , token , user . name , user . email , localHour ) ;
res . status ( 200 ) . json ( { message : 'Verification email sent to new address' } ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
app . get ( '/verify-email-change' , async ( req , res ) => {
const { token } = req . query ;
if ( ! token ) {
2026-01-08 17:28:46 -06:00
return res . status ( 400 ) . send (
generateVerificationPageHTML (
'Missing Token' ,
'The verification link is incomplete. Please request a new email change from the app.' ,
false
)
) ;
2026-01-08 16:28:12 -06:00
}
try {
2026-01-08 17:28:46 -06:00
// First, check if token exists at all (expired or not)
const tokenCheck = await pool . query (
` SELECT user_id, pending_email, expires_at FROM user_pending_emails WHERE token = $ 1 ` ,
2026-01-08 16:28:12 -06:00
[ token ]
) ;
2026-01-08 17:28:46 -06:00
if ( tokenCheck . rows . length === 0 ) {
// Token doesn't exist - likely already processed
return res . send (
generateVerificationPageHTML (
2026-01-08 17:45:53 -06:00
'Already Verified or Expired' ,
'This verification link has already been used, or the link has expired. Check your app for updated status.' ,
2026-01-08 17:28:46 -06:00
true
)
) ;
2026-01-08 16:28:12 -06:00
}
2026-01-08 17:28:46 -06:00
const { user _id , pending _email , expires _at } = tokenCheck . rows [ 0 ] ;
// Check if expired
if ( new Date ( expires _at ) <= new Date ( ) ) {
return res . status ( 400 ) . send (
generateVerificationPageHTML (
'Link Expired' ,
'This verification link has expired. Please request a new email change from the app.' ,
false
)
) ;
}
2026-01-08 16:28:12 -06:00
// Check if new email is now taken (race condition check)
const { rows : emailCheck } = await pool . query ( 'SELECT id FROM users WHERE email = $1' , [ pending _email ] ) ;
if ( emailCheck . length > 0 && emailCheck [ 0 ] . id !== user _id ) {
await pool . query ( 'DELETE FROM user_pending_emails WHERE user_id = $1' , [ user _id ] ) ;
await agenda . cancel ( { name : 'deleteUserPendingEmail' , 'data.userId' : user _id } ) ;
2026-01-08 17:28:46 -06:00
return res . status ( 400 ) . send (
generateVerificationPageHTML (
'Email Unavailable' ,
'This email address is no longer available. Please try a different email address.' ,
false
)
) ;
2026-01-08 16:28:12 -06:00
}
// Update the user's email
await pool . query (
` UPDATE users SET email = $ 1 WHERE id = $ 2 ` ,
[ pending _email , user _id ]
) ;
// Delete the pending email record
await pool . query ( 'DELETE FROM user_pending_emails WHERE user_id = $1' , [ user _id ] ) ;
// Cancel the scheduled deletion job
await agenda . cancel ( { name : 'deleteUserPendingEmail' , 'data.userId' : user _id } ) ;
2026-01-08 17:28:46 -06:00
res . send (
generateVerificationPageHTML (
'Email Changed Successfully!' ,
'Your email has been updated successfully. You can now log in to the app with your new email address.' ,
true
)
) ;
2026-01-08 16:28:12 -06:00
} catch ( err ) {
console . error ( err ) ;
2026-01-08 17:28:46 -06:00
res . status ( 500 ) . send (
generateVerificationPageHTML (
'Server Error' ,
'An unexpected error occurred. Please try again or contact support if the problem persists.' ,
false
)
) ;
2026-01-08 16:28:12 -06:00
}
} ) ;
app . get ( '/pending-email-status' , authenticateToken , async ( req , res ) => {
try {
const { rows } = await pool . query (
'SELECT pending_email FROM user_pending_emails WHERE user_id = $1' ,
[ req . user ]
) ;
if ( rows . length === 0 ) {
return res . status ( 200 ) . json ( { hasPending : false } ) ;
}
res . status ( 200 ) . json ( {
hasPending : true ,
pendingEmail : rows [ 0 ] . pending _email
} ) ;
} catch ( err ) {
2026-01-08 11:51:31 -06:00
console . error ( err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
2026-01-08 13:06:20 -06:00
// Helper function to generate 6-character alphanumeric code
function generateResetCode ( ) {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' ; // Exclude confusing chars like 0, O, 1, I
let code = '' ;
for ( let i = 0 ; i < 6 ; i ++ ) {
code += chars . charAt ( crypto . randomInt ( chars . length ) ) ;
}
return code ;
}
app . post ( '/forgot-password' , async ( req , res ) => {
const ip = req . ip || req . connection . remoteAddress ;
try {
await resendVerificationRateLimiter . consume ( ip ) ;
} catch ( rejRes ) {
return res . status ( 429 ) . json ( {
error : 'Please wait before requesting another code.' ,
retryAfter : Math . ceil ( rejRes . msBeforeNext / 1000 ) , // seconds
remainingAttempts : 0
} ) ;
}
2026-01-08 15:18:15 -06:00
const { email , localHour } = req . body ;
2026-01-08 13:06:20 -06:00
if ( ! email ) {
return res . status ( 400 ) . json ( { error : 'Email is required' } ) ;
}
try {
// Check if user exists
const { rows } = await pool . query ( 'SELECT id, name FROM users WHERE email = $1' , [ email ] ) ;
// Always return success to prevent email enumeration
if ( rows . length === 0 ) {
return res . status ( 200 ) . json ( { message : 'If an account exists, a reset code has been sent' } ) ;
}
const user = rows [ 0 ] ;
const code = generateResetCode ( ) ;
const expiresAt = new Date ( Date . now ( ) + 15 * 60 * 1000 ) ; // 15 minutes
// Insert or update token with ON CONFLICT
await pool . query (
` INSERT INTO password_reset_tokens (email, token, expires_at)
VALUES ( $1 , $2 , $3 )
ON CONFLICT ( email )
DO UPDATE SET token = $2 , expires _at = $3 , created _at = CURRENT _TIMESTAMP ` ,
[ email , code , expiresAt ]
) ;
// Cancel any existing job for this email
await agenda . cancel ( { name : 'deletePasswordResetToken' , 'data.email' : email } ) ;
// Schedule job to delete token after 15 minutes
await agenda . schedule ( expiresAt , 'deletePasswordResetToken' , { email } ) ;
// Send email
2026-01-08 15:18:15 -06:00
await sendPasswordResetEmail ( email , code , user . name , localHour ) ;
2026-01-08 13:06:20 -06:00
res . status ( 200 ) . json ( { message : 'If an account exists, a reset code has been sent' } ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
app . post ( '/verify-reset-code' , async ( req , res ) => {
const { email , code } = req . body ;
if ( ! email || ! code ) {
return res . status ( 400 ) . json ( { error : 'Email and code are required' } ) ;
}
// Rate limit verification attempts
const ip = req . ip || req . connection . remoteAddress ;
try {
const rateLimiterRes = await passwordResetRateLimiter . consume ( ip ) ;
// Store remaining attempts for later use
res . locals . remainingAttempts = rateLimiterRes . remainingPoints ;
} catch ( rejRes ) {
return res . status ( 429 ) . json ( {
error : 'Too many verification attempts. Please try again later.' ,
retryAfter : Math . ceil ( rejRes . msBeforeNext / 1000 / 60 ) , // minutes
remainingAttempts : 0
} ) ;
}
try {
const { rows } = await pool . query (
'SELECT token, expires_at FROM password_reset_tokens WHERE email = $1' ,
[ email ]
) ;
if ( rows . length === 0 ) {
return res . status ( 401 ) . json ( { error : 'Invalid or expired code' } ) ;
}
const resetToken = rows [ 0 ] ;
if ( new Date ( ) > new Date ( resetToken . expires _at ) ) {
await pool . query ( 'DELETE FROM password_reset_tokens WHERE email = $1' , [ email ] ) ;
return res . status ( 401 ) . json ( { error : 'Code has expired' } ) ;
}
if ( resetToken . token !== code . toUpperCase ( ) ) {
return res . status ( 401 ) . json ( {
error : 'Invalid code' ,
remainingAttempts : res . locals . remainingAttempts || 0
} ) ;
}
res . status ( 200 ) . json ( { message : 'Code verified' } ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
app . post ( '/reset-password' , async ( req , res ) => {
const { email , code , newPassword } = req . body ;
if ( ! email || ! code || ! newPassword ) {
return res . status ( 400 ) . json ( { error : 'Email, code, and new password are required' } ) ;
}
if ( newPassword . length < 8 ) {
return res . status ( 400 ) . json ( { error : 'Password must be at least 8 characters' } ) ;
}
// Rate limit password reset attempts
const ip = req . ip || req . connection . remoteAddress ;
try {
await passwordResetRateLimiter . consume ( ip ) ;
} catch ( rejRes ) {
return res . status ( 429 ) . json ( {
error : 'Too many password reset attempts. Please try again later.' ,
retryAfter : Math . ceil ( rejRes . msBeforeNext / 1000 / 60 ) , // minutes
remainingAttempts : 0
} ) ;
}
try {
const { rows } = await pool . query (
'SELECT token, expires_at FROM password_reset_tokens WHERE email = $1' ,
[ email ]
) ;
if ( rows . length === 0 ) {
return res . status ( 401 ) . json ( { error : 'Invalid or expired code' } ) ;
}
const resetToken = rows [ 0 ] ;
if ( new Date ( ) > new Date ( resetToken . expires _at ) ) {
await pool . query ( 'DELETE FROM password_reset_tokens WHERE email = $1' , [ email ] ) ;
return res . status ( 401 ) . json ( { error : 'Code has expired' } ) ;
}
if ( resetToken . token !== code . toUpperCase ( ) ) {
return res . status ( 401 ) . json ( { error : 'Invalid code' } ) ;
}
// Hash new password
const hashedPassword = await hash ( newPassword ) ;
// Update password
await pool . query (
'UPDATE users SET password_hash_string = $1 WHERE email = $2' ,
[ hashedPassword , email ]
) ;
// Delete the used token
await pool . query ( 'DELETE FROM password_reset_tokens WHERE email = $1' , [ email ] ) ;
// Cancel scheduled deletion job
await agenda . cancel ( { name : 'deletePasswordResetToken' , 'data.email' : email } ) ;
res . status ( 200 ) . json ( { message : 'Password reset successfully' } ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
2025-06-28 19:11:25 -05:00
app . get ( '/device_list' , authenticateToken , async ( req , res ) => {
try {
console . log ( "device List request" ) ;
console . log ( req . user ) ;
2025-12-24 18:39:48 -06:00
const { rows } = await pool . query ( 'select id, device_name, max_ports from devices where user_id = $1' , [ req . user ] ) ;
2025-06-28 19:11:25 -05:00
const deviceNames = rows . map ( row => row . device _name ) ;
const deviceIds = rows . map ( row => row . id ) ;
2025-12-24 18:39:48 -06:00
const maxPorts = rows . map ( row => row . max _ports ) ;
res . status ( 200 ) . json ( { device _ids : deviceIds , devices : deviceNames , max _ports : maxPorts } ) ;
2025-06-28 19:11:25 -05:00
} catch {
res . status ( 500 ) . json ( { error : 'Internal Server Error' } ) ;
}
} ) ;
app . get ( '/device_name' , authenticateToken , async ( req , res ) => {
console . log ( "deviceName" ) ;
try {
const { deviceId } = req . query ;
2026-03-09 02:30:38 -05:00
const { rows } = await pool . query ( 'select device_name, max_ports, battery_soc from devices where id=$1 and user_id=$2' ,
2025-06-28 19:11:25 -05:00
[ deviceId , req . user ] ) ;
if ( rows . length != 1 ) return res . sendStatus ( 404 ) ;
2026-03-09 02:30:38 -05:00
res . status ( 200 ) . json ( { device _name : rows [ 0 ] . device _name , max _ports : rows [ 0 ] . max _ports , battery _soc : rows [ 0 ] . battery _soc } ) ;
2025-06-28 19:11:25 -05:00
} catch {
res . sendStatus ( 500 ) ;
}
} ) ;
app . get ( '/peripheral_list' , authenticateToken , async ( req , res ) => {
console . log ( "periph list" )
try {
const { deviceId } = req . query ;
const { rows } = await pool . query ( 'select id, peripheral_number, peripheral_name from peripherals where device_id=$1 and user_id=$2' ,
[ deviceId , req . user ] ) ;
const peripheralIds = rows . map ( row => row . id ) ;
const portNums = rows . map ( row => row . peripheral _number ) ;
const peripheralNames = rows . map ( row => row . peripheral _name ) ;
res . status ( 200 ) . json ( { peripheral _ids : peripheralIds , port _nums : portNums , peripheral _names : peripheralNames } ) ;
} catch {
res . sendStatus ( 500 ) ;
}
} )
app . post ( '/add_device' , authenticateToken , async ( req , res ) => {
try {
console . log ( "add device request" ) ;
console . log ( req . user ) ;
console . log ( req . peripheral ) ;
2025-12-24 18:39:48 -06:00
const { deviceName , maxPorts } = req . body ;
2025-06-28 19:11:25 -05:00
console . log ( deviceName ) ;
2025-12-24 18:39:48 -06:00
const ports = maxPorts || 4 ; // Default to 4 for multi-port devices
const { rows } = await pool . query ( "insert into devices (user_id, device_name, max_ports) values ($1, $2, $3) returning id" ,
[ req . user , deviceName , ports ]
2025-06-28 19:11:25 -05:00
) ; // finish token return based on device ID.
const deviceInitToken = await createTempPeriphToken ( rows [ 0 ] . id ) ;
2026-03-17 20:24:25 -05:00
console . log ( "complete" ) ;
2025-06-28 19:11:25 -05:00
res . status ( 201 ) . json ( { token : deviceInitToken } ) ;
} catch ( err ) {
console . log ( err ) ;
if ( err . code == '23505' ) {
return res . status ( 409 ) . json ( { error : 'Device Name in use' } ) ;
}
res . status ( 500 ) . json ( { error : 'Internal Server Error' } ) ;
}
} ) ;
app . post ( '/add_peripheral' , authenticateToken , async ( req , res ) => {
try {
const { device _id , port _num , peripheral _name } = req . body ;
await pool . query ( "insert into peripherals (device_id, peripheral_number, peripheral_name, user_id) values ($1, $2, $3, $4)" ,
[ device _id , port _num , peripheral _name , req . user ]
) ;
res . sendStatus ( 201 ) ;
} catch ( err ) {
if ( err . code == '23505' ) return res . sendStatus ( 409 ) ;
res . sendStatus ( 500 ) ;
}
} ) ;
app . get ( '/verify_device' , authenticateToken , async ( req , res ) => {
console . log ( "device verify" ) ;
try {
console . log ( req . peripheral ) ;
2025-12-24 18:39:48 -06:00
// Check if this is a single-port device
const { rows : deviceRows } = await pool . query ( "select max_ports, user_id from devices where id=$1" , [ req . peripheral ] ) ;
if ( deviceRows . length === 0 ) {
return res . status ( 404 ) . json ( { error : "Device not found" } ) ;
}
const maxPorts = deviceRows [ 0 ] . max _ports ;
const userId = deviceRows [ 0 ] . user _id ;
// For single-port devices, automatically create the peripheral if it doesn't exist
if ( maxPorts === 1 ) {
const { rows : periphRows } = await pool . query (
"select id from peripherals where device_id=$1" ,
[ req . peripheral ]
) ;
if ( periphRows . length === 0 ) {
// Create default peripheral for C6 device
await pool . query (
"insert into peripherals (device_id, peripheral_number, peripheral_name, user_id) values ($1, 1, $2, $3)" ,
[ req . peripheral , "Main Blind" , userId ]
) ;
console . log ( "Created default peripheral for single-port device" ) ;
}
}
2025-06-28 19:11:25 -05:00
await pool . query ( "delete from device_tokens where device_id=$1" , [ req . peripheral ] ) ;
const newToken = await createPeripheralToken ( req . peripheral ) ;
console . log ( "New Token" , newToken ) ;
res . json ( { token : newToken , id : req . peripheral } ) ;
2025-12-24 18:39:48 -06:00
} catch ( error ) {
console . error ( "Error in verify_device:" , error ) ;
2025-06-28 19:11:25 -05:00
res . status ( 500 ) . json ( { error : "server error" } ) ;
}
} ) ;
2026-02-22 23:11:21 -06:00
app . get ( '/position' , authenticateToken , async ( req , res ) => {
console . log ( "devicepos" ) ;
try {
const { rows } = await pool . query ( "select last_pos, peripheral_number, await_calib from peripherals where device_id=$1" ,
[ req . peripheral ] ) ;
if ( rows . length == 0 ) {
return res . sendStatus ( 404 ) ;
}
res . status ( 200 ) . json ( rows ) ;
} catch {
res . status ( 500 ) . json ( { error : "server error" } ) ;
}
} ) ;
app . post ( '/position' , authenticateToken , async ( req , res ) => {
console . log ( "devicepos" ) ;
try {
const { port , pos } = req . body ;
2026-03-18 01:07:21 -05:00
const { rows } = await pool . query ( "update peripherals set last_pos=$1 where device_id=$2 and peripheral_number=$3 returning await_calib, id, user_id" ,
2026-02-22 23:11:21 -06:00
[ pos , req . peripheral , port ] ) ;
if ( rows . length != 1 ) {
return res . sendStatus ( 404 ) ;
}
2026-03-18 01:07:21 -05:00
res . status ( 201 ) . json ( { await _calib : rows [ 0 ] . await _calib } ) ;
// Notify user app of device-reported position
const { rows : userRows } = await pool . query ( "select socket from user_tokens where user_id=$1 and connected=TRUE" , [ rows [ 0 ] . user _id ] ) ;
if ( userRows . length === 1 && userRows [ 0 ] ) {
io . to ( userRows [ 0 ] . socket ) . emit ( "device_pos_report" , { periphID : rows [ 0 ] . id , pos } ) ;
}
2026-02-22 23:11:21 -06:00
} catch {
res . status ( 500 ) . json ( { error : "server error" } ) ;
}
} ) ;
// Device reports its calibration status after connection
app . post ( '/report_calib_status' , authenticateToken , async ( req , res ) => {
console . log ( ` Device reporting calibration status: ${ req . peripheral } ` ) ;
try {
const { port , calibrated } = req . body ;
// Update calibration status in database based on device's actual state
if ( port && typeof calibrated === 'boolean' ) {
const result = await pool . query (
"update peripherals set calibrated=$1 where device_id=$2 and peripheral_number=$3 returning id, user_id" ,
[ calibrated , req . peripheral , port ]
) ;
console . log ( ` Updated port ${ port } calibrated status to ${ calibrated } ` ) ;
// Notify user app of calibration status change
if ( result . rowCount === 1 ) {
const { rows : userRows } = await pool . query (
"select socket from user_tokens where user_id=$1 and connected=TRUE" ,
[ result . rows [ 0 ] . user _id ]
) ;
if ( userRows . length === 1 && userRows [ 0 ] ) {
io . to ( userRows [ 0 ] . socket ) . emit ( "calib_status_changed" , {
periphID : result . rows [ 0 ] . id ,
calibrated : calibrated
} ) ;
}
}
res . sendStatus ( 201 ) ;
}
} catch ( error ) {
console . error ( ` Error in report_calib_status: ` , error ) ;
res . status ( 500 ) . json ( { success : false , message : 'Failed to update status' , error : error . message } ) ;
}
} ) ;
2025-06-28 19:11:25 -05:00
app . post ( '/manual_position_update' , authenticateToken , async ( req , res ) => {
console . log ( "setpos" ) ;
try {
2025-07-10 18:31:11 -05:00
const { periphId , periphNum , deviceId , newPos } = req . body ;
const changedPosList = [ { periphNum : periphNum , periphID : periphId , pos : newPos } ] ;
// Schedule the job to run immediately
const job = await agenda . now ( 'posChange' , { deviceID : deviceId , changedPosList : changedPosList , userID : req . user } ) ;
res . status ( 202 ) . json ( { // 202 Accepted, as processing happens in background
success : true ,
message : 'Request accepted for immediate processing.' ,
jobId : job . attrs . _id
} ) ;
} catch ( error ) {
console . error ( 'Error triggering immediate action:' , error ) ;
res . status ( 500 ) . json ( { success : false , message : 'Failed to trigger immediate action' , error : error . message } ) ;
2025-06-28 19:11:25 -05:00
}
} ) ;
app . post ( '/calib' , authenticateToken , async ( req , res ) => {
console . log ( "calibrate" ) ;
try {
const { periphId } = req . body ;
2025-07-10 18:31:11 -05:00
// Schedule the job to run immediately
const job = await agenda . now ( 'calib' , { periphID : periphId , userID : req . user } ) ;
res . status ( 202 ) . json ( { // 202 Accepted, as processing happens in background
success : true ,
message : 'Request accepted for immediate processing.' ,
jobId : job . attrs . _id
} ) ;
} catch ( err ) {
console . error ( 'Error triggering immediate action:' , err ) ;
2025-06-28 19:11:25 -05:00
res . sendStatus ( 500 ) ;
}
} )
app . post ( '/cancel_calib' , authenticateToken , async ( req , res ) => {
console . log ( "cancelCalib" ) ;
try {
const { periphId } = req . body ;
2025-07-10 18:31:11 -05:00
const job = await agenda . now ( 'cancel_calib' , { periphID : periphId , userID : req . user } ) ;
res . status ( 202 ) . json ( { // 202 Accepted, as processing happens in background
success : true ,
message : 'Request accepted for immediate processing.' ,
jobId : job . attrs . _id
} ) ;
2025-06-28 19:11:25 -05:00
} catch {
res . sendStatus ( 500 ) ;
}
} ) ;
app . get ( '/peripheral_status' , authenticateToken , async ( req , res ) => {
console . log ( "status" ) ;
try {
const { periphId } = req . query ;
const { rows } = await pool . query ( "select last_pos, last_set, calibrated, await_calib from peripherals where id=$1 and user_id=$2" ,
[ periphId , req . user ]
) ;
if ( rows . length != 1 ) return res . sendStatus ( 404 ) ;
res . status ( 200 ) . json ( { last _pos : rows [ 0 ] . last _pos , last _set : rows [ 0 ] . last _set ,
calibrated : rows [ 0 ] . calibrated , await _calib : rows [ 0 ] . await _calib } ) ;
} catch {
res . sendStatus ( 500 ) ;
}
} ) ;
app . get ( '/peripheral_name' , authenticateToken , async ( req , res ) => {
console . log ( "urmom" ) ;
try {
const { periphId } = req . query ;
const { rows } = await pool . query ( "select peripheral_name from peripherals where id=$1 and user_id=$2" ,
[ periphId , req . user ]
) ;
if ( rows . length != 1 ) return res . sendStatus ( 404 ) ;
res . status ( 200 ) . json ( { name : rows [ 0 ] . peripheral _name } ) ;
} catch {
res . sendStatus ( 500 ) ;
}
} )
2025-12-28 14:06:52 -06:00
app . get ( '/device_connection_status' , authenticateToken , async ( req , res ) => {
console . log ( "device connection status" ) ;
try {
const { deviceId } = req . query ;
// Verify device belongs to user
const { rows : deviceRows } = await pool . query ( "select id from devices where id=$1 and user_id=$2" ,
[ deviceId , req . user ]
) ;
if ( deviceRows . length != 1 ) return res . sendStatus ( 404 ) ;
// Check if device has an active socket connection
const { rows } = await pool . query ( "select connected from device_tokens where device_id=$1 and connected=TRUE" ,
[ deviceId ]
) ;
res . status ( 200 ) . json ( { connected : rows . length > 0 } ) ;
} catch {
res . sendStatus ( 500 ) ;
}
} )
2025-06-28 19:11:25 -05:00
app . post ( '/completed_calib' , authenticateToken , async ( req , res ) => {
console . log ( "calibration complete" ) ;
try {
const { portNum } = req . body ;
const result = await pool . query ( "update peripherals set calibrated=true, await_calib=false where device_id=$1 and peripheral_number=$2" ,
[ req . peripheral , portNum ]
) ;
if ( result . rowCount === 0 ) return res . sendStatus ( 404 ) ;
res . sendStatus ( 204 ) ;
} catch ( error ) {
console . error ( error ) ;
res . sendStatus ( 500 ) ;
}
} ) ;
app . post ( '/rename_device' , authenticateToken , async ( req , res ) => {
console . log ( "Hub rename" ) ;
try {
const { deviceId , newName } = req . body ;
const result = await pool . query ( "update devices set device_name=$1 where id=$2 and user_id=$3" , [ newName , deviceId , req . user ] ) ;
if ( result . rowCount === 0 ) return res . sendStatus ( 404 ) ;
res . sendStatus ( 204 ) ;
} catch ( err ) {
if ( err . code == '23505' ) return res . sendStatus ( 409 ) ;
console . error ( err ) ;
res . sendStatus ( 500 ) ;
}
} ) ;
app . post ( '/rename_peripheral' , authenticateToken , async ( req , res ) => {
console . log ( "Hub rename" ) ;
try {
const { periphId , newName } = req . body ;
const result = await pool . query ( "update peripherals set peripheral_name=$1 where id=$2 and user_id=$3" , [ newName , periphId , req . user ] ) ;
if ( result . rowCount === 0 ) return res . sendStatus ( 404 ) ;
res . sendStatus ( 204 ) ;
} catch ( err ) {
if ( err . code == '23505' ) return res . sendStatus ( 409 ) ;
console . error ( err ) ;
res . sendStatus ( 500 ) ;
}
} ) ;
2026-01-02 22:27:27 -06:00
app . post ( '/rename_group' , authenticateToken , async ( req , res ) => {
console . log ( "Rename group" ) ;
try {
const { groupId , newName } = req . body ;
const result = await pool . query ( "update groups set name=$1 where id=$2 and user_id=$3" , [ newName , groupId , req . user ] ) ;
if ( result . rowCount === 0 ) return res . status ( 404 ) . json ( { error : 'Group not found' } ) ;
res . sendStatus ( 204 ) ;
} catch ( err ) {
if ( err . code == '23505' ) return res . status ( 409 ) . json ( { error : 'Group name must be unique' } ) ;
console . error ( err ) ;
res . sendStatus ( 500 ) ;
}
} ) ;
app . post ( '/update_group' , authenticateToken , async ( req , res ) => {
console . log ( "Update group members" ) ;
try {
const { groupId , peripheral _ids } = req . body ;
if ( ! groupId || ! peripheral _ids || ! Array . isArray ( peripheral _ids ) ) {
return res . status ( 400 ) . json ( { error : 'Invalid request parameters' } ) ;
}
// Verify group belongs to user
const { rows : groupRows } = await pool . query (
'SELECT id FROM groups WHERE id=$1 AND user_id=$2' ,
[ groupId , req . user ]
) ;
if ( groupRows . length === 0 ) {
return res . status ( 404 ) . json ( { error : 'Group not found' } ) ;
}
// Verify all peripherals belong to user
const { rows : periphRows } = await pool . query (
'SELECT id FROM peripherals WHERE id = ANY($1::int[]) AND user_id=$2' ,
[ peripheral _ids , req . user ]
) ;
if ( periphRows . length !== peripheral _ids . length ) {
return res . status ( 403 ) . json ( { error : 'One or more peripherals do not belong to you' } ) ;
}
// Check if this exact peripheral set already exists in another group
const { rows : duplicateRows } = await pool . query (
` SELECT g.id, g.name
FROM groups g
JOIN (
SELECT group _id , array _agg ( peripheral _id ORDER BY peripheral _id ) as periph _set
FROM group _peripherals
GROUP BY group _id
) gp ON g . id = gp . group _id
WHERE gp . periph _set = $1 : : int [ ]
AND g . user _id = $2
AND g . id != $3 ` ,
[ peripheral _ids . sort ( ( a , b ) => a - b ) , req . user , groupId ]
) ;
if ( duplicateRows . length > 0 ) {
return res . status ( 409 ) . json ( {
error : 'This combination of blinds already exists in another group' ,
existing _group : duplicateRows [ 0 ] . name
} ) ;
}
// Delete existing group_peripherals entries
await pool . query ( 'DELETE FROM group_peripherals WHERE group_id=$1' , [ groupId ] ) ;
// Insert new group_peripherals entries
const insertValues = peripheral _ids . map ( pid => ` ( ${ groupId } , ${ pid } ) ` ) . join ( ',' ) ;
await pool . query ( ` INSERT INTO group_peripherals (group_id, peripheral_id) VALUES ${ insertValues } ` ) ;
res . status ( 200 ) . json ( { success : true , message : 'Group updated successfully' } ) ;
} catch ( error ) {
console . error ( 'Error updating group:' , error ) ;
res . status ( 500 ) . json ( { error : 'Internal Server Error' } ) ;
}
} ) ;
2025-06-28 19:11:25 -05:00
app . post ( '/delete_device' , authenticateToken , async ( req , res ) => {
console . log ( "delete device" ) ;
try {
const { deviceId } = req . body ;
const { rows } = await pool . query ( "delete from devices where user_id=$1 and id=$2 returning id" ,
[ req . user , deviceId ]
) ;
if ( rows . length != 1 ) {
return res . status ( 404 ) . json ( { error : 'Device not found' } ) ;
}
2025-12-24 18:39:48 -06:00
// Get socket ID before deleting tokens
const { rows : tokenRows } = await pool . query (
"select socket from device_tokens where device_id=$1 and connected=TRUE" ,
[ rows [ 0 ] . id ]
) ;
// Delete device tokens
2025-06-28 19:11:25 -05:00
await pool . query ( "delete from device_tokens where device_id=$1" , [ rows [ 0 ] . id ] ) ;
2025-12-24 18:39:48 -06:00
// Forcefully disconnect the Socket.IO connection if device is connected
if ( tokenRows . length > 0 ) {
tokenRows . forEach ( row => {
if ( row . socket ) {
const socket = io . sockets . sockets . get ( row . socket ) ;
if ( socket ) {
console . log ( ` Forcefully disconnecting device socket ${ row . socket } ` ) ;
socket . emit ( 'device_deleted' , { message : 'Device has been deleted from the account' } ) ;
socket . disconnect ( true ) ;
}
}
} ) ;
}
2025-06-28 19:11:25 -05:00
res . sendStatus ( 204 ) ;
2025-12-24 18:39:48 -06:00
} catch ( error ) {
console . error ( "Error deleting device:" , error ) ;
2025-06-28 19:11:25 -05:00
res . status ( 500 ) . json ( { error : "server error" } ) ;
}
} ) ;
app . post ( '/delete_peripheral' , authenticateToken , async ( req , res ) => {
console . log ( "delete peripheral" ) ;
try {
const { periphId } = req . body ;
const { rows } = await pool . query ( "delete from peripherals where user_id = $1 and id=$2 returning id" ,
[ req . user , periphId ]
) ;
if ( rows . length != 1 ) {
return res . status ( 404 ) . json ( { error : 'Device not found' } ) ;
}
res . sendStatus ( 204 ) ;
} catch {
res . sendStatus ( 500 ) ;
}
} )
2026-01-01 12:55:34 -06:00
// Helper function to create cron expression from time and days
function createCronExpression ( time , daysOfWeek ) {
const cronDays = daysOfWeek . join ( ',' ) ;
return ` ${ time . minute } ${ time . hour } * * ${ cronDays } ` ;
}
// Helper function to find and verify a schedule job belongs to the user
async function findUserScheduleJob ( jobId , userId ) {
const jobs = await agenda . jobs ( {
_id : new ObjectId ( jobId ) ,
'data.userID' : userId
} ) ;
return jobs . length === 1 ? jobs [ 0 ] : null ;
}
app . post ( '/add_schedule' , authenticateToken , async ( req , res ) => {
console . log ( "add schedule" ) ;
try {
const { periphId , periphNum , deviceId , newPos , time , daysOfWeek } = req . body ;
// Validate required fields
if ( ! periphId || periphNum === undefined || ! deviceId || newPos === undefined || ! time || ! daysOfWeek || daysOfWeek . length === 0 ) {
return res . status ( 400 ) . json ( { error : 'Missing required fields' } ) ;
}
// Create cron expression
const cronExpression = createCronExpression ( time , daysOfWeek ) ;
// Check for duplicate schedule
const existingJobs = await agenda . jobs ( {
'name' : 'posChangeScheduled' ,
'data.changedPosList.0.periphID' : periphId ,
'data.userID' : req . user
} ) ;
// Check if any existing job has the same cron expression (time + days)
const duplicate = existingJobs . find ( job => {
const jobCron = job . attrs . repeatInterval ;
return jobCron === cronExpression ;
} ) ;
if ( duplicate ) {
console . log ( "Duplicate schedule detected" ) ;
return res . status ( 409 ) . json ( {
success : false ,
message : 'A schedule with the same time and days already exists for this blind' ,
duplicate : true
} ) ;
}
const changedPosList = [ { periphNum : periphNum , periphID : periphId , pos : newPos } ] ;
// Schedule the recurring job
const job = await agenda . create ( 'posChangeScheduled' , {
deviceID : deviceId ,
changedPosList : changedPosList ,
userID : req . user
} ) ;
job . repeatEvery ( cronExpression , {
skipImmediate : true
} ) ;
await job . save ( ) ;
res . status ( 201 ) . json ( {
success : true ,
message : 'Schedule created successfully' ,
jobId : job . attrs . _id
} ) ;
} catch ( error ) {
console . error ( 'Error creating schedule:' , error ) ;
res . status ( 500 ) . json ( { success : false , message : 'Failed to create schedule' , error : error . message } ) ;
}
} ) ;
app . post ( '/delete_schedule' , authenticateToken , async ( req , res ) => {
console . log ( "delete schedule" ) ;
try {
const { jobId } = req . body ;
if ( ! jobId ) {
return res . status ( 400 ) . json ( { error : 'Missing jobId' } ) ;
}
// Find and verify the existing job belongs to the user
const existingJob = await findUserScheduleJob ( jobId , req . user ) ;
if ( ! existingJob ) {
return res . status ( 404 ) . json ( { error : 'Schedule not found' } ) ;
}
// Remove the job
await existingJob . remove ( ) ;
console . log ( "Schedule deleted successfully:" , jobId ) ;
res . status ( 200 ) . json ( {
success : true ,
message : 'Schedule deleted successfully'
} ) ;
} catch ( error ) {
console . error ( 'Error deleting schedule:' , error ) ;
res . status ( 500 ) . json ( { success : false , message : 'Failed to delete schedule' , error : error . message } ) ;
}
} ) ;
app . post ( '/update_schedule' , authenticateToken , async ( req , res ) => {
console . log ( "update schedule" ) ;
console . log ( "Request body:" , req . body ) ;
try {
const { jobId , periphId , periphNum , deviceId , newPos , time , daysOfWeek } = req . body ;
console . log ( "jobId type:" , typeof jobId , "value:" , jobId ) ;
// Validate required fields
if ( ! jobId || ! periphId || periphNum === undefined || ! deviceId || newPos === undefined || ! time || ! daysOfWeek || daysOfWeek . length === 0 ) {
console . log ( "Missing required fields" ) ;
return res . status ( 400 ) . json ( { error : 'Missing required fields' } ) ;
}
// Find and verify the existing job belongs to the user
console . log ( "Searching for job with _id:" , jobId , "and userID:" , req . user ) ;
const existingJob = await findUserScheduleJob ( jobId , req . user ) ;
console . log ( "Found job:" , existingJob ? 'yes' : 'no' ) ;
if ( ! existingJob ) {
console . log ( "Schedule not found" ) ;
return res . status ( 404 ) . json ( { error : 'Schedule not found' } ) ;
}
console . log ( "Removing old job..." ) ;
// Cancel the old job
await existingJob . remove ( ) ;
// Create cron expression
const cronExpression = createCronExpression ( time , daysOfWeek ) ;
const changedPosList = [ { periphNum : periphNum , periphID : periphId , pos : newPos } ] ;
console . log ( "Creating new job with cron:" , cronExpression ) ;
// Create new job with updated schedule
const job = await agenda . create ( 'posChangeScheduled' , {
deviceID : deviceId ,
changedPosList : changedPosList ,
userID : req . user
} ) ;
job . repeatEvery ( cronExpression , {
skipImmediate : true
} ) ;
await job . save ( ) ;
console . log ( "Job saved successfully with ID:" , job . attrs . _id ) ;
res . status ( 200 ) . json ( {
success : true ,
message : 'Schedule updated successfully' ,
jobId : job . attrs . _id
} ) ;
} catch ( error ) {
console . error ( 'Error updating schedule:' , error ) ;
res . status ( 500 ) . json ( { success : false , message : 'Failed to update schedule' , error : error . message } ) ;
}
} ) ;
2026-01-07 17:29:00 -06:00
server . listen ( port , '127.0.0.1' , ( ) => {
console . log ( ` Example app listening on 127.0.0.1: ${ port } ` ) ;
2025-12-23 17:22:33 -06:00
} ) ;
app . post ( '/periph_schedule_list' , authenticateToken , async ( req , res ) => {
try {
console . log ( "Schedule List request for user:" , req . user ) ;
const { periphId } = req . body ;
if ( ! periphId ) {
return res . status ( 400 ) . json ( { error : 'periphId is required in the request body.' } ) ;
}
// FIX 1: Assign the returned array directly to a variable (e.g., 'jobs')
const jobs = await agenda . jobs ( {
'name' : 'posChangeScheduled' ,
'data.changedPosList' : { $size : 1 } ,
'data.changedPosList.0.periphID' : periphId ,
'data.userID' : req . user
} ) ;
// FIX 2: Use .filter() and .map() to handle all cases cleanly.
// This creates a cleaner, more predictable transformation pipeline.
const details = jobs
// Step 1: Filter out any jobs that are not recurring.
. filter ( job => job . attrs . repeatInterval )
// Step 2: Map the remaining recurring jobs to our desired format.
. map ( job => {
const { repeatInterval , data , repeatTimezone } = job . attrs ;
try {
const interval = cronParser . parseExpression ( repeatInterval , {
tz : repeatTimezone || undefined
} ) ;
const fields = interval . fields ;
// Make sure to declare the variable
const parsedSchedule = {
minutes : fields . minute ,
hours : fields . hour ,
daysOfMonth : fields . dayOfMonth ,
months : fields . month ,
daysOfWeek : fields . dayOfWeek ,
} ;
return {
id : job . attrs . _id , // It's good practice to return the job ID
schedule : parsedSchedule ,
pos : data . changedPosList [ 0 ] . pos
} ;
} catch ( err ) {
// If parsing fails for a specific job, log it and filter it out.
console . error ( ` Could not parse " ${ repeatInterval } " for job ${ job . attrs . _id } . Skipping. ` ) ;
return null ; // Return null for now
}
} )
// Step 3: Filter out any nulls that resulted from a parsing error.
. filter ( detail => detail !== null ) ;
res . status ( 200 ) . json ( { scheduledUpdates : details } ) ;
} catch ( error ) { // FIX 3: Capture and log the actual error
console . error ( "Error in /periph_schedule_list:" , error ) ;
res . status ( 500 ) . json ( { error : 'Internal Server Error' } ) ;
}
} ) ;
2026-01-02 15:51:37 -06:00
app . get ( '/group_list' , authenticateToken , async ( req , res ) => {
console . log ( "group_list request for user:" , req . user ) ;
try {
// Get all groups for the user - no joins needed for menu view
const { rows } = await pool . query (
'SELECT id, name, created_at FROM groups WHERE user_id = $1 ORDER BY name' ,
[ req . user ]
) ;
res . status ( 200 ) . json ( { groups : rows } ) ;
} catch ( error ) {
console . error ( "Error in /group_list:" , error ) ;
res . status ( 500 ) . json ( { error : 'Internal Server Error' } ) ;
}
} ) ;
app . post ( '/add_group' , authenticateToken , async ( req , res ) => {
console . log ( "add_group request for user:" , req . user ) ;
try {
const { name , peripheral _ids } = req . body ;
// Validate input
if ( ! name || ! name . trim ( ) ) {
return res . status ( 400 ) . json ( { error : 'Group name is required' } ) ;
}
2026-01-05 16:59:00 -06:00
if ( ! peripheral _ids || ! Array . isArray ( peripheral _ids ) || peripheral _ids . length < 2 ) {
2026-01-02 15:51:37 -06:00
return res . status ( 400 ) . json ( { error : 'At least 2 peripherals are required' } ) ;
}
// Verify all peripherals belong to the user
const verifyQuery = `
SELECT id FROM peripherals
WHERE id = ANY ( $1 ) AND user _id = $2
` ;
const { rows : verifiedPeripherals } = await pool . query ( verifyQuery , [ peripheral _ids , req . user ] ) ;
if ( verifiedPeripherals . length !== peripheral _ids . length ) {
return res . status ( 403 ) . json ( { error : 'Invalid peripheral IDs' } ) ;
}
// Sort peripheral IDs for consistent comparison
const sortedPeripheralIds = [ ... peripheral _ids ] . sort ( ( a , b ) => a - b ) ;
// Check if a group with this exact set of peripherals already exists
const duplicateCheckQuery = `
SELECT g . id , g . name
FROM groups g
JOIN group _peripherals gp ON g . id = gp . group _id
WHERE g . user _id = $1
GROUP BY g . id
HAVING array _agg ( gp . peripheral _id ORDER BY gp . peripheral _id ) = $2 : : int [ ]
` ;
const { rows : duplicateGroups } = await pool . query ( duplicateCheckQuery , [ req . user , sortedPeripheralIds ] ) ;
if ( duplicateGroups . length > 0 ) {
return res . status ( 409 ) . json ( {
error : ` A group named " ${ duplicateGroups [ 0 ] . name } " already contains these exact blinds ` ,
duplicate : true ,
existing _group _name : duplicateGroups [ 0 ] . name
} ) ;
}
// Insert into groups table
const insertGroupQuery = `
INSERT INTO groups ( user _id , name )
VALUES ( $1 , $2 )
RETURNING id
` ;
const { rows : groupRows } = await pool . query ( insertGroupQuery , [ req . user , name . trim ( ) ] ) ;
const groupId = groupRows [ 0 ] . id ;
// Insert into group_peripherals table
const insertPeripheralsQuery = `
INSERT INTO group _peripherals ( group _id , peripheral _id )
VALUES $ { peripheral _ids . map ( ( _ , i ) => ` ( $ 1, $ ${ i + 2 } ) ` ) . join ( ', ' ) }
` ;
await pool . query ( insertPeripheralsQuery , [ groupId , ... peripheral _ids ] ) ;
res . status ( 201 ) . json ( {
success : true ,
group _id : groupId ,
message : 'Group created successfully'
} ) ;
} catch ( error ) {
console . error ( "Error in /add_group:" , error ) ;
// Handle unique constraint violation (duplicate group name)
if ( error . code === '23505' ) {
return res . status ( 409 ) . json ( { error : 'A group with this name already exists' } ) ;
}
res . status ( 500 ) . json ( { error : 'Internal Server Error' } ) ;
}
} ) ;
app . post ( '/delete_group' , authenticateToken , async ( req , res ) => {
console . log ( "delete_group request for user:" , req . user ) ;
try {
const { groupId } = req . body ;
if ( ! groupId ) {
return res . status ( 400 ) . json ( { error : 'Group ID is required' } ) ;
}
// Delete the group - CASCADE will automatically delete group_peripherals entries
const { rows } = await pool . query (
'DELETE FROM groups WHERE id = $1 AND user_id = $2 RETURNING id, name' ,
[ groupId , req . user ]
) ;
if ( rows . length === 0 ) {
return res . status ( 404 ) . json ( { error : 'Group not found' } ) ;
}
console . log ( ` Group " ${ rows [ 0 ] . name } " deleted successfully ` ) ;
res . sendStatus ( 204 ) ;
} catch ( error ) {
console . error ( "Error in /delete_group:" , error ) ;
res . status ( 500 ) . json ( { error : 'Internal Server Error' } ) ;
}
} ) ;
app . get ( '/group_details' , authenticateToken , async ( req , res ) => {
console . log ( "group_details request for user:" , req . user ) ;
try {
const { groupId } = req . query ;
if ( ! groupId ) {
return res . status ( 400 ) . json ( { error : 'Group ID is required' } ) ;
}
// Get group info and all peripherals with their device info
const { rows } = await pool . query ( `
SELECT
g . id as group _id ,
g . name as group _name ,
json _agg (
json _build _object (
'peripheral_id' , p . id ,
'peripheral_name' , p . peripheral _name ,
'peripheral_number' , p . peripheral _number ,
'device_id' , p . device _id ,
'last_pos' , p . last _pos ,
'calibrated' , p . calibrated
) ORDER BY p . peripheral _name
) as peripherals
FROM groups g
JOIN group _peripherals gp ON g . id = gp . group _id
JOIN peripherals p ON gp . peripheral _id = p . id
WHERE g . id = $1 AND g . user _id = $2
GROUP BY g . id , g . name
` , [groupId, req.user]);
if ( rows . length === 0 ) {
return res . status ( 404 ) . json ( { error : 'Group not found' } ) ;
}
res . status ( 200 ) . json ( rows [ 0 ] ) ;
} catch ( error ) {
console . error ( "Error in /group_details:" , error ) ;
res . status ( 500 ) . json ( { error : 'Internal Server Error' } ) ;
}
} ) ;
app . post ( '/group_position_update' , authenticateToken , async ( req , res ) => {
console . log ( "group_position_update request for user:" , req . user ) ;
try {
const { groupId , newPos } = req . body ;
if ( ! groupId || newPos === undefined ) {
return res . status ( 400 ) . json ( { error : 'Group ID and position are required' } ) ;
}
// Get all peripherals in the group with their device info
const { rows : peripherals } = await pool . query ( `
SELECT p . id , p . peripheral _number , p . device _id
FROM group _peripherals gp
JOIN peripherals p ON gp . peripheral _id = p . id
JOIN groups g ON gp . group _id = g . id
WHERE g . id = $1 AND g . user _id = $2
` , [groupId, req.user]);
if ( peripherals . length === 0 ) {
return res . status ( 404 ) . json ( { error : 'Group not found or empty' } ) ;
}
// Group peripherals by device to send commands efficiently
const deviceMap = new Map ( ) ;
peripherals . forEach ( p => {
if ( ! deviceMap . has ( p . device _id ) ) {
deviceMap . set ( p . device _id , [ ] ) ;
}
deviceMap . get ( p . device _id ) . push ( {
periphNum : p . peripheral _number ,
periphID : p . id ,
pos : newPos
} ) ;
} ) ;
// Schedule position change jobs for each device
const jobPromises = Array . from ( deviceMap . entries ( ) ) . map ( ( [ deviceId , changedPosList ] ) =>
agenda . now ( 'posChange' , { deviceID : deviceId , changedPosList , userID : req . user } )
) ;
await Promise . all ( jobPromises ) ;
res . status ( 202 ) . json ( {
success : true ,
message : 'Group position update accepted' ,
peripherals _updated : peripherals . length
} ) ;
} catch ( error ) {
console . error ( "Error in /group_position_update:" , error ) ;
res . status ( 500 ) . json ( { error : 'Internal Server Error' } ) ;
}
} ) ;
2026-01-02 22:27:27 -06:00
app . post ( '/add_group_schedule' , authenticateToken , async ( req , res ) => {
console . log ( "add_group_schedule" ) ;
try {
const { groupId , newPos , time , daysOfWeek } = req . body ;
if ( ! groupId || newPos === undefined || ! time || ! daysOfWeek || ! Array . isArray ( daysOfWeek ) ) {
return res . status ( 400 ) . json ( { error : 'Missing required fields' } ) ;
}
// Verify group belongs to user
const { rows : groupRows } = await pool . query (
'SELECT id FROM groups WHERE id=$1 AND user_id=$2' ,
[ groupId , req . user ]
) ;
if ( groupRows . length === 0 ) {
return res . status ( 404 ) . json ( { error : 'Group not found' } ) ;
}
// Check for duplicate schedule
const cronExpression = createCronExpression ( time , daysOfWeek ) ;
const existingJobs = await agenda . jobs ( {
name : 'groupPosChangeScheduled' ,
'data.groupID' : groupId ,
'data.userID' : req . user
} ) ;
for ( const existingJob of existingJobs ) {
if ( existingJob . attrs . repeatInterval === cronExpression && existingJob . attrs . data . newPos === newPos ) {
return res . status ( 409 ) . json ( { error : 'Duplicate schedule already exists' } ) ;
}
}
// Create the job
const job = await agenda . create ( 'groupPosChangeScheduled' , {
groupID : groupId ,
newPos ,
userID : req . user
} ) ;
job . repeatEvery ( cronExpression , {
timezone : 'America/New_York' ,
skipImmediate : true
} ) ;
await job . save ( ) ;
res . status ( 201 ) . json ( {
success : true ,
message : 'Group schedule created successfully' ,
jobId : job . attrs . _id
} ) ;
} catch ( error ) {
console . error ( 'Error creating group schedule:' , error ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
app . post ( '/delete_group_schedule' , authenticateToken , async ( req , res ) => {
console . log ( "delete_group_schedule" ) ;
try {
const { jobId } = req . body ;
if ( ! jobId ) {
return res . status ( 400 ) . json ( { error : 'Missing jobId' } ) ;
}
const job = await findUserScheduleJob ( jobId , req . user ) ;
if ( ! job ) {
return res . status ( 404 ) . json ( { error : 'Schedule not found' } ) ;
}
await job . remove ( ) ;
res . status ( 200 ) . json ( { success : true , message : 'Group schedule deleted successfully' } ) ;
} catch ( error ) {
console . error ( 'Error deleting group schedule:' , error ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
app . post ( '/update_group_schedule' , authenticateToken , async ( req , res ) => {
console . log ( "update_group_schedule" ) ;
try {
const { jobId , newPos , time , daysOfWeek } = req . body ;
if ( ! jobId || newPos === undefined || ! time || ! daysOfWeek ) {
return res . status ( 400 ) . json ( { error : 'Missing required fields' } ) ;
}
const job = await findUserScheduleJob ( jobId , req . user ) ;
if ( ! job ) {
return res . status ( 404 ) . json ( { error : 'Schedule not found' } ) ;
}
const groupId = job . attrs . data . groupID ;
const cronExpression = createCronExpression ( time , daysOfWeek ) ;
// Check for duplicates excluding current job
const existingJobs = await agenda . jobs ( {
name : 'groupPosChangeScheduled' ,
'data.groupID' : groupId ,
'data.userID' : req . user ,
_id : { $ne : new ObjectId ( jobId ) }
} ) ;
for ( const existingJob of existingJobs ) {
if ( existingJob . attrs . repeatInterval === cronExpression && existingJob . attrs . data . newPos === newPos ) {
return res . status ( 409 ) . json ( { error : 'Duplicate schedule already exists' } ) ;
}
}
// Update job
job . attrs . data . newPos = newPos ;
job . repeatEvery ( cronExpression , {
timezone : 'America/New_York' ,
skipImmediate : true
} ) ;
await job . save ( ) ;
res . status ( 200 ) . json ( {
success : true ,
message : 'Group schedule updated successfully'
} ) ;
} catch ( error ) {
console . error ( 'Error updating group schedule:' , error ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;
2026-03-09 02:30:38 -05:00
// Store/update the FCM token for the authenticated user
app . post ( '/register_fcm_token' , authenticateToken , async ( req , res ) => {
const { token } = req . body ;
if ( ! token || typeof token !== 'string' ) return res . sendStatus ( 400 ) ;
try {
await pool . query ( "UPDATE users SET fcm_token=$1 WHERE id=$2" , [ token , req . user ] ) ;
res . sendStatus ( 204 ) ;
} catch {
res . sendStatus ( 500 ) ;
}
} ) ;
// ── Battery endpoints (device-authenticated only) ─────────────────────────────
// Periodic SOC sync — called by wakeTimer every 60 s
app . post ( '/battery_update' , authenticateToken , async ( req , res ) => {
if ( ! req . peripheral ) return res . sendStatus ( 403 ) ;
try {
const { soc } = req . body ;
if ( soc == null || soc < 0 || soc > 100 ) return res . sendStatus ( 400 ) ;
await pool . query ( "UPDATE devices SET battery_soc=$1 WHERE id=$2" , [ soc , req . peripheral ] ) ;
res . sendStatus ( 204 ) ;
} catch {
res . sendStatus ( 500 ) ;
}
} ) ;
// Alert events — fires on threshold crossings and critical conditions.
// Notifies the user via Socket.IO if connected, and via FCM push if the app is in the background.
app . post ( '/battery_alert' , authenticateToken , async ( req , res ) => {
if ( ! req . peripheral ) return res . sendStatus ( 403 ) ;
const validTypes = [ 'overvoltage' , 'critical_low' , 'low_voltage_warning' , 'low_20' , 'low_10' ] ;
try {
const { type , soc } = req . body ;
if ( ! validTypes . includes ( type ) || soc == null ) return res . sendStatus ( 400 ) ;
await pool . query ( "UPDATE devices SET battery_soc=$1 WHERE id=$2" , [ soc , req . peripheral ] ) ;
// Socket.IO — real-time delivery when app is open
const { rows } = await pool . query (
"SELECT ut.socket FROM user_tokens ut JOIN devices d ON d.user_id = ut.user_id WHERE d.id=$1 AND ut.connected=TRUE" ,
[ req . peripheral ]
) ;
if ( rows . length === 1 ) {
io . to ( rows [ 0 ] . socket ) . emit ( "battery_alert" , { deviceId : req . peripheral , type , soc } ) ;
}
// FCM — background push for persistent alerts (not transient voltage dips)
const fcmPushTypes = [ 'overvoltage' , 'critical_low' , 'low_20' , 'low_10' ] ;
if ( fcmPushTypes . includes ( type ) && admin . apps . length > 0 ) {
const { rows : fcmRows } = await pool . query (
"SELECT u.fcm_token FROM users u JOIN devices d ON d.user_id=u.id WHERE d.id=$1 AND u.fcm_token IS NOT NULL" ,
[ req . peripheral ]
) ;
if ( fcmRows . length === 1 ) {
const fcmContent = {
overvoltage : { title : 'Battery Fault' , body : 'Overvoltage detected. Please check your charger.' } ,
critical _low : { title : 'Battery Critical' , body : ` Battery at ${ soc } % — device is shutting down. ` } ,
low _20 : { title : 'Battery Low' , body : ` Battery at ${ soc } %. Consider charging soon. ` } ,
low _10 : { title : 'Battery Very Low' , body : ` Battery at ${ soc } % — charge now. ` } ,
} ;
const { title , body } = fcmContent [ type ] ;
await admin . messaging ( ) . send ( {
token : fcmRows [ 0 ] . fcm _token ,
notification : { title , body } ,
data : { type , soc : String ( soc ) , deviceId : String ( req . peripheral ) } ,
} ) . catch ( err => console . error ( 'FCM send failed:' , err . message ) ) ;
}
}
res . sendStatus ( 204 ) ;
} catch {
res . sendStatus ( 500 ) ;
}
} ) ;
2026-01-02 22:27:27 -06:00
app . post ( '/group_schedule_list' , authenticateToken , async ( req , res ) => {
try {
const { groupId } = req . body ;
if ( ! groupId ) {
return res . status ( 400 ) . json ( { error : 'Missing groupId' } ) ;
}
// Verify group belongs to user
const { rows : groupRows } = await pool . query (
'SELECT id FROM groups WHERE id=$1 AND user_id=$2' ,
[ groupId , req . user ]
) ;
if ( groupRows . length === 0 ) {
return res . status ( 404 ) . json ( { error : 'Group not found' } ) ;
}
const jobs = await agenda . jobs ( {
name : 'groupPosChangeScheduled' ,
'data.groupID' : groupId ,
'data.userID' : req . user
} ) ;
const scheduledUpdates = jobs . map ( job => {
const interval = cronParser . parseExpression ( job . attrs . repeatInterval ) ;
const schedule = {
minutes : interval . fields . minute ,
hours : interval . fields . hour ,
daysOfWeek : interval . fields . dayOfWeek
} ;
return {
id : job . attrs . _id ,
pos : job . attrs . data . newPos ,
schedule
} ;
} ) ;
res . status ( 200 ) . json ( { scheduledUpdates } ) ;
} catch ( error ) {
console . error ( 'Error fetching group schedules:' , error ) ;
res . status ( 500 ) . json ( { error : 'Internal server error' } ) ;
}
} ) ;