diff --git a/packages/auth/src/tenancy/tenancy.js b/packages/auth/src/tenancy/tenancy.js index ebd573496c..127ee96c6e 100644 --- a/packages/auth/src/tenancy/tenancy.js +++ b/packages/auth/src/tenancy/tenancy.js @@ -73,6 +73,54 @@ exports.tryAddTenant = async (tenantId, userId, email) => { await Promise.all(promises) } +const DocumentTypes = { + USER: "us", +} +const UNICODE_MAX = "\ufff0" + +/** + * Gets parameters for retrieving users. + * Duplicate of "../db/utils" due to circular dependency + */ +const getGlobalUserParams = (globalId, otherProps = {}) => { + if (!globalId) { + globalId = "" + } + return { + ...otherProps, + startkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}`, + endkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}${UNICODE_MAX}`, + } +} + +exports.deleteTenant = async tenantId => { + const globalDb = exports.getGlobalDB() + + let promises = [] + // remove the tenant entry from global info + const infoDb = getDB(PLATFORM_INFO_DB) + let tenants = await infoDb.get(TENANT_DOC) + tenants.tenantIds = tenants.tenantIds.filter(id => id !== tenantId) + promises.push(infoDb.put(tenants)) + + // remove the users + const allUsers = await globalDb.allDocs( + getGlobalUserParams(null, { + include_docs: true, + }) + ) + allUsers.rows.map(row => { + promises.push(infoDb.remove(row.id, row.value.rev)) + promises.push(infoDb.remove(row.doc.email, row.value.rev)) + }) + + // remove the global db + promises.push(globalDb.destroy()) + + await Promise.all(promises) + // TODO: Delete all apps +} + exports.getGlobalDB = (tenantId = null) => { // tenant ID can be set externally, for example user API where // new tenants are being created, this may be the case diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index 12db55efdc..75c3e9b492 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -546,7 +546,7 @@ module External { }, meta: { table, - } + }, } // can't really use response right now const response = await makeExternalQuery(appId, json) diff --git a/packages/server/src/definitions/datasource.ts b/packages/server/src/definitions/datasource.ts index d7d4e77961..2daef8eda7 100644 --- a/packages/server/src/definitions/datasource.ts +++ b/packages/server/src/definitions/datasource.ts @@ -1,4 +1,4 @@ -import {Table} from "./common"; +import { Table } from "./common" export enum Operation { CREATE = "CREATE", @@ -139,7 +139,7 @@ export interface QueryJson { paginate?: PaginationJson body?: object meta?: { - table?: Table, + table?: Table } extra?: { idFilter?: SearchFilters diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index 91af3e1a85..c5e9bdb0bb 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -148,7 +148,7 @@ function buildRead(knex: Knex, json: QueryJson, limit: number): KnexQuery { if (!resource) { resource = { fields: [] } } - let selectStatement: string|string[] = "*" + let selectStatement: string | string[] = "*" // handle select if (resource.fields && resource.fields.length > 0) { // select the resources as the format "table.columnName" - this is what is provided diff --git a/packages/server/src/integrations/mysql.ts b/packages/server/src/integrations/mysql.ts index 11220afb46..c17cca0745 100644 --- a/packages/server/src/integrations/mysql.ts +++ b/packages/server/src/integrations/mysql.ts @@ -12,7 +12,11 @@ import { getSqlQuery } from "./utils" module MySQLModule { const mysql = require("mysql") const Sql = require("./base/sql") - const { buildExternalTableId, convertType, copyExistingPropsOver } = require("./utils") + const { + buildExternalTableId, + convertType, + copyExistingPropsOver, + } = require("./utils") const { FieldTypes } = require("../constants") interface MySQLConfig { @@ -104,7 +108,7 @@ module MySQLModule { client: any, query: SqlQuery, connect: boolean = true - ): Promise { + ): Promise { // Node MySQL is callback based, so we must wrap our call in a promise return new Promise((resolve, reject) => { if (connect) { @@ -248,9 +252,9 @@ module MySQLModule { json.extra = { idFilter: { equal: { - [primaryKey]: results.insertId + [primaryKey]: results.insertId, }, - } + }, } return json } diff --git a/packages/server/src/integrations/postgres.ts b/packages/server/src/integrations/postgres.ts index e06e3936c8..db81e183d1 100644 --- a/packages/server/src/integrations/postgres.ts +++ b/packages/server/src/integrations/postgres.ts @@ -12,7 +12,11 @@ module PostgresModule { const { Pool } = require("pg") const Sql = require("./base/sql") const { FieldTypes } = require("../constants") - const { buildExternalTableId, convertType, copyExistingPropsOver } = require("./utils") + const { + buildExternalTableId, + convertType, + copyExistingPropsOver, + } = require("./utils") const { escapeDangerousCharacters } = require("../utilities") const JSON_REGEX = /'{.*}'::json/s @@ -193,10 +197,16 @@ module PostgresModule { } const type: string = convertType(column.data_type, TYPE_MAP) - const identity = !!(column.identity_generation || column.identity_start || column.identity_increment) - const hasDefault = typeof column.column_default === "string" && + const identity = !!( + column.identity_generation || + column.identity_start || + column.identity_increment + ) + const hasDefault = + typeof column.column_default === "string" && column.column_default.startsWith("nextval") - const isGenerated = column.is_generated && column.is_generated !== "NEVER" + const isGenerated = + column.is_generated && column.is_generated !== "NEVER" const isAuto: boolean = hasDefault || identity || isGenerated tables[tableName].schema[columnName] = { autocolumn: isAuto, diff --git a/packages/server/src/integrations/utils.ts b/packages/server/src/integrations/utils.ts index 82c35bc2d9..6e3dc6f684 100644 --- a/packages/server/src/integrations/utils.ts +++ b/packages/server/src/integrations/utils.ts @@ -84,7 +84,11 @@ export function isIsoDateString(str: string) { } // add the existing relationships from the entities if they exist, to prevent them from being overridden -export function copyExistingPropsOver(tableName: string, tables: { [key: string]: any }, entities: { [key: string]: any }) { +export function copyExistingPropsOver( + tableName: string, + tables: { [key: string]: any }, + entities: { [key: string]: any } +) { if (entities && entities[tableName]) { if (entities[tableName].primaryDisplay) { tables[tableName].primaryDisplay = entities[tableName].primaryDisplay diff --git a/packages/worker/src/api/controllers/system/tenants.js b/packages/worker/src/api/controllers/system/tenants.js index e053216dd9..34f2ed9664 100644 --- a/packages/worker/src/api/controllers/system/tenants.js +++ b/packages/worker/src/api/controllers/system/tenants.js @@ -1,5 +1,6 @@ const CouchDB = require("../../../db") const { StaticDatabases } = require("@budibase/auth/db") +const { deleteTenant, getTenantId } = require("@budibase/auth/tenancy") exports.exists = async ctx => { const tenantId = ctx.request.params @@ -31,3 +32,13 @@ exports.fetch = async ctx => { } ctx.body = tenants } + +exports.delete = async ctx => { + const tenantId = getTenantId() + + if (ctx.params.tenantId !== tenantId) { + ctx.throw(403, "Unauthorized") + } + + await deleteTenant(tenantId) +} diff --git a/packages/worker/src/api/routes/system/tenants.js b/packages/worker/src/api/routes/system/tenants.js index 223ba9f26e..49c7509a67 100644 --- a/packages/worker/src/api/routes/system/tenants.js +++ b/packages/worker/src/api/routes/system/tenants.js @@ -7,5 +7,6 @@ const router = Router() router .get("/api/system/tenants/:tenantId/exists", controller.exists) .get("/api/system/tenants", adminOnly, controller.fetch) + .delete("/api/system/tenants/:tenantId", adminOnly, controller.delete) module.exports = router