Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 5 additions & 5 deletions PRICING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions docs/LICENSING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`

---

Expand Down
16 changes: 14 additions & 2 deletions electron-builder.enterprise.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
"to": "assets"
}
],
"publish": {
"provider": "github",
"owner": "TransTrackMedical",
"repo": "TransTrack",
"releaseType": "release"
},
"win": {
"target": [
{
Expand All @@ -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": [
Expand All @@ -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",
Expand Down
12 changes: 10 additions & 2 deletions electron-builder.evaluation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand All @@ -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"],
Expand Down
4 changes: 2 additions & 2 deletions electron/database/init.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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('');
}
Expand Down
55 changes: 50 additions & 5 deletions electron/database/migrations.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {
Expand All @@ -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')) {
Expand All @@ -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(
Expand All @@ -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(`
Expand All @@ -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');
}
}

/**
Expand Down Expand Up @@ -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();
Expand All @@ -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.
*/
Expand All @@ -147,6 +191,7 @@ function getMigrationStatus(db) {

module.exports = {
runMigrations,
rollbackLastMigration,
getMigrationStatus,
getCurrentVersion,
MIGRATIONS,
Expand Down
28 changes: 28 additions & 0 deletions electron/ipc/handlers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -66,6 +93,7 @@ function registerExtendedHandlers() {
}

function setupIPCHandlers() {
installRateLimitMiddleware();
authHandlers.register();
entityHandlers.register();
adminHandlers.register();
Expand Down
9 changes: 6 additions & 3 deletions electron/ipc/handlers/auth.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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') ||
Expand Down Expand Up @@ -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 };
Expand Down
5 changes: 3 additions & 2 deletions electron/ipc/rateLimiter.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -85,6 +85,7 @@ setInterval(() => {
}
}
}, CLEANUP_INTERVAL_MS);
if (cleanupTimer.unref) cleanupTimer.unref();

module.exports = {
checkRateLimit,
Expand Down
Loading
Loading