diff --git a/README.md b/README.md index 7312fef..1a50a7c 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,2 @@ -# Base-REST-API - -A robust, modular, and secure REST API boilerplate built with **Node.js** and **Express**. -It features a **recursive file loader** for routes and schemas, along with a powerful validation middleware using **Zod**. - -## 🚀 Features - -- **Automated Loading**: Recursively loads routes and validation schemas from the file system. -- **Strict Validation**: Request bodies and query parameters are validated using [Zod](https://zod.dev/) before reaching the controller. -- **Clean Architecture**: Separation of concerns with `Routes` (HTTP layer), `Services` (Business logic), and `Schemas` (Validation). -- **Security First**: Inputs are stripped of unknown fields automatically. -- **Custom Logger**: Integrated color-coded logging system for development and file logging for production. -- **Error Handling**: Standardized JSON error responses. - -## 📦 Installation - -1. **Clone the repository** - ```bash - git clone https://gitea.azures.fr/azures04/Base-REST-API.git - cd Base-REST-API - ``` \ No newline at end of file +# TCJourney Server +TCJourney (TCJ) est un jeu de collection de transports. Ce dépôt contient le code de son backend \ No newline at end of file diff --git a/data/ddl/13_checkins.sql b/data/ddl/13_checkins.sql new file mode 100644 index 0000000..2cf7600 --- /dev/null +++ b/data/ddl/13_checkins.sql @@ -0,0 +1,21 @@ +CREATE TABLE IF NOT EXISTS `checkins` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `userId` UUID NOT NULL, + `stationId` INT UNSIGNED NOT NULL, + `platformId` INT UNSIGNED NULL COMMENT 'Platform optionnelle pour plus de détail', + `latitude` DECIMAL(10, 8) NOT NULL, + `longitude` DECIMAL(11, 8) NOT NULL, + `distance` FLOAT NOT NULL COMMENT 'Distance en mètres du point de poinçonnage à la plateforme/station', + `checkinAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY (id), + INDEX idx_checkins_user (userId), + INDEX idx_checkins_station (stationId), + INDEX idx_checkins_platform (platformId), + INDEX idx_checkins_timestamp (checkinAt), + UNIQUE KEY uq_checkins_user_station_time (userId, stationId, checkinAt), + + CONSTRAINT `fk_checkins_userId` FOREIGN KEY (`userId`) REFERENCES `users` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_checkins_stationId` FOREIGN KEY (`stationId`) REFERENCES `stations` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_checkins_platformId` FOREIGN KEY (`platformId`) REFERENCES `platforms` (`id`) ON DELETE SET NULL +); \ No newline at end of file diff --git a/data/ddl/14_badges.sql b/data/ddl/14_badges.sql new file mode 100644 index 0000000..037bb3e --- /dev/null +++ b/data/ddl/14_badges.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS `badges` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) NOT NULL UNIQUE, + `description` TEXT, + `icon` TEXT, + `condition` VARCHAR(255) NOT NULL COMMENT 'Ex: checkins_count >= 10', + `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY (id) +); \ No newline at end of file diff --git a/data/ddl/15_user_badges.sql b/data/ddl/15_user_badges.sql new file mode 100644 index 0000000..f3b02a6 --- /dev/null +++ b/data/ddl/15_user_badges.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS `user_badges` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `userId` UUID NOT NULL, + `badgeId` INT UNSIGNED NOT NULL, + `unlockedAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY (id), + UNIQUE KEY uq_user_badges_user_badge (userId, badgeId), + INDEX idx_user_badges_user (userId), + + CONSTRAINT `fk_user_badges_userId` FOREIGN KEY (`userId`) REFERENCES `users` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_user_badges_badgeId` FOREIGN KEY (`badgeId`) REFERENCES `badges` (`id`) ON DELETE CASCADE +); \ No newline at end of file diff --git a/package.json b/package.json index d4b3f41..b8a11e3 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "base-rest-api", + "name": "tcjourney/server", "version": "0.0.1-alpha", "description": "", "repository": { diff --git a/repositories/badgeRepository.js b/repositories/badgeRepository.js new file mode 100644 index 0000000..c94db3c --- /dev/null +++ b/repositories/badgeRepository.js @@ -0,0 +1,63 @@ +const { pool } = require("../modules/database") +const { DefaultError } = require("../errors/errors") + +async function createBadge(name, description, icon, condition) { + try { + const sql = "INSERT INTO `badges` (name, description, icon, condition) VALUES (?, ?, ?, ?)" + const rows = await pool.query(sql, [name, description, icon, condition]) + return rows.insertId + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function findAllBadges() { + try { + const sql = "SELECT * FROM `badges` ORDER BY name" + const rows = await pool.query(sql) + return rows + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function findBadgeById(badgeId) { + try { + const sql = "SELECT * FROM `badges` WHERE id = ?" + const rows = await pool.query(sql, [badgeId]) + return rows[0] || null + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function updateBadge(badgeId, updateData) { + try { + const fields = Object.keys(updateData) + const values = Object.values(updateData) + const setClause = fields.map(f => `${f} = ?`).join(", ") + const sql = `UPDATE \`badges\` SET ${setClause} WHERE id = ?` + const rows = await pool.query(sql, [...values, badgeId]) + return rows.affectedRows > 0 + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function deleteBadge(badgeId) { + try { + const sql = "DELETE FROM `badges` WHERE id = ?" + const rows = await pool.query(sql, [badgeId]) + return rows.affectedRows > 0 + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +module.exports = { + createBadge, + findAllBadges, + findBadgeById, + updateBadge, + deleteBadge +} \ No newline at end of file diff --git a/repositories/checkinRepository.js b/repositories/checkinRepository.js new file mode 100644 index 0000000..85474bd --- /dev/null +++ b/repositories/checkinRepository.js @@ -0,0 +1,93 @@ +const { pool } = require("../modules/database") +const { DefaultError } = require("../errors/errors") + +async function createCheckin(userId, stationId, platformId, latitude, longitude, distance) { + try { + const sql = "INSERT INTO `checkins` (userId, stationId, platformId, latitude, longitude, distance) VALUES (?, ?, ?, ?, ?, ?)" + const rows = await pool.query(sql, [userId, stationId, platformId, latitude, longitude, distance]) + return rows.insertId + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function findCheckinById(checkinId) { + try { + const sql = "SELECT * FROM `checkins` WHERE id = ?" + const rows = await pool.query(sql, [checkinId]) + return rows[0] || null + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function findCheckinsByUserId(userId, limit = 50, offset = 0) { + try { + const sql = "SELECT * FROM `checkins` WHERE userId = ? ORDER BY checkinAt DESC LIMIT ? OFFSET ?" + const rows = await pool.query(sql, [userId, limit, offset]) + return rows + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function findCheckinsByStation(stationId, limit = 50, offset = 0) { + try { + const sql = "SELECT * FROM `checkins` WHERE stationId = ? ORDER BY checkinAt DESC LIMIT ? OFFSET ?" + const rows = await pool.query(sql, [stationId, limit, offset]) + return rows + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function countCheckinsByUser(userId) { + try { + const sql = "SELECT COUNT(*) as count FROM `checkins` WHERE userId = ?" + const rows = await pool.query(sql, [userId]) + return rows[0].count + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function countCheckinsByUserAndStation(userId, stationId) { + try { + const sql = "SELECT COUNT(*) as count FROM `checkins` WHERE userId = ? AND stationId = ?" + const rows = await pool.query(sql, [userId, stationId]) + return rows[0].count + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function findUniqueStationsByUser(userId) { + try { + const sql = "SELECT DISTINCT stationId FROM `checkins` WHERE userId = ? ORDER BY checkinAt DESC" + const rows = await pool.query(sql, [userId]) + return rows + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function deleteCheckin(checkinId) { + try { + const sql = "DELETE FROM `checkins` WHERE id = ?" + const rows = await pool.query(sql, [checkinId]) + return rows.affectedRows > 0 + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +module.exports = { + createCheckin, + findCheckinById, + findCheckinsByUserId, + findCheckinsByStation, + countCheckinsByUser, + countCheckinsByUserAndStation, + findUniqueStationsByUser, + deleteCheckin +} \ No newline at end of file diff --git a/repositories/userBadgeRepository.js b/repositories/userBadgeRepository.js new file mode 100644 index 0000000..ba74f8e --- /dev/null +++ b/repositories/userBadgeRepository.js @@ -0,0 +1,64 @@ +const { pool } = require("../modules/database") +const { DefaultError } = require("../errors/errors") + +async function assignBadgeToUser(userId, badgeId) { + try { + const sql = "INSERT INTO `user_badges` (userId, badgeId) VALUES (?, ?)" + const rows = await pool.query(sql, [userId, badgeId]) + return rows.insertId + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function findUserBadges(userId) { + try { + const sql = `SELECT ub.*, b.name, b.description, b.icon, b.condition + FROM \`user_badges\` ub + JOIN \`badges\` b ON ub.badgeId = b.id + WHERE ub.userId = ? + ORDER BY ub.unlockedAt DESC` + const rows = await pool.query(sql, [userId]) + return rows + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function hasBadge(userId, badgeId) { + try { + const sql = "SELECT COUNT(*) as count FROM `user_badges` WHERE userId = ? AND badgeId = ?" + const rows = await pool.query(sql, [userId, badgeId]) + return rows[0].count > 0 + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function removeBadgeFromUser(userId, badgeId) { + try { + const sql = "DELETE FROM `user_badges` WHERE userId = ? AND badgeId = ?" + const rows = await pool.query(sql, [userId, badgeId]) + return rows.affectedRows > 0 + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function countUserBadges(userId) { + try { + const sql = "SELECT COUNT(*) as count FROM `user_badges` WHERE userId = ?" + const rows = await pool.query(sql, [userId]) + return rows[0].count + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +module.exports = { + assignBadgeToUser, + findUserBadges, + hasBadge, + removeBadgeFromUser, + countUserBadges +} \ No newline at end of file diff --git a/routes/api/v1/admin/badges.js b/routes/api/v1/admin/badges.js new file mode 100644 index 0000000..849024e --- /dev/null +++ b/routes/api/v1/admin/badges.js @@ -0,0 +1,47 @@ +const express = require("express") +const adminService = require("../../../../services/adminService") +const router = express.Router({ mergeParams: true }) + +router.post("/", async (req, res) => { + const { name, icon, condition, description } = req.body + const badge = await adminService.createBadge({ name, icon, condition, description }) + return res.status(201).json(badge) +}) + +router.patch("/:badgeId", async (req, res) => { + const { badgeId } = req.params + const updateData = req.body.data + const badge = await adminService.updateBadge({ badgeId, updateData }) + return res.status(200).json(badge) +}) + +router.delete("/:badgeId", async (req, res) => { + const { badgeId } = req.params + await adminService.deleteBadge({ badgeId }) + return res.sendStatus(204) +}) + +router.put("/user/:userId/:badgeId", async (req, res) => { + const { badgeId, userId } = req.params + const assignedBadge = await adminService.assignBadgeToUser({ badgeId, userId }) + return res.status(200).json(assignedBadge) +}) + +router.delete("/user/:userId/:badgeId", async (req, res) => { + const { badgeId, userId } = req.params + await adminService.removeBadgeFromUser({ badgeId, userId }) + return res.sendStatus(204) +}) + +router.get("/all", async (req, res) => { + const badges = await adminService.getAllBadges() + return res.status(200).json(badges) +}) + +router.get("/:badgeId", async (req, res) => { + const { badgeId } = req.params.badgeId + const badge = await adminService.getBadgeById({ badgeId }) + return res.status(200).json(badge) +}) + +module.exports = router \ No newline at end of file diff --git a/routes/api/v1/admin/index.js b/routes/api/v1/admin/index.js new file mode 100644 index 0000000..5c43b27 --- /dev/null +++ b/routes/api/v1/admin/index.js @@ -0,0 +1,7 @@ +const express = require("express") +const authService = require("../../../../services/authService") +const router = express.Router() + +router.use("", authService.isLogged, authService.hasAdminRights) + +module.exports = router \ No newline at end of file diff --git a/routes/api/v1/admin/transport/lines.js b/routes/api/v1/admin/transport/lines.js new file mode 100644 index 0000000..031413d --- /dev/null +++ b/routes/api/v1/admin/transport/lines.js @@ -0,0 +1,24 @@ +const express = require("express") +const transportService = require("../../../../../services/transportService") +const router = express.Router({ mergeParams: true }) + +router.post("/", async (req, res) => { + const { code, name, backColorHex, textColorHex, transportTypeId } = req.body + const lines = await transportService.createLine({ code, name, backColorHex, textColorHex, transportTypeId }) + return res.status(201).json(lines) +}) + +router.delete("/:lineId", async (req, res) => { + const { lineId } = req.params + await transportService.deleteLine({ lineId }) + return res.sendStatus(204) +}) + +router.patch("/:lineId", async (req, res) => { + const { lineId } = req.params + const updateData = req.body.data + await transportService.updateLine({ lineId, updateData }) + return res.sendStatus(200) +}) + +module.exports = router \ No newline at end of file diff --git a/routes/api/v1/admin/transport/platform.js b/routes/api/v1/admin/transport/platform.js new file mode 100644 index 0000000..5ba1613 --- /dev/null +++ b/routes/api/v1/admin/transport/platform.js @@ -0,0 +1,44 @@ +const express = require("express") +const transportService = require("../../../../../services/transportService") +const router = express.Router({ mergeParams: true }) + +router.post("/", async (req, res) => { + const { direction, latitude, longitude, name, stationId, hasElevator, isPRM } = req.body + const lines = await transportService.createPlatform({ direction: direction || 0, latitude, longitude, name, stationId, hasElevator, isPRM }) + return res.status(201).json(lines) +}) + +router.delete("/:platformId", async (req, res) => { + const { platformId } = req.params + await transportService.deletePlatform({ platformId }) + return res.sendStatus(204) +}) + +router.post("/:platformId/line/:lineId", async (req, res) => { + const { platformId, lineId } = req.params + const { orderInLine, directionId } = req.body + const patchedPlatform = await transportService.addLineToPlatform({ platformId, lineId, orderInLine, directionId }) + return res.status(201).json(patchedPlatform) +}) + +router.patch("/:platformId", async (req, res) => { + const { platformId } = req.params + const { hasElevator, isPRM } = req.body + const patchedPlatform = await transportService.updatePlatformAccessibility({ platformId, hasElevator, isPRM }) + return res.status(200).json(patchedPlatform) +}) + + +router.patch("/:platformLineId/:orderInLine", async (req, res) => { + const { platformLineId, orderInLine } = req.params + const patchedPlatform = await transportService.updatePlatformLineOrder({ platformLineId, orderInLine }) + return res.status(200).json(patchedPlatform) +}) + +router.delete("/:platformId/:lineId", async (req, res) => { + const { lineId, platformId, directionId } = req.body + await transportService.removeLineFromPlatform({ lineId, platformId, directionId }) + return res.sendStatus(204) +}) + +module.exports = router \ No newline at end of file diff --git a/routes/api/v1/admin/transport/stations.js b/routes/api/v1/admin/transport/stations.js new file mode 100644 index 0000000..949f624 --- /dev/null +++ b/routes/api/v1/admin/transport/stations.js @@ -0,0 +1,24 @@ +const express = require("express") +const transportService = require("../../../../../services/transportService") +const router = express.Router({ mergeParams: true }) + +router.post("/", async (req, res) => { + const { name, description } = req.body + const lines = await transportService.createStation({ name, description }) + return res.status(201).json(lines) +}) + +router.delete("/:stationId", async (req, res) => { + const { stationId } = req.params + await transportService.deleteStation({ stationId }) + return res.status(204) +}) + +router.patch("/:stationId", async (req, res) => { + const { stationId } = req.params + const updateData = req.body.data + const patchedStation = await transportService.updateStation({ stationId, updateData }) + return res.status(200).json({ patched: patchedStation }) +}) + +module.exports = router \ No newline at end of file diff --git a/routes/api/v1/transports/lines.js b/routes/api/v1/transports/lines.js new file mode 100644 index 0000000..4096dcb --- /dev/null +++ b/routes/api/v1/transports/lines.js @@ -0,0 +1,28 @@ +const express = require("express") +const transportService = require("../../../../services/transportService") +const router = express.Router({ mergeParams: true }) + +router.get("/:lineId", async (req, res) => { + const { lineId } = req.params + const line = await transportService.getLineById({ lineId }) + return res.status(200).json(line) +}) + +router.get("/:lineId/stations", async (req, res) => { + const { lineId } = req.params + const stations = await transportService.getLineStations({ lineId, directionId: 0 }) + return res.status(200).json(stations) +}) + +router.get("/all", async (req, res) => { + const lines = await transportService.getAllLines() + return res.status(200).json(lines) +}) + +router.get("/all/:transportType", async (req, res) => { + const { transportType } = req.params + const lines = await transportService.getLinesByTransportType(transportType) + return res.status(200).json(lines) +}) + +module.exports = router \ No newline at end of file diff --git a/routes/api/v1/transports/platforms.js b/routes/api/v1/transports/platforms.js new file mode 100644 index 0000000..0663ef4 --- /dev/null +++ b/routes/api/v1/transports/platforms.js @@ -0,0 +1,29 @@ +const express = require("express") +const transportService = require("../../../../services/transportService") +const router = express.Router({ mergeParams: true }) + +router.get("/:platformId", async (req, res) => { + const { platformId } = req.params + const line = await transportService.getPlatformById({ platformId }) + return res.status(200).json(line) +}) + +router.get("/:platformId/lines", async (req, res) => { + const { platformId } = req.params + const stations = await transportService.getPlatformLines({ platformId, directionId: 0 }) + return res.status(200).json(stations) +}) + +router.get("/bystation/:stationId", async (req, res) => { + const { stationId } = req.params + const platforms = await transportService.getPlatformsByStation({ stationId }) + return res.status(200).json(platforms) +}) + +router.get("/nearest/:lineId/:latitude/:longitude/:radiusM", async (req, res) => { + const { lineId, latitude, longitude, radiusM } = req.params + const lines = await transportService.findNearestPlatformForLine({ lineId, latitude, longitude, radiusM }) + return res.status(200).json(lines) +}) + +module.exports = router \ No newline at end of file diff --git a/routes/api/v1/transports/stations.js b/routes/api/v1/transports/stations.js new file mode 100644 index 0000000..921bfda --- /dev/null +++ b/routes/api/v1/transports/stations.js @@ -0,0 +1,34 @@ +const express = require("express") +const transportService = require("../../../../services/transportService") +const router = express.Router({ mergeParams: true }) + +router.get("/:stationId", async (req, res) => { + const { stationId } = req.params + const station = await transportService.getStationByIdx({ stationId }) + return res.status(200).json(station) +}) + +router.get("/:stationId/lines", async (req, res) => { + const { stationId } = req.params + const lines = await transportService.getStationLines(stationId) + return res.status(200).json(lines) +}) + +router.get("/all", async (req, res) => { + const stations = await transportService.getAllStations() + return res.status(200).json(stations) +}) + +router.get("/:radiusKm/:latitude/:longitude", async (req, res) => { + const { radiusKm, latitude, longitude } = req.params + const station = await transportService.getStationsByDistance({ latitude, longitude, radiusKm }) + return res.status(200).json(station) +}) + +router.get("/accessible/:radiusKm/:latitude/:longitude", async (req, res) => { + const { radiusKm, latitude, longitude } = req.params + const station = await transportService.getAccessibleStations({ latitude, longitude, radiusKm }) + return res.status(200).json(station) +}) + +module.exports = router \ No newline at end of file diff --git a/routes/api/v1/user/checkins.js b/routes/api/v1/user/checkins.js new file mode 100644 index 0000000..07df9db --- /dev/null +++ b/routes/api/v1/user/checkins.js @@ -0,0 +1,22 @@ +const express = require("express") +const userService = require("../../../../services/userService") +const router = express.Router() + +router.post("/", async (req, res) => { + const { stationId, platformId, latitude, longitude, distance } = req.body + const checkin = await userService.createCheckin({ distance, latitude, longitude, platformId, stationId, userId: req.user.id }) + return res.status(200).json(checkin) +}) + +router.get("/", async (req, res) => { + const { limit, offset } = req.query + const checkins = await userService.getUserCheckins({ userId: req.user.id, limit, offset }) + return res.status(200).json(checkins) +}) + +router.get("/count", async (req, res) => { + const checkins = await userService.getUserCheckinsCount({ userId: req.user.id }) + return res.status(200).json(checkins) +}) + +module.exports = router \ No newline at end of file diff --git a/routes/api/v1/user/me.js b/routes/api/v1/user/me.js index 3f78bc9..cc6d9ad 100644 --- a/routes/api/v1/user/me.js +++ b/routes/api/v1/user/me.js @@ -19,4 +19,9 @@ router.patch("/displayname", async (req, res) => { return res.status(200).json(updatedProfile) }) +router.get("/badges", async (req, res) => { + const badges = await userService.getUserBadges({ userId: req.user.id }) + return res.status(200).json(badges) +}) + module.exports = router \ No newline at end of file diff --git a/routes/api/v1/user/stations.js b/routes/api/v1/user/stations.js new file mode 100644 index 0000000..a8b777e --- /dev/null +++ b/routes/api/v1/user/stations.js @@ -0,0 +1,16 @@ +const express = require("express") +const userService = require("../../../../services/userService") +const router = express.Router() + +router.get("/", async (req, res) => { + const stationCheckinsCount = await userService.getUserUniqueStations({ userId: req.user.id }) + return res.status(200).json(stationCheckinsCount) +}) + +router.get("/:stationId/checkins/count", async (req, res) => { + const { stationId } = req.params + const stationCheckinsCount = await userService.getUserStationCheckinsCount({ stationId, userId: req.user.id }) + return res.status(200).json(stationCheckinsCount) +}) + +module.exports = router \ No newline at end of file diff --git a/services/adminService.js b/services/adminService.js index 55e69bb..0be8bc7 100644 --- a/services/adminService.js +++ b/services/adminService.js @@ -1,5 +1,7 @@ const { DefaultError } = require("../errors/errors") const banRepo = require("../repositories/banRepo") +const badgeRepo = require("../repositories/badgeRepository") +const userBadgeRepo = require("../repositories/userBadgeRepository") async function banUser(userId, message, expiresAt) { const existingBan = await banRepo.isBanned(userId) @@ -31,8 +33,91 @@ async function isUserBanned(userId) { return true } +async function createBadge({ name, description, icon, condition }) { + const existing = await badgeRepo.findAllBadges() + if (existing.some(b => b.name === name)) { + throw new DefaultError(409, "Badge already exists", "Duplicate badge name") + } + return await badgeRepo.createBadge(name, description, icon, condition) +} + +async function getAllBadges() { + return await badgeRepo.findAllBadges() +} + +async function getBadgeById({ badgeId }) { + const badge = await badgeRepo.findBadgeById(badgeId) + if (!badge) { + throw new DefaultError(404, "Badge not found", "Badge ID does not exist") + } + return badge +} + +async function updateBadge({ badgeId, updateData }) { + const badge = await badgeRepo.findBadgeById(badgeId) + if (!badge) { + throw new DefaultError(404, "Badge not found", "Badge ID does not exist") + } + + const allowedFields = ["name", "description", "icon", "condition"] + const filteredData = Object.keys(updateData) + .filter(key => allowedFields.includes(key)) + .reduce((obj, key) => { + obj[key] = updateData[key] + return obj + }, {}) + + if (Object.keys(filteredData).length === 0) { + throw new DefaultError(400, "No valid fields to update", "Invalid update data") + } + + return await badgeRepo.updateBadge(badgeId, filteredData) +} + +async function deleteBadge({ badgeId }) { + const badge = await badgeRepo.findBadgeById(badgeId) + if (!badge) { + throw new DefaultError(404, "Badge not found", "Badge ID does not exist") + } + return await badgeRepo.deleteBadge(badgeId) +} + +async function assignBadgeToUser({ userId, badgeId }) { + const badge = await badgeRepo.findBadgeById(badgeId) + if (!badge) { + throw new DefaultError(404, "Badge not found", "Badge ID does not exist") + } + + const hasAlready = await userBadgeRepo.hasBadge(userId, badgeId) + if (hasAlready) { + throw new DefaultError(409, "User already has this badge", "Duplicate badge assignment") + } + + return await userBadgeRepo.assignBadgeToUser(userId, badgeId) +} + +async function removeBadgeFromUser({ userId, badgeId }) { + const badge = await badgeRepo.findBadgeById(badgeId) + if (!badge) { + throw new DefaultError(404, "Badge not found", "Badge ID does not exist") + } + + const success = await userBadgeRepo.removeBadgeFromUser(userId, badgeId) + if (!success) { + throw new DefaultError(404, "Badge not assigned to user", "User does not have this badge") + } + return success +} + module.exports = { banUser, unbanUser, - isUserBanned + isUserBanned, + createBadge, + getAllBadges, + getBadgeById, + updateBadge, + deleteBadge, + assignBadgeToUser, + removeBadgeFromUser } \ No newline at end of file diff --git a/services/authService.js b/services/authService.js index afb60c2..b24562c 100644 --- a/services/authService.js +++ b/services/authService.js @@ -111,6 +111,13 @@ async function isLogged(req, res, next) { next() } +function hasAdminRights(req, res, next) { + if (req.user?.role == "admin") { + return next() + } + throw new DefaultError(403, "Forbidden", "You must be administrator.") +} + module.exports = { isLogged, loginLocal, @@ -118,5 +125,6 @@ module.exports = { resetPassword, registerLocal, changePassword, + hasAdminRights, refreshAccessToken, } \ No newline at end of file diff --git a/services/userService.js b/services/userService.js index ba72481..b22dfdb 100644 --- a/services/userService.js +++ b/services/userService.js @@ -1,4 +1,6 @@ const userRepo = require("../repositories/usersRepo") +const userBadgeRepo = require("../repositories/userBadgeRepository") +const checkinRepo = require("../repositories/checkinRepository") const cache = require("../modules/cache") async function getProfile({ userId }) { @@ -30,11 +32,53 @@ async function getShadowUser({ serverId, remoteId }) { return await userRepo.findByServerAndRemoteId(serverId, remoteId) } +async function getUserBadges({ userId }) { + return await userBadgeRepo.findUserBadges(userId) +} + +async function countUserBadges({ userId }) { + return await userBadgeRepo.countUserBadges(userId) +} + +async function hasBadge({ userId, badgeId }) { + return await userBadgeRepo.hasBadge(userId, badgeId) +} + +async function createCheckin({ userId, stationId, platformId, latitude, longitude, distance }) { + const checkinId = await checkinRepo.createCheckin(userId, stationId, platformId, latitude, longitude, distance) + cache.users.delete(userId) + return checkinId +} + +async function getUserCheckins({ userId, limit = 50, offset = 0 }) { + return await checkinRepo.findCheckinsByUserId(userId, limit, offset) +} + +async function getUserCheckinsCount({ userId }) { + return await checkinRepo.countCheckinsByUser(userId) +} + +async function getUserUniqueStations({ userId }) { + return await checkinRepo.findUniqueStationsByUser(userId) +} + +async function getUserStationCheckinsCount({ userId, stationId }) { + return await checkinRepo.countCheckinsByUserAndStation(userId, stationId) +} + module.exports = { isLocal, getProfile, createUser, updateProfile, deleteAccount, - getShadowUser + getShadowUser, + getUserBadges, + countUserBadges, + hasBadge, + createCheckin, + getUserCheckins, + getUserCheckinsCount, + getUserUniqueStations, + getUserStationCheckinsCount } \ No newline at end of file