const express = require('express'); const router = express.Router(); const jwt = require('jsonwebtoken'); const { supabase } = require('../config/supabase'); const Stripe = require('stripe'); const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); // JWT-based admin auth middleware // Verifies Bearer token and checks user role is 'admin' const adminAuth = async (req, res, next) => { try { // Check for Bearer token const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Unauthorized' }); } const token = authHeader.split(' ')[1]; // Verify JWT token let decoded; try { decoded = jwt.verify(token, process.env.JWT_SECRET); } catch (err) { return res.status(401).json({ error: 'Invalid token' }); } // Get user from database and check role const { data: user, error } = await supabase .from('users') .select('id, email, role') .eq('id', decoded.userId) .single(); if (error || !user) { return res.status(401).json({ error: 'User not found' }); } // Check if user has admin role if (user.role !== 'admin') { return res.status(403).json({ error: 'Access denied. Admin only.' }); } // Attach user to request req.user = user; next(); } catch (error) { return res.status(401).json({ error: 'Unauthorized' }); } }; // Apply admin auth to all routes router.use(adminAuth); // ============ DASHBOARD STATS ============ /** * GET /api/admin/stats * Dashboard statistics */ router.get('/stats', async (req, res) => { try { // Get order counts by status const { data: orders, error: ordersError } = await supabase .from('orders') .select('status, created_at'); if (ordersError) throw ordersError; const today = new Date(); today.setHours(0, 0, 0, 0); const stats = { orders: { total: orders.length, today: orders.filter(o => new Date(o.created_at) >= today).length, byStatus: { paid: orders.filter(o => o.status === 'paid').length, preparing: orders.filter(o => o.status === 'preparing').length, shipped: orders.filter(o => o.status === 'shipped').length, delivered: orders.filter(o => o.status === 'delivered').length, installed: orders.filter(o => o.status === 'installed').length } } }; // Subscription stats are derived from Stripe (no local DB storage). // Get beneficiary stats const { data: beneficiaries } = await supabase .from('beneficiaries') .select('status'); if (beneficiaries) { stats.beneficiaries = { total: beneficiaries.length, active: beneficiaries.filter(b => b.status === 'active').length, awaitingSensors: beneficiaries.filter(b => b.status === 'awaiting_sensors').length }; } res.json(stats); } catch (error) { res.status(500).json({ error: error.message }); } }); // ============ ORDERS ============ /** * POST /api/admin/orders * Create a new order manually */ router.post('/orders', async (req, res) => { try { const { user_id, customer_email, customer_name, shipping_address, total_amount, status, notes } = req.body; // Generate order number const orderNumber = `ORD-${Date.now().toString(36).toUpperCase()}`; const { data: order, error } = await supabase .from('orders') .insert({ user_id: user_id || null, customer_email: customer_email || null, customer_name: customer_name || null, shipping_address, total_amount: total_amount || 29900, status: status || 'paid', order_number: orderNumber, notes: notes || null, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }) .select() .single(); if (error) throw error; res.json(order); } catch (error) { res.status(500).json({ error: error.message }); } }); /** * GET /api/admin/orders * List orders with pagination (optimized) */ router.get('/orders', async (req, res) => { try { const { status } = req.query; const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 50; const offset = (page - 1) * limit; let query = supabase .from('orders') .select('*', { count: 'exact' }) .order('created_at', { ascending: false }) .range(offset, offset + limit - 1); if (status) { query = query.eq('status', status); } const { data: orders, error, count: totalCount } = await query; if (error) throw error; // Get all user IDs from orders const userIds = [...new Set((orders || []).map(o => o.user_id).filter(Boolean))]; // Single query to get all users let usersMap = {}; if (userIds.length > 0) { const { data: users } = await supabase .from('users') .select('id, email') .in('id', userIds); usersMap = (users || []).reduce((acc, u) => { acc[u.id] = u; return acc; }, {}); } // Combine data const result = (orders || []).map(order => ({ ...order, user: usersMap[order.user_id] || null })); res.json({ orders: result, pagination: { page, limit, total: totalCount || 0, pages: Math.ceil((totalCount || 0) / limit) } }); } catch (error) { res.status(500).json({ error: error.message }); } }); /** * GET /api/admin/orders/:id * Get single order details */ router.get('/orders/:id', async (req, res) => { try { const { data: order, error } = await supabase .from('orders') .select('*') .eq('id', req.params.id) .single(); if (error) throw error; if (!order) return res.status(404).json({ error: 'Order not found' }); // Get related data let beneficiary = null; if (order.beneficiary_id) { const { data: b } = await supabase .from('users') .select('*') .eq('id', order.beneficiary_id) .single(); beneficiary = b; } res.json({ ...order, beneficiary }); } catch (error) { res.status(500).json({ error: error.message }); } }); /** * PATCH /api/admin/orders/:id * Update order status, add tracking, etc. */ router.patch('/orders/:id', async (req, res) => { try { const { status, tracking_number, carrier, estimated_delivery } = req.body; const updates = { updated_at: new Date().toISOString() }; if (status) { updates.status = status; // Set timestamps based on status if (status === 'shipped') { updates.shipped_at = new Date().toISOString(); } else if (status === 'delivered') { updates.delivered_at = new Date().toISOString(); } } if (tracking_number) updates.tracking_number = tracking_number; if (carrier) updates.carrier = carrier; if (estimated_delivery) updates.estimated_delivery = estimated_delivery; const { data, error } = await supabase .from('orders') .update(updates) .eq('id', req.params.id) .select() .single(); if (error) throw error; // TODO: Send email notification when status changes if (status === 'shipped') { } else if (status === 'delivered') { } res.json(data); } catch (error) { res.status(500).json({ error: error.message }); } }); // ============ USERS ============ /** * GET /api/admin/users * List users with pagination and relationships (optimized) */ router.get('/users', async (req, res) => { try { const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 50; const offset = (page - 1) * limit; const filter = req.query.filter; // 'beneficiaries', 'caretakers', 'admins' // Get all user_access relationships first (for filtering) const { data: accessRecords } = await supabase .from('user_access') .select('beneficiary_id, accessor_id, role'); // Create lookup sets const beneficiaryIds = new Set((accessRecords || []).map(a => a.beneficiary_id)); const caretakerIds = new Set((accessRecords || []).map(a => a.accessor_id)); // Build query let query = supabase .from('users') .select('*', { count: 'exact' }) .order('created_at', { ascending: false }); // Apply filter if needed if (filter === 'admins') { query = query.eq('role', 'admin'); } // Get users const { data: allUsers, error, count: totalCount } = await query; if (error) throw error; // Apply client-side filtering for beneficiaries/caretakers let filteredUsers = allUsers || []; if (filter === 'beneficiaries') { filteredUsers = filteredUsers.filter(u => beneficiaryIds.has(u.id)); } else if (filter === 'caretakers') { filteredUsers = filteredUsers.filter(u => caretakerIds.has(u.id)); } // Apply pagination after filtering const paginatedUsers = filteredUsers.slice(offset, offset + limit); // Create user email lookup const userEmailMap = (allUsers || []).reduce((acc, u) => { acc[u.id] = u.email; return acc; }, {}); // Enrich paginated users with relationship info const enrichedUsers = paginatedUsers.map(user => { const watches = (accessRecords || []) .filter(a => a.accessor_id === user.id) .map(a => ({ ...a, beneficiary_email: userEmailMap[a.beneficiary_id] })); const watchedBy = (accessRecords || []) .filter(a => a.beneficiary_id === user.id) .map(a => ({ ...a, accessor_email: userEmailMap[a.accessor_id] })); return { ...user, watches, watched_by: watchedBy, is_beneficiary: watchedBy.length > 0, is_caretaker: watches.length > 0, }; }); res.json({ users: enrichedUsers, pagination: { page, limit, total: filteredUsers.length, pages: Math.ceil(filteredUsers.length / limit) } }); } catch (error) { res.status(500).json({ error: error.message }); } }); /** * GET /api/admin/users/:id * Get user with full details: relationships, devices, orders, subscriptions */ router.get('/users/:id', async (req, res) => { try { const { data: user, error: userError } = await supabase .from('users') .select('*') .eq('id', req.params.id) .single(); if (userError) throw userError; if (!user) return res.status(404).json({ error: 'User not found' }); // Get all users for email lookup const { data: allUsers } = await supabase .from('users') .select('id, email, first_name, last_name'); // Get relationships: who this user watches const { data: watchesAccess } = await supabase .from('user_access') .select('*') .eq('accessor_id', req.params.id); const watches = (watchesAccess || []).map(a => { const beneficiary = allUsers?.find(u => u.id === a.beneficiary_id); return { ...a, user: beneficiary }; }); // Get relationships: who watches this user const { data: watchedByAccess } = await supabase .from('user_access') .select('*') .eq('beneficiary_id', req.params.id); const watched_by = (watchedByAccess || []).map(a => { const accessor = allUsers?.find(u => u.id === a.accessor_id); return { ...a, user: accessor }; }); // Get deployments where user is owner const { data: deployments } = await supabase .from('deployments') .select('*') .eq('owner_user_id', req.params.id); // Get devices from user's deployments let devices = []; if (deployments && deployments.length > 0) { const deploymentIds = deployments.map(d => d.deployment_id); const { data: devs } = await supabase .from('devices') .select('*') .in('well_id', deploymentIds); devices = devs || []; } // Get orders const { data: orders } = await supabase .from('orders') .select('*') .eq('user_id', req.params.id) .order('created_at', { ascending: false }); // Get subscriptions const subscriptions = []; res.json({ ...user, watches, watched_by, is_beneficiary: watched_by.length > 0, is_caretaker: watches.length > 0, deployments: deployments || [], devices, orders: orders || [], subscriptions }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ============ SUBSCRIPTIONS ============ /** * GET /api/admin/subscriptions * List all subscriptions with pagination (optimized) */ router.get('/subscriptions', async (req, res) => { try { res.json({ subscriptions: [], pagination: { page: 1, limit: 0, total: 0, pages: 0 } }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ============ REFUNDS ============ /** * POST /api/admin/refund * Process a refund via Stripe */ router.post('/refund', async (req, res) => { try { const { orderId, amount, reason } = req.body; // Get order const { data: order, error: orderError } = await supabase .from('orders') .select('*') .eq('id', orderId) .single(); if (orderError || !order) { return res.status(404).json({ error: 'Order not found' }); } // Get payment intent from Stripe session const session = await stripe.checkout.sessions.retrieve(order.stripe_session_id); const paymentIntentId = session.payment_intent; // Create refund const refund = await stripe.refunds.create({ payment_intent: paymentIntentId, amount: amount ? amount : undefined, // Full refund if no amount specified reason: reason || 'requested_by_customer' }); // Update order status await supabase .from('orders') .update({ status: 'canceled', updated_at: new Date().toISOString() }) .eq('id', orderId); res.json({ success: true, refund: { id: refund.id, amount: refund.amount / 100, status: refund.status } }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ============ DEPLOYMENTS ============ /** * GET /api/admin/deployments * List deployments with pagination (optimized - single queries) */ router.get('/deployments', async (req, res) => { try { const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 50; const offset = (page - 1) * limit; // Get deployments with count const { data: deployments, error, count: totalCount } = await supabase .from('deployments') .select('*', { count: 'exact' }) .order('deployment_id', { ascending: false }) .range(offset, offset + limit - 1); if (error) throw error; // Get all owner IDs const ownerIds = [...new Set((deployments || []).map(d => d.owner_user_id).filter(Boolean))]; // Single query to get all owners let ownersMap = {}; if (ownerIds.length > 0) { const { data: owners } = await supabase .from('users') .select('id, email, first_name, last_name') .in('id', ownerIds); ownersMap = (owners || []).reduce((acc, u) => { acc[u.id] = u; return acc; }, {}); } // Get deployment IDs const deploymentIds = (deployments || []).map(d => d.deployment_id); // Single query to get device counts let deviceCounts = {}; if (deploymentIds.length > 0) { const { data: devices } = await supabase .from('devices') .select('well_id') .in('well_id', deploymentIds); // Count devices per deployment (devices || []).forEach(d => { deviceCounts[d.well_id] = (deviceCounts[d.well_id] || 0) + 1; }); } // Combine data const result = (deployments || []).map(dep => ({ ...dep, owner: ownersMap[dep.owner_user_id] || null, device_count: deviceCounts[dep.deployment_id] || 0 })); res.json({ deployments: result, pagination: { page, limit, total: totalCount || 0, pages: Math.ceil((totalCount || 0) / limit) } }); } catch (error) { res.status(500).json({ error: error.message }); } }); /** * GET /api/admin/deployments/:id * Get single deployment with devices, users with access, events */ router.get('/deployments/:id', async (req, res) => { try { const { data: deployment, error } = await supabase .from('deployments') .select('*') .eq('deployment_id', req.params.id) .single(); if (error) throw error; if (!deployment) return res.status(404).json({ error: 'Deployment not found' }); // Get devices for this deployment const { data: devices } = await supabase .from('devices') .select('*') .eq('well_id', deployment.deployment_id); // Get owner info let owner = null; if (deployment.owner_user_id) { const { data: u } = await supabase .from('users') .select('id, email, first_name, last_name, phone') .eq('id', deployment.owner_user_id) .single(); owner = u; } // Get all users with access to this deployment's beneficiary // First find beneficiary (owner is also beneficiary in most cases) let users_with_access = []; if (deployment.owner_user_id) { const { data: accessRecords } = await supabase .from('user_access') .select('*') .eq('beneficiary_id', deployment.owner_user_id); if (accessRecords && accessRecords.length > 0) { const accessorIds = accessRecords.map(a => a.accessor_id); const { data: accessors } = await supabase .from('users') .select('id, email, first_name, last_name') .in('id', accessorIds); users_with_access = accessRecords.map(a => { const user = accessors?.find(u => u.id === a.accessor_id); return { ...a, user }; }); } } // Get recent events/alerts for this deployment (if events table exists) let events = []; try { const { data: eventsData } = await supabase .from('events') .select('*') .eq('deployment_id', deployment.deployment_id) .order('created_at', { ascending: false }) .limit(20); events = eventsData || []; } catch (e) { // events table might not exist } res.json({ ...deployment, devices: devices || [], owner, users_with_access, events }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ============ DEVICES ============ /** * GET /api/admin/devices * List all devices */ router.get('/devices', async (req, res) => { try { const { well_id } = req.query; let query = supabase .from('devices') .select('*') .order('device_id', { ascending: false }); if (well_id) { query = query.eq('well_id', parseInt(well_id)); } const { data: devices, error } = await query; if (error) throw error; res.json({ devices: devices || [] }); } catch (error) { res.status(500).json({ error: error.message }); } }); /** * GET /api/admin/devices/:id * Get single device */ router.get('/devices/:id', async (req, res) => { try { const { data: device, error } = await supabase .from('devices') .select('*') .eq('device_id', req.params.id) .single(); if (error) throw error; if (!device) return res.status(404).json({ error: 'Device not found' }); // Get deployment info if exists let deployment = null; if (device.well_id) { const { data: d } = await supabase .from('deployments') .select('*') .eq('deployment_id', device.well_id) .single(); deployment = d; } res.json({ ...device, deployment }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ============ BENEFICIARIES ============ /** * GET /api/admin/beneficiaries * List all beneficiaries (optimized) */ router.get('/beneficiaries', async (req, res) => { try { const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 50; const offset = (page - 1) * limit; // Get all users who are beneficiaries (have user_access records where they are beneficiary_id) const { data: accessRecords, error } = await supabase .from('user_access') .select('beneficiary_id') .order('granted_at', { ascending: false }); if (error) throw error; // Get unique beneficiary IDs const beneficiaryIds = [...new Set((accessRecords || []).map(a => a.beneficiary_id))]; // Single query to get all beneficiary users let beneficiaries = []; if (beneficiaryIds.length > 0) { const { data: users } = await supabase .from('users') .select('*') .in('id', beneficiaryIds); beneficiaries = users || []; } // Apply pagination const paginatedBeneficiaries = beneficiaries.slice(offset, offset + limit); res.json({ beneficiaries: paginatedBeneficiaries, pagination: { page, limit, total: beneficiaries.length, pages: Math.ceil(beneficiaries.length / limit) } }); } catch (error) { res.status(500).json({ error: error.message }); } }); module.exports = router;