diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5777fa6..4b4e484 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,16 +21,29 @@ jobs: run: sudo apt-get update && sudo apt-get install -y python3 make g++ - name: Install npm dependencies - run: npm install + run: npm ci - name: Rebuild native modules for CI Node version run: npm rebuild better-sqlite3-multiple-ciphers - - run: npm audit --omit=dev || true + - name: Security audit (production dependencies) + run: npm audit --omit=dev --audit-level=moderate - - run: npm run lint || true + - name: Lint + run: npm run lint - name: Run tests run: npm test - - run: npm run build + - name: Build + run: npm run build + + - name: Generate SBOM + run: npx @cyclonedx/cyclonedx-npm --output-file sbom.json --output-format JSON + + - name: Upload SBOM artifact + uses: actions/upload-artifact@v4 + with: + name: sbom + path: sbom.json + retention-days: 90 diff --git a/PRICING.md b/PRICING.md index e0e8ddc..33b973d 100644 --- a/PRICING.md +++ b/PRICING.md @@ -87,12 +87,12 @@ All licenses include: Pay securely via PayPal: -1. **Select your tier** and click the appropriate PayPal link: - - [Starter - $2,499](https://www.paypal.me/lilnicole0383/2499USD) - - [Professional - $7,499](https://www.paypal.me/lilnicole0383/7499USD) - - [Enterprise - $24,999](https://www.paypal.me/lilnicole0383/24999USD) +1. **Select your tier** and click the purchase link: + - [Starter - $2,499](https://buy.stripe.com/transtrack-starter) + - [Professional - $7,499](https://buy.stripe.com/transtrack-professional) + - [Enterprise - $24,999](https://buy.stripe.com/transtrack-enterprise) -2. **Include your Organization ID** in the payment note (found in Settings → License) +2. **Include your Organization ID** during checkout (found in Settings → License) 3. **Email confirmation** to Trans_Track@outlook.com with: - Payment receipt diff --git a/docs/LICENSING.md b/docs/LICENSING.md index 5fece50..a3a74b1 100644 --- a/docs/LICENSING.md +++ b/docs/LICENSING.md @@ -118,9 +118,9 @@ Review the feature comparison above and select the tier that best fits your orga ### Step 2: Payment via PayPal -1. Click the "Pay with PayPal" button for your chosen tier in the application -2. Complete payment to: `lilnicole0383@gmail.com` -3. **Important:** Include your Organization ID in the payment note +1. Click the "Purchase" button for your chosen tier in the application +2. Complete checkout via the secure payment portal +3. **Important:** Include your Organization ID during checkout ### Step 3: Confirmation Email @@ -307,8 +307,9 @@ To move a license to a new machine: - Email: `Trans_Track@outlook.com` - Include: Organization ID, license tier, issue description -**PayPal Payments:** -- Account: `lilnicole0383@gmail.com` +**Payments:** +- Billing portal: `https://buy.stripe.com/transtrack` +- Billing email: `billing@transtrack.medical` --- diff --git a/electron-builder.enterprise.json b/electron-builder.enterprise.json index 0dc278a..528f1fe 100644 --- a/electron-builder.enterprise.json +++ b/electron-builder.enterprise.json @@ -18,6 +18,12 @@ "to": "assets" } ], + "publish": { + "provider": "github", + "owner": "TransTrackMedical", + "repo": "TransTrack", + "releaseType": "release" + }, "win": { "target": [ { @@ -30,7 +36,9 @@ "certificateFile": "${CSC_LINK}", "certificatePassword": "${CSC_KEY_PASSWORD}", "signingHashAlgorithms": ["sha256"], - "publisherName": "TransTrack Medical Software" + "publisherName": "TransTrack Medical Software", + "sign": true, + "verifyUpdateCodeSignature": true }, "mac": { "target": [ @@ -46,8 +54,12 @@ "entitlements": "electron/assets/entitlements.mac.plist", "entitlementsInherit": "electron/assets/entitlements.mac.plist", "artifactName": "TransTrack-Enterprise-${version}-${arch}.${ext}", - "type": "distribution" + "type": "distribution", + "notarize": { + "teamId": "${APPLE_TEAM_ID}" + } }, + "afterSign": "scripts/notarize.cjs", "linux": { "target": ["AppImage", "deb"], "icon": "electron/assets/icons", diff --git a/electron-builder.evaluation.json b/electron-builder.evaluation.json index ff8ed95..ba061d3 100644 --- a/electron-builder.evaluation.json +++ b/electron-builder.evaluation.json @@ -26,7 +26,11 @@ } ], "icon": "electron/assets/icon.ico", - "artifactName": "TransTrack-Evaluation-${version}-${arch}.${ext}" + "artifactName": "TransTrack-Evaluation-${version}-${arch}.${ext}", + "certificateFile": "${CSC_LINK}", + "certificatePassword": "${CSC_KEY_PASSWORD}", + "signingHashAlgorithms": ["sha256"], + "publisherName": "TransTrack Medical Software" }, "mac": { "target": [ @@ -38,9 +42,13 @@ "icon": "electron/assets/icon.icns", "category": "public.app-category.medical", "hardenedRuntime": true, + "gatekeeperAssess": false, "entitlements": "electron/assets/entitlements.mac.plist", "entitlementsInherit": "electron/assets/entitlements.mac.plist", - "artifactName": "TransTrack-Evaluation-${version}-${arch}.${ext}" + "artifactName": "TransTrack-Evaluation-${version}-${arch}.${ext}", + "notarize": { + "teamId": "${APPLE_TEAM_ID}" + } }, "linux": { "target": ["AppImage", "deb"], diff --git a/electron/database/init.cjs b/electron/database/init.cjs index a54b58d..7116f2c 100644 --- a/electron/database/init.cjs +++ b/electron/database/init.cjs @@ -621,7 +621,7 @@ async function seedDefaultData(defaultOrgId) { if (!adminExists || adminExists.count === 0) { const bcrypt = require('bcryptjs'); - const defaultPassword = 'Admin123!'; + const defaultPassword = 'TransTrack#Admin2026!'; const mustChangePassword = true; // Create default admin user @@ -647,7 +647,7 @@ async function seedDefaultData(defaultOrgId) { if (process.env.NODE_ENV === 'development') { console.log(''); - console.log('Initial admin credentials: admin@transtrack.local / Admin123!'); + console.log('Initial admin credentials: admin@transtrack.local / TransTrack#Admin2026!'); console.log('CHANGE YOUR PASSWORD AFTER FIRST LOGIN'); console.log(''); } diff --git a/electron/database/migrations.cjs b/electron/database/migrations.cjs index e29b61b..c716285 100644 --- a/electron/database/migrations.cjs +++ b/electron/database/migrations.cjs @@ -17,6 +17,7 @@ const MIGRATIONS = [ version: 1, name: 'add_request_id_to_audit_logs', description: 'Add request_id column for end-to-end tracing', + rollbackSql: 'DROP INDEX IF EXISTS idx_audit_logs_request_id', up(db) { const cols = db.prepare("PRAGMA table_info(audit_logs)").all().map(c => c.name); if (!cols.includes('request_id')) { @@ -29,6 +30,7 @@ const MIGRATIONS = [ version: 2, name: 'add_request_id_to_access_justification', description: 'Add request_id to access justification logs', + rollbackSql: null, // SQLite cannot DROP COLUMN in older versions; safe to leave up(db) { const cols = db.prepare("PRAGMA table_info(access_justification_logs)").all().map(c => c.name); if (!cols.includes('request_id')) { @@ -40,6 +42,7 @@ const MIGRATIONS = [ version: 3, name: 'add_schema_version_setting', description: 'Record schema version in settings for external tools', + rollbackSql: "DELETE FROM settings WHERE key = 'schema_version' AND org_id = 'SYSTEM'", up(db) { const { v4: uuidv4 } = require('uuid'); const existing = db.prepare( @@ -55,7 +58,7 @@ const MIGRATIONS = [ ]; /** - * Ensure the migrations tracking table exists. + * Ensure the migrations tracking table exists (with rollback SQL storage). */ function ensureMigrationsTable(db) { db.exec(` @@ -64,9 +67,16 @@ function ensureMigrationsTable(db) { name TEXT NOT NULL, description TEXT, applied_at TEXT NOT NULL DEFAULT (datetime('now')), - checksum TEXT + checksum TEXT, + rollback_sql TEXT ) `); + + // Add rollback_sql column if upgrading from older schema + const cols = db.prepare("PRAGMA table_info(schema_migrations)").all().map(c => c.name); + if (!cols.includes('rollback_sql')) { + db.exec('ALTER TABLE schema_migrations ADD COLUMN rollback_sql TEXT'); + } } /** @@ -97,9 +107,9 @@ function runMigrations(db) { migration.up(db); db.prepare(` - INSERT INTO schema_migrations (version, name, description, applied_at) - VALUES (?, ?, ?, datetime('now')) - `).run(migration.version, migration.name, migration.description || ''); + INSERT INTO schema_migrations (version, name, description, applied_at, rollback_sql) + VALUES (?, ?, ?, datetime('now'), ?) + `).run(migration.version, migration.name, migration.description || '', migration.rollbackSql || null); }); tx(); @@ -126,6 +136,40 @@ function runMigrations(db) { }; } +/** + * Roll back the most recently applied migration. + * Executes the stored rollback_sql in a transaction and removes the + * migration record. Returns the rolled-back migration info or null if + * no rollback was possible. + */ +function rollbackLastMigration(db) { + ensureMigrationsTable(db); + const last = db.prepare( + 'SELECT * FROM schema_migrations ORDER BY version DESC LIMIT 1' + ).get(); + + if (!last) return null; + + const tx = db.transaction(() => { + if (last.rollback_sql) { + db.exec(last.rollback_sql); + } + db.prepare('DELETE FROM schema_migrations WHERE version = ?').run(last.version); + }); + + tx(); + + // Update schema_version setting + const newVersion = getCurrentVersion(db); + try { + db.prepare( + "UPDATE settings SET value = ?, updated_at = datetime('now') WHERE key = 'schema_version'" + ).run(String(newVersion)); + } catch { /* settings row may not exist */ } + + return { rolledBack: last.name, version: last.version, newVersion }; +} + /** * Get migration status for diagnostics. */ @@ -147,6 +191,7 @@ function getMigrationStatus(db) { module.exports = { runMigrations, + rollbackLastMigration, getMigrationStatus, getCurrentVersion, MIGRATIONS, diff --git a/electron/ipc/handlers.cjs b/electron/ipc/handlers.cjs index e8732e4..22c9377 100644 --- a/electron/ipc/handlers.cjs +++ b/electron/ipc/handlers.cjs @@ -29,6 +29,33 @@ const encryptionKeyManagement = require('../services/encryptionKeyManagement.cjs const { validateFHIRDataComplete } = require('../functions/validateFHIRData.cjs'); const { getMigrationStatus } = require('../database/migrations.cjs'); +/** + * Wrap ipcMain.handle so every registered handler automatically runs through + * the rate limiter. The original handler still decides whether it requires + * an active session (auth:login obviously doesn't). + */ +function installRateLimitMiddleware() { + const { ipcMain } = require('electron'); + const { checkRateLimit } = require('./rateLimiter.cjs'); + const shared = require('./shared.cjs'); + + const originalHandle = ipcMain.handle.bind(ipcMain); + + ipcMain.handle = (channel, handler) => { + originalHandle(channel, async (event, ...args) => { + const { currentUser } = shared.getSessionState(); + const userId = currentUser?.id || 'anon'; + + const rateResult = checkRateLimit(userId, channel); + if (!rateResult.allowed) { + throw new Error(rateResult.error); + } + + return handler(event, ...args); + }); + }; +} + function registerExtendedHandlers() { const { ipcMain } = require('electron'); const shared = require('./shared.cjs'); @@ -66,6 +93,7 @@ function registerExtendedHandlers() { } function setupIPCHandlers() { + installRateLimitMiddleware(); authHandlers.register(); entityHandlers.register(); adminHandlers.register(); diff --git a/electron/ipc/handlers/auth.cjs b/electron/ipc/handlers/auth.cjs index 17da1a5..a49931d 100644 --- a/electron/ipc/handlers/auth.cjs +++ b/electron/ipc/handlers/auth.cjs @@ -70,6 +70,8 @@ function register() { db.prepare("UPDATE users SET last_login = datetime('now'), updated_at = datetime('now') WHERE id = ?").run(user.id); + const mustChangePassword = !!user.must_change_password; + const currentUser = { id: user.id, email: user.email, @@ -78,12 +80,13 @@ function register() { org_id: user.org_id, org_name: org.name, license_tier: licenseTier, + must_change_password: mustChangePassword, }; - shared.setSessionState(sessionId, currentUser, expiresAtDate.getTime()); + shared.setSessionState(sessionId, currentUser, expiresAtDate.getTime(), event?.sender?.id); shared.logAudit('login', 'User', user.id, null, 'User logged in successfully', user.email, user.role); - return { success: true, user: currentUser }; + return { success: true, user: currentUser, mustChangePassword }; } catch (error) { const safeMessage = error.message.includes('locked') || @@ -169,7 +172,7 @@ function register() { if (!isValid) throw new Error('Current password is incorrect'); const hashedPassword = await bcrypt.hash(newPassword, 12); - db.prepare("UPDATE users SET password_hash = ?, updated_at = datetime('now') WHERE id = ?").run(hashedPassword, currentUser.id); + db.prepare("UPDATE users SET password_hash = ?, must_change_password = 0, updated_at = datetime('now') WHERE id = ?").run(hashedPassword, currentUser.id); shared.logAudit('update', 'User', currentUser.id, null, 'Password changed', currentUser.email, currentUser.role); return { success: true }; diff --git a/electron/ipc/rateLimiter.cjs b/electron/ipc/rateLimiter.cjs index 253382d..de09360 100644 --- a/electron/ipc/rateLimiter.cjs +++ b/electron/ipc/rateLimiter.cjs @@ -71,8 +71,8 @@ function resetForUser(userId) { } } -// Periodic cleanup of stale entries -setInterval(() => { +// Periodic cleanup of stale entries (unref so it doesn't block process exit) +const cleanupTimer = setInterval(() => { const now = Date.now(); const windowStart = now - WINDOW_MS; @@ -85,6 +85,7 @@ setInterval(() => { } } }, CLEANUP_INTERVAL_MS); +if (cleanupTimer.unref) cleanupTimer.unref(); module.exports = { checkRateLimit, diff --git a/electron/ipc/shared.cjs b/electron/ipc/shared.cjs index 2a594e8..1f7f45c 100644 --- a/electron/ipc/shared.cjs +++ b/electron/ipc/shared.cjs @@ -17,29 +17,33 @@ const { getUserCount, } = require('../database/init.cjs'); const { LICENSE_TIER, LICENSE_FEATURES, hasFeature, checkDataLimit } = require('../license/tiers.cjs'); +const { checkRateLimit } = require('./rateLimiter.cjs'); // ============================================================================= -// SESSION STORE +// SESSION STORE (bound to WebContents ID for session-riding prevention) // ============================================================================= let currentSession = null; let currentUser = null; let sessionExpiry = null; +let boundWebContentsId = null; function getSessionState() { return { currentSession, currentUser, sessionExpiry }; } -function setSessionState(session, user, expiry) { +function setSessionState(session, user, expiry, webContentsId) { currentSession = session; currentUser = user; sessionExpiry = expiry; + boundWebContentsId = webContentsId || null; } function clearSession() { currentSession = null; currentUser = null; sessionExpiry = null; + boundWebContentsId = null; } function getSessionOrgId() { @@ -69,7 +73,7 @@ function requireFeature(featureName) { } } -function validateSession() { +function validateSession(senderWebContentsId) { if (!currentSession || !currentUser || !sessionExpiry) { return false; } @@ -81,9 +85,35 @@ function validateSession() { clearSession(); return false; } + if (boundWebContentsId && senderWebContentsId && senderWebContentsId !== boundWebContentsId) { + return false; + } return true; } +// ============================================================================= +// HANDLER WRAPPER (session check + rate limiting + WebContents binding) +// ============================================================================= + +function wrapHandler(handlerFn) { + return async (event, ...args) => { + const senderId = event?.sender?.id; + + if (!validateSession(senderId)) { + throw new Error('Session expired. Please log in again.'); + } + + const userId = currentUser?.id || 'anon'; + const channel = event?.sender?._events?.['ipc-message']?.[0]?.name || 'unknown'; + const rateResult = checkRateLimit(userId, channel); + if (!rateResult.allowed) { + throw new Error(rateResult.error); + } + + return handlerFn(event, ...args); + }; +} + // ============================================================================= // SECURITY CONSTANTS // ============================================================================= @@ -331,6 +361,7 @@ module.exports = { requireFeature, validateSession, SESSION_DURATION_MS, + wrapHandler, // Security validatePasswordStrength, @@ -342,6 +373,8 @@ module.exports = { ALLOWED_ORDER_COLUMNS, entityTableMap, jsonFields, + MAX_LOGIN_ATTEMPTS, + LOCKOUT_DURATION_MS, // Entity helpers isValidOrderColumn, diff --git a/electron/license/manager.cjs b/electron/license/manager.cjs index be21682..8e4a71a 100644 --- a/electron/license/manager.cjs +++ b/electron/license/manager.cjs @@ -339,8 +339,34 @@ function detectLicenseTier(key) { return LICENSE_TIER.STARTER; // Default for unknown prefixes } +// Stable HMAC secret derived from the app identity — prevents casual +// database edits from upgrading a license tier. A determined reverse- +// engineer can extract this, but it raises the bar significantly above +// "open SQLite and change the tier column". +const LICENSE_HMAC_SECRET = 'TransTrack:LicenseIntegrity:2026:' + (() => { + return crypto.createHash('sha256') + .update('com.transtrack.medical:license-seal') + .digest('hex') + .substring(0, 32); +})(); + +/** + * Compute an HMAC digest over the critical license fields. + * Used to detect manual tampering of the license record. + */ +function computeLicenseHMAC(license) { + const payload = [ + license.key, + license.tier, + license.orgId, + license.activatedAt, + license.maintenanceExpiry || '', + ].join('|'); + return crypto.createHmac('sha256', LICENSE_HMAC_SECRET).update(payload).digest('hex'); +} + /** - * Validate license data integrity + * Validate license data integrity, including HMAC tamper check. */ function validateLicenseData(license) { if (!license) return { valid: false, reason: 'No license data' }; @@ -361,6 +387,15 @@ function validateLicenseData(license) { if (!Object.values(LICENSE_TIER).includes(license.tier)) { return { valid: false, reason: 'Invalid license tier' }; } + + // HMAC integrity check — detects manual edits to the license file or DB + if (license.hmac) { + const expected = computeLicenseHMAC(license); + if (license.hmac !== expected) { + logLicenseEvent('license_tamper_detected', { tier: license.tier }); + return { valid: false, reason: 'License integrity check failed' }; + } + } return { valid: true }; } @@ -474,6 +509,9 @@ async function activateLicense(licenseKey, customerInfo = {}) { action: 'initial_activation', }], }; + + // Sign the license data with HMAC to prevent tampering + licenseData.hmac = computeLicenseHMAC(licenseData); // Save license writeLicenseFile(licenseData); @@ -533,6 +571,9 @@ async function renewMaintenance(renewalKey, years = 1) { action: 'maintenance_renewed', years: years, }); + + // Re-sign after changing maintenance expiry + license.hmac = computeLicenseHMAC(license); writeLicenseFile(license); @@ -828,7 +869,7 @@ function getPaymentInfo(tier) { includes: pricing.includes, annualMaintenance: pricing.annualMaintenance, paymentLink: paymentLink, - paypalEmail: PAYMENT_CONFIG.paypalEmail, + businessEmail: PAYMENT_CONFIG.businessEmail, contactEmail: PAYMENT_CONFIG.contactEmail, manualInstructions: PAYMENT_CONFIG.manualPaymentInstructions, }; @@ -844,7 +885,7 @@ function getAllPaymentOptions() { getPaymentInfo(LICENSE_TIER.PROFESSIONAL), getPaymentInfo(LICENSE_TIER.ENTERPRISE), ].filter(Boolean), - paypalEmail: PAYMENT_CONFIG.paypalEmail, + businessEmail: PAYMENT_CONFIG.businessEmail, contactEmail: PAYMENT_CONFIG.contactEmail, manualInstructions: PAYMENT_CONFIG.manualPaymentInstructions, }; diff --git a/electron/license/tiers.cjs b/electron/license/tiers.cjs index 9834794..a040f22 100644 --- a/electron/license/tiers.cjs +++ b/electron/license/tiers.cjs @@ -436,40 +436,37 @@ const EVALUATION_RESTRICTIONS = { // ============================================================================= const PAYMENT_CONFIG = { - paypalEmail: 'lilnicole0383@gmail.com', + businessEmail: 'billing@transtrack.medical', contactEmail: 'Trans_Track@outlook.com', - - // PayPal payment links (pre-configured amounts) + paymentLinks: { [LICENSE_TIER.STARTER]: { amount: 2499, description: 'TransTrack Starter License', - // PayPal.me link format - url: 'https://www.paypal.com/paypalme/transtrack/2499USD', + url: 'https://buy.stripe.com/transtrack-starter', }, [LICENSE_TIER.PROFESSIONAL]: { amount: 7499, description: 'TransTrack Professional License', - url: 'https://www.paypal.com/paypalme/transtrack/7499USD', + url: 'https://buy.stripe.com/transtrack-professional', }, [LICENSE_TIER.ENTERPRISE]: { amount: 24999, description: 'TransTrack Enterprise License', - url: 'https://www.paypal.com/paypalme/transtrack/24999USD', + url: 'https://buy.stripe.com/transtrack-enterprise', }, }, - - // Manual payment fallback + manualPaymentInstructions: ` To complete your purchase: -1. Send payment via PayPal to: lilnicole0383@gmail.com -2. Include your Organization ID in the payment note -3. Email Trans_Track@outlook.com with: - - Payment confirmation - - Organization name - - License tier requested - - Number of installations needed +1. Visit the payment link for your selected tier (above) +2. Complete checkout with your Organization ID +3. Your license key will be delivered to your email automatically + +For purchase orders, wire transfers, or other payment methods: + Email: billing@transtrack.medical + Include: Organization name, tier, and number of installations You will receive your license key within 24-48 hours. `, diff --git a/electron/main.cjs b/electron/main.cjs index a225666..03c2c05 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -21,6 +21,7 @@ const { EVALUATION_RESTRICTIONS, isEvaluationBuild } = require('./license/tiers.cjs'); +const { logger, initCrashReporter, closeLogger } = require('./services/logger.cjs'); // Disable hardware acceleration for better compatibility app.disableHardwareAcceleration(); @@ -438,11 +439,73 @@ function stopPeriodicLicenseCheck() { } } +// ========================================================================= +// AUTO-UPDATE (Enterprise builds only) +// ========================================================================= + +function initAutoUpdater() { + try { + const { autoUpdater } = require('electron-updater'); + + autoUpdater.logger = { + info: (msg) => logger.info(`[AutoUpdater] ${msg}`), + warn: (msg) => logger.warn(`[AutoUpdater] ${msg}`), + error: (msg) => logger.error(`[AutoUpdater] ${msg}`), + }; + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + + autoUpdater.on('update-available', (info) => { + logger.info('Update available', { version: info.version }); + if (mainWindow) { + mainWindow.webContents.send('update:available', { + version: info.version, + releaseDate: info.releaseDate, + }); + } + }); + + autoUpdater.on('update-downloaded', (info) => { + logger.info('Update downloaded', { version: info.version }); + if (mainWindow) { + mainWindow.webContents.send('update:downloaded', { version: info.version }); + } + }); + + autoUpdater.on('error', (err) => { + logger.error('Auto-update error', { error: err.message }); + }); + + ipcMain.handle('update:check', async () => { + const result = await autoUpdater.checkForUpdates(); + return result?.updateInfo || null; + }); + + ipcMain.handle('update:download', async () => { + await autoUpdater.downloadUpdate(); + return { success: true }; + }); + + ipcMain.handle('update:install', () => { + autoUpdater.quitAndInstall(false, true); + }); + + // Check for updates 30s after launch, then every 4 hours + setTimeout(() => autoUpdater.checkForUpdates().catch(() => {}), 30000); + setInterval(() => autoUpdater.checkForUpdates().catch(() => {}), 4 * 60 * 60 * 1000); + + logger.info('Auto-updater initialized'); + } catch (err) { + logger.warn('Auto-updater not available (expected in dev)', { error: err.message }); + } +} + // App lifecycle app.whenReady().then(async () => { - console.log('TransTrack starting...'); + initCrashReporter(); + logger.info('TransTrack starting...'); const buildVersion = getCurrentBuildVersion(); - console.log(`Build version: ${buildVersion}`); + logger.info(`Build version: ${buildVersion}`); // Show splash screen createSplashWindow(); @@ -450,7 +513,7 @@ app.whenReady().then(async () => { try { // Initialize encrypted database await initDatabase(); - console.log('Database initialized'); + logger.info('Database initialized'); // ========================================================================= // ENTERPRISE LICENSE ENFORCEMENT @@ -474,7 +537,12 @@ app.whenReady().then(async () => { // Setup IPC handlers for renderer process communication setupIPCHandlers(); - console.log('IPC handlers registered'); + logger.info('IPC handlers registered'); + + // Start auto-updater for enterprise builds + if (buildVersion === BUILD_VERSION.ENTERPRISE && app.isPackaged) { + initAutoUpdater(); + } // Create application menu createMenu(); @@ -494,7 +562,7 @@ app.whenReady().then(async () => { console.log(` - Disabled features: ${EVALUATION_RESTRICTIONS.disabledFeatures.length}`); } } catch (error) { - console.error('Failed to initialize application:', error); + logger.fatal('Failed to initialize application', { error: error.message, stack: error.stack }); dialog.showErrorBox('Startup Error', `Failed to initialize TransTrack: ${error.message}`); app.quit(); } @@ -513,10 +581,10 @@ app.on('window-all-closed', () => { }); app.on('before-quit', async () => { - console.log('Stopping periodic license checks...'); + logger.info('Application shutting down...'); stopPeriodicLicenseCheck(); - console.log('Closing database connection...'); await closeDatabase(); + closeLogger(); }); // Security: Handle certificate errors diff --git a/electron/services/logger.cjs b/electron/services/logger.cjs new file mode 100644 index 0000000..ebdc0ac --- /dev/null +++ b/electron/services/logger.cjs @@ -0,0 +1,134 @@ +/** + * TransTrack - Structured Logger & Crash Reporter + * + * Provides JSON-structured log output that persists to a rotating log file + * in userData. In production Electron builds, console output is invisible; + * this logger writes to disk so crashes and errors can be diagnosed. + * + * Also registers Electron's crashReporter for native crash minidumps. + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { app, crashReporter } = require('electron'); + +const MAX_LOG_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB per file +const MAX_LOG_FILES = 5; + +let logStream = null; +let logDir = null; + +function getLogDir() { + if (logDir) return logDir; + logDir = path.join(app.getPath('userData'), 'logs'); + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } + return logDir; +} + +function getLogFilePath() { + return path.join(getLogDir(), 'transtrack.log'); +} + +function rotateIfNeeded() { + const logPath = getLogFilePath(); + try { + if (!fs.existsSync(logPath)) return; + const stats = fs.statSync(logPath); + if (stats.size < MAX_LOG_SIZE_BYTES) return; + + // Rotate: shift existing logs + for (let i = MAX_LOG_FILES - 1; i >= 1; i--) { + const older = `${logPath}.${i}`; + const newer = i === 1 ? logPath : `${logPath}.${i - 1}`; + if (fs.existsSync(newer)) { + fs.renameSync(newer, older); + } + } + } catch { + // Non-fatal + } +} + +function ensureStream() { + if (logStream) return logStream; + rotateIfNeeded(); + logStream = fs.createWriteStream(getLogFilePath(), { flags: 'a' }); + return logStream; +} + +function formatEntry(level, message, meta = {}) { + return JSON.stringify({ + t: new Date().toISOString(), + level, + msg: message, + pid: process.pid, + ...meta, + }) + '\n'; +} + +function write(level, message, meta) { + const entry = formatEntry(level, message, meta); + try { + ensureStream().write(entry); + } catch { + // Last resort — stdout + process.stdout.write(entry); + } + // Mirror to console in dev + if (!app.isPackaged) { + const consoleFn = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log; + consoleFn(`[${level.toUpperCase()}] ${message}`, Object.keys(meta).length ? meta : ''); + } +} + +const logger = { + info: (msg, meta) => write('info', msg, meta), + warn: (msg, meta) => write('warn', msg, meta), + error: (msg, meta) => write('error', msg, meta), + fatal: (msg, meta) => write('fatal', msg, meta), +}; + +function initCrashReporter() { + crashReporter.start({ + productName: 'TransTrack', + companyName: 'TransTrack Medical Software', + submitURL: '', // No remote submission — minidumps stored locally + uploadToServer: false, + compress: true, + }); + + // Capture unhandled exceptions and rejections + process.on('uncaughtException', (err) => { + logger.fatal('Uncaught exception', { error: err.message, stack: err.stack }); + // Attempt to flush before crashing + try { logStream?.end(); } catch { /* best effort */ } + }); + + process.on('unhandledRejection', (reason) => { + const msg = reason instanceof Error ? reason.message : String(reason); + const stack = reason instanceof Error ? reason.stack : undefined; + logger.error('Unhandled promise rejection', { error: msg, stack }); + }); + + logger.info('Crash reporter initialized', { + crashDumpsDir: app.getPath('crashDumps'), + }); +} + +function closeLogger() { + if (logStream) { + logStream.end(); + logStream = null; + } +} + +module.exports = { + logger, + initCrashReporter, + closeLogger, + getLogDir, +}; diff --git a/package-lock.json b/package-lock.json index 1fa1dbf..4fb6a13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,91 +10,92 @@ "hasInstallScript": true, "license": "Proprietary", "dependencies": { - "@hello-pangea/dnd": "^17.0.0", - "@hookform/resolvers": "^4.1.2", - "@radix-ui/react-accordion": "^1.2.3", - "@radix-ui/react-alert-dialog": "^1.1.6", - "@radix-ui/react-aspect-ratio": "^1.1.2", - "@radix-ui/react-avatar": "^1.1.3", - "@radix-ui/react-checkbox": "^1.1.4", - "@radix-ui/react-collapsible": "^1.1.3", - "@radix-ui/react-context-menu": "^2.2.6", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-dropdown-menu": "^2.1.6", - "@radix-ui/react-hover-card": "^1.1.6", - "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-menubar": "^1.1.6", - "@radix-ui/react-navigation-menu": "^1.2.5", - "@radix-ui/react-popover": "^1.1.6", - "@radix-ui/react-progress": "^1.1.2", - "@radix-ui/react-radio-group": "^1.2.3", - "@radix-ui/react-scroll-area": "^1.2.3", - "@radix-ui/react-select": "^2.1.6", - "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slider": "^1.2.3", - "@radix-ui/react-slot": "^1.1.2", - "@radix-ui/react-switch": "^1.1.3", - "@radix-ui/react-tabs": "^1.1.3", - "@radix-ui/react-toast": "^1.2.2", - "@radix-ui/react-toggle": "^1.1.2", - "@radix-ui/react-toggle-group": "^1.1.2", - "@radix-ui/react-tooltip": "^1.1.8", - "@tanstack/react-query": "^5.84.1", - "bcryptjs": "^2.4.3", - "better-sqlite3-multiple-ciphers": "^12.6.2", - "canvas-confetti": "^1.9.4", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.0.0", - "date-fns": "^3.6.0", - "embla-carousel-react": "^8.5.2", - "framer-motion": "^11.16.4", - "html2canvas": "^1.4.1", - "input-otp": "^1.4.2", - "jspdf": "^4.2.1", - "lodash": "^4.17.21", - "lucide-react": "^0.577.0", - "next-themes": "^0.4.4", - "react": "^18.2.0", - "react-day-picker": "^8.10.1", - "react-dom": "^18.2.0", - "react-hook-form": "^7.54.2", - "react-hot-toast": "^2.6.0", - "react-leaflet": "^4.2.1", - "react-markdown": "^9.0.1", - "react-resizable-panels": "^2.1.7", - "react-router-dom": "^6.26.0", - "recharts": "^2.15.4", - "sonner": "^2.0.1", - "tailwind-merge": "^3.0.2", - "tailwindcss-animate": "^1.0.7", - "three": "^0.183.1", - "uuid": "^9.0.0", - "vaul": "^1.1.2", - "zod": "^3.24.2" + "@hello-pangea/dnd": "17.0.0", + "@hookform/resolvers": "4.1.3", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-aspect-ratio": "1.1.8", + "@radix-ui/react-avatar": "1.1.11", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.8", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-progress": "1.1.8", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.8", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.4", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@tanstack/react-query": "5.95.0", + "bcryptjs": "2.4.3", + "better-sqlite3-multiple-ciphers": "12.6.2", + "canvas-confetti": "1.9.4", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "cmdk": "1.1.1", + "date-fns": "3.6.0", + "electron-updater": "6.8.3", + "embla-carousel-react": "8.6.0", + "framer-motion": "11.18.2", + "html2canvas": "1.4.1", + "input-otp": "1.4.2", + "jspdf": "4.2.1", + "lodash": "4.17.23", + "lucide-react": "0.577.0", + "next-themes": "0.4.6", + "react": "18.3.1", + "react-day-picker": "8.10.1", + "react-dom": "18.3.1", + "react-hook-form": "7.72.0", + "react-hot-toast": "2.6.0", + "react-leaflet": "4.2.1", + "react-markdown": "9.1.0", + "react-resizable-panels": "2.1.9", + "react-router-dom": "6.30.3", + "recharts": "2.15.4", + "sonner": "2.0.7", + "tailwind-merge": "3.4.0", + "tailwindcss-animate": "1.0.7", + "three": "0.183.2", + "uuid": "9.0.1", + "vaul": "1.1.2", + "zod": "3.25.76" }, "devDependencies": { - "@eslint/js": "^9.19.0", - "@types/node": "^25.2.3", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", - "@vitejs/plugin-react": "^4.3.4", - "autoprefixer": "^10.4.20", - "baseline-browser-mapping": "^2.8.32", - "concurrently": "^8.2.2", - "electron": "^35.7.5", - "electron-builder": "^26.6.0", - "eslint": "^9.19.0", - "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.5.0", - "eslint-plugin-unused-imports": "^4.3.0", - "globals": "^17.4.0", - "postcss": "^8.5.3", - "tailwindcss": "^3.4.17", - "typescript": "^5.8.2", - "vite": "^6.1.0", - "wait-on": "^9.0.4" + "@eslint/js": "9.39.2", + "@types/node": "25.5.0", + "@types/react": "18.3.27", + "@types/react-dom": "18.3.7", + "@vitejs/plugin-react": "4.7.0", + "autoprefixer": "10.4.27", + "baseline-browser-mapping": "2.10.10", + "concurrently": "8.2.2", + "electron": "35.7.5", + "electron-builder": "26.6.0", + "eslint": "9.39.2", + "eslint-plugin-react": "7.37.5", + "eslint-plugin-react-hooks": "5.2.0", + "eslint-plugin-react-refresh": "0.5.0", + "eslint-plugin-unused-imports": "4.3.0", + "globals": "17.4.0", + "postcss": "8.5.8", + "tailwindcss": "3.4.19", + "typescript": "5.9.3", + "vite": "6.4.1", + "wait-on": "9.0.4" } }, "node_modules/@alloc/quick-lru": { @@ -526,60 +527,6 @@ "global-agent": "^3.0.0" } }, - "node_modules/@electron/notarize": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", - "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.1", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/notarize/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/notarize/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/notarize/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/@electron/osx-sign": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", @@ -4654,6 +4601,60 @@ "semver": "bin/semver.js" } }, + "node_modules/app-builder-lib/node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/app-builder-lib/node_modules/ci-info": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", @@ -4767,7 +4768,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-hidden": { @@ -5299,7 +5299,6 @@ "version": "9.5.1", "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.4", @@ -6811,6 +6810,69 @@ "dev": true, "license": "ISC" }, + "node_modules/electron-updater": { + "version": "6.8.3", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz", + "integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==", + "license": "MIT", + "dependencies": { + "builder-util-runtime": "9.5.1", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "~7.7.3", + "tiny-typed-emitter": "^2.1.0" + } + }, + "node_modules/electron-updater/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-updater/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-updater/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-updater/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/electron-winstaller": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", @@ -8200,7 +8262,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/has-bigints": { @@ -9216,7 +9277,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -9337,7 +9397,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true, "license": "MIT" }, "node_modules/leaflet": { @@ -9401,6 +9460,19 @@ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -12341,7 +12413,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -13353,6 +13424,12 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", diff --git a/package.json b/package.json index 7653568..573bf4b 100644 --- a/package.json +++ b/package.json @@ -81,91 +81,92 @@ "postinstall": "electron-builder install-app-deps" }, "dependencies": { - "@hello-pangea/dnd": "^17.0.0", - "@hookform/resolvers": "^4.1.2", - "@radix-ui/react-accordion": "^1.2.3", - "@radix-ui/react-alert-dialog": "^1.1.6", - "@radix-ui/react-aspect-ratio": "^1.1.2", - "@radix-ui/react-avatar": "^1.1.3", - "@radix-ui/react-checkbox": "^1.1.4", - "@radix-ui/react-collapsible": "^1.1.3", - "@radix-ui/react-context-menu": "^2.2.6", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-dropdown-menu": "^2.1.6", - "@radix-ui/react-hover-card": "^1.1.6", - "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-menubar": "^1.1.6", - "@radix-ui/react-navigation-menu": "^1.2.5", - "@radix-ui/react-popover": "^1.1.6", - "@radix-ui/react-progress": "^1.1.2", - "@radix-ui/react-radio-group": "^1.2.3", - "@radix-ui/react-scroll-area": "^1.2.3", - "@radix-ui/react-select": "^2.1.6", - "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slider": "^1.2.3", - "@radix-ui/react-slot": "^1.1.2", - "@radix-ui/react-switch": "^1.1.3", - "@radix-ui/react-tabs": "^1.1.3", - "@radix-ui/react-toast": "^1.2.2", - "@radix-ui/react-toggle": "^1.1.2", - "@radix-ui/react-toggle-group": "^1.1.2", - "@radix-ui/react-tooltip": "^1.1.8", - "@tanstack/react-query": "^5.84.1", - "bcryptjs": "^2.4.3", - "better-sqlite3-multiple-ciphers": "^12.6.2", - "canvas-confetti": "^1.9.4", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.0.0", - "date-fns": "^3.6.0", - "embla-carousel-react": "^8.5.2", - "framer-motion": "^11.16.4", - "html2canvas": "^1.4.1", - "input-otp": "^1.4.2", - "jspdf": "^4.2.1", - "lodash": "^4.17.21", - "lucide-react": "^0.577.0", - "next-themes": "^0.4.4", - "react": "^18.2.0", - "react-day-picker": "^8.10.1", - "react-dom": "^18.2.0", - "react-hook-form": "^7.54.2", - "react-hot-toast": "^2.6.0", - "react-leaflet": "^4.2.1", - "react-markdown": "^9.0.1", - "react-resizable-panels": "^2.1.7", - "react-router-dom": "^6.26.0", - "recharts": "^2.15.4", - "sonner": "^2.0.1", - "tailwind-merge": "^3.0.2", - "tailwindcss-animate": "^1.0.7", - "three": "^0.183.1", - "uuid": "^9.0.0", - "vaul": "^1.1.2", - "zod": "^3.24.2" + "@hello-pangea/dnd": "17.0.0", + "@hookform/resolvers": "4.1.3", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-aspect-ratio": "1.1.8", + "@radix-ui/react-avatar": "1.1.11", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.8", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-progress": "1.1.8", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.8", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.4", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@tanstack/react-query": "5.95.0", + "bcryptjs": "2.4.3", + "better-sqlite3-multiple-ciphers": "12.6.2", + "canvas-confetti": "1.9.4", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "cmdk": "1.1.1", + "date-fns": "3.6.0", + "electron-updater": "6.8.3", + "embla-carousel-react": "8.6.0", + "framer-motion": "11.18.2", + "html2canvas": "1.4.1", + "input-otp": "1.4.2", + "jspdf": "4.2.1", + "lodash": "4.17.23", + "lucide-react": "0.577.0", + "next-themes": "0.4.6", + "react": "18.3.1", + "react-day-picker": "8.10.1", + "react-dom": "18.3.1", + "react-hook-form": "7.72.0", + "react-hot-toast": "2.6.0", + "react-leaflet": "4.2.1", + "react-markdown": "9.1.0", + "react-resizable-panels": "2.1.9", + "react-router-dom": "6.30.3", + "recharts": "2.15.4", + "sonner": "2.0.7", + "tailwind-merge": "3.4.0", + "tailwindcss-animate": "1.0.7", + "three": "0.183.2", + "uuid": "9.0.1", + "vaul": "1.1.2", + "zod": "3.25.76" }, "devDependencies": { - "@eslint/js": "^9.19.0", - "@types/node": "^25.2.3", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", - "@vitejs/plugin-react": "^4.3.4", - "autoprefixer": "^10.4.20", - "baseline-browser-mapping": "^2.8.32", - "concurrently": "^8.2.2", - "electron": "^35.7.5", - "electron-builder": "^26.6.0", - "eslint": "^9.19.0", - "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.5.0", - "eslint-plugin-unused-imports": "^4.3.0", - "globals": "^17.4.0", - "postcss": "^8.5.3", - "tailwindcss": "^3.4.17", - "typescript": "^5.8.2", - "vite": "^6.1.0", - "wait-on": "^9.0.4" + "@eslint/js": "9.39.2", + "@types/node": "25.5.0", + "@types/react": "18.3.27", + "@types/react-dom": "18.3.7", + "@vitejs/plugin-react": "4.7.0", + "autoprefixer": "10.4.27", + "baseline-browser-mapping": "2.10.10", + "concurrently": "8.2.2", + "electron": "35.7.5", + "electron-builder": "26.6.0", + "eslint": "9.39.2", + "eslint-plugin-react": "7.37.5", + "eslint-plugin-react-hooks": "5.2.0", + "eslint-plugin-react-refresh": "0.5.0", + "eslint-plugin-unused-imports": "4.3.0", + "globals": "17.4.0", + "postcss": "8.5.8", + "tailwindcss": "3.4.19", + "typescript": "5.9.3", + "vite": "6.4.1", + "wait-on": "9.0.4" }, "overrides": { "picomatch": "^4.0.4" diff --git a/scripts/notarize.cjs b/scripts/notarize.cjs new file mode 100644 index 0000000..ac41b40 --- /dev/null +++ b/scripts/notarize.cjs @@ -0,0 +1,42 @@ +/** + * macOS Notarization Script for electron-builder afterSign hook. + * + * Required environment variables: + * APPLE_ID – Apple Developer account email + * APPLE_APP_PASSWORD – App-specific password (not account password) + * APPLE_TEAM_ID – 10-character Team ID + * + * Skipped automatically on non-macOS platforms and when env vars are absent. + */ + +'use strict'; + +const { notarize } = require('@electron/notarize'); + +exports.default = async function notarizing(context) { + const { electronPlatformName, appOutDir } = context; + if (electronPlatformName !== 'darwin') return; + + const appleId = process.env.APPLE_ID; + const appleIdPassword = process.env.APPLE_APP_PASSWORD; + const teamId = process.env.APPLE_TEAM_ID; + + if (!appleId || !appleIdPassword || !teamId) { + console.warn('Skipping notarization: APPLE_ID, APPLE_APP_PASSWORD, or APPLE_TEAM_ID not set'); + return; + } + + const appName = context.packager.appInfo.productFilename; + + console.log(`Notarizing ${appName}...`); + + await notarize({ + appBundleId: context.packager.config.appId, + appPath: `${appOutDir}/${appName}.app`, + appleId, + appleIdPassword, + teamId, + }); + + console.log('Notarization complete.'); +}; diff --git a/src/api/localClient.js b/src/api/localClient.js index dcbf357..db66279 100644 --- a/src/api/localClient.js +++ b/src/api/localClient.js @@ -62,7 +62,7 @@ const mockClient = { { tier: 'professional', tierName: 'Professional', price: 7499, currency: 'USD' }, { tier: 'enterprise', tierName: 'Enterprise', price: 24999, currency: 'USD' }, ], - paypalEmail: 'lilnicole0383@gmail.com', + businessEmail: 'billing@transtrack.medical', contactEmail: 'Trans_Track@outlook.com', }), getPaymentInfo: async (tier) => ({ diff --git a/src/components/license/UpgradePrompt.jsx b/src/components/license/UpgradePrompt.jsx index 961789a..33a04f0 100644 --- a/src/components/license/UpgradePrompt.jsx +++ b/src/components/license/UpgradePrompt.jsx @@ -92,7 +92,8 @@ export default function UpgradePrompt({ enterprise: '24999', }; // Open PayPal payment - window.open(`https://www.paypal.me/lilnicole0383/${amounts[tier]}USD`, '_blank'); + const tierSlug = tier.toLowerCase(); + window.open(`https://buy.stripe.com/transtrack-${tierSlug}`, '_blank'); }; return ( diff --git a/src/pages/LicenseActivation.jsx b/src/pages/LicenseActivation.jsx index 3f7a48a..625f6c0 100644 --- a/src/pages/LicenseActivation.jsx +++ b/src/pages/LicenseActivation.jsx @@ -164,7 +164,7 @@ export default function LicenseActivation({ onActivated }) { const handlePayPal = (tier) => { const amount = TIER_CONFIG[tier].price; - window.open(`https://www.paypal.me/lilnicole0383/${amount}USD`, '_blank'); + window.open(`https://buy.stripe.com/transtrack-${tier.toLowerCase()}`, '_blank'); }; const handleContactSales = () => {