2 Commits

3 changed files with 301 additions and 106 deletions

View File

@@ -1,121 +1,283 @@
# Backend API docs
# Borrow System Backend API
If you want to coorperate with me, or build something new with my backend API, feel free to reach out!
Base URL: `http://localhost:8002`
On this page you will learn how my API works.
- App server: [backend/server.js](backend/server.js)
- Auth/JWT: [`authenticate`](backend/services/tokenService.js), [`generateToken`](backend/services/tokenService.js)
- App API (JWT): [backend/routes/api.js](backend/routes/api.js)
- Admin API (key): [backend/routes/apiV2.js](backend/routes/apiV2.js)
- DB layer: [backend/services/database.js](backend/services/database.js)
- Schema: [backend/scheme.sql](backend/scheme.sql)
## General information
## Authentication
When you look at my backend folder and file structure, yu can see that I have two files called `API`. The first file called `api.js` is for my web frontend. Because this file works together with my JWT token service.
Most endpoints under `/api` require a Bearer JWT.
But I have build a second API. You can see the second API file in the same directory, the file is called `apiV2.js`.
1. Login to get a token
This is the file that you can use to build an API.
- POST /api/login
- Body: `{ "username": string, "password": string }`
- Response 200: `{ "message": "Login successful", "token": string }`
- Response 401: `{ "message": "Invalid credentials" }`
But first you have to get the Admin API key, stored in an .env file on my server.
### Current endpoints
- /apiV2/items/`secretKey`
#### /apiV2/items/
When you call this API you will get like this result back:
Example:
```sh
curl -s -X POST http://localhost:8002/api/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"password1"}'
```
2. Use the token for all protected endpoints
- Header: `Authorization: Bearer <token>`
The middleware [`authenticate`](backend/services/tokenService.js) verifies tokens and attaches `req.user = { username, role }`.
Environment:
- SECRET_KEY: HMAC secret for JWT
- DB_HOST, DB_USER, DB_PASSWORD, DB_NAME: MySQL connection
- ADMIN_ID: Admin API key for /apiV2
## Data model (items)
Items (as returned by endpoints) have:
- `id`: number
- `item_name`: string
- `can_borrow_role`: number (minimum role required)
- `inSafe`: 0/1 (not in locker / in locker)
See the full schema in [backend/scheme.sql](backend/scheme.sql).
---
## App API (JWT) — /api
All routes below require `Authorization: Bearer <token>` unless noted.
### GET /api/items
Returns items filtered by the user role:
- role == 0: all items
- role > 0: items with `can_borrow_role >= role`
Implements: [`getItemsFromDatabase`](backend/services/database.js)
Response 200:
```json
[
{ "id": 1, "item_name": "Laptop", "can_borrow_role": 1, "inSafe": 1 },
...
]
```
Example:
```sh
curl -s http://localhost:8002/api/items \
-H "Authorization: Bearer $TOKEN"
```
### GET /api/loans
Returns all loans.
Implements: [`getLoansFromDatabase`](backend/services/database.js)
Response 200:
```json
[
{
"id": 1,
"item_name": "DJI 1er Mikro",
"can_borrow_role": "4",
"inSafe": 1
},
{
"id": 2,
"item_name": "DJI 2er Mikro 1",
"can_borrow_role": "4",
"inSafe": 1
},
{
"id": 3,
"item_name": "DJI 2er Mikro 2",
"can_borrow_role": "4",
"inSafe": 1
},
{
"id": 5,
"item_name": "Rode Richt Mikrofon",
"can_borrow_role": "2",
"inSafe": 1
},
{
"id": 6,
"item_name": "Kamera Stativ",
"can_borrow_role": "1",
"inSafe": 1
},
{
"id": 7,
"item_name": "SONY Kamera - inkl. Akkus und Objektiv",
"can_borrow_role": "1",
"inSafe": 1
},
{
"id": 8,
"item_name": "MacBook inkl. Adapter",
"can_borrow_role": "2",
"inSafe": 1
},
{
"id": 9,
"item_name": "SD Karten",
"can_borrow_role": "3",
"inSafe": 1
},
{
"id": 10,
"item_name": "Kameragimbal",
"can_borrow_role": "1",
"inSafe": 1
},
{
"id": 11,
"item_name": "ATEM MINI PRO",
"can_borrow_role": "1",
"inSafe": 1
},
{
"id": 12,
"item_name": "Handygimbal",
"can_borrow_role": "4",
"inSafe": 1
},
{
"id": 13,
"item_name": "Kameralüfter",
"can_borrow_role": "1",
"inSafe": 1
},
{
"id": 14,
"item_name": "Kleine Kamera 1 - inkl. Objektiv",
"can_borrow_role": "2",
"inSafe": 1
},
{
"id": 15,
"item_name": "Kleine Kamera 2 - inkl. Objektiv",
"can_borrow_role": "2",
"inSafe": 1
"username": "alice",
"loan_code": 1001,
"start_date": "2025-08-01T09:00:00.000Z",
"end_date": "2025-08-10T09:00:00.000Z",
"returned_date": null,
"created_at": "2025-08-01T09:00:00.000Z",
"loaned_items_id": [1, 2],
"loaned_items_name": ["Laptop", "Projector"]
}
]
```
There you can see all items and their details.
### GET /api/userLoans
Each item has the following properties:
Returns loans for the authenticated user.
- `id`: The unique identifier for the item.
- `item_name`: The name of the item.
- `can_borrow_role`: The role ID that is allowed to borrow the item.
- `inSafe`: Indicates whether the item is currently in the locker (1) or not (0).
Implements: [`getUserLoansFromDatabase`](backend/services/database.js)
Response 200:
- On success: `Loan[]`
- If none found: `"No loans found for this user"` (string)
Tip: Treat a non-array response as “no loans”.
### DELETE /api/deleteLoan/:id
Deletes a loan by numeric ID.
Implements: [`deleteLoanFromDatabase`](backend/services/database.js)
- 200: `{ "message": "Loan deleted successfully" }`
- 500: `{ "message": "Failed to delete loan" }`
Example:
```sh
curl -s -X DELETE http://localhost:8002/api/deleteLoan/42 \
-H "Authorization: Bearer $TOKEN"
```
### POST /api/borrowableItems
Returns items available in the given time range (excludes items with overlapping loans). Also enforces role filtering.
Implements: [`getBorrowableItemsFromDatabase`](backend/services/database.js)
Request body:
```json
{ "startDate": "2025-08-01T09:00:00Z", "endDate": "2025-08-02T09:00:00Z" }
```
- 200: `Item[]`
- 400: `{ "message": "startDate and endDate are required" }`
- 500: `{ "message": "Failed to fetch borrowable items" }`
Example:
```sh
curl -s -X POST http://localhost:8002/api/borrowableItems \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"startDate":"2025-08-01T09:00:00Z","endDate":"2025-08-02T09:00:00Z"}'
```
### POST /api/createLoan
Creates a loan for the authenticated user.
Implements: [`createLoanInDatabase`](backend/services/database.js)
Request body:
```json
{
"items": [1, 2, 3], // array of item IDs (required)
"startDate": "2025-08-01T09:00:00Z", // required
"endDate": "2025-08-02T09:00:00Z" // required
}
```
Notes:
- IDs are coerced to numbers; invalid entries are dropped.
- Date range must be valid and `startDate < endDate`.
- Overlaps with existing loans cause 409 Conflict.
- On success, returns the generated `loanCode`.
Responses:
- 201:
```json
{
"message": "Loan created successfully",
"loanId": 123,
"loanCode": 1007
}
```
- 400: `{ "message": "Items array is required" | "No valid item IDs provided" | "Invalid date range" | ... }`
- 409: `{ "message": "Items not available in the selected period" }`
- 500: `{ "message": "Failed to create loan" }`
Example:
```sh
curl -s -X POST http://localhost:8002/api/createLoan \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"items":[1,2],"startDate":"2025-08-01T09:00:00Z","endDate":"2025-08-02T09:00:00Z"}'
```
---
## Admin API — /apiV2
These endpoints are protected by a static admin key in the path. Set `ADMIN_ID` in environment. No JWT required.
### GET /apiV2/items/:key
Returns all items (no role filtering).
Implements: [`getItemsFromDatabaseV2`](backend/services/database.js)
- 200: `Item[]`
- 403: `{ "message": "Access denied" }`
Example:
```sh
curl -s http://localhost:8002/apiV2/items/$ADMIN_ID
```
### POST /apiV2/controlInSafe/:key/:itemId/:state
Updates `inSafe` state (0 or 1) for an item by ID.
Implements: [`changeInSafeStateV2`](backend/services/database.js)
- `state`: `"0"` or `"1"`
- 200: `{ "message": "Item state updated successfully" }`
- 400: `{ "message": "Invalid state value" }`
- 403: `{ "message": "Access denied" }`
- 500: `{ "message": "Failed to update item state" }`
Example:
```sh
curl -s -X POST http://localhost:8002/apiV2/controlInSafe/$ADMIN_ID/5/0
```
---
## Error handling summary
- 400 Bad Request: invalid payloads or missing fields
- 401 Unauthorized: missing/invalid JWT (for `/api` routes)
- 403 Forbidden: wrong admin key (for `/apiV2` routes)
- 409 Conflict: loan overlaps with selected period
- 500 Internal Server Error: unexpected server/database errors
---
## Running locally
With Docker Compose: [docker-compose.yml](docker-compose.yml)
- Backend: http://localhost:8002 (mounted from [backend](backend))
- MySQL: root password from `.env` as `DB_PASSWORD`, port 3309 on host
- Seed schema/data: import [backend/scheme.sql](backend/scheme.sql) into the DB
Environment required by backend:
- `DB_HOST`, `DB_USER`, `DB_PASSWORD`, `DB_NAME`
- `SECRET_KEY`
- `ADMIN_ID`
---
References:
- App routes: [backend/routes/api.js](backend/routes/api.js)
- [`loginFunc`](backend/services/database.js), [`getItemsFromDatabase`](backend/services/database.js), [`getLoansFromDatabase`](backend/services/database.js), [`getUserLoansFromDatabase`](backend/services/database.js), [`deleteLoanFromDatabase`](backend/services/database.js), [`getBorrowableItemsFromDatabase`](backend/services/database.js), [`createLoanInDatabase`](backend/services/database.js)
- Admin routes: [backend/routes/apiV2.js](backend/routes/apiV2.js)
- Auth: [`authenticate`](backend/services/tokenService.js),

View File

@@ -1,12 +1,15 @@
import express from "express";
import dotenv from "dotenv";
import { getItemsFromDatabaseV2 } from "../services/database.js";
import {
getItemsFromDatabaseV2,
changeInSafeStateV2,
} from "../services/database.js";
dotenv.config();
const router = express.Router();
router.get("/items/:id", async (req, res) => {
if (req.params.id === process.env.ADMIN_ID) {
router.get("/items/:key", async (req, res) => {
if (req.params.key === process.env.ADMIN_ID) {
const result = await getItemsFromDatabaseV2();
if (result.success) {
res.status(200).json(result.data);
@@ -18,4 +21,23 @@ router.get("/items/:id", async (req, res) => {
}
});
router.post("/controlInSafe/:key/:itemId/:state", async (req, res) => {
if (req.params.key === process.env.ADMIN_ID) {
const itemId = req.params.itemId;
const state = req.params.state;
if (state === "1" || state === "0") {
const result = await changeInSafeStateV2(itemId, state);
if (result.success) {
res.status(200).json({ message: "Item state updated successfully" });
} else {
res.status(500).json({ message: "Failed to update item state" });
}
} else {
res.status(400).json({ message: "Invalid state value" });
}
} else {
res.status(403).json({ message: "Access denied" });
}
});
export default router;

View File

@@ -28,6 +28,17 @@ export const getItemsFromDatabaseV2 = async () => {
return { success: false };
};
export const changeInSafeStateV2 = async (itemId, state) => {
const [result] = await pool.query(
"UPDATE items SET inSafe = ? WHERE id = ?",
[state, itemId]
);
if (result.affectedRows > 0) {
return { success: true };
}
return { success: false };
};
export const getItemsFromDatabase = async (role) => {
const sql =
role == 0