@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,293 @@
|
|
|
1
|
+
import { Router, Request, Response } from "express";
|
|
2
|
+
import { v4 as uuidv4 } from "uuid";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { db } from "../config/database.js";
|
|
5
|
+
import { encrypt, decrypt } from "../services/encryption.js";
|
|
6
|
+
import { writeAuditLog } from "../services/audit.js";
|
|
7
|
+
import { authenticate, validateSession } from "../middleware/auth.js";
|
|
8
|
+
|
|
9
|
+
export const appointmentRouter = Router();
|
|
10
|
+
appointmentRouter.use(authenticate, validateSession);
|
|
11
|
+
|
|
12
|
+
const appointmentSchema = z.object({
|
|
13
|
+
title: z.string().min(1).max(200),
|
|
14
|
+
providerName: z.string().min(1).max(100),
|
|
15
|
+
providerType: z.enum(["OBGYN", "Cardiologist", "Doula", "Lactation Specialist", "Pharmacist", "Therapist", "Dietician", "Other"]),
|
|
16
|
+
date: z.string().datetime(),
|
|
17
|
+
location: z.string().max(500).optional(),
|
|
18
|
+
notes: z.string().max(2000).optional(),
|
|
19
|
+
confirmed: z.boolean().optional(),
|
|
20
|
+
isTelehealth: z.boolean().optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// GET /appointments
|
|
24
|
+
appointmentRouter.get("/", async (req: Request, res: Response) => {
|
|
25
|
+
try {
|
|
26
|
+
const appointments = await db("appointments")
|
|
27
|
+
.where("user_id", req.user!.userId)
|
|
28
|
+
.where("is_deleted", false)
|
|
29
|
+
.orderBy("appointment_date", "asc");
|
|
30
|
+
|
|
31
|
+
const decrypted = appointments.map((a: any) => ({
|
|
32
|
+
id: a.id,
|
|
33
|
+
title: a.title,
|
|
34
|
+
providerName: a.provider_name,
|
|
35
|
+
providerType: a.provider_type,
|
|
36
|
+
date: a.appointment_date,
|
|
37
|
+
location: a.location_encrypted ? decrypt(a.location_encrypted) : null,
|
|
38
|
+
notes: a.notes_encrypted ? decrypt(a.notes_encrypted) : null,
|
|
39
|
+
confirmed: a.confirmed,
|
|
40
|
+
isTelehealth: a.is_telehealth,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
await writeAuditLog({
|
|
44
|
+
user_id: req.user!.userId,
|
|
45
|
+
action: "VIEW",
|
|
46
|
+
resource_type: "appointment",
|
|
47
|
+
ip_address: req.ip,
|
|
48
|
+
outcome: "success",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
res.json({ data: decrypted });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
res.status(500).json({ error: "Failed to fetch appointments" });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// POST /appointments
|
|
58
|
+
appointmentRouter.post("/", async (req: Request, res: Response) => {
|
|
59
|
+
try {
|
|
60
|
+
const data = appointmentSchema.parse(req.body);
|
|
61
|
+
const id = uuidv4();
|
|
62
|
+
|
|
63
|
+
await db("appointments").insert({
|
|
64
|
+
id,
|
|
65
|
+
user_id: req.user!.userId,
|
|
66
|
+
title: data.title,
|
|
67
|
+
provider_name: data.providerName,
|
|
68
|
+
provider_type: data.providerType,
|
|
69
|
+
appointment_date: new Date(data.date),
|
|
70
|
+
location_encrypted: data.location ? encrypt(data.location) : null,
|
|
71
|
+
notes_encrypted: data.notes ? encrypt(data.notes) : null,
|
|
72
|
+
confirmed: data.confirmed ?? false,
|
|
73
|
+
is_telehealth: data.isTelehealth ?? false,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await writeAuditLog({
|
|
77
|
+
user_id: req.user!.userId,
|
|
78
|
+
action: "CREATE",
|
|
79
|
+
resource_type: "appointment",
|
|
80
|
+
resource_id: id,
|
|
81
|
+
ip_address: req.ip,
|
|
82
|
+
outcome: "success",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
res.status(201).json({ id, message: "Appointment created" });
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (err instanceof z.ZodError) {
|
|
88
|
+
res.status(400).json({ error: "Validation failed", details: err.errors });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
res.status(500).json({ error: "Failed to create appointment" });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// PUT /appointments/:id
|
|
96
|
+
appointmentRouter.put("/:id", async (req: Request, res: Response) => {
|
|
97
|
+
try {
|
|
98
|
+
const data = appointmentSchema.partial().parse(req.body);
|
|
99
|
+
|
|
100
|
+
const existing = await db("appointments")
|
|
101
|
+
.where("id", req.params.id)
|
|
102
|
+
.where("user_id", req.user!.userId)
|
|
103
|
+
.where("is_deleted", false)
|
|
104
|
+
.first();
|
|
105
|
+
|
|
106
|
+
if (!existing) {
|
|
107
|
+
res.status(404).json({ error: "Appointment not found" });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const updates: Record<string, unknown> = { updated_at: new Date() };
|
|
112
|
+
if (data.title) updates.title = data.title;
|
|
113
|
+
if (data.providerName) updates.provider_name = data.providerName;
|
|
114
|
+
if (data.providerType) updates.provider_type = data.providerType;
|
|
115
|
+
if (data.date) updates.appointment_date = new Date(data.date);
|
|
116
|
+
if (data.location !== undefined) updates.location_encrypted = data.location ? encrypt(data.location) : null;
|
|
117
|
+
if (data.notes !== undefined) updates.notes_encrypted = data.notes ? encrypt(data.notes) : null;
|
|
118
|
+
if (data.confirmed !== undefined) updates.confirmed = data.confirmed;
|
|
119
|
+
if (data.isTelehealth !== undefined) updates.is_telehealth = data.isTelehealth;
|
|
120
|
+
|
|
121
|
+
await db("appointments").where("id", req.params.id).update(updates);
|
|
122
|
+
|
|
123
|
+
await writeAuditLog({
|
|
124
|
+
user_id: req.user!.userId,
|
|
125
|
+
action: "UPDATE",
|
|
126
|
+
resource_type: "appointment",
|
|
127
|
+
resource_id: req.params.id,
|
|
128
|
+
ip_address: req.ip,
|
|
129
|
+
outcome: "success",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
res.json({ message: "Appointment updated" });
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (err instanceof z.ZodError) {
|
|
135
|
+
res.status(400).json({ error: "Validation failed", details: err.errors });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
res.status(500).json({ error: "Failed to update appointment" });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// DELETE /appointments/:id (soft delete)
|
|
143
|
+
appointmentRouter.delete("/:id", async (req: Request, res: Response) => {
|
|
144
|
+
try {
|
|
145
|
+
const result = await db("appointments")
|
|
146
|
+
.where("id", req.params.id)
|
|
147
|
+
.where("user_id", req.user!.userId)
|
|
148
|
+
.update({ is_deleted: true, updated_at: new Date() });
|
|
149
|
+
|
|
150
|
+
if (!result) {
|
|
151
|
+
res.status(404).json({ error: "Appointment not found" });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await writeAuditLog({
|
|
156
|
+
user_id: req.user!.userId,
|
|
157
|
+
action: "DELETE",
|
|
158
|
+
resource_type: "appointment",
|
|
159
|
+
resource_id: req.params.id,
|
|
160
|
+
ip_address: req.ip,
|
|
161
|
+
outcome: "success",
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
res.json({ message: "Appointment deleted" });
|
|
165
|
+
} catch (err) {
|
|
166
|
+
res.status(500).json({ error: "Failed to delete appointment" });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ════════════════════════════════════════════════════════════
|
|
171
|
+
// DATA SHARING (HIPAA-compliant with consent tracking)
|
|
172
|
+
// ════════════════════════════════════════════════════════════
|
|
173
|
+
|
|
174
|
+
export const shareRouter = Router();
|
|
175
|
+
shareRouter.use(authenticate, validateSession);
|
|
176
|
+
|
|
177
|
+
const shareSchema = z.object({
|
|
178
|
+
recipientName: z.string().min(1).max(100),
|
|
179
|
+
recipientType: z.enum(["provider", "family", "other"]),
|
|
180
|
+
recipientEmail: z.string().email().optional(),
|
|
181
|
+
dataTypes: z.array(z.enum(["health_metrics", "symptoms", "mood", "journal", "appointments"])).min(1),
|
|
182
|
+
expiresInDays: z.number().int().min(1).max(365).optional(),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
shareRouter.get("/", async (req: Request, res: Response) => {
|
|
186
|
+
try {
|
|
187
|
+
const shares = await db("shared_data")
|
|
188
|
+
.where("user_id", req.user!.userId)
|
|
189
|
+
.orderBy("shared_at", "desc");
|
|
190
|
+
|
|
191
|
+
const decrypted = shares.map((s: any) => ({
|
|
192
|
+
id: s.id,
|
|
193
|
+
recipientName: decrypt(s.recipient_name_encrypted),
|
|
194
|
+
recipientType: s.recipient_type,
|
|
195
|
+
recipientEmail: s.recipient_email_encrypted ? decrypt(s.recipient_email_encrypted) : null,
|
|
196
|
+
dataTypes: s.data_types,
|
|
197
|
+
status: s.status,
|
|
198
|
+
sharedAt: s.shared_at,
|
|
199
|
+
expiresAt: s.expires_at,
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
await writeAuditLog({
|
|
203
|
+
user_id: req.user!.userId,
|
|
204
|
+
action: "VIEW",
|
|
205
|
+
resource_type: "shared_data",
|
|
206
|
+
ip_address: req.ip,
|
|
207
|
+
outcome: "success",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
res.json({ data: decrypted });
|
|
211
|
+
} catch (err) {
|
|
212
|
+
res.status(500).json({ error: "Failed to fetch shared data" });
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
shareRouter.post("/", async (req: Request, res: Response) => {
|
|
217
|
+
try {
|
|
218
|
+
const data = shareSchema.parse(req.body);
|
|
219
|
+
const id = uuidv4();
|
|
220
|
+
|
|
221
|
+
await db("shared_data").insert({
|
|
222
|
+
id,
|
|
223
|
+
user_id: req.user!.userId,
|
|
224
|
+
recipient_name_encrypted: encrypt(data.recipientName),
|
|
225
|
+
recipient_type: data.recipientType,
|
|
226
|
+
recipient_email_encrypted: data.recipientEmail ? encrypt(data.recipientEmail) : null,
|
|
227
|
+
data_types: data.dataTypes,
|
|
228
|
+
status: "active",
|
|
229
|
+
shared_at: new Date(),
|
|
230
|
+
expires_at: data.expiresInDays
|
|
231
|
+
? new Date(Date.now() + data.expiresInDays * 24 * 60 * 60 * 1000)
|
|
232
|
+
: null,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Record consent
|
|
236
|
+
await db("consent_records").insert({
|
|
237
|
+
id: uuidv4(),
|
|
238
|
+
user_id: req.user!.userId,
|
|
239
|
+
consent_type: "data_sharing",
|
|
240
|
+
granted: true,
|
|
241
|
+
details: JSON.stringify({ shareId: id, dataTypes: data.dataTypes, recipient: data.recipientName }),
|
|
242
|
+
ip_address: req.ip,
|
|
243
|
+
granted_at: new Date(),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
await writeAuditLog({
|
|
247
|
+
user_id: req.user!.userId,
|
|
248
|
+
action: "SHARE",
|
|
249
|
+
resource_type: "shared_data",
|
|
250
|
+
resource_id: id,
|
|
251
|
+
details: { dataTypes: data.dataTypes, recipientType: data.recipientType },
|
|
252
|
+
ip_address: req.ip,
|
|
253
|
+
outcome: "success",
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
res.status(201).json({ id, message: "Data shared successfully" });
|
|
257
|
+
} catch (err) {
|
|
258
|
+
if (err instanceof z.ZodError) {
|
|
259
|
+
res.status(400).json({ error: "Validation failed", details: err.errors });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
res.status(500).json({ error: "Failed to share data" });
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// POST /share/:id/revoke — revoke a data share
|
|
267
|
+
shareRouter.post("/:id/revoke", async (req: Request, res: Response) => {
|
|
268
|
+
try {
|
|
269
|
+
const result = await db("shared_data")
|
|
270
|
+
.where("id", req.params.id)
|
|
271
|
+
.where("user_id", req.user!.userId)
|
|
272
|
+
.where("status", "active")
|
|
273
|
+
.update({ status: "revoked", revoked_at: new Date() });
|
|
274
|
+
|
|
275
|
+
if (!result) {
|
|
276
|
+
res.status(404).json({ error: "Active share not found" });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await writeAuditLog({
|
|
281
|
+
user_id: req.user!.userId,
|
|
282
|
+
action: "REVOKE_SHARE",
|
|
283
|
+
resource_type: "shared_data",
|
|
284
|
+
resource_id: req.params.id,
|
|
285
|
+
ip_address: req.ip,
|
|
286
|
+
outcome: "success",
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
res.json({ message: "Data sharing revoked" });
|
|
290
|
+
} catch (err) {
|
|
291
|
+
res.status(500).json({ error: "Failed to revoke share" });
|
|
292
|
+
}
|
|
293
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Router, Request, Response } from "express";
|
|
2
|
+
import { authenticate, validateSession, requireRole } from "../middleware/auth.js";
|
|
3
|
+
import { queryAuditLogs } from "../services/audit.js";
|
|
4
|
+
|
|
5
|
+
export const auditRouter = Router();
|
|
6
|
+
|
|
7
|
+
// Only admin/compliance roles can view audit logs
|
|
8
|
+
auditRouter.use(authenticate, validateSession, requireRole("admin"));
|
|
9
|
+
|
|
10
|
+
// GET /audit/logs — query audit trail
|
|
11
|
+
auditRouter.get("/logs", async (req: Request, res: Response) => {
|
|
12
|
+
try {
|
|
13
|
+
const { userId, action, resourceType, startDate, endDate, limit, offset } = req.query;
|
|
14
|
+
|
|
15
|
+
const logs = await queryAuditLogs({
|
|
16
|
+
userId: userId as string,
|
|
17
|
+
action: action as any,
|
|
18
|
+
resourceType: resourceType as any,
|
|
19
|
+
startDate: startDate ? new Date(startDate as string) : undefined,
|
|
20
|
+
endDate: endDate ? new Date(endDate as string) : undefined,
|
|
21
|
+
limit: limit ? Number(limit) : 100,
|
|
22
|
+
offset: offset ? Number(offset) : 0,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
res.json({ data: logs, total: logs.length });
|
|
26
|
+
} catch (err) {
|
|
27
|
+
res.status(500).json({ error: "Failed to query audit logs" });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Router, Request, Response } from "express";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { registerUser, loginUser, refreshAccessToken, logoutUser } from "../services/auth.js";
|
|
4
|
+
import { authenticate, validateSession } from "../middleware/auth.js";
|
|
5
|
+
import { writeAuditLog } from "../services/audit.js";
|
|
6
|
+
|
|
7
|
+
export const authRouter = Router();
|
|
8
|
+
|
|
9
|
+
// ── Validation Schemas ──────────────────────────────────
|
|
10
|
+
const registerSchema = z.object({
|
|
11
|
+
email: z.string().email("Invalid email"),
|
|
12
|
+
password: z
|
|
13
|
+
.string()
|
|
14
|
+
.min(12, "Password must be at least 12 characters")
|
|
15
|
+
.regex(/[A-Z]/, "Must include an uppercase letter")
|
|
16
|
+
.regex(/[a-z]/, "Must include a lowercase letter")
|
|
17
|
+
.regex(/[0-9]/, "Must include a number")
|
|
18
|
+
.regex(/[^A-Za-z0-9]/, "Must include a special character"),
|
|
19
|
+
fullName: z.string().min(2).max(100),
|
|
20
|
+
dateOfBirth: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use YYYY-MM-DD format"),
|
|
21
|
+
phone: z.string().optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const loginSchema = z.object({
|
|
25
|
+
email: z.string().email(),
|
|
26
|
+
password: z.string().min(1),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// ── POST /auth/register ─────────────────────────────────
|
|
30
|
+
authRouter.post("/register", async (req: Request, res: Response) => {
|
|
31
|
+
try {
|
|
32
|
+
const data = registerSchema.parse(req.body);
|
|
33
|
+
const result = await registerUser(data);
|
|
34
|
+
res.status(201).json({ message: "Account created", userId: result.userId });
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (err instanceof z.ZodError) {
|
|
37
|
+
res.status(400).json({ error: "Validation failed", details: err.errors });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const message = (err as Error).message;
|
|
41
|
+
const status = message.includes("already exists") ? 409 : 500;
|
|
42
|
+
res.status(status).json({ error: message });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ── POST /auth/login ────────────────────────────────────
|
|
47
|
+
authRouter.post("/login", async (req: Request, res: Response) => {
|
|
48
|
+
try {
|
|
49
|
+
const data = loginSchema.parse(req.body);
|
|
50
|
+
const tokens = await loginUser(data.email, data.password, req.ip, req.get("user-agent"));
|
|
51
|
+
|
|
52
|
+
// Set refresh token as httpOnly cookie (more secure than localStorage)
|
|
53
|
+
res.cookie("refreshToken", tokens.refreshToken, {
|
|
54
|
+
httpOnly: true,
|
|
55
|
+
secure: process.env.NODE_ENV === "production",
|
|
56
|
+
sameSite: "strict",
|
|
57
|
+
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
58
|
+
path: "/api/v1/auth/refresh",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
res.json({
|
|
62
|
+
accessToken: tokens.accessToken,
|
|
63
|
+
expiresIn: tokens.expiresIn,
|
|
64
|
+
});
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (err instanceof z.ZodError) {
|
|
67
|
+
res.status(400).json({ error: "Validation failed", details: err.errors });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const message = (err as Error).message;
|
|
71
|
+
const status = message.includes("locked") ? 423 : 401;
|
|
72
|
+
res.status(status).json({ error: message });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ── POST /auth/refresh ──────────────────────────────────
|
|
77
|
+
authRouter.post("/refresh", async (req: Request, res: Response) => {
|
|
78
|
+
try {
|
|
79
|
+
const refreshToken = req.cookies?.refreshToken || req.body.refreshToken;
|
|
80
|
+
if (!refreshToken) {
|
|
81
|
+
res.status(401).json({ error: "Refresh token required" });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const tokens = await refreshAccessToken(refreshToken);
|
|
86
|
+
res.json({
|
|
87
|
+
accessToken: tokens.accessToken,
|
|
88
|
+
expiresIn: tokens.expiresIn,
|
|
89
|
+
});
|
|
90
|
+
} catch (err) {
|
|
91
|
+
res.status(401).json({ error: (err as Error).message });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ── POST /auth/logout ───────────────────────────────────
|
|
96
|
+
authRouter.post("/logout", authenticate, async (req: Request, res: Response) => {
|
|
97
|
+
try {
|
|
98
|
+
await logoutUser(req.user!.userId, req.user!.sessionId, req.ip);
|
|
99
|
+
|
|
100
|
+
res.clearCookie("refreshToken", { path: "/api/v1/auth/refresh" });
|
|
101
|
+
res.json({ message: "Logged out successfully" });
|
|
102
|
+
} catch (err) {
|
|
103
|
+
res.status(500).json({ error: "Logout failed" });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ── GET /auth/me — current user profile ─────────────────
|
|
108
|
+
authRouter.get("/me", authenticate, validateSession, async (req: Request, res: Response) => {
|
|
109
|
+
try {
|
|
110
|
+
const { db } = await import("../config/database.js");
|
|
111
|
+
const { decrypt } = await import("../services/encryption.js");
|
|
112
|
+
|
|
113
|
+
const user = await db("users").where("id", req.user!.userId).first();
|
|
114
|
+
if (!user) {
|
|
115
|
+
res.status(404).json({ error: "User not found" });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await writeAuditLog({
|
|
120
|
+
user_id: req.user!.userId,
|
|
121
|
+
action: "VIEW",
|
|
122
|
+
resource_type: "user",
|
|
123
|
+
resource_id: req.user!.userId,
|
|
124
|
+
ip_address: req.ip,
|
|
125
|
+
outcome: "success",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
res.json({
|
|
129
|
+
id: user.id,
|
|
130
|
+
email: decrypt(user.email_encrypted),
|
|
131
|
+
fullName: decrypt(user.full_name_encrypted),
|
|
132
|
+
dateOfBirth: decrypt(user.date_of_birth_encrypted),
|
|
133
|
+
phone: user.phone_encrypted ? decrypt(user.phone_encrypted) : null,
|
|
134
|
+
pregnancyWeek: user.pregnancy_week,
|
|
135
|
+
role: user.role,
|
|
136
|
+
createdAt: user.created_at,
|
|
137
|
+
});
|
|
138
|
+
} catch (err) {
|
|
139
|
+
res.status(500).json({ error: "Failed to fetch profile" });
|
|
140
|
+
}
|
|
141
|
+
});
|