- Add displayName (customName || name) to BeneficiaryCard component - Update header and MockDashboard to show customName when set - Add custom name editing for non-custodian users (guardian/caretaker) - Backend PATCH endpoint now supports customName updates via user_access table 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1299 lines
42 KiB
JavaScript
1299 lines
42 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const jwt = require('jsonwebtoken');
|
|
const Stripe = require('stripe');
|
|
const { supabase } = require('../config/supabase');
|
|
const storage = require('../services/storage');
|
|
const legacyAPI = require('../services/legacyAPI');
|
|
|
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
|
|
|
/**
|
|
* Helper: Normalize Stripe subscription status to app statuses
|
|
*/
|
|
function normalizeStripeStatus(status) {
|
|
switch (status) {
|
|
case 'active':
|
|
return 'active';
|
|
case 'trialing':
|
|
return 'trialing';
|
|
case 'past_due':
|
|
case 'unpaid':
|
|
return 'past_due';
|
|
case 'canceled':
|
|
return 'canceled';
|
|
case 'incomplete':
|
|
case 'incomplete_expired':
|
|
return 'expired';
|
|
default:
|
|
return 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper: Get subscription status from Stripe (source of truth)
|
|
* Used for single beneficiary requests
|
|
*/
|
|
async function getStripeSubscriptionStatus(stripeCustomerId) {
|
|
if (!stripeCustomerId) {
|
|
return { plan: 'free', status: 'none', hasSubscription: false };
|
|
}
|
|
|
|
try {
|
|
// Get active subscriptions from Stripe
|
|
const subscriptions = await stripe.subscriptions.list({
|
|
customer: stripeCustomerId,
|
|
status: 'active',
|
|
limit: 1
|
|
});
|
|
|
|
if (subscriptions.data.length > 0) {
|
|
const sub = subscriptions.data[0];
|
|
// Use cancel_at if subscription is set to cancel, otherwise use current_period_end
|
|
const periodEndTimestamp = sub.cancel_at || sub.current_period_end;
|
|
const endDate = periodEndTimestamp ? new Date(periodEndTimestamp * 1000).toISOString() : null;
|
|
|
|
return {
|
|
plan: 'premium',
|
|
status: 'active',
|
|
hasSubscription: true,
|
|
endDate: endDate,
|
|
cancelAtPeriodEnd: sub.cancel_at_period_end || false
|
|
};
|
|
}
|
|
|
|
// Check for past_due or other statuses
|
|
const allSubs = await stripe.subscriptions.list({
|
|
customer: stripeCustomerId,
|
|
limit: 1
|
|
});
|
|
|
|
if (allSubs.data.length > 0) {
|
|
const sub = allSubs.data[0];
|
|
const normalizedStatus = normalizeStripeStatus(sub.status);
|
|
const periodEndTimestamp = sub.cancel_at || sub.current_period_end;
|
|
const endDate = periodEndTimestamp ? new Date(periodEndTimestamp * 1000).toISOString() : null;
|
|
|
|
return {
|
|
plan: normalizedStatus === 'canceled' || normalizedStatus === 'none' || normalizedStatus === 'expired' ? 'free' : 'premium',
|
|
status: normalizedStatus,
|
|
hasSubscription: normalizedStatus === 'active' || normalizedStatus === 'trialing',
|
|
endDate: endDate,
|
|
cancelAtPeriodEnd: sub.cancel_at_period_end || false
|
|
};
|
|
}
|
|
|
|
return { plan: 'free', status: 'none', hasSubscription: false };
|
|
} catch (error) {
|
|
console.error('Error fetching Stripe subscription:', error);
|
|
return { plan: 'free', status: 'none', hasSubscription: false };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper: Batch fetch subscription statuses for multiple customers
|
|
* Makes 1 Stripe API call instead of N calls
|
|
*/
|
|
async function getBatchStripeSubscriptions(stripeCustomerIds) {
|
|
// Filter out null/undefined
|
|
const validIds = stripeCustomerIds.filter(Boolean);
|
|
|
|
if (validIds.length === 0) {
|
|
return {};
|
|
}
|
|
|
|
const subscriptionMap = {};
|
|
|
|
// Initialize all as free/none
|
|
for (const customerId of validIds) {
|
|
subscriptionMap[customerId] = { plan: 'free', status: 'none', hasSubscription: false };
|
|
}
|
|
|
|
try {
|
|
const startTime = Date.now();
|
|
|
|
// Fetch all subscriptions in one call (Stripe allows up to 100)
|
|
// We fetch all statuses and filter in code
|
|
const subscriptions = await stripe.subscriptions.list({
|
|
limit: 100,
|
|
expand: ['data.customer']
|
|
});
|
|
|
|
console.log(`[STRIPE BATCH] Fetched ${subscriptions.data.length} subscriptions in ${Date.now() - startTime}ms`);
|
|
|
|
// Build map of customer_id -> subscription
|
|
for (const sub of subscriptions.data) {
|
|
const customerId = typeof sub.customer === 'string' ? sub.customer : sub.customer?.id;
|
|
|
|
if (!customerId || !validIds.includes(customerId)) {
|
|
continue;
|
|
}
|
|
|
|
const periodEndTimestamp = sub.cancel_at || sub.current_period_end;
|
|
const endDate = periodEndTimestamp ? new Date(periodEndTimestamp * 1000).toISOString() : null;
|
|
const normalizedStatus = normalizeStripeStatus(sub.status);
|
|
|
|
// Only update if this is a better status (active > trialing > past_due > etc)
|
|
const currentStatus = subscriptionMap[customerId]?.status || 'none';
|
|
const statusPriority = { active: 4, trialing: 3, past_due: 2, canceled: 1, expired: 0, none: 0 };
|
|
|
|
if (statusPriority[normalizedStatus] > statusPriority[currentStatus]) {
|
|
subscriptionMap[customerId] = {
|
|
plan: normalizedStatus === 'active' || normalizedStatus === 'trialing' ? 'premium' : 'free',
|
|
status: normalizedStatus,
|
|
hasSubscription: normalizedStatus === 'active' || normalizedStatus === 'trialing',
|
|
endDate: endDate,
|
|
cancelAtPeriodEnd: sub.cancel_at_period_end || false
|
|
};
|
|
}
|
|
}
|
|
|
|
return subscriptionMap;
|
|
} catch (error) {
|
|
console.error('[STRIPE BATCH] Error fetching subscriptions:', error.message);
|
|
return subscriptionMap; // Return map with defaults
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Middleware to verify JWT token
|
|
*/
|
|
function authMiddleware(req, res, next) {
|
|
const authHeader = req.headers.authorization;
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
return res.status(401).json({ error: 'No token provided' });
|
|
}
|
|
|
|
try {
|
|
const token = authHeader.split(' ')[1];
|
|
req.user = jwt.verify(token, process.env.JWT_SECRET);
|
|
next();
|
|
} catch (err) {
|
|
return res.status(401).json({ error: 'Invalid token' });
|
|
}
|
|
}
|
|
|
|
// Apply auth middleware to all routes
|
|
router.use(authMiddleware);
|
|
|
|
/**
|
|
* GET /api/me/beneficiaries
|
|
* Returns list of beneficiaries the user has access to
|
|
* Now uses the proper beneficiaries table (not users)
|
|
* OPTIMIZED: Reads subscription_status from DB (synced from Stripe hourly)
|
|
*/
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const startTime = Date.now();
|
|
const userId = req.user.userId;
|
|
|
|
// Get access records with beneficiary_id (points to beneficiaries table)
|
|
const { data: accessRecords, error: accessError } = await supabase
|
|
.from('user_access')
|
|
.select('id, beneficiary_id, role, granted_at, custom_name')
|
|
.eq('accessor_id', userId);
|
|
|
|
if (accessError) {
|
|
console.error('Get access records error:', accessError);
|
|
return res.status(500).json({ error: 'Failed to get beneficiaries' });
|
|
}
|
|
|
|
if (!accessRecords || accessRecords.length === 0) {
|
|
return res.json({ beneficiaries: [] });
|
|
}
|
|
|
|
// Get all beneficiary IDs
|
|
const beneficiaryIds = accessRecords
|
|
.map(r => r.beneficiary_id)
|
|
.filter(Boolean);
|
|
|
|
if (beneficiaryIds.length === 0) {
|
|
return res.json({ beneficiaries: [] });
|
|
}
|
|
|
|
// Batch query: Get all beneficiaries in one DB call (including subscription_status)
|
|
const { data: beneficiariesData, error: beneficiariesError } = await supabase
|
|
.from('beneficiaries')
|
|
.select('id, name, phone, address, avatar_url, created_at, equipment_status, subscription_status, subscription_updated_at')
|
|
.in('id', beneficiaryIds);
|
|
|
|
if (beneficiariesError) {
|
|
console.error('Get beneficiaries error:', beneficiariesError);
|
|
return res.status(500).json({ error: 'Failed to get beneficiaries' });
|
|
}
|
|
|
|
const dbTime = Date.now() - startTime;
|
|
|
|
// Build response - subscription status from DB (no Stripe calls!)
|
|
const beneficiaries = [];
|
|
for (const record of accessRecords) {
|
|
const beneficiary = beneficiariesData.find(b => b.id === record.beneficiary_id);
|
|
if (!beneficiary) continue;
|
|
|
|
// Build subscription object from cached DB status
|
|
const status = beneficiary.subscription_status || 'none';
|
|
const subscription = {
|
|
plan: status === 'active' || status === 'trialing' ? 'premium' : 'free',
|
|
status: status,
|
|
hasSubscription: status === 'active' || status === 'trialing'
|
|
};
|
|
|
|
beneficiaries.push({
|
|
accessId: record.id,
|
|
id: beneficiary.id,
|
|
role: record.role,
|
|
grantedAt: record.granted_at,
|
|
name: beneficiary.name,
|
|
customName: record.custom_name || null, // User's custom name for this beneficiary
|
|
phone: beneficiary.phone,
|
|
address: beneficiary.address || null,
|
|
avatarUrl: beneficiary.avatar_url,
|
|
createdAt: beneficiary.created_at,
|
|
subscription: subscription,
|
|
// Equipment status from beneficiaries table - CRITICAL for navigation!
|
|
hasDevices: beneficiary.equipment_status === 'active' || beneficiary.equipment_status === 'demo',
|
|
equipmentStatus: beneficiary.equipment_status || 'none'
|
|
});
|
|
}
|
|
|
|
const totalTime = Date.now() - startTime;
|
|
console.log(`[GET BENEFICIARIES] ${beneficiaries.length} items in ${totalTime}ms (DB only)`);
|
|
|
|
res.json({ beneficiaries });
|
|
|
|
} catch (error) {
|
|
console.error('Get beneficiaries error:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/me/beneficiaries/:id
|
|
* Returns details of a specific beneficiary
|
|
* Now uses the proper beneficiaries table (not users)
|
|
*/
|
|
router.get('/:id', async (req, res) => {
|
|
try {
|
|
const userId = req.user.userId;
|
|
const beneficiaryId = parseInt(req.params.id, 10);
|
|
|
|
// Check user has access - beneficiaryId is now from beneficiaries table
|
|
const { data: access, error: accessError } = await supabase
|
|
.from('user_access')
|
|
.select('role, beneficiary_id, custom_name')
|
|
.eq('accessor_id', userId)
|
|
.eq('beneficiary_id', beneficiaryId)
|
|
.single();
|
|
|
|
if (accessError || !access) {
|
|
return res.status(403).json({ error: 'Access denied' });
|
|
}
|
|
|
|
// Get beneficiary details from beneficiaries table
|
|
const { data: beneficiary, error } = await supabase
|
|
.from('beneficiaries')
|
|
.select('*')
|
|
.eq('id', beneficiaryId)
|
|
.single();
|
|
|
|
if (error || !beneficiary) {
|
|
return res.status(404).json({ error: 'Beneficiary not found' });
|
|
}
|
|
|
|
// Get subscription status from Stripe (source of truth)
|
|
const subscription = await getStripeSubscriptionStatus(beneficiary.stripe_customer_id);
|
|
|
|
// Get primary deployment with legacy_deployment_id
|
|
const { data: deployment } = await supabase
|
|
.from('beneficiary_deployments')
|
|
.select('id, legacy_deployment_id')
|
|
.eq('beneficiary_id', beneficiaryId)
|
|
.eq('is_primary', true)
|
|
.single();
|
|
|
|
// Get orders for this beneficiary
|
|
const { data: orders } = await supabase
|
|
.from('orders')
|
|
.select('id, order_number, status, total_amount, created_at')
|
|
.eq('beneficiary_id', beneficiaryId)
|
|
.eq('user_id', userId)
|
|
.order('created_at', { ascending: false });
|
|
|
|
res.json({
|
|
id: beneficiary.id,
|
|
name: beneficiary.name,
|
|
customName: access.custom_name || null, // User's custom name for this beneficiary
|
|
phone: beneficiary.phone,
|
|
address: beneficiary.address || null,
|
|
avatarUrl: beneficiary.avatar_url,
|
|
role: access.role,
|
|
subscription: subscription,
|
|
orders: orders || [],
|
|
// Equipment status from beneficiaries table - CRITICAL for navigation!
|
|
hasDevices: beneficiary.equipment_status === 'active' || beneficiary.equipment_status === 'demo',
|
|
equipmentStatus: beneficiary.equipment_status || 'none',
|
|
// Legacy deployment ID for fetching sensors from Legacy API
|
|
deploymentId: deployment?.legacy_deployment_id || null
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Get beneficiary error:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/me/beneficiaries
|
|
* Creates a new beneficiary and grants custodian access to creator
|
|
* Now uses the proper beneficiaries table (not users)
|
|
* AUTO-CREATES FIRST DEPLOYMENT
|
|
*/
|
|
router.post('/', async (req, res) => {
|
|
try {
|
|
const userId = req.user.userId;
|
|
const { name, phone, address } = req.body;
|
|
|
|
if (!name) {
|
|
return res.status(400).json({ error: 'name is required' });
|
|
}
|
|
|
|
console.log('[BENEFICIARY] Creating beneficiary:', { userId, name });
|
|
|
|
// Create beneficiary in the proper beneficiaries table (not users!)
|
|
const { data: beneficiary, error: createError } = await supabase
|
|
.from('beneficiaries')
|
|
.insert({
|
|
name: name,
|
|
phone: phone || null,
|
|
address: address || null,
|
|
equipment_status: 'none',
|
|
created_by: userId,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (createError) {
|
|
console.error('[BENEFICIARY] Create error:', createError);
|
|
return res.status(500).json({ error: 'Failed to create beneficiary' });
|
|
}
|
|
|
|
console.log('[BENEFICIARY] Created beneficiary:', beneficiary.id);
|
|
|
|
// Create user_access record with custodian role
|
|
// beneficiary_id points to the beneficiaries table
|
|
const { error: accessError } = await supabase
|
|
.from('user_access')
|
|
.insert({
|
|
accessor_id: userId,
|
|
beneficiary_id: beneficiary.id,
|
|
role: 'custodian',
|
|
granted_by: userId,
|
|
granted_at: new Date().toISOString()
|
|
});
|
|
|
|
if (accessError) {
|
|
console.error('[BENEFICIARY] Access error:', accessError);
|
|
// Rollback - delete the beneficiary
|
|
await supabase.from('beneficiaries').delete().eq('id', beneficiary.id);
|
|
return res.status(500).json({ error: 'Failed to grant access' });
|
|
}
|
|
|
|
console.log('[BENEFICIARY] Custodian access granted');
|
|
|
|
// AUTO-CREATE FIRST DEPLOYMENT
|
|
// This is the "Home" deployment - primary deployment for this beneficiary
|
|
const { data: deployment, error: deploymentError } = await supabase
|
|
.from('beneficiary_deployments')
|
|
.insert({
|
|
beneficiary_id: beneficiary.id,
|
|
name: 'Home',
|
|
address: address || null,
|
|
is_primary: true,
|
|
legacy_deployment_id: null, // Will be set after Legacy API call
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (deploymentError) {
|
|
console.error('[BENEFICIARY] Deployment create error:', deploymentError);
|
|
// Rollback - delete access and beneficiary
|
|
await supabase.from('user_access').delete().eq('accessor_id', userId).eq('beneficiary_id', beneficiary.id);
|
|
await supabase.from('beneficiaries').delete().eq('id', beneficiary.id);
|
|
return res.status(500).json({ error: 'Failed to create deployment' });
|
|
}
|
|
|
|
console.log('[BENEFICIARY] Created primary deployment:', deployment.id);
|
|
|
|
// CREATE DEPLOYMENT IN LEGACY API
|
|
// This links our beneficiary to the Legacy API system for device management
|
|
try {
|
|
const legacyUsername = process.env.LEGACY_API_USERNAME || '';
|
|
const legacyPassword = process.env.LEGACY_API_PASSWORD || '';
|
|
|
|
if (!legacyUsername || !legacyPassword) {
|
|
console.warn('[BENEFICIARY] Legacy API credentials not configured, skipping Legacy deployment creation');
|
|
} else {
|
|
// Get Legacy API token
|
|
const legacyToken = await legacyAPI.getLegacyToken(legacyUsername, legacyPassword);
|
|
|
|
// Create deployment in Legacy API
|
|
const legacyDeploymentId = await legacyAPI.createLegacyDeployment({
|
|
username: legacyUsername,
|
|
token: legacyToken,
|
|
beneficiaryName: name,
|
|
beneficiaryEmail: `beneficiary-${beneficiary.id}@wellnuo.app`, // Auto-generated email
|
|
beneficiaryUsername: `beneficiary_${beneficiary.id}`,
|
|
beneficiaryPassword: Math.random().toString(36).substring(2, 15), // Random password
|
|
address: address || '',
|
|
caretakerUsername: legacyUsername,
|
|
caretakerEmail: '', // Can be set later
|
|
persons: 1,
|
|
pets: 0,
|
|
gender: 'Other',
|
|
race: 0,
|
|
born: new Date().getFullYear() - 65,
|
|
lat: 0,
|
|
lng: 0,
|
|
wifis: [],
|
|
devices: []
|
|
});
|
|
|
|
console.log('[BENEFICIARY] Created Legacy deployment:', legacyDeploymentId);
|
|
|
|
// Update our deployment with legacy_deployment_id
|
|
const { error: updateError } = await supabase
|
|
.from('beneficiary_deployments')
|
|
.update({
|
|
legacy_deployment_id: legacyDeploymentId,
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
.eq('id', deployment.id);
|
|
|
|
if (updateError) {
|
|
console.error('[BENEFICIARY] Failed to update legacy_deployment_id:', updateError);
|
|
// Not critical - deployment still works without this link
|
|
} else {
|
|
deployment.legacy_deployment_id = legacyDeploymentId;
|
|
}
|
|
}
|
|
} catch (legacyError) {
|
|
console.error('[BENEFICIARY] Legacy API deployment failed:', legacyError);
|
|
// Not critical - continue without Legacy API link
|
|
// Beneficiary and deployment are still created in our database
|
|
}
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
beneficiary: {
|
|
id: beneficiary.id,
|
|
name: beneficiary.name,
|
|
phone: beneficiary.phone,
|
|
address: beneficiary.address || null,
|
|
avatarUrl: beneficiary.avatar_url,
|
|
role: 'custodian',
|
|
equipmentStatus: 'none',
|
|
primaryDeploymentId: deployment.id // Return deployment ID for future use
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('[BENEFICIARY] Create error:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PATCH /api/me/beneficiaries/:id
|
|
* Updates beneficiary info
|
|
* - Custodian: can update name, phone, address in beneficiaries table
|
|
* - Guardian/Caretaker: can only update customName in user_access table
|
|
*/
|
|
router.patch('/:id', async (req, res) => {
|
|
try {
|
|
const userId = req.user.userId;
|
|
const beneficiaryId = parseInt(req.params.id, 10);
|
|
|
|
console.log('[BENEFICIARY PATCH] Request:', { userId, beneficiaryId, body: req.body });
|
|
|
|
// Check user has access - using beneficiary_id
|
|
const { data: access, error: accessError } = await supabase
|
|
.from('user_access')
|
|
.select('id, role')
|
|
.eq('accessor_id', userId)
|
|
.eq('beneficiary_id', beneficiaryId)
|
|
.single();
|
|
|
|
if (accessError || !access) {
|
|
return res.status(403).json({ error: 'Access denied to this beneficiary' });
|
|
}
|
|
|
|
const { name, phone, address, customName } = req.body;
|
|
const isCustodian = access.role === 'custodian';
|
|
|
|
// Custodian can update beneficiary data (name, phone, address)
|
|
if (isCustodian) {
|
|
const updateData = {
|
|
updated_at: new Date().toISOString()
|
|
};
|
|
|
|
if (name !== undefined) updateData.name = name;
|
|
if (phone !== undefined) updateData.phone = phone;
|
|
if (address !== undefined) updateData.address = address;
|
|
|
|
// Update in beneficiaries table
|
|
const { data: beneficiary, error } = await supabase
|
|
.from('beneficiaries')
|
|
.update(updateData)
|
|
.eq('id', beneficiaryId)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) {
|
|
console.error('[BENEFICIARY PATCH] Supabase error:', error);
|
|
return res.status(500).json({ error: 'Failed to update beneficiary' });
|
|
}
|
|
|
|
console.log('[BENEFICIARY PATCH] Custodian updated:', { id: beneficiary.id, name: beneficiary.name, address: beneficiary.address });
|
|
|
|
res.json({
|
|
success: true,
|
|
beneficiary: {
|
|
id: beneficiary.id,
|
|
name: beneficiary.name,
|
|
displayName: beneficiary.name, // For custodian, displayName = name
|
|
originalName: beneficiary.name,
|
|
phone: beneficiary.phone,
|
|
address: beneficiary.address || null,
|
|
avatarUrl: beneficiary.avatar_url
|
|
}
|
|
});
|
|
} else {
|
|
// Guardian/Caretaker can only update their custom_name
|
|
if (customName === undefined) {
|
|
return res.status(400).json({ error: 'customName is required for non-custodian roles' });
|
|
}
|
|
|
|
// Validate custom name
|
|
if (customName !== null && typeof customName !== 'string') {
|
|
return res.status(400).json({ error: 'customName must be a string or null' });
|
|
}
|
|
if (customName && customName.length > 100) {
|
|
return res.status(400).json({ error: 'customName must be 100 characters or less' });
|
|
}
|
|
|
|
// Update custom_name in user_access
|
|
const { error: updateError } = await supabase
|
|
.from('user_access')
|
|
.update({
|
|
custom_name: customName || null // Empty string becomes null
|
|
})
|
|
.eq('id', access.id);
|
|
|
|
if (updateError) {
|
|
console.error('[BENEFICIARY PATCH] Custom name update error:', updateError);
|
|
return res.status(500).json({ error: 'Failed to update custom name' });
|
|
}
|
|
|
|
// Get beneficiary data for response
|
|
const { data: beneficiary } = await supabase
|
|
.from('beneficiaries')
|
|
.select('id, name, phone, address, avatar_url')
|
|
.eq('id', beneficiaryId)
|
|
.single();
|
|
|
|
const displayName = customName || beneficiary?.name || null;
|
|
|
|
console.log('[BENEFICIARY PATCH] Custom name updated:', { beneficiaryId, customName, displayName });
|
|
|
|
res.json({
|
|
success: true,
|
|
beneficiary: {
|
|
id: beneficiaryId,
|
|
name: beneficiary?.name || null,
|
|
displayName: displayName,
|
|
originalName: beneficiary?.name || null,
|
|
customName: customName || null,
|
|
phone: beneficiary?.phone || null,
|
|
address: beneficiary?.address || null,
|
|
avatarUrl: beneficiary?.avatar_url || null
|
|
}
|
|
});
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('[BENEFICIARY PATCH] Error:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/me/beneficiaries/:id
|
|
* Deletes a beneficiary and all related data (custodian only)
|
|
* Now uses the proper beneficiaries table (not users)
|
|
*/
|
|
router.delete('/:id', async (req, res) => {
|
|
try {
|
|
const userId = req.user.userId;
|
|
const beneficiaryId = parseInt(req.params.id, 10);
|
|
|
|
console.log('[BENEFICIARY] Delete request:', { userId, beneficiaryId });
|
|
|
|
// Check user has custodian access (only custodians can delete) - using beneficiary_id
|
|
const { data: access, error: accessError } = await supabase
|
|
.from('user_access')
|
|
.select('role')
|
|
.eq('accessor_id', userId)
|
|
.eq('beneficiary_id', beneficiaryId)
|
|
.single();
|
|
|
|
if (accessError || !access || access.role !== 'custodian') {
|
|
console.log('[BENEFICIARY] Delete denied:', {
|
|
userId,
|
|
beneficiaryId,
|
|
role: access?.role || 'none'
|
|
});
|
|
return res.status(403).json({ error: 'Only custodian can delete beneficiary' });
|
|
}
|
|
|
|
// Delete all related data in order (to respect foreign keys)
|
|
// 1. Delete invitations
|
|
await supabase
|
|
.from('invitations')
|
|
.delete()
|
|
.eq('beneficiary_id', beneficiaryId);
|
|
|
|
// 2. Delete all user_access records (using beneficiary_id)
|
|
await supabase
|
|
.from('user_access')
|
|
.delete()
|
|
.eq('beneficiary_id', beneficiaryId);
|
|
|
|
// 3. Delete orders
|
|
await supabase
|
|
.from('orders')
|
|
.delete()
|
|
.eq('beneficiary_id', beneficiaryId);
|
|
|
|
// 4. Delete devices
|
|
await supabase
|
|
.from('devices')
|
|
.delete()
|
|
.eq('beneficiary_id', beneficiaryId);
|
|
|
|
// 5. Finally delete the beneficiary from beneficiaries table (not users!)
|
|
const { error: deleteError } = await supabase
|
|
.from('beneficiaries')
|
|
.delete()
|
|
.eq('id', beneficiaryId);
|
|
|
|
if (deleteError) {
|
|
console.error('[BENEFICIARY] Delete error:', deleteError);
|
|
return res.status(500).json({ error: 'Failed to delete beneficiary' });
|
|
}
|
|
|
|
console.log('[BENEFICIARY] Deleted beneficiary:', beneficiaryId);
|
|
|
|
res.json({ success: true, message: 'Beneficiary deleted' });
|
|
|
|
} catch (error) {
|
|
console.error('[BENEFICIARY] Delete error:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/me/beneficiaries/:id/access
|
|
* Returns list of users who have access to this beneficiary
|
|
* Now uses beneficiary_id (points to beneficiaries table)
|
|
*/
|
|
router.get('/:id/access', async (req, res) => {
|
|
try {
|
|
const userId = req.user.userId;
|
|
const beneficiaryId = parseInt(req.params.id, 10);
|
|
|
|
// Check user has custodian or guardian access - using beneficiary_id
|
|
const { data: access } = await supabase
|
|
.from('user_access')
|
|
.select('role')
|
|
.eq('accessor_id', userId)
|
|
.eq('beneficiary_id', beneficiaryId)
|
|
.single();
|
|
|
|
if (!access || !['custodian', 'guardian'].includes(access.role)) {
|
|
return res.status(403).json({ error: 'Only custodian or guardian can view access list' });
|
|
}
|
|
|
|
// Get all access records for this beneficiary - using beneficiary_id
|
|
const { data: accessRecords, error } = await supabase
|
|
.from('user_access')
|
|
.select('id, accessor_id, role, granted_at, granted_by')
|
|
.eq('beneficiary_id', beneficiaryId);
|
|
|
|
if (error) {
|
|
return res.status(500).json({ error: 'Failed to get access list' });
|
|
}
|
|
|
|
// Get user details for each access record
|
|
const result = [];
|
|
for (const record of (accessRecords || [])) {
|
|
const { data: user } = await supabase
|
|
.from('users')
|
|
.select('id, email, first_name, last_name')
|
|
.eq('id', record.accessor_id)
|
|
.single();
|
|
|
|
if (user) {
|
|
result.push({
|
|
accessId: record.id,
|
|
userId: user.id,
|
|
email: user.email,
|
|
firstName: user.first_name,
|
|
lastName: user.last_name,
|
|
name: [user.first_name, user.last_name].filter(Boolean).join(' '),
|
|
role: record.role,
|
|
grantedAt: record.granted_at,
|
|
isCurrentUser: record.accessor_id === userId
|
|
});
|
|
}
|
|
}
|
|
|
|
res.json({ accessList: result });
|
|
|
|
} catch (error) {
|
|
console.error('[BENEFICIARY] Get access list error:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PATCH /api/me/beneficiaries/:id/access/:targetUserId
|
|
* Updates role for a user's access (custodian/guardian only)
|
|
* Now uses beneficiary_id (points to beneficiaries table)
|
|
*/
|
|
router.patch('/:id/access/:targetUserId', async (req, res) => {
|
|
try {
|
|
const userId = req.user.userId;
|
|
const beneficiaryId = parseInt(req.params.id, 10);
|
|
const targetUserId = parseInt(req.params.targetUserId, 10);
|
|
const { role } = req.body;
|
|
|
|
console.log('[BENEFICIARY] Update access:', { userId, beneficiaryId, targetUserId, role });
|
|
|
|
if (!role || !['guardian', 'caretaker'].includes(role)) {
|
|
return res.status(400).json({ error: 'Valid role required (guardian or caretaker)' });
|
|
}
|
|
|
|
// Check user has custodian or guardian access - using beneficiary_id
|
|
const { data: access } = await supabase
|
|
.from('user_access')
|
|
.select('role')
|
|
.eq('accessor_id', userId)
|
|
.eq('beneficiary_id', beneficiaryId)
|
|
.single();
|
|
|
|
if (!access || !['custodian', 'guardian'].includes(access.role)) {
|
|
return res.status(403).json({ error: 'Only custodian or guardian can update access' });
|
|
}
|
|
|
|
// Can't change custodian's role this way (use transfer instead) - using beneficiary_id
|
|
const { data: targetAccess } = await supabase
|
|
.from('user_access')
|
|
.select('id, role')
|
|
.eq('accessor_id', targetUserId)
|
|
.eq('beneficiary_id', beneficiaryId)
|
|
.single();
|
|
|
|
if (!targetAccess) {
|
|
return res.status(404).json({ error: 'User access not found' });
|
|
}
|
|
|
|
if (targetAccess.role === 'custodian') {
|
|
return res.status(400).json({ error: 'Cannot change custodian role. Use transfer instead.' });
|
|
}
|
|
|
|
// Update role
|
|
const { error } = await supabase
|
|
.from('user_access')
|
|
.update({ role })
|
|
.eq('id', targetAccess.id);
|
|
|
|
if (error) {
|
|
return res.status(500).json({ error: 'Failed to update role' });
|
|
}
|
|
|
|
console.log('[BENEFICIARY] Access updated:', { targetUserId, newRole: role });
|
|
|
|
res.json({ success: true, newRole: role });
|
|
|
|
} catch (error) {
|
|
console.error('[BENEFICIARY] Update access error:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/me/beneficiaries/:id/access/:targetUserId
|
|
* Revokes access for a user (custodian/guardian only)
|
|
* Now uses beneficiary_id (points to beneficiaries table)
|
|
*/
|
|
router.delete('/:id/access/:targetUserId', async (req, res) => {
|
|
try {
|
|
const userId = req.user.userId;
|
|
const beneficiaryId = parseInt(req.params.id, 10);
|
|
const targetUserId = parseInt(req.params.targetUserId, 10);
|
|
|
|
console.log('[BENEFICIARY] Revoke access:', { userId, beneficiaryId, targetUserId });
|
|
|
|
// Check user has custodian or guardian access - using beneficiary_id
|
|
const { data: access } = await supabase
|
|
.from('user_access')
|
|
.select('role')
|
|
.eq('accessor_id', userId)
|
|
.eq('beneficiary_id', beneficiaryId)
|
|
.single();
|
|
|
|
if (!access || !['custodian', 'guardian'].includes(access.role)) {
|
|
return res.status(403).json({ error: 'Only custodian or guardian can revoke access' });
|
|
}
|
|
|
|
// Can't revoke your own access
|
|
if (targetUserId === userId) {
|
|
return res.status(400).json({ error: 'Cannot revoke your own access' });
|
|
}
|
|
|
|
// Can't revoke custodian's access - using beneficiary_id
|
|
const { data: targetAccess } = await supabase
|
|
.from('user_access')
|
|
.select('id, role')
|
|
.eq('accessor_id', targetUserId)
|
|
.eq('beneficiary_id', beneficiaryId)
|
|
.single();
|
|
|
|
if (!targetAccess) {
|
|
return res.status(404).json({ error: 'User access not found' });
|
|
}
|
|
|
|
if (targetAccess.role === 'custodian') {
|
|
return res.status(400).json({ error: 'Cannot revoke custodian access' });
|
|
}
|
|
|
|
// Delete access record
|
|
const { error } = await supabase
|
|
.from('user_access')
|
|
.delete()
|
|
.eq('id', targetAccess.id);
|
|
|
|
if (error) {
|
|
return res.status(500).json({ error: 'Failed to revoke access' });
|
|
}
|
|
|
|
console.log('[BENEFICIARY] Access revoked:', { targetUserId });
|
|
|
|
res.json({ success: true });
|
|
|
|
} catch (error) {
|
|
console.error('[BENEFICIARY] Revoke access error:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/me/beneficiaries/:id/activate
|
|
* Activates equipment for a beneficiary (demo or real serial number)
|
|
* Now uses the proper beneficiaries table (not users)
|
|
*/
|
|
router.post('/:id/activate', async (req, res) => {
|
|
try {
|
|
const userId = req.user.userId;
|
|
const beneficiaryId = parseInt(req.params.id, 10);
|
|
const { serialNumber } = req.body;
|
|
|
|
console.log('[BENEFICIARY] Activate request:', { userId, beneficiaryId, serialNumber });
|
|
|
|
if (!serialNumber) {
|
|
return res.status(400).json({ error: 'serialNumber is required' });
|
|
}
|
|
|
|
// Check user has custodian or guardian access - using beneficiary_id
|
|
const { data: access, error: accessError } = await supabase
|
|
.from('user_access')
|
|
.select('role')
|
|
.eq('accessor_id', userId)
|
|
.eq('beneficiary_id', beneficiaryId)
|
|
.single();
|
|
|
|
if (accessError || !access || !['custodian', 'guardian'].includes(access.role)) {
|
|
return res.status(403).json({ error: 'Only custodian or guardian can activate equipment' });
|
|
}
|
|
|
|
// Check for demo serial
|
|
const isDemoMode = serialNumber === 'DEMO-00000' || serialNumber === 'DEMO-1234-5678';
|
|
const equipmentStatus = isDemoMode ? 'demo' : 'active';
|
|
|
|
// Update beneficiary record in beneficiaries table (not users!)
|
|
const { data: beneficiary, error: updateError } = await supabase
|
|
.from('beneficiaries')
|
|
.update({
|
|
equipment_status: equipmentStatus,
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
.eq('id', beneficiaryId)
|
|
.select('id, name, equipment_status')
|
|
.single();
|
|
|
|
if (updateError) {
|
|
console.error('[BENEFICIARY] Update error:', updateError);
|
|
return res.status(500).json({ error: 'Failed to activate equipment' });
|
|
}
|
|
|
|
console.log('[BENEFICIARY] Activated:', { beneficiaryId, equipmentStatus, isDemoMode });
|
|
|
|
res.json({
|
|
success: true,
|
|
beneficiary: {
|
|
id: beneficiary?.id || beneficiaryId,
|
|
name: beneficiary?.name || null,
|
|
hasDevices: true,
|
|
equipmentStatus: equipmentStatus
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('[BENEFICIARY] Activate error:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/me/beneficiaries/:id/transfer
|
|
* Transfers custodian rights to another user (custodian only)
|
|
* Now uses beneficiary_id (points to beneficiaries table)
|
|
*/
|
|
router.post('/:id/transfer', async (req, res) => {
|
|
try {
|
|
const userId = req.user.userId;
|
|
const beneficiaryId = parseInt(req.params.id, 10);
|
|
const { newCustodianId } = req.body;
|
|
|
|
console.log('[BENEFICIARY] Transfer request:', { userId, beneficiaryId, newCustodianId });
|
|
|
|
if (!newCustodianId) {
|
|
return res.status(400).json({ error: 'newCustodianId is required' });
|
|
}
|
|
|
|
// Check user has custodian access - using beneficiary_id
|
|
const { data: access, error: accessError } = await supabase
|
|
.from('user_access')
|
|
.select('id, role')
|
|
.eq('accessor_id', userId)
|
|
.eq('beneficiary_id', beneficiaryId)
|
|
.single();
|
|
|
|
if (accessError || !access || access.role !== 'custodian') {
|
|
return res.status(403).json({ error: 'Only custodian can transfer rights' });
|
|
}
|
|
|
|
// Check new custodian exists and has access to this beneficiary - using beneficiary_id
|
|
const { data: newUserAccess } = await supabase
|
|
.from('user_access')
|
|
.select('id, role')
|
|
.eq('accessor_id', newCustodianId)
|
|
.eq('beneficiary_id', beneficiaryId)
|
|
.single();
|
|
|
|
if (!newUserAccess) {
|
|
return res.status(400).json({ error: 'New custodian must have access to this beneficiary first' });
|
|
}
|
|
|
|
// Transaction: update both roles
|
|
// 1. Demote current custodian to guardian
|
|
const { error: demoteError } = await supabase
|
|
.from('user_access')
|
|
.update({ role: 'guardian' })
|
|
.eq('id', access.id);
|
|
|
|
if (demoteError) {
|
|
console.error('[BENEFICIARY] Demote error:', demoteError);
|
|
return res.status(500).json({ error: 'Failed to transfer rights' });
|
|
}
|
|
|
|
// 2. Promote new user to custodian
|
|
const { error: promoteError } = await supabase
|
|
.from('user_access')
|
|
.update({ role: 'custodian' })
|
|
.eq('id', newUserAccess.id);
|
|
|
|
if (promoteError) {
|
|
// Rollback: restore original custodian
|
|
await supabase
|
|
.from('user_access')
|
|
.update({ role: 'custodian' })
|
|
.eq('id', access.id);
|
|
|
|
console.error('[BENEFICIARY] Promote error:', promoteError);
|
|
return res.status(500).json({ error: 'Failed to transfer rights' });
|
|
}
|
|
|
|
console.log('[BENEFICIARY] Transferred custodian rights:', {
|
|
from: userId,
|
|
to: newCustodianId,
|
|
beneficiaryId
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Custodian rights transferred',
|
|
newRole: 'guardian' // Current user's new role
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('[BENEFICIARY] Transfer error:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PATCH /api/me/beneficiaries/:id/avatar
|
|
* Upload/update beneficiary avatar
|
|
* - Uploads to MinIO if configured
|
|
* - Falls back to base64 in DB if MinIO not available
|
|
*/
|
|
router.patch('/:id/avatar', async (req, res) => {
|
|
try {
|
|
const userId = req.user.userId;
|
|
const beneficiaryId = parseInt(req.params.id, 10);
|
|
const { avatar } = req.body; // base64 string or null to remove
|
|
|
|
console.log('[BENEFICIARY] Avatar update:', { userId, beneficiaryId, hasAvatar: !!avatar });
|
|
|
|
// Check user has custodian or guardian access
|
|
const { data: access, error: accessError } = await supabase
|
|
.from('user_access')
|
|
.select('role')
|
|
.eq('accessor_id', userId)
|
|
.eq('beneficiary_id', beneficiaryId)
|
|
.single();
|
|
|
|
if (accessError || !access || !['custodian', 'guardian', 'caretaker'].includes(access.role)) {
|
|
return res.status(403).json({ error: 'Only custodian, guardian or caretaker can update avatar' });
|
|
}
|
|
|
|
// Validate base64 if provided
|
|
if (avatar && !avatar.startsWith('data:image/')) {
|
|
return res.status(400).json({ error: 'Invalid image format. Must be base64 data URI' });
|
|
}
|
|
|
|
let avatarUrl = null;
|
|
|
|
if (avatar) {
|
|
// Try to upload to MinIO
|
|
if (storage.isConfigured()) {
|
|
try {
|
|
// Get current avatar to delete old file
|
|
const { data: current } = await supabase
|
|
.from('beneficiaries')
|
|
.select('avatar_url')
|
|
.eq('id', beneficiaryId)
|
|
.single();
|
|
|
|
// Delete old avatar from MinIO if exists
|
|
if (current?.avatar_url && current.avatar_url.includes('minio')) {
|
|
const oldKey = storage.extractKeyFromUrl(current.avatar_url);
|
|
if (oldKey) {
|
|
try {
|
|
await storage.deleteFile(oldKey);
|
|
} catch (e) {
|
|
console.warn('[BENEFICIARY] Failed to delete old avatar:', e.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Upload new avatar to MinIO
|
|
const filename = `beneficiary-${beneficiaryId}-${Date.now()}`;
|
|
const result = await storage.uploadBase64Image(avatar, 'avatars/beneficiaries', filename);
|
|
avatarUrl = result.url;
|
|
|
|
console.log('[BENEFICIARY] Avatar uploaded to MinIO:', avatarUrl);
|
|
} catch (uploadError) {
|
|
console.error('[BENEFICIARY] MinIO upload failed, falling back to DB:', uploadError.message);
|
|
// Fallback: store base64 in DB
|
|
avatarUrl = avatar;
|
|
}
|
|
} else {
|
|
// MinIO not configured - store base64 in DB
|
|
console.log('[BENEFICIARY] MinIO not configured, storing base64 in DB');
|
|
avatarUrl = avatar;
|
|
}
|
|
}
|
|
|
|
// Update avatar_url in beneficiaries table
|
|
const { data: beneficiary, error } = await supabase
|
|
.from('beneficiaries')
|
|
.update({
|
|
avatar_url: avatarUrl,
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
.eq('id', beneficiaryId)
|
|
.select('id, name, avatar_url')
|
|
.single();
|
|
|
|
if (error) {
|
|
console.error('[BENEFICIARY] Avatar update error:', error);
|
|
return res.status(500).json({ error: 'Failed to update avatar' });
|
|
}
|
|
|
|
console.log('[BENEFICIARY] Avatar updated:', { beneficiaryId, avatarUrl: beneficiary.avatar_url?.substring(0, 50) });
|
|
|
|
res.json({
|
|
success: true,
|
|
beneficiary: {
|
|
id: beneficiary.id,
|
|
name: beneficiary.name,
|
|
avatarUrl: beneficiary.avatar_url
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('[BENEFICIARY] Avatar error:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Update equipment status for a beneficiary
|
|
* PATCH /me/beneficiaries/:id/equipment-status
|
|
* Now uses beneficiary_id and beneficiaries table
|
|
*/
|
|
router.patch('/:id/equipment-status', authMiddleware, async (req, res) => {
|
|
try {
|
|
const beneficiaryId = parseInt(req.params.id, 10);
|
|
const userId = req.user.userId;
|
|
const { status } = req.body;
|
|
|
|
// Validate status
|
|
const validStatuses = ['none', 'ordered', 'shipped', 'delivered', 'active', 'demo'];
|
|
if (!validStatuses.includes(status)) {
|
|
return res.status(400).json({ error: 'Invalid status. Must be one of: ' + validStatuses.join(', ') });
|
|
}
|
|
|
|
// Check access - using beneficiary_id
|
|
const { data: access, error: accessError } = await supabase
|
|
.from('user_access')
|
|
.select('role')
|
|
.eq('accessor_id', userId)
|
|
.eq('beneficiary_id', beneficiaryId)
|
|
.single();
|
|
|
|
if (accessError || !access) {
|
|
return res.status(403).json({ error: 'Access denied to this beneficiary' });
|
|
}
|
|
|
|
// Only custodian can change equipment status
|
|
if (access.role !== 'custodian') {
|
|
return res.status(403).json({ error: 'Only custodian can update equipment status' });
|
|
}
|
|
|
|
// Update equipment status in beneficiaries table (new architecture!)
|
|
const { data: updated, error: updateError } = await supabase
|
|
.from('beneficiaries')
|
|
.update({
|
|
equipment_status: status,
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
.eq('id', beneficiaryId)
|
|
.select('id, name, equipment_status')
|
|
.single();
|
|
|
|
if (updateError) {
|
|
console.error('[BENEFICIARY] Failed to update equipment status:', updateError);
|
|
return res.status(500).json({ error: 'Failed to update equipment status' });
|
|
}
|
|
|
|
if (!updated) {
|
|
console.error('[BENEFICIARY] Beneficiary not found:', beneficiaryId);
|
|
return res.status(404).json({ error: 'Beneficiary not found' });
|
|
}
|
|
|
|
console.log('[BENEFICIARY] Equipment status updated:', { beneficiaryId, status });
|
|
|
|
res.json({
|
|
success: true,
|
|
id: updated.id,
|
|
name: updated.name,
|
|
equipmentStatus: updated.equipment_status
|
|
});
|
|
} catch (error) {
|
|
console.error('[BENEFICIARY] Equipment status update error:', error);
|
|
res.status(500).json({ error: 'Server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PATCH /api/me/beneficiaries/:id/custom-name
|
|
* Updates the user's custom name for a beneficiary
|
|
* This is stored in user_access (per-user, not global)
|
|
*/
|
|
router.patch('/:id/custom-name', async (req, res) => {
|
|
try {
|
|
const userId = req.user.userId;
|
|
const beneficiaryId = parseInt(req.params.id, 10);
|
|
const { customName } = req.body;
|
|
|
|
console.log('[BENEFICIARY] Custom name update:', { userId, beneficiaryId, customName });
|
|
|
|
// Validate custom name (allow null to clear, or string up to 100 chars)
|
|
if (customName !== null && customName !== undefined) {
|
|
if (typeof customName !== 'string') {
|
|
return res.status(400).json({ error: 'customName must be a string or null' });
|
|
}
|
|
if (customName.length > 100) {
|
|
return res.status(400).json({ error: 'customName must be 100 characters or less' });
|
|
}
|
|
}
|
|
|
|
// Check user has access to this beneficiary
|
|
const { data: access, error: accessError } = await supabase
|
|
.from('user_access')
|
|
.select('id, role')
|
|
.eq('accessor_id', userId)
|
|
.eq('beneficiary_id', beneficiaryId)
|
|
.single();
|
|
|
|
if (accessError || !access) {
|
|
return res.status(403).json({ error: 'Access denied to this beneficiary' });
|
|
}
|
|
|
|
// Update custom_name in user_access
|
|
const { data: updated, error: updateError } = await supabase
|
|
.from('user_access')
|
|
.update({
|
|
custom_name: customName || null // Empty string becomes null
|
|
})
|
|
.eq('id', access.id)
|
|
.select('id, custom_name')
|
|
.single();
|
|
|
|
if (updateError) {
|
|
console.error('[BENEFICIARY] Custom name update error:', updateError);
|
|
return res.status(500).json({ error: 'Failed to update custom name' });
|
|
}
|
|
|
|
console.log('[BENEFICIARY] Custom name updated:', { beneficiaryId, customName: updated.custom_name });
|
|
|
|
res.json({
|
|
success: true,
|
|
customName: updated.custom_name
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('[BENEFICIARY] Custom name error:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|