listmonk sync

This commit is contained in:
2025-08-15 11:14:38 -06:00
parent b885d89ae4
commit c973fe55cd
20 changed files with 3416 additions and 6 deletions

View File

@@ -0,0 +1,252 @@
const listmonkService = require('../services/listmonk');
const nocodbService = require('../services/nocodb');
const logger = require('../utils/logger');
// Get Listmonk sync status
exports.getSyncStatus = async (req, res) => {
try {
const status = listmonkService.getSyncStatus();
// Also check connection if it's enabled
if (status.enabled && !status.connected) {
// Try to reconnect
const reconnected = await listmonkService.checkConnection();
status.connected = reconnected;
}
res.json(status);
} catch (error) {
logger.error('Failed to get Listmonk status', error);
res.status(500).json({
success: false,
error: 'Failed to get sync status'
});
}
};
// Bulk sync all locations to Listmonk
exports.syncAllLocations = async (req, res) => {
try {
if (!listmonkService.syncEnabled) {
return res.status(400).json({
success: false,
error: 'Listmonk sync is disabled'
});
}
const locationData = await nocodbService.getLocations();
const locations = locationData?.list || [];
if (!locations || locations.length === 0) {
return res.json({
success: true,
message: 'No locations to sync',
results: { total: 0, success: 0, failed: 0, errors: [] }
});
}
const results = await listmonkService.bulkSync(locations, 'location');
res.json({
success: true,
message: `Bulk location sync completed: ${results.success} succeeded, ${results.failed} failed`,
results
});
} catch (error) {
logger.error('Bulk location sync failed', error);
res.status(500).json({
success: false,
error: 'Failed to sync locations to Listmonk'
});
}
};
// Bulk sync all users to Listmonk
exports.syncAllUsers = async (req, res) => {
try {
if (!listmonkService.syncEnabled) {
return res.status(400).json({
success: false,
error: 'Listmonk sync is disabled'
});
}
const config = require('../config');
const userData = await nocodbService.getAllPaginated(config.nocodb.loginSheetId);
const users = userData?.list || [];
if (!users || users.length === 0) {
return res.json({
success: true,
message: 'No users to sync',
results: { total: 0, success: 0, failed: 0, errors: [] }
});
}
const results = await listmonkService.bulkSync(users, 'user');
res.json({
success: true,
message: `Bulk user sync completed: ${results.success} succeeded, ${results.failed} failed`,
results
});
} catch (error) {
logger.error('Bulk user sync failed', error);
res.status(500).json({
success: false,
error: 'Failed to sync users to Listmonk'
});
}
};
// Sync both locations and users
exports.syncAll = async (req, res) => {
try {
if (!listmonkService.syncEnabled) {
return res.status(400).json({
success: false,
error: 'Listmonk sync is disabled'
});
}
let results = {
locations: { total: 0, success: 0, failed: 0, errors: [] },
users: { total: 0, success: 0, failed: 0, errors: [] }
};
// Sync locations
try {
const locationData = await nocodbService.getLocations();
const locations = locationData?.list || [];
if (locations && locations.length > 0) {
results.locations = await listmonkService.bulkSync(locations, 'location');
}
} catch (error) {
logger.error('Failed to sync locations during full sync', error);
results.locations.errors.push({ error: error.message });
}
// Sync users
try {
const userData = await nocodbService.getAllPaginated(config.nocodb.loginSheetId);
const users = userData?.list || [];
if (users && users.length > 0) {
results.users = await listmonkService.bulkSync(users, 'user');
}
} catch (error) {
logger.error('Failed to sync users during full sync', error);
results.users.errors.push({ error: error.message });
}
const totalSuccess = results.locations.success + results.users.success;
const totalFailed = results.locations.failed + results.users.failed;
res.json({
success: true,
message: `Complete sync finished: ${totalSuccess} succeeded, ${totalFailed} failed`,
results
});
} catch (error) {
logger.error('Complete sync failed', error);
res.status(500).json({
success: false,
error: 'Failed to perform complete sync'
});
}
};
// Get Listmonk list statistics
exports.getListStats = async (req, res) => {
try {
if (!listmonkService.syncEnabled) {
return res.json({
success: false,
error: 'Listmonk sync is disabled',
stats: null
});
}
const stats = await listmonkService.getListStats();
// Convert stats object to array format for frontend
let statsArray = [];
if (stats && typeof stats === 'object') {
statsArray = Object.entries(stats).map(([key, list]) => ({
id: key,
name: list.name,
subscriberCount: list.subscriber_count || 0,
description: `Email list for ${key}`
}));
}
res.json({
success: true,
stats: statsArray
});
} catch (error) {
logger.error('Failed to get Listmonk list stats', error);
res.status(500).json({
success: false,
error: 'Failed to get list statistics'
});
}
};
// Test Listmonk connection
exports.testConnection = async (req, res) => {
try {
const connected = await listmonkService.checkConnection();
if (connected) {
res.json({
success: true,
message: 'Listmonk connection successful',
connected: true
});
} else {
res.json({
success: false,
message: listmonkService.lastError || 'Connection failed',
connected: false
});
}
} catch (error) {
logger.error('Failed to test Listmonk connection', error);
res.status(500).json({
success: false,
error: 'Failed to test connection'
});
}
};
// Reinitialize Listmonk lists
exports.reinitializeLists = async (req, res) => {
try {
if (!listmonkService.syncEnabled) {
return res.status(400).json({
success: false,
error: 'Listmonk sync is disabled'
});
}
const initialized = await listmonkService.initializeLists();
if (initialized) {
res.json({
success: true,
message: 'Listmonk lists reinitialized successfully'
});
} else {
res.json({
success: false,
message: listmonkService.lastError || 'Failed to initialize lists'
});
}
} catch (error) {
logger.error('Failed to reinitialize Listmonk lists', error);
res.status(500).json({
success: false,
error: 'Failed to reinitialize lists'
});
}
};

View File

@@ -1,6 +1,7 @@
const nocodbService = require('../services/nocodb');
const logger = require('../utils/logger');
const config = require('../config');
const listmonkService = require('../services/listmonk');
const {
syncGeoFields,
validateCoordinates,
@@ -196,6 +197,32 @@ class LocationsController {
logger.info('Location created successfully:', extractId(response));
// Real-time sync to Listmonk (async, don't block response)
if (listmonkService.syncEnabled && response.Email) {
setImmediate(async () => {
try {
const syncResult = await listmonkService.syncLocation(response);
if (!syncResult.success) {
logger.warn('Listmonk sync failed for new location', {
locationId: extractId(response),
email: response.Email,
error: syncResult.error
});
} else {
logger.debug('Location synced to Listmonk', {
locationId: extractId(response),
email: response.Email
});
}
} catch (error) {
logger.error('Listmonk sync error for new location', {
locationId: extractId(response),
error: error.message
});
}
});
}
res.status(201).json({
success: true,
location: response
@@ -257,6 +284,32 @@ class LocationsController {
logger.info('Location updated successfully:', locationId);
// Real-time sync to Listmonk (async, don't block response)
if (listmonkService.syncEnabled && response.Email) {
setImmediate(async () => {
try {
const syncResult = await listmonkService.syncLocation(response);
if (!syncResult.success) {
logger.warn('Listmonk sync failed for updated location', {
locationId: locationId,
email: response.Email,
error: syncResult.error
});
} else {
logger.debug('Updated location synced to Listmonk', {
locationId: locationId,
email: response.Email
});
}
} catch (error) {
logger.error('Listmonk sync error for updated location', {
locationId: locationId,
error: error.message
});
}
});
}
res.json({
success: true,
location: response
@@ -285,6 +338,17 @@ class LocationsController {
});
}
// Get location data before deletion (for Listmonk cleanup)
let locationData = null;
if (listmonkService.syncEnabled) {
try {
const getResponse = await nocodbService.getById(config.nocodb.tableId, locationId);
locationData = getResponse;
} catch (error) {
logger.warn('Could not fetch location data before deletion', error.message);
}
}
await nocodbService.delete(
config.nocodb.tableId,
locationId
@@ -292,6 +356,32 @@ class LocationsController {
logger.info(`Location ${locationId} deleted by ${req.session.userEmail}`);
// Remove from Listmonk (async, don't block response)
if (listmonkService.syncEnabled && locationData && locationData.Email) {
setImmediate(async () => {
try {
const syncResult = await listmonkService.removeSubscriber(locationData.Email);
if (!syncResult.success) {
logger.warn('Failed to remove deleted location from Listmonk', {
locationId: locationId,
email: locationData.Email,
error: syncResult.error
});
} else {
logger.debug('Deleted location removed from Listmonk', {
locationId: locationId,
email: locationData.Email
});
}
} catch (error) {
logger.error('Listmonk cleanup error for deleted location', {
locationId: locationId,
error: error.message
});
}
});
}
res.json({
success: true,
message: 'Location deleted successfully'

View File

@@ -3,6 +3,7 @@ const logger = require('../utils/logger');
const config = require('../config');
const { sanitizeUser, extractId } = require('../utils/helpers');
const { sendLoginDetails } = require('../services/email');
const listmonkService = require('../services/listmonk');
class UsersController {
async getAll(req, res) {
@@ -111,6 +112,42 @@ class UsersController {
userData
);
// Real-time sync to Listmonk (async, don't block response)
if (listmonkService.syncEnabled && email) {
setImmediate(async () => {
try {
const userForSync = {
ID: extractId(response),
Email: email,
Name: name,
Admin: isAdmin,
UserType: userType,
'Created At': new Date().toISOString(),
ExpiresAt: expiresAt
};
const syncResult = await listmonkService.syncUser(userForSync);
if (!syncResult.success) {
logger.warn('Listmonk sync failed for new user', {
userId: extractId(response),
email: email,
error: syncResult.error
});
} else {
logger.debug('User synced to Listmonk', {
userId: extractId(response),
email: email
});
}
} catch (error) {
logger.error('Listmonk sync error for new user', {
email: email,
error: error.message
});
}
});
}
res.status(201).json({
success: true,
message: 'User created successfully',
@@ -152,11 +189,48 @@ class UsersController {
});
}
// Get user data before deletion (for Listmonk cleanup)
let userData = null;
if (listmonkService.syncEnabled) {
try {
const getResponse = await nocodbService.getById(config.nocodb.loginSheetId, userId);
userData = getResponse;
} catch (error) {
logger.warn('Could not fetch user data before deletion', error.message);
}
}
await nocodbService.delete(
config.nocodb.loginSheetId,
userId
);
// Remove from Listmonk (async, don't block response)
if (listmonkService.syncEnabled && userData && userData.Email) {
setImmediate(async () => {
try {
const syncResult = await listmonkService.syncUser(userData, 'delete');
if (!syncResult.success) {
logger.warn('Failed to remove deleted user from Listmonk', {
userId: userId,
email: userData.Email,
error: syncResult.error
});
} else {
logger.debug('Deleted user removed from Listmonk', {
userId: userId,
email: userData.Email
});
}
} catch (error) {
logger.error('Listmonk cleanup error for deleted user', {
userId: userId,
error: error.message
});
}
});
}
res.json({
success: true,
message: 'User deleted successfully'