generated from azures04/Base-REST-API
Add badges, checkins, and transport endpoints
Introduce gamification and check-in support plus expanded transport/admin APIs. Added DDL for checkins, badges and user_badges. New repositories: badgeRepository, userBadgeRepository, checkinRepository. New routes: admin badge management, admin transport (lines/platforms/stations), public transport endpoints, user checkins and stations endpoints, and user badges on /me. Services updated: adminService (badge CRUD & user assignment), userService (checkins, badge queries), authService (hasAdminRights). Minor package.json name update. These changes implement database, repo, service and route wiring for badges, user_badins and checkin features and additional transport admin routes.
This commit is contained in:
@@ -1,21 +1,2 @@
|
|||||||
# Base-REST-API
|
# TCJourney Server
|
||||||
|
TCJourney (TCJ) est un jeu de collection de transports. Ce dépôt contient le code de son backend
|
||||||
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
|
|
||||||
```
|
|
||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -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)
|
||||||
|
);
|
||||||
@@ -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
|
||||||
|
);
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "base-rest-api",
|
"name": "tcjourney/server",
|
||||||
"version": "0.0.1-alpha",
|
"version": "0.0.1-alpha",
|
||||||
"description": "",
|
"description": "",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -19,4 +19,9 @@ router.patch("/displayname", async (req, res) => {
|
|||||||
return res.status(200).json(updatedProfile)
|
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
|
module.exports = router
|
||||||
@@ -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
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
const { DefaultError } = require("../errors/errors")
|
const { DefaultError } = require("../errors/errors")
|
||||||
const banRepo = require("../repositories/banRepo")
|
const banRepo = require("../repositories/banRepo")
|
||||||
|
const badgeRepo = require("../repositories/badgeRepository")
|
||||||
|
const userBadgeRepo = require("../repositories/userBadgeRepository")
|
||||||
|
|
||||||
async function banUser(userId, message, expiresAt) {
|
async function banUser(userId, message, expiresAt) {
|
||||||
const existingBan = await banRepo.isBanned(userId)
|
const existingBan = await banRepo.isBanned(userId)
|
||||||
@@ -31,8 +33,91 @@ async function isUserBanned(userId) {
|
|||||||
return true
|
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 = {
|
module.exports = {
|
||||||
banUser,
|
banUser,
|
||||||
unbanUser,
|
unbanUser,
|
||||||
isUserBanned
|
isUserBanned,
|
||||||
|
createBadge,
|
||||||
|
getAllBadges,
|
||||||
|
getBadgeById,
|
||||||
|
updateBadge,
|
||||||
|
deleteBadge,
|
||||||
|
assignBadgeToUser,
|
||||||
|
removeBadgeFromUser
|
||||||
}
|
}
|
||||||
@@ -111,6 +111,13 @@ async function isLogged(req, res, next) {
|
|||||||
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 = {
|
module.exports = {
|
||||||
isLogged,
|
isLogged,
|
||||||
loginLocal,
|
loginLocal,
|
||||||
@@ -118,5 +125,6 @@ module.exports = {
|
|||||||
resetPassword,
|
resetPassword,
|
||||||
registerLocal,
|
registerLocal,
|
||||||
changePassword,
|
changePassword,
|
||||||
|
hasAdminRights,
|
||||||
refreshAccessToken,
|
refreshAccessToken,
|
||||||
}
|
}
|
||||||
+45
-1
@@ -1,4 +1,6 @@
|
|||||||
const userRepo = require("../repositories/usersRepo")
|
const userRepo = require("../repositories/usersRepo")
|
||||||
|
const userBadgeRepo = require("../repositories/userBadgeRepository")
|
||||||
|
const checkinRepo = require("../repositories/checkinRepository")
|
||||||
const cache = require("../modules/cache")
|
const cache = require("../modules/cache")
|
||||||
|
|
||||||
async function getProfile({ userId }) {
|
async function getProfile({ userId }) {
|
||||||
@@ -30,11 +32,53 @@ async function getShadowUser({ serverId, remoteId }) {
|
|||||||
return await userRepo.findByServerAndRemoteId(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 = {
|
module.exports = {
|
||||||
isLocal,
|
isLocal,
|
||||||
getProfile,
|
getProfile,
|
||||||
createUser,
|
createUser,
|
||||||
updateProfile,
|
updateProfile,
|
||||||
deleteAccount,
|
deleteAccount,
|
||||||
getShadowUser
|
getShadowUser,
|
||||||
|
getUserBadges,
|
||||||
|
countUserBadges,
|
||||||
|
hasBadge,
|
||||||
|
createCheckin,
|
||||||
|
getUserCheckins,
|
||||||
|
getUserCheckinsCount,
|
||||||
|
getUserUniqueStations,
|
||||||
|
getUserStationCheckinsCount
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user