Created a centralized logger utility (src/utils/logger.js) that provides: - Structured logging with context labels - Log levels (ERROR, WARN, INFO, DEBUG) - Environment-based log level control via LOG_LEVEL env variable - Consistent timestamp and JSON data formatting Removed console.log/error/warn statements from: - All service files (mqtt, notifications, legacyAPI, email, storage, subscription-sync) - All route handlers (auth, beneficiaries, deployments, webhook, admin, etc) - Controllers (dashboard, auth, alarm) - Database connection handler - Main server file (index.js) Preserved: - Critical startup validation error for JWT_SECRET in index.js Benefits: - Production-ready logging that can be easily integrated with log aggregation services - Reduced noise in production logs - Easier debugging with structured context and data - Configurable log levels per environment 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
825 lines
21 KiB
JavaScript
825 lines
21 KiB
JavaScript
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;
|