diff --git a/Client/sportify-court/.firebase/hosting.ZGlzdA.cache b/Client/sportify-court/.firebase/hosting.ZGlzdA.cache new file mode 100644 index 0000000..46b4298 --- /dev/null +++ b/Client/sportify-court/.firebase/hosting.ZGlzdA.cache @@ -0,0 +1,7 @@ +vite.svg,1752623480906,699a02e0e68a579f687d364bbbe7633161244f35af068220aee37b1b33dfb3c7 +Sportify-Courts.png,1753306514770,77a139ab083a120726a83030003dac57a951498e11e0376b6cb05efad3c59bac +Sportify-Court.png,1753103371364,bfb3d09ebb09d85b96f006a565019c2cdef15ac25ac1941c56f38186567492e3 +Sportify-Court-removebg-preview.png,1753306359665,bbe07785d291fc509d6dd62a9e3ff8adeb01e153f5d77d14f3a2e4172184774b +index.html,1753987913839,d181778e0ba6154f686aa9e5a455e1610fa85c6cc5e1c3bc2f4461cff054b45e +assets/index-C3LF-bwm.css,1753987913839,113b4aaa60317eda62802248aab1a51dfcaecb245e2e24b069a4e80c8806f337 +assets/index-DiYXdOaV.js,1753987913839,11534f3da52e12fdece89bab7e2e4d7622e7b7ca9e9a5b011edc1690ece37176 diff --git a/Client/sportify-court/.firebaserc b/Client/sportify-court/.firebaserc new file mode 100644 index 0000000..f5d62b9 --- /dev/null +++ b/Client/sportify-court/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "sportifycourts" + } +} diff --git a/Client/sportify-court/.gitignore b/Client/sportify-court/.gitignore new file mode 100644 index 0000000..3b0b403 --- /dev/null +++ b/Client/sportify-court/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.env \ No newline at end of file diff --git a/Client/sportify-court/README.md b/Client/sportify-court/README.md new file mode 100644 index 0000000..7059a96 --- /dev/null +++ b/Client/sportify-court/README.md @@ -0,0 +1,12 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/Client/sportify-court/eslint.config.js b/Client/sportify-court/eslint.config.js new file mode 100644 index 0000000..cee1e2c --- /dev/null +++ b/Client/sportify-court/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/Client/sportify-court/firebase.json b/Client/sportify-court/firebase.json new file mode 100644 index 0000000..2c33c29 --- /dev/null +++ b/Client/sportify-court/firebase.json @@ -0,0 +1,16 @@ +{ + "hosting": { + "public": "dist", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } +} diff --git a/Client/sportify-court/index.html b/Client/sportify-court/index.html new file mode 100644 index 0000000..ecf152a --- /dev/null +++ b/Client/sportify-court/index.html @@ -0,0 +1,22 @@ + + +
+ + ++ + {/* Location icon changed to a map pin */} + + + {court.location} +
+| # | +User | ++ Court + | +Date | +Time | ++ Payment + | ++ Status + | ++ Action + | +
|---|---|---|---|---|---|---|---|
| {i + 1} | +{b.User?.email} | +{b.Court?.name} | ++ {new Date(b.date).toLocaleDateString("id-ID")} + | ++ {b.timeStart} - {b.timeEnd} + | ++ {b.isPaid ? ( + + Paid + + ) : ( + + Belum Bayar + + )} + | ++ + {b.status.charAt(0).toUpperCase() + b.status.slice(1)} + + | +
+
+ {b.status === "pending" && (
+
+ )}
+ {b.status !== "cancelled" && (
+
+ )}
+ {b.status === "pending" && (
+
+ )}
+
+ |
+
+ Terima kasih telah memesan lapangan di Sportify. +
+ + Lihat Booking Saya + +{court.location}
++ Rp {court.pricePerHour?.toLocaleString()}/jam +
+{court.description}
+ +
+ Login to your account
++ Don’t have an account?{" "} + + Register here + +
+Belum ada booking.
++ 📍 + {b.Court?.location ?? "Unknown Location"} +
++ Tanggal:{" "} + {b.date ? formatDate(b.date) : "Unknown Date"} +
++ Waktu:{" "} + {b.timeStart ? formatTime(b.timeStart) : "?"} -{" "} + {b.timeEnd ? formatTime(b.timeEnd) : "?"} +
+
+ + Create your account to start booking courts +
++ Already have an account?{" "} + + Login here + +
++ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 | 5x +5x + + +5x + + +5x +5x +5x +5x +5x +5x +5x +5x + +5x + +5x +5x + + + + + + + + + + +5x + + +5x +5x + +5x +5x +5x +5x + + + + + +5x + | console.log({ env: process.env.NODE_ENV });
+Eif (process.env.NODE_ENV !== "production") {
+ // hanya dipake ketika proses development
+ // kalo production kita tidak menggunakan library dotenv -> env bawaan dari pm2 (runner)
+ require("dotenv").config();
+}
+
+const express = require("express");
+const cors = require("cors");
+const app = express();
+const authRoutes = require("./routes/auth"); // nanti kita buat
+const publicRoutes = require("./routes/public");
+const courtRoutes = require("./routes/courts"); // nanti
+const bookingRoutes = require("./routes/bookings"); // nanti
+const paymentRoutes = require("./routes/payments"); // nanti
+// const errorHandler = require("./middleware/errorHandler"); // nanti
+const aiRoutes = require("./routes/ai"); // untuk AI
+
+app.use(cors());
+app.use(express.urlencoded({ extended: false }));
+
+// ⚠️ PENTING: PASANG RAW BODY HANYA UNTUK MIDTRANS CALLBACK
+// app.use(
+// "/payments/midtrans/callback",
+// express.raw({ type: "*/*" }) // Midtrans butuh raw body
+// );
+// app.get("/", (req, res) => {
+// res.status(200).json({ message: "its my life" });
+// });
+
+app.use(express.json());
+
+// Routes
+app.use("/auth", authRoutes); // POST /auth/register, /login
+app.use("/public", publicRoutes);
+
+app.use("/courts", courtRoutes);
+app.use("/bookings", bookingRoutes);
+app.use("/payments", paymentRoutes);
+app.use("/ai", aiRoutes);
+// console.log("✅ /ai route berhasil dimount");
+// router.use(authentication); // Semua route setelah ini butuh login
+// Error handler
+// app.use(errorHandler);
+
+module.exports = app;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 | 5x + +5x + + + + + + + + + + + + + + + + + + + + + + + + + + | require("dotenv").config();
+
+module.exports = {
+ development: {
+ username: process.env.DB_USER,
+ password: process.env.DB_PASS,
+ database: process.env.DB_NAME,
+ host: process.env.DB_HOST,
+ dialect: "postgres",
+ },
+ test: {
+ username: process.env.DB_USER,
+ password: process.env.DB_PASS,
+ database: process.env.DB_NAME_TEST || "sportify_test",
+ host: process.env.DB_HOST,
+ dialect: "postgres",
+ },
+ production: {
+ use_env_variable: "DATABASE_URL",
+ dialect: "postgres",
+ dialectOptions: {
+ ssl: {
+ require: true,
+ rejectUnauthorized: false,
+ },
+ },
+ },
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| config.js | +
+
+ |
+ 100% | +2/2 | +50% | +1/2 | +100% | +0/0 | +100% | +2/2 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 | 5x +5x +5x + +5x +5x + +5x + +11x +11x + +11x +1x + + + + + + + + + +10x + + + + +11x +11x +11x +3x + +8x +8x +1x + +7x +2x + +5x +1x + + + + +4x +3x +1x + +2x +2x +1x + +1x +1x + + + + + + + + + +8x + + + + +3x +3x +3x +1x + +2x + + + +2x +2x +1x +1x + + + + + + +1x +1x + + + + + + + + + +2x + + + + | const { comparePassword } = require("../helpers/bcrypt");
+const { signToken } = require("../helpers/jwt");
+const { User } = require("../models");
+
+const { OAuth2Client } = require("google-auth-library");
+const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
+
+module.exports = class authController {
+ static async register(req, res, next) {
+ try {
+ const { name, email, password, role } = req.body;
+
+ const newUser = await User.create(req.body);
+ res.status(201).json({
+ message: "User registered successfully",
+ user: {
+ id: newUser.id,
+ name: newUser.name,
+ email: newUser.email,
+ role: newUser.role,
+ },
+ });
+ } catch (err) {
+ next(err);
+ }
+ }
+
+ static async login(req, res, next) {
+ const { email, password } = req.body;
+ try {
+ if (!email) {
+ throw { name: "BadRequest", message: "Email is required" }; // 400
+ }
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(email)) {
+ throw { name: "BadRequest", message: "Invalid email format" }; // 400
+ }
+ if (!password) {
+ throw { name: "BadRequest", message: "Password is required" }; // 400
+ }
+ if (password.length < 6) {
+ throw {
+ name: "BadRequest",
+ message: "Password must be at least 6 characters long",
+ };
+ }
+ const user = await User.findOne({ where: { email } });
+ if (!user) {
+ return res.status(404).json({ message: "User not found" });
+ }
+ const isValidPassword = comparePassword(password, user.password);
+ if (!isValidPassword) {
+ return res.status(401).json({ message: "Invalid email or password" });
+ }
+ const accessToken = signToken({ id: user.id, role: user.role });
+ res.status(200).json({
+ message: "Login Success",
+ access_token: accessToken,
+ user: {
+ id: user.id,
+ name: user.name,
+ role: user.role,
+ },
+ });
+ } catch (err) {
+ next(err);
+ }
+ }
+
+ static async googleLogin(req, res, next) {
+ const { id_token } = req.body;
+ try {
+ if (!id_token) {
+ throw { name: "BadRequest", message: "id_token is required" }; // 400
+ }
+ const ticket = await client.verifyIdToken({
+ idToken: id_token,
+ audience: process.env.GOOGLE_CLIENT_ID,
+ });
+ const { name, email } = ticket.getPayload();
+ let user = await User.findOne({ where: { email } });
+ Eif (!user) {
+ user = await User.create({
+ name,
+ email,
+ password: Math.random().toString(36).slice(-8),
+ role: "user",
+ });
+ }
+ const access_token = signToken({ id: user.id, role: user.role });
+ res.status(200).json({
+ message: "Google Login Success",
+ access_token,
+ user: {
+ id: user.id,
+ name: user.name,
+ role: user.role,
+ },
+ });
+ } catch (err) {
+ next(err);
+ }
+ }
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 | 5x + +5x + + +1x +1x + +1x + + + + + + + + + + +1x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +4x +4x +4x + + +4x +4x +1x + + + +3x + + + + + + + + + + + + + + + + + + + + + + + + +3x +1x + + + + + +2x + + + + + + +2x + +2x + + + + +1x +1x +1x +1x +1x + + +1x + + + + + + + +1x + + + + + + +1x + + + + + + +1x + + + + + + +2x +2x +2x + +2x +2x + + +1x +1x + + + +1x + +1x + +1x + + + + +2x +2x +2x +2x +1x +1x + +1x + + + + | const { Booking, Court, User, Payment } = require("../models");
+
+module.exports = class bookingController {
+ //* Buat Booking (user)
+ static async getMyBookings(req, res, next) {
+ try {
+ const userId = req.user.id;
+ // console.log("🚀 ~ getMyBookings ~ userId:", userId);
+ const bookings = await Booking.findAll({
+ where: { UserId: userId },
+ include: [
+ Court,
+ {
+ model: Payment,
+ attributes: ["status", "paidAt"],
+ required: false,
+ },
+ ],
+ });
+ res.status(200).json(bookings);
+ } catch (err) {
+ // console.log("🚀 ~ getMyBookings ~ err:", err);
+ next(err);
+ }
+ }
+ //* Ambil semua booking (admin)
+ static async getAllBookings(req, res, next) {
+ try {
+ const { status } = req.query;
+
+ const options = {
+ include: [
+ { model: Court },
+ {
+ model: User,
+ attributes: ["id", "email", "name"],
+ },
+ ],
+ order: [["createdAt", "DESC"]],
+ };
+
+ // Tambahkan filter jika ada query status
+ if (status) {
+ options.where = { status };
+ // console.log("🚀 ~ getAllBookings ~ options:", options);
+ }
+
+ const bookings = await Booking.findAll(options);
+ res.status(200).json(bookings);
+ } catch (err) {
+ next(err); // pastikan error dikirim ke error handler
+ }
+ }
+ //* Create Booking (user)
+ static async createBooking(req, res, next) {
+ try {
+ const userId = req.user.id;
+ const { CourtId, date, timeStart, timeEnd } = req.body;
+
+ // Check if court exists
+ const court = await Court.findByPk(CourtId);
+ if (!court) {
+ throw { name: "NotFound", message: "Court not found" };
+ }
+
+ // Check for overlapping bookings
+ const existingBooking = await Booking.findOne({
+ where: {
+ CourtId,
+ date,
+ status: { [require("sequelize").Op.ne]: "cancelled" }, // exclude cancelled bookings
+ [require("sequelize").Op.or]: [
+ // New booking starts during existing booking
+ {
+ timeStart: { [require("sequelize").Op.lte]: timeStart },
+ timeEnd: { [require("sequelize").Op.gt]: timeStart },
+ },
+ // New booking ends during existing booking
+ {
+ timeStart: { [require("sequelize").Op.lt]: timeEnd },
+ timeEnd: { [require("sequelize").Op.gte]: timeEnd },
+ },
+ // New booking completely contains existing booking
+ {
+ timeStart: { [require("sequelize").Op.gte]: timeStart },
+ timeEnd: { [require("sequelize").Op.lte]: timeEnd },
+ },
+ ],
+ },
+ });
+
+ if (existingBooking) {
+ throw {
+ name: "BadRequest",
+ message: "Booking time overlaps with existing booking",
+ };
+ }
+
+ const booking = await Booking.create({
+ CourtId,
+ UserId: userId,
+ date,
+ timeStart,
+ timeEnd,
+ });
+ res.status(201).json({ message: "Booking created", booking });
+ } catch (err) {
+ next(err);
+ }
+ }
+ //* Update Booking (user can update their own, admin can update any)
+ static async updateBooking(req, res, next) {
+ try {
+ const { id } = req.params;
+ const { date, timeStart, timeEnd, status } = req.body;
+ const booking = await Booking.findByPk(id);
+ Iif (!booking) throw { name: "NotFound", message: "Booking not found" };
+
+ // Check if user owns this booking (unless admin)
+ Iif (req.user.role !== "admin" && booking.UserId !== req.user.id) {
+ throw {
+ name: "Forbidden",
+ message: "You can only update your own bookings",
+ };
+ }
+
+ //* Validasi hanya boleh update booking dengan status pending
+ Iif (booking.status !== "pending") {
+ throw {
+ name: "Forbidden",
+ message: "Only bookings with status 'pending' can be updated",
+ };
+ }
+ //* Update hanya field tertentu
+ await booking.update({
+ date: date || booking.date,
+ timeStart: timeStart || booking.timeStart,
+ timeEnd: timeEnd || booking.timeEnd,
+ status: status || booking.status,
+ });
+
+ res.status(200).json({ message: "Booking updated" });
+ } catch (err) {
+ next(err);
+ }
+ }
+ //* Update Booking Status (admin)
+ static async updateBookingStatus(req, res, next) {
+ try {
+ const { id } = req.params;
+ const { status } = req.body;
+
+ const booking = await Booking.findByPk(id);
+ if (!booking) throw { name: "NotFound", message: "Booking not found" };
+
+ // Validasi status (opsional, hanya allow certain status)
+ const validStatuses = ["pending", "approved", "cancelled"];
+ Iif (!validStatuses.includes(status)) {
+ throw { name: "BadRequest", message: "Invalid status value" };
+ }
+
+ await booking.update({ status });
+
+ res.status(200).json({ message: "Booking status updated", booking });
+ } catch (err) {
+ next(err);
+ }
+ }
+ //* Hapus Booking (admin)
+ static async deleteBooking(req, res, next) {
+ try {
+ const { id } = req.params;
+ const booking = await Booking.findByPk(id);
+ if (!booking) throw { name: "NotFound", message: "Booking not found" };
+ await booking.destroy();
+ res.status(200).json({ message: "Booking deleted" });
+ } catch (err) {
+ next(err);
+ }
+ }
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 | 5x + +5x + +2x +2x + +2x + + +2x + + + + + + +2x +2x +2x +2x +1x + +1x + +1x + + + + +2x + +2x +2x + + + + + + + +1x + +1x + + + + +2x +2x + +2x +2x +2x +1x + +1x + + + + + + + +1x + +1x + + + + +2x +2x +2x +2x +1x + +1x +1x + +1x + + + + | const { Court } = require("../models");
+
+module.exports = class courtController {
+ static async getAllCourts(req, res, next) {
+ try {
+ const { order = "DESC" } = req.query;
+
+ const courts = await Court.findAll({
+ order: [["updatedAt", order.toUpperCase()]], // ASC / DESC
+ });
+ res.status(200).json(courts);
+ } catch (err) {
+ next(err);
+ }
+ }
+
+ static async getCourtById(req, res, next) {
+ try {
+ const courtId = req.params.id;
+ const court = await Court.findByPk(courtId);
+ if (!court) {
+ throw { name: "NotFound", message: "Court not found" };
+ }
+ res.status(200).json(court);
+ } catch (err) {
+ next(err);
+ }
+ }
+
+ static async createCourt(req, res, next) {
+ try {
+ const { name, category, location, pricePerHour, description, imageUrl } =
+ req.body;
+ const newCourt = await Court.create({
+ name,
+ category,
+ location,
+ pricePerHour,
+ description,
+ imageUrl,
+ });
+ res.status(201).json(newCourt);
+ } catch (err) {
+ next(err);
+ }
+ }
+
+ static async updateCourt(req, res, next) {
+ try {
+ const courtId = req.params.id;
+ const { name, category, location, pricePerHour, description, imageUrl } =
+ req.body;
+ const court = await Court.findByPk(courtId);
+ if (!court) {
+ throw { name: "NotFound", message: "Court not found" };
+ }
+ await court.update({
+ name,
+ category,
+ location,
+ pricePerHour,
+ description,
+ imageUrl,
+ });
+ res.status(200).json({ message: "Court updated successfully" });
+ } catch (err) {
+ next(err);
+ }
+ }
+
+ static async deleteCourt(req, res, next) {
+ try {
+ const courtId = req.params.id;
+ const court = await Court.findByPk(courtId);
+ if (!court) {
+ throw { name: "NotFound", message: "Court not found" };
+ }
+ await court.destroy();
+ res.status(200).json({ message: "Court deleted successfully" });
+ } catch (err) {
+ next(err);
+ }
+ }
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| authController.js | +
+
+ |
+ 100% | +43/43 | +93.75% | +15/16 | +100% | +3/3 | +100% | +43/43 | +
| bookingController.js | +
+
+ |
+ 76.66% | +46/60 | +67.85% | +19/28 | +83.33% | +5/6 | +77.19% | +44/57 | +
| courtController.js | +
+
+ |
+ 97.22% | +35/36 | +100% | +7/7 | +100% | +5/5 | +97.22% | +35/36 | +
| paymentController.js | +
+
+ |
+ 75.38% | +49/65 | +50% | +12/24 | +100% | +2/2 | +75.38% | +49/65 | +
| publicController.js | +
+
+ |
+ 95.23% | +20/21 | +100% | +11/11 | +100% | +2/2 | +95.23% | +20/21 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 | 5x +5x +5x +5x +5x +5x + +5x + +1x +1x +1x + +1x + + + + + +1x +1x + + + + + +1x +1x +1x + + +1x +1x +1x + + +1x + + + + + +1x + + + + + + +1x + + + +1x +1x + + +1x +1x + + + + + + + + + + + + + + + + +1x + +1x +1x + +1x + + + + + + + + + +1x + + +1x + + + + + +1x + + + + + + + + + +1x +1x + + +1x + + + + +1x + + +1x +1x +1x + +1x + + + +1x +1x +1x +1x + + + + + + + +1x + +1x + +1x + + + +1x + + + + + + + + + + + + | const { Payment, Booking, Court } = require("../models");
+const midtransClient = require("midtrans-client");
+const { signToken } = require("../helpers/jwt");
+const { Op, or } = require("sequelize");
+const dayjs = require("dayjs");
+const axios = require("axios");
+
+module.exports = class paymentController {
+ static async initiateMidtransTrx(req, res, next) {
+ try {
+ console.log("Request Headers:", req.headers);
+ console.log("Request Body:", req.body);
+
+ Iif (!process.env.MIDTRANS_SERVER_KEY) {
+ throw {
+ name: "ServerError",
+ message: "MIDTRANS_SERVER_KEY is missing",
+ };
+ }
+ console.log("MIDTRANS_SERVER_KEY:", process.env.MIDTRANS_SERVER_KEY);
+ let snap = new midtransClient.Snap({
+ // Set to true if you want Production Environment (accept real transaction).
+ isProduction: false,
+ serverKey: process.env.MIDTRANS_SERVER_KEY,
+ clientKey: process.env.MIDTRANS_CLIENT_KEY,
+ });
+ console.log("Request Body:", req.body);
+ const { BookingId } = req.body;
+ Iif (!BookingId) {
+ throw { name: "InvalidInput", message: "BookingId is required" };
+ }
+ console.log("User ID:", req.user.id);
+ console.log("Booking ID:", req.body.BookingId);
+ Iif (!req.body.BookingId) {
+ return res.status(400).json({ message: "BookingId is required" });
+ }
+ Iif (!req.user) {
+ return res
+ .status(401)
+ .json({ message: "Unauthorized: user not found" });
+ }
+ // Cari booking berdasarkan BookingId dan UserId
+ const booking = await Booking.findOne({
+ where: {
+ id: BookingId,
+ UserId: req.user.id,
+ },
+ });
+
+ Iif (!booking) {
+ return res.status(404).json({ message: "Booking not found" });
+ }
+
+ const orderId = `BOOK-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
+ Iif (!orderId) {
+ return res.status(500).json({ message: "Order ID generation failed" });
+ }
+ const amount = 10000;
+ let parameter = {
+ // ini adalah data detail order
+ transaction_details: {
+ order_id: orderId,
+ gross_amount: amount,
+ },
+ // ini adalah data jenis pembayaran
+ credit_card: {
+ secure: true,
+ },
+ // ini adalah data customer
+ customer_details: {
+ first_name: req.user.name,
+ email: req.user.email,
+ },
+ };
+ let transaction;
+ try {
+ // 1.create transaction to midtrans
+ transaction = await snap.createTransaction(parameter);
+ console.log("📦 Midtrans Transaction Response:", transaction);
+ // transaction token
+ Iif (!transaction.token) {
+ throw new Error(
+ "Transaction token is missing from Midtrans response"
+ );
+ }
+ // console.log("transactionToken:", transactionToken);
+ } catch (err) {
+ console.error("Midtrans Error:", err.response?.data || err.message);
+ throw err;
+ }
+ let transactionToken = transaction.token;
+ // 2. create order in our database
+
+ await Payment.create({
+ BookingId: booking.id,
+ amount,
+ orderId,
+ });
+
+ res.json({ message: "Order created", transactionToken, orderId });
+ } catch (err) {
+ console.log("❌ INITIATE MIDTRANS TRX ERROR:");
+ console.dir(err, { depth: null });
+ next(err);
+ }
+ }
+
+ static async upgradeAccount(req, res, next) {
+ // check orderId, order ke midtrans, apakah sudah dibayar atau belum
+ try {
+ const { orderId } = req.body;
+
+ // Cek apakah user sudah pernah upgrade
+ const order = await Payment.findOne({
+ where: {
+ orderId,
+ },
+ });
+ Iif (!order) {
+ return res.status(404).json({ message: "Order not found" });
+ }
+ console.log("🔍 Order:", order);
+ console.log("Headers:", req.headers);
+ console.log("Authorization Header:", req.headers.authorization);
+
+ Iif (order.status === "paid") {
+ return res.status(400).json({ message: "Account already upgraded" });
+ }
+
+ const serverKey = process.env.MIDTRANS_SERVER_KEY;
+ const base64ServerKey = Buffer.from(serverKey + ":").toString("base64");
+ console.log(`Authorization: Basic ${base64ServerKey}`);
+ const { data } = await axios.get(
+ `https://api.sandbox.midtrans.com/v2/${orderId}/status`,
+ {
+ headers: {
+ Authorization: `Basic ${base64ServerKey}`,
+ },
+ }
+ );
+ console.log("📦 Midtrans Response:", data);
+ // midtrans validasi status pembayaran
+ if (data.transaction_status === "capture" && data.status_code === "200") {
+ // Update status pembayaran di database
+ await order.update({
+ status: "paid",
+ paidAt: new Date(),
+ });
+ res.status(200).json({ message: "Account upgraded successfully" });
+ } else E{
+ return res.status(400).json({
+ message: "Payment not completed or invalid transaction status",
+ midtransMessage: data.status_message,
+ });
+ }
+ } catch (err) {
+ next(err);
+ }
+ }
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 | 5x +5x + +5x + +3x +3x +3x + +3x +1x + + + + + +3x +1x + + +3x +3x + + + + +3x + + + + + + + + + + + + + +2x +2x +2x +2x +1x + +1x + +1x + + + + | const { Court } = require("../models");
+const { Op } = require("sequelize");
+
+module.exports = class publicController {
+ static async getCourts(req, res, next) {
+ try {
+ const { search = "", category, page = 1, limit = 10 } = req.query;
+ const where = {};
+ // Search by name or location (case-insensitive)
+ if (search && !category) {
+ where[Op.or] = [
+ { name: { [Op.iLike]: `%${search}%` } },
+ { location: { [Op.iLike]: `%${search}%` } },
+ ];
+ }
+ // Filter by category
+ if (category) {
+ where.category = { [Op.iLike]: `%${category}%` };
+ }
+ // console.log("FILTER CONDITIONS:", { where });
+ const offset = (page - 1) * limit;
+ const { rows, count } = await Court.findAndCountAll({
+ where,
+ limit: +limit,
+ offset: +offset,
+ });
+ res.status(200).json({
+ data: rows,
+ pagination: {
+ page: +page,
+ totalPages: Math.ceil(count / limit),
+ totalData: count,
+ },
+ });
+ } catch (err) {
+ next(err);
+ }
+ }
+
+ static async getCourtsById(req, res, next) {
+ try {
+ const courtId = req.params.id;
+ const court = await Court.findByPk(courtId);
+ if (!court) {
+ throw { name: "NotFound", message: "Court not found" };
+ }
+ res.status(200).json(court);
+ } catch (err) {
+ next(err);
+ }
+ }
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 | 6x +6x + +6x + + +2x + +2x + +2x +2x +2x + + +6x + | const { GoogleGenerativeAI } = require("@google/generative-ai");
+require("dotenv").config();
+
+const genAI = new GoogleGenerativeAI(process.env.GOOGLE_GENAI_API_KEY);
+
+async function askGemini(prompt) {
+ console.log("🔥 askGemini DIPANGGIL");
+
+ const model = genAI.getGenerativeModel({ model: "models/gemini-1.5-flash" });
+
+ const result = await model.generateContent(prompt);
+ const response = await result.response;
+ return response.text();
+}
+
+module.exports = { askGemini };
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 | 5x + +5x +7x +7x + + +5x +2x + + +5x + + + + | const bcrypt = require("bcryptjs");
+
+const hashPassword = (password) => {
+ const salt = bcrypt.genSaltSync(10);
+ return bcrypt.hashSync(password, salt);
+};
+
+const comparePassword = (plainPassword, hashedPassword) => {
+ return bcrypt.compareSync(plainPassword, hashedPassword);
+};
+
+module.exports = {
+ hashPassword,
+ comparePassword,
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| AskGemini.js | +
+
+ |
+ 100% | +9/9 | +100% | +0/0 | +100% | +1/1 | +100% | +9/9 | +
| bcrypt.js | +
+
+ |
+ 100% | +7/7 | +100% | +0/0 | +100% | +2/2 | +100% | +7/7 | +
| jwt.js | +
+
+ |
+ 100% | +5/5 | +50% | +1/2 | +100% | +2/2 | +100% | +5/5 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 | 5x +5x + + +6x + + + +23x + + +5x + | const jwt = require("jsonwebtoken");
+const SECRET = process.env.JWT_SECRET || "sportify123";
+
+function signToken(payload) {
+ return jwt.sign(payload, SECRET);
+}
+
+function verifyToken(token) {
+ return jwt.verify(token, SECRET);
+}
+
+module.exports = { signToken, verifyToken };
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| app.js | +
+
+ |
+ 100% | +22/22 | +50% | +1/2 | +100% | +0/0 | +100% | +22/22 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 | 5x +5x + + +5x + +25x +25x +25x +25x +2x +2x + +23x + +23x + +23x + +23x + +23x + + + +23x + +23x + + + + + | const { User } = require("../models");
+const { verifyToken } = require("../helpers/jwt");
+
+//! authentication = untuk identify user/verifikasi(dia itu siapa?)
+module.exports = async function authentication(req, res, next) {
+ //! Extract token: Get Bearer token from authorization header
+ console.log("Headers:", req.headers);
+ const bearerToken = req.headers.authorization;
+ console.log("🔐 Authorization Header:", bearerToken);
+ if (!bearerToken) {
+ next({ name: "Unauthorized", message: "Invalid token" }); //401
+ return;
+ }
+ const access_token = bearerToken.split(" ")[1];
+
+ try {
+ //! Verify token
+ const data = verifyToken(access_token);
+
+ const user = await User.findByPk(data.id);
+
+ Iif (!user) {
+ throw { name: "Unauthorized", message: "Invalid token" }; //401
+ }
+ //! Attach user
+ req.user = user;
+ //! continue: call next() to proceed
+ next();
+ } catch (err) {
+ next(err);
+ }
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 | 6x +33x +9x +24x +10x +14x +2x +12x + +12x +1x +11x +7x + +4x + + + | module.exports = function errorHandler(err, req, res, next) {
+ if (err.name === "BadRequest") {
+ res.status(400).json({ message: err.message });
+ } else if (err.name === "SequelizeValidationError") {
+ res.status(400).json({ message: err.errors[0].message });
+ } else if (err.name === "Unauthorized") {
+ res.status(401).json({ message: err.message });
+ } else Iif (err.name === "JsonWebTokenError") {
+ res.status(401).json({ message: "Invalid token" });
+ } else if (err.name === "Forbidden") {
+ res.status(403).json({ message: err.message });
+ } else if (err.name === "NotFound") {
+ res.status(404).json({ message: err.message });
+ } else {
+ res.status(500).json({ message: "Internal Server Error" });
+ }
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| authentication.js | +
+
+ |
+ 88.88% | +16/18 | +75% | +3/4 | +100% | +1/1 | +88.88% | +16/18 | +
| errorHandler.js | +
+
+ |
+ 92.85% | +13/14 | +91.66% | +11/12 | +100% | +1/1 | +92.85% | +13/14 | +
| isAdmin.js | +
+
+ |
+ 100% | +4/4 | +100% | +2/2 | +100% | +1/1 | +100% | +4/4 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 | 5x +15x +1x + + + + +14x + + | module.exports = function authorizeAdmin(req, res, next) {
+ if (req.user.role !== "admin") {
+ return next({
+ name: "Forbidden",
+ message: "Only admin can access this resource",
+ }); // 403
+ }
+ next();
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 | +5x +5x + + + + + + + + +5x +5x +5x + + +5x + + + + + + + + + + + + + + + + + + + + + + + + + + +5x + + | "use strict";
+const { Model } = require("sequelize");
+module.exports = (sequelize, DataTypes) => {
+ class Booking extends Model {
+ /**
+ * Helper method for defining associations.
+ * This method is not a part of Sequelize lifecycle.
+ * The `models/index` file will call this method automatically.
+ */
+ static associate(models) {
+ // define association here
+ Booking.belongsTo(models.User, { foreignKey: "UserId" });
+ Booking.belongsTo(models.Court, { foreignKey: "CourtId" });
+ Booking.hasOne(models.Payment, { foreignKey: "BookingId" });
+ }
+ }
+ Booking.init(
+ {
+ UserId: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ },
+ CourtId: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ },
+ date: DataTypes.DATEONLY,
+ timeStart: DataTypes.TIME,
+ timeEnd: DataTypes.TIME,
+ status: {
+ type: DataTypes.STRING,
+ defaultValue: "pending",
+ },
+ isPaid: {
+ type: DataTypes.BOOLEAN,
+ defaultValue: false,
+ },
+ },
+ {
+ sequelize,
+ modelName: "Booking",
+ }
+ );
+ return Booking;
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 | +5x +5x + + + + + + + + +5x + + +5x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +5x + + | "use strict";
+const { Model } = require("sequelize");
+module.exports = (sequelize, DataTypes) => {
+ class Court extends Model {
+ /**
+ * Helper method for defining associations.
+ * This method is not a part of Sequelize lifecycle.
+ * The `models/index` file will call this method automatically.
+ */
+ static associate(models) {
+ // define association here
+ Court.hasMany(models.Booking, { foreignKey: "CourtId" });
+ }
+ }
+ Court.init(
+ {
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ notNull: {
+ msg: "Name is required",
+ },
+ notEmpty: {
+ msg: "Name is required",
+ },
+ },
+ },
+ category: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ notNull: {
+ msg: "Category is required",
+ },
+ notEmpty: {
+ msg: "Category is required",
+ },
+ },
+ },
+ location: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ notNull: {
+ msg: "Location is required",
+ },
+ notEmpty: {
+ msg: "Location is required",
+ },
+ },
+ },
+ pricePerHour: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ validate: {
+ notNull: {
+ msg: "Price is required",
+ },
+ notEmpty: {
+ msg: "Price is required",
+ },
+ },
+ },
+ description: {
+ type: DataTypes.TEXT,
+ allowNull: false,
+ validate: {
+ notNull: {
+ msg: "Description is required",
+ },
+ notEmpty: {
+ msg: "Description is required",
+ },
+ },
+ },
+ imageUrl: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ notNull: {
+ msg: "Image URL is required",
+ },
+ notEmpty: {
+ msg: "Image URL is required",
+ },
+ },
+ },
+ },
+ {
+ sequelize,
+ modelName: "Court",
+ }
+ );
+ return Court;
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| booking.js | +
+
+ |
+ 100% | +7/7 | +100% | +0/0 | +100% | +2/2 | +100% | +7/7 | +
| court.js | +
+
+ |
+ 100% | +5/5 | +100% | +0/0 | +100% | +2/2 | +100% | +5/5 | +
| index.js | +
+
+ |
+ 95.23% | +20/21 | +70% | +7/10 | +100% | +3/3 | +95.23% | +20/21 | +
| payment.js | +
+
+ |
+ 100% | +5/5 | +100% | +0/0 | +100% | +2/2 | +100% | +5/5 | +
| user.js | +
+
+ |
+ 100% | +8/8 | +100% | +0/0 | +100% | +3/3 | +100% | +8/8 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 | + +5x +5x +5x +5x +5x +5x +5x +5x + + +5x + + +5x + + + + + + + +5x + +25x + + + + + + + +20x + + + +20x + + +5x +20x +20x + + + +5x +5x + +5x + | "use strict";
+
+const fs = require("fs");
+const path = require("path");
+const Sequelize = require("sequelize");
+const process = require("process");
+const basename = path.basename(__filename);
+const env = process.env.NODE_ENV || "development";
+const config = require(__dirname + "/../config/config.js")[env];
+const db = {};
+
+let sequelize;
+Iif (config.use_env_variable) {
+ sequelize = new Sequelize(process.env[config.use_env_variable], config);
+} else {
+ sequelize = new Sequelize(
+ config.database,
+ config.username,
+ config.password,
+ config
+ );
+}
+
+fs.readdirSync(__dirname)
+ .filter((file) => {
+ return (
+ file.indexOf(".") !== 0 &&
+ file !== basename &&
+ file.slice(-3) === ".js" &&
+ file.indexOf(".test.js") === -1
+ );
+ })
+ .forEach((file) => {
+ const model = require(path.join(__dirname, file))(
+ sequelize,
+ Sequelize.DataTypes
+ );
+ db[model.name] = model;
+ });
+
+Object.keys(db).forEach((modelName) => {
+ Eif (db[modelName].associate) {
+ db[modelName].associate(db);
+ }
+});
+
+db.sequelize = sequelize;
+db.Sequelize = Sequelize;
+
+module.exports = db;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 | +5x +5x + + + +5x + + +5x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +5x + + | "use strict";
+const { Model } = require("sequelize");
+module.exports = (sequelize, DataTypes) => {
+ class Payment extends Model {
+ static associate(models) {
+ // define association here
+ Payment.belongsTo(models.Booking, { foreignKey: "BookingId" });
+ }
+ }
+ Payment.init(
+ {
+ BookingId: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ validate: {
+ notNull: { msg: "BookingId is required" },
+ isInt: { msg: "BookingId must be an integer" },
+ },
+ },
+ amount: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ validate: {
+ notNull: { msg: "Amount is required" },
+ min: { args: [1], msg: "Amount must be at least 1" },
+ isInt: { msg: "Amount must be a number" },
+ },
+ },
+ method: {
+ type: DataTypes.STRING,
+ defaultValue: "midtrans",
+ },
+ status: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ defaultValue: "pending",
+ validate: {
+ isIn: {
+ args: [["unpaid", "pending", "paid", "failed"]],
+ msg: "Status must be one of: unpaid, pending, paid, failed",
+ },
+ },
+ },
+ paymentUrl: {
+ type: DataTypes.STRING,
+ validate: {
+ isUrl: { msg: "paymentUrl must be a valid URL" },
+ },
+ },
+ paidAt: {
+ type: DataTypes.DATE,
+ },
+ orderId: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ notNull: {
+ msg: "OrderId is required",
+ },
+ notEmpty: {
+ msg: "OrderId cannot be empty",
+ },
+ },
+ },
+ },
+ {
+ sequelize,
+ modelName: "Payment",
+ }
+ );
+ return Payment;
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 | +5x +5x +5x + + + + + + + + +5x + + +5x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +5x +7x + +5x + + | "use strict";
+const { Model } = require("sequelize");
+const { hashPassword } = require("../helpers/bcrypt");
+module.exports = (sequelize, DataTypes) => {
+ class User extends Model {
+ /**
+ * Helper method for defining associations.
+ * This method is not a part of Sequelize lifecycle.
+ * The `models/index` file will call this method automatically.
+ */
+ static associate(models) {
+ // define association here
+ User.hasMany(models.Booking, { foreignKey: "UserId" });
+ }
+ }
+ User.init(
+ {
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ notEmpty: {
+ msg: "Name cannot be empty",
+ },
+ notNull: {
+ msg: "Name is required",
+ },
+ },
+ },
+ email: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ unique: true,
+ validate: {
+ isEmail: true,
+ notEmpty: {
+ msg: "Email cannot be empty",
+ },
+ notNull: {
+ msg: "Email is required",
+ },
+ },
+ },
+ password: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ notEmpty: {
+ msg: "Password cannot be empty",
+ },
+ notNull: {
+ msg: "Password is required",
+ },
+ len: {
+ args: [6],
+ msg: "Password must be at least 6 characters long",
+ },
+ },
+ },
+ role: {
+ type: DataTypes.STRING,
+ defaultValue: "user",
+ },
+ },
+ {
+ sequelize,
+ modelName: "User",
+ }
+ );
+ User.beforeCreate((user) => {
+ user.password = hashPassword(user.password);
+ });
+ return User;
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 | 6x +6x +6x + +6x + +6x +2x +2x +2x + + + +2x +1x + +1x + + + +6x + +6x + | const express = require("express");
+const { askGemini } = require("../helpers/AskGemini");
+const errorHandler = require("../middleware/errorHandler");
+
+const router = express.Router();
+
+router.post("/recommend", async (req, res, next) => {
+ try {
+ const { availableSlots, preference } = req.body;
+ const prompt = `Berikan rekomendasi waktu terbaik untuk booking berdasarkan preferensi berikut: ${preference}. Pilihan slot tersedia: ${availableSlots.join(
+ ", "
+ )}`;
+
+ const recommendation = await askGemini(prompt);
+ res.json({ recommendation });
+ } catch (err) {
+ next(err);
+ }
+});
+
+router.use(errorHandler);
+
+module.exports = router;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 | 5x +5x +5x +5x + +5x +5x +5x + + +5x + +5x + | const express = require("express");
+const router = express.Router();
+const authController = require("../controllers/authController");
+const errorHandler = require("../middleware/errorHandler");
+
+router.post("/login", authController.login);
+router.post("/register", authController.register);
+router.post("/login/google", authController.googleLogin);
+
+//* error handler
+router.use(errorHandler);
+
+module.exports = router;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 | 5x +5x +5x +5x +5x +5x + +5x + +5x +5x +5x + + +5x +5x +5x +5x + + +5x + +5x + | const express = require("express");
+const router = require("express").Router();
+const bookingController = require("../controllers/bookingController");
+const authentication = require("../middleware/authentication");
+const isAdmin = require("../middleware/isAdmin");
+const errorHandler = require("../middleware/errorHandler");
+
+router.use(authentication);
+// USER
+router.get("/mine", bookingController.getMyBookings);
+router.post("/", bookingController.createBooking);
+router.patch("/:id", bookingController.updateBooking); // Allow user to update their own booking
+
+// ADMIN
+router.get("/", isAdmin, bookingController.getAllBookings);
+router.put("/:id", isAdmin, bookingController.updateBooking);
+router.delete("/:id", isAdmin, bookingController.deleteBooking);
+router.patch("/:id/status", isAdmin, bookingController.updateBookingStatus);
+
+//* error handler
+router.use(errorHandler);
+
+module.exports = router;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 | 5x +5x +5x +5x +5x +5x + + +5x + + +5x + + +5x + + +5x + + +5x + + +5x + + +5x + +5x + | const express = require("express");
+const router = express.Router();
+const courtController = require("../controllers/courtController");
+const authentication = require("../middleware/authentication");
+const isAdmin = require("../middleware/isAdmin");
+const errorHandler = require("../middleware/errorHandler");
+
+// Semua endpoint di sini harus login dulu
+router.use(authentication); //* Semua route setelah ini butuh login
+
+// GET semua lapangan (semua user bisa lihat)
+router.get("/", isAdmin, courtController.getAllCourts);
+
+// GET detail lapangan by id
+router.get("/:id", isAdmin, courtController.getCourtById);
+
+// POST buat tambah lapangan (hanya admin)
+router.post("/", isAdmin, courtController.createCourt);
+
+// PUT edit lapangan (admin)
+router.put("/:id", isAdmin, courtController.updateCourt);
+
+// DELETE hapus lapangan (admin)
+router.delete("/:id", isAdmin, courtController.deleteCourt);
+
+//* error handler
+router.use(errorHandler);
+
+module.exports = router;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| ai.js | +
+
+ |
+ 100% | +13/13 | +100% | +0/0 | +100% | +1/1 | +100% | +13/13 | +
| auth.js | +
+
+ |
+ 100% | +9/9 | +100% | +0/0 | +100% | +0/0 | +100% | +9/9 | +
| bookings.js | +
+
+ |
+ 100% | +16/16 | +100% | +0/0 | +100% | +0/0 | +100% | +16/16 | +
| courts.js | +
+
+ |
+ 100% | +14/14 | +100% | +0/0 | +100% | +0/0 | +100% | +14/14 | +
| payments.js | +
+
+ |
+ 100% | +10/10 | +100% | +0/0 | +100% | +0/0 | +100% | +10/10 | +
| public.js | +
+
+ |
+ 100% | +8/8 | +100% | +0/0 | +100% | +0/0 | +100% | +8/8 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 | 5x +5x +5x +5x +5x + +5x + +5x +5x + +5x + +5x + | const express = require("express");
+const router = express.Router();
+const authentication = require("../middleware/authentication");
+const paymentController = require("../controllers/paymentController");
+const errorHandler = require("../middleware/errorHandler");
+
+router.use(authentication);
+
+router.patch("/me/upgrade", paymentController.upgradeAccount); // hanya user yang login
+router.post("/midtrans/initiate", paymentController.initiateMidtransTrx);
+// error handler
+router.use(errorHandler);
+
+module.exports = router;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 | 5x +5x +5x +5x + + + +5x + +5x + + +5x + +5x + | const express = require("express");
+const router = express.Router();
+const publicController = require("../controllers/publicController");
+const errorHandler = require("../middleware/errorHandler");
+
+//* Public routes
+// GET semua lapangan (semua user bisa lihat)
+router.get("/courts", publicController.getCourts);
+// GET detail lapangan by id
+router.get("/courts/:id", publicController.getCourtsById);
+
+//* error handler
+router.use(errorHandler);
+
+module.exports = router;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| Server | +
+
+ |
+ 100% | +22/22 | +50% | +1/2 | +100% | +0/0 | +100% | +22/22 | +
| Server/config | +
+
+ |
+ 100% | +2/2 | +50% | +1/2 | +100% | +0/0 | +100% | +2/2 | +
| Server/controllers | +
+
+ |
+ 85.77% | +193/225 | +74.41% | +64/86 | +94.44% | +17/18 | +86.03% | +191/222 | +
| Server/helpers | +
+
+ |
+ 100% | +21/21 | +50% | +1/2 | +100% | +5/5 | +100% | +21/21 | +
| Server/middleware | +
+
+ |
+ 91.66% | +33/36 | +88.88% | +16/18 | +100% | +3/3 | +91.66% | +33/36 | +
| Server/models | +
+
+ |
+ 97.82% | +45/46 | +70% | +7/10 | +100% | +12/12 | +97.82% | +45/46 | +
| Server/routes | +
+
+ |
+ 100% | +70/70 | +100% | +0/0 | +100% | +1/1 | +100% | +70/70 | +