@ratim818/allyve-wellness-backend 1.0.0
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/.env.example +49 -0
- package/.github/workflows/ci.yml +34 -0
- package/.github/workflows/publish.yml +81 -0
- package/README.md +140 -0
- package/docs/HIPAA_COMPLIANCE.md +141 -0
- package/docs/frontend-api-client.ts +259 -0
- package/package.json +60 -0
- package/src/config/database.ts +45 -0
- package/src/config/index.ts +52 -0
- package/src/middleware/auth.ts +167 -0
- package/src/middleware/security.ts +101 -0
- package/src/migrations/rollback.ts +17 -0
- package/src/migrations/run.ts +17 -0
- package/src/migrations/schema.ts +339 -0
- package/src/migrations/seed.ts +159 -0
- package/src/routes/appointments.ts +293 -0
- package/src/routes/audit.ts +29 -0
- package/src/routes/auth.ts +141 -0
- package/src/routes/health.ts +387 -0
- package/src/server.ts +124 -0
- package/src/services/audit.ts +117 -0
- package/src/services/auth.ts +293 -0
- package/src/services/encryption.ts +76 -0
- package/src/utils/logger.ts +57 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { db } from "../config/database.js";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
|
|
4
|
+
// ─── HIPAA DATABASE DESIGN PRINCIPLES ───────────────────────
|
|
5
|
+
// 1. All PHI stored encrypted (AES-256-GCM) at application level
|
|
6
|
+
// 2. HMAC hashes for indexed lookups on encrypted fields
|
|
7
|
+
// 3. Append-only audit_logs table (no UPDATE/DELETE triggers)
|
|
8
|
+
// 4. Row-level soft deletes (is_deleted flag, never hard delete)
|
|
9
|
+
// 5. Timestamps on every table for retention policy enforcement
|
|
10
|
+
// 6. Foreign keys enforce referential integrity
|
|
11
|
+
// ─────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export async function runMigrations() {
|
|
14
|
+
logger.info("Running database migrations...");
|
|
15
|
+
|
|
16
|
+
// ── Users ──────────────────────────────────────────────
|
|
17
|
+
if (!(await db.schema.hasTable("users"))) {
|
|
18
|
+
await db.schema.createTable("users", (t) => {
|
|
19
|
+
t.uuid("id").primary();
|
|
20
|
+
t.text("email_encrypted").notNullable(); // AES-256-GCM encrypted
|
|
21
|
+
t.string("email_hash", 64).notNullable().unique(); // HMAC-SHA256 for lookups
|
|
22
|
+
t.text("password_hash").notNullable(); // bcrypt
|
|
23
|
+
t.text("full_name_encrypted").notNullable();
|
|
24
|
+
t.text("date_of_birth_encrypted").notNullable();
|
|
25
|
+
t.text("phone_encrypted");
|
|
26
|
+
t.text("photo_url");
|
|
27
|
+
t.integer("pregnancy_week");
|
|
28
|
+
t.string("role", 20).notNullable().defaultTo("patient"); // patient | provider | admin
|
|
29
|
+
t.boolean("is_active").notNullable().defaultTo(true);
|
|
30
|
+
t.integer("login_attempts").notNullable().defaultTo(0);
|
|
31
|
+
t.timestamp("locked_until");
|
|
32
|
+
t.timestamp("last_login");
|
|
33
|
+
t.boolean("is_deleted").notNullable().defaultTo(false);
|
|
34
|
+
t.timestamps(true, true);
|
|
35
|
+
});
|
|
36
|
+
logger.info(" ✅ Created: users");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Sessions ───────────────────────────────────────────
|
|
40
|
+
if (!(await db.schema.hasTable("sessions"))) {
|
|
41
|
+
await db.schema.createTable("sessions", (t) => {
|
|
42
|
+
t.uuid("id").primary();
|
|
43
|
+
t.uuid("user_id").notNullable().references("id").inTable("users").onDelete("CASCADE");
|
|
44
|
+
t.string("ip_address", 45);
|
|
45
|
+
t.text("user_agent");
|
|
46
|
+
t.boolean("revoked").notNullable().defaultTo(false);
|
|
47
|
+
t.timestamp("expires_at").notNullable();
|
|
48
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
49
|
+
});
|
|
50
|
+
logger.info(" ✅ Created: sessions");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Refresh Tokens ─────────────────────────────────────
|
|
54
|
+
if (!(await db.schema.hasTable("refresh_tokens"))) {
|
|
55
|
+
await db.schema.createTable("refresh_tokens", (t) => {
|
|
56
|
+
t.uuid("id").primary();
|
|
57
|
+
t.uuid("user_id").notNullable().references("id").inTable("users").onDelete("CASCADE");
|
|
58
|
+
t.uuid("session_id").notNullable().references("id").inTable("sessions").onDelete("CASCADE");
|
|
59
|
+
t.string("token_hash", 64).notNullable().unique();
|
|
60
|
+
t.boolean("revoked").notNullable().defaultTo(false);
|
|
61
|
+
t.timestamp("expires_at").notNullable();
|
|
62
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
63
|
+
});
|
|
64
|
+
logger.info(" ✅ Created: refresh_tokens");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Wearable Devices ───────────────────────────────────
|
|
68
|
+
if (!(await db.schema.hasTable("wearable_devices"))) {
|
|
69
|
+
await db.schema.createTable("wearable_devices", (t) => {
|
|
70
|
+
t.uuid("id").primary();
|
|
71
|
+
t.uuid("user_id").notNullable().references("id").inTable("users").onDelete("CASCADE");
|
|
72
|
+
t.string("name").notNullable();
|
|
73
|
+
t.string("device_type", 50);
|
|
74
|
+
t.integer("battery_level");
|
|
75
|
+
t.boolean("is_connected").notNullable().defaultTo(false);
|
|
76
|
+
t.timestamp("last_synced");
|
|
77
|
+
t.timestamps(true, true);
|
|
78
|
+
});
|
|
79
|
+
logger.info(" ✅ Created: wearable_devices");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Health Metrics (PHI — encrypted) ───────────────────
|
|
83
|
+
if (!(await db.schema.hasTable("health_metrics"))) {
|
|
84
|
+
await db.schema.createTable("health_metrics", (t) => {
|
|
85
|
+
t.uuid("id").primary();
|
|
86
|
+
t.uuid("user_id").notNullable().references("id").inTable("users").onDelete("CASCADE");
|
|
87
|
+
t.string("metric_name", 50).notNullable(); // e.g., "Heart Rate", "Blood Pressure"
|
|
88
|
+
t.text("value_encrypted").notNullable(); // AES-256-GCM encrypted
|
|
89
|
+
t.string("unit", 20).notNullable();
|
|
90
|
+
t.string("status", 20).notNullable(); // normal | warning | critical
|
|
91
|
+
t.string("trend", 20); // rising | falling | stable | fluctuating
|
|
92
|
+
t.decimal("min_threshold");
|
|
93
|
+
t.decimal("max_threshold");
|
|
94
|
+
t.uuid("device_id").references("id").inTable("wearable_devices");
|
|
95
|
+
t.boolean("is_deleted").notNullable().defaultTo(false);
|
|
96
|
+
t.timestamp("recorded_at").notNullable();
|
|
97
|
+
t.timestamps(true, true);
|
|
98
|
+
|
|
99
|
+
t.index(["user_id", "metric_name", "recorded_at"]);
|
|
100
|
+
});
|
|
101
|
+
logger.info(" ✅ Created: health_metrics");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── ECG Data (PHI — encrypted, large payloads) ────────
|
|
105
|
+
if (!(await db.schema.hasTable("ecg_data"))) {
|
|
106
|
+
await db.schema.createTable("ecg_data", (t) => {
|
|
107
|
+
t.uuid("id").primary();
|
|
108
|
+
t.uuid("user_id").notNullable().references("id").inTable("users").onDelete("CASCADE");
|
|
109
|
+
t.uuid("health_metric_id").references("id").inTable("health_metrics");
|
|
110
|
+
t.text("waveform_encrypted").notNullable(); // Full ECG waveform data, encrypted
|
|
111
|
+
t.text("interpretation_encrypted"); // AI or physician interpretation
|
|
112
|
+
t.string("status", 20).notNullable();
|
|
113
|
+
t.integer("duration_seconds");
|
|
114
|
+
t.boolean("is_deleted").notNullable().defaultTo(false);
|
|
115
|
+
t.timestamp("recorded_at").notNullable();
|
|
116
|
+
t.timestamps(true, true);
|
|
117
|
+
});
|
|
118
|
+
logger.info(" ✅ Created: ecg_data");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Symptoms ───────────────────────────────────────────
|
|
122
|
+
if (!(await db.schema.hasTable("symptoms"))) {
|
|
123
|
+
await db.schema.createTable("symptoms", (t) => {
|
|
124
|
+
t.uuid("id").primary();
|
|
125
|
+
t.uuid("user_id").notNullable().references("id").inTable("users").onDelete("CASCADE");
|
|
126
|
+
t.string("name", 100).notNullable();
|
|
127
|
+
t.integer("severity").notNullable(); // 1-10
|
|
128
|
+
t.text("notes_encrypted"); // Encrypted free-text
|
|
129
|
+
t.boolean("is_cardiovascular_related").notNullable().defaultTo(false);
|
|
130
|
+
t.boolean("is_deleted").notNullable().defaultTo(false);
|
|
131
|
+
t.timestamp("recorded_at").notNullable();
|
|
132
|
+
t.timestamps(true, true);
|
|
133
|
+
|
|
134
|
+
t.index(["user_id", "recorded_at"]);
|
|
135
|
+
});
|
|
136
|
+
logger.info(" ✅ Created: symptoms");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Mood Entries ───────────────────────────────────────
|
|
140
|
+
if (!(await db.schema.hasTable("mood_entries"))) {
|
|
141
|
+
await db.schema.createTable("mood_entries", (t) => {
|
|
142
|
+
t.uuid("id").primary();
|
|
143
|
+
t.uuid("user_id").notNullable().references("id").inTable("users").onDelete("CASCADE");
|
|
144
|
+
t.string("mood", 20).notNullable();
|
|
145
|
+
t.text("notes_encrypted");
|
|
146
|
+
t.integer("anxiety_level"); // 1-10
|
|
147
|
+
t.boolean("is_deleted").notNullable().defaultTo(false);
|
|
148
|
+
t.timestamp("recorded_at").notNullable();
|
|
149
|
+
t.timestamps(true, true);
|
|
150
|
+
});
|
|
151
|
+
logger.info(" ✅ Created: mood_entries");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Journal Entries (PHI — encrypted) ──────────────────
|
|
155
|
+
if (!(await db.schema.hasTable("journal_entries"))) {
|
|
156
|
+
await db.schema.createTable("journal_entries", (t) => {
|
|
157
|
+
t.uuid("id").primary();
|
|
158
|
+
t.uuid("user_id").notNullable().references("id").inTable("users").onDelete("CASCADE");
|
|
159
|
+
t.text("content_encrypted").notNullable(); // Encrypted
|
|
160
|
+
t.specificType("tags", "text[]"); // PostgreSQL array
|
|
161
|
+
t.integer("pain_score");
|
|
162
|
+
t.integer("anxiety_level");
|
|
163
|
+
t.boolean("is_deleted").notNullable().defaultTo(false);
|
|
164
|
+
t.timestamp("recorded_at").notNullable();
|
|
165
|
+
t.timestamps(true, true);
|
|
166
|
+
});
|
|
167
|
+
logger.info(" ✅ Created: journal_entries");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Cardiovascular Risk Assessments ────────────────────
|
|
171
|
+
if (!(await db.schema.hasTable("cardiovascular_risks"))) {
|
|
172
|
+
await db.schema.createTable("cardiovascular_risks", (t) => {
|
|
173
|
+
t.uuid("id").primary();
|
|
174
|
+
t.uuid("user_id").notNullable().references("id").inTable("users").onDelete("CASCADE");
|
|
175
|
+
t.integer("overall_score").notNullable(); // 0-100
|
|
176
|
+
t.text("factors_encrypted").notNullable(); // JSON array, encrypted
|
|
177
|
+
t.string("trend", 20);
|
|
178
|
+
t.text("recommendations_encrypted"); // JSON array, encrypted
|
|
179
|
+
t.boolean("is_deleted").notNullable().defaultTo(false);
|
|
180
|
+
t.timestamp("assessed_at").notNullable();
|
|
181
|
+
t.timestamps(true, true);
|
|
182
|
+
});
|
|
183
|
+
logger.info(" ✅ Created: cardiovascular_risks");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Provider Contacts ──────────────────────────────────
|
|
187
|
+
if (!(await db.schema.hasTable("provider_contacts"))) {
|
|
188
|
+
await db.schema.createTable("provider_contacts", (t) => {
|
|
189
|
+
t.uuid("id").primary();
|
|
190
|
+
t.uuid("user_id").notNullable().references("id").inTable("users").onDelete("CASCADE");
|
|
191
|
+
t.text("name_encrypted").notNullable();
|
|
192
|
+
t.string("provider_type", 30).notNullable();
|
|
193
|
+
t.text("phone_encrypted");
|
|
194
|
+
t.text("email_encrypted");
|
|
195
|
+
t.text("address_encrypted");
|
|
196
|
+
t.string("preferred_contact_method", 20);
|
|
197
|
+
t.boolean("is_deleted").notNullable().defaultTo(false);
|
|
198
|
+
t.timestamps(true, true);
|
|
199
|
+
});
|
|
200
|
+
logger.info(" ✅ Created: provider_contacts");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Appointments ───────────────────────────────────────
|
|
204
|
+
if (!(await db.schema.hasTable("appointments"))) {
|
|
205
|
+
await db.schema.createTable("appointments", (t) => {
|
|
206
|
+
t.uuid("id").primary();
|
|
207
|
+
t.uuid("user_id").notNullable().references("id").inTable("users").onDelete("CASCADE");
|
|
208
|
+
t.uuid("provider_id").references("id").inTable("provider_contacts");
|
|
209
|
+
t.string("title", 200).notNullable();
|
|
210
|
+
t.string("provider_name", 100).notNullable();
|
|
211
|
+
t.string("provider_type", 30).notNullable();
|
|
212
|
+
t.timestamp("appointment_date").notNullable();
|
|
213
|
+
t.text("location_encrypted");
|
|
214
|
+
t.text("notes_encrypted");
|
|
215
|
+
t.boolean("confirmed").notNullable().defaultTo(false);
|
|
216
|
+
t.boolean("is_telehealth").notNullable().defaultTo(false);
|
|
217
|
+
t.boolean("is_deleted").notNullable().defaultTo(false);
|
|
218
|
+
t.timestamps(true, true);
|
|
219
|
+
|
|
220
|
+
t.index(["user_id", "appointment_date"]);
|
|
221
|
+
});
|
|
222
|
+
logger.info(" ✅ Created: appointments");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Shared Data Records ────────────────────────────────
|
|
226
|
+
if (!(await db.schema.hasTable("shared_data"))) {
|
|
227
|
+
await db.schema.createTable("shared_data", (t) => {
|
|
228
|
+
t.uuid("id").primary();
|
|
229
|
+
t.uuid("user_id").notNullable().references("id").inTable("users").onDelete("CASCADE");
|
|
230
|
+
t.text("recipient_name_encrypted").notNullable();
|
|
231
|
+
t.string("recipient_type", 20).notNullable();
|
|
232
|
+
t.text("recipient_email_encrypted");
|
|
233
|
+
t.specificType("data_types", "text[]").notNullable();
|
|
234
|
+
t.string("status", 20).notNullable().defaultTo("pending");
|
|
235
|
+
t.timestamp("shared_at").notNullable();
|
|
236
|
+
t.timestamp("expires_at");
|
|
237
|
+
t.timestamp("revoked_at");
|
|
238
|
+
t.timestamps(true, true);
|
|
239
|
+
});
|
|
240
|
+
logger.info(" ✅ Created: shared_data");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── Health Reports ─────────────────────────────────────
|
|
244
|
+
if (!(await db.schema.hasTable("health_reports"))) {
|
|
245
|
+
await db.schema.createTable("health_reports", (t) => {
|
|
246
|
+
t.uuid("id").primary();
|
|
247
|
+
t.uuid("user_id").notNullable().references("id").inTable("users").onDelete("CASCADE");
|
|
248
|
+
t.string("title", 200).notNullable();
|
|
249
|
+
t.text("summary_encrypted").notNullable();
|
|
250
|
+
t.string("doctor", 100);
|
|
251
|
+
t.text("recommendations_encrypted");
|
|
252
|
+
t.string("status", 20).notNullable().defaultTo("normal");
|
|
253
|
+
t.boolean("is_deleted").notNullable().defaultTo(false);
|
|
254
|
+
t.timestamp("report_date").notNullable();
|
|
255
|
+
t.timestamps(true, true);
|
|
256
|
+
});
|
|
257
|
+
logger.info(" ✅ Created: health_reports");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Consent Records (HIPAA required) ───────────────────
|
|
261
|
+
if (!(await db.schema.hasTable("consent_records"))) {
|
|
262
|
+
await db.schema.createTable("consent_records", (t) => {
|
|
263
|
+
t.uuid("id").primary();
|
|
264
|
+
t.uuid("user_id").notNullable().references("id").inTable("users").onDelete("CASCADE");
|
|
265
|
+
t.string("consent_type", 50).notNullable(); // data_processing | data_sharing | telehealth | research
|
|
266
|
+
t.boolean("granted").notNullable();
|
|
267
|
+
t.text("details");
|
|
268
|
+
t.string("ip_address", 45);
|
|
269
|
+
t.timestamp("granted_at").notNullable();
|
|
270
|
+
t.timestamp("revoked_at");
|
|
271
|
+
t.timestamps(true, true);
|
|
272
|
+
});
|
|
273
|
+
logger.info(" ✅ Created: consent_records");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── AUDIT LOGS (append-only, HIPAA §164.312(b)) ───────
|
|
277
|
+
if (!(await db.schema.hasTable("audit_logs"))) {
|
|
278
|
+
await db.schema.createTable("audit_logs", (t) => {
|
|
279
|
+
t.bigIncrements("id").primary();
|
|
280
|
+
t.uuid("user_id");
|
|
281
|
+
t.string("action", 30).notNullable();
|
|
282
|
+
t.string("resource_type", 30).notNullable();
|
|
283
|
+
t.uuid("resource_id");
|
|
284
|
+
t.text("details"); // JSON, never contains PHI
|
|
285
|
+
t.string("ip_address", 45);
|
|
286
|
+
t.text("user_agent");
|
|
287
|
+
t.string("outcome", 10).notNullable(); // success | failure | denied
|
|
288
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
289
|
+
|
|
290
|
+
// Indexes for compliance queries
|
|
291
|
+
t.index(["user_id", "created_at"]);
|
|
292
|
+
t.index(["action", "created_at"]);
|
|
293
|
+
t.index(["resource_type", "resource_id"]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// CRITICAL: Prevent DELETE/UPDATE on audit_logs
|
|
297
|
+
// This is enforced at the database level for tamper-evidence
|
|
298
|
+
await db.raw(`
|
|
299
|
+
CREATE OR REPLACE FUNCTION prevent_audit_modification()
|
|
300
|
+
RETURNS TRIGGER AS $$
|
|
301
|
+
BEGIN
|
|
302
|
+
RAISE EXCEPTION 'Audit logs are immutable. DELETE and UPDATE are prohibited.';
|
|
303
|
+
RETURN NULL;
|
|
304
|
+
END;
|
|
305
|
+
$$ LANGUAGE plpgsql;
|
|
306
|
+
|
|
307
|
+
CREATE TRIGGER audit_logs_no_update
|
|
308
|
+
BEFORE UPDATE ON audit_logs
|
|
309
|
+
FOR EACH ROW EXECUTE FUNCTION prevent_audit_modification();
|
|
310
|
+
|
|
311
|
+
CREATE TRIGGER audit_logs_no_delete
|
|
312
|
+
BEFORE DELETE ON audit_logs
|
|
313
|
+
FOR EACH ROW EXECUTE FUNCTION prevent_audit_modification();
|
|
314
|
+
`);
|
|
315
|
+
logger.info(" ✅ Created: audit_logs (immutable, with triggers)");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
logger.info("✅ All migrations complete");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export async function rollbackMigrations() {
|
|
322
|
+
const tables = [
|
|
323
|
+
"audit_logs", "consent_records", "health_reports", "shared_data",
|
|
324
|
+
"appointments", "provider_contacts", "cardiovascular_risks",
|
|
325
|
+
"journal_entries", "mood_entries", "symptoms", "ecg_data",
|
|
326
|
+
"health_metrics", "wearable_devices", "refresh_tokens", "sessions", "users",
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
// Drop triggers first
|
|
330
|
+
await db.raw("DROP TRIGGER IF EXISTS audit_logs_no_update ON audit_logs");
|
|
331
|
+
await db.raw("DROP TRIGGER IF EXISTS audit_logs_no_delete ON audit_logs");
|
|
332
|
+
await db.raw("DROP FUNCTION IF EXISTS prevent_audit_modification()");
|
|
333
|
+
|
|
334
|
+
for (const table of tables) {
|
|
335
|
+
await db.schema.dropTableIfExists(table);
|
|
336
|
+
logger.info(` 🗑️ Dropped: ${table}`);
|
|
337
|
+
}
|
|
338
|
+
logger.info("✅ Rollback complete");
|
|
339
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
import bcrypt from "bcryptjs";
|
|
3
|
+
import { testConnection, closeConnection, db } from "../config/database.js";
|
|
4
|
+
import { encrypt, encryptJSON, hmacHash } from "../services/encryption.js";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Seed the database with demo data matching the original Lovable frontend mock data.
|
|
9
|
+
* This lets you run the app immediately with realistic data.
|
|
10
|
+
*/
|
|
11
|
+
async function seed() {
|
|
12
|
+
await testConnection();
|
|
13
|
+
logger.info("Seeding database...");
|
|
14
|
+
|
|
15
|
+
const userId = uuidv4();
|
|
16
|
+
const passwordHash = await bcrypt.hash("SecureDemo123!", 12);
|
|
17
|
+
|
|
18
|
+
// ── User (Sarah Johnson from mock data) ────────────────
|
|
19
|
+
await db("users").insert({
|
|
20
|
+
id: userId,
|
|
21
|
+
email_encrypted: encrypt("sarah.johnson@example.com"),
|
|
22
|
+
email_hash: hmacHash("sarah.johnson@example.com"),
|
|
23
|
+
password_hash: passwordHash,
|
|
24
|
+
full_name_encrypted: encrypt("Sarah Johnson"),
|
|
25
|
+
date_of_birth_encrypted: encrypt("1993-06-15"),
|
|
26
|
+
phone_encrypted: encrypt("+1-555-0192"),
|
|
27
|
+
pregnancy_week: 24,
|
|
28
|
+
role: "patient",
|
|
29
|
+
is_active: true,
|
|
30
|
+
login_attempts: 0,
|
|
31
|
+
});
|
|
32
|
+
logger.info(" ✅ Seeded user: Sarah Johnson (sarah.johnson@example.com / SecureDemo123!)");
|
|
33
|
+
|
|
34
|
+
// ── Wearable Device ────────────────────────────────────
|
|
35
|
+
const deviceId = uuidv4();
|
|
36
|
+
await db("wearable_devices").insert({
|
|
37
|
+
id: deviceId,
|
|
38
|
+
user_id: userId,
|
|
39
|
+
name: "Allyve Maternal Monitor",
|
|
40
|
+
device_type: "maternal_monitor",
|
|
41
|
+
battery_level: 68,
|
|
42
|
+
is_connected: true,
|
|
43
|
+
last_synced: new Date(Date.now() - 15 * 60 * 1000),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ── Health Metrics ─────────────────────────────────────
|
|
47
|
+
const metrics = [
|
|
48
|
+
{ name: "Heart Rate", value: "92", unit: "bpm", status: "warning", trend: "rising" },
|
|
49
|
+
{ name: "Blood Pressure", value: "142/88", unit: "mmHg", status: "warning", trend: "rising" },
|
|
50
|
+
{ name: "Blood Glucose", value: "115", unit: "mg/dL", status: "warning", trend: "rising" },
|
|
51
|
+
{ name: "Oxygen Saturation", value: "94", unit: "%", status: "warning", trend: "falling" },
|
|
52
|
+
{ name: "ECG", value: "Ventricular Fibrillation detected", unit: "", status: "critical", trend: "fluctuating" },
|
|
53
|
+
{ name: "PPG", value: "Reduced perfusion", unit: "", status: "warning", trend: "fluctuating" },
|
|
54
|
+
{ name: "Sleep", value: "5.8", unit: "hours", status: "warning", trend: "falling" },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
for (const m of metrics) {
|
|
58
|
+
await db("health_metrics").insert({
|
|
59
|
+
id: uuidv4(),
|
|
60
|
+
user_id: userId,
|
|
61
|
+
metric_name: m.name,
|
|
62
|
+
value_encrypted: encrypt(m.value),
|
|
63
|
+
unit: m.unit,
|
|
64
|
+
status: m.status,
|
|
65
|
+
trend: m.trend,
|
|
66
|
+
device_id: deviceId,
|
|
67
|
+
recorded_at: new Date(),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
logger.info(` ✅ Seeded ${metrics.length} health metrics`);
|
|
71
|
+
|
|
72
|
+
// ── Symptoms ───────────────────────────────────────────
|
|
73
|
+
const symptoms = [
|
|
74
|
+
{ name: "Shortness of breath", severity: 3, cv: true, daysAgo: 2 },
|
|
75
|
+
{ name: "Heart palpitations", severity: 2, cv: true, daysAgo: 3, notes: "Occurred after climbing stairs" },
|
|
76
|
+
{ name: "Heartburn", severity: 4, cv: true, daysAgo: 1, notes: "After dinner" },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
for (const s of symptoms) {
|
|
80
|
+
await db("symptoms").insert({
|
|
81
|
+
id: uuidv4(),
|
|
82
|
+
user_id: userId,
|
|
83
|
+
name: s.name,
|
|
84
|
+
severity: s.severity,
|
|
85
|
+
notes_encrypted: s.notes ? encrypt(s.notes) : null,
|
|
86
|
+
is_cardiovascular_related: s.cv,
|
|
87
|
+
recorded_at: new Date(Date.now() - s.daysAgo * 24 * 60 * 60 * 1000),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
logger.info(` ✅ Seeded ${symptoms.length} symptoms`);
|
|
91
|
+
|
|
92
|
+
// ── Mood Entries ───────────────────────────────────────
|
|
93
|
+
const moods = [
|
|
94
|
+
{ mood: "happy", daysAgo: 1, notes: "Had a good prenatal appointment" },
|
|
95
|
+
{ mood: "neutral", daysAgo: 2 },
|
|
96
|
+
{ mood: "sad", daysAgo: 4, notes: "Feeling anxious about upcoming tests" },
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
for (const m of moods) {
|
|
100
|
+
await db("mood_entries").insert({
|
|
101
|
+
id: uuidv4(),
|
|
102
|
+
user_id: userId,
|
|
103
|
+
mood: m.mood,
|
|
104
|
+
notes_encrypted: m.notes ? encrypt(m.notes) : null,
|
|
105
|
+
recorded_at: new Date(Date.now() - m.daysAgo * 24 * 60 * 60 * 1000),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
logger.info(` ✅ Seeded ${moods.length} mood entries`);
|
|
109
|
+
|
|
110
|
+
// ── Cardiovascular Risk ────────────────────────────────
|
|
111
|
+
await db("cardiovascular_risks").insert({
|
|
112
|
+
id: uuidv4(),
|
|
113
|
+
user_id: userId,
|
|
114
|
+
overall_score: 48,
|
|
115
|
+
factors_encrypted: encryptJSON([
|
|
116
|
+
{ name: "Blood Pressure", contribution: 65, description: "Elevated and trending upward." },
|
|
117
|
+
{ name: "Heart Rate", contribution: 45, description: "Higher than expected for pregnancy stage." },
|
|
118
|
+
{ name: "Symptoms", contribution: 55, description: "Shortness of breath and palpitations are concerning." },
|
|
119
|
+
{ name: "Sleep Pattern", contribution: 40, description: "Sleep disturbances and reduced quality." },
|
|
120
|
+
]),
|
|
121
|
+
trend: "worsening",
|
|
122
|
+
recommendations_encrypted: encryptJSON([
|
|
123
|
+
"Contact your cardiologist within 24 hours",
|
|
124
|
+
"Reduce sodium intake immediately",
|
|
125
|
+
"Monitor blood pressure three times daily",
|
|
126
|
+
"Increase rest periods throughout the day",
|
|
127
|
+
"Report any new symptoms immediately",
|
|
128
|
+
]),
|
|
129
|
+
assessed_at: new Date(),
|
|
130
|
+
});
|
|
131
|
+
logger.info(" ✅ Seeded cardiovascular risk assessment");
|
|
132
|
+
|
|
133
|
+
// ── Journal Entries ────────────────────────────────────
|
|
134
|
+
await db("journal_entries").insert({
|
|
135
|
+
id: uuidv4(),
|
|
136
|
+
user_id: userId,
|
|
137
|
+
content_encrypted: encrypt("Feeling the baby kick more today. Had some back pain but otherwise feeling good."),
|
|
138
|
+
tags: ["movement", "pain", "backache"],
|
|
139
|
+
recorded_at: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await db("journal_entries").insert({
|
|
143
|
+
id: uuidv4(),
|
|
144
|
+
user_id: userId,
|
|
145
|
+
content_encrypted: encrypt("Spoke with my doctor about my last test results. Everything looks normal with the baby. Need to monitor my blood pressure more closely."),
|
|
146
|
+
tags: ["appointment", "tests", "blood pressure"],
|
|
147
|
+
recorded_at: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000),
|
|
148
|
+
});
|
|
149
|
+
logger.info(" ✅ Seeded 2 journal entries");
|
|
150
|
+
|
|
151
|
+
logger.info("\n✅ Seed complete! Demo login: sarah.johnson@example.com / SecureDemo123!");
|
|
152
|
+
|
|
153
|
+
await closeConnection();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
seed().catch((err) => {
|
|
157
|
+
logger.error("Seed failed:", err);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
});
|