Skip to content

Commit 38190e4

Browse files
committed
(Auth): added patient authentication JWT: access and refresh token
1 parent 1c6251b commit 38190e4

29 files changed

Lines changed: 3914 additions & 340 deletions

File tree

api-gateway/package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,20 @@
1414
"license": "ISC",
1515
"packageManager": "pnpm@10.28.2",
1616
"dependencies": {
17+
"bcrypt": "^6.0.0",
18+
"cors": "^2.8.6",
1719
"express": "^5.2.1",
18-
"http-proxy-middleware": "^3.0.5"
20+
"http-proxy-middleware": "^3.0.5",
21+
"jsonwebtoken": "^9.0.3",
22+
"pg": "^8.18.0"
1923
},
2024
"devDependencies": {
25+
"@types/bcrypt": "^6.0.0",
26+
"@types/cors": "^2.8.19",
2127
"@types/express": "^5.0.6",
28+
"@types/jsonwebtoken": "^9.0.10",
2229
"@types/node": "^25.1.0",
30+
"@types/pg": "^8.16.0",
2331
"dotenv": "^17.2.3",
2432
"tsx": "^4.21.0",
2533
"typescript": "^5.9.3"

api-gateway/src/app.ts

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,22 @@
11
import express from 'express';
2-
import healthRouter from './routes/health';
3-
import { patientProxy } from './proxy/patient.proxy';
2+
import cors from 'cors';
3+
import healthRoutes from './routes/health';
4+
import { patientAuthProxy } from './proxy/patient.auth.proxy';
5+
import { patientDataProxy } from './proxy/patient.data.proxy';
6+
import { authenticate } from './middlewares/auth.middleware';
7+
import { requirePatientSelf } from './middlewares/patient.guard';
48

59
const app = express();
610

7-
// Debug middleware
8-
app.use((req, res, next) => {
9-
console.log(' GATEWAY REQUEST:', req.method, req.originalUrl);
10-
next();
11-
});
11+
app.use(cors());
1212

13-
app.use('/health', healthRouter);
14-
app.use('/patients', patientProxy);
13+
// health
14+
app.use('/health', healthRoutes);
1515

16-
// ✅ FIXED catch-all route
17-
app.use((req, res) => {
18-
console.log(' NO ROUTE MATCHED:', req.method, req.originalUrl);
19-
res.status(404).json({
20-
error: 'Route not found in gateway',
21-
method: req.method,
22-
url: req.originalUrl,
23-
timestamp: new Date().toISOString(),
24-
});
25-
});
16+
// 🔓 PUBLIC auth (NO JWT)
17+
app.use('/patients/public', patientAuthProxy);
18+
19+
// 🔐 PROTECTED patient data
20+
app.use('/patients', authenticate, requirePatientSelf, patientDataProxy);
2621

2722
export default app;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Request, Response, NextFunction } from 'express';
2+
import jwt from 'jsonwebtoken';
3+
import { error } from 'node:console';
4+
5+
export interface AuthenticatedRequest extends Request {
6+
user?: {
7+
sub: string;
8+
role: string;
9+
type: 'PATIENT' | 'STAFF';
10+
};
11+
}
12+
13+
export function authenticate(
14+
req: AuthenticatedRequest,
15+
res: Response,
16+
next: NextFunction
17+
) {
18+
const authHeader = req.headers.authorization;
19+
20+
console.log('AUTH HEADER:', authHeader);
21+
22+
if (!authHeader?.startsWith('Bearer ')) {
23+
return res.status(401).json({ error: 'Unauthorized' });
24+
}
25+
26+
const token = authHeader.split(' ')[1];
27+
28+
console.log('TOKEN LENGTH:', token.length);
29+
console.log('TOKEN PREVIEW:', token.slice(0, 20));
30+
31+
try {
32+
const payload = jwt.verify(
33+
token,
34+
process.env.JWT_SECRET!
35+
) as AuthenticatedRequest['user'];
36+
37+
console.log('JWT PAYLOAD:', payload);
38+
39+
req.user = payload;
40+
next();
41+
} catch (err: any) {
42+
console.error('JWT VERIFY ERROR:', err.name, err.message);
43+
return res.status(401).json({ error: 'Invalid token' });
44+
}
45+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Response, NextFunction } from 'express';
2+
import { AuthenticatedRequest } from './auth.middleware';
3+
4+
export function requirePatientSelf(
5+
req: AuthenticatedRequest,
6+
res: Response,
7+
next: NextFunction
8+
) {
9+
if (!req.user || req.user.type !== 'PATIENT') {
10+
return res.status(401).json({ error: 'Unauthorized' });
11+
}
12+
13+
// URL will be /:id after /patients is stripped
14+
const patientIdFromPath = req.path.split('/')[1];
15+
16+
if (!patientIdFromPath) {
17+
return res.status(400).json({ error: 'Invalid patient path' });
18+
}
19+
20+
if (req.user.sub !== patientIdFromPath) {
21+
return res.status(403).json({ error: 'Access denied' });
22+
}
23+
24+
next();
25+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { createProxyMiddleware } from 'http-proxy-middleware';
2+
3+
export const patientAuthProxy = createProxyMiddleware({
4+
target: 'http://localhost:3001',
5+
changeOrigin: true,
6+
7+
// /patients/public/auth/login → /auth/login
8+
pathRewrite: {
9+
'^/patients/public': '',
10+
},
11+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createProxyMiddleware } from 'http-proxy-middleware';
2+
3+
export const patientDataProxy = createProxyMiddleware({
4+
target: 'http://localhost:3001',
5+
changeOrigin: true,
6+
7+
// /patients/:id → /patients/:id (re-add prefix)
8+
pathRewrite: (path) => `/patients${path}`,
9+
});

api-gateway/src/proxy/patient.proxy.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

api-gateway/src/server.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@ import 'dotenv/config';
22
import app from './app.js';
33
import logger from './logger.js';
44

5-
const PORT = process.env.PORT;
5+
const PORT = process.env.PORT || 3000;
6+
67
app.listen(PORT, () => {
7-
logger.info(`API Gateway running on PORT ${PORT}`);
8+
logger.info(`🚀 API Gateway running on port ${PORT}`);
9+
});
10+
11+
// Graceful shutdown
12+
process.on('SIGTERM', async () => {
13+
logger.info('SIGTERM received, shutting down gracefully');
14+
process.exit(0);
815
});

frontend/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)