mirror of https://github.com/Budibase/budibase.git
24 changed files with 410 additions and 37 deletions
@ -0,0 +1,55 @@ |
|||||
|
// THIS will create API Keys and App Ids input in a local Dynamo instance if it is running
|
||||
|
const dynamoClient = require("../src/db/dynamoClient") |
||||
|
|
||||
|
if (process.argv[2] == null || process.argv[3] == null) { |
||||
|
console.error( |
||||
|
"Inputs incorrect format, was expecting: node createApiKeyAndAppId.js <API_KEY> <APP_ID>" |
||||
|
) |
||||
|
process.exit(-1) |
||||
|
} |
||||
|
|
||||
|
const FAKE_STRING = "fakestring" |
||||
|
|
||||
|
// set fake credentials for local dynamo to actually work
|
||||
|
process.env.AWS_ACCESS_KEY_ID = "KEY_ID" |
||||
|
process.env.AWS_SECRET_ACCESS_KEY = "SECRET_KEY" |
||||
|
dynamoClient.init("http://localhost:8333") |
||||
|
|
||||
|
async function run() { |
||||
|
await dynamoClient.apiKeyTable.put({ |
||||
|
item: { |
||||
|
pk: process.argv[2], |
||||
|
accountId: FAKE_STRING, |
||||
|
trackingId: FAKE_STRING, |
||||
|
quotaReset: Date.now() + 2592000000, |
||||
|
usageQuota: { |
||||
|
automationRuns: 0, |
||||
|
records: 0, |
||||
|
storage: 0, |
||||
|
users: 0, |
||||
|
views: 0, |
||||
|
}, |
||||
|
usageLimits: { |
||||
|
automationRuns: 10, |
||||
|
records: 10, |
||||
|
storage: 1000, |
||||
|
users: 10, |
||||
|
views: 10, |
||||
|
}, |
||||
|
}, |
||||
|
}) |
||||
|
await dynamoClient.apiKeyTable.put({ |
||||
|
item: { |
||||
|
pk: process.argv[3], |
||||
|
apiKey: process.argv[2], |
||||
|
}, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
run() |
||||
|
.then(() => { |
||||
|
console.log("Records should have been created.") |
||||
|
}) |
||||
|
.catch(err => { |
||||
|
console.error("Cannot create records - " + err) |
||||
|
}) |
||||
@ -0,0 +1,128 @@ |
|||||
|
let _ = require("lodash") |
||||
|
let environment = require("../environment") |
||||
|
|
||||
|
const AWS_REGION = environment.AWS_REGION ? environment.AWS_REGION : "eu-west-1" |
||||
|
|
||||
|
const TableInfo = { |
||||
|
API_KEYS: { |
||||
|
name: "beta-api-key-table", |
||||
|
primary: "pk", |
||||
|
}, |
||||
|
USERS: { |
||||
|
name: "prod-budi-table", |
||||
|
primary: "pk", |
||||
|
sort: "sk", |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
let docClient = null |
||||
|
|
||||
|
class Table { |
||||
|
constructor(tableInfo) { |
||||
|
if (!tableInfo.name || !tableInfo.primary) { |
||||
|
throw "Table info must specify a name and a primary key" |
||||
|
} |
||||
|
this._name = tableInfo.name |
||||
|
this._primary = tableInfo.primary |
||||
|
this._sort = tableInfo.sort |
||||
|
} |
||||
|
|
||||
|
async get({ primary, sort, otherProps }) { |
||||
|
let params = { |
||||
|
TableName: this._name, |
||||
|
Key: { |
||||
|
[this._primary]: primary, |
||||
|
}, |
||||
|
} |
||||
|
if (this._sort && sort) { |
||||
|
params.Key[this._sort] = sort |
||||
|
} |
||||
|
if (otherProps) { |
||||
|
params = _.merge(params, otherProps) |
||||
|
} |
||||
|
let response = await docClient.get(params).promise() |
||||
|
return response.Item |
||||
|
} |
||||
|
|
||||
|
async update({ |
||||
|
primary, |
||||
|
sort, |
||||
|
expression, |
||||
|
condition, |
||||
|
names, |
||||
|
values, |
||||
|
exists, |
||||
|
otherProps, |
||||
|
}) { |
||||
|
let params = { |
||||
|
TableName: this._name, |
||||
|
Key: { |
||||
|
[this._primary]: primary, |
||||
|
}, |
||||
|
ExpressionAttributeNames: names, |
||||
|
ExpressionAttributeValues: values, |
||||
|
UpdateExpression: expression, |
||||
|
} |
||||
|
if (condition) { |
||||
|
params.ConditionExpression = condition |
||||
|
} |
||||
|
if (this._sort && sort) { |
||||
|
params.Key[this._sort] = sort |
||||
|
} |
||||
|
if (exists) { |
||||
|
params.ExpressionAttributeNames["#PRIMARY"] = this._primary |
||||
|
if (params.ConditionExpression) { |
||||
|
params.ConditionExpression += " AND " |
||||
|
} |
||||
|
params.ConditionExpression += "attribute_exists(#PRIMARY)" |
||||
|
} |
||||
|
if (otherProps) { |
||||
|
params = _.merge(params, otherProps) |
||||
|
} |
||||
|
return docClient.update(params).promise() |
||||
|
} |
||||
|
|
||||
|
async put({ item, otherProps }) { |
||||
|
if ( |
||||
|
item[this._primary] == null || |
||||
|
(this._sort && item[this._sort] == null) |
||||
|
) { |
||||
|
throw "Cannot put item without primary and sort key (if required)" |
||||
|
} |
||||
|
let params = { |
||||
|
TableName: this._name, |
||||
|
Item: item, |
||||
|
} |
||||
|
if (otherProps) { |
||||
|
params = _.merge(params, otherProps) |
||||
|
} |
||||
|
return docClient.put(params).promise() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
exports.init = endpoint => { |
||||
|
let AWS = require("aws-sdk") |
||||
|
AWS.config.update({ |
||||
|
region: AWS_REGION, |
||||
|
}) |
||||
|
let docClientParams = { |
||||
|
correctClockSkew: true, |
||||
|
} |
||||
|
if (endpoint) { |
||||
|
docClientParams.endpoint = endpoint |
||||
|
} else if (environment.DYNAMO_ENDPOINT) { |
||||
|
docClientParams.endpoint = environment.DYNAMO_ENDPOINT |
||||
|
} |
||||
|
docClient = new AWS.DynamoDB.DocumentClient(docClientParams) |
||||
|
} |
||||
|
|
||||
|
exports.apiKeyTable = new Table(TableInfo.API_KEYS) |
||||
|
exports.userTable = new Table(TableInfo.USERS) |
||||
|
|
||||
|
if (environment.CLOUD) { |
||||
|
exports.init(`https://dynamodb.${AWS_REGION}.amazonaws.com`) |
||||
|
} else { |
||||
|
process.env.AWS_ACCESS_KEY_ID = "KEY_ID" |
||||
|
process.env.AWS_SECRET_ACCESS_KEY = "SECRET_KEY" |
||||
|
exports.init("http://localhost:8333") |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
const CouchDB = require("../db") |
||||
|
const usageQuota = require("../utilities/usageQuota") |
||||
|
const environment = require("../environment") |
||||
|
|
||||
|
// currently only counting new writes and deletes
|
||||
|
const METHOD_MAP = { |
||||
|
POST: 1, |
||||
|
DELETE: -1, |
||||
|
} |
||||
|
|
||||
|
const DOMAIN_MAP = { |
||||
|
records: usageQuota.Properties.RECORD, |
||||
|
upload: usageQuota.Properties.UPLOAD, |
||||
|
views: usageQuota.Properties.VIEW, |
||||
|
users: usageQuota.Properties.USER, |
||||
|
// this will not be updated by endpoint calls
|
||||
|
// instead it will be updated by triggers
|
||||
|
automationRuns: usageQuota.Properties.AUTOMATION, |
||||
|
} |
||||
|
|
||||
|
function getProperty(url) { |
||||
|
for (let domain of Object.keys(DOMAIN_MAP)) { |
||||
|
if (url.indexOf(domain) !== -1) { |
||||
|
return DOMAIN_MAP[domain] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = async (ctx, next) => { |
||||
|
const db = new CouchDB(ctx.user.instanceId) |
||||
|
let usage = METHOD_MAP[ctx.req.method] |
||||
|
const property = getProperty(ctx.req.url) |
||||
|
if (usage == null || property == null) { |
||||
|
return next() |
||||
|
} |
||||
|
// post request could be a save of a pre-existing entry
|
||||
|
if (ctx.request.body && ctx.request.body._id) { |
||||
|
try { |
||||
|
ctx.preExisting = await db.get(ctx.request.body._id) |
||||
|
return next() |
||||
|
} catch (err) { |
||||
|
ctx.throw(404, `${ctx.request.body._id} does not exist`) |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
// update usage for uploads to be the total size
|
||||
|
if (property === usageQuota.Properties.UPLOAD) { |
||||
|
const files = |
||||
|
ctx.request.files.file.length > 1 |
||||
|
? Array.from(ctx.request.files.file) |
||||
|
: [ctx.request.files.file] |
||||
|
usage = files.map(file => file.size).reduce((total, size) => total + size) |
||||
|
} |
||||
|
if (!environment.CLOUD) { |
||||
|
return next() |
||||
|
} |
||||
|
try { |
||||
|
await usageQuota.update(ctx.apiKey, property, usage) |
||||
|
return next() |
||||
|
} catch (err) { |
||||
|
ctx.throw(403, err) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,73 @@ |
|||||
|
const environment = require("../environment") |
||||
|
const { apiKeyTable } = require("../db/dynamoClient") |
||||
|
|
||||
|
function buildUpdateParams(key, property, usage) { |
||||
|
return { |
||||
|
primary: key, |
||||
|
condition: "#quota.#prop < #limits.#prop AND #quotaReset > :now", |
||||
|
expression: "ADD #quota.#prop :usage", |
||||
|
names: { |
||||
|
"#quota": "usageQuota", |
||||
|
"#prop": property, |
||||
|
"#limits": "usageLimits", |
||||
|
"#quotaReset": "quotaReset", |
||||
|
}, |
||||
|
values: { |
||||
|
":usage": usage, |
||||
|
":now": Date.now(), |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// a normalised month in milliseconds
|
||||
|
const QUOTA_RESET = 2592000000 |
||||
|
|
||||
|
exports.Properties = { |
||||
|
RECORD: "records", |
||||
|
UPLOAD: "storage", |
||||
|
VIEW: "views", |
||||
|
USER: "users", |
||||
|
AUTOMATION: "automationRuns", |
||||
|
} |
||||
|
|
||||
|
exports.getAPIKey = async appId => { |
||||
|
return apiKeyTable.get({ primary: appId }) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Given a specified API key this will add to the usage object for the specified property. |
||||
|
* @param {string} apiKey The API key which is to be updated. |
||||
|
* @param {string} property The property which is to be added to (within the nested usageQuota object). |
||||
|
* @param {number} usage The amount (this can be negative) to adjust the number by. |
||||
|
* @returns {Promise<void>} When this completes the API key will now be up to date - the quota period may have |
||||
|
* also been reset after this call. |
||||
|
*/ |
||||
|
exports.update = async (apiKey, property, usage) => { |
||||
|
// don't try validate in builder
|
||||
|
if (!environment.CLOUD) { |
||||
|
return |
||||
|
} |
||||
|
try { |
||||
|
await apiKeyTable.update(buildUpdateParams(apiKey, property, usage)) |
||||
|
} catch (err) { |
||||
|
if (err.code === "ConditionalCheckFailedException") { |
||||
|
// get the API key so we can check it
|
||||
|
const keyObj = await apiKeyTable.get({ primary: apiKey }) |
||||
|
// we have infact breached the reset period
|
||||
|
if (keyObj && keyObj.quotaReset <= Date.now()) { |
||||
|
// update the quota reset period and reset the values for all properties
|
||||
|
keyObj.quotaReset = Date.now() + QUOTA_RESET |
||||
|
for (let prop of Object.keys(keyObj.usageQuota)) { |
||||
|
if (prop === property) { |
||||
|
keyObj.usageQuota[prop] = usage > 0 ? usage : 0 |
||||
|
} else { |
||||
|
keyObj.usageQuota[prop] = 0 |
||||
|
} |
||||
|
} |
||||
|
await apiKeyTable.put({ item: keyObj }) |
||||
|
return |
||||
|
} |
||||
|
throw "Resource limits have been reached" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue