Initial T06 security template
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
certs/
|
||||||
|
api/private.pem
|
||||||
|
api/public.pem
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
51
README.md
Normal file
51
README.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# T06 — Sigurnost mikro-servisnih platformi
|
||||||
|
|
||||||
|
Ovo je početni repozitorij za laboratorijsku vježbu T06.
|
||||||
|
|
||||||
|
## Pokretanje
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
API se pokreće na:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
Za HTTPS način rada:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
HTTPS=true npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
API se tada pokreće na:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://localhost:3443
|
||||||
|
```
|
||||||
|
|
||||||
|
## Grane
|
||||||
|
|
||||||
|
Svaki zadatak ima svoju granu:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout zadatak1
|
||||||
|
git checkout zadatak2
|
||||||
|
git checkout zadatak3
|
||||||
|
git checkout zadatak4
|
||||||
|
```
|
||||||
|
|
||||||
|
## Korisnici za testiranje
|
||||||
|
|
||||||
|
```text
|
||||||
|
student / fpmoz2024
|
||||||
|
student1 / pass1
|
||||||
|
student2 / pass2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Napomena
|
||||||
|
|
||||||
|
Repozitorij je namjerno pripremljen s TODO komentarima i nekim ranjivim dijelovima koda jer studenti u vježbi trebaju demonstrirati i popraviti sigurnosne probleme.
|
||||||
19
api/src/admin-service.js
Normal file
19
api/src/admin-service.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
function startAdminService() {
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.get('/admin', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
service: 'internal-admin-service',
|
||||||
|
warning: 'Ovaj servis ne bi smio biti dostupan kroz javni API.',
|
||||||
|
secret: 'internal-admin-token-demo'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(3001, () => {
|
||||||
|
console.log('Interni admin servis na http://localhost:3001');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = startAdminService;
|
||||||
37
api/src/auth.js
Normal file
37
api/src/auth.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-ne-koristiti-u-produkciji';
|
||||||
|
|
||||||
|
function privateKeyPath() {
|
||||||
|
return path.join(__dirname, '..', 'private.pem');
|
||||||
|
}
|
||||||
|
|
||||||
|
function publicKeyPath() {
|
||||||
|
return path.join(__dirname, '..', 'public.pem');
|
||||||
|
}
|
||||||
|
|
||||||
|
function signToken(user) {
|
||||||
|
const payload = {
|
||||||
|
sub: user.id,
|
||||||
|
username: user.username,
|
||||||
|
role: user.role
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO Z1.4:
|
||||||
|
// Zamijeni simetrično HS256 potpisivanje asimetričnim RS256 potpisivanjem.
|
||||||
|
// 1. Generiraj api/private.pem i api/public.pem.
|
||||||
|
// 2. Umjesto JWT_SECRET koristi fs.readFileSync(privateKeyPath()).
|
||||||
|
// 3. Promijeni algorithm iz HS256 u RS256.
|
||||||
|
return jwt.sign(payload, JWT_SECRET, {
|
||||||
|
algorithm: 'HS256',
|
||||||
|
expiresIn: '15m'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPublicKeyPath() {
|
||||||
|
return publicKeyPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { signToken, getPublicKeyPath, privateKeyPath, publicKeyPath };
|
||||||
19
api/src/data.js
Normal file
19
api/src/data.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const users = [
|
||||||
|
{ id: 'u0', username: 'student', password: 'fpmoz2024', name: 'Demo Student', role: 'student' },
|
||||||
|
{ id: 'u1', username: 'student1', password: 'pass1', name: 'Student Jedan', role: 'student' },
|
||||||
|
{ id: 'u2', username: 'student2', password: 'pass2', name: 'Student Dva', role: 'student' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const profiles = {
|
||||||
|
u0: { id: 'u0', name: 'Demo Student', email: 'student@fpmoz.sum.ba', role: 'student' },
|
||||||
|
u1: { id: 'u1', name: 'Student Jedan', email: 'student1@fpmoz.sum.ba', role: 'student' },
|
||||||
|
u2: { id: 'u2', name: 'Student Dva', email: 'student2@fpmoz.sum.ba', role: 'student' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const orders = [
|
||||||
|
{ id: 1, userId: 'u1', item: 'Knjiga: Mikroservisi', amount: 25 },
|
||||||
|
{ id: 2, userId: 'u2', item: 'Knjiga: API Security', amount: 30 },
|
||||||
|
{ id: 3, userId: 'u1', item: 'Tečaj: Docker osnove', amount: 15 }
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = { users, profiles, orders };
|
||||||
26
api/src/middleware/authenticate.js
Normal file
26
api/src/middleware/authenticate.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const { getPublicKeyPath } = require('../auth');
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-ne-koristiti-u-produkciji';
|
||||||
|
|
||||||
|
function authenticate(req, res, next) {
|
||||||
|
const authHeader = req.headers.authorization || '';
|
||||||
|
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ error: 'Missing bearer token' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO Z1.4:
|
||||||
|
// Nakon prelaska na RS256 ovdje umjesto JWT_SECRET koristi javni ključ:
|
||||||
|
// jwt.verify(token, fs.readFileSync(getPublicKeyPath()))
|
||||||
|
req.user = jwt.verify(token, JWT_SECRET);
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(401).json({ error: 'Invalid token' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = authenticate;
|
||||||
18
api/src/routes/auth.js
Normal file
18
api/src/routes/auth.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { users } = require('../data');
|
||||||
|
const { signToken } = require('../auth');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post('/login', (req, res) => {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
const user = users.find((candidate) => candidate.username === username && candidate.password === password);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({ error: 'Invalid username or password' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({ token: signToken(user) });
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
24
api/src/routes/orders.js
Normal file
24
api/src/routes/orders.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const authenticate = require('../middleware/authenticate');
|
||||||
|
const { orders } = require('../data');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/orders/:id', authenticate, (req, res) => {
|
||||||
|
const order = orders.find((candidate) => candidate.id === Number(req.params.id));
|
||||||
|
|
||||||
|
if (!order) {
|
||||||
|
return res.status(404).json({ error: 'Order not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Z4.2:
|
||||||
|
// Ova provjera je namjerno zakomentirana kako bi se demonstrirala BOLA ranjivost.
|
||||||
|
// Odkomentiraj blok, restartaj server i ponovno testiraj pristup tuđoj narudžbi.
|
||||||
|
// if (order.userId !== req.user.sub) {
|
||||||
|
// return res.status(403).json({ error: 'Forbidden' });
|
||||||
|
// }
|
||||||
|
|
||||||
|
return res.json(order);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
42
api/src/routes/preview.js
Normal file
42
api/src/routes/preview.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/fetch-preview', async (req, res) => {
|
||||||
|
const { url } = req.query;
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return res.status(400).json({ error: 'Missing url query parameter' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO Z4.4:
|
||||||
|
// Ova aplikacija je namjerno ranjiva na SSRF jer prihvaća bilo koji URL.
|
||||||
|
// Odkomentiraj whitelist provjeru i dopusti samo poznate javne domene.
|
||||||
|
// const ALLOWED_DOMAINS = ['example.com', 'httpbin.org', 'jsonplaceholder.typicode.com'];
|
||||||
|
// const parsed = new URL(url);
|
||||||
|
// if (!ALLOWED_DOMAINS.includes(parsed.hostname)) {
|
||||||
|
// return res.status(403).json({
|
||||||
|
// error: 'Domain not allowed',
|
||||||
|
// allowed: ALLOWED_DOMAINS
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
const response = await axios.get(url, {
|
||||||
|
timeout: 3000,
|
||||||
|
maxRedirects: 2,
|
||||||
|
validateStatus: () => true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof response.data === 'object') {
|
||||||
|
return res.status(response.status).json(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(response.status).type('text/plain').send(String(response.data).slice(0, 2000));
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(502).json({ error: 'Fetch failed', details: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
11
api/src/routes/profile.js
Normal file
11
api/src/routes/profile.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const authenticate = require('../middleware/authenticate');
|
||||||
|
const { profiles } = require('../data');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/profile', authenticate, (req, res) => {
|
||||||
|
return res.json(profiles[req.user.sub] || { id: req.user.sub, message: 'Profil nije pronađen' });
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
59
api/src/server.js
Normal file
59
api/src/server.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const http = require('http');
|
||||||
|
const https = require('https');
|
||||||
|
const path = require('path');
|
||||||
|
const express = require('express');
|
||||||
|
const authRoutes = require('./routes/auth');
|
||||||
|
const profileRoutes = require('./routes/profile');
|
||||||
|
const ordersRoutes = require('./routes/orders');
|
||||||
|
const previewRoutes = require('./routes/preview');
|
||||||
|
const startAdminService = require('./admin-service');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.get('/api/public', (req, res) => {
|
||||||
|
res.json({ message: 'Javni endpoint radi.' });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/internal', (req, res) => {
|
||||||
|
const cert = req.socket.getPeerCertificate();
|
||||||
|
|
||||||
|
if (!req.client.authorized) {
|
||||||
|
return res.status(401).json({ error: 'Client certificate required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({ message: 'mTLS pristup odobren.', client: cert.subject });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use('/auth', authRoutes);
|
||||||
|
app.use('/api', profileRoutes);
|
||||||
|
app.use('/api', ordersRoutes);
|
||||||
|
app.use('/api', previewRoutes);
|
||||||
|
|
||||||
|
const useHttps = String(process.env.HTTPS || '').toLowerCase() === 'true';
|
||||||
|
|
||||||
|
if (useHttps) {
|
||||||
|
const certsDir = path.join(process.cwd(), 'certs');
|
||||||
|
const options = {
|
||||||
|
key: fs.readFileSync(path.join(certsDir, 'server.key')),
|
||||||
|
cert: fs.readFileSync(path.join(certsDir, 'server.crt')),
|
||||||
|
ca: fs.readFileSync(path.join(certsDir, 'ca.crt')),
|
||||||
|
requestCert: true,
|
||||||
|
rejectUnauthorized: false
|
||||||
|
};
|
||||||
|
|
||||||
|
https.createServer(options, app).listen(3443, () => {
|
||||||
|
console.log('API na https://localhost:3443');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
http.createServer(app).listen(3000, () => {
|
||||||
|
console.log('API na http://localhost:3000');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.START_ADMIN_SERVICE !== 'false') {
|
||||||
|
startAdminService();
|
||||||
|
}
|
||||||
23
docker-compose.yml
Normal file
23
docker-compose.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
services:
|
||||||
|
gateway:
|
||||||
|
image: nginx:1.27-alpine
|
||||||
|
container_name: t06-gateway
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- ./gateway/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
depends_on:
|
||||||
|
- users-api
|
||||||
|
- orders-api
|
||||||
|
|
||||||
|
users-api:
|
||||||
|
build: ./services/users-api
|
||||||
|
container_name: t06-users-api
|
||||||
|
environment:
|
||||||
|
JWT_SECRET: dev-secret-ne-koristiti-u-produkciji
|
||||||
|
|
||||||
|
orders-api:
|
||||||
|
build: ./services/orders-api
|
||||||
|
container_name: t06-orders-api
|
||||||
|
environment:
|
||||||
|
JWT_SECRET: dev-secret-ne-koristiti-u-produkciji
|
||||||
46
gateway/nginx.conf
Normal file
46
gateway/nginx.conf
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
events {}
|
||||||
|
|
||||||
|
http {
|
||||||
|
# TODO Z3.3:
|
||||||
|
# Dodaj rate limiting zonu u ovaj http blok:
|
||||||
|
# limit_req_zone $binary_remote_addr zone=api:10m rate=5r/s;
|
||||||
|
|
||||||
|
upstream users_api {
|
||||||
|
server users-api:3001;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream orders_api {
|
||||||
|
server orders-api:3002;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location = /auth/login {
|
||||||
|
proxy_pass http://users_api/auth/login;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/users {
|
||||||
|
# TODO Z3.3:
|
||||||
|
# Dodaj u ovaj location:
|
||||||
|
# limit_req zone=api burst=10 nodelay;
|
||||||
|
# limit_req_status 429;
|
||||||
|
|
||||||
|
proxy_pass http://users_api/api/users;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header Authorization $http_authorization;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/orders {
|
||||||
|
# TODO Z3.3:
|
||||||
|
# Dodaj u ovaj location:
|
||||||
|
# limit_req zone=api burst=10 nodelay;
|
||||||
|
# limit_req_status 429;
|
||||||
|
|
||||||
|
proxy_pass http://orders_api/api/orders;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header Authorization $http_authorization;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
package.json
Normal file
19
package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "t06-sigurnost",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Laboratorijska vježba T06 — sigurnost mikro-servisnih platformi",
|
||||||
|
"main": "api/src/server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node api/src/server.js",
|
||||||
|
"dev": "nodemon api/src/server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.7.9",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"jsonwebtoken": "^9.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.1.9"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
services/orders-api/Dockerfile
Normal file
7
services/orders-api/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install --omit=dev
|
||||||
|
COPY . .
|
||||||
|
EXPOSE 3002
|
||||||
|
CMD ["npm", "start"]
|
||||||
12
services/orders-api/package.json
Normal file
12
services/orders-api/package.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "orders-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"jsonwebtoken": "^9.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
33
services/orders-api/server.js
Normal file
33
services/orders-api/server.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-ne-koristiti-u-produkciji';
|
||||||
|
|
||||||
|
function authenticate(req, res, next) {
|
||||||
|
const authHeader = req.headers.authorization || '';
|
||||||
|
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ error: 'Missing bearer token' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
req.user = jwt.verify(token, JWT_SECRET);
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(401).json({ error: 'Invalid token' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/api/orders', authenticate, (req, res) => {
|
||||||
|
res.setHeader('X-Served-By', 'orders-api');
|
||||||
|
return res.json([
|
||||||
|
{ id: 1, item: 'Knjiga: Mikroservisi', amount: 25 },
|
||||||
|
{ id: 2, item: 'Knjiga: API Security', amount: 30 }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(3002, () => {
|
||||||
|
console.log('orders-api na portu 3002');
|
||||||
|
});
|
||||||
7
services/users-api/Dockerfile
Normal file
7
services/users-api/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install --omit=dev
|
||||||
|
COPY . .
|
||||||
|
EXPOSE 3001
|
||||||
|
CMD ["npm", "start"]
|
||||||
12
services/users-api/package.json
Normal file
12
services/users-api/package.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "users-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"jsonwebtoken": "^9.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
57
services/users-api/server.js
Normal file
57
services/users-api/server.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-ne-koristiti-u-produkciji';
|
||||||
|
|
||||||
|
const users = [
|
||||||
|
{ id: 'u0', username: 'student', password: 'fpmoz2024', name: 'Demo Student', role: 'student' }
|
||||||
|
];
|
||||||
|
|
||||||
|
function authenticate(req, res, next) {
|
||||||
|
const authHeader = req.headers.authorization || '';
|
||||||
|
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ error: 'Missing bearer token' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
req.user = jwt.verify(token, JWT_SECRET);
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(401).json({ error: 'Invalid token' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.post('/auth/login', (req, res) => {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
const user = users.find((candidate) => candidate.username === username && candidate.password === password);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({ error: 'Invalid username or password' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ sub: user.id, username: user.username, role: user.role },
|
||||||
|
JWT_SECRET,
|
||||||
|
{ algorithm: 'HS256', expiresIn: '15m' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.json({ token });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/users', authenticate, (req, res) => {
|
||||||
|
res.setHeader('X-Served-By', 'users-api');
|
||||||
|
return res.json([
|
||||||
|
{ id: 'u0', username: 'student', name: 'Demo Student' },
|
||||||
|
{ id: 'u1', username: 'student1', name: 'Student Jedan' },
|
||||||
|
{ id: 'u2', username: 'student2', name: 'Student Dva' }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(3001, () => {
|
||||||
|
console.log('users-api na portu 3001');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user