Compare commits
11 Commits
889d727df8
...
091496e95c
| Author | SHA1 | Date | |
|---|---|---|---|
| 091496e95c | |||
| 7c93bc3ff6 | |||
| 3acd41e1f7 | |||
| 326de3f929 | |||
| 5d091d6c46 | |||
| e26e3ad3c4 | |||
| 220b552f5d | |||
| 9fa0340a09 | |||
| 57e3b0e366 | |||
| 23b02d7441 | |||
| 4faf7bce1f |
@@ -0,0 +1,13 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
.git
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
oldCode
|
||||||
|
test
|
||||||
|
docs
|
||||||
|
README.md
|
||||||
|
vercel.json
|
||||||
|
.github
|
||||||
|
src/uploads
|
||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
PORT=**********
|
PORT=**********
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
ALLOWED_ORIGINS="http://localhost:5173,http://localhost:3000"
|
||||||
|
|
||||||
SEED_ADMIN_USERNAME=admin
|
SEED_ADMIN_EMAIL=admin@internify.com
|
||||||
SEED_ADMIN_PASSWORD=****
|
SEED_ADMIN_PASSWORD=****
|
||||||
SEED_ACTIVE_BATCH_NUMBER=7
|
SEED_ACTIVE_BATCH_NUMBER=7
|
||||||
|
|
||||||
|
|||||||
@@ -151,3 +151,8 @@ uploads/
|
|||||||
.vercel
|
.vercel
|
||||||
.vercel/*.json
|
.vercel/*.json
|
||||||
.env*.local
|
.env*.local
|
||||||
|
|
||||||
|
# AI agent tooling
|
||||||
|
.agents/
|
||||||
|
skills-lock.json
|
||||||
|
.claude/
|
||||||
|
|||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
FROM node:20-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends openssl ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY prisma ./prisma
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
COPY src ./src
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["sh", "-c", "npx prisma migrate deploy && node src/index.js"]
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mysql:8
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MYSQL_DATABASE: ${MYSQL_DATABASE:-internify_db}
|
||||||
|
MYSQL_USER: ${MYSQL_USER:-internify}
|
||||||
|
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?set MYSQL_PASSWORD}
|
||||||
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?set MYSQL_ROOT_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/mysql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-p$$MYSQL_ROOT_PASSWORD"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: 3000
|
||||||
|
DATABASE_URL: ${DATABASE_URL:-mysql://${MYSQL_USER:-internify}:${MYSQL_PASSWORD}@db:3306/${MYSQL_DATABASE:-internify_db}}
|
||||||
|
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS}
|
||||||
|
JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
|
||||||
|
JWT_PUBLIC_KEY: ${JWT_PUBLIC_KEY}
|
||||||
|
RECAPTCHA_SECRET_KEY: ${RECAPTCHA_SECRET_KEY}
|
||||||
|
RECAPTCHA_SITE_KEY: ${RECAPTCHA_SITE_KEY}
|
||||||
|
SWAGGER_ENABLED: ${SWAGGER_ENABLED:-false}
|
||||||
|
AUTH_EMAIL: ${AUTH_EMAIL}
|
||||||
|
AUTH_PASSWORD: ${AUTH_PASSWORD}
|
||||||
|
SEED_ADMIN_EMAIL: ${SEED_ADMIN_EMAIL}
|
||||||
|
SEED_ADMIN_PASSWORD: ${SEED_ADMIN_PASSWORD}
|
||||||
|
SEED_ACTIVE_BATCH_NUMBER: ${SEED_ACTIVE_BATCH_NUMBER}
|
||||||
|
volumes:
|
||||||
|
- uploads:/tmp/uploads
|
||||||
|
- mentor_uploads:/app/src/uploads
|
||||||
|
expose:
|
||||||
|
- "3000"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
|
uploads:
|
||||||
|
mentor_uploads:
|
||||||
Generated
+43
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `certificate` ADD COLUMN `intern_position` VARCHAR(255) NULL;
|
||||||
@@ -359,12 +359,13 @@ enum SubmissionType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Certificate {
|
model Certificate {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
uuid String @unique @default(uuid()) @db.VarChar(36)
|
uuid String @unique @default(uuid()) @db.VarChar(36)
|
||||||
id_project Int
|
id_project Int
|
||||||
id_user Int
|
id_user Int
|
||||||
certificate_no String @unique @db.VarChar(255)
|
certificate_no String @unique @db.VarChar(255)
|
||||||
issued_at DateTime @default(now())
|
intern_position String? @db.VarChar(255)
|
||||||
|
issued_at DateTime @default(now())
|
||||||
|
|
||||||
project Project @relation(fields: [id_project], references: [id], map: "certificate_id_project_foreign")
|
project Project @relation(fields: [id_project], references: [id], map: "certificate_id_project_foreign")
|
||||||
user User @relation(fields: [id_user], references: [id], map: "certificate_id_user_foreign")
|
user User @relation(fields: [id_user], references: [id], map: "certificate_id_user_foreign")
|
||||||
|
|||||||
+8
-8
@@ -258,28 +258,28 @@ async function main() {
|
|||||||
|
|
||||||
// 13. User
|
// 13. User
|
||||||
const user1 = await prisma.user.create({
|
const user1 = await prisma.user.create({
|
||||||
data: { id_mahasiswa: mhs1.id, id_lamaran_magang: lamaran1.id, full_name: 'Rafi Athallah', email: 'rafi@student.com', password: hashedPassword, role: 'intern', is_active: true }
|
data: { id_mahasiswa: mhs1.id, id_lamaran_magang: lamaran1.id, full_name: 'Rafi Athallah', email: 'rafi@student.com', password: hashedPassword, role: 'intern', is_active: true, intern_position: lowongan1.posisi, intern_interest_group: lowongan1.kelompok_peminatan }
|
||||||
});
|
});
|
||||||
const user2 = await prisma.user.create({
|
const user2 = await prisma.user.create({
|
||||||
data: { id_mahasiswa: mhs2.id, id_lamaran_magang: lamaran2.id, full_name: 'Alice Smith', email: 'alice@student.com', password: hashedPassword, role: 'intern', is_active: true }
|
data: { id_mahasiswa: mhs2.id, id_lamaran_magang: lamaran2.id, full_name: 'Alice Smith', email: 'alice@student.com', password: hashedPassword, role: 'intern', is_active: true, intern_position: lowongan2.posisi, intern_interest_group: lowongan2.kelompok_peminatan }
|
||||||
});
|
});
|
||||||
const user3 = await prisma.user.create({
|
const user3 = await prisma.user.create({
|
||||||
data: { id_mahasiswa: mhs3.id, id_lamaran_magang: lamaran3.id, full_name: 'Bob Johnson', email: 'bob@student.com', password: hashedPassword, role: 'intern', is_active: true }
|
data: { id_mahasiswa: mhs3.id, id_lamaran_magang: lamaran3.id, full_name: 'Bob Johnson', email: 'bob@student.com', password: hashedPassword, role: 'intern', is_active: true, intern_position: lowongan3.posisi, intern_interest_group: lowongan3.kelompok_peminatan }
|
||||||
});
|
});
|
||||||
const user4 = await prisma.user.create({
|
const user4 = await prisma.user.create({
|
||||||
data: { id_mahasiswa: mhs4.id, id_lamaran_magang: lamaran4.id, full_name: 'Dina Pratama', email: 'dina@student.com', password: hashedPassword, role: 'intern', is_active: true }
|
data: { id_mahasiswa: mhs4.id, id_lamaran_magang: lamaran4.id, full_name: 'Dina Pratama', email: 'dina@student.com', password: hashedPassword, role: 'intern', is_active: true, intern_position: lowongan1.posisi, intern_interest_group: lowongan1.kelompok_peminatan }
|
||||||
});
|
});
|
||||||
const user5 = await prisma.user.create({
|
const user5 = await prisma.user.create({
|
||||||
data: { id_mahasiswa: mhs5.id, id_lamaran_magang: lamaran5.id, full_name: 'Eka Saputra', email: 'eka@student.com', password: hashedPassword, role: 'intern', is_active: true }
|
data: { id_mahasiswa: mhs5.id, id_lamaran_magang: lamaran5.id, full_name: 'Eka Saputra', email: 'eka@student.com', password: hashedPassword, role: 'intern', is_active: true, intern_position: lowongan1.posisi, intern_interest_group: lowongan1.kelompok_peminatan }
|
||||||
});
|
});
|
||||||
const user6 = await prisma.user.create({
|
const user6 = await prisma.user.create({
|
||||||
data: { id_mahasiswa: mhs6.id, id_lamaran_magang: lamaran6.id, full_name: 'Fajar Nugroho', email: 'fajar@student.com', password: hashedPassword, role: 'intern', is_active: true }
|
data: { id_mahasiswa: mhs6.id, id_lamaran_magang: lamaran6.id, full_name: 'Fajar Nugroho', email: 'fajar@student.com', password: hashedPassword, role: 'intern', is_active: true, intern_position: lowongan3.posisi, intern_interest_group: lowongan3.kelompok_peminatan }
|
||||||
});
|
});
|
||||||
const user7 = await prisma.user.create({
|
const user7 = await prisma.user.create({
|
||||||
data: { id_mahasiswa: mhs7.id, id_lamaran_magang: lamaran7.id, full_name: 'Gita Ramadhani', email: 'gita@student.com', password: hashedPassword, role: 'intern', is_active: true }
|
data: { id_mahasiswa: mhs7.id, id_lamaran_magang: lamaran7.id, full_name: 'Gita Ramadhani', email: 'gita@student.com', password: hashedPassword, role: 'intern', is_active: true, intern_position: lowongan2.posisi, intern_interest_group: lowongan2.kelompok_peminatan }
|
||||||
});
|
});
|
||||||
const user8 = await prisma.user.create({
|
const user8 = await prisma.user.create({
|
||||||
data: { id_mahasiswa: mhs8.id, id_lamaran_magang: lamaran8.id, full_name: 'Hana Wijaya', email: 'hana@student.com', password: hashedPassword, role: 'intern', is_active: true }
|
data: { id_mahasiswa: mhs8.id, id_lamaran_magang: lamaran8.id, full_name: 'Hana Wijaya', email: 'hana@student.com', password: hashedPassword, role: 'intern', is_active: true, intern_position: lowongan1.posisi, intern_interest_group: lowongan1.kelompok_peminatan }
|
||||||
});
|
});
|
||||||
|
|
||||||
// 14. ProjectMember
|
// 14. ProjectMember
|
||||||
|
|||||||
@@ -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
|
||||||
+44
-6
@@ -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,12 +97,30 @@ 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);
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (!process.env.VERCEL) {
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT || 3000, () => {
|
||||||
console.log(`listening at http://localhost:${PORT}`);
|
console.log(`listening on port ${PORT || 3000}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -89,6 +89,21 @@ class CertificateRepository {
|
|||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
intern_position: true,
|
||||||
|
lamaran_magang: {
|
||||||
|
select: {
|
||||||
|
lowongan_magang: {
|
||||||
|
select: {
|
||||||
|
posisi: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -139,6 +154,7 @@ class CertificateRepository {
|
|||||||
id: true,
|
id: true,
|
||||||
full_name: true,
|
full_name: true,
|
||||||
email: true,
|
email: true,
|
||||||
|
intern_position: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project: {
|
project: {
|
||||||
|
|||||||
@@ -67,11 +67,13 @@ class CertificateService {
|
|||||||
|
|
||||||
const count = await certificateRepository.maxSequenceByProject(id_project);
|
const count = await certificateRepository.maxSequenceByProject(id_project);
|
||||||
const certificateNo = `No.${fullYear}/SPI3/CERT/${dateString}/${count + 1}`;
|
const certificateNo = `No.${fullYear}/SPI3/CERT/${dateString}/${count + 1}`;
|
||||||
|
const internPosition = this.getInternPositionFromProjectInfo(projectInfo);
|
||||||
|
|
||||||
const certificateData = {
|
const certificateData = {
|
||||||
id_project: parseInt(id_project),
|
id_project: parseInt(id_project),
|
||||||
id_user: parseInt(actor.id),
|
id_user: parseInt(actor.id),
|
||||||
certificate_no: certificateNo,
|
certificate_no: certificateNo,
|
||||||
|
intern_position: internPosition,
|
||||||
issued_at: now,
|
issued_at: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -147,7 +149,10 @@ class CertificateService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
validatedUsers.push(userId);
|
validatedUsers.push({
|
||||||
|
id: userId,
|
||||||
|
intern_position: this.getInternPositionFromProjectInfo(projectInfo),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const generatedCertificates = [];
|
const generatedCertificates = [];
|
||||||
@@ -160,13 +165,14 @@ class CertificateService {
|
|||||||
|
|
||||||
let currentCount = await certificateRepository.maxSequenceByProject(id_project);
|
let currentCount = await certificateRepository.maxSequenceByProject(id_project);
|
||||||
|
|
||||||
for (const userId of validatedUsers) {
|
for (const user of validatedUsers) {
|
||||||
currentCount++;
|
currentCount++;
|
||||||
const certificateNo = `No.${fullYear}/SPI3/CERT/${dateString}/${currentCount}`;
|
const certificateNo = `No.${fullYear}/SPI3/CERT/${dateString}/${currentCount}`;
|
||||||
const certificateData = {
|
const certificateData = {
|
||||||
id_project: parseInt(id_project),
|
id_project: parseInt(id_project),
|
||||||
id_user: parseInt(userId),
|
id_user: parseInt(user.id),
|
||||||
certificate_no: certificateNo,
|
certificate_no: certificateNo,
|
||||||
|
intern_position: user.intern_position,
|
||||||
issued_at: now,
|
issued_at: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -288,6 +294,7 @@ class CertificateService {
|
|||||||
return data({
|
return data({
|
||||||
uuid: certificate.uuid,
|
uuid: certificate.uuid,
|
||||||
intern_name: certificate.user ? certificate.user.full_name : null,
|
intern_name: certificate.user ? certificate.user.full_name : null,
|
||||||
|
intern_position: certificate.intern_position || null,
|
||||||
project: certificate.project ? {
|
project: certificate.project ? {
|
||||||
id: certificate.project.id,
|
id: certificate.project.id,
|
||||||
project_name: certificate.project.project_name,
|
project_name: certificate.project.project_name,
|
||||||
@@ -352,11 +359,13 @@ class CertificateService {
|
|||||||
id_project: cert.id_project,
|
id_project: cert.id_project,
|
||||||
id_user: cert.id_user,
|
id_user: cert.id_user,
|
||||||
certificate_no: cert.certificate_no,
|
certificate_no: cert.certificate_no,
|
||||||
|
intern_position: cert.intern_position || null,
|
||||||
issued_at: cert.issued_at,
|
issued_at: cert.issued_at,
|
||||||
user: cert.user ? {
|
user: cert.user ? {
|
||||||
id: cert.user.id,
|
id: cert.user.id,
|
||||||
full_name: cert.user.full_name,
|
full_name: cert.user.full_name,
|
||||||
email: cert.user.email,
|
email: cert.user.email,
|
||||||
|
intern_position: cert.user.intern_position || null,
|
||||||
} : null,
|
} : null,
|
||||||
project: cert.project ? {
|
project: cert.project ? {
|
||||||
id: cert.project.id,
|
id: cert.project.id,
|
||||||
@@ -368,6 +377,16 @@ class CertificateService {
|
|||||||
} : null,
|
} : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getInternPositionFromProjectInfo(projectInfo) {
|
||||||
|
const user = projectInfo?.members?.[0]?.user;
|
||||||
|
|
||||||
|
return (
|
||||||
|
user?.intern_position ||
|
||||||
|
user?.lamaran_magang?.lowongan_magang?.posisi ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new CertificateService();
|
module.exports = new CertificateService();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ const router = express.Router();
|
|||||||
* certificate_no:
|
* certificate_no:
|
||||||
* type: string
|
* type: string
|
||||||
* example: "CERT/20260609/PRJ2/USR5"
|
* example: "CERT/20260609/PRJ2/USR5"
|
||||||
|
* intern_position:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* example: "Backend Developer"
|
||||||
* issued_at:
|
* issued_at:
|
||||||
* type: string
|
* type: string
|
||||||
* format: date-time
|
* format: date-time
|
||||||
@@ -43,6 +47,10 @@ const router = express.Router();
|
|||||||
* email:
|
* email:
|
||||||
* type: string
|
* type: string
|
||||||
* example: "rafi@student.com"
|
* example: "rafi@student.com"
|
||||||
|
* intern_position:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* example: "Backend Developer"
|
||||||
* project:
|
* project:
|
||||||
* type: object
|
* type: object
|
||||||
* properties:
|
* properties:
|
||||||
@@ -373,6 +381,10 @@ router.get('/project/:id_project', verifyJWT, isMentorOrAdmin, certificateContro
|
|||||||
* intern_name:
|
* intern_name:
|
||||||
* type: string
|
* type: string
|
||||||
* example: "Rafi Athallah"
|
* example: "Rafi Athallah"
|
||||||
|
* intern_position:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* example: "Backend Developer"
|
||||||
* project:
|
* project:
|
||||||
* type: object
|
* type: object
|
||||||
* properties:
|
* properties:
|
||||||
@@ -385,6 +397,9 @@ router.get('/project/:id_project', verifyJWT, isMentorOrAdmin, certificateContro
|
|||||||
* description:
|
* description:
|
||||||
* type: string
|
* type: string
|
||||||
* example: "Project to develop features of the Internify web app"
|
* example: "Project to develop features of the Internify web app"
|
||||||
|
* intern_position:
|
||||||
|
* type: string
|
||||||
|
* example: "Backend Developer"
|
||||||
* start_date:
|
* start_date:
|
||||||
* type: string
|
* type: string
|
||||||
* format: date
|
* format: date
|
||||||
|
|||||||
Reference in New Issue
Block a user