Merge pull request #46 from internship-humic/feat/security-improvement

Menambahkan keamanan API
This commit is contained in:
Rafi Athallah
2026-08-10 21:12:28 +07:00
committed by GitHub
9 changed files with 148 additions and 11 deletions
+1
View File
@@ -1,5 +1,6 @@
PORT=********** PORT=**********
NODE_ENV=production NODE_ENV=production
ALLOWED_ORIGINS="http://localhost:5173,http://localhost:3000"
SEED_ADMIN_EMAIL=admin@internify.com SEED_ADMIN_EMAIL=admin@internify.com
SEED_ADMIN_PASSWORD=**** SEED_ADMIN_PASSWORD=****
+43
View File
@@ -19,7 +19,9 @@
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"express": "^4.21.2", "express": "^4.21.2",
"express-rate-limit": "^8.6.1",
"express-recaptcha": "^5.1.0", "express-recaptcha": "^5.1.0",
"helmet": "^8.3.0",
"joi": "^18.0.1", "joi": "^18.0.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.2", "multer": "^1.4.5-lts.2",
@@ -1631,6 +1633,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
@@ -1672,6 +1675,25 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/express-rate-limit": {
"version": "8.6.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.1.tgz",
"integrity": "sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.3",
"ip-address": "^10.2.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": {
"express": ">= 4.11"
}
},
"node_modules/express-recaptcha": { "node_modules/express-recaptcha": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/express-recaptcha/-/express-recaptcha-5.1.0.tgz", "resolved": "https://registry.npmjs.org/express-recaptcha/-/express-recaptcha-5.1.0.tgz",
@@ -2129,6 +2151,18 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/helmet": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz",
"integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"funding": {
"url": "https://github.com/sponsors/EvanHahn"
}
},
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -2219,6 +2253,15 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/ip-address": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz",
"integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+2
View File
@@ -23,7 +23,9 @@
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"express": "^4.21.2", "express": "^4.21.2",
"express-rate-limit": "^8.6.1",
"express-recaptcha": "^5.1.0", "express-recaptcha": "^5.1.0",
"helmet": "^8.3.0",
"joi": "^18.0.1", "joi": "^18.0.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.2", "multer": "^1.4.5-lts.2",
+3 -1
View File
@@ -1,7 +1,9 @@
const { PrismaClient } = require('../../generated/prisma'); const { PrismaClient } = require('../../generated/prisma');
const prisma = new PrismaClient({ const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error'], log: process.env.NODE_ENV === 'production'
? ['error', 'warn']
: ['query', 'info', 'warn', 'error'],
}); });
module.exports = prisma module.exports = prisma
+41 -3
View File
@@ -3,6 +3,7 @@ const express = require('express');
const http = require('http'); const http = require('http');
const { initSocket } = require('./helpers/socket/socket_connection'); const { initSocket } = require('./helpers/socket/socket_connection');
const cors = require('cors'); const cors = require('cors');
const helmet = require('helmet');
const path = require('path'); const path = require('path');
const adminRoutes = require('./routes/admin.routes'); const adminRoutes = require('./routes/admin.routes');
const authRoutes = require('./routes/auth.routes'); const authRoutes = require('./routes/auth.routes');
@@ -23,8 +24,17 @@ const setupSwaggerDocs = require('./docs/swagger');
const PORT = process.env.PORT; const PORT = process.env.PORT;
const app = express(); const app = express();
const allowedOrigins = (process.env.ALLOWED_ORIGINS || 'http://localhost:5173,http://localhost:3000').split(',');
const corsOptions = { const corsOptions = {
origin: true, origin: (origin, callback) => {
// allow requests with no origin (like mobile apps or curl requests) or origins in allowedOrigins
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS policy'));
}
},
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE", "OPTIONS"], methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE", "OPTIONS"],
credentials: true, credentials: true,
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With", "Accept"], allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With", "Accept"],
@@ -36,12 +46,22 @@ const uploadDir = process.env.NODE_ENV === 'production'
? '/tmp/uploads' ? '/tmp/uploads'
: path.join(__dirname, 'uploads'); : path.join(__dirname, 'uploads');
app.use(helmet({
crossOriginResourcePolicy: { policy: "cross-origin" },
contentSecurityPolicy: false,
}));
app.options('*', cors(corsOptions)); app.options('*', cors(corsOptions));
app.use(express.json()); app.use(express.json({ limit: '100kb' }));
app.use(cors(corsOptions)); app.use(cors(corsOptions));
app.use("/uploads", express.static(uploadDir), (req, res, next) => { app.use("/uploads", express.static(uploadDir, {
setHeaders: (res) => {
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("Content-Disposition", "inline");
},
}), (req, res, next) => {
if (!res.headersSent) { if (!res.headersSent) {
res.status(404).json({ res.status(404).json({
status: false, status: false,
@@ -77,6 +97,24 @@ app.use((req, res) => {
}); });
}); });
app.use((err, req, res, next) => {
const statusCode = err.status || err.statusCode || 500;
const message = process.env.NODE_ENV === 'production' && statusCode === 500
? 'Internal Server Error'
: err.message || 'Unexpected server error';
if (statusCode === 500) {
console.error('[Unhandled Error]:', err);
}
res.status(statusCode).json({
status: false,
data: null,
message,
code: statusCode,
});
});
const server = http.createServer(app); const server = http.createServer(app);
initSocket(server); initSocket(server);
+15 -1
View File
@@ -15,7 +15,8 @@ const storage = multer.diskStorage({
destination: (_req, _file, cb) => cb(null, uploadDir), destination: (_req, _file, cb) => cb(null, uploadDir),
filename: (_req, file, cb) => { filename: (_req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueSuffix + path.extname(file.originalname)); const safeExt = path.extname(file.originalname).replace(/[^a-zA-Z0-9.]/g, '').toLowerCase();
cb(null, `${uniqueSuffix}${safeExt}`);
}, },
}); });
@@ -39,6 +40,10 @@ const fileFilter = (req, file, cb) => {
const upload = multer({ const upload = multer({
storage: storage, storage: storage,
fileFilter: fileFilter, fileFilter: fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB Makss
files: 5,
},
}); });
// middleware untuk ngechek ukuran file // middleware untuk ngechek ukuran file
@@ -88,6 +93,15 @@ const checkFileSizes = (req, res, next) => {
// middleware untuk error multer // middleware untuk error multer
const multerErrorHandler = (err, req, res, next) => { const multerErrorHandler = (err, req, res, next) => {
if (err instanceof multer.MulterError) { if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(413).json({
status: false,
data: null,
message: 'Ukuran file terlalu besar. Maksimal 5MB',
code: 413,
});
}
if (err.code === 'LIMIT_UNEXPECTED_FILE') { if (err.code === 'LIMIT_UNEXPECTED_FILE') {
return res.status(400).json({ return res.status(400).json({
status: false, status: false,
+32
View File
@@ -0,0 +1,32 @@
const rateLimit = require('express-rate-limit');
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 menit
max: 15,
standardHeaders: true,
legacyHeaders: false,
message: {
status: false,
data: null,
message: 'Kebanyakan login, mohon coba lagi setelah 15 menit',
code: 429,
},
});
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 menit
max: 200,
standardHeaders: true,
legacyHeaders: false,
message: {
status: false,
data: null,
message: 'Terlalu banyak request, mohon coba lagi setelah 15 menit',
code: 429,
},
});
module.exports = {
authLimiter,
globalLimiter,
};
@@ -28,12 +28,11 @@ class AuthController {
if (result.err) { if (result.err) {
return response(res, 'fail', result); return response(res, 'fail', result);
} }
console.log(result.data.token)
res.cookie("token", result.data.token, { res.cookie("token", result.data.token, {
httpOnly: true, httpOnly: true,
secure: true, secure: true,
sameSite: 'None', sameSite: 'Lax',
maxAge: 7 * 24 * 60 * 60, maxAge: 7 * 24 * 60 * 60 * 1000,
}); });
return response(res, 'success', result, 'Login berhasil', SUCCESS.OK); return response(res, 'success', result, 'Login berhasil', SUCCESS.OK);
@@ -64,6 +63,11 @@ class AuthController {
async logout(req, res) { async logout(req, res) {
try { try {
res.clearCookie("token", {
httpOnly: true,
secure: true,
sameSite: 'Lax',
});
return response(res, 'success', data(), 'Logout berhasil', SUCCESS.OK); return response(res, 'success', data(), 'Logout berhasil', SUCCESS.OK);
} catch (err) { } catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR); return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
+4 -3
View File
@@ -2,7 +2,8 @@ const express = require('express');
const { authController } = require('../modules/auth'); const { authController } = require('../modules/auth');
const { verifyJWT } = require('../middleware/verifyJWT'); const { verifyJWT } = require('../middleware/verifyJWT');
const multer = require('../middleware/multer'); const multer = require('../middleware/multer');
const { multerErrorHandler } = require('../middleware/multer'); const { multerErrorHandler, checkFileSizes } = require('../middleware/multer');
const { authLimiter } = require('../middleware/rateLimiter');
const router = express.Router(); const router = express.Router();
@@ -44,7 +45,7 @@ const router = express.Router();
* 500: * 500:
* description: Internal server error * description: Internal server error
*/ */
router.post("/login", authController.login); router.post("/login", authLimiter, authController.login);
/** /**
* @swagger * @swagger
@@ -318,6 +319,6 @@ router.get("/me", verifyJWT, authController.me);
* 500: * 500:
* description: Internal server error * description: Internal server error
*/ */
router.patch('/update-profile', verifyJWT, multer.single('profile_picture'), multerErrorHandler, authController.updateProfile); router.patch('/update-profile', verifyJWT, multer.single('profile_picture'), checkFileSizes, multerErrorHandler, authController.updateProfile);
module.exports = router; module.exports = router;