API: Add experimental Json API

in preparation for external healthchecks and the
admin backend. We define a couple test routes:

- / List all routes
- /ping Replies pong for online-checks
- /stats Get high-level bot statistics
- /most-recent-drop Returns the most recent entry from dropHistories

The last two routes require a valid apikey header.
All routes are prefixed by /api/v1
This commit is contained in:
2023-04-05 12:00:46 +02:00
parent fdf5a4074b
commit f1e01f2a9f
6 changed files with 1181 additions and 0 deletions

View File

@@ -7,3 +7,5 @@ DB_PASSWORD=
DB_DATABASE= DB_DATABASE=
DB_ROOTPW= DB_ROOTPW=
DB_PORT= DB_PORT=
API_PORT=3080
API_ACCESS_TOKEN=

89
api/jsonApi.js Normal file
View File

@@ -0,0 +1,89 @@
require("dotenv").config();
const express = require('express');
const bodyParser = require('body-parser');
const { Card, User, DropHistory, Character, Group } = require("../models");
const { Op } = require('sequelize');
const ACCESS_TOKEN = process.env.API_ACCESS_TOKEN;
const app = express();
const router = express.Router();
const PREFIX = '/api/v1';
app.use(bodyParser.json());
function isAuthorized(req, res) {
const providedToken = req.headers['apikey'];
if (providedToken !== ACCESS_TOKEN) {
res.status(401).json({ error: 'Unauthorized' });
return false;
}
return true;
}
router.get('/', (req, res) => {
const routes = router.stack
.filter(layer => layer.route) // Filter out non-routes
.map(layer => {
return {
route: PREFIX + layer.route.path,
methods: layer.route.methods
};
});
res.json({ routes: routes });
});
router.get('/ping', (req, res) => {
res.json({ status: 'Pong' });
});
router.get('/stats', async (req, res) => {
if(!isAuthorized(req, res)){return;}
res.json({
users: await User.count(),
cards: await Card.count({where: { burned: { [Op.eq]: false }}}),
burned: await Card.count({where: { burned: { [Op.eq]: true }}}),
drops: await DropHistory.count(),
groups: await Group.count({where: { enabled: { [Op.eq]: true }}}),
characters: await Character.count({where: { enabled: { [Op.eq]: true }}}),
uptime: app.client.uptime
});
});
router.get('/most-recent-drop', async (req, res) => {
if(!isAuthorized(req, res)){return;}
try {
const mostRecentDrop = await DropHistory.findOne({
order: [['createdAt', 'DESC']]
});
if (!mostRecentDrop) {
return res.status(404).send('No drops found');
}
const dropData = JSON.parse(mostRecentDrop.dropData);
const cards = await Promise.all(Object.keys(dropData).map(async key => {
const cardData = dropData[key]?.cardData;
if(!cardData) {return};
const card = JSON.parse(cardData);
return { identifier: card.identifier, quality: card.quality, character: await Character.findByPk(card.characterId) };
}));
let response = { dropper: await User.findByPk(dropData.dropper), cards: cards.filter(Boolean)};
res.json(response);
} catch (error) {
console.error(error);
res.status(500).send('Error fetching most recent drop');
}
}).needsAuth= true;
app.use(PREFIX, router);
module.exports = app;

View File

@@ -10,6 +10,8 @@ services:
depends_on: depends_on:
- "mysql" - "mysql"
working_dir: /app working_dir: /app
ports:
- 127.0.0.1:${API_PORT}:${API_PORT}
volumes: volumes:
- ./:/app - ./:/app
- /usr/share/fonts/:/usr/share/fonts/ - /usr/share/fonts/:/usr/share/fonts/

View File

@@ -2,6 +2,7 @@ require("dotenv").config();
const { Console } = require("console"); const { Console } = require("console");
const fs = require("fs"); const fs = require("fs");
const {Client, GatewayIntentBits, Collection} = require("discord.js"); const {Client, GatewayIntentBits, Collection} = require("discord.js");
const webApi = require('./api/jsonApi');
const dbUtil = require("./util/db") const dbUtil = require("./util/db")
const logger = new Console({ const logger = new Console({
@@ -44,6 +45,11 @@ logger.log("Syncing database...");
dbUtil.syncDb(); dbUtil.syncDb();
client.login(process.env.TOKEN); client.login(process.env.TOKEN);
webApi.client = client;
const PORT = process.env.API_PORT;
webApi.listen(PORT, () => {
console.log(`HTTP API listening on port ${PORT}`);
});

1080
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,9 +13,11 @@
"dependencies": { "dependencies": {
"@discordjs/rest": "^0.3.0", "@discordjs/rest": "^0.3.0",
"axios": "^0.27.2", "axios": "^0.27.2",
"body-parser": "^1.20.2",
"discord-api-types": "^0.37.2", "discord-api-types": "^0.37.2",
"discord.js": "^14.0.0", "discord.js": "^14.0.0",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"express": "^4.18.2",
"mysql2": "^2.3.3", "mysql2": "^2.3.3",
"nanoid": "^3.0.0", "nanoid": "^3.0.0",
"nodemon": "^2.0.15", "nodemon": "^2.0.15",