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:
@@ -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
89
api/jsonApi.js
Normal 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;
|
||||||
@@ -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/
|
||||||
|
|||||||
6
index.js
6
index.js
@@ -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
1080
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user