mirror of
https://github.com/Zandercraft/TechnicFlux.git
synced 2026-10-29 04:39:20 +00:00
Added multi-API-key support and API rate-limiting.
This commit is contained in:
10
.env.example
10
.env.example
@@ -33,3 +33,13 @@ API_KEY=""
|
|||||||
# This is the name that will be pre-filled when you go to upload custom files for a modpack.
|
# This is the name that will be pre-filled when you go to upload custom files for a modpack.
|
||||||
# Unset behaviour: You will need to specify the author every time.
|
# Unset behaviour: You will need to specify the author every time.
|
||||||
AUTHOR_NAME="John Doe"
|
AUTHOR_NAME="John Doe"
|
||||||
|
|
||||||
|
# (Optional) API Rate-limit Requests
|
||||||
|
# The number of requests that can be made to the API by one IP address within the set window before it will block them.
|
||||||
|
# Default: 1000
|
||||||
|
API_RATE_REQUESTS=1000
|
||||||
|
|
||||||
|
# (Optional) API Rate-limit Window
|
||||||
|
# The amount of time (in milliseconds) that the request limit is applied to.
|
||||||
|
# Default: 60000
|
||||||
|
API_RATE_WINDOW=60000
|
||||||
|
|||||||
4
app.js
4
app.js
@@ -15,7 +15,7 @@ const storage = multer.diskStorage({
|
|||||||
cb(null, uniquePrefix + '-' + file.originalname)
|
cb(null, uniquePrefix + '-' + file.originalname)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const upload = multer({ storage: storage })
|
const upload = multer({ storage })
|
||||||
|
|
||||||
// --- Routers ---
|
// --- Routers ---
|
||||||
const indexRouter = require('./routes/index')
|
const indexRouter = require('./routes/index')
|
||||||
@@ -49,7 +49,7 @@ app.use(function (err, req, res, next) {
|
|||||||
|
|
||||||
// render the error page
|
// render the error page
|
||||||
res.status(err.status || 500)
|
res.status(err.status || 500)
|
||||||
res.render('error', {title: `Error ${err.status}: ${err.message}`})
|
res.render('error', { title: `Error ${err.status}: ${err.message}` })
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports.app = app
|
module.exports.app = app
|
||||||
|
|||||||
101
database.js
101
database.js
@@ -16,6 +16,13 @@ const userSchema = new Schema({
|
|||||||
login_history: [{ timestamp: Date, userAgent: String }]
|
login_history: [{ timestamp: Date, userAgent: String }]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const apiKeySchema = new Schema({
|
||||||
|
owner: { type: SchemaTypes.ObjectId, ref: 'technicflux_users' },
|
||||||
|
key: String,
|
||||||
|
name: String,
|
||||||
|
created_at: Date
|
||||||
|
})
|
||||||
|
|
||||||
const modpackSchema = new Schema({
|
const modpackSchema = new Schema({
|
||||||
name: String,
|
name: String,
|
||||||
display_name: String,
|
display_name: String,
|
||||||
@@ -55,6 +62,7 @@ const buildSchema = new Schema({
|
|||||||
|
|
||||||
// --- Models ---
|
// --- Models ---
|
||||||
const User = mongoose.model('technicflux_users', userSchema)
|
const User = mongoose.model('technicflux_users', userSchema)
|
||||||
|
const APIKey = mongoose.model('technicflux_keys', apiKeySchema)
|
||||||
const Modpack = mongoose.model('technicflux_modpacks', modpackSchema)
|
const Modpack = mongoose.model('technicflux_modpacks', modpackSchema)
|
||||||
const Mod = mongoose.model('technicflux_mods', modSchema)
|
const Mod = mongoose.model('technicflux_mods', modSchema)
|
||||||
const Build = mongoose.model('technicflux_builds', buildSchema)
|
const Build = mongoose.model('technicflux_builds', buildSchema)
|
||||||
@@ -119,7 +127,7 @@ exports.getUserById = (uId) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Gets the user with the provided email.
|
* Gets the user with the provided username.
|
||||||
*/
|
*/
|
||||||
exports.getUserByUsername = (uUsername) => {
|
exports.getUserByUsername = (uUsername) => {
|
||||||
// Fetch information about the user with the given username
|
// Fetch information about the user with the given username
|
||||||
@@ -155,7 +163,7 @@ exports.authUser = (uUsername, uPassword, logHistory = false, uAgent = 'Unknown'
|
|||||||
// Fetch information about the user with the given username
|
// Fetch information about the user with the given username
|
||||||
return User.findOne({ username: uUsername }).exec().then((user) => {
|
return User.findOne({ username: uUsername }).exec().then((user) => {
|
||||||
// User found. Check password.
|
// User found. Check password.
|
||||||
return bcrypt.compare(uPassword, user.password).then((isMatch) => {
|
return bcrypt.compare(uPassword, user.password).then(async (isMatch) => {
|
||||||
// Check if it is a match
|
// Check if it is a match
|
||||||
if (isMatch) {
|
if (isMatch) {
|
||||||
// Update the user's login history.
|
// Update the user's login history.
|
||||||
@@ -163,7 +171,7 @@ exports.authUser = (uUsername, uPassword, logHistory = false, uAgent = 'Unknown'
|
|||||||
const newLogin = { timestamp: new Date(), userAgent: String(uAgent) }
|
const newLogin = { timestamp: new Date(), userAgent: String(uAgent) }
|
||||||
|
|
||||||
// Add the new login to the user's login history
|
// Add the new login to the user's login history
|
||||||
return User.updateOne(
|
return await User.updateOne(
|
||||||
{ username: uUsername },
|
{ username: uUsername },
|
||||||
{ $push: { login_history: newLogin } }
|
{ $push: { login_history: newLogin } }
|
||||||
).exec().then(() => {
|
).exec().then(() => {
|
||||||
@@ -229,6 +237,91 @@ exports.updateUser = (uUsername, object) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- API-Key-Related Functions
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Creates a new api key with the given information.
|
||||||
|
*/
|
||||||
|
exports.createAPIKey = (kOwner, kKey, kName) => {
|
||||||
|
// Encrypt the password using bcrypt and save user
|
||||||
|
return bcrypt.genSalt(10).then((Salt) => {
|
||||||
|
return bcrypt.hash(kKey, Salt).then((hash) => {
|
||||||
|
// Create an APIKey object
|
||||||
|
const newKey = new APIKey({
|
||||||
|
owner: kOwner,
|
||||||
|
key: hash,
|
||||||
|
name: kName,
|
||||||
|
created_at: new Date()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Commit it to the database
|
||||||
|
return newKey.save().then((key) => {
|
||||||
|
process.stdout.write(`Successfully added new API key '${kName}' for ${kOwner}\n`)
|
||||||
|
return key
|
||||||
|
}).catch((reason) => {
|
||||||
|
process.stdout.write(`ERROR (when saving new API key): ${reason}\n`)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}).catch((err) => {
|
||||||
|
// Issue creating hashed api key
|
||||||
|
process.stdout.write(`ERROR (while hashing key): ${err}`)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}).catch((err) => {
|
||||||
|
// Issue creating hashed object salt
|
||||||
|
process.stdout.write(`ERROR (while generating api key salt): ${err}`)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Checks if the corresponding password matches that of the provided username.
|
||||||
|
*/
|
||||||
|
exports.authAPIKey = (uKey) => {
|
||||||
|
// Fetch information about the user with the given username
|
||||||
|
return APIKey.find({ }).exec().then(async (keys) => {
|
||||||
|
// Check if any of the keys match.
|
||||||
|
let valid = [false, undefined]
|
||||||
|
for (const key of keys) {
|
||||||
|
await bcrypt.compare(uKey, key.key).then((isMatch) => {
|
||||||
|
// Check if it is a match
|
||||||
|
if (isMatch) {
|
||||||
|
// The key matched.
|
||||||
|
valid = [true, key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return valid
|
||||||
|
}).catch((reason) => {
|
||||||
|
console.log(reason)
|
||||||
|
// DB error
|
||||||
|
debug('ERROR (DB): Could not fetch API keys.')
|
||||||
|
return [false, undefined]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getAPIKeysByOwner = (mOwner) => {
|
||||||
|
return APIKey.find({ owner: mOwner }).populate('owner').exec().then((keys) => {
|
||||||
|
// API Keys found.
|
||||||
|
return keys
|
||||||
|
}).catch((reason) => {
|
||||||
|
// No API keys found for this owner.
|
||||||
|
debug(`ERROR (DB): Failed to find any API keys with this owner because of: ${reason}`)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.deleteAPIKey = (kId) => {
|
||||||
|
return APIKey.deleteOne({ _id: kId }).exec().then(() => {
|
||||||
|
// Successfully deleted this api key
|
||||||
|
return true
|
||||||
|
}).catch((reason) => {
|
||||||
|
// Failed to delete the api key
|
||||||
|
debug(`ERROR (DB): Failed to delete API Key '${kId}' because of: ${reason}`)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// --- Modpack-Related Functions ---
|
// --- Modpack-Related Functions ---
|
||||||
exports.createModpack = (mSlug, mDisplayName, mOwner) => {
|
exports.createModpack = (mSlug, mDisplayName, mOwner) => {
|
||||||
// Create a modpack object
|
// Create a modpack object
|
||||||
@@ -335,7 +428,7 @@ exports.getModBySlug = (mSlug) => {
|
|||||||
author: mods[0].author,
|
author: mods[0].author,
|
||||||
description: mods[0].description,
|
description: mods[0].description,
|
||||||
link: mods[0].link,
|
link: mods[0].link,
|
||||||
versions: mods.map((modVersion) => modVersion['version'])
|
versions: mods.map((modVersion) => modVersion.version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).catch((reason) => {
|
}).catch((reason) => {
|
||||||
|
|||||||
115
package-lock.json
generated
115
package-lock.json
generated
@@ -22,6 +22,7 @@
|
|||||||
"mongoose": "^7.5.3",
|
"mongoose": "^7.5.3",
|
||||||
"morgan": "~1.9.1",
|
"morgan": "~1.9.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"rate-limit-mongo": "^2.3.2",
|
||||||
"standard": "^17.1.0"
|
"standard": "^17.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -439,6 +440,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
||||||
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
|
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/bl": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==",
|
||||||
|
"dependencies": {
|
||||||
|
"readable-stream": "^2.3.5",
|
||||||
|
"safe-buffer": "^5.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.1",
|
"version": "1.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||||
@@ -801,6 +811,14 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/denque": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||||
@@ -3077,6 +3095,17 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/optional-require": {
|
||||||
|
"version": "1.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.8.tgz",
|
||||||
|
"integrity": "sha512-jq83qaUb0wNg9Krv1c5OQ+58EK+vHde6aBPzLvPPqJm89UQWsvSuFy9X/OSNJnFeSOKo7btE0n8Nl2+nE+z5nA==",
|
||||||
|
"dependencies": {
|
||||||
|
"require-at": "^1.0.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.3",
|
"version": "0.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
|
||||||
@@ -3367,6 +3396,62 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rate-limit-mongo": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rate-limit-mongo/-/rate-limit-mongo-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-dLck0j5N/AX9ycVHn5lX9Ti2Wrrwi1LfbXitu/mMBZOo2nC26RgYKJVbcb2mYgb9VMaPI2IwJVzIa2hAQrMaDA==",
|
||||||
|
"dependencies": {
|
||||||
|
"mongodb": "^3.6.7",
|
||||||
|
"twostep": "0.4.2",
|
||||||
|
"underscore": "1.12.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rate-limit-mongo/node_modules/bson": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6.19"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rate-limit-mongo/node_modules/mongodb": {
|
||||||
|
"version": "3.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.4.tgz",
|
||||||
|
"integrity": "sha512-K5q8aBqEXMwWdVNh94UQTwZ6BejVbFhh1uB6c5FKtPE9eUMZPUO3sRZdgIEcHSrAWmxzpG/FeODDKL388sqRmw==",
|
||||||
|
"dependencies": {
|
||||||
|
"bl": "^2.2.1",
|
||||||
|
"bson": "^1.1.4",
|
||||||
|
"denque": "^1.4.1",
|
||||||
|
"optional-require": "^1.1.8",
|
||||||
|
"safe-buffer": "^5.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"saslprep": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"aws4": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"bson-ext": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"kerberos": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"mongodb-client-encryption": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"mongodb-extjson": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"snappy": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/raw-body": {
|
"node_modules/raw-body": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
||||||
@@ -3492,6 +3577,14 @@
|
|||||||
"url": "https://github.com/sponsors/mysticatea"
|
"url": "https://github.com/sponsors/mysticatea"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-at": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-at/-/require-at-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.6",
|
"version": "1.22.6",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz",
|
||||||
@@ -3609,6 +3702,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/saslprep": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"sparse-bitfield": "^3.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
@@ -4105,6 +4210,11 @@
|
|||||||
"strip-bom": "^3.0.0"
|
"strip-bom": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/twostep": {
|
||||||
|
"version": "0.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/twostep/-/twostep-0.4.2.tgz",
|
||||||
|
"integrity": "sha512-O/wdPYk9ey04qcCiw8AQN74DbvLFZLAgnryrNTpV7T/sxB4lcGkCMHynx5xCcA6fCh739ZAqp3HcGhy770X1qA=="
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
@@ -4231,6 +4341,11 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/underscore": {
|
||||||
|
"version": "1.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz",
|
||||||
|
"integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw=="
|
||||||
|
},
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
|||||||
@@ -32,7 +32,9 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "set DEBUG=technicflux:* && node ./bin/www"
|
"start": "set DEBUG=technicflux:* && node ./bin/www",
|
||||||
|
"lint": "standard",
|
||||||
|
"lint-and-fix": "standard --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zip.js/zip.js": "^2.7.29",
|
"@zip.js/zip.js": "^2.7.29",
|
||||||
@@ -48,6 +50,7 @@
|
|||||||
"mongoose": "^7.5.3",
|
"mongoose": "^7.5.3",
|
||||||
"morgan": "~1.9.1",
|
"morgan": "~1.9.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"rate-limit-mongo": "^2.3.2",
|
||||||
"standard": "^17.1.0"
|
"standard": "^17.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
158
routes/api.js
158
routes/api.js
@@ -1,9 +1,44 @@
|
|||||||
const express = require('express')
|
const express = require('express')
|
||||||
const pckg = require('../package.json')
|
const rateLimit = require('express-rate-limit')
|
||||||
const branchName = require('current-git-branch')() || "release"
|
const MongoStore = require('rate-limit-mongo')
|
||||||
const database = require('../database')
|
const database = require('../database')
|
||||||
|
const branchName = require('current-git-branch')() || 'release'
|
||||||
|
const pckg = require('../package.json')
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
|
// --- Rate Limiter ---
|
||||||
|
const maxRequests = process.env.API_RATE_REQUESTS || 1000
|
||||||
|
const rateWindow = process.env.API_RATE_WINDOW || 60000
|
||||||
|
|
||||||
|
const limiter = rateLimit({
|
||||||
|
store: new MongoStore({
|
||||||
|
uri: process.env.MONGODB_CONN_STRING,
|
||||||
|
collectionName: 'technicflux_ratelimit',
|
||||||
|
expireTimeMs: rateLimit,
|
||||||
|
errorHandler: console.error.bind(null, 'rate-limit-mongo')
|
||||||
|
}),
|
||||||
|
max: maxRequests,
|
||||||
|
windowMs: rateWindow,
|
||||||
|
legacyHeaders: true,
|
||||||
|
message: { error: 'Too many requests from this IP. You are being rate-limited.' }
|
||||||
|
})
|
||||||
|
router.use(limiter)
|
||||||
|
|
||||||
|
// --- Module Error Functions ---
|
||||||
|
|
||||||
|
function error500 (res) {
|
||||||
|
return res.status(500).json({
|
||||||
|
error: 'Internal Server Error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- API Routes ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base API Route : /api/
|
||||||
|
* Possible Statuses: 200
|
||||||
|
* Purpose: Displays information about the TechnicFlux Solder API.
|
||||||
|
*/
|
||||||
router.get('/', (req, res) => {
|
router.get('/', (req, res) => {
|
||||||
return res.json({
|
return res.json({
|
||||||
api: pckg.prettyName,
|
api: pckg.prettyName,
|
||||||
@@ -12,27 +47,32 @@ router.get('/', (req, res) => {
|
|||||||
meta: {
|
meta: {
|
||||||
description: "TechnicFlux implementation of Technic's Solder API for retrieval of modpack and mod info.",
|
description: "TechnicFlux implementation of Technic's Solder API for retrieval of modpack and mod info.",
|
||||||
license: `https://github.com/Zandercraft/TechnicFlux/blob/${branchName}/LICENSE.txt`,
|
license: `https://github.com/Zandercraft/TechnicFlux/blob/${branchName}/LICENSE.txt`,
|
||||||
repo: "https://github.com/Zandercraft/TechnicFlux",
|
repo: 'https://github.com/Zandercraft/TechnicFlux',
|
||||||
documentation: "https://github.com/Zandercraft/TechnicFlux/wiki",
|
documentation: 'https://github.com/Zandercraft/TechnicFlux/wiki',
|
||||||
attribution: {
|
attribution: {
|
||||||
name: "Zandercraft",
|
name: 'Zandercraft',
|
||||||
github: "https://github.com/Zandercraft",
|
github: 'https://github.com/Zandercraft',
|
||||||
website: "https://zandercraft.ca",
|
website: 'https://zandercraft.ca'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mod API Route : /api/mod/:modname/:modversion?
|
||||||
|
* Possible Statuses: 200, 404, 500
|
||||||
|
* Purpose: Displays information about a mod or a specific version of that mod.
|
||||||
|
*/
|
||||||
router.get('/mod/:modname/:modversion?', (req, res) => {
|
router.get('/mod/:modname/:modversion?', (req, res) => {
|
||||||
let modName = req.params.modname
|
const modName = req.params.modname
|
||||||
let modVersion = req.params?.modversion
|
const modVersion = req.params?.modversion
|
||||||
|
|
||||||
return database.getModBySlug(`${modName}`).then((mod) => {
|
return database.getModBySlug(`${modName}`).then((mod) => {
|
||||||
// Check if a mod was found
|
// Check if a mod was found
|
||||||
if (mod === null) {
|
if (mod === null) {
|
||||||
// Mod Not Found
|
// Mod Not Found
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
"error": "Mod does not exist"
|
error: 'Mod does not exist'
|
||||||
})
|
})
|
||||||
} else if (modVersion === undefined) {
|
} else if (modVersion === undefined) {
|
||||||
// Request is for the mod's info.
|
// Request is for the mod's info.
|
||||||
@@ -43,7 +83,7 @@ router.get('/mod/:modname/:modversion?', (req, res) => {
|
|||||||
if (version === null) {
|
if (version === null) {
|
||||||
// Version Not Found
|
// Version Not Found
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
"error": "Mod version does not exist"
|
error: 'Mod version does not exist'
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Found mod version
|
// Found mod version
|
||||||
@@ -56,46 +96,52 @@ router.get('/mod/:modname/:modversion?', (req, res) => {
|
|||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// Issue with db request
|
// Issue with db request
|
||||||
return res.status(500).json({
|
return error500(res)
|
||||||
"error": "Internal Server Error"
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// Issue with db request
|
// Issue with db request
|
||||||
return res.status(500).json({
|
return error500(res)
|
||||||
"error": "Internal Server Error"
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modpack Index API Route : /api/modpack/
|
||||||
|
* Possible Statuses: 200, 500
|
||||||
|
* Purpose: Displays a list of all modpacks that are on this TechnicFlux instance.
|
||||||
|
*/
|
||||||
router.get('/modpack', (req, res) => {
|
router.get('/modpack', (req, res) => {
|
||||||
return database.getAllModpacks().then((modpacks) => {
|
return database.getAllModpacks().then((modpacks) => {
|
||||||
// Modpack Index
|
// Modpack Index
|
||||||
return res.json({
|
return res.json({
|
||||||
modpacks: (modpacks === null) ? {} : modpacks.reduce((obj, item) => {
|
modpacks: (modpacks === null)
|
||||||
obj[item['name']] = String(item['display_name'])
|
? {}
|
||||||
return obj
|
: modpacks.reduce((obj, item) => {
|
||||||
}, {}),
|
obj[item.name] = String(item.display_name)
|
||||||
|
return obj
|
||||||
|
}, {}),
|
||||||
mirror_url: `http://${process.env.HOST}/mods/`
|
mirror_url: `http://${process.env.HOST}/mods/`
|
||||||
})
|
})
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// Issue with db request
|
// Issue with db request
|
||||||
return res.status(500).json({
|
return error500(res)
|
||||||
"error": "Internal Server Error"
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modpack API Route : /api/modpack/:modpack/:build?
|
||||||
|
* Possible Statuses: 200, 404, 500
|
||||||
|
* Purpose: Displays information about a modpack or a specific build of that modpack.
|
||||||
|
*/
|
||||||
router.get('/modpack/:slug/:build?', (req, res) => {
|
router.get('/modpack/:slug/:build?', (req, res) => {
|
||||||
let slug = req.params.slug
|
const slug = req.params.slug
|
||||||
let build = req.params?.build
|
const build = req.params?.build
|
||||||
|
|
||||||
return database.getModpackBySlug(`${slug}`).then((modpack) => {
|
return database.getModpackBySlug(`${slug}`).then((modpack) => {
|
||||||
if (modpack === null) {
|
if (modpack === null) {
|
||||||
// Modpack Not Found
|
// Modpack Not Found
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
"error": "Modpack does not exist"
|
error: 'Modpack does not exist'
|
||||||
})
|
})
|
||||||
} else if (build === undefined) {
|
} else if (build === undefined) {
|
||||||
// Modpack info
|
// Modpack info
|
||||||
@@ -104,7 +150,7 @@ router.get('/modpack/:slug/:build?', (req, res) => {
|
|||||||
display_name: modpack.display_name,
|
display_name: modpack.display_name,
|
||||||
recommended: modpack.recommended,
|
recommended: modpack.recommended,
|
||||||
latest: modpack.latest,
|
latest: modpack.latest,
|
||||||
builds: modpack.builds.map((build) => build['version'])
|
builds: modpack.builds.map((build) => build.version)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Modpack build info
|
// Modpack build info
|
||||||
@@ -133,38 +179,54 @@ router.get('/modpack/:slug/:build?', (req, res) => {
|
|||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// Issue with db request
|
// Issue with db request
|
||||||
return res.status(500).json({
|
return error500(res)
|
||||||
"error": "Internal Server Error"
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Key Verification Route : /api/verify/:key?
|
||||||
|
* Possible Statuses: 200, 400, 403
|
||||||
|
* Purpose: Checks if the given API key is valid with this instance of TechnicFlux
|
||||||
|
*/
|
||||||
router.get('/verify/:key?', (req, res) => {
|
router.get('/verify/:key?', (req, res) => {
|
||||||
let api_key = req.params?.key
|
const apiKey = req.params?.key
|
||||||
|
|
||||||
if (api_key !== undefined) {
|
if (apiKey !== undefined) {
|
||||||
if (api_key === process.env?.API_KEY)
|
database.authAPIKey(apiKey).then((key) => {
|
||||||
// Key validated
|
if (apiKey === process.env?.API_KEY) {
|
||||||
return res.json({
|
// Key validated
|
||||||
valid: 'Key validated.',
|
return res.json({
|
||||||
name: 'API KEY',
|
valid: 'Key validated.',
|
||||||
created_at: 'A long time ago'
|
name: 'API KEY',
|
||||||
})
|
created_at: 'A long time ago'
|
||||||
else {
|
})
|
||||||
// Invalid key provided
|
} else if (key[0] === true) {
|
||||||
return res.json({
|
// Key validated
|
||||||
error: 'Invalid key provided.'
|
return res.json({
|
||||||
})
|
valid: 'Key validated.',
|
||||||
}
|
name: key[1].name,
|
||||||
|
created_at: key[1].created_at
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Invalid key provided
|
||||||
|
return res.status(403).json({
|
||||||
|
error: 'Invalid key provided.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// No key provided
|
// No key provided
|
||||||
return res.json({
|
return res.status(400).json({
|
||||||
error: 'No API key provided.'
|
error: 'No API key provided.'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/* Invalid API Routes */
|
/**
|
||||||
|
* Invalid API Routes : /api/<invalid route>
|
||||||
|
* Possible Statuses: 405
|
||||||
|
* Purpose: Displays an error when an invalid route is called.
|
||||||
|
*/
|
||||||
router.all('*', (req, res) => {
|
router.all('*', (req, res) => {
|
||||||
return res.status(405).json({
|
return res.status(405).json({
|
||||||
code: res.statusCode,
|
code: res.statusCode,
|
||||||
|
|||||||
Reference in New Issue
Block a user