first commit
This commit is contained in:
commit
03e39564c1
|
@ -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);
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,13 @@
|
|||
FROM node:23-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install --production
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "start"]
|
|
@ -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"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
|
@ -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;
|
|
@ -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({})],
|
||||
});
|
|
@ -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);
|
|
@ -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 };
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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}`);
|
||||
});
|
|
@ -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"));
|
||||
}
|
||||
};
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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,
|
||||
};
|
|
@ -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 };
|
|
@ -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,
|
||||
};
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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
|
|
@ -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/
|
|
@ -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"]
|
|
@ -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
|
|
@ -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
|
|
@ -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")
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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}")
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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}")
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
||||
|
|
@ -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()
|
|
@ -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()
|
|
@ -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")
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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"]
|
|
@ -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
|
||||
)
|
|
@ -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"
|
|
@ -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!")
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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": "",
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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, ", ")
|
||||
}
|
Loading…
Reference in New Issue