1
0
Fork 0
mirror of synced 2024-06-22 16:10:40 +12:00

Merge branch 'develop' of github.com:Budibase/budibase into feature/replace-pouch

This commit is contained in:
mike12345567 2022-11-16 18:12:31 +00:00
commit bf86640e9d
110 changed files with 1930 additions and 773 deletions

View file

@ -58,12 +58,15 @@ http {
}
location ~ ^/api/(system|admin|global)/ {
proxy_pass http://worker-service;
proxy_read_timeout 120s;
proxy_connect_timeout 120s;
proxy_send_timeout 120s;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
proxy_pass http://worker-service;
}
location /api/backups/ {
@ -78,60 +81,78 @@ http {
location /api/ {
proxy_read_timeout 120s;
proxy_connect_timeout 120s;
proxy_send_timeout 120s;
proxy_pass http://app-service;
proxy_send_timeout 120s;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
proxy_pass http://app-service;
}
location = / {
proxy_pass http://app-service;
proxy_read_timeout 120s;
proxy_connect_timeout 120s;
proxy_send_timeout 120s;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
proxy_pass http://app-service;
}
location /app_ {
proxy_pass http://app-service;
proxy_read_timeout 120s;
proxy_connect_timeout 120s;
proxy_send_timeout 120s;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
proxy_pass http://app-service;
}
location /app {
proxy_pass http://app-service;
proxy_read_timeout 120s;
proxy_connect_timeout 120s;
proxy_send_timeout 120s;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
proxy_pass http://app-service;
}
location /builder {
proxy_pass http://builder;
proxy_read_timeout 120s;
proxy_connect_timeout 120s;
proxy_send_timeout 120s;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
proxy_pass http://builder;
rewrite ^/builder(.*)$ /builder/$1 break;
}
location /builder/ {
proxy_pass http://builder;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 120s;
proxy_connect_timeout 120s;
proxy_send_timeout 120s;
proxy_pass http://builder;
}
location /vite/ {

View file

@ -100,18 +100,25 @@ http {
location ~ ^/(builder|app_) {
proxy_http_version 1.1;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass http://$apps:4002;
}
location ~ ^/api/(system|admin|global)/ {
proxy_set_header Host $host;
proxy_pass http://$worker:4003;
}
location /worker/ {
proxy_set_header Host $host;
proxy_pass http://$worker:4003;
rewrite ^/worker/(.*)$ /$1 break;
}
@ -139,6 +146,7 @@ http {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass http://$apps:4002;
}
@ -158,6 +166,7 @@ http {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass http://$apps:4002;
}

View file

@ -1,5 +1,5 @@
{
"version": "2.1.22-alpha.3",
"version": "2.1.22-alpha.6",
"npmClient": "yarn",
"packages": [
"packages/*"

View file

@ -75,8 +75,8 @@
"env:multi:disable": "lerna run env:multi:disable",
"env:selfhost:enable": "lerna run env:selfhost:enable",
"env:selfhost:disable": "lerna run env:selfhost:disable",
"env:localdomain:enable": "lerna run env:localdomain:enable",
"env:localdomain:disable": "lerna run env:localdomain:disable",
"env:localdomain:enable": "./scripts/localdomain.sh enable",
"env:localdomain:disable": "./scripts/localdomain.sh disable",
"env:account:enable": "lerna run env:account:enable",
"env:account:disable": "lerna run env:account:disable",
"mode:self": "yarn env:selfhost:enable && yarn env:multi:disable && yarn env:account:disable",

View file

@ -1 +0,0 @@
jest.mock("node-fetch", () => jest.fn())

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "2.1.22-alpha.3",
"version": "2.1.22-alpha.6",
"description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
@ -20,7 +20,7 @@
"test:watch": "jest --watchAll"
},
"dependencies": {
"@budibase/types": "2.1.22-alpha.3",
"@budibase/types": "2.1.22-alpha.6",
"@shopify/jest-koa-mocks": "5.0.1",
"@techpass/passport-openidconnect": "0.3.2",
"aws-sdk": "2.1030.0",
@ -57,7 +57,7 @@
"@types/chance": "1.1.3",
"@types/ioredis": "4.28.0",
"@types/jest": "27.5.1",
"@types/koa": "2.0.52",
"@types/koa": "2.13.4",
"@types/lodash": "4.14.180",
"@types/node": "14.18.20",
"@types/node-fetch": "2.6.1",
@ -69,7 +69,7 @@
"chance": "1.1.3",
"ioredis-mock": "5.8.0",
"jest": "28.1.1",
"koa": "2.7.0",
"koa": "2.13.4",
"nodemon": "2.0.16",
"pouchdb-adapter-memory": "7.2.2",
"timekeeper": "2.2.0",

View file

@ -1,8 +1,12 @@
import { getGlobalUserParams, getAllApps } from "../db/utils"
import { doWithDB, PouchLike } from "../db"
import {
getGlobalUserParams,
getAllApps,
doWithDB,
StaticDatabases,
PouchLike,
} from "../db"
import { doWithGlobalDB } from "../tenancy"
import { StaticDatabases } from "../db/constants"
import { User } from "@budibase/types"
import { App, Tenants, User } from "@budibase/types"
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
@ -10,10 +14,8 @@ const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
async function removeTenantFromInfoDB(tenantId: string) {
try {
await doWithDB(PLATFORM_INFO_DB, async (infoDb: PouchLike) => {
let tenants = await infoDb.get(TENANT_DOC)
tenants.tenantIds = tenants.tenantIds.filter(
(id: string) => id !== tenantId
)
const tenants = (await infoDb.get(TENANT_DOC)) as Tenants
tenants.tenantIds = tenants.tenantIds.filter(id => id !== tenantId)
await infoDb.put(tenants)
})
@ -23,18 +25,35 @@ async function removeTenantFromInfoDB(tenantId: string) {
}
}
export async function removeUserFromInfoDB(dbUser: User) {
await doWithDB(PLATFORM_INFO_DB, async (infoDb: PouchLike) => {
const keys = [dbUser._id!, dbUser.email]
const userDocs = await infoDb.allDocs({
keys,
include_docs: true,
})
const toDelete = userDocs.rows.map((row: any) => {
return {
...row.doc,
_deleted: true,
}
})
await infoDb.bulkDocs(toDelete)
})
}
async function removeUsersFromInfoDB(tenantId: string) {
return doWithGlobalDB(tenantId, async (db: PouchLike) => {
return doWithGlobalDB(tenantId, async (db: any) => {
try {
const allUsers = await db.allDocs(
getGlobalUserParams(null, {
include_docs: true,
})
)
await doWithDB(PLATFORM_INFO_DB, async (infoDb: PouchLike) => {
const allEmails = allUsers.rows.map(row => row.doc.email)
await doWithDB(PLATFORM_INFO_DB, async (infoDb: any) => {
const allEmails = allUsers.rows.map((row: any) => row.doc.email)
// get the id docs
let keys = allUsers.rows.map(row => row.id)
let keys = allUsers.rows.map((row: any) => row.id)
// and the email docs
keys = keys.concat(allEmails)
// retrieve the docs and delete them
@ -42,7 +61,7 @@ async function removeUsersFromInfoDB(tenantId: string) {
keys,
include_docs: true,
})
const toDelete = userDocs.rows.map(row => {
const toDelete = userDocs.rows.map((row: any) => {
return {
...row.doc,
_deleted: true,
@ -70,7 +89,7 @@ async function removeGlobalDB(tenantId: string) {
async function removeTenantApps(tenantId: string) {
try {
const apps = await getAllApps({ all: true })
const apps = (await getAllApps({ all: true })) as App[]
const destroyPromises = apps.map(app =>
doWithDB(app.appId, (db: PouchLike) => db.destroy())
)
@ -81,23 +100,6 @@ async function removeTenantApps(tenantId: string) {
}
}
export async function removeUserFromInfoDB(dbUser: User) {
await doWithDB(PLATFORM_INFO_DB, async (infoDb: PouchLike) => {
const keys = [dbUser._id!, dbUser.email]
const userDocs = await infoDb.allDocs({
keys,
include_docs: true,
})
const toDelete = userDocs.rows.map(row => {
return {
...row.doc,
_deleted: true,
}
})
await infoDb.bulkDocs(toDelete)
})
}
// can't live in tenancy package due to circular dependency on db/utils
export async function deleteTenant(tenantId: string) {
await removeTenantFromInfoDB(tenantId)

View file

@ -16,6 +16,7 @@ import { isDevApp, isDevAppID, getProdAppID } from "./conversions"
import { APP_PREFIX } from "./constants"
import * as events from "../events"
import { PouchLike } from "./couch"
import { App } from "@budibase/types"
export * from "./constants"
export * from "./conversions"
@ -302,7 +303,12 @@ export async function getAllDbs(opts = { efficient: false }) {
*
* @return {Promise<object[]>} returns the app information document stored in each app database.
*/
export async function getAllApps({ dev, all, idsOnly, efficient }: any = {}) {
export async function getAllApps({
dev,
all,
idsOnly,
efficient,
}: any = {}): Promise<App[] | string[]> {
let tenantId = getTenantId()
if (!env.MULTI_TENANCY && !tenantId) {
tenantId = DEFAULT_TENANT_ID
@ -374,18 +380,16 @@ export async function getAllApps({ dev, all, idsOnly, efficient }: any = {}) {
* Utility function for getAllApps but filters to production apps only.
*/
export async function getProdAppIDs() {
return (await getAllApps({ idsOnly: true })).filter(
(id: any) => !isDevAppID(id)
)
const apps = (await getAllApps({ idsOnly: true })) as string[]
return apps.filter((id: any) => !isDevAppID(id))
}
/**
* Utility function for the inverse of above.
*/
export async function getDevAppIDs() {
return (await getAllApps({ idsOnly: true })).filter((id: any) =>
isDevAppID(id)
)
const apps = (await getAllApps({ idsOnly: true })) as string[]
return apps.filter((id: any) => isDevAppID(id))
}
export async function dbExists(dbName: any) {

View file

@ -1,27 +1,34 @@
import { BBContext, EndpointMatcher, RegexMatcher } from "@budibase/types"
const PARAM_REGEX = /\/:(.*?)(\/.*)?$/g
exports.buildMatcherRegex = patterns => {
export const buildMatcherRegex = (
patterns: EndpointMatcher[]
): RegexMatcher[] => {
if (!patterns) {
return []
}
return patterns.map(pattern => {
const isObj = typeof pattern === "object" && pattern.route
const method = isObj ? pattern.method : "GET"
let route = pattern.route
const method = pattern.method
const strict = pattern.strict ? pattern.strict : false
let route = isObj ? pattern.route : pattern
// if there is a param in the route
// use a wildcard pattern
const matches = route.match(PARAM_REGEX)
if (matches) {
for (let match of matches) {
const pattern = "/.*" + (match.endsWith("/") ? "/" : "")
const suffix = match.endsWith("/") ? "/" : ""
const pattern = "/.*" + suffix
route = route.replace(match, pattern)
}
}
return { regex: new RegExp(route), method, strict, route }
})
}
exports.matches = (ctx, options) => {
export const matches = (ctx: BBContext, options: RegexMatcher[]) => {
return options.find(({ regex, method, strict, route }) => {
let urlMatch
if (strict) {

View file

@ -1,52 +0,0 @@
const { doInTenant, isMultiTenant, DEFAULT_TENANT_ID } = require("../tenancy")
const { buildMatcherRegex, matches } = require("./matchers")
const { Header } = require("../constants")
const getTenantID = (ctx, opts = { allowQs: false, allowNoTenant: false }) => {
// exit early if not multi-tenant
if (!isMultiTenant()) {
return DEFAULT_TENANT_ID
}
let tenantId
const allowQs = opts && opts.allowQs
const allowNoTenant = opts && opts.allowNoTenant
const header = ctx.request.headers[Header.TENANT_ID]
const user = ctx.user || {}
if (allowQs) {
const query = ctx.request.query || {}
tenantId = query.tenantId
}
// override query string (if allowed) by user, or header
// URL params cannot be used in a middleware, as they are
// processed later in the chain
tenantId = user.tenantId || header || tenantId
// Set the tenantId from the subdomain
if (!tenantId) {
tenantId = ctx.subdomains && ctx.subdomains[0]
}
if (!tenantId && !allowNoTenant) {
ctx.throw(403, "Tenant id not set")
}
return tenantId
}
module.exports = (
allowQueryStringPatterns,
noTenancyPatterns,
opts = { noTenancyRequired: false }
) => {
const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns)
const noTenancyOptions = buildMatcherRegex(noTenancyPatterns)
return async function (ctx, next) {
const allowNoTenant =
opts.noTenancyRequired || !!matches(ctx, noTenancyOptions)
const allowQs = !!matches(ctx, allowQsOptions)
const tenantId = getTenantID(ctx, { allowQs, allowNoTenant })
return doInTenant(tenantId, next)
}
}

View file

@ -0,0 +1,37 @@
import { doInTenant, getTenantIDFromCtx } from "../tenancy"
import { buildMatcherRegex, matches } from "./matchers"
import { Headers } from "../constants"
import {
BBContext,
EndpointMatcher,
GetTenantIdOptions,
TenantResolutionStrategy,
} from "@budibase/types"
const tenancy = (
allowQueryStringPatterns: EndpointMatcher[],
noTenancyPatterns: EndpointMatcher[],
opts = { noTenancyRequired: false }
) => {
const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns)
const noTenancyOptions = buildMatcherRegex(noTenancyPatterns)
return async function (ctx: BBContext, next: any) {
const allowNoTenant =
opts.noTenancyRequired || !!matches(ctx, noTenancyOptions)
const tenantOpts: GetTenantIdOptions = {
allowNoTenant,
}
const allowQs = !!matches(ctx, allowQsOptions)
if (!allowQs) {
tenantOpts.excludeStrategies = [TenantResolutionStrategy.QUERY]
}
const tenantId = getTenantIDFromCtx(ctx, tenantOpts)
ctx.set(Headers.TENANT_ID, tenantId as string)
return doInTenant(tenantId, next)
}
}
export = tenancy

View file

@ -0,0 +1,134 @@
import * as matchers from "../matchers"
import { structures } from "../../../tests"
describe("matchers", () => {
it("matches by path and method", () => {
const pattern = [
{
route: "/api/tests",
method: "POST",
},
]
const ctx = structures.koa.newContext()
ctx.request.url = "/api/tests"
ctx.request.method = "POST"
const built = matchers.buildMatcherRegex(pattern)
expect(!!matchers.matches(ctx, built)).toBe(true)
})
it("wildcards path", () => {
const pattern = [
{
route: "/api/tests",
method: "POST",
},
]
const ctx = structures.koa.newContext()
ctx.request.url = "/api/tests/id/something/else"
ctx.request.method = "POST"
const built = matchers.buildMatcherRegex(pattern)
expect(!!matchers.matches(ctx, built)).toBe(true)
})
it("doesn't wildcard path with strict", () => {
const pattern = [
{
route: "/api/tests",
method: "POST",
strict: true,
},
]
const ctx = structures.koa.newContext()
ctx.request.url = "/api/tests/id/something/else"
ctx.request.method = "POST"
const built = matchers.buildMatcherRegex(pattern)
expect(!!matchers.matches(ctx, built)).toBe(false)
})
it("matches with param", () => {
const pattern = [
{
route: "/api/tests/:testId",
method: "GET",
},
]
const ctx = structures.koa.newContext()
ctx.request.url = "/api/tests/id"
ctx.request.method = "GET"
const built = matchers.buildMatcherRegex(pattern)
expect(!!matchers.matches(ctx, built)).toBe(true)
})
// TODO: Support the below behaviour
// Strict does not work when a param is present
// it("matches with param with strict", () => {
// const pattern = [{
// route: "/api/tests/:testId",
// method: "GET",
// strict: true
// }]
// const ctx = structures.koa.newContext()
// ctx.request.url = "/api/tests/id"
// ctx.request.method = "GET"
//
// const built = matchers.buildMatcherRegex(pattern)
//
// expect(!!matchers.matches(ctx, built)).toBe(true)
// })
it("doesn't match by path", () => {
const pattern = [
{
route: "/api/tests",
method: "POST",
},
]
const ctx = structures.koa.newContext()
ctx.request.url = "/api/unknown"
ctx.request.method = "POST"
const built = matchers.buildMatcherRegex(pattern)
expect(!!matchers.matches(ctx, built)).toBe(false)
})
it("doesn't match by method", () => {
const pattern = [
{
route: "/api/tests",
method: "POST",
},
]
const ctx = structures.koa.newContext()
ctx.request.url = "/api/tests"
ctx.request.method = "GET"
const built = matchers.buildMatcherRegex(pattern)
expect(!!matchers.matches(ctx, built)).toBe(false)
})
it("matches by path and wildcard method", () => {
const pattern = [
{
route: "/api/tests",
method: "ALL",
},
]
const ctx = structures.koa.newContext()
ctx.request.url = "/api/tests"
ctx.request.method = "GET"
const built = matchers.buildMatcherRegex(pattern)
expect(!!matchers.matches(ctx, built)).toBe(true)
})
})

View file

@ -12,6 +12,7 @@ import {
MigrationOptions,
MigrationType,
MigrationNoOpOptions,
App,
} from "@budibase/types"
export const getMigrationsDoc = async (db: any) => {
@ -55,14 +56,17 @@ export const runMigration = async (
}
// get the db to store the migration in
let dbNames
let dbNames: string[]
if (migrationType === MigrationType.GLOBAL) {
dbNames = [getGlobalDBName()]
} else if (migrationType === MigrationType.APP) {
if (options.noOp) {
if (!options.noOp.appId) {
throw new Error("appId is required for noOp app migration")
}
dbNames = [options.noOp.appId]
} else {
const apps = await getAllApps(migration.appOpts)
const apps = (await getAllApps(migration.appOpts)) as App[]
dbNames = apps.map(app => app.appId)
}
} else if (migrationType === MigrationType.INSTALLATION) {

View file

@ -9,7 +9,13 @@ import {
isMultiTenant,
} from "../context"
import env from "../environment"
import { PlatformUser } from "@budibase/types"
import {
BBContext,
PlatformUser,
TenantResolutionStrategy,
GetTenantIdOptions,
} from "@budibase/types"
import { Headers } from "../constants"
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
@ -151,3 +157,108 @@ export async function getTenantIds() {
return (tenants && tenants.tenantIds) || []
})
}
const ALL_STRATEGIES = Object.values(TenantResolutionStrategy)
export const getTenantIDFromCtx = (
ctx: BBContext,
opts: GetTenantIdOptions
): string | null => {
// exit early if not multi-tenant
if (!isMultiTenant()) {
return DEFAULT_TENANT_ID
}
// opt defaults
if (opts.allowNoTenant === undefined) {
opts.allowNoTenant = false
}
if (!opts.includeStrategies) {
opts.includeStrategies = ALL_STRATEGIES
}
if (!opts.excludeStrategies) {
opts.excludeStrategies = []
}
const isAllowed = (strategy: TenantResolutionStrategy) => {
// excluded takes precedence
if (opts.excludeStrategies?.includes(strategy)) {
return false
}
if (opts.includeStrategies?.includes(strategy)) {
return true
}
}
// always use user first
if (isAllowed(TenantResolutionStrategy.USER)) {
const userTenantId = ctx.user?.tenantId
if (userTenantId) {
return userTenantId
}
}
// header
if (isAllowed(TenantResolutionStrategy.HEADER)) {
const headerTenantId = ctx.request.headers[Headers.TENANT_ID]
if (headerTenantId) {
return headerTenantId as string
}
}
// query param
if (isAllowed(TenantResolutionStrategy.QUERY)) {
const queryTenantId = ctx.request.query.tenantId
if (queryTenantId) {
return queryTenantId as string
}
}
// subdomain
if (isAllowed(TenantResolutionStrategy.SUBDOMAIN)) {
// e.g. budibase.app or local.com:10000
const platformHost = new URL(env.PLATFORM_URL).host.split(":")[0]
// e.g. tenant.budibase.app or tenant.local.com
const requestHost = ctx.host
// parse the tenant id from the difference
if (requestHost.includes(platformHost)) {
const tenantId = requestHost.substring(
0,
requestHost.indexOf(`.${platformHost}`)
)
if (tenantId) {
return tenantId
}
}
}
// path
if (isAllowed(TenantResolutionStrategy.PATH)) {
// params - have to parse manually due to koa-router not run yet
const match = ctx.matched.find(
(m: any) => !!m.paramNames.find((p: any) => p.name === "tenantId")
)
// get the raw path url - without any query params
const ctxUrl = ctx.originalUrl
let url
if (ctxUrl.includes("?")) {
url = ctxUrl.split("?")[0]
} else {
url = ctxUrl
}
if (match) {
const params = match.params(url, match.captures(url), {})
if (params.tenantId) {
return params.tenantId
}
}
}
if (!opts.allowNoTenant) {
ctx.throw(403, "Tenant id not set")
}
return null
}

View file

@ -1,38 +1,51 @@
const { DocumentType, SEPARATOR, ViewName, getAllApps } = require("./db/utils")
import {
DocumentType,
SEPARATOR,
ViewName,
getAllApps,
queryGlobalView,
} from "./db"
import { options } from "./middleware/passport/jwt"
import { Header, Cookie, MAX_VALID_DATE } from "./constants"
import env from "./environment"
import userCache from "./cache/user"
import { getSessionsForUser, invalidateSessions } from "./security/sessions"
import * as events from "./events"
import tenancy from "./tenancy"
import {
App,
BBContext,
PlatformLogoutOpts,
TenantResolutionStrategy,
} from "@budibase/types"
import { SetOption } from "cookies"
const jwt = require("jsonwebtoken")
const { options } = require("./middleware/passport/jwt")
const { queryGlobalView } = require("./db/views")
const { Header, Cookie, MAX_VALID_DATE } = require("./constants")
const env = require("./environment")
const userCache = require("./cache/user")
const {
getSessionsForUser,
invalidateSessions,
} = require("./security/sessions")
const events = require("./events")
const tenancy = require("./tenancy")
const APP_PREFIX = DocumentType.APP + SEPARATOR
const PROD_APP_PREFIX = "/app/"
function confirmAppId(possibleAppId) {
function confirmAppId(possibleAppId: string | undefined) {
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
? possibleAppId
: undefined
}
async function resolveAppUrl(ctx) {
async function resolveAppUrl(ctx: BBContext) {
const appUrl = ctx.path.split("/")[2]
let possibleAppUrl = `/${appUrl.toLowerCase()}`
let tenantId = tenancy.getTenantId()
if (!env.SELF_HOSTED && ctx.subdomains.length) {
// always use the tenant id from the url in cloud
tenantId = ctx.subdomains[0]
let tenantId: string | null = tenancy.getTenantId()
if (env.MULTI_TENANCY) {
// always use the tenant id from the subdomain in multi tenancy
// this ensures the logged-in user tenant id doesn't overwrite
// e.g. in the case of viewing a public app while already logged-in to another tenant
tenantId = tenancy.getTenantIDFromCtx(ctx, {
includeStrategies: [TenantResolutionStrategy.SUBDOMAIN],
})
}
// search prod apps for a url that matches
const apps = await tenancy.doInTenant(tenantId, () =>
const apps: App[] = await tenancy.doInTenant(tenantId, () =>
getAllApps({ dev: false })
)
const app = apps.filter(
@ -42,7 +55,7 @@ async function resolveAppUrl(ctx) {
return app && app.appId ? app.appId : undefined
}
exports.isServingApp = ctx => {
export function isServingApp(ctx: BBContext) {
// dev app
if (ctx.path.startsWith(`/${APP_PREFIX}`)) {
return true
@ -59,12 +72,12 @@ exports.isServingApp = ctx => {
* @param {object} ctx The main request body to look through.
* @returns {string|undefined} If an appId was found it will be returned.
*/
exports.getAppIdFromCtx = async ctx => {
export async function getAppIdFromCtx(ctx: BBContext) {
// look in headers
const options = [ctx.headers[Header.APP_ID]]
let appId
for (let option of options) {
appId = confirmAppId(option)
appId = confirmAppId(option as string)
if (appId) {
break
}
@ -95,7 +108,7 @@ exports.getAppIdFromCtx = async ctx => {
* opens the contents of the specified encrypted JWT.
* @return {object} the contents of the token.
*/
exports.openJwt = token => {
export function openJwt(token: string) {
if (!token) {
return token
}
@ -107,14 +120,14 @@ exports.openJwt = token => {
* @param {object} ctx The request which is to be manipulated.
* @param {string} name The name of the cookie to get.
*/
exports.getCookie = (ctx, name) => {
export function getCookie(ctx: BBContext, name: string) {
const cookie = ctx.cookies.get(name)
if (!cookie) {
return cookie
}
return exports.openJwt(cookie)
return openJwt(cookie)
}
/**
@ -124,12 +137,17 @@ exports.getCookie = (ctx, name) => {
* @param {string|object} value The value of cookie which will be set.
* @param {object} opts options like whether to sign.
*/
exports.setCookie = (ctx, value, name = "builder", opts = { sign: true }) => {
export function setCookie(
ctx: BBContext,
value: any,
name = "builder",
opts = { sign: true }
) {
if (value && opts && opts.sign) {
value = jwt.sign(value, options.secretOrKey)
}
const config = {
const config: SetOption = {
expires: MAX_VALID_DATE,
path: "/",
httpOnly: false,
@ -146,8 +164,8 @@ exports.setCookie = (ctx, value, name = "builder", opts = { sign: true }) => {
/**
* Utility function, simply calls setCookie with an empty string for value
*/
exports.clearCookie = (ctx, name) => {
exports.setCookie(ctx, null, name)
export function clearCookie(ctx: BBContext, name: string) {
setCookie(ctx, null, name)
}
/**
@ -156,11 +174,11 @@ exports.clearCookie = (ctx, name) => {
* @param {object} ctx The koa context object to be tested.
* @return {boolean} returns true if the call is from the client lib (a built app rather than the builder).
*/
exports.isClient = ctx => {
export function isClient(ctx: BBContext) {
return ctx.headers[Header.TYPE] === "client"
}
const getBuilders = async () => {
async function getBuilders() {
const builders = await queryGlobalView(ViewName.USER_BY_BUILDERS, {
include_docs: false,
})
@ -176,7 +194,7 @@ const getBuilders = async () => {
}
}
exports.getBuildersCount = async () => {
export async function getBuildersCount() {
const builders = await getBuilders()
return builders.length
}
@ -184,10 +202,14 @@ exports.getBuildersCount = async () => {
/**
* Logs a user out from budibase. Re-used across account portal and builder.
*/
exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
export async function platformLogout(opts: PlatformLogoutOpts) {
const ctx = opts.ctx
const userId = opts.userId
const keepActiveSession = opts.keepActiveSession
if (!ctx) throw new Error("Koa context must be supplied to logout.")
const currentSession = exports.getCookie(ctx, Cookie.Auth)
const currentSession = getCookie(ctx, Cookie.Auth)
let sessions = await getSessionsForUser(userId)
if (keepActiveSession) {
@ -196,8 +218,8 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
)
} else {
// clear cookies
exports.clearCookie(ctx, Cookie.Auth)
exports.clearCookie(ctx, Cookie.CurrentApp)
clearCookie(ctx, Cookie.Auth)
clearCookie(ctx, Cookie.CurrentApp)
}
const sessionIds = sessions.map(({ sessionId }) => sessionId)
@ -206,6 +228,6 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
await userCache.invalidateUser(userId)
}
exports.timeout = timeMs => {
export function timeout(timeMs: number) {
return new Promise(resolve => setTimeout(resolve, timeMs))
}

View file

@ -1,6 +1,9 @@
import env from "../src/environment"
import { mocks } from "./utilities"
// must explicitly enable fetch mock
mocks.fetch.enable()
// mock all dates to 2020-01-01T00:00:00.000Z
// use tk.reset() to use real dates in individual tests
import tk from "timekeeper"

View file

@ -1,5 +1,6 @@
export * as mocks from "./mocks"
export * as structures from "./structures"
export { generator } from "./structures"
import * as dbConfig from "./db"
dbConfig.init()

View file

@ -1,7 +1,9 @@
export const getAccount = jest.fn()
export const getAccountByTenantId = jest.fn()
export const getStatus = jest.fn()
jest.mock("../../../src/cloud/accounts", () => ({
getAccount,
getAccountByTenantId,
getStatus,
}))

View file

@ -0,0 +1,10 @@
const mockFetch = jest.fn()
const enable = () => {
jest.mock("node-fetch", () => mockFetch)
}
export default {
...mockFetch,
enable,
}

View file

@ -2,3 +2,4 @@ import "./posthog"
import "./events"
export * as accounts from "./accounts"
export * as date from "./date"
export { default as fetch } from "./fetch"

View file

@ -1,23 +1,29 @@
import { generator, uuid } from "."
import { AuthType, CloudAccount, Hosting } from "@budibase/types"
import * as db from "../../../src/db/utils"
import { Account, AuthType, CloudAccount, Hosting } from "@budibase/types"
export const cloudAccount = (): CloudAccount => {
export const account = (): Account => {
return {
accountId: uuid(),
tenantId: generator.word(),
email: generator.email(),
tenantName: generator.word(),
hosting: Hosting.SELF,
createdAt: Date.now(),
verified: true,
verificationSent: true,
tier: "",
email: generator.email(),
tenantId: generator.word(),
hosting: Hosting.CLOUD,
tier: "FREE", // DEPRECATED
authType: AuthType.PASSWORD,
password: generator.word(),
tenantName: generator.word(),
name: generator.name(),
size: "10+",
profession: "Software Engineer",
}
}
export const cloudAccount = (): CloudAccount => {
return {
...account(),
hosting: Hosting.CLOUD,
budibaseUserId: db.generateGlobalUserID(),
}
}

View file

@ -1 +1,7 @@
import { v4 as uuid } from "uuid"
export { v4 as uuid } from "uuid"
export const email = () => {
return `${uuid()}@test.com`
}

View file

@ -1,5 +1,14 @@
import { createMockContext } from "@shopify/jest-koa-mocks"
import { createMockContext, createMockCookies } from "@shopify/jest-koa-mocks"
import { BBContext } from "@budibase/types"
export const newContext = () => {
return createMockContext()
export const newContext = (): BBContext => {
const ctx = createMockContext()
return {
...ctx,
cookies: createMockCookies(),
request: {
...ctx.request,
body: {},
},
}
}

View file

@ -1079,7 +1079,7 @@
dependencies:
"@types/koa" "*"
"@types/koa@*":
"@types/koa@*", "@types/koa@2.13.4":
version "2.13.4"
resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.4.tgz#10620b3f24a8027ef5cbae88b393d1b31205726b"
integrity sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw==
@ -1093,18 +1093,6 @@
"@types/koa-compose" "*"
"@types/node" "*"
"@types/koa@2.0.52":
version "2.0.52"
resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.0.52.tgz#7dd11de4189ab339ad66c4ccad153716b14e525f"
integrity sha512-cp/GTOhOYwomlSKqEoG0kaVEVJEzP4ojYmfa7EKaGkmkkRwJ4B/1VBLbQZ49Z+WJNvzXejQB/9GIKqMo9XLgFQ==
dependencies:
"@types/accepts" "*"
"@types/cookies" "*"
"@types/http-assert" "*"
"@types/keygrip" "*"
"@types/koa-compose" "*"
"@types/node" "*"
"@types/lodash@4.14.180":
version "4.14.180"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670"
@ -1483,11 +1471,6 @@ ansi-styles@^5.0.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
any-promise@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==
anymatch@^3.0.3, anymatch@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
@ -2078,14 +2061,6 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
dependencies:
safe-buffer "~5.1.1"
cookies@~0.7.1:
version "0.7.3"
resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.3.tgz#7912ce21fbf2e8c2da70cf1c3f351aecf59dadfa"
integrity sha512-+gixgxYSgQLTaTIilDHAdlNPZDENDQernEMiIcZpYYP14zgHsCt4Ce1FEjFtcp6GefhozebB6orvhAAWx/IS0A==
dependencies:
depd "~1.1.2"
keygrip "~1.0.3"
cookies@~0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90"
@ -2156,13 +2131,6 @@ debug@^3.2.7:
dependencies:
ms "^2.1.1"
debug@~3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
debuglog@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
@ -2223,7 +2191,7 @@ denque@^1.1.0:
resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf"
integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==
depd@^1.1.0, depd@^1.1.2, depd@~1.1.2:
depd@^1.1.0, depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==
@ -2375,11 +2343,6 @@ error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
error-inject@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37"
integrity sha512-JM8N6PytDbmIYm1IhPWlo8vr3NtfjhDY/1MhD/a5b/aad/USE8a0+NsqE9d5n+GVGmuNkPQWm4bFQWv18d8tMg==
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@ -3654,11 +3617,6 @@ jws@^3.0.0, jws@^3.1.4, jws@^3.2.2:
jwa "^1.4.1"
safe-buffer "^5.0.1"
keygrip@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc"
integrity sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g==
keygrip@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226"
@ -3678,26 +3636,11 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
koa-compose@^3.0.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7"
integrity sha512-8gen2cvKHIZ35eDEik5WOo8zbVp9t4cP8p4hW4uE55waxolLRexKKrqfCpwhGVppnB40jWeF8bZeTVg99eZgPw==
dependencies:
any-promise "^1.1.0"
koa-compose@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877"
integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==
koa-convert@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0"
integrity sha512-K9XqjmEDStGX09v3oxR7t5uPRy0jqJdvodHa6wxWTHrTfDq0WUNnYTOOUZN6g8OM8oZQXprQASbiIXG2Ez8ehA==
dependencies:
co "^4.6.0"
koa-compose "^3.0.0"
koa-convert@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-2.0.0.tgz#86a0c44d81d40551bae22fee6709904573eea4f5"
@ -3706,11 +3649,6 @@ koa-convert@^2.0.0:
co "^4.6.0"
koa-compose "^4.1.0"
koa-is-json@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14"
integrity sha512-+97CtHAlWDx0ndt0J8y3P12EWLwTLMXIfMnYDev3wOTwH/RpBGMlfn4bDXlMEg1u73K6XRE9BbUp+5ZAYoRYWw==
koa-passport@4.1.4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/koa-passport/-/koa-passport-4.1.4.tgz#5f1665c1c2a37ace79af9f970b770885ca30ccfa"
@ -3718,37 +3656,7 @@ koa-passport@4.1.4:
dependencies:
passport "^0.4.0"
koa@2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/koa/-/koa-2.7.0.tgz#7e00843506942b9d82c6cc33749f657c6e5e7adf"
integrity sha512-7ojD05s2Q+hFudF8tDLZ1CpCdVZw8JQELWSkcfG9bdtoTDzMmkRF6BQBU7JzIzCCOY3xd3tftiy/loHBUYaY2Q==
dependencies:
accepts "^1.3.5"
cache-content-type "^1.0.0"
content-disposition "~0.5.2"
content-type "^1.0.4"
cookies "~0.7.1"
debug "~3.1.0"
delegates "^1.0.0"
depd "^1.1.2"
destroy "^1.0.4"
error-inject "^1.0.0"
escape-html "^1.0.3"
fresh "~0.5.2"
http-assert "^1.3.0"
http-errors "^1.6.3"
is-generator-function "^1.0.7"
koa-compose "^4.1.0"
koa-convert "^1.2.0"
koa-is-json "^1.0.0"
on-finished "^2.3.0"
only "~0.0.2"
parseurl "^1.3.2"
statuses "^1.5.0"
type-is "^1.6.16"
vary "^1.1.2"
koa@^2.13.4:
koa@2.13.4, koa@^2.13.4:
version "2.13.4"
resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.4.tgz#ee5b0cb39e0b8069c38d115139c774833d32462e"
integrity sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g==
@ -4131,11 +4039,6 @@ mkdirp@^1.0.3:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "2.1.22-alpha.3",
"version": "2.1.22-alpha.6",
"license": "MPL-2.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",
@ -38,7 +38,7 @@
],
"dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
"@budibase/string-templates": "2.1.22-alpha.3",
"@budibase/string-templates": "2.1.22-alpha.6",
"@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2",

View file

@ -10,7 +10,7 @@ filterTests(['smoke', 'all'], () => {
})
if (!(Cypress.env("TEST_ENV"))) {
it("should show the new user UI/UX", () => {
it.skip("should show the new user UI/UX", () => {
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/create`, { timeout: 5000 }) //added /portal/apps/create
cy.wait(1000)
cy.get(interact.CREATE_APP_BUTTON, { timeout: 10000 }).contains('Start from scratch').should("exist")
@ -83,7 +83,7 @@ filterTests(['smoke', 'all'], () => {
})
})
it("should create the first application from scratch", () => {
it.skip("should create the first application from scratch", () => {
const appName = "Cypress Tests"
cy.createApp(appName, false)
@ -93,7 +93,7 @@ filterTests(['smoke', 'all'], () => {
cy.deleteApp(appName)
})
it("should create the first application from scratch with a default name", () => {
it.skip("should create the first application from scratch with a default name", () => {
cy.updateUserInformation("", "")
cy.createApp("", false)
cy.applicationInAppTable("My app")

View file

@ -12,7 +12,7 @@ filterTests(['smoke', 'all'], () => {
cy.createTestTableWithData()
cy.wait(2000)
cy.contains("Automate").click()
cy.get(interact.ADD_BUTTON_SPECTRUM).click()
cy.get(interact.SPECTRUM_BUTTON_TEMPLATE).contains("Add automation").click({ force: true })
cy.get(interact.MODAL_INNER_WRAPPER).within(() => {
cy.get("input").type("Add Row")
cy.contains("Row Created").click({ force: true })
@ -24,7 +24,7 @@ filterTests(['smoke', 'all'], () => {
cy.wait(500)
cy.contains("dog").click()
// Create action
cy.get('[aria-label="AddCircle"]', { timeout: 2000 }).eq(1).click()
cy.get('[aria-label="AddCircle"]', { timeout: 2000 }).click()
cy.get(interact.MODAL_INNER_WRAPPER).within(() => {
cy.wait(1000)
cy.contains("Create Row").trigger('mouseover').click().click()

View file

@ -9,7 +9,7 @@ filterTests(["smoke", "all"], () => {
cy.navigateToFrontend()
})
it("Should successfully create a screen", () => {
it.skip("Should successfully create a screen", () => {
cy.createScreen("test")
cy.get(interact.BODY).within(() => {
cy.contains("/test").should("exist")
@ -23,7 +23,7 @@ filterTests(["smoke", "all"], () => {
})
})
it("should delete all screens then create first screen via button", () => {
it.skip("should delete all screens then create first screen via button", () => {
cy.deleteAllScreens()
cy.contains("Create first screen").click()

View file

@ -2,7 +2,7 @@ import filterTests from "../support/filterTests"
const interact = require('../support/interact')
filterTests(['smoke', 'all'], () => {
context("Revert apps", () => {
xcontext("Revert apps", () => {
before(() => {
cy.login()
cy.createTestApp()

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "2.1.22-alpha.3",
"version": "2.1.22-alpha.6",
"license": "GPL-3.0",
"private": true,
"scripts": {
@ -71,10 +71,10 @@
}
},
"dependencies": {
"@budibase/bbui": "2.1.22-alpha.3",
"@budibase/client": "2.1.22-alpha.3",
"@budibase/frontend-core": "2.1.22-alpha.3",
"@budibase/string-templates": "2.1.22-alpha.3",
"@budibase/bbui": "2.1.22-alpha.6",
"@budibase/client": "2.1.22-alpha.6",
"@budibase/frontend-core": "2.1.22-alpha.6",
"@budibase/string-templates": "2.1.22-alpha.6",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",

View file

@ -20,11 +20,12 @@
Toggle,
Tag,
Tags,
Icon,
Helpers,
} from "@budibase/bbui"
import { onMount } from "svelte"
import { API } from "api"
import { organisation, admin } from "stores/portal"
import { Helpers } from "@budibase/bbui"
const ConfigTypes = {
Google: "google",
@ -40,7 +41,9 @@
// Indicate to user that callback is based on platform url
// If there is an existing value, indicate that it may be removed to return to default behaviour
$: googleCallbackTooltip = googleCallbackReadonly
$: googleCallbackTooltip = $admin.cloud
? null
: googleCallbackReadonly
? "Vist the organisation page to update the platform URL"
: "Leave blank to use the default callback URL"
@ -54,6 +57,7 @@
readonly: googleCallbackReadonly,
tooltip: googleCallbackTooltip,
placeholder: $organisation.googleCallbackUrl,
copyButton: true,
},
],
}
@ -66,9 +70,12 @@
{
name: "callbackURL",
readonly: true,
tooltip: "Vist the organisation page to update the platform URL",
tooltip: $admin.cloud
? null
: "Vist the organisation page to update the platform URL",
label: "Callback URL",
placeholder: $organisation.oidcCallbackUrl,
copyButton: true,
},
],
}
@ -231,6 +238,11 @@
},
]
const copyToClipboard = async value => {
await Helpers.copyToClipboard(value)
notifications.success("Copied")
}
onMount(async () => {
try {
await organisation.init()
@ -336,11 +348,23 @@
{#each GoogleConfigFields.Google as field}
<div class="form-row">
<Label size="L" tooltip={field.tooltip}>{field.label}</Label>
<Input
bind:value={providers.google.config[field.name]}
readonly={field.readonly}
placeholder={field.placeholder}
/>
<div class="inputContainer">
<div class="input">
<Input
bind:value={providers.google.config[field.name]}
readonly={field.readonly}
placeholder={field.placeholder}
/>
</div>
{#if field.copyButton}
<div
class="copy"
on:click={() => copyToClipboard(field.placeholder)}
>
<Icon size="S" name="Copy" />
</div>
{/if}
</div>
</div>
{/each}
<div class="form-row">
@ -375,12 +399,23 @@
{#each OIDCConfigFields.Oidc as field}
<div class="form-row">
<Label size="L" tooltip={field.tooltip}>{field.label}</Label>
<Input
bind:value={providers.oidc.config.configs[0][field.name]}
readonly={field.readonly}
placeholder={field.placeholder}
dataCy={field.name}
/>
<div class="inputContainer">
<div class="input">
<Input
bind:value={providers.oidc.config.configs[0][field.name]}
readonly={field.readonly}
placeholder={field.placeholder}
/>
</div>
{#if field.copyButton}
<div
class="copy"
on:click={() => copyToClipboard(field.placeholder)}
>
<Icon size="S" name="Copy" />
</div>
{/if}
</div>
</div>
{/each}
</Layout>
@ -557,4 +592,16 @@
.provider-title span {
flex: 1 1 auto;
}
.inputContainer {
display: flex;
flex-direction: row;
}
.input {
flex: 1;
}
.copy {
display: flex;
align-items: center;
margin-left: 10px;
}
</style>

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/cli",
"version": "2.1.22-alpha.3",
"version": "2.1.22-alpha.6",
"description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js",
"bin": {
@ -26,9 +26,9 @@
"outputPath": "build"
},
"dependencies": {
"@budibase/backend-core": "2.1.22-alpha.3",
"@budibase/string-templates": "2.1.22-alpha.3",
"@budibase/types": "2.1.22-alpha.3",
"@budibase/backend-core": "2.1.22-alpha.6",
"@budibase/string-templates": "2.1.22-alpha.6",
"@budibase/types": "2.1.22-alpha.6",
"axios": "0.21.2",
"chalk": "4.1.0",
"cli-progress": "3.11.2",

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "2.1.22-alpha.3",
"version": "2.1.22-alpha.6",
"license": "MPL-2.0",
"module": "dist/budibase-client.js",
"main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw"
},
"dependencies": {
"@budibase/bbui": "2.1.22-alpha.3",
"@budibase/frontend-core": "2.1.22-alpha.3",
"@budibase/string-templates": "2.1.22-alpha.3",
"@budibase/bbui": "2.1.22-alpha.6",
"@budibase/frontend-core": "2.1.22-alpha.6",
"@budibase/string-templates": "2.1.22-alpha.6",
"@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3",

View file

@ -1,12 +1,12 @@
{
"name": "@budibase/frontend-core",
"version": "2.1.22-alpha.3",
"version": "2.1.22-alpha.6",
"description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase",
"license": "MPL-2.0",
"svelte": "src/index.js",
"dependencies": {
"@budibase/bbui": "2.1.22-alpha.3",
"@budibase/bbui": "2.1.22-alpha.6",
"lodash": "^4.17.21",
"svelte": "^3.46.2"
}

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/sdk",
"version": "2.1.22-alpha.3",
"version": "2.1.22-alpha.6",
"description": "Budibase Public API SDK",
"author": "Budibase",
"license": "MPL-2.0",

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "2.1.22-alpha.3",
"version": "2.1.22-alpha.6",
"description": "Budibase Web Server",
"main": "src/index.ts",
"repository": {
@ -43,11 +43,11 @@
"license": "GPL-3.0",
"dependencies": {
"@apidevtools/swagger-parser": "10.0.3",
"@budibase/backend-core": "2.1.22-alpha.3",
"@budibase/client": "2.1.22-alpha.3",
"@budibase/pro": "2.1.22-alpha.3",
"@budibase/string-templates": "2.1.22-alpha.3",
"@budibase/types": "2.1.22-alpha.3",
"@budibase/backend-core": "2.1.22-alpha.6",
"@budibase/client": "2.1.22-alpha.6",
"@budibase/pro": "2.1.22-alpha.6",
"@budibase/string-templates": "2.1.22-alpha.6",
"@budibase/types": "2.1.22-alpha.6",
"@bull-board/api": "3.7.0",
"@bull-board/koa": "3.9.4",
"@elastic/elasticsearch": "7.10.0",

View file

@ -2,6 +2,36 @@
const updateDotEnv = require("update-dotenv")
const arg = process.argv.slice(2)[0]
const isEnable = arg === "enable"
let domain = process.argv.slice(2)[1]
if (!domain) {
domain = "local.com"
}
const getAccountPortalUrl = () => {
if (isEnable) {
return `http://account.${domain}:10001`
} else {
return `http://localhost:10001`
}
}
const getBudibaseUrl = () => {
if (isEnable) {
return `http://${domain}:10000`
} else {
return `http://localhost:10000`
}
}
const getCookieDomain = () => {
if (isEnable) {
return `.${domain}`
} else {
return ""
}
}
/**
* For testing multi tenancy sub domains locally.
@ -16,9 +46,7 @@ const arg = process.argv.slice(2)[0]
* 127.0.0.1 t2.local.com
*/
updateDotEnv({
ACCOUNT_PORTAL_URL:
arg === "enable"
? "http://account.local.com:10001"
: "http://localhost:10001",
COOKIE_DOMAIN: arg === "enable" ? ".local.com" : "",
}).then(() => console.log("Updated worker!"))
ACCOUNT_PORTAL_URL: getAccountPortalUrl(),
COOKIE_DOMAIN: getCookieDomain(),
PLATFORM_URL: getBudibaseUrl(),
}).then(() => console.log("Updated server!"))

View file

@ -149,7 +149,7 @@ export const run = async (db: any) => {
}
try {
const allApps: App[] = await dbUtils.getAllApps({ dev: true })
const allApps = (await dbUtils.getAllApps({ dev: true })) as App[]
totals.apps = allApps.length
totals.usage = await quotas.backfill(allApps)

View file

@ -2,11 +2,11 @@ import { getTenantId } from "@budibase/backend-core/tenancy"
import { getAllApps } from "@budibase/backend-core/db"
import { getUniqueRows } from "../../../utilities/usageQuota/rows"
import { quotas } from "@budibase/pro"
import { StaticQuotaName, QuotaUsageType } from "@budibase/types"
import { StaticQuotaName, QuotaUsageType, App } from "@budibase/types"
export const run = async () => {
// get all rows in all apps
const allApps = await getAllApps({ all: true })
const allApps = (await getAllApps({ all: true })) as App[]
const appIds = allApps ? allApps.map((app: { appId: any }) => app.appId) : []
const { appRows } = await getUniqueRows(appIds)

View file

@ -1273,12 +1273,12 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/backend-core@2.1.22-alpha.3":
version "2.1.22-alpha.3"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.1.22-alpha.3.tgz#5cdff781cf6a448677304aa80a9658e9e51c36a5"
integrity sha512-je1mKTb1h9f+tyCAiFNJykg9O8ZM3smVQ3JJf24rXDYvLYN8GG8hxZ5fOJuWoeF/aGu8m+w3v3+EdWuTlQPcKg==
"@budibase/backend-core@2.1.22-alpha.6":
version "2.1.22-alpha.6"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.1.22-alpha.6.tgz#e9886620fcbee6fe0348365cb96ac6484fc5f8fd"
integrity sha512-P3NSgNuQXKmdeT8MLfeCji3ibRSeIIMSOQeNSQBWpaOTA69rpXQk753lHRwUWMpqil/ybsOuE/h1/Y3eTa+/UA==
dependencies:
"@budibase/types" "2.1.22-alpha.3"
"@budibase/types" "2.1.22-alpha.6"
"@shopify/jest-koa-mocks" "5.0.1"
"@techpass/passport-openidconnect" "0.3.2"
aws-sdk "2.1030.0"
@ -1360,13 +1360,13 @@
svelte-flatpickr "^3.2.3"
svelte-portal "^1.0.0"
"@budibase/pro@2.1.22-alpha.3":
version "2.1.22-alpha.3"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.1.22-alpha.3.tgz#8f87ffc1c6c158ad467216d5cfcb7844e9a08363"
integrity sha512-IE0eHPswBycPYzvduZCAp4T6U8t1qTcT1A9jVJ8TeX2jiL22hPOQ+8nVNIaqTGfQvJ7foGiqtDcugQbk3QLGOg==
"@budibase/pro@2.1.22-alpha.6":
version "2.1.22-alpha.6"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.1.22-alpha.6.tgz#e4d8238f1727eab85ac72b3ce2fcaec2a5c18456"
integrity sha512-1CKqQ2HMX+/5p24aHpPlUgxoMjKRZxRoyK5fPD/X35Z0mDj+9Ohny3oqbF1fC20pl/20bmcaE4J+q2ph/pbxdQ==
dependencies:
"@budibase/backend-core" "2.1.22-alpha.3"
"@budibase/types" "2.1.22-alpha.3"
"@budibase/backend-core" "2.1.22-alpha.6"
"@budibase/types" "2.1.22-alpha.6"
"@koa/router" "8.0.8"
bull "4.10.1"
joi "17.6.0"
@ -1390,10 +1390,10 @@
svelte-apexcharts "^1.0.2"
svelte-flatpickr "^3.1.0"
"@budibase/types@2.1.22-alpha.3":
version "2.1.22-alpha.3"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.1.22-alpha.3.tgz#30cfcb6a58989a94dfba43ec7f9052e4855b6803"
integrity sha512-3WKZ5DVkygUi9H3KJTL8geQf9cjmssM8tsbDEFi8KJHogfszJPia9di/DnN8rd/CXbsx3Zbsbe8LHiAAADz5og==
"@budibase/types@2.1.22-alpha.6":
version "2.1.22-alpha.6"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.1.22-alpha.6.tgz#a280321373c26a5bf1a52ed119de9178292893c0"
integrity sha512-rVrhs9u7OTzlCxgUFqBu5H6jsMHhwH8uduPhT8Eo7J+Wr4J/0Io7WeuAt1egK6t83JiEPxDBH3WLzAH+04hPVA==
"@bull-board/api@3.7.0":
version "3.7.0"

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/string-templates",
"version": "2.1.22-alpha.3",
"version": "2.1.22-alpha.6",
"description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs",
"module": "dist/bundle.mjs",

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/types",
"version": "2.1.22-alpha.3",
"version": "2.1.22-alpha.6",
"description": "Budibase types",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View file

@ -51,3 +51,9 @@ export interface SearchUsersRequest {
appId?: string
userIds?: string[]
}
export interface CreateAdminUserRequest {
email: string
password: string
tenantId: string
}

View file

@ -1,3 +1,4 @@
export * from "./info"
export * from "./users"
export * from "./accounts"
export * from "./tenants"

View file

@ -0,0 +1,5 @@
import { Document } from "../document"
export interface Tenants extends Document {
tenantIds: string[]
}

View file

@ -1,3 +1,5 @@
import { BBContext } from "./koa"
export interface AuthToken {
userId: string
tenantId: string
@ -25,3 +27,9 @@ export interface SessionKey {
export interface ScannedSession {
value: Session
}
export interface PlatformLogoutOpts {
ctx: BBContext
userId: string
keepActiveSession?: boolean
}

View file

@ -9,3 +9,4 @@ export * from "./koa"
export * from "./auth"
export * from "./locks"
export * from "./db"
export * from "./middleware"

View file

@ -0,0 +1,2 @@
export * from "./matchers"
export * from "./tenancy"

View file

@ -0,0 +1,22 @@
export interface EndpointMatcher {
/**
* The HTTP Path. e.g. /api/things/:thingId
*/
route: string
/**
* The HTTP Verb. e.g. GET, POST, etc.
* ALL is also accepted to cover all verbs.
*/
method: string
/**
* The route must match exactly - not just begins with
*/
strict?: boolean
}
export interface RegexMatcher {
regex: RegExp
method: string
strict: boolean
route: string
}

View file

@ -0,0 +1,13 @@
export interface GetTenantIdOptions {
allowNoTenant?: boolean
excludeStrategies?: TenantResolutionStrategy[]
includeStrategies?: TenantResolutionStrategy[]
}
export enum TenantResolutionStrategy {
USER = "user",
HEADER = "header",
QUERY = "query",
SUBDOMAIN = "subdomain",
PATH = "path",
}

View file

@ -1 +0,0 @@
jest.mock("node-fetch", () => jest.fn())

View file

@ -0,0 +1,57 @@
import * as jwt from "jsonwebtoken"
const mockOAuth2 = {
getOAuthAccessToken: (code: string, p: any, cb: any) => {
const err = null
const accessToken = "access_token"
const refreshToken = "refresh_token"
const exp = new Date()
exp.setDate(exp.getDate() + 1)
const iat = new Date()
iat.setDate(iat.getDate() - 1)
const claims = {
iss: "test",
sub: "sub",
aud: "clientId",
exp: exp.getTime() / 1000,
iat: iat.getTime() / 1000,
email: "oauth@example.com",
}
const idToken = jwt.sign(claims, "secret")
const params = {
id_token: idToken,
}
return cb(err, accessToken, refreshToken, params)
},
_request: (
method: string,
url: string,
headers: any,
postBody: any,
accessToken: string,
cb: any
) => {
const err = null
const body = {
sub: "sub",
user_id: "userId",
name: "OAuth",
family_name: "2",
given_name: "OAuth",
middle_name: "",
}
const res = {}
return cb(err, JSON.stringify(body), res)
},
}
const oauth = {
OAuth2: jest.fn(() => mockOAuth2),
}
export = oauth

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/worker",
"email": "hi@budibase.com",
"version": "2.1.22-alpha.3",
"version": "2.1.22-alpha.6",
"description": "Budibase background service",
"main": "src/index.ts",
"repository": {
@ -36,10 +36,10 @@
"author": "Budibase",
"license": "GPL-3.0",
"dependencies": {
"@budibase/backend-core": "2.1.22-alpha.3",
"@budibase/pro": "2.1.22-alpha.3",
"@budibase/string-templates": "2.1.22-alpha.3",
"@budibase/types": "2.1.22-alpha.3",
"@budibase/backend-core": "2.1.22-alpha.6",
"@budibase/pro": "2.1.22-alpha.6",
"@budibase/string-templates": "2.1.22-alpha.6",
"@budibase/types": "2.1.22-alpha.6",
"@koa/router": "8.0.8",
"@sentry/node": "6.17.7",
"@techpass/passport-openidconnect": "0.3.2",
@ -71,9 +71,11 @@
},
"devDependencies": {
"@types/jest": "26.0.23",
"@types/jsonwebtoken": "8.5.1",
"@types/koa": "2.13.4",
"@types/koa__router": "8.0.11",
"@types/node": "14.18.20",
"@types/node-fetch": "2.6.1",
"@types/pouchdb": "6.4.0",
"@types/uuid": "8.3.4",
"@typescript-eslint/parser": "5.12.0",

View file

@ -2,6 +2,36 @@
const updateDotEnv = require("update-dotenv")
const arg = process.argv.slice(2)[0]
const isEnable = arg === "enable"
let domain = process.argv.slice(2)[1]
if (!domain) {
domain = "local.com"
}
const getAccountPortalUrl = () => {
if (isEnable) {
return `http://account.${domain}:10001`
} else {
return `http://localhost:10001`
}
}
const getBudibaseUrl = () => {
if (isEnable) {
return `http://${domain}:10000`
} else {
return `http://localhost:10000`
}
}
const getCookieDomain = () => {
if (isEnable) {
return `.${domain}`
} else {
return ""
}
}
/**
* For testing multi tenancy sub domains locally.
@ -16,11 +46,7 @@ const arg = process.argv.slice(2)[0]
* 127.0.0.1 t2.local.com
*/
updateDotEnv({
ACCOUNT_PORTAL_URL:
arg === "enable"
? "http://account.local.com:10001"
: "http://localhost:10001",
COOKIE_DOMAIN: arg === "enable" ? ".local.com" : "",
PLATFORM_URL:
arg === "enable" ? "http://local.com:10000" : "http://localhost:10000",
ACCOUNT_PORTAL_URL: getAccountPortalUrl(),
COOKIE_DOMAIN: getCookieDomain(),
PLATFORM_URL: getBudibaseUrl(),
}).then(() => console.log("Updated worker!"))

View file

@ -1,22 +1,26 @@
const core = require("@budibase/backend-core")
const { Config, EmailTemplatePurpose } = require("../../../constants")
const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
import core from "@budibase/backend-core"
import {
events,
users as usersCore,
context,
tenancy,
} from "@budibase/backend-core"
import { Config, EmailTemplatePurpose } from "../../../constants"
import { sendEmail, isEmailConfigured } from "../../../utilities/email"
import { checkResetPasswordCode } from "../../../utilities/redis"
import env from "../../../environment"
import sdk from "../../../sdk"
import { User } from "@budibase/types"
const { setCookie, getCookie, clearCookie, hash, platformLogout } = core.utils
const { Cookie, Header } = core.constants
const { passport, ssoCallbackUrl, google, oidc } = core.auth
const { checkResetPasswordCode } = require("../../../utilities/redis")
const { getGlobalDB } = require("@budibase/backend-core/tenancy")
const env = require("../../../environment")
import { events, users as usersCore, context } from "@budibase/backend-core"
import sdk from "../../../sdk"
import { User } from "@budibase/types"
export const googleCallbackUrl = async (config: any) => {
return ssoCallbackUrl(getGlobalDB(), config, "google")
return ssoCallbackUrl(tenancy.getGlobalDB(), config, "google")
}
export const oidcCallbackUrl = async (config: any) => {
return ssoCallbackUrl(getGlobalDB(), config, "oidc")
return ssoCallbackUrl(tenancy.getGlobalDB(), config, "oidc")
}
async function authInternal(ctx: any, user: any, err = null, info = null) {
@ -106,7 +110,7 @@ export const resetUpdate = async (ctx: any) => {
const { resetCode, password } = ctx.request.body
try {
const { userId } = await checkResetPasswordCode(resetCode)
const db = getGlobalDB()
const db = tenancy.getGlobalDB()
const user = await db.get(userId)
user.password = await hash(password)
await db.put(user)
@ -160,7 +164,7 @@ export const datasourceAuth = async (ctx: any, next: any) => {
* On a successful login, you will be redirected to the googleAuth callback route.
*/
export const googlePreAuth = async (ctx: any, next: any) => {
const db = getGlobalDB()
const db = tenancy.getGlobalDB()
const config = await core.db.getScopedConfig(db, {
type: Config.GOOGLE,
@ -181,7 +185,7 @@ export const googlePreAuth = async (ctx: any, next: any) => {
}
export const googleAuth = async (ctx: any, next: any) => {
const db = getGlobalDB()
const db = tenancy.getGlobalDB()
const config = await core.db.getScopedConfig(db, {
type: Config.GOOGLE,
@ -208,7 +212,7 @@ export const googleAuth = async (ctx: any, next: any) => {
}
export const oidcStrategyFactory = async (ctx: any, configId: any) => {
const db = getGlobalDB()
const db = tenancy.getGlobalDB()
const config = await core.db.getScopedConfig(db, {
type: Config.OIDC,
group: ctx.query.group,
@ -235,7 +239,7 @@ export const oidcPreAuth = async (ctx: any, next: any) => {
setCookie(ctx, configId, Cookie.OIDC_CONFIG)
const db = getGlobalDB()
const db = tenancy.getGlobalDB()
const config = await core.db.getScopedConfig(db, {
type: Config.OIDC,
group: ctx.query.group,

View file

@ -5,6 +5,7 @@ import {
BulkUserRequest,
BulkUserResponse,
CloudAccount,
CreateAdminUserRequest,
InviteUserRequest,
InviteUsersRequest,
SearchUsersRequest,
@ -67,7 +68,8 @@ const parseBooleanParam = (param: any) => {
}
export const adminUser = async (ctx: any) => {
const { email, password, tenantId } = ctx.request.body
const { email, password, tenantId } = ctx.request
.body as CreateAdminUserRequest
await tenancy.doInTenant(tenantId, async () => {
// account portal sends a pre-hashed password - honour param to prevent double hashing
const hashPassword = parseBooleanParam(ctx.request.query.hashPassword)

View file

@ -1,6 +1,7 @@
const env = require("../../../environment")
import { BBContext } from "@budibase/types"
import env from "../../../environment"
exports.fetch = async ctx => {
export const fetch = async (ctx: BBContext) => {
ctx.body = {
multiTenancy: !!env.MULTI_TENANCY,
cloud: !env.SELF_HOSTED,

View file

@ -1,7 +1,8 @@
const accounts = require("@budibase/backend-core/accounts")
const env = require("../../../environment")
import { accounts } from "@budibase/backend-core"
import env from "../../../environment"
import { BBContext } from "@budibase/types"
exports.fetch = async ctx => {
export const fetch = async (ctx: BBContext) => {
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
const status = await accounts.getStatus()
ctx.body = status

View file

@ -1,61 +1,18 @@
const { StaticDatabases, doWithDB } = require("@budibase/backend-core/db")
const { getTenantId } = require("@budibase/backend-core/tenancy")
const { deleteTenant } = require("@budibase/backend-core/deprovision")
import { BBContext } from "@budibase/types"
import { deprovisioning } from "@budibase/backend-core"
import { quotas } from "@budibase/pro"
export const exists = async (ctx: any) => {
const tenantId = ctx.request.params
ctx.body = {
exists: await doWithDB(
StaticDatabases.PLATFORM_INFO.name,
async (db: any) => {
let exists = false
try {
const tenantsDoc = await db.get(
StaticDatabases.PLATFORM_INFO.docs.tenants
)
if (tenantsDoc) {
exists = tenantsDoc.tenantIds.indexOf(tenantId) !== -1
}
} catch (err) {
// if error it doesn't exist
}
return exists
}
),
}
}
const _delete = async (ctx: BBContext) => {
const user = ctx.user!
const tenantId = ctx.params.tenantId
export const fetch = async (ctx: any) => {
ctx.body = await doWithDB(
StaticDatabases.PLATFORM_INFO.name,
async (db: any) => {
let tenants = []
try {
const tenantsDoc = await db.get(
StaticDatabases.PLATFORM_INFO.docs.tenants
)
if (tenantsDoc) {
tenants = tenantsDoc.tenantIds
}
} catch (err) {
// if error it doesn't exist
}
return tenants
}
)
}
const _delete = async (ctx: any) => {
const tenantId = getTenantId()
if (ctx.params.tenantId !== tenantId) {
ctx.throw(403, "Unauthorized")
if (tenantId !== user.tenantId) {
ctx.throw(403, "Tenant ID does not match current user")
}
try {
await deleteTenant(tenantId)
await quotas.bustCache()
await deprovisioning.deleteTenant(tenantId)
ctx.status = 204
} catch (err) {
ctx.log.error(err)

View file

@ -7,11 +7,12 @@ import { errors, auth, middleware } from "@budibase/backend-core"
import { APIError } from "@budibase/types"
const PUBLIC_ENDPOINTS = [
// old deprecated endpoints kept for backwards compat
// deprecated single tenant sso callback
{
route: "/api/admin/auth/google/callback",
method: "GET",
},
// deprecated single tenant sso callback
{
route: "/api/admin/auth/oidc/callback",
method: "GET",
@ -44,17 +45,19 @@ const PUBLIC_ENDPOINTS = [
method: "POST",
},
{
route: "api/system/environment",
route: "/api/system/environment",
method: "GET",
},
{
route: "api/system/status",
route: "/api/system/status",
method: "GET",
},
// TODO: This should be an internal api
{
route: "/api/global/users/tenant/:id",
method: "GET",
},
// TODO: This should be an internal api
{
route: "/api/system/restored",
method: "POST",
@ -62,17 +65,37 @@ const PUBLIC_ENDPOINTS = [
]
const NO_TENANCY_ENDPOINTS = [
...PUBLIC_ENDPOINTS,
// system endpoints are not specific to any tenant
{
route: "/api/system",
method: "ALL",
},
// tenant is determined in request body
// used for creating the tenant
{
route: "/api/global/users/self",
route: "/api/global/users/init",
method: "POST",
},
// deprecated single tenant sso callback
{
route: "/api/admin/auth/google/callback",
method: "GET",
},
// deprecated single tenant sso callback
{
route: "/api/global/self",
route: "/api/admin/auth/oidc/callback",
method: "GET",
},
// tenant is determined from code in redis
{
route: "/api/global/users/invite/accept",
method: "POST",
},
// global user search - no tenancy
// :id is user id
// TODO: this should really be `/api/system/users/:id`
{
route: "/api/global/users/tenant/:id",
method: "GET",
},
]

View file

@ -2,7 +2,6 @@ const Router = require("@koa/router")
const authController = require("../../controllers/global/auth")
const { joiValidator } = require("@budibase/backend-core/auth")
const Joi = require("joi")
const { updateTenantId } = require("@budibase/backend-core/tenancy")
const router = new Router()
@ -29,77 +28,61 @@ function buildResetUpdateValidation() {
}).required().unknown(false))
}
function updateTenant(ctx, next) {
if (ctx.params) {
updateTenantId(ctx.params.tenantId)
}
return next()
}
router
// PASSWORD
.post(
"/api/global/auth/:tenantId/login",
buildAuthValidation(),
updateTenant,
authController.authenticate
)
.post("/api/global/auth/logout", authController.logout)
.post(
"/api/global/auth/:tenantId/reset",
buildResetValidation(),
updateTenant,
authController.reset
)
.post(
"/api/global/auth/:tenantId/reset/update",
buildResetUpdateValidation(),
updateTenant,
authController.resetUpdate
)
.post("/api/global/auth/logout", authController.logout)
// INIT
.post("/api/global/auth/init", authController.setInitInfo)
.get("/api/global/auth/init", authController.getInitInfo)
.get(
"/api/global/auth/:tenantId/google",
updateTenant,
authController.googlePreAuth
)
// DATASOURCE - MULTI TENANT
.get(
"/api/global/auth/:tenantId/datasource/:provider",
updateTenant,
authController.datasourcePreAuth
)
// single tenancy endpoint
.get("/api/global/auth/google/callback", authController.googleAuth)
.get(
"/api/global/auth/:tenantId/datasource/:provider/callback",
authController.datasourceAuth
)
// DATASOURCE - SINGLE TENANT - DEPRECATED
.get(
"/api/global/auth/datasource/:provider/callback",
authController.datasourceAuth
)
// multi-tenancy endpoint
.get(
"/api/global/auth/:tenantId/google/callback",
updateTenant,
authController.googleAuth
)
.get(
"/api/global/auth/:tenantId/datasource/:provider/callback",
updateTenant,
authController.datasourceAuth
)
// GOOGLE - MULTI TENANT
.get("/api/global/auth/:tenantId/google", authController.googlePreAuth)
.get("/api/global/auth/:tenantId/google/callback", authController.googleAuth)
// GOOGLE - SINGLE TENANT - DEPRECATED
.get("/api/global/auth/google/callback", authController.googleAuth)
.get("/api/admin/auth/google/callback", authController.googleAuth)
// OIDC - MULTI TENANT
.get(
"/api/global/auth/:tenantId/oidc/configs/:configId",
updateTenant,
authController.oidcPreAuth
)
// single tenancy endpoint
.get("/api/global/auth/:tenantId/oidc/callback", authController.oidcAuth)
// OIDC - SINGLE TENANT - DEPRECATED
.get("/api/global/auth/oidc/callback", authController.oidcAuth)
// multi-tenancy endpoint
.get(
"/api/global/auth/:tenantId/oidc/callback",
updateTenant,
authController.oidcAuth
)
// deprecated - used by the default system before tenancy
.get("/api/admin/auth/google/callback", authController.googleAuth)
.get("/api/admin/auth/oidc/callback", authController.oidcAuth)
module.exports = router

View file

@ -1,6 +1,6 @@
import Router from "@koa/router"
import * as controller from "../../controllers/global/templates"
import { TemplatePurpose, TemplateTypes } from "../../../constants"
import { TemplatePurpose, TemplateType } from "../../../constants"
import { auth as authCore } from "@budibase/backend-core"
import Joi from "joi"
const { adminOnly, joiValidator } = authCore
@ -16,7 +16,7 @@ function buildTemplateSaveValidation() {
name: Joi.string().allow(null, ""),
contents: Joi.string().required(),
purpose: Joi.string().required().valid(...Object.values(TemplatePurpose)),
type: Joi.string().required().valid(...Object.values(TemplateTypes)),
type: Joi.string().required().valid(...Object.values(TemplateType)),
}).required().unknown(true).optional())
}

View file

@ -1,11 +1,16 @@
jest.mock("nodemailer")
import { TestConfiguration, mocks, API } from "../../../../tests"
import { TestConfiguration, mocks } from "../../../../tests"
const sendMailMock = mocks.email.mock()
import { events } from "@budibase/backend-core"
const expectSetAuthCookie = (res: any) => {
expect(
res.get("Set-Cookie").find((c: string) => c.startsWith("budibase:auth"))
).toBeDefined()
}
describe("/api/global/auth", () => {
const config = new TestConfiguration()
const api = new API(config)
beforeAll(async () => {
await config.beforeAll()
@ -19,90 +24,155 @@ describe("/api/global/auth", () => {
jest.clearAllMocks()
})
it("should logout", async () => {
await api.auth.logout()
expect(events.auth.logout).toBeCalledTimes(1)
})
it("should be able to generate password reset email", async () => {
const { res, code } = await api.auth.requestPasswordReset(sendMailMock)
const user = await config.getUser("test@test.com")
expect(res.body).toEqual({
message: "Please check your email for a reset link.",
describe("password", () => {
describe("POST /api/global/auth/:tenantId/login", () => {
it("should login", () => {})
})
expect(sendMailMock).toHaveBeenCalled()
expect(code).toBeDefined()
expect(events.user.passwordResetRequested).toBeCalledTimes(1)
expect(events.user.passwordResetRequested).toBeCalledWith(user)
describe("POST /api/global/auth/logout", () => {
it("should logout", async () => {
await config.api.auth.logout()
expect(events.auth.logout).toBeCalledTimes(1)
// TODO: Verify sessions deleted
})
})
describe("POST /api/global/auth/:tenantId/reset", () => {
it("should generate password reset email", async () => {
const { res, code } = await config.api.auth.requestPasswordReset(
sendMailMock
)
const user = await config.getUser("test@test.com")
expect(res.body).toEqual({
message: "Please check your email for a reset link.",
})
expect(sendMailMock).toHaveBeenCalled()
expect(code).toBeDefined()
expect(events.user.passwordResetRequested).toBeCalledTimes(1)
expect(events.user.passwordResetRequested).toBeCalledWith(user)
})
})
describe("POST /api/global/auth/:tenantId/reset/update", () => {
it("should reset password", async () => {
const { code } = await config.api.auth.requestPasswordReset(
sendMailMock
)
const user = await config.getUser("test@test.com")
delete user.password
const res = await config.api.auth.updatePassword(code)
expect(res.body).toEqual({ message: "password reset successfully." })
expect(events.user.passwordReset).toBeCalledTimes(1)
expect(events.user.passwordReset).toBeCalledWith(user)
// TODO: Login using new password
})
})
})
it("should allow resetting user password with code", async () => {
const { code } = await api.auth.requestPasswordReset(sendMailMock)
const user = await config.getUser("test@test.com")
delete user.password
describe("init", () => {
describe("POST /api/global/auth/init", () => {})
const res = await api.auth.updatePassword(code)
describe("GET /api/global/auth/init", () => {})
})
expect(res.body).toEqual({ message: "password reset successfully." })
expect(events.user.passwordReset).toBeCalledTimes(1)
expect(events.user.passwordReset).toBeCalledWith(user)
describe("datasource", () => {
// MULTI TENANT
describe("GET /api/global/auth/:tenantId/datasource/:provider", () => {})
describe("GET /api/global/auth/:tenantId/datasource/:provider/callback", () => {})
// SINGLE TENANT
describe("GET /api/global/auth/datasource/:provider/callback", () => {})
})
describe("google", () => {
// MULTI TENANT
describe("GET /api/global/auth/:tenantId/google", () => {})
describe("GET /api/global/auth/:tenantId/google/callback", () => {})
// SINGLE TENANT
describe("GET /api/global/auth/google/callback", () => {})
describe("GET /api/admin/auth/google/callback", () => {})
})
describe("oidc", () => {
const auth = require("@budibase/backend-core/auth")
const passportSpy = jest.spyOn(auth.passport, "authenticate")
let oidcConf
let chosenConfig: any
let configId: string
// mock the oidc strategy implementation and return value
let strategyFactory = jest.fn()
let mockStrategyReturn = jest.fn()
let mockStrategyConfig = jest.fn()
auth.oidc.fetchStrategyConfig = mockStrategyConfig
strategyFactory.mockReturnValue(mockStrategyReturn)
auth.oidc.strategyFactory = strategyFactory
beforeEach(async () => {
oidcConf = await config.saveOIDCConfig()
chosenConfig = oidcConf.config.configs[0]
configId = chosenConfig.uuid
mockStrategyConfig.mockReturnValue(chosenConfig)
jest.clearAllMocks()
mockGetWellKnownConfig()
// see: __mocks__/oauth
// for associated mocking inside passport
})
afterEach(() => {
expect(strategyFactory).toBeCalledWith(chosenConfig, expect.any(Function))
})
const generateOidcConfig = async () => {
const oidcConf = await config.saveOIDCConfig()
const chosenConfig = oidcConf.config.configs[0]
return chosenConfig.uuid
}
describe("oidc configs", () => {
it("should load strategy and delegate to passport", async () => {
await api.configs.getOIDCConfig(configId)
const mockGetWellKnownConfig = () => {
mocks.fetch.mockReturnValue({
ok: true,
json: () => ({
issuer: "test",
authorization_endpoint: "http://localhost/auth",
token_endpoint: "http://localhost/token",
userinfo_endpoint: "http://localhost/userinfo",
}),
})
}
expect(passportSpy).toBeCalledWith(mockStrategyReturn, {
scope: ["profile", "email", "offline_access"],
})
expect(passportSpy.mock.calls.length).toBe(1)
// MULTI TENANT
describe("GET /api/global/auth/:tenantId/oidc/configs/:configId", () => {
it("redirects to auth provider", async () => {
const configId = await generateOidcConfig()
const res = await config.api.configs.getOIDCConfig(configId)
expect(res.status).toBe(302)
const location: string = res.get("location")
expect(
location.startsWith(
"http://localhost/auth?response_type=code&client_id=clientId&redirect_uri=http%3A%2F%2Flocalhost%3A10000%2Fapi%2Fglobal%2Fauth%2Fdefault%2Foidc%2Fcallback&scope=openid%20profile%20email%20offline_access"
)
).toBe(true)
})
})
describe("oidc callback", () => {
it("should load strategy and delegate to passport", async () => {
await api.configs.OIDCCallback(configId)
describe("GET /api/global/auth/:tenantId/oidc/callback", () => {
it("logs in", async () => {
const configId = await generateOidcConfig()
const preAuthRes = await config.api.configs.getOIDCConfig(configId)
expect(passportSpy).toBeCalledWith(
mockStrategyReturn,
{
successRedirect: "/",
failureRedirect: "/error",
},
expect.anything()
)
expect(passportSpy.mock.calls.length).toBe(1)
const res = await config.api.configs.OIDCCallback(configId, preAuthRes)
expect(events.auth.login).toBeCalledWith("oidc")
expect(events.auth.login).toBeCalledTimes(1)
expect(res.status).toBe(302)
const location: string = res.get("location")
expect(location).toBe("/")
expectSetAuthCookie(res)
})
})
// SINGLE TENANT
describe("GET /api/global/auth/oidc/callback", () => {})
describe("GET /api/global/auth/oidc/callback", () => {})
describe("GET /api/admin/auth/oidc/callback", () => {})
})
})

View file

@ -1,12 +1,11 @@
// mock the email system
jest.mock("nodemailer")
import { TestConfiguration, structures, mocks, API } from "../../../../tests"
import { TestConfiguration, structures, mocks } from "../../../../tests"
mocks.email.mock()
import { Config, events } from "@budibase/backend-core"
describe("configs", () => {
const config = new TestConfiguration()
const api = new API(config)
beforeAll(async () => {
await config.beforeAll()
@ -28,7 +27,7 @@ describe("configs", () => {
_rev,
}
const res = await api.configs.saveConfig(data)
const res = await config.api.configs.saveConfig(data)
return {
...data,
@ -235,7 +234,7 @@ describe("configs", () => {
expect(events.org.nameUpdated).toBeCalledTimes(1)
expect(events.org.logoUpdated).toBeCalledTimes(1)
expect(events.org.platformURLUpdated).toBeCalledTimes(1)
config.modeAccount()
config.modeCloud()
})
})
@ -257,7 +256,7 @@ describe("configs", () => {
expect(events.org.nameUpdated).toBeCalledTimes(1)
expect(events.org.logoUpdated).toBeCalledTimes(1)
expect(events.org.platformURLUpdated).toBeCalledTimes(1)
config.modeAccount()
config.modeCloud()
})
})
})
@ -266,7 +265,7 @@ describe("configs", () => {
it("should return the correct checklist status based on the state of the budibase installation", async () => {
await config.saveSmtpConfig()
const res = await api.configs.getConfigChecklist()
const res = await config.api.configs.getConfigChecklist()
const checklist = res.body
expect(checklist.apps.checked).toBeFalsy()

View file

@ -1,11 +1,10 @@
jest.mock("nodemailer")
import { TestConfiguration, mocks, API } from "../../../../tests"
import { TestConfiguration, mocks } from "../../../../tests"
const sendMailMock = mocks.email.mock()
import { EmailTemplatePurpose } from "../../../../constants"
describe("/api/global/email", () => {
const config = new TestConfiguration()
const api = new API(config)
beforeAll(async () => {
await config.beforeAll()
@ -20,7 +19,9 @@ describe("/api/global/email", () => {
await config.saveSmtpConfig()
await config.saveSettingsConfig()
const res = await api.emails.sendEmail(EmailTemplatePurpose.INVITATION)
const res = await config.api.emails.sendEmail(
EmailTemplatePurpose.INVITATION
)
expect(res.body.message).toBeDefined()
expect(sendMailMock).toHaveBeenCalled()

View file

@ -0,0 +1,31 @@
import { TestConfiguration } from "../../../../tests"
// TODO
describe("/api/global/license", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
afterEach(() => {
jest.clearAllMocks()
})
describe("POST /api/global/license/activate", () => {
it("activates license", () => {})
})
describe("POST /api/global/license/refresh", () => {})
describe("GET /api/global/license/info", () => {})
describe("DELETE /api/global/license/info", () => {})
describe("GET /api/global/license/usage", () => {})
})

View file

@ -1,4 +1,4 @@
import { TestConfiguration, API } from "../../../../tests"
import { TestConfiguration } from "../../../../tests"
import { EmailTemplatePurpose } from "../../../../constants"
const nodemailer = require("nodemailer")
const fetch = require("node-fetch")
@ -8,7 +8,6 @@ jest.setTimeout(30000)
describe("/api/global/email", () => {
const config = new TestConfiguration()
const api = new API(config)
beforeAll(async () => {
await config.beforeAll()
@ -35,7 +34,7 @@ describe("/api/global/email", () => {
await Promise.race([config.saveEtherealSmtpConfig(), timeout()])
await Promise.race([config.saveSettingsConfig(), timeout()])
const res = await api.emails.sendEmail(purpose).timeout(20000)
const res = await config.api.emails.sendEmail(purpose).timeout(20000)
// ethereal hiccup, can't test right now
if (res.status >= 300) {
return

View file

@ -0,0 +1,27 @@
import { TestConfiguration } from "../../../../tests"
// TODO
describe("/api/global/roles", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
afterEach(() => {
jest.clearAllMocks()
})
describe("GET /api/global/roles", () => {
it("retrieves roles", () => {})
})
describe("GET /api/global/roles/:appId", () => {})
describe("DELETE /api/global/roles/:appId", () => {})
})

View file

@ -1,10 +1,9 @@
jest.mock("nodemailer")
import { TestConfiguration, API, mocks } from "../../../../tests"
import { TestConfiguration, mocks } from "../../../../tests"
import { events } from "@budibase/backend-core"
describe("/api/global/self", () => {
const config = new TestConfiguration()
const api = new API(config)
beforeAll(async () => {
await config.beforeAll()
@ -24,7 +23,7 @@ describe("/api/global/self", () => {
await config.createSession(user)
delete user.password
const res = await api.self.updateSelf(user)
const res = await config.api.self.updateSelf(user)
const dbUser = await config.getUser(user.email)
user._rev = dbUser._rev
@ -40,7 +39,7 @@ describe("/api/global/self", () => {
await config.createSession(user)
user.password = "newPassword"
const res = await api.self.updateSelf(user)
const res = await config.api.self.updateSelf(user)
const dbUser = await config.getUser(user.email)
user._rev = dbUser._rev

View file

@ -0,0 +1,35 @@
import { TestConfiguration } from "../../../../tests"
// TODO
describe("/api/global/template", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
afterEach(() => {
jest.clearAllMocks()
})
describe("GET /api/global/template/definitions", () => {
it("retrieves definitions", () => {})
})
describe("POST /api/global/template", () => {})
describe("GET /api/global/template", () => {})
describe("GET /api/global/template/:type", () => {})
describe("GET /api/global/template/:ownerId", () => {})
describe("GET /api/global/template/:id", () => {})
describe("DELETE /api/global/template/:id/:rev", () => {})
})

View file

@ -6,14 +6,12 @@ import {
mocks,
structures,
TENANT_1,
API,
} from "../../../../tests"
const sendMailMock = mocks.email.mock()
import { events, tenancy } from "@budibase/backend-core"
describe("/api/global/users", () => {
const config = new TestConfiguration()
const api = new API(config)
beforeAll(async () => {
await config.beforeAll()
@ -30,7 +28,10 @@ describe("/api/global/users", () => {
describe("invite", () => {
it("should be able to generate an invitation", async () => {
const email = structures.users.newEmail()
const { code, res } = await api.users.sendUserInvite(sendMailMock, email)
const { code, res } = await config.api.users.sendUserInvite(
sendMailMock,
email
)
expect(res.body).toEqual({ message: "Invitation has been sent." })
expect(sendMailMock).toHaveBeenCalled()
@ -39,7 +40,7 @@ describe("/api/global/users", () => {
})
it("should not be able to generate an invitation for existing user", async () => {
const { code, res } = await api.users.sendUserInvite(
const { code, res } = await config.api.users.sendUserInvite(
sendMailMock,
config.defaultUser!.email,
400
@ -53,9 +54,12 @@ describe("/api/global/users", () => {
it("should be able to create new user from invite", async () => {
const email = structures.users.newEmail()
const { code } = await api.users.sendUserInvite(sendMailMock, email)
const { code } = await config.api.users.sendUserInvite(
sendMailMock,
email
)
const res = await api.users.acceptInvite(code)
const res = await config.api.users.acceptInvite(code)
expect(res.body._id).toBeDefined()
const user = await config.getUser(email)
@ -74,7 +78,7 @@ describe("/api/global/users", () => {
})
const request = [newUserInvite(), newUserInvite()]
const res = await api.users.sendMultiUserInvite(request)
const res = await config.api.users.sendMultiUserInvite(request)
const body = res.body as InviteUsersResponse
expect(body.successful.length).toBe(2)
@ -86,7 +90,7 @@ describe("/api/global/users", () => {
it("should not be able to generate an invitation for existing user", async () => {
const request = [{ email: config.defaultUser!.email, userInfo: {} }]
const res = await api.users.sendMultiUserInvite(request)
const res = await config.api.users.sendMultiUserInvite(request)
const body = res.body as InviteUsersResponse
expect(body.successful.length).toBe(0)
@ -102,7 +106,7 @@ describe("/api/global/users", () => {
const user = await config.createUser()
jest.clearAllMocks()
const response = await api.users.bulkCreateUsers([user])
const response = await config.api.users.bulkCreateUsers([user])
expect(response.created?.successful.length).toBe(0)
expect(response.created?.unsuccessful.length).toBe(1)
@ -115,7 +119,7 @@ describe("/api/global/users", () => {
jest.resetAllMocks()
await tenancy.doInTenant(TENANT_1, async () => {
const response = await api.users.bulkCreateUsers([user])
const response = await config.api.users.bulkCreateUsers([user])
expect(response.created?.successful.length).toBe(0)
expect(response.created?.unsuccessful.length).toBe(1)
@ -126,11 +130,11 @@ describe("/api/global/users", () => {
it("should ignore accounts using the same email", async () => {
const account = structures.accounts.account()
const resp = await api.accounts.saveMetadata(account)
const resp = await config.api.accounts.saveMetadata(account)
const user = structures.users.user({ email: resp.email })
jest.clearAllMocks()
const response = await api.users.bulkCreateUsers([user])
const response = await config.api.users.bulkCreateUsers([user])
expect(response.created?.successful.length).toBe(0)
expect(response.created?.unsuccessful.length).toBe(1)
@ -143,7 +147,11 @@ describe("/api/global/users", () => {
const admin = structures.users.adminUser()
const user = structures.users.user()
const response = await api.users.bulkCreateUsers([builder, admin, user])
const response = await config.api.users.bulkCreateUsers([
builder,
admin,
user,
])
expect(response.created?.successful.length).toBe(3)
expect(response.created?.successful[0].email).toBe(builder.email)
@ -160,7 +168,7 @@ describe("/api/global/users", () => {
it("should be able to create a basic user", async () => {
const user = structures.users.user()
await api.users.saveUser(user)
await config.api.users.saveUser(user)
expect(events.user.created).toBeCalledTimes(1)
expect(events.user.updated).not.toBeCalled()
@ -171,7 +179,7 @@ describe("/api/global/users", () => {
it("should be able to create an admin user", async () => {
const user = structures.users.adminUser()
await api.users.saveUser(user)
await config.api.users.saveUser(user)
expect(events.user.created).toBeCalledTimes(1)
expect(events.user.updated).not.toBeCalled()
@ -182,7 +190,7 @@ describe("/api/global/users", () => {
it("should be able to create a builder user", async () => {
const user = structures.users.builderUser()
await api.users.saveUser(user)
await config.api.users.saveUser(user)
expect(events.user.created).toBeCalledTimes(1)
expect(events.user.updated).not.toBeCalled()
@ -197,7 +205,7 @@ describe("/api/global/users", () => {
app_456: "role2",
}
await api.users.saveUser(user)
await config.api.users.saveUser(user)
const savedUser = await config.getUser(user.email)
expect(events.user.created).toBeCalledTimes(1)
@ -213,7 +221,7 @@ describe("/api/global/users", () => {
delete user._id
delete user._rev
const response = await api.users.saveUser(user, 400)
const response = await config.api.users.saveUser(user, 400)
expect(response.body.message).toBe(`Unavailable`)
expect(events.user.created).toBeCalledTimes(0)
@ -225,7 +233,7 @@ describe("/api/global/users", () => {
await tenancy.doInTenant(TENANT_1, async () => {
delete user._id
const response = await api.users.saveUser(user, 400)
const response = await config.api.users.saveUser(user, 400)
expect(response.body.message).toBe(`Unavailable`)
expect(events.user.created).toBeCalledTimes(0)
@ -237,7 +245,7 @@ describe("/api/global/users", () => {
const account = structures.accounts.cloudAccount()
mocks.accounts.getAccount.mockReturnValueOnce(account)
const response = await api.users.saveUser(user, 400)
const response = await config.api.users.saveUser(user, 400)
expect(response.body.message).toBe(`Unavailable`)
expect(events.user.created).toBeCalledTimes(0)
@ -245,20 +253,20 @@ describe("/api/global/users", () => {
it("should not be able to create a user with the same email and different casing", async () => {
const user = structures.users.user()
await api.users.saveUser(user)
await config.api.users.saveUser(user)
user.email = user.email.toUpperCase()
await api.users.saveUser(user, 400)
await config.api.users.saveUser(user, 400)
expect(events.user.created).toBeCalledTimes(1)
})
it("should not be able to bulk create a user with the same email and different casing", async () => {
const user = structures.users.user()
await api.users.saveUser(user)
await config.api.users.saveUser(user)
user.email = user.email.toUpperCase()
await api.users.bulkCreateUsers([user])
await config.api.users.bulkCreateUsers([user])
expect(events.user.created).toBeCalledTimes(1)
})
@ -269,7 +277,7 @@ describe("/api/global/users", () => {
const user = await config.createUser()
jest.clearAllMocks()
await api.users.saveUser(user)
await config.api.users.saveUser(user)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
@ -284,7 +292,7 @@ describe("/api/global/users", () => {
user.forceResetPassword = true
user.password = "tempPassword"
await api.users.saveUser(user)
await config.api.users.saveUser(user)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
@ -297,7 +305,7 @@ describe("/api/global/users", () => {
const user = await config.createUser()
jest.clearAllMocks()
await api.users.saveUser(structures.users.adminUser(user))
await config.api.users.saveUser(structures.users.adminUser(user))
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
@ -309,7 +317,7 @@ describe("/api/global/users", () => {
const user = await config.createUser()
jest.clearAllMocks()
await api.users.saveUser(structures.users.builderUser(user))
await config.api.users.saveUser(structures.users.builderUser(user))
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
@ -323,7 +331,7 @@ describe("/api/global/users", () => {
user.admin!.global = false
user.builder!.global = false
await api.users.saveUser(user)
await config.api.users.saveUser(user)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
@ -336,7 +344,7 @@ describe("/api/global/users", () => {
jest.clearAllMocks()
user.builder!.global = false
await api.users.saveUser(user)
await config.api.users.saveUser(user)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
@ -352,7 +360,7 @@ describe("/api/global/users", () => {
app_456: "role2",
}
await api.users.saveUser(user)
await config.api.users.saveUser(user)
const savedUser = await config.getUser(user.email)
expect(events.user.created).not.toBeCalled()
@ -372,7 +380,7 @@ describe("/api/global/users", () => {
jest.clearAllMocks()
user.roles = {}
await api.users.saveUser(user)
await config.api.users.saveUser(user)
const savedUser = await config.getUser(user.email)
expect(events.user.created).not.toBeCalled()
@ -395,7 +403,7 @@ describe("/api/global/users", () => {
app_456: "role2-edit",
}
await api.users.saveUser(user)
await config.api.users.saveUser(user)
const savedUser = await config.getUser(user.email)
expect(events.user.created).not.toBeCalled()
@ -411,7 +419,7 @@ describe("/api/global/users", () => {
const user = await config.createUser(structures.users.user({ email }))
user.email = "new@test.com"
const response = await api.users.saveUser(user, 400)
const response = await config.api.users.saveUser(user, 400)
const dbUser = await config.getUser(email)
user.email = email
@ -424,7 +432,7 @@ describe("/api/global/users", () => {
it("should not be able to bulk delete current user", async () => {
const user = await config.defaultUser!
const response = await api.users.bulkDeleteUsers([user._id!], 400)
const response = await config.api.users.bulkDeleteUsers([user._id!], 400)
expect(response.message).toBe("Unable to delete self.")
expect(events.user.deleted).not.toBeCalled()
@ -436,7 +444,7 @@ describe("/api/global/users", () => {
account.budibaseUserId = user._id!
mocks.accounts.getAccountByTenantId.mockReturnValue(account)
const response = await api.users.bulkDeleteUsers([user._id!])
const response = await config.api.users.bulkDeleteUsers([user._id!])
expect(response.deleted?.successful.length).toBe(0)
expect(response.deleted?.unsuccessful.length).toBe(1)
@ -454,7 +462,7 @@ describe("/api/global/users", () => {
const builder = structures.users.builderUser()
const admin = structures.users.adminUser()
const user = structures.users.user()
const createdUsers = await api.users.bulkCreateUsers([
const createdUsers = await config.api.users.bulkCreateUsers([
builder,
admin,
user,
@ -463,7 +471,7 @@ describe("/api/global/users", () => {
const toDelete = createdUsers.created?.successful.map(
u => u._id!
) as string[]
const response = await api.users.bulkDeleteUsers(toDelete)
const response = await config.api.users.bulkDeleteUsers(toDelete)
expect(response.deleted?.successful.length).toBe(3)
expect(response.deleted?.unsuccessful.length).toBe(0)
@ -478,7 +486,7 @@ describe("/api/global/users", () => {
const user = await config.createUser()
jest.clearAllMocks()
await api.users.deleteUser(user._id!)
await config.api.users.deleteUser(user._id!)
expect(events.user.deleted).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).not.toBeCalled()
@ -489,7 +497,7 @@ describe("/api/global/users", () => {
const user = await config.createUser(structures.users.adminUser())
jest.clearAllMocks()
await api.users.deleteUser(user._id!)
await config.api.users.deleteUser(user._id!)
expect(events.user.deleted).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1)
@ -500,7 +508,7 @@ describe("/api/global/users", () => {
const user = await config.createUser(structures.users.builderUser())
jest.clearAllMocks()
await api.users.deleteUser(user._id!)
await config.api.users.deleteUser(user._id!)
expect(events.user.deleted).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1)
@ -512,7 +520,7 @@ describe("/api/global/users", () => {
const account = structures.accounts.cloudAccount()
mocks.accounts.getAccount.mockReturnValueOnce(account)
const response = await api.users.deleteUser(user._id!, 400)
const response = await config.api.users.deleteUser(user._id!, 400)
expect(response.body.message).toBe("Account holder cannot be deleted")
})
@ -523,7 +531,7 @@ describe("/api/global/users", () => {
account.email = user.email
mocks.accounts.getAccount.mockReturnValueOnce(account)
const response = await api.users.deleteUser(user._id!, 400)
const response = await config.api.users.deleteUser(user._id!, 400)
expect(response.body.message).toBe("Unable to delete self.")
})

View file

@ -0,0 +1,29 @@
import { TestConfiguration } from "../../../../tests"
// TODO
describe("/api/global/workspaces", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
afterEach(() => {
jest.clearAllMocks()
})
describe("GET /api/global/workspaces", () => {
it("retrieves workspaces", () => {})
})
describe("DELETE /api/global/workspaces/:id", () => {})
describe("GET /api/global/workspaces", () => {})
describe("GET /api/global/workspaces/:id", () => {})
})

View file

@ -1,8 +0,0 @@
const Router = require("@koa/router")
const controller = require("../../controllers/system/environment")
const router = new Router()
router.get("/api/system/environment", controller.fetch)
module.exports = router

View file

@ -0,0 +1,8 @@
import Router from "@koa/router"
import * as controller from "../../controllers/system/environment"
const router = new Router()
router.get("/api/system/environment", controller.fetch)
export default router

View file

@ -1,8 +0,0 @@
const Router = require("@koa/router")
const controller = require("../../controllers/system/status")
const router = new Router()
router.get("/api/system/status", controller.fetch)
module.exports = router

View file

@ -0,0 +1,8 @@
import Router from "@koa/router"
import * as controller from "../../controllers/system/status"
const router = new Router()
router.get("/api/system/status", controller.fetch)
export default router

View file

@ -1,12 +0,0 @@
const Router = require("@koa/router")
const controller = require("../../controllers/system/tenants")
const { adminOnly } = require("@budibase/backend-core/auth")
const router = new 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

View file

@ -0,0 +1,13 @@
import Router from "@koa/router"
import * as controller from "../../controllers/system/tenants"
import { middleware } from "@budibase/backend-core"
const router = new Router()
router.delete(
"/api/system/tenants/:tenantId",
middleware.adminOnly,
controller.delete
)
export default router

View file

@ -1,10 +1,9 @@
import sdk from "../../../../sdk"
import { TestConfiguration, structures, API } from "../../../../tests"
import { TestConfiguration, structures } from "../../../../tests"
import { v4 as uuid } from "uuid"
describe("accounts", () => {
const config = new TestConfiguration()
const api = new API(config)
beforeAll(async () => {
await config.beforeAll()
@ -23,7 +22,7 @@ describe("accounts", () => {
it("saves account metadata", async () => {
let account = structures.accounts.account()
const response = await api.accounts.saveMetadata(account)
const response = await config.api.accounts.saveMetadata(account)
const id = sdk.accounts.formatAccountMetadataId(account.accountId)
const metadata = await sdk.accounts.getMetadata(id)
@ -34,9 +33,9 @@ describe("accounts", () => {
describe("destroyMetadata", () => {
it("destroys account metadata", async () => {
const account = structures.accounts.account()
await api.accounts.saveMetadata(account)
await config.api.accounts.saveMetadata(account)
await api.accounts.destroyMetadata(account.accountId)
await config.api.accounts.destroyMetadata(account.accountId)
const deleted = await sdk.accounts.getMetadata(account.accountId)
expect(deleted).toBe(undefined)
@ -45,7 +44,7 @@ describe("accounts", () => {
it("destroys account metadata that does not exist", async () => {
const id = uuid()
const response = await api.accounts.destroyMetadata(id)
const response = await config.api.accounts.destroyMetadata(id)
expect(response.status).toBe(204)
})

View file

@ -0,0 +1,29 @@
import { TestConfiguration } from "../../../../tests"
describe("/api/system/environment", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
afterEach(() => {
jest.clearAllMocks()
})
describe("GET /api/system/environment", () => {
it("returns the expected environment", async () => {
const env = await config.api.environment.getEnvironment()
expect(env.body).toEqual({
cloud: true,
disableAccountPortal: false,
isDev: false,
multiTenancy: true,
})
})
})
})

View file

@ -0,0 +1,63 @@
const migrateFn = jest.fn()
import { TestConfiguration } from "../../../../tests"
jest.mock("../../../../migrations", () => {
return {
...jest.requireActual("../../../../migrations"),
migrate: migrateFn,
}
})
describe("/api/system/migrations", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
afterEach(() => {
jest.clearAllMocks()
})
describe("POST /api/system/migrations/run", () => {
it("fails with no internal api key", async () => {
const res = await config.api.migrations.runMigrations({
headers: {},
status: 403,
})
expect(res.text).toBe("Unauthorized - no public worker access")
expect(migrateFn).toBeCalledTimes(0)
})
it("runs migrations", async () => {
const res = await config.api.migrations.runMigrations()
expect(res.text).toBe("OK")
expect(migrateFn).toBeCalledTimes(1)
})
})
describe("DELETE /api/system/migrations/definitions", () => {
it("fails with no internal api key", async () => {
const res = await config.api.migrations.getMigrationDefinitions({
headers: {},
status: 403,
})
expect(res.text).toBe("Unauthorized - no public worker access")
})
it("returns definitions", async () => {
const res = await config.api.migrations.getMigrationDefinitions()
expect(res.body).toEqual([
{
name: "global_info_sync_users",
type: "global",
},
])
})
})
})

View file

@ -0,0 +1,36 @@
import { TestConfiguration } from "../../../../tests"
describe("/api/system/restore", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
afterEach(() => {
jest.clearAllMocks()
})
describe("POST /api/global/restore", () => {
it("doesn't allow restore in cloud", async () => {
const res = await config.api.restore.restored({ status: 405 })
expect(res.body).toEqual({
message: "This operation is not allowed in cloud.",
status: 405,
})
})
it("restores in self host", async () => {
config.modeSelf()
const res = await config.api.restore.restored()
expect(res.body).toEqual({
message: "System prepared after restore.",
})
config.modeCloud()
})
})
})

View file

@ -0,0 +1,48 @@
import { TestConfiguration } from "../../../../tests"
import { accounts } from "@budibase/backend-core"
import { mocks } from "@budibase/backend-core/tests"
describe("/api/system/status", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
afterEach(() => {
jest.clearAllMocks()
})
describe("GET /api/system/status", () => {
it("returns status in self host", async () => {
config.modeSelf()
const res = await config.api.status.getStatus()
expect(res.body).toEqual({
health: {
passing: true,
},
})
expect(accounts.getStatus).toBeCalledTimes(0)
config.modeCloud()
})
it("returns status in cloud", async () => {
const value = {
health: {
passing: false,
},
}
mocks.accounts.getStatus.mockReturnValueOnce(value)
const res = await config.api.status.getStatus()
expect(accounts.getStatus).toBeCalledTimes(1)
expect(res.body).toEqual(value)
})
})
})

View file

@ -0,0 +1,61 @@
import { TestConfiguration } from "../../../../tests"
import { tenancy } from "@budibase/backend-core"
describe("/api/global/tenants", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
afterEach(() => {
jest.clearAllMocks()
})
describe("DELETE /api/system/tenants/:tenantId", () => {
it("allows deleting the current tenant", async () => {
const user = await config.createTenant()
await config.api.tenants.delete(user.tenantId, {
headers: config.authHeaders(user),
})
})
it("rejects deleting another tenant", async () => {
const user1 = await config.createTenant()
// create a second user in another tenant
const user2 = await config.createTenant()
const status = 403
const res = await config.api.tenants.delete(user1.tenantId, {
status,
headers: config.authHeaders(user2),
})
expect(res.body).toEqual({
message: "Tenant ID does not match current user",
status,
})
})
it("rejects non-admin", async () => {
const user1 = await config.createTenant()
// create an internal non-admin user
const user2 = await tenancy.doInTenant(user1.tenantId, () => {
return config.createUser()
})
await config.createSession(user2)
const res = await config.api.tenants.delete(user1.tenantId, {
status: 403,
headers: config.authHeaders(user2),
})
expect(res.body).toEqual(config.adminOnlyResponse())
})
})
})

View file

@ -0,0 +1,73 @@
import { TestConfiguration, structures } from "../../tests"
import { constants } from "@budibase/backend-core"
describe("tenancy middleware", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
afterEach(() => {
jest.clearAllMocks()
})
it("should get tenant id from user", async () => {
const user = await config.createTenant()
await config.createSession(user)
const res = await config.api.self.getSelf(user)
expect(res.headers[constants.Headers.TENANT_ID]).toBe(user.tenantId)
})
it("should get tenant id from header", async () => {
const tenantId = structures.uuid()
const headers = {
[constants.Headers.TENANT_ID]: tenantId,
}
const res = await config.request
.get(`/api/global/configs/checklist`)
.set(headers)
expect(res.headers[constants.Headers.TENANT_ID]).toBe(tenantId)
})
it("should get tenant id from query param", async () => {
const tenantId = structures.uuid()
const res = await config.request.get(
`/api/global/configs/checklist?tenantId=${tenantId}`
)
expect(res.headers[constants.Headers.TENANT_ID]).toBe(tenantId)
})
it("should get tenant id from subdomain", async () => {
const tenantId = structures.uuid()
const headers = {
host: `${tenantId}.localhost:10000`,
}
const res = await config.request
.get(`/api/global/configs/checklist`)
.set(headers)
expect(res.headers[constants.Headers.TENANT_ID]).toBe(tenantId)
})
it("should get tenant id from path variable", async () => {
const user = await config.createTenant()
const res = await config.request
.post(`/api/global/auth/${user.tenantId}/login`)
.send({
username: user.email,
password: user.password,
})
expect(res.headers[constants.Headers.TENANT_ID]).toBe(user.tenantId)
})
it("should throw when no tenant id is found", async () => {
const res = await config.request.get(`/api/global/configs/checklist`)
expect(res.status).toBe(403)
expect(res.text).toBe("Tenant id not set")
expect(res.headers[constants.Headers.TENANT_ID]).toBe(undefined)
})
})

View file

@ -8,34 +8,35 @@ import { Config } from "../constants"
import {
users,
tenancy,
Cookie,
Header,
sessions,
auth,
constants,
env as coreEnv,
} from "@budibase/backend-core"
import { TENANT_ID, TENANT_1, CSRF_TOKEN } from "./structures"
import structures from "./structures"
import structures, { TENANT_ID, TENANT_1, CSRF_TOKEN } from "./structures"
import { CreateUserResponse, User, AuthToken } from "@budibase/types"
import API from "./api"
enum Mode {
ACCOUNT = "account",
CLOUD = "cloud",
SELF = "self",
}
class TestConfiguration {
server: any
request: any
api: API
defaultUser?: User
tenant1User?: User
constructor(
opts: { openServer: boolean; mode: Mode } = {
openServer: true,
mode: Mode.ACCOUNT,
mode: Mode.CLOUD,
}
) {
if (opts.mode === Mode.ACCOUNT) {
this.modeAccount()
if (opts.mode === Mode.CLOUD) {
this.modeCloud()
} else if (opts.mode === Mode.SELF) {
this.modeSelf()
}
@ -46,6 +47,8 @@ class TestConfiguration {
// we need the request for logging in, involves cookies, hard to fake
this.request = supertest(this.server)
}
this.api = new API(this)
}
getRequest() {
@ -54,20 +57,24 @@ class TestConfiguration {
// MODES
modeAccount = () => {
env.SELF_HOSTED = false
// @ts-ignore
env.MULTI_TENANCY = true
// @ts-ignore
env.DISABLE_ACCOUNT_PORTAL = false
setMultiTenancy = (value: boolean) => {
env._set("MULTI_TENANCY", value)
coreEnv._set("MULTI_TENANCY", value)
}
setSelfHosted = (value: boolean) => {
env._set("SELF_HOSTED", value)
coreEnv._set("SELF_HOSTED", value)
}
modeCloud = () => {
this.setSelfHosted(false)
this.setMultiTenancy(true)
}
modeSelf = () => {
env.SELF_HOSTED = true
// @ts-ignore
env.MULTI_TENANCY = false
// @ts-ignore
env.DISABLE_ACCOUNT_PORTAL = true
this.setSelfHosted(true)
this.setMultiTenancy(false)
}
// UTILS
@ -114,6 +121,25 @@ class TestConfiguration {
// TENANCY
createTenant = async (): Promise<User> => {
// create user / new tenant
const res = await this.api.users.createAdminUser()
// return the created user
const userRes = await this.api.users.getUser(res.userId, {
headers: {
...this.internalAPIHeaders(),
[constants.Header.TENANT_ID]: res.tenantId,
},
})
// create a session for the new user
const user = userRes.body
await this.createSession(user)
return user
}
getTenantId() {
try {
return tenancy.getTenantId()
@ -122,7 +148,69 @@ class TestConfiguration {
}
}
// USER / AUTH
// AUTH
async _createSession({
userId,
tenantId,
}: {
userId: string
tenantId: string
}) {
await sessions.createASession(userId!, {
sessionId: "sessionid",
tenantId: tenantId,
csrfToken: CSRF_TOKEN,
})
}
async createSession(user: User) {
return this._createSession({ userId: user._id!, tenantId: user.tenantId })
}
cookieHeader(cookies: any) {
if (!Array.isArray(cookies)) {
cookies = [cookies]
}
return {
Cookie: cookies,
}
}
authHeaders(user: User) {
const authToken: AuthToken = {
userId: user._id!,
sessionId: "sessionid",
tenantId: user.tenantId,
}
const authCookie = auth.jwt.sign(authToken, env.JWT_SECRET)
return {
Accept: "application/json",
...this.cookieHeader([`${constants.Cookie.Auth}=${authCookie}`]),
[constants.Header.CSRF_TOKEN]: CSRF_TOKEN,
}
}
defaultHeaders() {
const tenantId = this.getTenantId()
if (tenantId === TENANT_ID) {
return this.authHeaders(this.defaultUser!)
} else if (tenantId === TENANT_1) {
return this.authHeaders(this.tenant1User!)
} else {
throw new Error("could not determine auth headers to use")
}
}
internalAPIHeaders() {
return { [constants.Header.API_KEY]: env.INTERNAL_API_KEY }
}
adminOnlyResponse = () => {
return { message: "Admin user only endpoint.", status: 403 }
}
// USERS
async createDefaultUser() {
const user = structures.users.adminUser({
@ -140,45 +228,6 @@ class TestConfiguration {
this.tenant1User = await this.createUser(user)
}
async createSession(user: User) {
await sessions.createASession(user._id!, {
sessionId: "sessionid",
tenantId: user.tenantId,
csrfToken: CSRF_TOKEN,
})
}
cookieHeader(cookies: any) {
return {
Cookie: [cookies],
}
}
authHeaders(user: User) {
const authToken: AuthToken = {
userId: user._id!,
sessionId: "sessionid",
tenantId: user.tenantId,
}
const authCookie = auth.jwt.sign(authToken, env.JWT_SECRET)
return {
Accept: "application/json",
...this.cookieHeader([`${Cookie.Auth}=${authCookie}`]),
[Header.CSRF_TOKEN]: CSRF_TOKEN,
}
}
defaultHeaders() {
const tenantId = this.getTenantId()
if (tenantId === TENANT_ID) {
return this.authHeaders(this.defaultUser!)
} else if (tenantId === TENANT_1) {
return this.authHeaders(this.tenant1User!)
} else {
throw new Error("could not determine auth headers to use")
}
}
async getUser(email: string): Promise<User> {
return tenancy.doInTenant(this.getTenantId(), () => {
return users.getGlobalUserByEmail(email)
@ -242,7 +291,7 @@ class TestConfiguration {
getOIDConfigCookie(configId: string) {
const token = auth.jwt.sign(configId, env.JWT_SECRET)
return this.cookieHeader([[`${Cookie.OIDC_CONFIG}=${token}`]])
return this.cookieHeader([[`${constants.Cookie.OIDC_CONFIG}=${token}`]])
}
async saveOIDCConfig() {

View file

@ -1,20 +1,17 @@
import { Account, AccountMetadata } from "@budibase/types"
import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base"
export class AccountAPI {
config: TestConfiguration
request: any
export class AccountAPI extends TestAPI {
constructor(config: TestConfiguration) {
this.config = config
this.request = config.request
super(config)
}
saveMetadata = async (account: Account) => {
const res = await this.request
.put(`/api/system/accounts/${account.accountId}/metadata`)
.send(account)
.set(this.config.defaultHeaders())
.set(this.config.internalAPIHeaders())
.expect("Content-Type", /json/)
.expect(200)
return res.body as AccountMetadata
@ -23,6 +20,6 @@ export class AccountAPI {
destroyMetadata = (accountId: string) => {
return this.request
.del(`/api/system/accounts/${accountId}/metadata`)
.set(this.config.defaultHeaders())
.set(this.config.internalAPIHeaders())
}
}

View file

@ -1,12 +1,9 @@
import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base"
export class AuthAPI {
config: TestConfiguration
request: any
export class AuthAPI extends TestAPI {
constructor(config: TestConfiguration) {
this.config = config
this.request = config.request
super(config)
}
updatePassword = (code: string) => {

View file

@ -0,0 +1,16 @@
import TestConfiguration from "../TestConfiguration"
export interface TestAPIOpts {
headers?: any
status?: number
}
export abstract class TestAPI {
config: TestConfiguration
request: any
protected constructor(config: TestConfiguration) {
this.config = config
this.request = config.request
}
}

View file

@ -1,12 +1,9 @@
import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base"
export class ConfigAPI {
config: TestConfiguration
request: any
export class ConfigAPI extends TestAPI {
constructor(config: TestConfiguration) {
this.config = config
this.request = config.request
super(config)
}
getConfigChecklist = () => {
@ -26,10 +23,20 @@ export class ConfigAPI {
.expect(200)
}
OIDCCallback = (configId: string) => {
OIDCCallback = (configId: string, preAuthRes: any) => {
const cookie = this.config.cookieHeader(preAuthRes.get("set-cookie"))
const setKoaSession = cookie.Cookie.find((c: string) =>
c.includes("koa:sess")
)
const koaSession = setKoaSession.split("=")[1] + "=="
const sessionContent = JSON.parse(
Buffer.from(koaSession, "base64").toString("utf-8")
)
const handle = sessionContent["openidconnect:localhost"].state.handle
return this.request
.get(`/api/global/auth/${this.config.getTenantId()}/oidc/callback`)
.set(this.config.getOIDConfigCookie(configId))
.query({ code: "test", state: handle })
.set(cookie)
}
getOIDCConfig = (configId: string) => {

View file

@ -1,12 +1,9 @@
import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base"
export class EmailAPI {
config: TestConfiguration
request: any
export class EmailAPI extends TestAPI {
constructor(config: TestConfiguration) {
this.config = config
this.request = config.request
super(config)
}
sendEmail = (purpose: string) => {

View file

@ -0,0 +1,15 @@
import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base"
export class EnvironmentAPI extends TestAPI {
constructor(config: TestConfiguration) {
super(config)
}
getEnvironment = () => {
return this.request
.get(`/api/system/environment`)
.expect("Content-Type", /json/)
.expect(200)
}
}

View file

@ -5,6 +5,11 @@ import { ConfigAPI } from "./configs"
import { EmailAPI } from "./email"
import { SelfAPI } from "./self"
import { UserAPI } from "./users"
import { EnvironmentAPI } from "./environment"
import { MigrationAPI } from "./migrations"
import { StatusAPI } from "./status"
import { RestoreAPI } from "./restore"
import { TenantAPI } from "./tenants"
export default class API {
accounts: AccountAPI
@ -13,6 +18,11 @@ export default class API {
emails: EmailAPI
self: SelfAPI
users: UserAPI
environment: EnvironmentAPI
migrations: MigrationAPI
status: StatusAPI
restore: RestoreAPI
tenants: TenantAPI
constructor(config: TestConfiguration) {
this.accounts = new AccountAPI(config)
@ -21,5 +31,10 @@ export default class API {
this.emails = new EmailAPI(config)
this.self = new SelfAPI(config)
this.users = new UserAPI(config)
this.environment = new EnvironmentAPI(config)
this.migrations = new MigrationAPI(config)
this.status = new StatusAPI(config)
this.restore = new RestoreAPI(config)
this.tenants = new TenantAPI(config)
}
}

View file

@ -0,0 +1,22 @@
import TestConfiguration from "../TestConfiguration"
import { TestAPI, TestAPIOpts } from "./base"
export class MigrationAPI extends TestAPI {
constructor(config: TestConfiguration) {
super(config)
}
runMigrations = (opts?: TestAPIOpts) => {
return this.request
.post(`/api/system/migrations/run`)
.set(opts?.headers ? opts.headers : this.config.internalAPIHeaders())
.expect(opts?.status ? opts.status : 200)
}
getMigrationDefinitions = (opts?: TestAPIOpts) => {
return this.request
.get(`/api/system/migrations/definitions`)
.set(opts?.headers ? opts.headers : this.config.internalAPIHeaders())
.expect(opts?.status ? opts.status : 200)
}
}

View file

@ -0,0 +1,14 @@
import TestConfiguration from "../TestConfiguration"
import { TestAPI, TestAPIOpts } from "./base"
export class RestoreAPI extends TestAPI {
constructor(config: TestConfiguration) {
super(config)
}
restored = (opts?: TestAPIOpts) => {
return this.request
.post(`/api/system/restored`)
.expect(opts?.status ? opts.status : 200)
}
}

View file

@ -1,13 +1,10 @@
import TestConfiguration from "../TestConfiguration"
import { User } from "@budibase/types"
import { TestAPI } from "./base"
export class SelfAPI {
config: TestConfiguration
request: any
export class SelfAPI extends TestAPI {
constructor(config: TestConfiguration) {
this.config = config
this.request = config.request
super(config)
}
updateSelf = (user: User) => {
@ -18,4 +15,12 @@ export class SelfAPI {
.expect("Content-Type", /json/)
.expect(200)
}
getSelf = (user: User) => {
return this.request
.get(`/api/global/self`)
.set(this.config.authHeaders(user))
.expect("Content-Type", /json/)
.expect(200)
}
}

View file

@ -0,0 +1,12 @@
import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base"
export class StatusAPI extends TestAPI {
constructor(config: TestConfiguration) {
super(config)
}
getStatus = () => {
return this.request.get(`/api/system/status`).expect(200)
}
}

View file

@ -0,0 +1,15 @@
import TestConfiguration from "../TestConfiguration"
import { TestAPI, TestAPIOpts } from "./base"
export class TenantAPI extends TestAPI {
constructor(config: TestConfiguration) {
super(config)
}
delete = (tenantId: string, opts?: TestAPIOpts) => {
return this.request
.delete(`/api/system/tenants/${tenantId}`)
.set(opts?.headers)
.expect(opts?.status ? opts.status : 204)
}
}

View file

@ -3,16 +3,16 @@ import {
BulkUserRequest,
InviteUsersRequest,
User,
CreateAdminUserRequest,
} from "@budibase/types"
import * as structures from "../structures"
import { generator } from "@budibase/backend-core/tests"
import TestConfiguration from "../TestConfiguration"
import { TestAPI, TestAPIOpts } from "./base"
export class UserAPI {
config: TestConfiguration
request: any
export class UserAPI extends TestAPI {
constructor(config: TestConfiguration) {
this.config = config
this.request = config.request
super(config)
}
// INVITE
@ -91,6 +91,30 @@ export class UserAPI {
// USER
createAdminUser = async (
request?: CreateAdminUserRequest,
opts?: TestAPIOpts
) => {
if (!request) {
request = {
email: structures.email(),
password: generator.string(),
tenantId: structures.uuid(),
}
}
const res = await this.request
.post(`/api/global/users/init`)
.send(request)
.set(this.config.internalAPIHeaders())
.expect("Content-Type", /json/)
.expect(opts?.status ? opts.status : 200)
return {
...request,
userId: res.body._id,
}
}
saveUser = (user: User, status?: number) => {
return this.request
.post(`/api/global/users`)
@ -107,4 +131,12 @@ export class UserAPI {
.expect("Content-Type", /json/)
.expect(status ? status : 200)
}
getUser = (userId: string, opts?: TestAPIOpts) => {
return this.request
.get(`/api/global/users/${userId}`)
.set(opts?.headers ? opts.headers : this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(opts?.status ? opts.status : 200)
}
}

Some files were not shown because too many files have changed in this diff Show more