fhirsmith 0.8.2 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +4 -4
- package/package.json +1 -1
- package/packages/package-crawler.js +1 -1
- package/publisher/publisher.js +257 -218
- package/root-bare-template.html +0 -3
- package/server.js +27 -22
- package/shl/readme.md +27 -0
- package/shl/shl.js +2 -8
- package/tx/cs/cs-api.js +19 -4
- package/tx/cs/cs-base.js +13 -0
- package/tx/cs/cs-snomed.js +64 -3
- package/tx/library/codesystem.js +20 -0
- package/tx/library/designations.js +37 -1
- package/tx/provider.js +30 -0
- package/tx/workers/expand.js +19 -12
- package/tx/workers/lookup.js +19 -3
- package/tx/workers/worker.js +8 -2
- package/tx/xversion/xv-codesystem.js +3 -3
- package/tx/xversion/xv-parameters.js +2 -2
- package/tx/xversion/xv-valueset.js +1 -2
package/publisher/publisher.js
CHANGED
|
@@ -6,6 +6,7 @@ const bcrypt = require('bcrypt');
|
|
|
6
6
|
const session = require('express-session');
|
|
7
7
|
const folders = require('../library/folder-setup');
|
|
8
8
|
const escape = require('escape-html');
|
|
9
|
+
const {Utilities} = require("../library/utilities");
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class PublisherModule {
|
|
@@ -51,8 +52,8 @@ class PublisherModule {
|
|
|
51
52
|
async initializeDatabase() {
|
|
52
53
|
// Ensure database directory exists
|
|
53
54
|
const dbPath = path.isAbsolute(this.config.database)
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
? this.config.database
|
|
56
|
+
: folders.filePath('publisher', this.config.database);
|
|
56
57
|
const dbDir = path.dirname(dbPath);
|
|
57
58
|
const fs = require('fs');
|
|
58
59
|
if (!fs.existsSync(dbDir)) {
|
|
@@ -78,84 +79,84 @@ class PublisherModule {
|
|
|
78
79
|
const tables = [
|
|
79
80
|
// Users table
|
|
80
81
|
`CREATE TABLE IF NOT EXISTS users (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
83
|
+
name TEXT NOT NULL,
|
|
84
|
+
login TEXT UNIQUE NOT NULL,
|
|
85
|
+
password_hash TEXT NOT NULL,
|
|
86
|
+
is_admin BOOLEAN DEFAULT 0,
|
|
87
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
87
88
|
)`,
|
|
88
89
|
|
|
89
90
|
// Websites table
|
|
90
91
|
`CREATE TABLE IF NOT EXISTS websites (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
93
|
+
name TEXT NOT NULL,
|
|
94
|
+
local_folder TEXT NOT NULL,
|
|
95
|
+
history_templates TEXT NOT NULL,
|
|
96
|
+
web_templates TEXT NOT NULL,
|
|
97
|
+
server_update_script TEXT NOT NULL,
|
|
98
|
+
is_active BOOLEAN DEFAULT 1,
|
|
99
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
99
100
|
)`,
|
|
100
101
|
|
|
101
102
|
// User website permissions
|
|
102
103
|
`CREATE TABLE IF NOT EXISTS user_website_permissions (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
user_id INTEGER,
|
|
105
|
+
website_id INTEGER,
|
|
106
|
+
can_queue BOOLEAN DEFAULT 0,
|
|
107
|
+
can_approve BOOLEAN DEFAULT 0,
|
|
108
|
+
PRIMARY KEY (user_id, website_id),
|
|
109
|
+
FOREIGN KEY (user_id) REFERENCES users (id),
|
|
110
|
+
FOREIGN KEY (website_id) REFERENCES websites (id)
|
|
111
|
+
)`,
|
|
111
112
|
|
|
112
113
|
// Tasks table
|
|
113
114
|
`CREATE TABLE IF NOT EXISTS tasks (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
115
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
116
|
+
user_id INTEGER NOT NULL,
|
|
117
|
+
website_id INTEGER NOT NULL,
|
|
118
|
+
status TEXT DEFAULT 'queued',
|
|
119
|
+
github_org TEXT NOT NULL,
|
|
120
|
+
github_repo TEXT NOT NULL,
|
|
121
|
+
git_branch TEXT NOT NULL,
|
|
122
|
+
npm_package_id TEXT NOT NULL,
|
|
123
|
+
version TEXT NOT NULL,
|
|
124
|
+
local_folder TEXT,
|
|
125
|
+
build_output_path TEXT,
|
|
126
|
+
failure_reason TEXT,
|
|
127
|
+
announcement TEXT,
|
|
128
|
+
approved_by INTEGER,
|
|
129
|
+
queued_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
130
|
+
building_at DATETIME,
|
|
131
|
+
waiting_approval_at DATETIME,
|
|
132
|
+
publishing_at DATETIME,
|
|
133
|
+
completed_at DATETIME,
|
|
134
|
+
failed_at DATETIME,
|
|
135
|
+
FOREIGN KEY (user_id) REFERENCES users (id),
|
|
136
|
+
FOREIGN KEY (website_id) REFERENCES websites (id),
|
|
137
|
+
FOREIGN KEY (approved_by) REFERENCES users (id)
|
|
138
|
+
)`,
|
|
138
139
|
|
|
139
140
|
// Task logs
|
|
140
141
|
`CREATE TABLE IF NOT EXISTS task_logs (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
142
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
143
|
+
task_id TEXT NOT NULL,
|
|
144
|
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
145
|
+
level TEXT NOT NULL,
|
|
146
|
+
message TEXT NOT NULL,
|
|
147
|
+
FOREIGN KEY (task_id) REFERENCES tasks (id)
|
|
148
|
+
)`,
|
|
148
149
|
|
|
149
150
|
// User actions audit
|
|
150
151
|
`CREATE TABLE IF NOT EXISTS user_actions (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
152
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
153
|
+
user_id INTEGER,
|
|
154
|
+
action TEXT NOT NULL,
|
|
155
|
+
target_id TEXT,
|
|
156
|
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
157
|
+
ip_address TEXT,
|
|
158
|
+
FOREIGN KEY (user_id) REFERENCES users (id)
|
|
159
|
+
)`
|
|
159
160
|
];
|
|
160
161
|
|
|
161
162
|
for (const sql of tables) {
|
|
@@ -235,17 +236,17 @@ class PublisherModule {
|
|
|
235
236
|
}
|
|
236
237
|
|
|
237
238
|
this.db.run(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
239
|
+
'INSERT INTO users (name, login, password_hash, is_admin) VALUES (?, ?, ?, ?)',
|
|
240
|
+
['Administrator', 'admin', hash, 1],
|
|
241
|
+
(err) => {
|
|
242
|
+
if (err) {
|
|
243
|
+
this.logger.error('Failed to create default admin:', err);
|
|
244
|
+
reject(err);
|
|
245
|
+
} else {
|
|
246
|
+
this.logger.warn('Created default admin user - login: admin, password: admin123 - CHANGE THIS!');
|
|
247
|
+
resolve();
|
|
248
|
+
}
|
|
247
249
|
}
|
|
248
|
-
}
|
|
249
250
|
);
|
|
250
251
|
});
|
|
251
252
|
} else {
|
|
@@ -268,7 +269,7 @@ class PublisherModule {
|
|
|
268
269
|
this.router.get('/tasks', this.renderTasks.bind(this));
|
|
269
270
|
this.router.post('/tasks', this.requireAuth.bind(this), this.createTask.bind(this));
|
|
270
271
|
this.router.post('/tasks/:id/approve', this.requireAuth.bind(this), this.approveTask.bind(this));
|
|
271
|
-
this.router.post('/tasks/:id/delete', this.
|
|
272
|
+
this.router.post('/tasks/:id/delete', this.requireAuth.bind(this), this.deleteTask.bind(this));
|
|
272
273
|
this.router.post('/tasks/:id/retry', this.requireAuth.bind(this), this.retryTask.bind(this));
|
|
273
274
|
this.router.get('/tasks/:id/output', this.getTaskOutput.bind(this));
|
|
274
275
|
this.router.get('/tasks/:id/history', this.getTaskHistory.bind(this));
|
|
@@ -301,6 +302,8 @@ class PublisherModule {
|
|
|
301
302
|
this.logger.info('Starting task processor with ' + pollInterval + 'ms poll interval');
|
|
302
303
|
this.isProcessingStarted = null;
|
|
303
304
|
|
|
305
|
+
this.stats.addTask('Publisher', Utilities.formatDuration(pollInterval)); // or however you want to display the frequency
|
|
306
|
+
|
|
304
307
|
this.taskProcessor = setInterval(async () => {
|
|
305
308
|
if (this.shutdownRequested) return;
|
|
306
309
|
|
|
@@ -326,20 +329,25 @@ class PublisherModule {
|
|
|
326
329
|
// Look for queued tasks first (draft builds)
|
|
327
330
|
let task = await this.getNextQueuedTask();
|
|
328
331
|
if (task) {
|
|
332
|
+
this.stats.task('Publisher', 'Building ' + task.npm_package_id + '#' + task.version);
|
|
329
333
|
await this.processDraftBuild(task);
|
|
334
|
+
this.stats.taskDone('Publisher', 'Built ' + task.npm_package_id + '#' + task.version);
|
|
330
335
|
return;
|
|
331
336
|
}
|
|
332
337
|
|
|
333
338
|
// Then look for approved tasks (publishing)
|
|
334
339
|
task = await this.getNextApprovedTask();
|
|
335
340
|
if (task) {
|
|
341
|
+
this.stats.task('Publisher', 'Publishing ' + task.npm_package_id + '#' + task.version);
|
|
336
342
|
await this.processPublication(task);
|
|
343
|
+
this.stats.taskDone('Publisher', 'Published ' + task.npm_package_id + '#' + task.version);
|
|
337
344
|
return;
|
|
338
345
|
}
|
|
339
346
|
|
|
340
347
|
// No tasks to process
|
|
341
348
|
} catch (error) {
|
|
342
349
|
this.logger.error('Error in task processor:', error);
|
|
350
|
+
this.stats.taskError('Publisher', 'Error: ' + error.message);
|
|
343
351
|
} finally {
|
|
344
352
|
this.isProcessing = false;
|
|
345
353
|
}
|
|
@@ -348,12 +356,12 @@ class PublisherModule {
|
|
|
348
356
|
async getNextQueuedTask() {
|
|
349
357
|
return new Promise((resolve, reject) => {
|
|
350
358
|
this.db.get(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
359
|
+
'SELECT * FROM tasks WHERE status = ? ORDER BY queued_at ASC LIMIT 1',
|
|
360
|
+
['queued'],
|
|
361
|
+
(err, row) => {
|
|
362
|
+
if (err) reject(err);
|
|
363
|
+
else resolve(row);
|
|
364
|
+
}
|
|
357
365
|
);
|
|
358
366
|
});
|
|
359
367
|
}
|
|
@@ -361,12 +369,12 @@ class PublisherModule {
|
|
|
361
369
|
async getNextApprovedTask() {
|
|
362
370
|
return new Promise((resolve, reject) => {
|
|
363
371
|
this.db.get(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
372
|
+
'SELECT * FROM tasks WHERE status = ? ORDER BY publishing_at ASC LIMIT 1',
|
|
373
|
+
['publishing'],
|
|
374
|
+
(err, row) => {
|
|
375
|
+
if (err) reject(err);
|
|
376
|
+
else resolve(row);
|
|
377
|
+
}
|
|
370
378
|
);
|
|
371
379
|
});
|
|
372
380
|
}
|
|
@@ -396,12 +404,12 @@ class PublisherModule {
|
|
|
396
404
|
|
|
397
405
|
return new Promise((resolve, reject) => {
|
|
398
406
|
this.db.run(
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
407
|
+
'UPDATE tasks SET ' + fields.join(', ') + ' WHERE id = ?',
|
|
408
|
+
values,
|
|
409
|
+
(err) => {
|
|
410
|
+
if (err) reject(err);
|
|
411
|
+
else resolve();
|
|
412
|
+
}
|
|
405
413
|
);
|
|
406
414
|
});
|
|
407
415
|
}
|
|
@@ -409,9 +417,9 @@ class PublisherModule {
|
|
|
409
417
|
async logTaskMessage(taskId, level, message) {
|
|
410
418
|
return new Promise((resolve) => {
|
|
411
419
|
this.db.run(
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
420
|
+
'INSERT INTO task_logs (task_id, level, message) VALUES (?, ?, ?)',
|
|
421
|
+
[taskId.toString(), level, message],
|
|
422
|
+
() => resolve() // Don't fail if logging fails
|
|
415
423
|
);
|
|
416
424
|
});
|
|
417
425
|
}
|
|
@@ -467,8 +475,8 @@ class PublisherModule {
|
|
|
467
475
|
|
|
468
476
|
async runDraftBuild(task) {
|
|
469
477
|
const workspaceRoot = path.isAbsolute(this.config.workspaceRoot)
|
|
470
|
-
|
|
471
|
-
|
|
478
|
+
? this.config.workspaceRoot
|
|
479
|
+
: folders.filePath('publisher', this.config.workspaceRoot);
|
|
472
480
|
|
|
473
481
|
const taskDir = path.join(workspaceRoot, 'task-' + task.id);
|
|
474
482
|
const draftDir = path.join(taskDir, 'draft');
|
|
@@ -521,7 +529,7 @@ class PublisherModule {
|
|
|
521
529
|
// Get latest release info from GitHub API
|
|
522
530
|
const releaseResponse = await axios.get('https://api.github.com/repos/HL7/fhir-ig-publisher/releases/latest');
|
|
523
531
|
const downloadUrl = releaseResponse.data.assets.find(asset =>
|
|
524
|
-
|
|
532
|
+
asset.name === 'publisher.jar'
|
|
525
533
|
)?.browser_download_url;
|
|
526
534
|
|
|
527
535
|
if (!downloadUrl) {
|
|
@@ -710,13 +718,13 @@ class PublisherModule {
|
|
|
710
718
|
const templatesDir = path.join(taskDir, 'fhir-web-templates');
|
|
711
719
|
|
|
712
720
|
await this.runCommand('git', ['clone', 'git@github.com:FHIR/ig-registry.git', registryDir],
|
|
713
|
-
|
|
721
|
+
{}, task.id, 'Cloning ig-registry');
|
|
714
722
|
|
|
715
723
|
await this.runCommand('git', ['clone', 'https://github.com/HL7/fhir-ig-history-template.git', historyDir],
|
|
716
|
-
|
|
724
|
+
{}, task.id, 'Cloning fhir-ig-history-template');
|
|
717
725
|
|
|
718
726
|
await this.runCommand('git', ['clone', 'https://github.com/HL7/fhir-web-templates.git', templatesDir],
|
|
719
|
-
|
|
727
|
+
{}, task.id, 'Cloning fhir-web-templates');
|
|
720
728
|
|
|
721
729
|
// Step 2: Reuse the publisher.jar from the draft build
|
|
722
730
|
const publisherJar = path.join(taskDir, 'publisher.jar');
|
|
@@ -729,7 +737,7 @@ class PublisherModule {
|
|
|
729
737
|
|
|
730
738
|
// Step 4: Run the IG publisher in go-publish mode
|
|
731
739
|
await this.runPublisherGoPublish(task.id, publisherJar, draftDir, website.local_folder,
|
|
732
|
-
|
|
740
|
+
registryDir, historyDir, templatesDir, zipsDir, publishLogFile);
|
|
733
741
|
|
|
734
742
|
// Step 5: Verify publication succeeded by checking for the log file
|
|
735
743
|
const pubLogName = task.npm_package_id + '#' + task.version + '.log';
|
|
@@ -886,7 +894,7 @@ class PublisherModule {
|
|
|
886
894
|
content += '<a href="/publisher/admin/users" class="btn btn-secondary">Manage Users</a>';
|
|
887
895
|
}
|
|
888
896
|
content += '<form style="display: inline-block; margin-left: 10px;" method="post" action="/publisher/logout">';
|
|
889
|
-
content += '<button type="submit" class="btn btn-outline-secondary">Logout</button>';
|
|
897
|
+
content += '<button type="submit" class="btn btn-outline-secondary">' + escape(req.session.userName) + ' \u2014 Logout</button>';
|
|
890
898
|
content += '</form>';
|
|
891
899
|
content += '</div>';
|
|
892
900
|
} else {
|
|
@@ -924,7 +932,7 @@ class PublisherModule {
|
|
|
924
932
|
const html = htmlServer.renderPage('publisher', 'FHIR Publisher', content, {
|
|
925
933
|
taskCount: tasks.length,
|
|
926
934
|
templateVars: {
|
|
927
|
-
loginTitle: req.session.userId ?
|
|
935
|
+
loginTitle: req.session.userId ? (req.session.userName + ' \u2014 Logout') : 'Login',
|
|
928
936
|
loginPath: req.session.userId ? "logout" : 'login',
|
|
929
937
|
loginAction: req.session.userId ? "POST" : 'GET'
|
|
930
938
|
}
|
|
@@ -966,7 +974,7 @@ class PublisherModule {
|
|
|
966
974
|
|
|
967
975
|
const html = htmlServer.renderPage('publisher', 'Login - FHIR Publisher', content, {
|
|
968
976
|
templateVars: {
|
|
969
|
-
loginTitle: req.session.userId ?
|
|
977
|
+
loginTitle: req.session.userId ? (req.session.userName + ' \u2014 Logout') : 'Login',
|
|
970
978
|
loginPath: req.session.userId ? "logout" : 'login',
|
|
971
979
|
loginAction: req.session.userId ? "POST" : 'GET'
|
|
972
980
|
}});
|
|
@@ -986,12 +994,12 @@ class PublisherModule {
|
|
|
986
994
|
|
|
987
995
|
const user = await new Promise((resolve, reject) => {
|
|
988
996
|
this.db.get(
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
997
|
+
'SELECT * FROM users WHERE login = ?',
|
|
998
|
+
[login],
|
|
999
|
+
(err, row) => {
|
|
1000
|
+
if (err) reject(err);
|
|
1001
|
+
else resolve(row);
|
|
1002
|
+
}
|
|
995
1003
|
);
|
|
996
1004
|
});
|
|
997
1005
|
|
|
@@ -1100,7 +1108,15 @@ class PublisherModule {
|
|
|
1100
1108
|
|
|
1101
1109
|
for (const task of tasks) {
|
|
1102
1110
|
const canApprove = req.session.userId && task.status === 'waiting for approval' &&
|
|
1103
|
-
|
|
1111
|
+
await this.userCanApprove(req.session.userId, task.website_id);
|
|
1112
|
+
|
|
1113
|
+
// Determine if the current user can delete this task (mirrors deleteTask backend logic)
|
|
1114
|
+
const isPreApprovalStatus = task.status === 'waiting for approval' || task.status === 'queued' || (task.status === 'failed' && !task.approved_by);
|
|
1115
|
+
const isPostApprovalFailed = task.status === 'failed' && task.approved_by;
|
|
1116
|
+
const canDelete = req.session.userId && (
|
|
1117
|
+
(isPreApprovalStatus && await this.userCanQueue(req.session.userId, task.website_id)) ||
|
|
1118
|
+
(isPostApprovalFailed && req.session.isAdmin)
|
|
1119
|
+
);
|
|
1104
1120
|
|
|
1105
1121
|
content += '<tr>';
|
|
1106
1122
|
content += '<td><strong>#' + task.id + '</strong></td>';
|
|
@@ -1134,7 +1150,7 @@ class PublisherModule {
|
|
|
1134
1150
|
}
|
|
1135
1151
|
}
|
|
1136
1152
|
|
|
1137
|
-
if (
|
|
1153
|
+
if (canDelete) {
|
|
1138
1154
|
content += '<form method="post" action="/publisher/tasks/' + task.id + '/delete" style="display: inline;" onsubmit="return confirm(\'Delete task #' + task.id + ' and all its build output? This cannot be undone.\')">';
|
|
1139
1155
|
content += '<button type="submit" class="btn btn-sm btn-danger">Delete</button>';
|
|
1140
1156
|
content += '</form>';
|
|
@@ -1159,7 +1175,7 @@ class PublisherModule {
|
|
|
1159
1175
|
|
|
1160
1176
|
const html = htmlServer.renderPage('publisher', 'Tasks - FHIR Publisher', content, {
|
|
1161
1177
|
templateVars: {
|
|
1162
|
-
loginTitle: req.session.userId ?
|
|
1178
|
+
loginTitle: req.session.userId ? (req.session.userName + ' \u2014 Logout') : 'Login',
|
|
1163
1179
|
loginPath: req.session.userId ? "logout" : 'login',
|
|
1164
1180
|
loginAction: req.session.userId ? "POST" : 'GET'
|
|
1165
1181
|
}});
|
|
@@ -1198,12 +1214,12 @@ class PublisherModule {
|
|
|
1198
1214
|
// Insert task (ID will be auto-generated)
|
|
1199
1215
|
const result = await new Promise((resolve, reject) => {
|
|
1200
1216
|
this.db.run(
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1217
|
+
'INSERT INTO tasks (user_id, website_id, github_org, github_repo, git_branch, npm_package_id, version) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
1218
|
+
[req.session.userId, website_id, github_org, github_repo, git_branch, npm_package_id, version],
|
|
1219
|
+
function (err) {
|
|
1220
|
+
if (err) reject(err);
|
|
1221
|
+
else resolve(this.lastID);
|
|
1222
|
+
}
|
|
1207
1223
|
);
|
|
1208
1224
|
});
|
|
1209
1225
|
|
|
@@ -1248,12 +1264,12 @@ class PublisherModule {
|
|
|
1248
1264
|
// Update task status
|
|
1249
1265
|
await new Promise((resolve, reject) => {
|
|
1250
1266
|
this.db.run(
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1267
|
+
'UPDATE tasks SET status = ?, approved_by = ?, publishing_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
1268
|
+
['publishing', req.session.userId, taskId],
|
|
1269
|
+
(err) => {
|
|
1270
|
+
if (err) reject(err);
|
|
1271
|
+
else resolve();
|
|
1272
|
+
}
|
|
1257
1273
|
);
|
|
1258
1274
|
});
|
|
1259
1275
|
|
|
@@ -1282,8 +1298,22 @@ class PublisherModule {
|
|
|
1282
1298
|
return res.status(404).send('Task not found');
|
|
1283
1299
|
}
|
|
1284
1300
|
|
|
1285
|
-
if (task.status !== 'waiting for approval' && task.status !== 'failed') {
|
|
1286
|
-
return res.status(400).send('Only tasks waiting for approval or failed can be deleted');
|
|
1301
|
+
if (task.status !== 'waiting for approval' && task.status !== 'failed' && task.status !== 'queued') {
|
|
1302
|
+
return res.status(400).send('Only tasks that are queued, waiting for approval, or failed can be deleted');
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// Pre-approval statuses: any user with queue rights on this website can delete
|
|
1306
|
+
// Post-approval (failed after publishing started): admin only
|
|
1307
|
+
const isPostApproval = task.status === 'failed' && task.approved_by;
|
|
1308
|
+
if (isPostApproval) {
|
|
1309
|
+
if (!req.session.isAdmin) {
|
|
1310
|
+
return res.status(403).send('Admin access required to delete a task that has been approved');
|
|
1311
|
+
}
|
|
1312
|
+
} else {
|
|
1313
|
+
const canQueue = await this.userCanQueue(req.session.userId, task.website_id);
|
|
1314
|
+
if (!canQueue) {
|
|
1315
|
+
return res.status(403).send('You do not have permission to delete tasks for this website');
|
|
1316
|
+
}
|
|
1287
1317
|
}
|
|
1288
1318
|
|
|
1289
1319
|
// Remove build output directory
|
|
@@ -1312,7 +1342,7 @@ class PublisherModule {
|
|
|
1312
1342
|
});
|
|
1313
1343
|
|
|
1314
1344
|
this.logUserAction(req.session.userId, 'delete_task', taskId, req.ip);
|
|
1315
|
-
this.logger.info('Task deleted: ' + taskId + ' by
|
|
1345
|
+
this.logger.info('Task deleted: ' + taskId + ' by user ' + req.session.userId);
|
|
1316
1346
|
res.redirect('/publisher/tasks');
|
|
1317
1347
|
} catch (error) {
|
|
1318
1348
|
this.logger.error('Error deleting task:', error);
|
|
@@ -1337,9 +1367,9 @@ class PublisherModule {
|
|
|
1337
1367
|
if (existingTask) return res.status(400).send('An active task for this package and version is already in progress.');
|
|
1338
1368
|
const newTaskId = await new Promise((resolve, reject) => {
|
|
1339
1369
|
this.db.run(
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1370
|
+
'INSERT INTO tasks (user_id, website_id, github_org, github_repo, git_branch, npm_package_id, version) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
1371
|
+
[req.session.userId, task.website_id, task.github_org, task.github_repo, task.git_branch, task.npm_package_id, task.version],
|
|
1372
|
+
function (err) { if (err) reject(err); else resolve(this.lastID); }
|
|
1343
1373
|
);
|
|
1344
1374
|
});
|
|
1345
1375
|
this.logUserAction(req.session.userId, 'retry_task', newTaskId.toString(), req.ip);
|
|
@@ -1449,7 +1479,7 @@ class PublisherModule {
|
|
|
1449
1479
|
|
|
1450
1480
|
const html = htmlServer.renderPage('publisher', 'Task Output - FHIR Publisher', content, {
|
|
1451
1481
|
templateVars: {
|
|
1452
|
-
loginTitle: req.session.userId ?
|
|
1482
|
+
loginTitle: req.session.userId ? (req.session.userName + ' \u2014 Logout') : 'Login',
|
|
1453
1483
|
loginPath: req.session.userId ? "logout" : 'login',
|
|
1454
1484
|
loginAction: req.session.userId ? "POST" : 'GET'
|
|
1455
1485
|
}});
|
|
@@ -1640,8 +1670,8 @@ class PublisherModule {
|
|
|
1640
1670
|
for (const evt of events) {
|
|
1641
1671
|
const ts = new Date(evt.timestamp).toLocaleString();
|
|
1642
1672
|
const typeBadge = evt.type === 'status' ? '<span class="badge bg-primary">status</span>'
|
|
1643
|
-
|
|
1644
|
-
|
|
1673
|
+
: evt.type === 'action' ? '<span class="badge bg-info">user</span>'
|
|
1674
|
+
: '<span class="badge bg-secondary">log</span>';
|
|
1645
1675
|
content += '<tr>';
|
|
1646
1676
|
content += '<td class="text-nowrap"><small>' + ts + '</small></td>';
|
|
1647
1677
|
content += '<td>' + evt.icon + '</td>';
|
|
@@ -1669,7 +1699,16 @@ class PublisherModule {
|
|
|
1669
1699
|
content += '<button type="submit" class="btn btn-warning">Retry</button>';
|
|
1670
1700
|
content += '</form>';
|
|
1671
1701
|
}
|
|
1672
|
-
|
|
1702
|
+
// Show delete button using same logic as backend deleteTask:
|
|
1703
|
+
// Pre-approval (queued, waiting for approval, or failed without approval): user with queue rights can delete
|
|
1704
|
+
// Post-approval (failed after approval): admin only
|
|
1705
|
+
const detailIsPreApproval = task.status === 'waiting for approval' || task.status === 'queued' || (task.status === 'failed' && !task.approved_by);
|
|
1706
|
+
const detailIsPostApprovalFailed = task.status === 'failed' && task.approved_by;
|
|
1707
|
+
const detailCanDelete = req.session.userId && (
|
|
1708
|
+
(detailIsPreApproval && await this.userCanQueue(req.session.userId, task.website_id)) ||
|
|
1709
|
+
(detailIsPostApprovalFailed && req.session.isAdmin)
|
|
1710
|
+
);
|
|
1711
|
+
if (detailCanDelete) {
|
|
1673
1712
|
content += '<form method="post" action="/publisher/tasks/' + task.id + '/delete" style="display: inline;" class="me-2" onsubmit="return confirm(\'Delete task #' + task.id + ' and all its build output? This cannot be undone.\')">';
|
|
1674
1713
|
content += '<button type="submit" class="btn btn-danger">Delete</button>';
|
|
1675
1714
|
content += '</form>';
|
|
@@ -1679,7 +1718,7 @@ class PublisherModule {
|
|
|
1679
1718
|
|
|
1680
1719
|
const html = htmlServer.renderPage('publisher', 'Task History - FHIR Publisher', content, {
|
|
1681
1720
|
templateVars: {
|
|
1682
|
-
loginTitle: req.session.userId ?
|
|
1721
|
+
loginTitle: req.session.userId ? (req.session.userName + ' \u2014 Logout') : 'Login',
|
|
1683
1722
|
loginPath: req.session.userId ? "logout" : 'login',
|
|
1684
1723
|
loginAction: req.session.userId ? "POST" : 'GET'
|
|
1685
1724
|
}});
|
|
@@ -1722,7 +1761,7 @@ class PublisherModule {
|
|
|
1722
1761
|
content += '</form>';
|
|
1723
1762
|
|
|
1724
1763
|
const html = htmlServer.renderPage('publisher', 'Edit Website - FHIR Publisher', content, {
|
|
1725
|
-
templateVars: { loginTitle: 'Logout', loginPath: 'logout', loginAction: 'POST' }
|
|
1764
|
+
templateVars: { loginTitle: (req.session.userName || '') + ' \u2014 Logout', loginPath: 'logout', loginAction: 'POST' }
|
|
1726
1765
|
});
|
|
1727
1766
|
res.setHeader('Content-Type', 'text/html');
|
|
1728
1767
|
res.send(html);
|
|
@@ -1742,9 +1781,9 @@ class PublisherModule {
|
|
|
1742
1781
|
|
|
1743
1782
|
await new Promise((resolve, reject) => {
|
|
1744
1783
|
this.db.run(
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1784
|
+
'UPDATE websites SET name=?, local_folder=?, git_root = ?, history_templates=?, web_templates=?, server_update_script=?, is_active=? WHERE id=?',
|
|
1785
|
+
[name, local_folder, git_root, history_templates, web_templates, server_update_script, is_active === '1' ? 1 : 0, websiteId],
|
|
1786
|
+
(err) => err ? reject(err) : resolve()
|
|
1748
1787
|
);
|
|
1749
1788
|
});
|
|
1750
1789
|
|
|
@@ -1840,7 +1879,7 @@ class PublisherModule {
|
|
|
1840
1879
|
|
|
1841
1880
|
const html = htmlServer.renderPage('publisher', 'Websites - FHIR Publisher', content, {
|
|
1842
1881
|
templateVars: {
|
|
1843
|
-
loginTitle: req.session.userId ?
|
|
1882
|
+
loginTitle: req.session.userId ? (req.session.userName + ' \u2014 Logout') : 'Login',
|
|
1844
1883
|
loginPath: req.session.userId ? "logout" : 'login',
|
|
1845
1884
|
loginAction: req.session.userId ? "POST" : 'GET'
|
|
1846
1885
|
}});
|
|
@@ -1863,12 +1902,12 @@ class PublisherModule {
|
|
|
1863
1902
|
|
|
1864
1903
|
await new Promise((resolve, reject) => {
|
|
1865
1904
|
this.db.run(
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1905
|
+
'INSERT INTO websites (name, local_folder, git_root, history_templates, web_templates, server_update_script) VALUES (?, ?, ?, ?, ?, ?)',
|
|
1906
|
+
[name, local_folder, git_root, history_templates, web_templates, server_update_script],
|
|
1907
|
+
function (err) {
|
|
1908
|
+
if (err) reject(err);
|
|
1909
|
+
else resolve();
|
|
1910
|
+
}
|
|
1872
1911
|
);
|
|
1873
1912
|
});
|
|
1874
1913
|
|
|
@@ -1982,7 +2021,7 @@ class PublisherModule {
|
|
|
1982
2021
|
|
|
1983
2022
|
const html = htmlServer.renderPage('publisher', 'Users - FHIR Publisher', content, {
|
|
1984
2023
|
templateVars: {
|
|
1985
|
-
loginTitle: req.session.userId ?
|
|
2024
|
+
loginTitle: req.session.userId ? (req.session.userName + ' \u2014 Logout') : 'Login',
|
|
1986
2025
|
loginPath: req.session.userId ? "logout" : 'login',
|
|
1987
2026
|
loginAction: req.session.userId ? "POST" : 'GET'
|
|
1988
2027
|
}});
|
|
@@ -2007,12 +2046,12 @@ class PublisherModule {
|
|
|
2007
2046
|
|
|
2008
2047
|
await new Promise((resolve, reject) => {
|
|
2009
2048
|
this.db.run(
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2049
|
+
'INSERT INTO users (name, login, password_hash, is_admin) VALUES (?, ?, ?, ?)',
|
|
2050
|
+
[name, login, passwordHash, is_admin ? 1 : 0],
|
|
2051
|
+
function (err) {
|
|
2052
|
+
if (err) reject(err);
|
|
2053
|
+
else resolve();
|
|
2054
|
+
}
|
|
2016
2055
|
);
|
|
2017
2056
|
});
|
|
2018
2057
|
|
|
@@ -2057,12 +2096,12 @@ class PublisherModule {
|
|
|
2057
2096
|
if (canQueue || canApprove) {
|
|
2058
2097
|
await new Promise((resolve, reject) => {
|
|
2059
2098
|
this.db.run(
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2099
|
+
'INSERT INTO user_website_permissions (user_id, website_id, can_queue, can_approve) VALUES (?, ?, ?, ?)',
|
|
2100
|
+
[user_id, website.id, canQueue ? 1 : 0, canApprove ? 1 : 0],
|
|
2101
|
+
(err) => {
|
|
2102
|
+
if (err) reject(err);
|
|
2103
|
+
else resolve();
|
|
2104
|
+
}
|
|
2066
2105
|
);
|
|
2067
2106
|
});
|
|
2068
2107
|
}
|
|
@@ -2098,12 +2137,12 @@ class PublisherModule {
|
|
|
2098
2137
|
async getTask(taskId) {
|
|
2099
2138
|
return new Promise((resolve, reject) => {
|
|
2100
2139
|
this.db.get(
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2140
|
+
'SELECT t.*, u.name as user_name, u.login as user_login, w.name as website_name, approver.name as approved_by_name FROM tasks t JOIN users u ON t.user_id = u.id JOIN websites w ON t.website_id = w.id LEFT JOIN users approver ON t.approved_by = approver.id WHERE t.id = ?',
|
|
2141
|
+
[taskId],
|
|
2142
|
+
(err, row) => {
|
|
2143
|
+
if (err) reject(err);
|
|
2144
|
+
else resolve(row);
|
|
2145
|
+
}
|
|
2107
2146
|
);
|
|
2108
2147
|
});
|
|
2109
2148
|
}
|
|
@@ -2111,12 +2150,12 @@ class PublisherModule {
|
|
|
2111
2150
|
async getTaskLogs(taskId) {
|
|
2112
2151
|
return new Promise((resolve, reject) => {
|
|
2113
2152
|
this.db.all(
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2153
|
+
'SELECT * FROM task_logs WHERE task_id = ? ORDER BY timestamp ASC',
|
|
2154
|
+
[taskId.toString()],
|
|
2155
|
+
(err, rows) => {
|
|
2156
|
+
if (err) reject(err);
|
|
2157
|
+
else resolve(rows || []);
|
|
2158
|
+
}
|
|
2120
2159
|
);
|
|
2121
2160
|
});
|
|
2122
2161
|
}
|
|
@@ -2124,12 +2163,12 @@ class PublisherModule {
|
|
|
2124
2163
|
async getTaskActions(taskId) {
|
|
2125
2164
|
return new Promise((resolve, reject) => {
|
|
2126
2165
|
this.db.all(
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2166
|
+
'SELECT ua.*, u.name as user_name, u.login as user_login FROM user_actions ua LEFT JOIN users u ON ua.user_id = u.id WHERE ua.target_id = ? ORDER BY ua.timestamp ASC',
|
|
2167
|
+
[taskId.toString()],
|
|
2168
|
+
(err, rows) => {
|
|
2169
|
+
if (err) reject(err);
|
|
2170
|
+
else resolve(rows || []);
|
|
2171
|
+
}
|
|
2133
2172
|
);
|
|
2134
2173
|
});
|
|
2135
2174
|
}
|
|
@@ -2137,12 +2176,12 @@ class PublisherModule {
|
|
|
2137
2176
|
async getUserWebsites(userId) {
|
|
2138
2177
|
return new Promise((resolve, reject) => {
|
|
2139
2178
|
this.db.all(
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2179
|
+
'SELECT w.* FROM websites w JOIN user_website_permissions p ON w.id = p.website_id WHERE p.user_id = ? AND p.can_queue = 1 AND w.is_active = 1',
|
|
2180
|
+
[userId],
|
|
2181
|
+
(err, rows) => {
|
|
2182
|
+
if (err) reject(err);
|
|
2183
|
+
else resolve(rows || []);
|
|
2184
|
+
}
|
|
2146
2185
|
);
|
|
2147
2186
|
});
|
|
2148
2187
|
}
|
|
@@ -2150,12 +2189,12 @@ class PublisherModule {
|
|
|
2150
2189
|
async userCanQueue(userId, websiteId) {
|
|
2151
2190
|
return new Promise((resolve, reject) => {
|
|
2152
2191
|
this.db.get(
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2192
|
+
'SELECT can_queue FROM user_website_permissions WHERE user_id = ? AND website_id = ?',
|
|
2193
|
+
[userId, websiteId],
|
|
2194
|
+
(err, row) => {
|
|
2195
|
+
if (err) reject(err);
|
|
2196
|
+
else resolve(row && row.can_queue);
|
|
2197
|
+
}
|
|
2159
2198
|
);
|
|
2160
2199
|
});
|
|
2161
2200
|
}
|
|
@@ -2163,12 +2202,12 @@ class PublisherModule {
|
|
|
2163
2202
|
async userCanApprove(userId, websiteId) {
|
|
2164
2203
|
return new Promise((resolve, reject) => {
|
|
2165
2204
|
this.db.get(
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2205
|
+
'SELECT can_approve FROM user_website_permissions WHERE user_id = ? AND website_id = ?',
|
|
2206
|
+
[userId, websiteId],
|
|
2207
|
+
(err, row) => {
|
|
2208
|
+
if (err) reject(err);
|
|
2209
|
+
else resolve(row && row.can_approve);
|
|
2210
|
+
}
|
|
2172
2211
|
);
|
|
2173
2212
|
});
|
|
2174
2213
|
}
|
|
@@ -2176,12 +2215,12 @@ class PublisherModule {
|
|
|
2176
2215
|
async findActiveTask(packageId, version) {
|
|
2177
2216
|
return new Promise((resolve, reject) => {
|
|
2178
2217
|
this.db.get(
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2218
|
+
'SELECT * FROM tasks WHERE npm_package_id = ? AND version = ? AND status NOT IN (?, ?) ORDER BY queued_at DESC LIMIT 1',
|
|
2219
|
+
[packageId, version, 'complete', 'failed'],
|
|
2220
|
+
(err, row) => {
|
|
2221
|
+
if (err) reject(err);
|
|
2222
|
+
else resolve(row);
|
|
2223
|
+
}
|
|
2185
2224
|
);
|
|
2186
2225
|
});
|
|
2187
2226
|
}
|
|
@@ -2189,12 +2228,12 @@ class PublisherModule {
|
|
|
2189
2228
|
async findExistingTask(packageId, version) {
|
|
2190
2229
|
return new Promise((resolve, reject) => {
|
|
2191
2230
|
this.db.get(
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2231
|
+
'SELECT * FROM tasks WHERE npm_package_id = ? AND version = ? ORDER BY queued_at DESC LIMIT 1',
|
|
2232
|
+
[packageId, version],
|
|
2233
|
+
(err, row) => {
|
|
2234
|
+
if (err) reject(err);
|
|
2235
|
+
else resolve(row);
|
|
2236
|
+
}
|
|
2198
2237
|
);
|
|
2199
2238
|
});
|
|
2200
2239
|
}
|
|
@@ -2268,12 +2307,12 @@ class PublisherModule {
|
|
|
2268
2307
|
async getUserPermissions(userId) {
|
|
2269
2308
|
return new Promise((resolve, reject) => {
|
|
2270
2309
|
this.db.all(
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2310
|
+
'SELECT * FROM user_website_permissions WHERE user_id = ?',
|
|
2311
|
+
[userId],
|
|
2312
|
+
(err, rows) => {
|
|
2313
|
+
if (err) reject(err);
|
|
2314
|
+
else resolve(rows || []);
|
|
2315
|
+
}
|
|
2277
2316
|
);
|
|
2278
2317
|
});
|
|
2279
2318
|
}
|
|
@@ -2329,9 +2368,9 @@ class PublisherModule {
|
|
|
2329
2368
|
async logUserAction(userId, action, targetId, ipAddress) {
|
|
2330
2369
|
return new Promise((resolve) => {
|
|
2331
2370
|
this.db.run(
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2371
|
+
'INSERT INTO user_actions (user_id, action, target_id, ip_address) VALUES (?, ?, ?, ?)',
|
|
2372
|
+
[userId, action, targetId, ipAddress],
|
|
2373
|
+
() => resolve() // Don't fail if logging fails
|
|
2335
2374
|
);
|
|
2336
2375
|
});
|
|
2337
2376
|
}
|