Enforce licensed developer count + error types/codes framework
This commit is contained in:
parent
515ade6bd3
commit
3b9303a1fb
|
@ -30,6 +30,7 @@ const UNICODE_MAX = "\ufff0"
|
|||
|
||||
exports.ViewNames = {
|
||||
USER_BY_EMAIL: "by_email",
|
||||
USER_BY_BUILDERS: "by_builders",
|
||||
}
|
||||
|
||||
exports.StaticDatabases = StaticDatabases
|
||||
|
|
|
@ -31,3 +31,25 @@ exports.createUserEmailView = async db => {
|
|||
}
|
||||
await db.put(designDoc)
|
||||
}
|
||||
|
||||
exports.createUserBuildersView = async db => {
|
||||
let designDoc
|
||||
try {
|
||||
designDoc = await db.get("_design/database")
|
||||
} catch (err) {
|
||||
// no design doc, make one
|
||||
designDoc = DesignDoc()
|
||||
}
|
||||
const view = {
|
||||
map: `function(doc) {
|
||||
if (doc.builder && doc.builder.global === true) {
|
||||
emit(doc._id, doc._id)
|
||||
}
|
||||
}`,
|
||||
}
|
||||
designDoc.views = {
|
||||
...designDoc.views,
|
||||
[ViewNames.USER_BY_BUILDERS]: view,
|
||||
}
|
||||
await db.put(designDoc)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
class BudibaseError extends Error {
|
||||
constructor(message, type, code) {
|
||||
super(message)
|
||||
this.type = type
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BudibaseError,
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
const licensing = require("./licensing")
|
||||
|
||||
const codes = {
|
||||
...licensing.codes,
|
||||
}
|
||||
|
||||
const types = {
|
||||
...licensing.types,
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
codes,
|
||||
types,
|
||||
UsageLimitError: licensing.UsageLimitError,
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
const { BudibaseError } = require("./base")
|
||||
|
||||
const types = {
|
||||
LICENSE_ERROR: "license_error",
|
||||
}
|
||||
|
||||
const codes = {
|
||||
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
|
||||
}
|
||||
|
||||
class UsageLimitError extends BudibaseError {
|
||||
constructor(message, limitName) {
|
||||
super(message, types.LICENSE_ERROR, codes.USAGE_LIMIT_EXCEEDED)
|
||||
this.limitName = limitName
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
types,
|
||||
codes,
|
||||
UsageLimitError,
|
||||
}
|
|
@ -16,4 +16,5 @@ module.exports = {
|
|||
constants: require("../constants"),
|
||||
migrations: require("../migrations"),
|
||||
licensing: require("./licensing"),
|
||||
errors: require("./errors"),
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ exports.getLicense = async (tenantId, opts = { populateLicense: null }) => {
|
|||
return license
|
||||
}
|
||||
|
||||
exports.invalidateLicense = async tenantId => {
|
||||
exports.invalidate = async tenantId => {
|
||||
const client = await redis.getClient()
|
||||
await client.delete(tenantId)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
const middleware = require("./middleware")
|
||||
const cache = require("./cache")
|
||||
const usage = require("./usage")
|
||||
|
||||
module.exports = {
|
||||
middleware,
|
||||
cache,
|
||||
usage,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
const { getTenantId } = require("../../context")
|
||||
const cache = require("../cache")
|
||||
const env = require("../../environment")
|
||||
const utils = require("../../utils")
|
||||
const { UsageLimitError } = require("../../errors")
|
||||
|
||||
const UsageType = {
|
||||
MONTHLY: "monthly",
|
||||
STATIC: "static",
|
||||
}
|
||||
|
||||
const StaticUsageLimits = {
|
||||
MAX_DEVELOPERS: "maxDevelopers",
|
||||
QUERY_TIMEOUT_SECONDS: "queryTimeoutSeconds",
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const MonthlyUsageLimits = {
|
||||
MAX_QUERY_COUNT: "maxQueryCount",
|
||||
}
|
||||
|
||||
exports.checkMaxDevelopers = async () => {
|
||||
const developerCount = await utils.getBuildersCount()
|
||||
await checkUsageLimit(
|
||||
developerCount,
|
||||
UsageType.STATIC,
|
||||
StaticUsageLimits.MAX_DEVELOPERS
|
||||
)
|
||||
}
|
||||
|
||||
const checkUsageLimit = async (currentUsage, usageType, usageLimit) => {
|
||||
const tenantId = getTenantId()
|
||||
const license = await cache.getLicense(tenantId)
|
||||
const limit = license.usageLimits[usageType][usageLimit]
|
||||
if (currentUsage >= limit.value) {
|
||||
throw new UsageLimitError(
|
||||
`Licensed ${limit.name} has been exceeded. To upgrade, visit ${env.ACCOUNT_PORTAL_URL}/portal/plans`,
|
||||
limit.name
|
||||
)
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ const {
|
|||
} = require("./db/utils")
|
||||
const jwt = require("jsonwebtoken")
|
||||
const { options } = require("./middleware/passport/jwt")
|
||||
const { createUserEmailView } = require("./db/views")
|
||||
const { createUserEmailView, createUserBuildersView } = require("./db/views")
|
||||
const { Headers, UserStatus, Cookies, MAX_VALID_DATE } = require("./constants")
|
||||
const {
|
||||
getGlobalDB,
|
||||
|
@ -20,6 +20,7 @@ const { hash } = require("./hashing")
|
|||
const userCache = require("./cache/user")
|
||||
const env = require("./environment")
|
||||
const { getUserSessions, invalidateSessions } = require("./security/sessions")
|
||||
const { usage } = require("./licensing")
|
||||
|
||||
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
||||
|
||||
|
@ -160,12 +161,33 @@ exports.getGlobalUserByEmail = async email => {
|
|||
}
|
||||
}
|
||||
|
||||
exports.getBuildersCount = async () => {
|
||||
const db = getGlobalDB()
|
||||
try {
|
||||
let users = await db.query(`database/${ViewNames.USER_BY_BUILDERS}`)
|
||||
return users.total_rows
|
||||
} catch (err) {
|
||||
if (err != null && err.name === "not_found") {
|
||||
await createUserBuildersView(db)
|
||||
return exports.getBuildersCount()
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.saveUser = async (
|
||||
user,
|
||||
tenantId,
|
||||
hashPassword = true,
|
||||
requirePassword = true
|
||||
) => {
|
||||
// new user
|
||||
// check license restrictions
|
||||
if (!user._id && user.builder) {
|
||||
await usage.checkMaxDevelopers()
|
||||
}
|
||||
|
||||
if (!tenantId) {
|
||||
throw "No tenancy specified."
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
notifications.success("Invitation accepted successfully")
|
||||
$goto("../auth/login")
|
||||
} catch (error) {
|
||||
notifications.error("Error accepting invitation")
|
||||
notifications.error(error.message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
})
|
||||
notifications.success("Successfully created user")
|
||||
} catch (error) {
|
||||
notifications.error("Error creating user")
|
||||
notifications.error(error.message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
const { createUserBuildersView } = require("@budibase/backend-core/db")
|
||||
|
||||
/**
|
||||
* Date:
|
||||
* March 2022
|
||||
*
|
||||
* Description:
|
||||
* Create the builder users view.
|
||||
*/
|
||||
|
||||
export const run = async (db: any) => {
|
||||
await createUserBuildersView(db)
|
||||
}
|
|
@ -8,6 +8,7 @@ const {
|
|||
import * as userEmailViewCasing from "./functions/userEmailViewCasing"
|
||||
import * as quota1 from "./functions/quotas1"
|
||||
import * as appUrls from "./functions/appUrls"
|
||||
import * as userBuildersView from "./functions/userBuildersView"
|
||||
|
||||
export interface Migration {
|
||||
type: string
|
||||
|
@ -49,6 +50,11 @@ export const MIGRATIONS: Migration[] = [
|
|||
opts: { all: true },
|
||||
fn: appUrls.run,
|
||||
},
|
||||
{
|
||||
type: MIGRATION_TYPES.GLOBAL,
|
||||
name: "user_builders_view",
|
||||
fn: userBuildersView.run,
|
||||
},
|
||||
]
|
||||
|
||||
export const migrate = async (options?: MigrationOptions) => {
|
||||
|
|
|
@ -2494,6 +2494,13 @@
|
|||
"@types/node" "*"
|
||||
safe-buffer "*"
|
||||
|
||||
"@types/redis@^2.8.0":
|
||||
version "2.8.32"
|
||||
resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.32.tgz#1d3430219afbee10f8cfa389dad2571a05ecfb11"
|
||||
integrity sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/serve-static@*":
|
||||
version "1.13.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9"
|
||||
|
|
|
@ -24,6 +24,7 @@ const {
|
|||
const { removeUserFromInfoDB } = require("@budibase/backend-core/deprovision")
|
||||
const env = require("../../../environment")
|
||||
const { syncUserInApps } = require("../../../utilities/appService")
|
||||
const { errors } = require("@budibase/backend-core")
|
||||
|
||||
async function allUsers() {
|
||||
const db = getGlobalDB()
|
||||
|
@ -295,6 +296,10 @@ exports.inviteAccept = async ctx => {
|
|||
info.tenantId
|
||||
)
|
||||
} catch (err) {
|
||||
if (err.code === errors.codes.USAGE_LIMIT_EXCEEDED) {
|
||||
// explicitly re-throw limit exceeded errors
|
||||
ctx.throw(400, err)
|
||||
}
|
||||
ctx.throw(400, "Unable to create new user, invitation invalid.")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ const {
|
|||
buildCsrfMiddleware,
|
||||
} = require("@budibase/backend-core/auth")
|
||||
const { licensing } = require("@budibase/backend-core")
|
||||
const { errors } = require("@budibase/backend-core")
|
||||
|
||||
const PUBLIC_ENDPOINTS = [
|
||||
// old deprecated endpoints kept for backwards compat
|
||||
|
@ -105,16 +106,32 @@ router
|
|||
})
|
||||
.use(auditLog)
|
||||
|
||||
// error handling middleware
|
||||
// error handling middleware - TODO: This could be moved to backend-core
|
||||
router.use(async (ctx, next) => {
|
||||
try {
|
||||
await next()
|
||||
} catch (err) {
|
||||
ctx.log.error(err)
|
||||
ctx.status = err.status || err.statusCode || 500
|
||||
|
||||
let error
|
||||
if (err.code || err.type) {
|
||||
// add generic error information
|
||||
error = {
|
||||
code: err.code,
|
||||
type: err.type,
|
||||
}
|
||||
|
||||
// add specific error information
|
||||
if (error.code === errors.codes.USAGE_LIMIT_EXCEEDED) {
|
||||
error.limitName = err.limitName
|
||||
}
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
message: err.message,
|
||||
status: ctx.status,
|
||||
error,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue