commit 6d35b93d9be48cfc6f58357d29b4096fe546c302 Author: WubzyGD Date: Thu Jan 19 02:13:20 2023 -0500 api user routes and auth setup diff --git a/api/ani/index.js b/api/ani/index.js new file mode 100644 index 0000000..240b7eb --- /dev/null +++ b/api/ani/index.js @@ -0,0 +1,5 @@ +module.exports = app => { + app.get('/ani', (req, res) => res.send("This is the Natsuki Anime Database API! We're currently at v1, so direct your queries to /ani/v1/.")); + + require('./v1/index')(app); +}; \ No newline at end of file diff --git a/api/ani/v1/index.js b/api/ani/v1/index.js new file mode 100644 index 0000000..ae63774 --- /dev/null +++ b/api/ani/v1/index.js @@ -0,0 +1,3 @@ +module.exports = app => { + app.use('/ani/v1', require('./router')(app)); +}; \ No newline at end of file diff --git a/api/ani/v1/router.js b/api/ani/v1/router.js new file mode 100644 index 0000000..7738460 --- /dev/null +++ b/api/ani/v1/router.js @@ -0,0 +1,15 @@ +const fs = require('fs'); +const {Router} = require("express"); + +const router = Router(); + +module.exports = app => { + router + .use((req, res, next) => {console.log(`[ANI/v1] ${req.path} | [${req.method}]`); next()}) + .get('/', (req, res) => res.send("This is the Natsuki Anime DB API v1 head.")) + + fs.readdirSync('./ani/v1/routes').filter(file => file.endsWith('.js')) + .forEach(route => require(`./routes/${route}`)(app, router)); //execute all router functions + + return router; +}; \ No newline at end of file diff --git a/api/ani/v1/routes/user.js b/api/ani/v1/routes/user.js new file mode 100644 index 0000000..ffecac3 --- /dev/null +++ b/api/ani/v1/routes/user.js @@ -0,0 +1,77 @@ +const {hashSync, compareSync} = require('bcrypt'); +const {sign} = require('jsonwebtoken'); + +module.exports = (app, router) => { + const Users = app.db.models.ani.users; + + router.get('/user', (req, res) => res.send("/user: /user/:id required.")); + + router.route('/user/:id') + .get(async (req, res) => { + if (!req.params.id) {return res.status(400).send("Missing ID!");} + const user = await Users.findOne({id: req.params.id.toLowerCase()}); + if (!user) {return res.status(404).send("That user doesn't exist!");} + return res.json({name: user.name, discord: user.discord, id: user.id, permissions: user.permissions}); + }) + .post(async (req, res) => { //TODO validate Discord ID + try { + if (!req.params.id) {return res.status(400).send("Missing ID!");} + if (await Users.findOne({id: req.params.id.toLowerCase()})) {return res.status(400).send("That user already exists!");} + if (!req.body) {return res.status(400).send("Missing body!");} + if ( + !req.body.name || !req.body.discord || !req.body.permissions || !Array.isArray(req.body.permissions) + || !req.body.name.match(/^[\w_ ]+$/gm) || req.body.name.length > 20 + || !req.params.id.toLowerCase().match(/^[\w_]+$/gm) || req.params.id.toLowerCase().length > 15 + || !req.body.password || req.body.password.length > 30 + ) {return res.status(400).send("Malformed body or missing body data. Make sure you have all the required parameters, and you don't have illegal characters present.");} + + const newUser = new Users({ + id: req.params.id.toLowerCase(), + name: req.body.name, + permissions: req.body.permissions, + discord: req.body.discord, + password: hashSync(req.body.password, 8) + }); + return newUser.save() + .then(() => res.json({ + message: "Successfully added user.", + name: newUser.name, + discord: newUser.discord, + id: newUser.id, + permissions: newUser.permissions, + accessToken: sign({id: newUser.id}, app.auth.jwt_secret, {expiresIn: "15d"}) + })) + .catch(e => {console.error("Error trying to add new user", e); res.status(500).send("Something went wrong.");}); + } + catch (e) {console.error("Error trying to add new user", e); res.status(500).send("Something went wrong.");} + }) + .put(app.auth.token, async (req, res) => { + + }); + + router.route('/user/:id/auth') + .post(async (req, res) => { + if (!req.params.id) {return res.status(400).send("Missing ID!");} + const user = await Users.findOne({id: req.params.id.toLowerCase()}); + if (!user) {return res.status(404).send("That user doesn't exist!");} + if (!req.body.password || !compareSync(req.body.password, user.password)) {return res.status(401).json({accessToken: null, message: "Invalid or missing password!"});} + return res.json({ + message: "Successfully authenticated.", + name: user.name, + discord: user.discord, + id: user.id, + permissions: user.permissions, + accessToken: sign({id: user.id}, app.auth.jwt_secret, {expiresIn: "15d"}) + }); + }) + .get(app.auth.token, (req, res) => { + if (!req.user) {return res.status(401).send("You have not been authenticated, and will not be able to access any sensitive routes.");} + return res.json({ + message: "You are authenticated, and your token is valid.", + name: req.user.name, + discord: req.user.discord, + id: req.user.id, + permissions: req.user.permissions + }); + }); +}; \ No newline at end of file diff --git a/api/baseAuthorize.js b/api/baseAuthorize.js new file mode 100644 index 0000000..aedf69b --- /dev/null +++ b/api/baseAuthorize.js @@ -0,0 +1,17 @@ +const {verify} = require("jsonwebtoken"); + +module.exports = app => { + const Users = app.db.models.ani.users + return (req, res, next) => { + if (req.headers && req.headers.authorization && req.headers.authorization.split(" ")[0] === "JWT") { + verify(req.headers.authorization.split(' ')[1], app.auth.jwt_secret, (e, d) => { + if (e) {req.user = undefined; return next();} + Users.findOne({id: d.id}) + .exec((err, user) => { + if (err) {res.status(500).send("Something went trying to authorize you!");} + else {req.user = user; next();} + }); + }); + } + }; +}; \ No newline at end of file diff --git a/api/index.js b/api/index.js new file mode 100644 index 0000000..86c9ca6 --- /dev/null +++ b/api/index.js @@ -0,0 +1,41 @@ +const express = require('express'); +const bodyParser = require('body-parser'); +const cors = require('cors'); +const helmet = require('helmet'); +const {set, createConnection} = require('mongoose'); + +const app = express(); +set('strictQuery', false); + +app.use(helmet()); +app.use(bodyParser.json()); +app.use(cors()); +app.use(express.urlencoded({extended: true})); + +app.get('/', (req, res) => res.send("You've reached the Natsuki API! This 200 status indicates we're online, and currently on v1. Natsuki bot-related queries live at /v1/, ani database queries live at /ani/v1/.")); + +app.auth = {}; +app.auth.jwt_secret = require('../auth.json').jwt_secret; + +let server; +const anidb = createConnection(`mongodb://127.0.0.1:27017/natsuki-anime-api`, { + useNewUrlParser: true, dbName: 'natsuki-anime-api', useUnifiedTopology: true +}); +const botdb = createConnection(`mongodb://127.0.0.1:27017/natsuki-api`, { + useNewUrlParser: true, dbName: 'natsuki-api', useUnifiedTopology: true +}); +console.log("Connected to Mongo Databases"); +server = app.listen(4062, async () => { + console.log(`API online at port ${server.address().port}`); + + app.db = {}; //set db connections into app object + app.db.ani = anidb; + app.db.bot = botdb; + + require('../db/build')(app); //place all models in memory to prevent double-compiling + + app.auth.token = require('./baseAuthorize')(app); //jwt token validation + + require('./v1/index')(app); //initialize bot API branch + require('./ani/index')(app); //initialize ani API branch +}); \ No newline at end of file diff --git a/api/v1/index.js b/api/v1/index.js new file mode 100644 index 0000000..f4c4197 --- /dev/null +++ b/api/v1/index.js @@ -0,0 +1,3 @@ +module.exports = (app) => { + app.get('/v1', (req, res) => res.send("This is the Natsuki API v1 head.")); +}; \ No newline at end of file