first commit

This commit is contained in:
DaffaAfifi 2025-05-06 12:02:35 +07:00
commit 03e39564c1
87 changed files with 5627 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

34
cpu_ram_avg.js Normal file
View File

@ -0,0 +1,34 @@
const cpuRamAvg = () => {
const data = `1.50 2.80
1.50 2.80
1.50 2.80
1.50 2.80
1.50 2.80
1.50 2.80
1.50 2.80
1.50 2.80
1.50 2.80
1.60 2.80
1.70 2.80
1.70 2.80
1.80 2.80
1.90 2.80
2.00 2.80
2.10 2.80
2.20 2.80
2.20 2.80
2.20 2.80`
.split("\n")
.map((line) => {
const [cpu, ram] = line.split(" ").map(Number);
return { cpu, ram };
});
const avgCpu = data.reduce((sum, item) => sum + item.cpu, 0) / data.length;
const avgRam = data.reduce((sum, item) => sum + item.ram, 0) / data.length;
return { avgCpu, avgRam };
};
const result = cpuRamAvg();
console.log(result.avgCpu, result.avgRam);

16
express/.dockerignore Normal file
View File

@ -0,0 +1,16 @@
# Folder lingkungan virtual atau dependencies
node_modules/
# Environment variables
.env
# Folder build dan output
dist/
build/
# File log
*.log
# Cache atau file sementara
.DS_Store
*.env # Mengabaikan .env dari image Docker, hapus baris ini jika ingin menyertakan .env

31
express/.gitignore vendored Normal file
View File

@ -0,0 +1,31 @@
# Dependencies
node_modules/
# Environment variables
.env
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Temporary files
.DS_Store
Thumbs.db
# Debug
.vscode/
.idea/
# Build output
dist/
build/
coverage/
# Optional npm cache
.npm/
# Optional eslint cache
.eslintcache

13
express/Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM node:23-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

28
express/package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "express",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node src/main.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"dependencies": {
"bcrypt": "^5.1.1",
"dotenv": "^16.4.5",
"express": "^4.21.0",
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.2",
"mysql2": "^3.11.3",
"winston": "^3.14.2"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/express": "^4.17.21",
"nodemon": "^3.1.5"
}
}

1660
express/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

9
express/readme.md Normal file
View File

@ -0,0 +1,9 @@
pnpm i joi(validation), express, --save-dev @types/express(auto complete), --save-dev prisma, winston(logger), bcrypt(hashing), --save-dev @types/bcrypt, uuid(unique id), --save-dev @types/uuid, --save-dev jest @types/jest(unit test), --save-dev babel-jest @babel/present-env(for jest bcz type module), --save-dev supertest @types/supertest(unit test express)
setup db -> create model @prisma(prisma/schema.prisma) -> migrate
setup project -> prisma client(src/application/database.js) -> winston logger(src/application/logging.js) -> express(src/application/web.js)
folder structure -> init(src/application) -> logic(src/service) -> handle api(src/controller) -> validation(src/validation) -> routing(src/route) -> response error(src/error) -> middleware(middleware)
create endpoint -> validation -> service -> controller -> route

View File

@ -0,0 +1,17 @@
import "dotenv/config";
import mysql from "mysql2";
// Membuat pool koneksi database dengan konfigurasi dari file .env
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 50,
queueLimit: 0,
});
// Mengekspor pool agar dapat digunakan di file lain
export default pool;

View File

@ -0,0 +1,8 @@
import winston from "winston";
// Membuat konfigurasi logger dengan level log "info"
export const logger = winston.createLogger({
level: "info",
format: winston.format.json(),
transports: [new winston.transports.Console({})],
});

View File

@ -0,0 +1,11 @@
import express from "express";
import { publicRouter } from "../route/public-api.js";
import { userRouter } from "../route/api.js";
import { errorMiddleware } from "../middleware/error-middleware.js";
// Membuat dan mengonfigurasi aplikasi Express
export const web = express();
web.use(express.json());
web.use(publicRouter);
web.use(userRouter);
web.use(errorMiddleware);

View File

@ -0,0 +1,24 @@
import assistanceService from "../service/assistance-service.js";
import { response } from "../response/response.js";
// Controller untuk mengambil bantuan berdasarkan ID
const getAssistanceById = async (req, res, next) => {
try {
const result = await assistanceService.getAssistanceById(req.params.id);
response(200, result, "Get assistance success", res);
} catch (error) {
next(error);
}
};
// Controller untuk membuat alat bantuan baru
const createAssistanceTools = async (req, res, next) => {
try {
const result = await assistanceService.createAssistanceTools(req.body);
response(201, result, "Create assistance tools success", res);
} catch (error) {
next(error);
}
};
export default { getAssistanceById, createAssistanceTools };

View File

@ -0,0 +1,28 @@
import authService from "../service/auth-service.js";
import { response } from "../response/response.js";
// Controller untuk login user
const login = async (req, res, next) => {
try {
const result = await authService.login(req.body);
response(200, result, "Login success", res);
} catch (error) {
next(error);
}
};
// Controller untuk logout user
const logout = async (req, res, next) => {
try {
const token = req.get("Authorization");
const result = await authService.logout(token);
response(200, result, "Logout success", res);
} catch (error) {
next(error);
}
};
export default {
login,
logout,
};

View File

@ -0,0 +1,28 @@
import newsService from "../service/news-service.js";
import { response } from "../response/response.js";
// Controller untuk mendapatkan komentar berita berdasarkan ID
const getNewsCommentsById = async (req, res, next) => {
try {
const result = await newsService.getNewsCommentsById(req.params.id);
response(200, result, "Get news comment success", res);
} catch (error) {
next(error);
}
};
// Controller untuk test, hanya mengembalikan data statis
const test = async (req, res, next) => {
try {
res.status(200).json({
data: "test",
});
} catch (error) {
next(error);
}
};
export default {
getNewsCommentsById,
test,
};

View File

@ -0,0 +1,113 @@
import userService from "../service/user-service.js";
import { response } from "../response/response.js";
import jwt from "jsonwebtoken";
// Controller untuk test, hanya mengembalikan data statis
const test = async (req, res, next) => {
try {
res.status(200).json({
data: "test",
});
} catch (error) {
next(error);
}
};
// Controller untuk memverifikasi token JWT dan mengembalikan decoded data
const testToken = (req, res, next) => {
try {
const token = req.get("Authorization");
const secret = process.env.JWT_SECRET;
const decoded = jwt.verify(token, secret);
res.status(200).json({
data: decoded,
});
} catch (error) {
next(error);
}
};
// Controller untuk mendapatkan semua pengguna
const getUsers = async (req, res, next) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 100;
const result = await userService.getUsers(page, limit);
response(200, result, "Get users success", res);
} catch (error) {
next(error);
}
};
// Controller untuk membuat pengguna baru
const createUser = async (req, res, next) => {
try {
const result = await userService.createUser(req.body);
response(201, result, "Create user success", res);
} catch (error) {
next(error);
}
};
// Controller untuk mendapatkan pengguna berdasarkan ID
const getUserById = async (req, res, next) => {
try {
const result = await userService.getUserById(req.params.id);
response(200, result, "Get user success", res);
} catch (error) {
next(error);
}
};
// Controller untuk mendapatkan berita yang disimpan oleh pengguna
const getSavedNews = async (req, res, next) => {
try {
const result = await userService.getSavedNews(req.params.id);
response(200, result, "Get saved news success", res);
} catch (error) {
next(error);
}
};
// Controller untuk mendapatkan fasilitas yang dimiliki pengguna
const getFacilities = async (req, res, next) => {
try {
const result = await userService.getFacilities(req.params.id);
response(200, result, "Get facilities success", res);
} catch (error) {
next(error);
}
};
// Controller untuk memperbarui data pengguna berdasarkan ID
const updateUser = async (req, res, next) => {
try {
const result = await userService.updateUser(req.params.id, req.body);
response(200, result, "Update user success", res);
} catch (error) {
next(error);
}
};
// Controller untuk mendapatkan komentar berita yang disimpan oleh pengguna
const getSavedNewsComment = async (req, res, next) => {
try {
const result = await userService.getSavedNewsComment(req.params.id);
response(200, result, "Get saved news comment success", res);
} catch (error) {
next(error);
}
};
export default {
test,
getUsers,
createUser,
getUserById,
getSavedNews,
getFacilities,
updateUser,
getSavedNewsComment,
testToken,
};

8
express/src/main.js Normal file
View File

@ -0,0 +1,8 @@
import { logger } from "./application/logging.js";
import { web } from "./application/web.js";
const port = 3000;
web.listen(port, () => {
logger.info(`App started and running on port ${port}`);
});

View File

@ -0,0 +1,29 @@
import db from "../application/database.js";
import { ResponseError } from "../response/response-error.js";
import jwt from "jsonwebtoken";
// Middleware untuk memeriksa autentikasi menggunakan token JWT
export const authMiddleware = async (req, res, next) => {
const token = req.get("Authorization");
if (!token) {
return next(new ResponseError(401, "Unauthorized"));
}
try {
const decodedToken = jwt.verify(token, process.env.JWT_SECRET);
req.user = decodedToken;
const [rows] = await db
.promise()
.query("SELECT * FROM sessions WHERE token = ?", [token]);
if (rows.length === 0) {
return next(new ResponseError(401, "Unauthorized"));
}
next();
} catch (error) {
return next(new ResponseError(401, "Unauthorized"));
}
};

View File

@ -0,0 +1,27 @@
import { ResponseError } from "../response/response-error.js";
// Middleware untuk menangani error yang terjadi pada aplikasi
const errorMiddleware = async (err, req, res, next) => {
if (!err) {
next();
return;
}
if (err instanceof ResponseError) {
res
.status(err.status)
.json({
errors: err.message,
})
.end();
} else {
res
.status(500)
.json({
errors: err.message,
})
.end();
}
};
export { errorMiddleware };

View File

@ -0,0 +1,9 @@
// Kelas ResponseError yang mewarisi dari Error untuk membuat error dengan status khusus
class ResponseError extends Error {
constructor(status, message) {
super(message);
this.status = status;
}
}
export { ResponseError };

View File

@ -0,0 +1,14 @@
// Fungsi untuk mengirimkan respons JSON dengan status code, data, pesan, dan metadata
const response = (statusCode, data, message, res) => {
res.status(statusCode).json({
payload: data,
message,
metadata: {
prev: "",
next: "",
current: "",
},
});
};
export { response };

42
express/src/route/api.js Normal file
View File

@ -0,0 +1,42 @@
import express from "express";
import { authMiddleware } from "../middleware/auth-middleware.js";
import authController from "../controller/auth-controller.js";
import userController from "../controller/user-controller.js";
import newsController from "../controller/news-controller.js";
import assistanceController from "../controller/assistance-controller.js";
// Membuat router untuk API user
const userRouter = new express.Router();
// Menambahkan middleware autentikasi pada setiap route di bawah ini
userRouter.use(authMiddleware);
// Route untuk test token
userRouter.get("/api/test-token", userController.testToken);
// Users API
userRouter.get("/api/users", userController.getUsers); // Mendapatkan semua pengguna - low complexity
userRouter.post("/api/users", userController.createUser); // Membuat pengguna baru - low complexity
userRouter.put("/api/users/:id", userController.updateUser); // Memperbarui data pengguna - low complexity
userRouter.get("/api/users/:id", userController.getUserById); // Mendapatkan data pengguna berdasarkan ID - low complexity
userRouter.get("/api/users/saved-news/:id", userController.getSavedNews); // Mendapatkan berita yang disimpan oleh pengguna - medium complexity
userRouter.get("/api/users/facilities/:id", userController.getFacilities); // Mendapatkan fasilitas yang dimiliki pengguna (sertifikat, pelatihan, bantuan) - high complexity
userRouter.get(
"/api/users/saved-news/comment/:id",
userController.getSavedNewsComment
); // Mendapatkan komentar dari berita yang disimpan pengguna - high complexity
// News API
userRouter.get("/api/news/:id", newsController.getNewsCommentsById); // Mendapatkan berita dan komentar berdasarkan ID - medium complexity
// Assistance API
userRouter.get("/api/assistance/:id", assistanceController.getAssistanceById); // Mendapatkan bantuan & alat berdasarkan ID - medium complexity
userRouter.post(
"/api/assistance-tools",
assistanceController.createAssistanceTools
); // Membuat alat bantuan yang memicu pembaruan total harga - high complexity
// Logout
userRouter.post("/api/logout", authController.logout); // Logout pengguna - low complexity
export { userRouter };

View File

@ -0,0 +1,11 @@
import express from "express";
import authController from "../controller/auth-controller.js";
import userController from "../controller/user-controller.js";
// Membuat router untuk API publik
const publicRouter = new express.Router();
publicRouter.get("/api/test-connection", userController.test); // Uji koneksi - low complexity
publicRouter.post("/api/login", authController.login); // Login pengguna - medium complexity
export { publicRouter };

View File

@ -0,0 +1,77 @@
import db from "../application/database.js";
import { ResponseError } from "../response/response-error.js";
import "dotenv/config";
import { logger } from "../application/logging.js";
import { createAssistanceToolsValidation } from "../validation/assistance-tools-validation.js";
import { validate } from "../validation/validation.js";
// Fungsi untuk mengambil data bantuan berdasarkan ID
const getAssistanceById = async (id) => {
const connection = await db.promise().getConnection();
try {
const [rows] = await connection.query(
`SELECT
assistance.id, assistance.nama, assistance.koordinator,
assistance.sumber_anggaran, assistance.total_anggaran,
assistance.tahun_pemberian,
assistance_tools.kuantitas,
tools.nama_item, tools.harga, tools.deskripsi
FROM assistance
LEFT JOIN assistance_tools ON assistance.id = assistance_tools.assistance_id
LEFT JOIN tools ON assistance_tools.tools_id = tools.id
WHERE assistance.id = ?`,
[id]
);
if (rows.length === 0) {
throw new ResponseError(404, "Assistance not found");
}
const result = {
id: rows[0].id,
nama: rows[0].nama,
koordinator: rows[0].koordinator,
sumber_anggaran: rows[0].sumber_anggaran,
total_anggaran: rows[0].total_anggaran,
tahun_pemberian: rows[0].tahun_pemberian,
tools: [],
};
rows.forEach((row) => {
if (row.nama_item) {
result.tools.push({
nama_item: row.nama_item,
kuantitas: row.kuantitas,
harga: row.harga,
deskripsi: row.deskripsi,
});
}
});
return result;
} catch (error) {
logger.error(error);
throw new ResponseError(500, error.message);
} finally {
connection.release();
}
};
// Fungsi untuk membuat alat bantuan baru
const createAssistanceTools = async (req, res) => {
const connection = await db.promise().getConnection();
try {
const data = validate(createAssistanceToolsValidation, req);
const result = await connection.query(
"INSERT INTO assistance_tools SET ?",
[data]
);
return result;
} catch (error) {
throw new ResponseError(400, error.message);
} finally {
connection.release();
}
};
export default { getAssistanceById, createAssistanceTools };

View File

@ -0,0 +1,71 @@
import { loginUserValidation } from "../validation/user-validation.js";
import { validate } from "../validation/validation.js";
import db from "../application/database.js";
import bcrypt from "bcrypt";
import { ResponseError } from "../response/response-error.js";
import jwt from "jsonwebtoken";
import "dotenv/config";
// Fungsi untuk login pengguna
const login = async (req) => {
const connection = await db.promise().getConnection();
try {
const loginRequest = validate(loginUserValidation, req);
const [rows] = await connection.query(
"SELECT id, email, nama, role_id, password FROM users WHERE email = ?",
[loginRequest.email]
);
if (rows.length === 0) {
throw new ResponseError(400, "Username or password wrong");
}
const user = rows[0];
const hash = user.password.replace("$2y$", "$2a$");
const isPasswordValid = await bcrypt.compare(loginRequest.password, hash);
if (!isPasswordValid) {
throw new ResponseError(400, "Username or password wrong");
}
const token = jwt.sign(
{ id: user.id, email: user.email, nama: user.nama, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: "2h" }
);
await connection.query(
"INSERT INTO sessions (token, user_id, expiry, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
[token, user.id, new Date(Date.now() + 7200000), new Date(), new Date()]
);
return token;
} catch (error) {
throw new ResponseError(400, error.message);
} finally {
connection.release();
}
};
// Fungsi untuk logout pengguna
const logout = async (token) => {
const connection = await db.promise().getConnection();
try {
const result = await connection.query(
"DELETE FROM sessions WHERE token = ?",
[token]
);
return result;
} catch (error) {
throw new ResponseError(400, error.message);
} finally {
connection.release();
}
};
export default {
login,
logout,
};

View File

@ -0,0 +1,53 @@
import db from "../application/database.js";
import { ResponseError } from "../response/response-error.js";
import "dotenv/config";
import { logger } from "../application/logging.js";
// Fungsi untuk mendapatkan berita beserta komentar berdasarkan ID berita
const getNewsCommentsById = async (id) => {
const connection = await db.promise().getConnection();
try {
const [rows] = await connection.query(
`SELECT
news.id, news.gambar, news.judul, news.subjudul, news.isi, news.created_at,
comments.user_id AS user_id, comments.comment, comments.created_at AS comment_created_at
FROM news
LEFT JOIN comments ON news.id = comments.news_id
WHERE news.id = ?`,
[id]
);
if (rows.length === 0) {
throw new ResponseError(404, "News not found");
}
const result = {
id: rows[0].id,
gambar: rows[0].gambar,
judul: rows[0].judul,
subjudul: rows[0].subjudul,
isi: rows[0].isi,
created_at: rows[0].created_at,
comments: [],
};
rows.forEach((row) => {
if (row.comment) {
result.comments.push({
user_id: row.user_id,
comment: row.comment,
created_at: row.comment_created_at,
});
}
});
return result;
} catch (error) {
logger.error(error);
throw new ResponseError(500, error.message);
} finally {
connection.release();
}
};
export default { getNewsCommentsById };

View File

@ -0,0 +1,334 @@
import {
createUserValidation,
updateUserValidation,
} from "../validation/user-validation.js";
import { validate } from "../validation/validation.js";
import db from "../application/database.js";
import bcrypt from "bcrypt";
import { ResponseError } from "../response/response-error.js";
import "dotenv/config";
// Fungsi untuk mendapatkan semua pengguna dari database
const getUsers = async (page, limit) => {
const connection = await db.promise().getConnection();
try {
const offset = (page - 1) * limit;
const [rows] = await connection.query(
"SELECT nama, email, NIK, alamat, telepon, jenis_kelamin, kepala_keluarga, tempat_lahir, tanggal_lahir, jenis_usaha FROM users LIMIT ? OFFSET ?",
[limit, offset]
);
return rows;
} catch (error) {
throw new ResponseError(400, error.message);
} finally {
connection.release();
}
};
// Fungsi untuk membuat pengguna baru
const createUser = async (req, res) => {
const connection = await db.promise().getConnection();
try {
const user = validate(createUserValidation, req);
user.password = await bcrypt.hash(user.password, 10);
const [result] = await connection.query(
`INSERT INTO users (nama, email, password, NIK, alamat, telepon, jenis_kelamin, kepala_keluarga, tempat_lahir, tanggal_lahir, jenis_usaha, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE email = email`,
[
user.nama,
user.email,
user.password,
user.NIK,
user.alamat,
user.telepon,
user.jenis_kelamin,
user.kepala_keluarga,
user.tempat_lahir,
user.tanggal_lahir,
user.jenis_usaha,
new Date(),
new Date(),
]
);
if (result.affectedRows === 0) {
throw new ResponseError(400, "Email or NIK already exists");
}
return result;
} catch (error) {
throw new ResponseError(400, error.message);
} finally {
connection.release();
}
};
// Fungsi untuk mendapatkan data pengguna berdasarkan ID
const getUserById = async (id) => {
const connection = await db.promise().getConnection();
try {
const [rows] = await connection.query(
"SELECT nama, email, NIK, alamat, telepon, jenis_kelamin, kepala_keluarga, tempat_lahir, tanggal_lahir, jenis_usaha FROM users WHERE id = ?",
[id]
);
if (rows.length === 0) {
throw new ResponseError(404, "User not found");
}
return rows[0];
} catch (error) {
throw new ResponseError(400, error.message);
} finally {
connection.release();
}
};
// Fungsi untuk mendapatkan berita yang disimpan oleh pengguna berdasarkan ID
const getSavedNews = async (id) => {
const connection = await db.promise().getConnection();
try {
const [rows] = await connection.query(
`SELECT
users.id, users.nama, users.email, news.id AS news_id, news.gambar, news.judul, news.subjudul, news.isi, news.created_at
FROM users
INNER JOIN saved_news ON users.id = saved_news.user_id
INNER JOIN news ON saved_news.news_id = news.id
WHERE users.id = ?`,
[id]
);
if (rows.length === 0) {
throw new ResponseError(404, "User or saved news not found");
}
const payload = {
id: rows[0].id,
nama: rows[0].nama,
email: rows[0].email,
berita_tersimpan: rows.map((row) => ({
id: row.news_id,
gambar: row.gambar,
judul: row.judul,
subjudul: row.subjudul,
isi: row.isi,
created_at: new Date(row.created_at).toLocaleDateString("en-GB"),
})),
};
return payload;
} catch (error) {
throw new ResponseError(400, error.message);
} finally {
connection.release();
}
};
// Mendapatkan fasilitas user (sertifikat, pelatihan, bantuan, dan alat)
const getFacilities = async (id) => {
const connection = await db.promise().getConnection();
try {
const [rows] = await connection.query(
`SELECT
users.id, users.email,
sertificates.id AS id_sertifikat, sertificates.nama AS nama_sertifikat, user_sertificates.no_sertifikat, sertificates.tanggal_terbit, sertificates.kadaluarsa, sertificates.keterangan,
trainings.id AS id_pelatihan, trainings.nama AS nama_pelatihan, trainings.penyelenggara, trainings.tanggal_pelaksanaan, trainings.tempat,
assistance.id AS id_bantuan, assistance.nama AS nama_bantuan, assistance.koordinator, assistance.sumber_anggaran, assistance.total_anggaran, assistance.tahun_pemberian,
assistance_tools.kuantitas,
tools.id AS id_alat, tools.nama_item, tools.harga, tools.deskripsi
FROM users
LEFT JOIN user_sertificates ON users.id = user_sertificates.user_id
LEFT JOIN sertificates ON user_sertificates.sertificates_id = sertificates.id
LEFT JOIN user_trainings ON users.id = user_trainings.user_id
LEFT JOIN trainings ON user_trainings.trainings_id = trainings.id
LEFT JOIN assistance ON users.id = assistance.user_id
LEFT JOIN assistance_tools ON assistance.id = assistance_tools.assistance_id
LEFT JOIN tools ON assistance_tools.tools_id = tools.id
WHERE users.id = ?`,
[id]
);
if (rows.length === 0) {
throw new ResponseError(404, "User or facilities not found");
}
const result = {
id: rows[0].id,
nama: rows[0].nama,
email: rows[0].email,
sertifikat: [],
pelatihan: [],
bantuan: [],
};
const helpMap = {};
rows.forEach((row) => {
if (
row.id_sertifikat &&
!result.sertifikat.some((c) => c.id === row.id_sertifikat)
) {
result.sertifikat.push({
id: row.id_sertifikat,
nama: row.nama_sertifikat,
no_sertifikat: row.no_sertifikat,
tanggal_terbit: row.tanggal_terbit,
kadaluarsa: row.kadaluarsa,
keterangan: row.keterangan,
});
}
if (
row.id_pelatihan &&
!result.pelatihan.some((t) => t.id === row.id_pelatihan)
) {
result.pelatihan.push({
id: row.id_pelatihan,
nama: row.nama_pelatihan,
koordinator: row.penyelenggara,
tanggal_pelaksanaan: row.tanggal_pelaksanaan,
tempat: row.tempat,
});
}
if (row.id_bantuan) {
if (!helpMap[row.id_bantuan]) {
helpMap[row.id_bantuan] = {
id: row.id_bantuan,
nama: row.nama_bantuan,
koordinator: row.koordinator,
sumber_anggaran: row.sumber_anggaran,
tahun_pemberian: row.tahun_pemberian,
total_anggaran: row.total_anggaran,
alat: [],
};
}
if (
row.id_alat &&
!helpMap[row.id_bantuan].alat.some((tool) => tool.id === row.id_alat)
) {
helpMap[row.id_bantuan].alat.push({
id: row.id_alat,
nama: row.nama_item,
harga: row.harga,
kuantitas: row.kuantitas,
});
}
}
});
result.bantuan = Object.values(helpMap);
return result;
} catch (error) {
throw new ResponseError(400, error.message);
} finally {
connection.release();
}
};
// Mengupdate data user
const updateUser = async (id, data) => {
const connection = await db.promise().getConnection();
const userUpdates = validate(updateUserValidation, data);
const updates = [];
const values = [];
Object.keys(userUpdates).forEach((key) => {
updates.push(`${key} = ?`);
values.push(userUpdates[key]);
});
if (updates.length === 0) {
throw new ResponseError(400, "No valid fields to update");
}
const query = `UPDATE users SET ${updates.join(", ")} WHERE id = ?`;
values.push(id);
try {
const result = await connection.query(query, values);
if (result.affectedRows === 0) {
throw new ResponseError(404, "User not found");
}
return result;
} catch (error) {
throw new ResponseError(400, error.message);
} finally {
connection.release();
}
};
// Mendapatkan komentar dari berita yang disimpan oleh user
const getSavedNewsComment = async (id) => {
const connection = await db.promise().getConnection();
try {
const [rows] = await connection.query(
`SELECT
users.id, users.nama, users.email, news.id AS news_id, news.gambar, news.judul, news.subjudul, news.isi, news.created_at, comments.news_id AS comment_id, comments.comment, comments.created_at
FROM users
LEFT JOIN saved_news ON users.id = saved_news.user_id
LEFT JOIN news ON saved_news.news_id = news.id
LEFT JOIN comments ON news.id = comments.news_id
WHERE users.id = ?`,
[id]
);
if (rows.length === 0) {
throw new ResponseError(404, "User or saved news not found");
}
const result = {
id: rows[0].id,
nama: rows[0].nama,
email: rows[0].email,
news: [],
};
const newsMap = {};
rows.forEach((row) => {
if (!newsMap[row.news_id]) {
newsMap[row.news_id] = {
id: row.news_id,
gambar: row.gambar,
judul: row.judul,
subjudul: row.subjudul,
isi: row.isi,
created_at: row.created_at,
comment: [],
};
}
if (row.comment_id) {
newsMap[row.news_id].comment.push({
comment: row.comment,
created_at: row.created_at,
});
}
});
result.news = Object.values(newsMap);
return result;
} catch (error) {
throw new ResponseError(400, error.message);
} finally {
connection.release();
}
};
export default {
getUsers,
createUser,
getUserById,
getSavedNews,
getFacilities,
updateUser,
getSavedNewsComment,
};

View File

@ -0,0 +1,18 @@
import Joi from "joi";
const createAssistanceToolsValidation = Joi.object({
assistance_id: Joi.number().integer().required().messages({
"number.base": "ID asistance harus berupa angka",
"any.required": "ID asistance harus diisi",
}),
tools_id: Joi.number().integer().required().messages({
"number.base": "ID tools harus berupa angka",
"any.required": "ID tools harus diisi",
}),
kuantitas: Joi.number().integer().required().messages({
"number.base": "Kuantitas harus berupa angka",
"any.required": "Kuantitas harus diisi",
}),
});
export { createAssistanceToolsValidation };

View File

@ -0,0 +1,105 @@
import Joi from "joi";
const createUserValidation = Joi.object({
nama: Joi.string().max(100).required().messages({
"string.base": "Nama harus berupa teks",
"string.max": "Nama tidak boleh lebih dari 100 karakter",
"any.required": "Nama harus diisi",
}),
email: Joi.string().email({ minDomainSegments: 2 }).required().messages({
"string.email": "Email tidak valid",
"any.required": "Email harus diisi",
}),
password: Joi.string().min(6).required().messages({
"string.min": "Password minimal harus memiliki 6 karakter",
"any.required": "Password harus diisi",
}),
NIK: Joi.string().length(16).regex(/^\d+$/).required().messages({
"string.length": "NIK harus terdiri dari 16 karakter",
"string.pattern.base": "NIK hanya boleh terdiri dari angka",
"any.required": "NIK harus diisi",
}),
alamat: Joi.string().max(100).required().messages({
"string.max": "Alamat tidak boleh lebih dari 100 karakter",
"any.required": "Alamat harus diisi",
}),
telepon: Joi.string().max(15).regex(/^\d+$/).required().messages({
"string.max": "Nomor telepon tidak boleh lebih dari 15 karakter",
"string.pattern.base": "Nomor telepon hanya boleh terdiri dari angka",
"any.required": "Nomor telepon harus diisi",
}),
jenis_kelamin: Joi.string().valid("L", "P").required().messages({
"any.only": "Jenis kelamin harus 'L' (Laki-laki) atau 'P' (Perempuan)",
"any.required": "Jenis kelamin harus diisi",
}),
kepala_keluarga: Joi.number().integer().valid(0, 1).required().messages({
"any.only": "Kepala keluarga harus bernilai 0 atau 1",
"number.base": "Kepala keluarga harus berupa angka",
"any.required": "Field kepala keluarga wajib diisi",
}),
tempat_lahir: Joi.string().max(50).required().messages({
"string.max": "Tempat lahir tidak boleh lebih dari 50 karakter",
"any.required": "Tempat lahir harus diisi",
}),
tanggal_lahir: Joi.date().iso().required().messages({
"date.format": "Tanggal lahir harus dalam format yang valid (YYYY-MM-DD)",
"any.required": "Tanggal lahir harus diisi",
}),
jenis_usaha: Joi.string().max(50).required().messages({
"string.max": "Jenis usaha tidak boleh lebih dari 50 karakter",
"any.required": "Jenis usaha harus diisi",
}),
});
const loginUserValidation = Joi.object({
email: Joi.string().email({ minDomainSegments: 2 }).required().messages({
"string.email": "Email tidak valid",
"any.required": "Email harus diisi",
}),
password: Joi.string().min(6).required().messages({
"string.min": "Password minimal harus memiliki 6 karakter",
"any.required": "Password harus diisi",
}),
});
const updateUserValidation = Joi.object({
nama: Joi.string().max(100).optional().messages({
"string.base": "Nama harus berupa teks",
"string.max": "Nama tidak boleh lebih dari 100 karakter",
}),
email: Joi.string().email({ minDomainSegments: 2 }).optional().messages({
"string.email": "Email tidak valid",
}),
password: Joi.string().min(6).optional().messages({
"string.min": "Password minimal harus memiliki 6 karakter",
}),
NIK: Joi.string().length(16).regex(/^\d+$/).optional().messages({
"string.length": "NIK harus terdiri dari 16 karakter",
"string.pattern.base": "NIK hanya boleh terdiri dari angka",
}),
alamat: Joi.string().max(100).optional().messages({
"string.max": "Alamat tidak boleh lebih dari 100 karakter",
}),
telepon: Joi.string().max(15).regex(/^\d+$/).optional().messages({
"string.max": "Nomor telepon tidak boleh lebih dari 15 karakter",
"string.pattern.base": "Nomor telepon hanya boleh terdiri dari angka",
}),
jenis_kelamin: Joi.string().valid("L", "P").optional().messages({
"any.only": "Jenis kelamin harus 'L' (Laki-laki) atau 'P' (Perempuan)",
}),
kepala_keluarga: Joi.number().integer().valid(0, 1).optional().messages({
"any.only": "Kepala keluarga harus bernilai 0 atau 1",
"number.base": "Kepala keluarga harus berupa angka",
}),
tempat_lahir: Joi.string().max(50).optional().messages({
"string.max": "Tempat lahir tidak boleh lebih dari 50 karakter",
}),
tanggal_lahir: Joi.date().iso().optional().messages({
"date.format": "Tanggal lahir harus dalam format yang valid (YYYY-MM-DD)",
}),
jenis_usaha: Joi.string().max(50).optional().messages({
"string.max": "Jenis usaha tidak boleh lebih dari 50 karakter",
}),
});
export { createUserValidation, loginUserValidation, updateUserValidation };

View File

@ -0,0 +1,20 @@
import { ResponseError } from "../response/response-error.js";
// Fungsi untuk melakukan validasi menggunakan schema yang telah didefinisikan
const validate = (schema, request) => {
// Melakukan validasi pada request menggunakan schema
const result = schema.validate(request, {
abortEarly: false, // Melanjutkan validasi meskipun ada kesalahan
allowUnknown: false, // Menolak field yang tidak dikenali dalam request
});
// Jika ada error, lemparkan ResponseError dengan status 400 dan pesan error
if (result.error) {
throw new ResponseError(400, result.error.message);
} else {
// Kembalikan data yang sudah divalidasi
return result.value;
}
};
export { validate };

27
flask/.dockerignore Normal file
View File

@ -0,0 +1,27 @@
# Virtual environment
env/
venv/
#
.env
# Bytecode dan cache
__pycache__/
*.py[cod]
*$py.class
# File log
*.log
# Build artifacts
*.egg-info/
dist/
build/
# Dependency directories
vendor/
# IDE/editor-specific directories
.idea/
.vscode/
.DS_Store

19
flask/.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
# Lingkungan virtual
env/
venv/
# File konfigurasi sensitif
.env
# Bytecode dan cache
__pycache__/
*.py[cod]
*$py.class
# File log
*.log
# Metadata build dan distribusi
*.egg-info/
dist/
build/

20
flask/Dockerfile Normal file
View File

@ -0,0 +1,20 @@
# Menggunakan Python 3.11 sebagai base image
FROM python:3.11-alpine
# Menetapkan direktori kerja
WORKDIR /app
# Menyalin file requirements.txt untuk mengelola dependensi
COPY requirements.txt .
# Menginstal dependensi aplikasi
RUN pip install --no-cache-dir -r requirements.txt
# Menyalin seluruh source code dari folder src ke dalam folder /app di container
COPY src/ /app/
# Mengekspos port Flask (default 5000)
EXPOSE 5000
# Menentukan perintah untuk menjalankan aplikasi
CMD ["python", "/app/main.py"]

18
flask/requirements.txt Normal file
View File

@ -0,0 +1,18 @@
annotated-types==0.7.0
bcrypt==4.2.0
blinker==1.8.2
click==8.1.7
Flask==3.0.3
itsdangerous==2.2.0
Jinja2==3.1.4
loguru==0.7.2
MarkupSafe==3.0.2
mysql-connector-python==8.0.33
packaging==24.1
pydantic==2.9.2
pydantic_core==2.23.4
PyJWT==2.9.0
python-dotenv==1.0.1
typing_extensions==4.12.2
Werkzeug==3.1.2
email-validator==2.2.0

View File

@ -0,0 +1,43 @@
import os
import mysql.connector
from mysql.connector import pooling
from dotenv import load_dotenv
load_dotenv()
POOL_SIZE = 32
# Fungsi untuk membuat pool koneksi MySQL
def create_pool():
try:
pool = pooling.MySQLConnectionPool(
pool_name="my_pool",
pool_size=POOL_SIZE,
host=os.getenv("DB_HOST"),
port=os.getenv("DB_PORT"),
user=os.getenv("DB_USER"),
password=os.getenv("DB_PASSWORD"),
database=os.getenv("DB_NAME")
)
print("MySQL Connection Pool created successfully")
return pool
except mysql.connector.Error as err:
print(f"Error creating connection pool: {err}")
return None
# Membuat pool koneksi dan menyimpannya ke variabel db_pool
db_pool = create_pool()
# Fungsi untuk mendapatkan koneksi baru dari pool
def get_connection():
try:
if db_pool is None:
raise Exception("Connection pool not initialized")
connection = db_pool.get_connection()
if connection.is_connected():
return connection
else:
raise Exception("Connection is not valid")
except Exception as e:
print(f"Error getting connection from pool: {e}")
return None

View File

@ -0,0 +1,9 @@
from loguru import logger
import sys
logger.remove()
logger.add(sys.stderr, level="INFO", format="{time} {level} {message}")
if __name__ == "__main__":
logger.info("Logger is ready")

View File

@ -0,0 +1,13 @@
from flask import Flask
from middleware.error_middleware import error_middleware
from response.response_error import ResponseError
from route.api import router
from route.public_api import public_router
# Fungsi untuk membuat dan mengonfigurasi aplikasi Flask
def create_app():
app = Flask(__name__)
app.register_blueprint(public_router, url_prefix='/api')
app.register_blueprint(router, url_prefix='/api')
app.register_error_handler(ResponseError, error_middleware)
return app

View File

@ -0,0 +1,21 @@
from flask import request
from response.response_error import ResponseError
from response.response import response
from service.assistance_service import get_assistance_tools, create_assistance_tools
# Controller untuk menangani permintaan GET untuk alat bantuan
def get_assistance_tools_controller(id):
try:
result = get_assistance_tools(id)
return response(200, result, "Get assistance tools success")
except ResponseError as e:
raise e
# Controller untuk menangani permintaan POST untuk membuat alat bantuan
def create_assistance_tools_controller():
try:
data = request.get_json()
result = create_assistance_tools(data)
return response(200, result, "Create assistance tools success")
except ResponseError as e:
raise e

View File

@ -0,0 +1,22 @@
from flask import request
from service.auth_service import login, logout
from response.response import response
from response.response_error import ResponseError
# Controller untuk menangani proses login pengguna
def login_controller():
try:
data = request.get_json()
result = login(data)
return response(200, result, "Login success")
except ResponseError as e:
raise e
# Controller untuk menangani proses logout pengguna
def logout_controller():
try:
token = request.headers.get("Authorization")
result = logout(token)
return response(200, result, "Logout success")
except ResponseError as e:
raise e

View File

@ -0,0 +1,11 @@
from response.response import response
from response.response_error import ResponseError
from service.news_service import get_news_comments
# Controller untuk menangani request dan mengembalikan komentar berita berdasarkan ID
def get_news_comments_controller(id):
try:
result = get_news_comments(id)
return response(200, result, "Get news comments success")
except ResponseError as e:
raise e

View File

@ -0,0 +1,65 @@
from flask import request
from response.response import response
from response.response_error import ResponseError
from service.user_service import get_users, get_user_by_id, create_user, get_user_saved_news, get_user_saved_news_comments, get_user_facilities, update_user
# Controller untuk mengambil semua data pengguna
def get_users_controller():
try:
page = request.args.get('page', default=1, type=int)
limit = request.args.get('limit', default=100, type=int)
result = get_users(page, limit)
return response(200, result, "Get users success")
except ResponseError as e:
raise e
# Controller untuk mengambil data pengguna berdasarkan ID
def get_user_by_id_controller(id):
try:
result = get_user_by_id(id)
return response(200, result, "Get user by id success")
except ResponseError as e:
raise e
# Controller untuk membuat pengguna baru
def create_user_controller():
try:
data = request.get_json()
result = create_user(data)
return response(200, result, "Create user success")
except ResponseError as e:
raise e
# Controller untuk mengambil berita yang disimpan oleh pengguna berdasarkan ID
def get_user_saved_news_controller(id):
try:
result = get_user_saved_news(id)
return response(200, result, "Get user saved news success")
except ResponseError as e:
raise e
# Controller untuk mengambil komentar berita yang disimpan oleh pengguna berdasarkan ID
def get_user_saved_news_comments_controller(id):
try:
result = get_user_saved_news_comments(id)
return response(200, result, "Get user saved news comments success")
except ResponseError as e:
raise e
# Controller untuk mengambil fasilitas yang dimiliki pengguna berdasarkan ID
def get_user_facilities_controller(id):
try:
result = get_user_facilities(id)
return response(200, result, "Get user facilities success")
except ResponseError as e:
raise e
# Controller untuk memperbarui data pengguna berdasarkan ID
def update_user_controller(id):
try:
data = request.get_json()
result = update_user(id, data)
return response(200, result, "Update user success")
except ResponseError as e:
raise e

9
flask/src/main.py Normal file
View File

@ -0,0 +1,9 @@
from application.web import create_app
from application.logging import logger
app = create_app()
port = 5000
if __name__ == "__main__":
app.run(host="0.0.0.0", port=port, debug=True)
logger.info(f"Server started on port {port}")

View File

@ -0,0 +1,50 @@
import os
import jwt
from functools import wraps
from flask import request, jsonify
from application.database import get_connection
from response.response_error import ResponseError
from application.logging import logger
# Middleware untuk autentikasi
def auth_middleware(f):
@wraps(f)
def decorated_function(*args, **kwargs):
token = request.headers.get("Authorization")
logger.info(token)
if not token:
raise ResponseError(401, "Unauthorized")
try:
jwt_secret = os.getenv("JWT_SECRET")
decoded_token = jwt.decode(token, jwt_secret, algorithms=["HS256"])
connection = get_connection()
if connection is None:
raise ResponseError(500, "Error obtaining database connection")
cursor = connection.cursor(dictionary=True)
query = "SELECT * FROM sessions WHERE token = %s"
cursor.execute(query, (token,))
session = cursor.fetchone()
if not session:
raise ResponseError(401, "Unauthorized")
request.user = decoded_token
return f(*args, **kwargs)
except jwt.ExpiredSignatureError:
raise ResponseError(401, "Token has expired")
except jwt.InvalidTokenError:
raise ResponseError(401, "Invalid token")
except ResponseError as e:
raise e
except Exception as e:
raise ResponseError(500, str(e))
finally:
if cursor:
cursor.close()
if connection:
connection.close()
return decorated_function

View File

@ -0,0 +1,15 @@
from flask import Flask, jsonify
from response.response_error import ResponseError
app = Flask(__name__)
# Error handler untuk menangani exception yang terjadi di aplikasi
@app.errorhandler(Exception)
def error_middleware(error):
if isinstance(error, ResponseError):
response = jsonify({"errors": str(error)})
response.status_code = error.status
else:
response = jsonify({"errors": str(error)})
response.status_code = 500
return response

View File

@ -0,0 +1,13 @@
from flask import jsonify
# Fungsi untuk membentuk response API yang konsisten dengan status, data, dan pesan
def response(status_code, data, message):
return jsonify({
"payload": data,
"message": message,
"metadata": {
"prev": "",
"next": "",
"current": "",
}
}), status_code

View File

@ -0,0 +1,8 @@
class ResponseError(Exception):
def __init__(self, status, message):
super().__init__(message)
self.status = status
if __name__ == "__main__":
error = ResponseError(404, "Not found")
print(f"Error: {error}, Status: {error.status}")

74
flask/src/route/api.py Normal file
View File

@ -0,0 +1,74 @@
from flask import Blueprint
from controller.user_controller import get_users_controller, get_user_by_id_controller, create_user_controller, get_user_saved_news_controller, get_user_saved_news_comments_controller, get_user_facilities_controller, update_user_controller
from controller.news_controller import get_news_comments_controller
from controller.assistance_controller import get_assistance_tools_controller, create_assistance_tools_controller
from controller.auth_controller import logout_controller
from middleware.auth_middleware import auth_middleware
router = Blueprint('user', __name__)
# get all users
@router.route('/users', methods=['GET'])
@auth_middleware
def get_users():
return get_users_controller()
# get user by id
@router.route('/users/<id>', methods=['GET'])
@auth_middleware
def get_user_by_id(id):
return get_user_by_id_controller(id)
# create user
@router.route('/users', methods=['POST'])
@auth_middleware
def create_user():
return create_user_controller()
# get user saved news
@router.route('/users/saved-news/<id>', methods=['GET'])
@auth_middleware
def get_user_saved_news(id):
return get_user_saved_news_controller(id)
# get user saved news comments
@router.route('/users/saved-news/comment/<id>', methods=['GET'])
@auth_middleware
def get_user_saved_news_comment(id):
return get_user_saved_news_comments_controller(id)
# get user facilities
@router.route('/users/facilities/<id>', methods=['GET'])
@auth_middleware
def get_user_facilities(id):
return get_user_facilities_controller(id)
# update user
@router.route('/users/<id>', methods=['PUT'])
@auth_middleware
def update_user(id):
return update_user_controller(id)
# get news comments by news id
@router.route('/news/<id>', methods=['GET'])
@auth_middleware
def get_news_comments(id):
return get_news_comments_controller(id)
# get assistance tools by assistance id
@router.route('/assistance/<id>', methods=['GET'])
@auth_middleware
def get_assistance_tools(id):
return get_assistance_tools_controller(id)
# create assistance tools
@router.route('/assistance-tools', methods=['POST'])
@auth_middleware
def create_assistance_tools():
return create_assistance_tools_controller()
# logout
@router.route('/logout', methods=['POST'])
@auth_middleware
def logout():
return logout_controller()

View File

@ -0,0 +1,9 @@
from flask import Blueprint
from controller.auth_controller import login_controller
public_router = Blueprint('public', __name__)
# login
@public_router.route('/login', methods=['POST'])
def login():
return login_controller()

View File

@ -0,0 +1,95 @@
from response.response_error import ResponseError
from validation.validation import validate
from validation.assistance_validation import CreateAssistanceToolsValidation
from application.database import get_connection
# Fungsi untuk mendapatkan data tools terkait bantuan berdasarkan ID
def get_assistance_tools(id):
connection = get_connection()
if connection is None:
raise ResponseError(500, "Error obtaining database connection")
cursor = connection.cursor(dictionary=True)
try:
query = """
SELECT
assistance.id, assistance.nama, assistance.koordinator,
assistance.sumber_anggaran, assistance.total_anggaran,
assistance.tahun_pemberian,
assistance_tools.kuantitas,
tools.nama_item, tools.harga, tools.deskripsi
FROM assistance
LEFT JOIN assistance_tools ON assistance.id = assistance_tools.assistance_id
LEFT JOIN tools ON assistance_tools.tools_id = tools.id
WHERE assistance.id = %s
"""
cursor.execute(query, (id,))
rows = cursor.fetchall()
if not rows:
raise ResponseError(404, "Assistance not found")
assistance = rows[0]
payload = {
'id': assistance['id'],
'nama': assistance['nama'],
'koordinator': assistance['koordinator'],
'sumber_anggaran': assistance['sumber_anggaran'],
'total_anggaran': assistance['total_anggaran'],
'tahun_pemberian': assistance['tahun_pemberian'],
'tools': []
}
for row in rows:
if row['nama_item']:
payload['tools'].append({
'kuantitas': row['kuantitas'],
'nama_item': row['nama_item'],
'harga': row['harga'],
'deskripsi': row['deskripsi']
})
return payload
except ResponseError as e:
raise e
except Exception as e:
raise ResponseError(500, str(e))
finally:
if cursor:
cursor.close()
if connection:
connection.close()
# Fungsi untuk membuat data alat yang terkait dengan bantuan
def create_assistance_tools(req):
connection = get_connection()
if connection is None:
raise ResponseError(500, "Error obtaining database connection")
cursor = connection.cursor(dictionary=True)
try:
data = validate(CreateAssistanceToolsValidation, req)
assistance_id = data.assistance_id
tools_id = data.tools_id
kuantitas = data.kuantitas
query = """
INSERT INTO assistance_tools (assistance_id, tools_id, kuantitas)
VALUES (%s, %s, %s)
"""
cursor.execute(query, (assistance_id, tools_id, kuantitas))
connection.commit()
return "oke"
except ResponseError as e:
raise e
except Exception as e:
raise ResponseError(500, str(e))
finally:
if cursor:
cursor.close()
if connection:
connection.close()

View File

@ -0,0 +1,104 @@
import bcrypt
import jwt
import os
from datetime import datetime, timedelta
from application.database import get_connection
from response.response_error import ResponseError
from validation.user_validation import LoginUserValidation
from validation.validation import validate
# Fungsi untuk login user, memverifikasi kredensial, dan menghasilkan token
def login(req):
connection = get_connection()
if connection is None:
raise ResponseError(500, "Error obtaining database connection")
cursor = connection.cursor(dictionary=True)
try:
data = validate(LoginUserValidation, req)
email = data.email
password = data.password
print(email)
cursor.execute(
"SELECT id, email, nama, role_id, password FROM users WHERE email = %s",
(email,)
)
user = cursor.fetchone()
print(user)
if not user:
raise ResponseError(400, "Username or password wrong hehe")
if not bcrypt.checkpw(password.encode('utf-8'), user['password'].encode('utf-8')):
raise ResponseError(400, "Username or password wrong haha")
exp_time = (datetime.now() + timedelta(hours=2)).timestamp()
token = jwt.encode(
{
'id': user['id'],
'email': user['email'],
'nama': user['nama'],
'role': user['role_id'],
'exp': exp_time
},
os.getenv('JWT_SECRET'),
algorithm='HS256'
)
cursor.execute(
"""
INSERT INTO sessions (token, user_id, expiry, created_at, updated_at)
VALUES (%s, %s, %s, %s, %s)
""",
(
token,
user['id'],
datetime.utcnow() + timedelta(hours=2),
datetime.utcnow(),
datetime.utcnow()
)
)
connection.commit()
return token
except ResponseError as e:
raise e
except Exception as e:
raise ResponseError(500, str(e))
finally:
if cursor:
cursor.close()
if connection:
connection.close()
# Fungsi untuk logout user dengan menghapus sesi token
def logout(token):
connection = get_connection()
if connection is None:
raise ResponseError(500, "Error obtaining database connection")
cursor = connection.cursor(dictionary=True)
try:
cursor.execute(
"DELETE FROM sessions WHERE token = %s",
(token,)
)
connection.commit()
return "oke"
except ResponseError as e:
raise e
except Exception as e:
raise ResponseError(500, str(e))
finally:
if cursor:
cursor.close()
if connection:
connection.close()

View File

@ -0,0 +1,54 @@
from application.database import get_connection
from response.response_error import ResponseError
# Fungsi untuk mendapatkan komentar dari berita berdasarkan ID berita
def get_news_comments(id):
connection = get_connection()
if connection is None:
raise ResponseError(500, "Error obtaining database connection")
cursor = connection.cursor(dictionary=True)
try:
query = """
SELECT
news.id, news.gambar, news.judul, news.subjudul, news.isi, news.created_at,
comments.user_id AS user_id, comments.comment, comments.created_at AS comment_created_at
FROM news
LEFT JOIN comments ON news.id = comments.news_id
WHERE news.id = %s
"""
cursor.execute(query, (id,))
rows = cursor.fetchall()
if not rows:
raise ResponseError(404, "News not found")
news = rows[0]
payload = {
'id': news['id'],
'gambar': news['gambar'],
'judul': news['judul'],
'subjudul': news['subjudul'],
'isi': news['isi'],
'created_at': news['created_at'],
'comments': []
}
for row in rows:
if row['comment']:
payload['comments'].append({
'user_id': row['user_id'],
'comment': row['comment'],
'comment_created_at': row['comment_created_at']
})
return payload
except ResponseError as e:
raise e
except Exception as e:
raise ResponseError(500, str(e))
finally:
if cursor:
cursor.close()
if connection:
connection.close()

View File

@ -0,0 +1,347 @@
from application.database import get_connection
from response.response_error import ResponseError
import bcrypt
from validation.validation import validate
from validation.user_validation import CreateUserValidation, UpdateUserValidation
from collections import defaultdict
# Fungsi untuk mengambil semua data pengguna
def get_users(page, limit):
connection = get_connection()
if connection is None:
raise ResponseError(500, "Error obtaining database connection")
cursor = connection.cursor(dictionary=True)
offset = (page - 1) * limit
try:
cursor.execute("SELECT nama, email, NIK, alamat, telepon, jenis_kelamin, kepala_keluarga, tempat_lahir, tanggal_lahir, jenis_usaha FROM users LIMIT %s OFFSET %s", (limit, offset))
users = cursor.fetchall()
return users
except Exception as e:
raise ResponseError(500, str(e))
finally:
if cursor:
cursor.close()
if connection:
connection.close()
# Fungsi untuk mengambil data pengguna berdasarkan ID
def get_user_by_id(id):
connection = get_connection()
if connection is None:
raise ResponseError(500, "Error obtaining database connection")
cursor = connection.cursor(dictionary=True)
try:
cursor.execute("SELECT nama, email, NIK, alamat, telepon, jenis_kelamin, kepala_keluarga, tempat_lahir, tanggal_lahir, jenis_usaha FROM users WHERE id = %s", (id,))
user = cursor.fetchone()
if not user:
raise ResponseError(404, "User not found")
return user
except ResponseError as e:
raise e
except Exception as e:
raise ResponseError(500, str(e))
finally:
if cursor:
cursor.close()
if connection:
connection.close()
# Fungsi untuk membuat pengguna baru
def create_user(req):
connection = get_connection()
if connection is None:
raise ResponseError(500, "Error obtaining database connection")
cursor = connection.cursor(dictionary=True)
try:
data = validate(CreateUserValidation, req)
nama = data.nama
email = data.email
password = bcrypt.hashpw(data.password.encode('utf-8'), bcrypt.gensalt())
NIK = data.NIK
alamat = data.alamat
telepon = data.telepon
jenis_kelamin = data.jenis_kelamin
kepala_keluarga = data.kepala_keluarga
tempat_lahir = data.tempat_lahir
tanggal_lahir = data.tanggal_lahir
jenis_usaha = data.jenis_usaha
query = """
INSERT INTO users (nama, email, password, NIK, alamat, telepon, jenis_kelamin, kepala_keluarga, tempat_lahir, tanggal_lahir, jenis_usaha)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE email = VALUES(email)
"""
cursor.execute(query, (nama, email, password, NIK, alamat, telepon, jenis_kelamin, kepala_keluarga, tempat_lahir, tanggal_lahir, jenis_usaha))
connection.commit()
if cursor.rowcount == 0:
raise ResponseError(400, "Email or NIK already exists")
return {"user_id": cursor.lastrowid}
except ResponseError as e:
raise e
except Exception as e:
raise ResponseError(500, str(e))
finally:
if cursor:
cursor.close()
if connection:
connection.close()
# Fungsi untuk mendapatkan berita yang disimpan oleh pengguna berdasarkan ID pengguna
def get_user_saved_news(id):
connection = get_connection()
if connection is None:
raise ResponseError(500, "Error obtaining database connection")
cursor = connection.cursor(dictionary=True)
try:
query = """
SELECT
users.id, users.nama, users.email,
news.id AS news_id, news.gambar, news.judul, news.subjudul, news.isi,
DATE_FORMAT(news.created_at, '%d/%m/%Y') AS formatted_created_at
FROM users
LEFT JOIN saved_news ON users.id = saved_news.user_id
LEFT JOIN news ON saved_news.news_id = news.id
WHERE users.id = %s
"""
cursor.execute(query, (id,))
rows = cursor.fetchall()
if not rows:
raise ResponseError(404, "User not found")
payload = {
'id': rows[0]['id'],
'nama': rows[0]['nama'],
'email': rows[0]['email'],
'berita_tersimpan': [
{
'id': row['news_id'],
'gambar': row['gambar'],
'judul': row['judul'],
'subjudul': row['subjudul'],
'isi': row['isi'],
'created_at': row['formatted_created_at']
}
for row in rows if row['news_id'] is not None
]
}
return payload
except ResponseError as e:
raise e
except Exception as e:
raise ResponseError(500, str(e))
finally:
if cursor:
cursor.close()
if connection:
connection.close()
# Fungsi untuk mendapatkan berita beserta komentarnya yang disimpan oleh pengguna berdasarkan ID pengguna
def get_user_saved_news_comments(id):
connection = get_connection()
if connection is None:
raise ResponseError(500, "Error obtaining database connection")
cursor = connection.cursor(dictionary=True)
try:
query = """
SELECT
users.id, users.nama, users.email, news.id AS news_id, news.gambar, news.judul, news.subjudul, news.isi, DATE_FORMAT(news.created_at, '%d/%m/%Y') AS formatted_created_at,
comments.news_id AS comment_id, comments.comment, DATE_FORMAT(comments.created_at, '%d/%m/%Y') AS comment_created_at
FROM users
LEFT JOIN saved_news ON users.id = saved_news.user_id
LEFT JOIN news ON saved_news.news_id = news.id
LEFT JOIN comments ON news.id = comments.news_id
WHERE users.id = %s
"""
cursor.execute(query, (id,))
rows = cursor.fetchall()
if not rows:
raise ResponseError(404, "User not found")
payload = {
'id': rows[0]['id'],
'nama': rows[0]['nama'],
'email': rows[0]['email'],
'news': []
}
news_map = {}
for row in rows:
if row['news_id'] not in news_map:
news_map[row['news_id']] = {
'id': row['news_id'],
'gambar': row['gambar'],
'judul': row['judul'],
'subjudul': row['subjudul'],
'isi': row['isi'],
'created_at': row['formatted_created_at'],
'comment': []
}
if row['comment_id']:
news_map[row['news_id']]['comment'].append({
'comment': row['comment'],
'created_at': row['comment_created_at']
})
payload['news'] = list(news_map.values())
return payload
except ResponseError as e:
raise e
except Exception as e:
raise ResponseError(500, str(e))
finally:
if cursor:
cursor.close()
if connection:
connection.close()
# Fungsi untuk mendapatkan fasilitas yang didapatkan oleh pengguna berdasarkan ID pengguna
def get_user_facilities(id):
connection = get_connection()
if connection is None:
raise ResponseError(500, "Error obtaining database connection")
cursor = connection.cursor(dictionary=True)
try:
query = """
SELECT
users.id, users.nama, users.email,
sertificates.id AS id_sertifikat, sertificates.nama AS nama_sertifikat, user_sertificates.no_sertifikat,
sertificates.tanggal_terbit, sertificates.kadaluarsa, sertificates.keterangan,
trainings.id AS id_pelatihan, trainings.nama AS nama_pelatihan, trainings.penyelenggara,
trainings.tanggal_pelaksanaan, trainings.tempat,
assistance.id AS id_bantuan, assistance.nama AS nama_bantuan, assistance.koordinator,
assistance.sumber_anggaran, assistance.total_anggaran, assistance.tahun_pemberian,
assistance_tools.kuantitas,
tools.id AS id_alat, tools.nama_item, tools.harga, tools.deskripsi
FROM users
LEFT JOIN user_sertificates ON users.id = user_sertificates.user_id
LEFT JOIN sertificates ON user_sertificates.sertificates_id = sertificates.id
LEFT JOIN user_trainings ON users.id = user_trainings.user_id
LEFT JOIN trainings ON user_trainings.trainings_id = trainings.id
LEFT JOIN assistance ON users.id = assistance.user_id
LEFT JOIN assistance_tools ON assistance.id = assistance_tools.assistance_id
LEFT JOIN tools ON assistance_tools.tools_id = tools.id
WHERE users.id = %s
"""
cursor.execute(query, (id,))
rows = cursor.fetchall()
if not rows:
raise ResponseError(404, "User not found")
payload = {
'id': rows[0]['id'],
'nama': rows[0]['nama'],
'email': rows[0]['email'],
'sertifikat': [],
'pelatihan': [],
'bantuan': []
}
sertifikat_map = {}
pelatihan_map = {}
bantuan_map = defaultdict(lambda: {"alat": []})
for row in rows:
if row['id_sertifikat'] and row['id_sertifikat'] not in sertifikat_map:
sertifikat_map[row['id_sertifikat']] = {
'id': row['id_sertifikat'],
'nama': row['nama_sertifikat'],
'no_sertifikat': row['no_sertifikat'],
'tanggal_terbit': row['tanggal_terbit'],
'kadaluarsa': row['kadaluarsa'],
'keterangan': row['keterangan']
}
if row['id_pelatihan'] and row['id_pelatihan'] not in pelatihan_map:
pelatihan_map[row['id_pelatihan']] = {
'id': row['id_pelatihan'],
'nama': row['nama_pelatihan'],
'penyelenggara': row['penyelenggara'],
'tanggal_pelaksanaan': row['tanggal_pelaksanaan'],
'tempat': row['tempat']
}
if row['id_bantuan']:
bantuan_item = bantuan_map[row['id_bantuan']]
bantuan_item.update({
'id': row['id_bantuan'],
'nama': row['nama_bantuan'],
'koordinator': row['koordinator'],
'sumber_anggaran': row['sumber_anggaran'],
'total_anggaran': row['total_anggaran'],
'tahun_pemberian': row['tahun_pemberian']
})
if row['id_alat'] and not any(tool['id'] == row['id_alat'] for tool in bantuan_item['alat']):
bantuan_item['alat'].append({
'id': row['id_alat'],
'nama': row['nama_item'],
'harga': row['harga'],
'kuantitas': row['kuantitas']
})
payload['sertifikat'] = list(sertifikat_map.values())
payload['pelatihan'] = list(pelatihan_map.values())
payload['bantuan'] = list(bantuan_map.values())
return payload
except ResponseError as e:
raise e
except Exception as e:
raise ResponseError(500, str(e))
finally:
if cursor:
cursor.close()
if connection:
connection.close()
# Fungsi untuk memperbarui data pengguna
def update_user(id, req):
connection = get_connection()
if connection is None:
raise ResponseError(500, "Error obtaining database connection")
cursor = connection.cursor(dictionary=True)
try:
data = validate(UpdateUserValidation, req)
data_dict = data.dict(exclude_unset=True)
if not data_dict:
raise ResponseError(400, "No valid fields to update")
updates = ", ".join(f"{key} = %s" for key in data_dict)
values = list(data_dict.values())
values.append(id)
query = f"UPDATE users SET {updates} WHERE id = %s"
cursor.execute(query, values)
connection.commit()
return {"affected_rows": cursor.rowcount}
except ResponseError as e:
raise e
except Exception as e:
raise ResponseError(500, str(e))
finally:
if cursor:
cursor.close()
if connection:
connection.close()

View File

@ -0,0 +1,6 @@
from pydantic import BaseModel, Field
class CreateAssistanceToolsValidation(BaseModel):
assistance_id: int = Field(..., description="ID assistance harus berupa angka")
tools_id: int = Field(..., description="ID tools harus berupa angka")
kuantitas: int = Field(..., description="Kuantitas harus berupa angka")

View File

@ -0,0 +1,44 @@
from pydantic import BaseModel, EmailStr, Field, field_validator
from datetime import date
class CreateUserValidation(BaseModel):
nama: str = Field(..., max_length=100, description="Nama harus berupa teks")
email: EmailStr = Field(..., description="Email tidak valid")
password: str = Field(..., min_length=6, description="Password minimal harus memiliki 6 karakter")
NIK: str = Field(..., pattern=r"^\d{16}$", description="NIK harus terdiri dari 16 karakter dan hanya angka")
alamat: str = Field(..., max_length=100, description="Alamat tidak boleh lebih dari 100 karakter")
telepon: str = Field(..., pattern=r"^\d{1,15}$", description="Nomor telepon harus terdiri dari angka maksimal 15 karakter")
jenis_kelamin: str = Field(..., pattern=r"^(L|P)$", description="Jenis kelamin harus 'L' (Laki-laki) atau 'P' (Perempuan)")
kepala_keluarga: int = Field(..., description="Kepala keluarga harus bernilai 0 atau 1")
tempat_lahir: str = Field(..., max_length=50, description="Tempat lahir tidak boleh lebih dari 50 karakter")
tanggal_lahir: date = Field(..., description="Tanggal lahir harus dalam format yang valid (YYYY-MM-DD)")
jenis_usaha: str = Field(..., max_length=50, description="Jenis usaha tidak boleh lebih dari 50 karakter")
@field_validator('kepala_keluarga')
def validate_kepala_keluarga(cls, value):
if value not in [0, 1]:
raise ValueError('Kepala keluarga harus bernilai 0 atau 1')
return value
class LoginUserValidation(BaseModel):
email: EmailStr = Field(..., description="Email tidak valid")
password: str = Field(..., min_length=6, description="Password minimal harus memiliki 6 karakter")
class UpdateUserValidation(BaseModel):
nama: str = Field(None, max_length=100, description="Nama harus berupa teks dan maksimal 100 karakter")
email: EmailStr = Field(None, description="Email tidak valid")
password: str = Field(None, min_length=6, description="Password minimal harus memiliki 6 karakter")
NIK: str = Field(None, pattern=r"^\d{16}$", description="NIK harus terdiri dari 16 karakter dan hanya angka")
alamat: str = Field(None, max_length=100, description="Alamat tidak boleh lebih dari 100 karakter")
telepon: str = Field(None, pattern=r"^\d{1,15}$", description="Nomor telepon harus terdiri dari angka maksimal 15 karakter")
jenis_kelamin: str = Field(None, pattern=r"^(L|P)$", description="Jenis kelamin harus 'L' (Laki-laki) atau 'P' (Perempuan)")
kepala_keluarga: int = Field(None, description="Kepala keluarga harus bernilai 0 atau 1")
tempat_lahir: str = Field(None, max_length=50, description="Tempat lahir tidak boleh lebih dari 50 karakter")
tanggal_lahir: date = Field(None, description="Tanggal lahir harus dalam format yang valid (YYYY-MM-DD)")
jenis_usaha: str = Field(None, max_length=50, description="Jenis usaha tidak boleh lebih dari 50 karakter")
@field_validator('kepala_keluarga')
def validate_kepala_keluarga(cls, value):
if value is not None and value not in [0, 1]:
raise ValueError('Kepala keluarga harus bernilai 0 atau 1')
return value

View File

@ -0,0 +1,15 @@
from pydantic import ValidationError
from response.response_error import ResponseError
# Fungsi untuk melakukan validasi data menggunakan schema_class
def validate(schema_class, data):
try:
# Mencoba untuk memvalidasi data sesuai dengan schema_class
return schema_class(**data)
except ValidationError as e:
# Jika terjadi error validasi, mengambil pesan error dari setiap masalah yang ditemukan
for error in e.errors():
message = error['msg']
# Menaikkan ResponseError jika terjadi error validasi
raise ResponseError(400, message=message)

25
gin/.dockerignore Normal file
View File

@ -0,0 +1,25 @@
# Binaries
bin/
*.exe
*.dll
*.so
*.dylib
# Environment variables
.env
# Build artifacts
*.out
*.a
# Caches
.cache/
tmp/
# Dependency directories (jika tidak menggunakan vendor)
vendor/
# IDE/editor-specific directories
.idea/
.vscode/
.DS_Store

28
gin/.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# Binaries
bin/
*.exe
*.dll
*.so
*.dylib
# Environment variables
.env
# Build artifacts
*.out
*.a
# Caches
.cache/
tmp/
# Dependency directories
vendor/
# Go module files (if using Go modules)
go.sum
# IDE/editor-specific directories
.idea/
.vscode/
.DS_Store

23
gin/Dockerfile Normal file
View File

@ -0,0 +1,23 @@
# Menggunakan Go 1.22 Alpine
FROM golang:1.22-alpine
# Menetapkan direktori kerja
WORKDIR /app
# Menyalin go.mod dan go.sum untuk caching dependensi
COPY go.mod go.sum ./
# Mengunduh dependensi
RUN go mod download
# Menyalin seluruh source code ke dalam container
COPY . .
# Membuild binary aplikasi
RUN go build -o app src/main.go
# Mengekspos port aplikasi
EXPOSE 8080
# Perintah untuk menjalankan binary aplikasi
CMD ["./app"]

40
gin/go.mod Normal file
View File

@ -0,0 +1,40 @@
module gin-project
go 1.22.5
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/bytedance/sonic v1.12.3 // indirect
github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.10.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.22.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/arch v0.11.0 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

8
gin/readme.md Normal file
View File

@ -0,0 +1,8 @@
import logger first
application.InitLogger()
application.Logger.Info("ping logger")
application.SyncLogger()
CompileDaemon
CompileDaemon --build="go build -o ./bin/main ./src" --command="./bin/main"

View File

@ -0,0 +1,44 @@
package application
import (
"database/sql"
"fmt"
"os"
_ "github.com/go-sql-driver/mysql"
)
var DB *sql.DB
func InitDB() {
// Memuat file .env
// envErr := godotenv.Load()
// if envErr != nil {
// panic("Error loading .env file")
// }
dbHost := os.Getenv("DB_HOST")
dbPort := os.Getenv("DB_PORT")
dbUser := os.Getenv("DB_USER")
dbPassword := os.Getenv("DB_PASSWORD")
dbName := os.Getenv("DB_NAME")
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", dbUser, dbPassword, dbHost, dbPort, dbName)
var err error
DB, err = sql.Open("mysql", dsn)
if err != nil {
panic(err)
}
DB.SetMaxOpenConns(50)
DB.SetMaxIdleConns(20)
DB.SetConnMaxLifetime(0)
err = DB.Ping()
if err != nil {
panic(err)
}
fmt.Println("Database connected!")
}

View File

@ -0,0 +1,36 @@
package application
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var Logger *zap.Logger
// InitLogger menginisialisasi logger dengan konfigurasi tertentu
func InitLogger() {
config := zap.Config{
Level: zap.NewAtomicLevelAt(zapcore.InfoLevel),
Encoding: "json",
OutputPaths: []string{"stdout"},
EncoderConfig: zapcore.EncoderConfig{
TimeKey: "time",
LevelKey: "level",
MessageKey: "msg",
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.RFC3339TimeEncoder,
EncodeDuration: zapcore.StringDurationEncoder,
},
}
var err error
Logger, err = config.Build()
if err != nil {
panic(err)
}
}
// SyncLogger melakukan sinkronisasi dan memastikan log dibersihkan dengan baik
func SyncLogger() {
defer Logger.Sync()
}

View File

@ -0,0 +1,25 @@
package application
import (
"database/sql"
"github.com/gin-gonic/gin"
"gin-project/src/middleware"
"gin-project/src/routes"
)
// SetupRouter menginisialisasi router dan mengonfigurasi middleware serta routing
func SetupRouter(db *sql.DB) *gin.Engine {
router := gin.Default()
router.Use(gin.Recovery())
router.Use(gin.Logger())
routes.PublicRoutes(router, db)
routes.PrivateRoutes(router, db)
router.Use(middleware.ErrorMiddleware())
return router
}

View File

@ -0,0 +1,56 @@
package controller
import (
"database/sql"
"gin-project/src/model"
"gin-project/src/response"
"gin-project/src/service"
"gin-project/src/validation"
"net/http"
"github.com/gin-gonic/gin"
)
// GetAssistanceTools meng-handle permintaan untuk mendapatkan data alat bantuan berdasarkan ID yang diberikan.
func GetAssistanceTools(c *gin.Context, db *sql.DB) {
id := c.Param("id")
assistanceTools, err := service.GetAssistanceTools(id, db)
if err != nil {
if responseErr, ok := err.(*response.ResponseError); ok {
c.JSON(responseErr.Status, gin.H{"error": responseErr.Error()})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
}
return
}
response.Response(200, assistanceTools, "success get assistance tools", c)
}
// CreateAssistanceTools meng-handle permintaan untuk membuat data alat bantuan baru, setelah melakukan validasi dan pengolahan data yang diberikan.
func CreateAssistanceTools(c *gin.Context, db *sql.DB) {
var request model.CreateAssistanceToolsRequest
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid request body"})
return
}
if err := validation.ValidateCreateAssistanceTools(request); err != nil {
if responseErr, ok := err.(*response.ResponseError); ok {
c.JSON(responseErr.Status, gin.H{"error": responseErr.Error()})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
}
return
}
err := service.CreateAssistanceTools(request, db)
if err != nil {
if responseErr, ok := err.(*response.ResponseError); ok {
c.JSON(responseErr.Status, gin.H{"error": responseErr.Error()})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
}
return
}
response.Response(200, nil, "success create assistance tools", c)
}

View File

@ -0,0 +1,56 @@
package controller
import (
"database/sql"
"gin-project/src/model"
"gin-project/src/response"
"gin-project/src/service"
"gin-project/src/validation"
"net/http"
"github.com/gin-gonic/gin"
)
// Login meng-handle permintaan login pengguna, memvalidasi data login, dan mengembalikan token jika login berhasil.
func Login(c *gin.Context, db *sql.DB) {
var request model.LoginUserRequest
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid request body"})
return
}
if err := validation.ValidateLogin(request); err != nil {
if responseErr, ok := err.(*response.ResponseError); ok {
c.JSON(responseErr.Status, gin.H{"error": responseErr.Error()})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
}
return
}
token, err := service.Login(request, db)
if err != nil {
if responseErr, ok := err.(*response.ResponseError); ok {
c.JSON(responseErr.Status, gin.H{"error": responseErr.Error()})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
}
return
}
response.Response(200, gin.H{"token": token}, "success login", c)
}
// Logout meng-handle permintaan logout pengguna, menghapus token yang digunakan, dan memberikan respons logout yang sukses.
func Logout(c *gin.Context, db *sql.DB) {
token := c.GetHeader("Authorization")
err := service.Logout(token, db)
if err != nil {
if responseErr, ok := err.(*response.ResponseError); ok {
c.JSON(responseErr.Status, gin.H{"error": responseErr.Error()})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
}
return
}
response.Response(200, nil, "success logout", c)
}

View File

@ -0,0 +1,25 @@
package controller
import (
"database/sql"
"gin-project/src/response"
"gin-project/src/service"
"net/http"
"github.com/gin-gonic/gin"
)
// GetNewsComments meng-handle permintaan untuk mendapatkan komentar-komentar pada berita berdasarkan ID berita yang diberikan.
func GetNewsComments(c *gin.Context, db *sql.DB) {
id := c.Param("id")
news, err := service.GetNewsComments(id, db)
if err != nil {
if responseErr, ok := err.(*response.ResponseError); ok {
c.JSON(responseErr.Status, gin.H{"error": responseErr.Error()})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
}
return
}
response.Response(200, news, "success get news comment", c)
}

View File

@ -0,0 +1,162 @@
package controller
import (
"database/sql"
"gin-project/src/model"
"gin-project/src/response"
"gin-project/src/service"
"gin-project/src/validation"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
// GetUsers meng-handle permintaan untuk mendapatkan daftar semua pengguna dari database.
func GetUsers(c *gin.Context, db *sql.DB) {
pageStr := c.DefaultQuery("page", "1")
limitStr := c.DefaultQuery("limit", "100")
page, err := strconv.Atoi(pageStr)
if err != nil {
page = 1
}
limit, err := strconv.Atoi(limitStr)
if err != nil {
limit = 100
}
users, err := service.GetUsers(db, page, limit)
if err != nil {
if responseErr, ok := err.(*response.ResponseError); ok {
c.JSON(responseErr.Status, gin.H{"error": responseErr.Error()})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
}
return
}
response.Response(200, users, "success get users", c)
}
// GetUserById meng-handle permintaan untuk mendapatkan data pengguna berdasarkan ID pengguna.
func GetUserById(c *gin.Context, db *sql.DB) {
id := c.Param("id")
user, err := service.GetUserById(id, db)
if err != nil {
if responseErr, ok := err.(*response.ResponseError); ok {
c.JSON(responseErr.Status, gin.H{"error": responseErr.Error()})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
}
return
}
response.Response(200, user, "success get user by id", c)
}
// CreateUser meng-handle permintaan untuk membuat pengguna baru dengan validasi input dan proses pembuatan.
func CreateUser(c *gin.Context, db *sql.DB) {
var request model.CreateUserRequest
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
if err := validation.ValidateCreateUser(request); err != nil {
if responseErr, ok := err.(*response.ResponseError); ok {
c.JSON(responseErr.Status, gin.H{"error": responseErr.Error()})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
}
return
}
err := service.CreateUser(request, db)
if err != nil {
if responseErr, ok := err.(*response.ResponseError); ok {
c.JSON(responseErr.Status, gin.H{"error": responseErr.Error()})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
}
return
}
response.Response(200, nil, "User created successfully", c)
}
// UpdateUser meng-handle permintaan untuk memperbarui data pengguna berdasarkan ID pengguna.
func UpdateUser(c *gin.Context, db *sql.DB) {
id := c.Param("id")
var request model.UpdateUserRequest
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
if err := validation.ValidateUpdateUser(request); err != nil {
if responseErr, ok := err.(*response.ResponseError); ok {
c.JSON(responseErr.Status, gin.H{"error": responseErr.Error()})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
}
return
}
err := service.UpdateUser(id, request, db)
if err != nil {
if responseErr, ok := err.(*response.ResponseError); ok {
c.JSON(responseErr.Status, gin.H{"error": responseErr.Error()})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
}
return
}
response.Response(200, nil, "success update user", c)
}
// GetSavedNews meng-handle permintaan untuk mendapatkan berita yang disimpan oleh pengguna berdasarkan ID pengguna.
func GetSavedNews(c *gin.Context, db *sql.DB) {
id := c.Param("id")
savedNews, err := service.GetSavedNews(id, db)
if err != nil {
if responseErr, ok := err.(*response.ResponseError); ok {
c.JSON(responseErr.Status, gin.H{"error": responseErr.Error()})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
}
return
}
response.Response(200, savedNews, "success get saved news", c)
}
// GetUserSavedNewsComment meng-handle permintaan untuk mendapatkan komentar pada berita yang disimpan oleh pengguna.
func GetUserSavedNewsComment(c *gin.Context, db *sql.DB) {
id := c.Param("id")
savedNews, err := service.GetUserSavedNewsComment(id, db)
if err != nil {
if responseErr, ok := err.(*response.ResponseError); ok {
c.JSON(responseErr.Status, gin.H{"error": responseErr.Error()})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
}
return
}
response.Response(200, savedNews, "success get saved news comment", c)
}
// GetUserFacilities meng-handle permintaan untuk mendapatkan fasilitas yang dimiliki oleh pengguna berdasarkan ID pengguna.
func GetUserFacilities(c *gin.Context, db *sql.DB) {
id := c.Param("id")
facilities, err := service.GetUserFacilities(id, db)
if err != nil {
if responseErr, ok := err.(*response.ResponseError); ok {
c.JSON(responseErr.Status, gin.H{"error": responseErr.Error()})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
}
return
}
response.Response(200, facilities, "success get facilities", c)
}

18
gin/src/main.go Normal file
View File

@ -0,0 +1,18 @@
package main
import (
"fmt"
"gin-project/src/application"
"gin-project/src/validation"
)
func main() {
application.InitDB()
validation.InitValidator()
port := 8080
router := application.SetupRouter(application.DB)
application.InitLogger()
application.Logger.Info("App started and running on port " + fmt.Sprintf("%d", port))
application.SyncLogger()
router.Run(fmt.Sprintf(":%d", port))
}

View File

@ -0,0 +1,55 @@
package middleware
import (
"database/sql"
"gin-project/src/model"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
)
// AuthMiddleware adalah middleware yang digunakan untuk memverifikasi token JWT yang dikirim dalam header Authorization.
func AuthMiddleware(db *sql.DB) gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := c.GetHeader("Authorization")
if tokenString == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
jwtSecret := os.Getenv("JWT_SECRET")
token, err := jwt.ParseWithClaims(tokenString, &model.Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(jwtSecret), nil
})
if err != nil || !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
claims, ok := token.Claims.(*model.Claims)
if !ok {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
query := "SELECT token FROM sessions WHERE token = ?"
var sessionToken string
err = db.QueryRow(query, tokenString).Scan(&sessionToken)
if err != nil {
if err == sql.ErrNoRows {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
return
}
c.Set("user", claims)
c.Next()
}
}

View File

@ -0,0 +1,29 @@
package middleware
import (
"gin-project/src/response"
"net/http"
"github.com/gin-gonic/gin"
)
// ErrorMiddleware adalah middleware yang menangani error di dalam konteks Gin.
func ErrorMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
errs := c.Errors.Last()
if errs != nil {
if responseError, ok := errs.Err.(*response.ResponseError); ok {
c.JSON(responseError.Status, gin.H{
"errors": responseError.Message,
})
} else {
c.JSON(http.StatusInternalServerError, gin.H{
"errors": errs.Error(),
})
}
c.Abort()
}
}
}

View File

@ -0,0 +1,11 @@
package model
import "github.com/golang-jwt/jwt/v4"
type Claims struct {
Id int `json:"id"`
Email string `json:"email"`
Nama string `json:"nama"`
Role int `json:"role"`
jwt.StandardClaims
}

View File

@ -0,0 +1,36 @@
package model
type Sertifikat struct {
Id NullString `json:"id"`
Name NullString `json:"nama"`
Tanggal_terbit NullTime `json:"tanggal_terbit"`
Kadaluarsa NullTime `json:"kadaluarsa"`
Keterangan NullString `json:"keterangan"`
No_sertifikat NullString `json:"no_sertifikat"`
}
type Pelatihan struct {
Id NullString `json:"id"`
Name NullString `json:"name"`
Penyelenggara NullString `json:"penyelenggara"`
Tanggal_pelaksanaan NullTime `json:"tanggal_pelaksanaan"`
Tempat NullString `json:"tempat"`
}
type Bantuan struct {
Id NullString `json:"id"`
Name NullString `json:"name"`
Koordinator NullString `json:"koordinator"`
Sumber_anggaran NullString `json:"sumber_anggaran"`
Tahun_pemberian NullTime `json:"tahun_pemberian"`
Total_anggaran NullFloat64 `json:"total_anggaran"`
Alat []Alat `json:"alat"`
}
type Alat struct {
Id NullString `json:"id"`
Name NullString `json:"name"`
Harga NullFloat64 `json:"harga"`
Deskripsi NullString `json:"deskripsi"`
Kuantitas NullInt64 `json:"kuantitas"`
}

View File

@ -0,0 +1,26 @@
package model
type News struct {
NewsId string `json:"news_id"`
Gambar NullString `json:"gambar"`
Judul string `json:"judul"`
Subjudul string `json:"sub_judul"`
Isi string `json:"isi"`
CreatedAt string `json:"created_at"`
}
type NewsComments struct {
NewsId string `json:"news_id"`
Gambar NullString `json:"gambar"`
Judul string `json:"judul"`
Subjudul string `json:"sub_judul"`
Isi string `json:"isi"`
CreatedAt string `json:"created_at"`
Comments []Comments `json:"comments"`
}
type Comments struct {
Comment string `json:"comment"`
User string `json:"user"`
CreatedAt string `json:"created_at"`
}

View File

@ -0,0 +1,93 @@
package model
import (
"database/sql"
"encoding/json"
"time"
"github.com/go-sql-driver/mysql"
)
// NullInt64 adalah tipe pembungkus untuk sql.NullInt64 agar dapat digunakan dalam JSON.
type NullInt64 sql.NullInt64
// Scan untuk NullInt64 mengimplementasikan interface sql.Scanner.
func (ni *NullInt64) Scan(value interface{}) error {
var i sql.NullInt64
if err := i.Scan(value); err != nil {
return err
}
*ni = NullInt64(i)
return nil
}
// MarshalJSON untuk NullInt64 mengubah nilai menjadi JSON.
func (ni NullInt64) MarshalJSON() ([]byte, error) {
if !ni.Valid {
return json.Marshal(nil) // Nilai tidak valid, kembalikan nil
}
return json.Marshal(ni.Int64) // Kembalikan nilai int64 sebagai JSON
}
// NullFloat64 adalah tipe pembungkus untuk sql.NullFloat64 agar dapat digunakan dalam JSON.
type NullFloat64 sql.NullFloat64
// Scan untuk NullFloat64 mengimplementasikan interface sql.Scanner.
func (nf *NullFloat64) Scan(value interface{}) error {
var f sql.NullFloat64
if err := f.Scan(value); err != nil {
return err
}
*nf = NullFloat64(f)
return nil
}
// MarshalJSON untuk NullFloat64 mengubah nilai menjadi JSON.
func (nf NullFloat64) MarshalJSON() ([]byte, error) {
if !nf.Valid {
return json.Marshal(nil) // Nilai tidak valid, kembalikan nil
}
return json.Marshal(nf.Float64) // Kembalikan nilai float64 sebagai JSON
}
// NullString adalah tipe pembungkus untuk sql.NullString agar dapat digunakan dalam JSON.
type NullString sql.NullString
// Scan untuk NullString mengimplementasikan interface sql.Scanner.
func (ns *NullString) Scan(value interface{}) error {
var s sql.NullString
if err := s.Scan(value); err != nil {
return err
}
*ns = NullString(s)
return nil
}
// MarshalJSON untuk NullString mengubah nilai menjadi JSON.
func (ns NullString) MarshalJSON() ([]byte, error) {
if !ns.Valid {
return json.Marshal(nil) // Nilai tidak valid, kembalikan nil
}
return json.Marshal(ns.String) // Kembalikan nilai string sebagai JSON
}
// NullTime adalah tipe pembungkus untuk mysql.NullTime agar dapat digunakan dalam JSON.
type NullTime mysql.NullTime
// Scan untuk NullTime mengimplementasikan interface sql.Scanner.
func (nt *NullTime) Scan(value interface{}) error {
var t mysql.NullTime
if err := t.Scan(value); err != nil {
return err
}
*nt = NullTime(t)
return nil
}
// MarshalJSON untuk NullTime mengubah nilai waktu menjadi JSON dengan format RFC3339.
func (nt NullTime) MarshalJSON() ([]byte, error) {
if !nt.Valid {
return json.Marshal(nil) // Nilai tidak valid, kembalikan nil
}
return json.Marshal(nt.Time.Format(time.RFC3339)) // Kembalikan waktu dalam format RFC3339
}

View File

@ -0,0 +1,34 @@
package model
type User struct {
Name string `json:"name"`
Email string `json:"email"`
NIK string `json:"nik"`
Alamat string `json:"alamat"`
Telepon string `json:"telepon"`
Jenis_kelamin string `json:"jenis_kelamin"`
Kepala_keluarga string `json:"kepala_keluarga"`
Tempat_lahir string `json:"tempat_lahir"`
Tanggal_lahir string `json:"tanggal_lahir"`
Jenis_usaha string `json:"jenis_usaha"`
}
type UserSavedNews struct {
Name string `json:"name"`
Email string `json:"email"`
News []News `json:"news"`
}
type UserSavedNewsComment struct {
Name string `json:"name"`
Email string `json:"email"`
NewsComments []NewsComments `json:"news"`
}
type UserFacilities struct {
Name string `json:"nama"`
Email string `json:"email"`
Sertifikat []Sertifikat `json:"sertifikat"`
Pelatihan []Pelatihan `json:"pelatihan"`
Bantuan []Bantuan `json:"bantuan"`
}

View File

@ -0,0 +1,40 @@
package model
type CreateUserRequest struct {
Nama string `validate:"required,max=100"`
Email string `validate:"required,email"`
Password string `validate:"required,min=6"`
NIK string `validate:"required,len=16,numeric"`
Alamat string `validate:"required,max=100"`
Telepon string `validate:"required,max=15,numeric"`
Jenis_kelamin string `validate:"required,oneof=L P"`
Kepala_keluarga int `validate:"oneof=0 1" default:"0"`
Tempat_lahir string `validate:"required,max=50"`
Tanggal_lahir string `validate:"required,datetime=2006-01-02"`
Jenis_usaha string `validate:"required,max=50"`
}
type LoginUserRequest struct {
Email string `validate:"required,email"`
Password string `validate:"required,min=6"`
}
type UpdateUserRequest struct {
Nama string `validate:"omitempty,max=100"`
Email string `validate:"omitempty,email"`
Password string `validate:"omitempty,min=6"`
NIK string `validate:"omitempty,len=16,numeric"`
Alamat string `validate:"omitempty,max=100"`
Telepon string `validate:"omitempty,max=15,numeric"`
Jenis_kelamin string `validate:"omitempty,oneof=L P"`
Kepala_keluarga int `validate:"omitempty,oneof=0 1"`
Tempat_lahir string `validate:"omitempty,max=50"`
Tanggal_lahir string `validate:"omitempty,datetime=2006-01-02"`
Jenis_usaha string `validate:"omitempty,max=50"`
}
type CreateAssistanceToolsRequest struct {
Assistance_id string `validate:"required"`
Tools_id string `validate:"required"`
Kuantitas int `validate:"required,number"`
}

View File

@ -0,0 +1,20 @@
package response
// ResponseError adalah tipe yang digunakan untuk menyimpan informasi error dengan status dan pesan.
type ResponseError struct {
Status int
Message string
}
// Error mengimplementasikan interface error untuk ResponseError, mengembalikan pesan error.
func (e *ResponseError) Error() string {
return e.Message
}
// NewResponseError adalah konstruktor untuk membuat objek ResponseError baru dengan status dan pesan.
func NewResponseError(status int, message string) *ResponseError {
return &ResponseError{
Status: status,
Message: message,
}
}

View File

@ -0,0 +1,16 @@
package response
import "github.com/gin-gonic/gin"
// Response mengirimkan response JSON dengan status, data, pesan, dan metadata ke client.
func Response(statusCode int, data interface{}, message string, c *gin.Context) {
c.JSON(statusCode, gin.H{
"payload": data,
"message": message,
"metadata": gin.H{
"prev": "",
"next": "",
"current": "",
},
})
}

55
gin/src/routes/api.go Normal file
View File

@ -0,0 +1,55 @@
package routes
import (
"database/sql"
"gin-project/src/controller"
"gin-project/src/middleware"
"github.com/gin-gonic/gin"
)
func PrivateRoutes(router *gin.Engine, db *sql.DB) {
router.Use(middleware.AuthMiddleware(db))
public := router.Group("/api")
// Users
public.GET("/users", func(c *gin.Context) {
controller.GetUsers(c, db)
})
public.GET("/users/:id", func(c *gin.Context) {
controller.GetUserById(c, db)
})
public.POST("/users", func(c *gin.Context) {
controller.CreateUser(c, db)
})
public.PUT("/users/:id", func(c *gin.Context) {
controller.UpdateUser(c, db)
})
public.GET("/users/saved-news/:id", func(c *gin.Context) {
controller.GetSavedNews(c, db)
})
public.GET("/users/saved-news/comment/:id", func(c *gin.Context) {
controller.GetUserSavedNewsComment(c, db)
})
public.GET("/users/facilities/:id", func(c *gin.Context) {
controller.GetUserFacilities(c, db)
})
// news
public.GET("/news/:id", func(c *gin.Context) {
controller.GetNewsComments(c, db)
})
// assistance
public.GET("/assistance/:id", func(c *gin.Context) {
controller.GetAssistanceTools(c, db)
})
public.POST("/assistance-tools", func(c *gin.Context) {
controller.CreateAssistanceTools(c, db)
})
// auth
public.POST("/logout", func(c *gin.Context) {
controller.Logout(c, db)
})
}

View File

@ -0,0 +1,16 @@
package routes
import (
"database/sql"
"gin-project/src/controller"
"github.com/gin-gonic/gin"
)
func PublicRoutes(router *gin.Engine, db *sql.DB) {
public := router.Group("/api")
public.POST("/login", func(c *gin.Context) {
controller.Login(c, db)
})
}

View File

@ -0,0 +1,76 @@
package service
import (
"database/sql"
"gin-project/src/model"
"gin-project/src/response"
)
// GetAssistanceTools mengambil data bantuan beserta alat yang terkait berdasarkan ID bantuan
func GetAssistanceTools(id string, db *sql.DB) (model.Bantuan, error) {
var bantuan model.Bantuan
query := `SELECT
assistance.id, assistance.nama, assistance.koordinator,
assistance.sumber_anggaran, assistance.total_anggaran,
assistance.tahun_pemberian,
assistance_tools.kuantitas,
tools.id, tools.nama_item, tools.harga, tools.deskripsi
FROM assistance
LEFT JOIN assistance_tools ON assistance.id = assistance_tools.assistance_id
LEFT JOIN tools ON assistance_tools.tools_id = tools.id
WHERE assistance.id = ?`
stmt, err := db.Prepare(query)
if err != nil {
return bantuan, response.NewResponseError(400, err.Error())
}
defer stmt.Close()
rows, err := stmt.Query(id)
if err != nil {
return bantuan, response.NewResponseError(400, err.Error())
}
defer rows.Close()
for rows.Next() {
var alat model.Alat
if err := rows.Scan(
&bantuan.Id,
&bantuan.Name,
&bantuan.Koordinator,
&bantuan.Sumber_anggaran,
&bantuan.Total_anggaran,
&bantuan.Tahun_pemberian,
&alat.Kuantitas,
&alat.Id,
&alat.Name,
&alat.Harga,
&alat.Deskripsi,
); err != nil {
return bantuan, response.NewResponseError(400, err.Error())
}
if alat.Id.Valid {
bantuan.Alat = append(bantuan.Alat, alat)
}
}
return bantuan, nil
}
// CreateAssistanceTools untuk membuat relasi antara bantuan dan alat dalam database
func CreateAssistanceTools(request model.CreateAssistanceToolsRequest, db *sql.DB) error {
query := `INSERT INTO assistance_tools (assistance_id, tools_id, kuantitas) VALUES (?, ?, ?)`
stmt, err := db.Prepare(query)
if err != nil {
return response.NewResponseError(500, err.Error())
}
defer stmt.Close()
_, err = stmt.Exec(request.Assistance_id, request.Tools_id, request.Kuantitas)
if err != nil {
return response.NewResponseError(400, err.Error())
}
return nil
}

View File

@ -0,0 +1,96 @@
package service
import (
"database/sql"
"gin-project/src/model"
"gin-project/src/response"
"os"
"time"
"github.com/golang-jwt/jwt/v4"
"golang.org/x/crypto/bcrypt"
)
// Login melakukan proses autentikasi pengguna berdasarkan email dan password
func Login(request model.LoginUserRequest, db *sql.DB) (string, error) {
var id, role int
var hashedPassword, nama, email string
query := "SELECT id, email, nama, role_id, password FROM users WHERE email = ?"
stmt, err := db.Prepare(query)
if err != nil {
return "", response.NewResponseError(500, err.Error())
}
defer stmt.Close()
err = stmt.QueryRow(request.Email).Scan(&id, &email, &nama, &role, &hashedPassword)
if err != nil {
if err == sql.ErrNoRows {
return "", response.NewResponseError(400, "Email or password is incorrect")
}
return "", response.NewResponseError(500, err.Error())
}
err = bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(request.Password))
if err != nil {
return "", response.NewResponseError(400, "Email or password is incorrect")
}
expirationTime := time.Now().Add(2 * time.Hour)
claims := &model.Claims{
Id: id,
Email: email,
Nama: nama,
Role: role,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
secret := os.Getenv("JWT_SECRET")
tokenString, err := token.SignedString([]byte(secret))
if err != nil {
return "", response.NewResponseError(500, err.Error())
}
insertQuery := `INSERT INTO sessions (token, user_id, expiry, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`
stmtInsert, err := db.Prepare(insertQuery)
if err != nil {
return "", response.NewResponseError(500, err.Error())
}
defer stmtInsert.Close()
_, err = stmtInsert.Exec(tokenString, id, expirationTime, time.Now(), time.Now())
if err != nil {
return "", response.NewResponseError(500, err.Error())
}
return tokenString, nil
}
// Logout menghapus sesi pengguna berdasarkan token yang diberikan
func Logout(token string, db *sql.DB) error {
query := `DELETE FROM sessions WHERE token = ?`
stmt, err := db.Prepare(query)
if err != nil {
return response.NewResponseError(500, err.Error())
}
defer stmt.Close()
result, err := stmt.Exec(token)
if err != nil {
return response.NewResponseError(400, err.Error())
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return response.NewResponseError(500, err.Error())
}
if rowsAffected == 0 {
return response.NewResponseError(404, "Token not found")
}
return nil
}

View File

@ -0,0 +1,63 @@
package service
import (
"database/sql"
"gin-project/src/model"
"gin-project/src/response"
)
// GetNewsComments mengambil komentar-komentar terkait sebuah berita berdasarkan id berita
func GetNewsComments(id string, db *sql.DB) (model.NewsComments, error) {
var newsComments model.NewsComments
// Query untuk mendapatkan berita beserta komentar yang terkait
query := `SELECT
news.id, news.gambar, news.judul, news.subjudul, news.isi, news.created_at,
comments.user_id AS user_id, comments.comment, comments.created_at AS comment_created_at
FROM news
LEFT JOIN comments ON news.id = comments.news_id
WHERE news.id = ?`
// Menyiapkan statement SQL
stmt, err := db.Prepare(query)
if err != nil {
// Mengembalikan error jika terjadi kesalahan saat menyiapkan query
return newsComments, response.NewResponseError(400, err.Error())
}
defer stmt.Close()
// Mengeksekusi query dengan id berita
rows, err := stmt.Query(id)
if err != nil {
// Mengembalikan error jika terjadi kesalahan saat menjalankan query
return newsComments, response.NewResponseError(400, err.Error())
}
defer rows.Close()
// Menyimpan hasil query ke dalam newsComments
for rows.Next() {
var comments model.Comments
// Memindahkan hasil query ke dalam struktur data newsComments dan comments
if err := rows.Scan(
&newsComments.NewsId,
&newsComments.Gambar,
&newsComments.Judul,
&newsComments.Subjudul,
&newsComments.Isi,
&newsComments.CreatedAt,
&comments.User,
&comments.Comment,
&comments.CreatedAt,
); err != nil {
// Mengembalikan error jika terjadi kesalahan saat memindahkan data
return newsComments, response.NewResponseError(400, err.Error())
}
// Menambahkan komentar ke dalam daftar komentar berita
newsComments.Comments = append(newsComments.Comments, comments)
}
// Mengembalikan data berita beserta komentar yang terkait
return newsComments, nil
}

View File

@ -0,0 +1,348 @@
package service
import (
"database/sql"
"fmt"
"gin-project/src/model"
"gin-project/src/response"
"reflect"
"strings"
"golang.org/x/crypto/bcrypt"
)
// GetUsers mengambil semua pengguna dari database
func GetUsers(db *sql.DB, page int, limit int) ([]model.User, error) {
var users []model.User
offset := (page - 1) * limit
query := "SELECT nama, email, NIK, alamat, telepon, jenis_kelamin, kepala_keluarga, tempat_lahir, tanggal_lahir, jenis_usaha FROM users LIMIT ? OFFSET ?"
stmt, err := db.Prepare(query)
if err != nil {
return users, response.NewResponseError(500, "Failed to prepare statement")
}
defer stmt.Close()
rows, err := stmt.Query(limit, offset)
if err != nil {
return users, response.NewResponseError(400, err.Error())
}
defer rows.Close()
for rows.Next() {
var user model.User
if err := rows.Scan(&user.Name, &user.Email, &user.NIK, &user.Alamat, &user.Telepon, &user.Jenis_kelamin, &user.Kepala_keluarga, &user.Tempat_lahir, &user.Tanggal_lahir, &user.Jenis_usaha); err != nil {
return users, response.NewResponseError(400, err.Error())
}
users = append(users, user)
}
return users, nil
}
// GetUserById mengambil data pengguna berdasarkan id
func GetUserById(id string, db *sql.DB) (model.User, error) {
var user model.User
query := "SELECT nama, email, NIK, alamat, telepon, jenis_kelamin, kepala_keluarga, tempat_lahir, tanggal_lahir, jenis_usaha FROM users WHERE id = ?"
stmt, err := db.Prepare(query)
if err != nil {
return user, response.NewResponseError(500, "Failed to prepare statement")
}
defer stmt.Close()
err = stmt.QueryRow(id).Scan(&user.Name, &user.Email, &user.NIK, &user.Alamat, &user.Telepon, &user.Jenis_kelamin, &user.Kepala_keluarga, &user.Tempat_lahir, &user.Tanggal_lahir, &user.Jenis_usaha)
if err != nil {
if err == sql.ErrNoRows {
return user, response.NewResponseError(404, "User not found")
}
return user, response.NewResponseError(400, err.Error())
}
return user, nil
}
// CreateUser membuat pengguna baru di database
func CreateUser(request model.CreateUserRequest, db *sql.DB) error {
newPassword, _ := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost)
stmt, err := db.Prepare("INSERT INTO users (nama, email, password, NIK, alamat, telepon, jenis_kelamin, kepala_keluarga, tempat_lahir, tanggal_lahir, jenis_usaha) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
if err != nil {
return response.NewResponseError(500, err.Error())
}
defer stmt.Close()
_, err = stmt.Exec(
request.Nama,
request.Email,
newPassword,
request.NIK,
request.Alamat,
request.Telepon,
request.Jenis_kelamin,
request.Kepala_keluarga,
request.Tempat_lahir,
request.Tanggal_lahir,
request.Jenis_usaha,
)
if err != nil {
return response.NewResponseError(400, err.Error())
}
return nil
}
func UpdateUser(id string, request model.UpdateUserRequest, db *sql.DB) error {
updates := []string{}
values := []interface{}{}
val := reflect.ValueOf(request)
typ := reflect.TypeOf(request)
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
value := val.Field(i).Interface()
jsonTag := field.Tag.Get("json")
if jsonTag == "" {
jsonTag = field.Name
}
if !isZero(value) {
updates = append(updates, fmt.Sprintf("%s = ?", jsonTag))
values = append(values, value)
}
}
if len(updates) == 0 {
return response.NewResponseError(400, "No valid fields to update")
}
query := fmt.Sprintf("UPDATE users SET %s WHERE id = ?", strings.Join(updates, ", "))
values = append(values, id)
stmt, err := db.Prepare(query)
if err != nil {
return response.NewResponseError(500, "Failed to prepare statement: "+err.Error())
}
defer stmt.Close()
_, err = stmt.Exec(values...)
if err != nil {
return response.NewResponseError(400, "Failed to execute update query: "+err.Error())
}
return nil
}
// isZero memeriksa apakah nilai dari field adalah nilai default (kosong)
func isZero(value interface{}) bool {
return reflect.DeepEqual(value, reflect.Zero(reflect.TypeOf(value)).Interface())
}
// GetSavedNews mengambil berita yang disimpan oleh pengguna berdasarkan id pengguna
func GetSavedNews(id string, db *sql.DB) (model.UserSavedNews, error) {
var userSavedNews model.UserSavedNews
var newsList []model.News
query := `SELECT
u.nama, u.email, n.id, n.gambar, n.judul, n.subjudul, n.isi, n.created_at
FROM users u
INNER JOIN saved_news sn ON u.id = sn.user_id
INNER JOIN news n ON sn.news_id = n.id
WHERE u.id = ?`
stmt, err := db.Prepare(query)
if err != nil {
return userSavedNews, response.NewResponseError(500, "Failed to prepare statement")
}
defer stmt.Close()
rows, err := stmt.Query(id)
if err != nil {
return userSavedNews, response.NewResponseError(400, err.Error())
}
defer rows.Close()
for rows.Next() {
var news model.News
if err := rows.Scan(&userSavedNews.Name, &userSavedNews.Email, &news.NewsId, &news.Gambar, &news.Judul, &news.Subjudul, &news.Isi, &news.CreatedAt); err != nil {
return userSavedNews, response.NewResponseError(400, err.Error())
}
newsList = append(newsList, news)
}
if err := rows.Err(); err != nil {
return userSavedNews, response.NewResponseError(500, err.Error())
}
userSavedNews.News = newsList
return userSavedNews, nil
}
// GetUserSavedNewsComment mengambil berita yang disimpan dan komentar terkait untuk pengguna dari database.
func GetUserSavedNewsComment(id string, db *sql.DB) (model.UserSavedNewsComment, error) {
var userSavedNewsComment model.UserSavedNewsComment
var newsCommentsMap = make(map[string]*model.NewsComments)
query := `SELECT
users.nama, users.email, news.id, news.gambar, news.judul, news.subjudul, news.isi, news.created_at, comments.comment, comments.user_id, comments.created_at
FROM users
LEFT JOIN saved_news ON users.id = saved_news.user_id
LEFT JOIN news ON saved_news.news_id = news.id
LEFT JOIN comments ON news.id = comments.news_id
WHERE users.id = ?`
stmt, err := db.Prepare(query)
if err != nil {
return userSavedNewsComment, response.NewResponseError(400, err.Error())
}
defer stmt.Close()
rows, err := stmt.Query(id)
if err != nil {
return userSavedNewsComment, response.NewResponseError(400, err.Error())
}
defer rows.Close()
for rows.Next() {
var newsComments model.NewsComments
var comments model.Comments
if err := rows.Scan(
&userSavedNewsComment.Name,
&userSavedNewsComment.Email,
&newsComments.NewsId,
&newsComments.Gambar,
&newsComments.Judul,
&newsComments.Subjudul,
&newsComments.Isi,
&newsComments.CreatedAt,
&comments.Comment,
&comments.User,
&comments.CreatedAt,
); err != nil {
return userSavedNewsComment, response.NewResponseError(400, err.Error())
}
if existingNews, exists := newsCommentsMap[newsComments.NewsId]; exists {
existingNews.Comments = append(existingNews.Comments, comments)
} else {
newsComments.Comments = append(newsComments.Comments, comments)
newsCommentsMap[newsComments.NewsId] = &newsComments
}
}
for _, news := range newsCommentsMap {
userSavedNewsComment.NewsComments = append(userSavedNewsComment.NewsComments, *news)
}
return userSavedNewsComment, nil
}
// GetUserFacilities mengambil fasilitas pengguna (sertifikat, pelatihan, bantuan, alat) dari database.
func GetUserFacilities(id string, db *sql.DB) (model.UserFacilities, error) {
var userFacilities model.UserFacilities
helpMap := make(map[string]*model.Bantuan)
sertifikatMap := make(map[string]bool)
pelatihanMap := make(map[string]bool)
query := `
SELECT
users.email, users.nama,
sertificates.id AS id_sertifikat, sertificates.nama AS nama_sertifikat, user_sertificates.no_sertifikat, sertificates.tanggal_terbit, sertificates.kadaluarsa, sertificates.keterangan,
trainings.id AS id_pelatihan, trainings.nama AS nama_pelatihan, trainings.penyelenggara, trainings.tanggal_pelaksanaan, trainings.tempat,
assistance.id AS id_bantuan, assistance.nama AS nama_bantuan, assistance.koordinator, assistance.sumber_anggaran, assistance.total_anggaran, assistance.tahun_pemberian,
assistance_tools.kuantitas,
tools.id AS id_alat, tools.nama_item, tools.harga, tools.deskripsi
FROM users
LEFT JOIN user_sertificates ON users.id = user_sertificates.user_id
LEFT JOIN sertificates ON user_sertificates.sertificates_id = sertificates.id
LEFT JOIN user_trainings ON users.id = user_trainings.user_id
LEFT JOIN trainings ON user_trainings.trainings_id = trainings.id
LEFT JOIN assistance ON users.id = assistance.user_id
LEFT JOIN assistance_tools ON assistance.id = assistance_tools.assistance_id
LEFT JOIN tools ON assistance_tools.tools_id = tools.id
WHERE users.id = ?
`
stmt, err := db.Prepare(query)
if err != nil {
return userFacilities, response.NewResponseError(400, err.Error())
}
defer stmt.Close()
rows, err := stmt.Query(id)
if err != nil {
return userFacilities, response.NewResponseError(400, err.Error())
}
defer rows.Close()
for rows.Next() {
var sertifikat model.Sertifikat
var pelatihan model.Pelatihan
var bantuan model.Bantuan
var alat model.Alat
var kuantitas sql.NullInt64
err := rows.Scan(
&userFacilities.Email, &userFacilities.Name,
&sertifikat.Id, &sertifikat.Name, &sertifikat.No_sertifikat, &sertifikat.Tanggal_terbit, &sertifikat.Kadaluarsa, &sertifikat.Keterangan,
&pelatihan.Id, &pelatihan.Name, &pelatihan.Penyelenggara, &pelatihan.Tanggal_pelaksanaan, &pelatihan.Tempat,
&bantuan.Id, &bantuan.Name, &bantuan.Koordinator, &bantuan.Sumber_anggaran, &bantuan.Total_anggaran, &bantuan.Tahun_pemberian,
&kuantitas, &alat.Id, &alat.Name, &alat.Harga, &alat.Deskripsi,
)
if err != nil {
return userFacilities, response.NewResponseError(400, err.Error())
}
if sertifikat.Id.Valid {
if _, exists := sertifikatMap[sertifikat.Id.String]; !exists {
userFacilities.Sertifikat = append(userFacilities.Sertifikat, sertifikat)
sertifikatMap[sertifikat.Id.String] = true
}
}
if pelatihan.Id.Valid {
if _, exists := pelatihanMap[pelatihan.Id.String]; !exists {
userFacilities.Pelatihan = append(userFacilities.Pelatihan, pelatihan)
pelatihanMap[pelatihan.Id.String] = true
}
}
if bantuan.Id.Valid {
bantuanIdStr := bantuan.Id.String
if _, exists := helpMap[bantuanIdStr]; !exists {
helpMap[bantuanIdStr] = &bantuan
}
if alat.Id.Valid {
alat.Kuantitas.Valid = kuantitas.Valid
alat.Kuantitas.Int64 = kuantitas.Int64
if helpMap[bantuanIdStr].Alat == nil {
helpMap[bantuanIdStr].Alat = []model.Alat{}
}
exists := false
for _, existingAlat := range helpMap[bantuanIdStr].Alat {
if existingAlat.Id == alat.Id {
exists = true
break
}
}
if !exists {
helpMap[bantuanIdStr].Alat = append(helpMap[bantuanIdStr].Alat, alat)
}
}
}
}
for _, bantuan := range helpMap {
if bantuan.Id.Valid {
userFacilities.Bantuan = append(userFacilities.Bantuan, *bantuan)
}
}
return userFacilities, nil
}

View File

@ -0,0 +1,15 @@
package validation
import (
"gin-project/src/model"
"gin-project/src/response"
)
func ValidateCreateAssistanceTools(request model.CreateAssistanceToolsRequest) error {
err := validate.Struct(request)
if err != nil {
return response.NewResponseError(400, FormatValidationError(err))
}
return nil
}

View File

@ -0,0 +1,15 @@
package validation
import (
"gin-project/src/model"
"gin-project/src/response"
)
func ValidateLogin(request model.LoginUserRequest) error {
err := validate.Struct(request)
if err != nil {
return response.NewResponseError(400, FormatValidationError(err))
}
return nil
}

View File

@ -0,0 +1,33 @@
package validation
import (
"gin-project/src/model"
"gin-project/src/response"
)
func ValidateCreateUser(request model.CreateUserRequest) error {
err := validate.Struct(request)
if err != nil {
return response.NewResponseError(400, FormatValidationError(err))
}
return nil
}
func ValidateLoginUser(request model.LoginUserRequest) error {
err := validate.Struct(request)
if err != nil {
return response.NewResponseError(400, FormatValidationError(err))
}
return nil
}
func ValidateUpdateUser(request model.UpdateUserRequest) error {
err := validate.Struct(request)
if err != nil {
return response.NewResponseError(400, FormatValidationError(err))
}
return nil
}

View File

@ -0,0 +1,55 @@
package validation
import (
"fmt"
"strings"
"github.com/go-playground/validator/v10"
)
// Variabel untuk menyimpan instance dari validator
var validate *validator.Validate
// InitValidator menginisialisasi objek validator
func InitValidator() {
// Membuat instance validator baru jika belum ada
validate = validator.New()
}
// FormatValidationError memformat error validasi menjadi pesan yang lebih deskriptif
func FormatValidationError(err error) string {
// Menyimpan daftar pesan error
var errors []string
// Melakukan type assertion untuk mendapatkan daftar error validasi
for _, err := range err.(validator.ValidationErrors) {
var message string
// Mengecek tag validasi untuk menghasilkan pesan error yang sesuai
switch err.Tag() {
case "required":
// Pesan jika field tidak boleh kosong
message = fmt.Sprintf("%s harus diisi", err.Field())
case "email":
// Pesan jika field harus berisi email yang valid
message = fmt.Sprintf("%s harus valid email", err.Field())
case "max":
// Pesan jika nilai field melebihi batas maksimum
message = fmt.Sprintf("%s maksimal %s", err.Field(), err.Param())
case "min":
// Pesan jika nilai field kurang dari batas minimum
message = fmt.Sprintf("%s minimal %s", err.Field(), err.Param())
case "len":
// Pesan jika panjang karakter tidak sesuai
message = fmt.Sprintf("%s harus berisi %s karakter", err.Field(), err.Param())
case "number":
// Pesan jika nilai field harus berupa angka
message = fmt.Sprintf("%s harus berupa angka", err.Field())
default:
// Pesan default jika tag error tidak teridentifikasi
message = fmt.Sprintf("%s tidak valid", err.Field())
}
// Menambahkan pesan ke daftar error
errors = append(errors, message)
}
// Menggabungkan semua pesan error menjadi satu string, dipisahkan dengan koma
return strings.Join(errors, ", ")
}