Files
marketing-app/server/helpers.js
fahed c31e6222d7
Some checks failed
Deploy / deploy (push) Failing after 9s
feat: consolidate auth into NocoDB, add password reset, health check
- Migrate auth credentials from SQLite (auth.db) to NocoDB Users table
  with one-time migration function (auth.db → auth.db.bak)
- Add email-based password reset via Cloudron SMTP (nodemailer)
- Add GET /api/health endpoint for monitoring
- Add startup env var validation with clear error messages
- Strip sensitive fields (password_hash, reset_token) from all API responses
- Add ForgotPassword + ResetPassword pages with i18n (en/ar)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 11:47:27 +03:00

116 lines
3.4 KiB
JavaScript

// server/helpers.js
const nocodb = require('./nocodb');
const { DEFAULTS } = require('./config');
// Name lookup cache
const _nameCache = {};
// Clear cache periodically
setInterval(() => { Object.keys(_nameCache).forEach(k => delete _nameCache[k]); }, DEFAULTS.cacheTTLMs);
// Get a single record's display name
async function getRecordName(table, id) {
if (!id) return null;
const key = `${table}:${id}`;
if (_nameCache[key] !== undefined) return _nameCache[key];
try {
const r = await nocodb.get(table, id);
const name = r?.name || r?.title || r?.Name || null;
_nameCache[key] = name;
return name;
} catch {
_nameCache[key] = null;
return null;
}
}
// Batch resolve names for multiple IDs across tables
// Usage: await batchResolveNames({ brand: { table: 'Brands', ids: [1,2,3] }, user: { table: 'Users', ids: [4,5] } })
// Returns: { 'brand:1': 'BrandA', 'user:4': 'Alice', ... }
async function batchResolveNames(groups) {
// groups is an object like: { brand: { table: 'Brands', ids: [1,2,3] }, user: { table: 'Users', ids: [4,5] } }
const names = {};
const fetches = [];
for (const [prefix, { table, ids }] of Object.entries(groups)) {
const uniqueIds = [...new Set(ids.filter(Boolean))];
for (const id of uniqueIds) {
const key = `${prefix}:${id}`;
if (_nameCache[`${table}:${id}`] !== undefined) {
names[key] = _nameCache[`${table}:${id}`];
} else {
fetches.push(
getRecordName(table, id).then(name => { names[key] = name; })
);
}
}
}
await Promise.all(fetches);
return names;
}
// Parse comma-separated approver IDs
function parseApproverIds(str) {
if (!str) return [];
return str.split(',').map(s => s.trim()).filter(Boolean).map(Number);
}
// Safely parse JSON with fallback
function safeJsonParse(str, fallback = null) {
if (!str || typeof str !== 'string') return fallback;
try { return JSON.parse(str); } catch { return fallback; }
}
// Pick allowed fields from request body
function pickBodyFields(body, fields) {
const data = {};
for (const f of fields) {
if (body[f] !== undefined) data[f] = body[f];
}
return data;
}
// Sanitize a value for use in NocoDB WHERE clauses
// Prevents injection by removing NocoDB query operators
function sanitizeWhereValue(val) {
if (val === null || val === undefined) return '';
const str = String(val);
// Remove characters that could manipulate NocoDB query syntax
return str.replace(/[~(),$]/g, '');
}
// Build user modules list from user record
function getUserModules(user, allModules) {
if (user.role === 'superadmin') return allModules;
if (user.modules) return safeJsonParse(user.modules, allModules);
return allModules;
}
// Strip sensitive fields from user data before sending to client
const SENSITIVE_USER_FIELDS = ['password_hash', 'reset_token', 'reset_token_expires'];
function stripSensitiveFields(data) {
if (Array.isArray(data)) return data.map(stripSensitiveFields);
if (data && typeof data === 'object') {
const out = { ...data };
for (const f of SENSITIVE_USER_FIELDS) {
delete out[f];
delete out[f.replace(/_([a-z])/g, (_, c) => c.toUpperCase())];
}
return out;
}
return data;
}
module.exports = {
getRecordName,
batchResolveNames,
parseApproverIds,
safeJsonParse,
pickBodyFields,
sanitizeWhereValue,
getUserModules,
stripSensitiveFields,
_nameCache,
};