1
0
Fork 0
mirror of synced 2024-06-14 08:24:48 +12:00

update bulk create and bulk delete backend

This commit is contained in:
Rory Powell 2022-08-25 19:41:47 +01:00
parent c29f3768fa
commit a519eb0cdc
66 changed files with 1770 additions and 1124 deletions

View file

@ -1,11 +1,11 @@
const passport = require("koa-passport")
const LocalStrategy = require("passport-local").Strategy
const JwtStrategy = require("passport-jwt").Strategy
const { getGlobalDB } = require("./tenancy")
import { getGlobalDB } from "./tenancy"
const refresh = require("passport-oauth2-refresh")
const { Configs } = require("./constants")
const { getScopedConfig } = require("./db/utils")
const {
import { Configs } from "./constants"
import { getScopedConfig } from "./db/utils"
import {
jwt,
local,
authenticated,
@ -13,7 +13,6 @@ const {
oidc,
auditLog,
tenancy,
appTenancy,
authError,
ssoCallbackUrl,
csrf,
@ -22,32 +21,36 @@ const {
builderOnly,
builderOrAdmin,
joiValidator,
} = require("./middleware")
const { invalidateUser } = require("./cache/user")
} from "./middleware"
import { invalidateUser } from "./cache/user"
import { User } from "@budibase/types"
// Strategies
passport.use(new LocalStrategy(local.options, local.authenticate))
passport.use(new JwtStrategy(jwt.options, jwt.authenticate))
passport.serializeUser((user, done) => done(null, user))
passport.serializeUser((user: User, done: any) => done(null, user))
passport.deserializeUser(async (user, done) => {
passport.deserializeUser(async (user: User, done: any) => {
const db = getGlobalDB()
try {
const user = await db.get(user._id)
return done(null, user)
const dbUser = await db.get(user._id)
return done(null, dbUser)
} catch (err) {
console.error(`User not found`, err)
return done(null, false, { message: "User not found" })
}
})
async function refreshOIDCAccessToken(db, chosenConfig, refreshToken) {
async function refreshOIDCAccessToken(
db: any,
chosenConfig: any,
refreshToken: string
) {
const callbackUrl = await oidc.getCallbackUrl(db, chosenConfig)
let enrichedConfig
let strategy
let enrichedConfig: any
let strategy: any
try {
enrichedConfig = await oidc.fetchStrategyConfig(chosenConfig, callbackUrl)
@ -70,22 +73,28 @@ async function refreshOIDCAccessToken(db, chosenConfig, refreshToken) {
refresh.requestNewAccessToken(
Configs.OIDC,
refreshToken,
(err, accessToken, refreshToken, params) => {
(err: any, accessToken: string, refreshToken: any, params: any) => {
resolve({ err, accessToken, refreshToken, params })
}
)
})
}
async function refreshGoogleAccessToken(db, config, refreshToken) {
async function refreshGoogleAccessToken(
db: any,
config: any,
refreshToken: any
) {
let callbackUrl = await google.getCallbackUrl(db, config)
let strategy
try {
strategy = await google.strategyFactory(config, callbackUrl)
} catch (err) {
} catch (err: any) {
console.error(err)
throw new Error("Error constructing OIDC refresh strategy", err)
throw new Error(
`Error constructing OIDC refresh strategy: message=${err.message}`
)
}
refresh.use(strategy)
@ -94,14 +103,18 @@ async function refreshGoogleAccessToken(db, config, refreshToken) {
refresh.requestNewAccessToken(
Configs.GOOGLE,
refreshToken,
(err, accessToken, refreshToken, params) => {
(err: any, accessToken: string, refreshToken: string, params: any) => {
resolve({ err, accessToken, refreshToken, params })
}
)
})
}
async function refreshOAuthToken(refreshToken, configType, configId) {
async function refreshOAuthToken(
refreshToken: string,
configType: string,
configId: string
) {
const db = getGlobalDB()
const config = await getScopedConfig(db, {
@ -113,7 +126,7 @@ async function refreshOAuthToken(refreshToken, configType, configId) {
let refreshResponse
if (configType === Configs.OIDC) {
// configId - retrieved from cookie.
chosenConfig = config.configs.filter(c => c.uuid === configId)[0]
chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
if (!chosenConfig) {
throw new Error("Invalid OIDC configuration")
}
@ -134,7 +147,7 @@ async function refreshOAuthToken(refreshToken, configType, configId) {
return refreshResponse
}
async function updateUserOAuth(userId, oAuthConfig) {
async function updateUserOAuth(userId: string, oAuthConfig: any) {
const details = {
accessToken: oAuthConfig.accessToken,
refreshToken: oAuthConfig.refreshToken,
@ -162,14 +175,13 @@ async function updateUserOAuth(userId, oAuthConfig) {
}
}
module.exports = {
export = {
buildAuthMiddleware: authenticated,
passport,
google,
oidc,
jwt: require("jsonwebtoken"),
buildTenancyMiddleware: tenancy,
buildAppTenancyMiddleware: appTenancy,
auditLog,
authError,
buildCsrfMiddleware: csrf,

View file

@ -42,7 +42,7 @@ export enum DocumentType {
MIGRATIONS = "migrations",
DEV_INFO = "devinfo",
AUTOMATION_LOG = "log_au",
ACCOUNT = "acc",
ACCOUNT_METADATA = "acc_metadata",
}
export const StaticDatabases = {

View file

@ -6,7 +6,7 @@ const {
} = require("./utils")
const { getGlobalDB } = require("../tenancy")
const { StaticDatabases } = require("./constants")
const { doWithDB } = require("./");
const { doWithDB } = require("./")
const DESIGN_DB = "_design/database"
@ -59,7 +59,7 @@ exports.createNewUserEmailView = async () => {
}
exports.createAccountEmailView = async () => {
await doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db) => {
await doWithDB(StaticDatabases.PLATFORM_INFO.name, async db => {
let designDoc
try {
designDoc = await db.get(DESIGN_DB)
@ -70,8 +70,8 @@ exports.createAccountEmailView = async () => {
const view = {
// if using variables in a map function need to inject them before use
map: `function(doc) {
if (doc._id.startsWith("${DocumentType.ACCOUNT}${SEPARATOR}")) {
emit(doc.email.toLowerCase(), doc.tenantId)
if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) {
emit(doc.email.toLowerCase(), doc._id)
}
}`,
}
@ -171,7 +171,7 @@ exports.queryView = async (viewName, params, db, CreateFuncByName) => {
const createFunc = CreateFuncByName[viewName]
await removeDeprecated(db, viewName)
await createFunc()
return exports.queryGlobalView(viewName, params)
return exports.queryView(viewName, params, db, CreateFuncByName)
} else {
throw err
}
@ -183,7 +183,7 @@ exports.queryPlatformView = async (viewName, params) => {
[ViewName.ACCOUNT_BY_EMAIL]: exports.createAccountEmailView,
}
return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db) => {
return doWithDB(StaticDatabases.PLATFORM_INFO.name, async db => {
return exports.queryView(viewName, params, db, CreateFuncByName)
})
}

View file

@ -8,4 +8,5 @@ import { processors } from "./processors"
export const shutdown = () => {
processors.shutdown()
console.log("Events shutdown")
}

View file

@ -17,6 +17,7 @@ import constants from "./constants"
import * as dbConstants from "./db/constants"
import logging from "./logging"
import pino from "./pino"
import * as middleware from "./middleware"
// mimic the outer package exports
import * as db from "./pkg/db"
@ -57,6 +58,7 @@ const core = {
roles,
...pino,
...errorClasses,
middleware,
}
export = core

View file

@ -65,7 +65,7 @@ async function checkApiKey(apiKey: string, populateUser?: Function) {
* The tenancy modules should not be used here and it should be assumed that the tenancy context
* has not yet been populated.
*/
module.exports = (
export = (
noAuthPatterns = [],
opts: { publicAllowed: boolean; populateUser?: Function } = {
publicAllowed: false,

View file

@ -13,7 +13,8 @@ const adminOnly = require("./adminOnly")
const builderOrAdmin = require("./builderOrAdmin")
const builderOnly = require("./builderOnly")
const joiValidator = require("./joi-validator")
module.exports = {
const pkg = {
google,
oidc,
jwt,
@ -33,3 +34,5 @@ module.exports = {
builderOrAdmin,
joiValidator,
}
export = pkg

View file

@ -3,17 +3,27 @@ const { v4: uuidv4 } = require("uuid")
const { logWarn } = require("../logging")
const env = require("../environment")
interface Session {
key: string
userId: string
interface CreateSession {
sessionId: string
lastAccessedAt: string
createdAt: string
tenantId: string
csrfToken?: string
value: string
}
type SessionKey = { key: string }[]
interface Session extends CreateSession {
userId: string
lastAccessedAt: string
createdAt: string
// make optional attributes required
csrfToken: string
}
interface SessionKey {
key: string
}
interface ScannedSession {
value: Session
}
// a week in seconds
const EXPIRY_SECONDS = 86400 * 7
@ -22,14 +32,14 @@ function makeSessionID(userId: string, sessionId: string) {
return `${userId}/${sessionId}`
}
export async function getSessionsForUser(userId: string) {
export async function getSessionsForUser(userId: string): Promise<Session[]> {
if (!userId) {
console.trace("Cannot get sessions for undefined userId")
return []
}
const client = await redis.getSessionClient()
const sessions = await client.scan(userId)
return sessions.map((session: Session) => session.value)
const sessions: ScannedSession[] = await client.scan(userId)
return sessions.map(session => session.value)
}
export async function invalidateSessions(
@ -39,33 +49,32 @@ export async function invalidateSessions(
try {
const reason = opts?.reason || "unknown"
let sessionIds: string[] = opts.sessionIds || []
let sessions: SessionKey
let sessionKeys: SessionKey[]
// If no sessionIds, get all the sessions for the user
if (sessionIds.length === 0) {
sessions = await getSessionsForUser(userId)
sessions.forEach(
(session: any) =>
(session.key = makeSessionID(session.userId, session.sessionId))
)
const sessions = await getSessionsForUser(userId)
sessionKeys = sessions.map(session => ({
key: makeSessionID(session.userId, session.sessionId),
}))
} else {
// use the passed array of sessionIds
sessionIds = Array.isArray(sessionIds) ? sessionIds : [sessionIds]
sessions = sessionIds.map((sessionId: string) => ({
sessionKeys = sessionIds.map(sessionId => ({
key: makeSessionID(userId, sessionId),
}))
}
if (sessions && sessions.length > 0) {
if (sessionKeys && sessionKeys.length > 0) {
const client = await redis.getSessionClient()
const promises = []
for (let session of sessions) {
promises.push(client.delete(session.key))
for (let sessionKey of sessionKeys) {
promises.push(client.delete(sessionKey.key))
}
if (!env.isTest()) {
logWarn(
`Invalidating sessions for ${userId} (reason: ${reason}) - ${sessions
.map(session => session.key)
`Invalidating sessions for ${userId} (reason: ${reason}) - ${sessionKeys
.map(sessionKey => sessionKey.key)
.join(", ")}`
)
}
@ -76,22 +85,26 @@ export async function invalidateSessions(
}
}
export async function createASession(userId: string, session: Session) {
export async function createASession(
userId: string,
createSession: CreateSession
) {
// invalidate all other sessions
await invalidateSessions(userId, { reason: "creation" })
const client = await redis.getSessionClient()
const sessionId = session.sessionId
if (!session.csrfToken) {
session.csrfToken = uuidv4()
}
session = {
...session,
const sessionId = createSession.sessionId
const csrfToken = createSession.csrfToken ? createSession.csrfToken : uuidv4()
const key = makeSessionID(userId, sessionId)
const session: Session = {
...createSession,
csrfToken,
createdAt: new Date().toISOString(),
lastAccessedAt: new Date().toISOString(),
userId,
}
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
await client.store(key, session, EXPIRY_SECONDS)
}
export async function updateSessionTTL(session: Session) {
@ -106,7 +119,10 @@ export async function endSession(userId: string, sessionId: string) {
await client.delete(makeSessionID(userId, sessionId))
}
export async function getSession(userId: string, sessionId: string) {
export async function getSession(
userId: string,
sessionId: string
): Promise<Session> {
if (!userId || !sessionId) {
throw new Error(`Invalid session details - ${userId} - ${sessionId}`)
}

View file

@ -11,7 +11,6 @@ const { UNICODE_MAX } = require("./db/constants")
* Given an email address this will use a view to search through
* all the users to find one with this email address.
* @param {string} email the email to lookup the user by.
* @return {Promise<object|null>}
*/
exports.getGlobalUserByEmail = async email => {
if (email == null) {

View file

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

View file

@ -1,2 +0,0 @@
exports.MOCK_DATE = new Date("2020-01-01T00:00:00.000Z")
exports.MOCK_DATE_TIMESTAMP = 1577836800000

View file

@ -0,0 +1,2 @@
export const MOCK_DATE = new Date("2020-01-01T00:00:00.000Z")
export const MOCK_DATE_TIMESTAMP = 1577836800000

View file

@ -1,9 +0,0 @@
const posthog = require("./posthog")
const events = require("./events")
const date = require("./date")
module.exports = {
posthog,
date,
events,
}

View file

@ -0,0 +1,4 @@
import "./posthog"
import "./events"
export * as accounts from "./accounts"
export * as date from "./date"

View file

@ -0,0 +1,5 @@
export interface APIError {
message: string
status: number
error?: any
}

View file

@ -1,2 +1,3 @@
export * from "./analytics"
export * from "./user"
export * from "./user"
export * from "./errors"

View file

@ -1,10 +1,31 @@
import { User } from "../../documents"
export interface CreateUserResponse {
_id: string
_rev: string
email: string
}
export interface BulkCreateUsersRequest {
users: User[]
groups: any[]
}
export interface UserDetails {
_id: string
email: string
}
export interface BulkCreateUsersResponse {
successful: UserDetails[]
unsuccessful: { email: string; reason: string }[]
}
export interface BulkDeleteUsersRequest {
userIds: string[]
}
}
export interface BulkDeleteUsersResponse {
successful: UserDetails[]
unsuccessful: { _id: string; email: string; reason: string }[]
}

View file

@ -22,7 +22,6 @@ export interface UserRoles {
[key: string]: string
}
// utility types
export interface BuilderUser extends User {
@ -34,8 +33,8 @@ export interface BuilderUser extends User {
export interface AdminUser extends User {
admin: {
global: boolean
},
}
builder: {
global: boolean
}
}
}

View file

@ -1,19 +1,20 @@
import { Document } from "../document"
import { User } from "./user"
export interface UserGroup extends Document {
name: string
icon: string
color: string
users: groupUser[]
users: GroupUser[]
apps: string[]
roles: UserGroupRoles
createdAt?: number
}
export interface groupUser {
export interface GroupUser {
_id: string
email: string[]
email: string
}
export interface UserGroupRoles {
[key: string]: string
}

View file

@ -3,3 +3,4 @@ export * from "./app"
export * from "./global"
export * from "./platform"
export * from "./document"
export * from "./pouch"

View file

@ -0,0 +1,5 @@
import { Document } from "../document"
export interface AccountMetadata extends Document {
email: string
}

View file

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

View file

@ -1,4 +1,4 @@
import { Document } from "../document";
import { Document } from "../document"
/**
* doc id is user email

View file

@ -0,0 +1,20 @@
export interface RowResponse<T> {
id: string
key: string
value: any
doc: T
}
export interface AllDocsResponse<T> {
offset: number
total_rows: number
rows: RowResponse<T>[]
}
export type BulkDocsResponse = BulkDocResponse[]
interface BulkDocResponse {
ok: boolean
id: string
rev: string
}

View file

@ -0,0 +1,5 @@
export interface AuthToken {
userId: string
tenantId: string
sessionId: string
}

View file

@ -5,3 +5,4 @@ export * from "./licensing"
export * from "./migrations"
export * from "./datasources"
export * from "./search"
export * from "./auth"

View file

@ -73,6 +73,7 @@
"@types/koa-router": "7.4.4",
"@types/koa__router": "8.0.11",
"@types/node": "14.18.20",
"@types/uuid": "8.3.4",
"@typescript-eslint/parser": "5.12.0",
"copyfiles": "2.4.1",
"eslint": "6.8.0",

View file

@ -14,3 +14,9 @@ const tk = require("timekeeper")
tk.freeze(mocks.date.MOCK_DATE)
global.console.log = jest.fn() // console.log are ignored in tests
if (!process.env.CI) {
// set a longer timeout in dev for debugging
// 100 seconds
jest.setTimeout(100000)
}

View file

@ -1,97 +0,0 @@
// get the JWT secret etc
require("../../src/environment")
require("@budibase/backend-core").init()
const {
getProdAppID,
generateGlobalUserID,
} = require("@budibase/backend-core/db")
const { doInTenant, getGlobalDB } = require("@budibase/backend-core/tenancy")
const users = require("../../src/sdk/users")
const { publicApiUserFix } = require("../../src/utilities/users")
const { hash } = require("@budibase/backend-core/utils")
const USER_LOAD_NUMBER = 10000
const BATCH_SIZE = 200
const PASSWORD = "test"
const TENANT_ID = "default"
const APP_ID = process.argv[2]
const words = [
"test",
"testing",
"budi",
"mail",
"age",
"risk",
"load",
"uno",
"arm",
"leg",
"pen",
"glass",
"box",
"chicken",
"bottle",
]
if (!APP_ID) {
console.error("Must supply app ID as first CLI option!")
process.exit(-1)
}
const WORD_1 = words[Math.floor(Math.random() * words.length)]
const WORD_2 = words[Math.floor(Math.random() * words.length)]
let HASHED_PASSWORD
function generateUser(count) {
return {
_id: generateGlobalUserID(),
password: HASHED_PASSWORD,
email: `${WORD_1}${count}@${WORD_2}.com`,
roles: {
[getProdAppID(APP_ID)]: "BASIC",
},
status: "active",
forceResetPassword: false,
firstName: "John",
lastName: "Smith",
}
}
async function run() {
HASHED_PASSWORD = await hash(PASSWORD)
return doInTenant(TENANT_ID, async () => {
const db = getGlobalDB()
for (let i = 0; i < USER_LOAD_NUMBER; i += BATCH_SIZE) {
let userSavePromises = []
for (let j = 0; j < BATCH_SIZE; j++) {
// like the public API
const ctx = publicApiUserFix({
request: {
body: generateUser(i + j),
},
})
userSavePromises.push(
users.save(ctx.request.body, {
hashPassword: false,
requirePassword: true,
bulkCreate: true,
})
)
}
const allUsers = await Promise.all(userSavePromises)
await db.bulkDocs(allUsers)
console.log(`${i + BATCH_SIZE} users have been created.`)
}
})
}
run()
.then(() => {
console.log(`Generated ${USER_LOAD_NUMBER} users!`)
})
.catch(err => {
console.error("Failed for reason: ", err)
process.exit(-1)
})

View file

@ -3,7 +3,7 @@ import { checkInviteCode } from "../../../utilities/redis"
import { sendEmail } from "../../../utilities/email"
import { users } from "../../../sdk"
import env from "../../../environment"
import { CloudAccount, User } from "@budibase/types"
import { BulkDeleteUsersRequest, CloudAccount, User } from "@budibase/types"
import {
accounts,
cache,
@ -138,17 +138,15 @@ export const destroy = async (ctx: any) => {
}
export const bulkDelete = async (ctx: any) => {
const { userIds } = ctx.request.body
const { userIds } = ctx.request.body as BulkDeleteUsersRequest
if (userIds?.indexOf(ctx.user._id) !== -1) {
ctx.throw(400, "Unable to delete self.")
}
try {
let usersResponse = await users.bulkDelete(userIds)
let response = await users.bulkDelete(userIds)
ctx.body = {
message: `${usersResponse.length} user(s) deleted`,
}
ctx.body = response
} catch (err) {
ctx.throw(err)
}

View file

@ -0,0 +1,21 @@
import { Account, AccountMetadata } from "@budibase/types"
import { accounts } from "../../../sdk"
export const save = async (ctx: any) => {
const account = ctx.request.body as Account
let metadata: AccountMetadata = {
_id: accounts.formatAccountMetadataId(account.accountId),
email: account.email,
}
metadata = await accounts.saveMetadata(metadata)
ctx.body = metadata
ctx.status = 200
}
export const destroy = async (ctx: any) => {
const accountId = accounts.formatAccountMetadataId(ctx.params.accountId)
await accounts.destroyMetadata(accountId)
ctx.status = 204
}

View file

@ -1,15 +1,16 @@
const Router = require("@koa/router")
import Router from "@koa/router"
const compress = require("koa-compress")
const zlib = require("zlib")
const { routes } = require("./routes")
const {
import { routes } from "./routes"
import {
buildAuthMiddleware,
auditLog,
buildTenancyMiddleware,
buildCsrfMiddleware,
} = require("@budibase/backend-core/auth")
const { middleware: pro } = require("@budibase/pro")
const { errors } = require("@budibase/backend-core")
} from "@budibase/backend-core/auth"
import { middleware as pro } from "@budibase/pro"
import { errors } from "@budibase/backend-core"
import { APIError } from "@budibase/types"
const PUBLIC_ENDPOINTS = [
// old deprecated endpoints kept for backwards compat
@ -120,15 +121,16 @@ router
router.use(async (ctx, next) => {
try {
await next()
} catch (err) {
} catch (err: any) {
ctx.log.error(err)
ctx.status = err.status || err.statusCode || 500
const error = errors.getPublicError(err)
ctx.body = {
const body: APIError = {
message: err.message,
status: ctx.status,
error,
}
ctx.body = body
}
})

View file

@ -1,11 +1,11 @@
jest.mock("nodemailer")
const { config, request, mocks, structures } = require("../../../tests")
import { TestConfiguration, mocks, API } from "../../../../tests"
const sendMailMock = mocks.email.mock()
const { events } = require("@budibase/backend-core")
const TENANT_ID = structures.TENANT_ID
import { events } from "@budibase/backend-core"
describe("/api/global/auth", () => {
const config = new TestConfiguration()
const api = new API(config)
beforeAll(async () => {
await config.beforeAll()
@ -19,56 +19,32 @@ describe("/api/global/auth", () => {
jest.clearAllMocks()
})
const requestPasswordReset = async () => {
await config.saveSmtpConfig()
await config.saveSettingsConfig()
await config.createUser()
const res = await request
.post(`/api/global/auth/${TENANT_ID}/reset`)
.send({
email: "test@test.com",
})
.expect("Content-Type", /json/)
.expect(200)
const emailCall = sendMailMock.mock.calls[0][0]
const parts = emailCall.html.split(`http://localhost:10000/builder/auth/reset?code=`)
const code = parts[1].split("\"")[0].split("&")[0]
return { code, res }
}
it("should logout", async () => {
await request
.post("/api/global/auth/logout")
.set(config.defaultHeaders())
.expect(200)
await api.auth.logout()
expect(events.auth.logout).toBeCalledTimes(1)
})
it("should be able to generate password reset email", async () => {
const { res, code } = await requestPasswordReset()
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." })
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)
})
it("should allow resetting user password with code", async () => {
const { code } = await requestPasswordReset()
const { code } = await api.auth.requestPasswordReset(sendMailMock)
const user = await config.getUser("test@test.com")
delete user.password
delete user.password
const res = await api.auth.updatePassword(code)
const res = await request
.post(`/api/global/auth/${TENANT_ID}/reset/update`)
.send({
password: "newpassword",
resetCode: code,
})
.expect("Content-Type", /json/)
.expect(200)
expect(res.body).toEqual({ message: "password reset successfully." })
expect(events.user.passwordReset).toBeCalledTimes(1)
expect(events.user.passwordReset).toBeCalledWith(user)
@ -79,15 +55,15 @@ describe("/api/global/auth", () => {
const passportSpy = jest.spyOn(auth.passport, "authenticate")
let oidcConf
let chosenConfig
let configId
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
@ -99,34 +75,34 @@ describe("/api/global/auth", () => {
})
afterEach(() => {
expect(strategyFactory).toBeCalledWith(
chosenConfig,
expect.any(Function)
)
expect(strategyFactory).toBeCalledWith(chosenConfig, expect.any(Function))
})
describe("oidc configs", () => {
it("should load strategy and delegate to passport", async () => {
await request.get(`/api/global/auth/${TENANT_ID}/oidc/configs/${configId}`)
await api.configs.getOIDCConfig(configId)
expect(passportSpy).toBeCalledWith(mockStrategyReturn, {
scope: ["profile", "email", "offline_access"]
scope: ["profile", "email", "offline_access"],
})
expect(passportSpy.mock.calls.length).toBe(1);
expect(passportSpy.mock.calls.length).toBe(1)
})
})
describe("oidc callback", () => {
it("should load strategy and delegate to passport", async () => {
await request.get(`/api/global/auth/${TENANT_ID}/oidc/callback`)
.set(config.getOIDConfigCookie(configId))
expect(passportSpy).toBeCalledWith(mockStrategyReturn, {
successRedirect: "/", failureRedirect: "/error"
}, expect.anything())
expect(passportSpy.mock.calls.length).toBe(1);
await api.configs.OIDCCallback(configId)
expect(passportSpy).toBeCalledWith(
mockStrategyReturn,
{
successRedirect: "/",
failureRedirect: "/error",
},
expect.anything()
)
expect(passportSpy.mock.calls.length).toBe(1)
})
})
})
})

View file

@ -1,11 +1,12 @@
// mock the email system
jest.mock("nodemailer")
const { config, structures, mocks, request } = require("../../../tests")
import { TestConfiguration, structures, mocks, API } from "../../../../tests"
mocks.email.mock()
const { Configs } = require("@budibase/backend-core/constants")
const { events } = require("@budibase/backend-core")
import { Configs, events } from "@budibase/backend-core"
describe("configs", () => {
const config = new TestConfiguration()
const api = new API(config)
beforeAll(async () => {
await config.beforeAll()
@ -20,35 +21,33 @@ describe("configs", () => {
})
describe("post /api/global/configs", () => {
const saveConfig = async (conf, _id, _rev) => {
const saveConfig = async (conf: any, _id?: string, _rev?: string) => {
const data = {
...conf,
_id,
_rev
_rev,
}
const res = await request
.post(`/api/global/configs`)
.send(data)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
const res = await api.configs.saveConfig(data)
return {
...data,
...res.body
...res.body,
}
}
describe("google", () => {
const saveGoogleConfig = async (conf, _id, _rev) => {
const saveGoogleConfig = async (
conf?: any,
_id?: string,
_rev?: string
) => {
const googleConfig = structures.configs.google(conf)
return saveConfig(googleConfig, _id, _rev)
}
describe("create", () => {
it ("should create activated google config", async () => {
it("should create activated google config", async () => {
await saveGoogleConfig()
expect(events.auth.SSOCreated).toBeCalledTimes(1)
expect(events.auth.SSOCreated).toBeCalledWith(Configs.GOOGLE)
@ -58,7 +57,7 @@ describe("configs", () => {
await config.deleteConfig(Configs.GOOGLE)
})
it ("should create deactivated google config", async () => {
it("should create deactivated google config", async () => {
await saveGoogleConfig({ activated: false })
expect(events.auth.SSOCreated).toBeCalledTimes(1)
expect(events.auth.SSOCreated).toBeCalledWith(Configs.GOOGLE)
@ -69,10 +68,14 @@ describe("configs", () => {
})
describe("update", () => {
it ("should update google config to deactivated", async () => {
it("should update google config to deactivated", async () => {
const googleConf = await saveGoogleConfig()
jest.clearAllMocks()
await saveGoogleConfig({ ...googleConf.config, activated: false }, googleConf._id, googleConf._rev)
await saveGoogleConfig(
{ ...googleConf.config, activated: false },
googleConf._id,
googleConf._rev
)
expect(events.auth.SSOUpdated).toBeCalledTimes(1)
expect(events.auth.SSOUpdated).toBeCalledWith(Configs.GOOGLE)
expect(events.auth.SSOActivated).not.toBeCalled()
@ -81,10 +84,14 @@ describe("configs", () => {
await config.deleteConfig(Configs.GOOGLE)
})
it ("should update google config to activated", async () => {
it("should update google config to activated", async () => {
const googleConf = await saveGoogleConfig({ activated: false })
jest.clearAllMocks()
await saveGoogleConfig({ ...googleConf.config, activated: true}, googleConf._id, googleConf._rev)
await saveGoogleConfig(
{ ...googleConf.config, activated: true },
googleConf._id,
googleConf._rev
)
expect(events.auth.SSOUpdated).toBeCalledTimes(1)
expect(events.auth.SSOUpdated).toBeCalledWith(Configs.GOOGLE)
expect(events.auth.SSODeactivated).not.toBeCalled()
@ -92,17 +99,21 @@ describe("configs", () => {
expect(events.auth.SSOActivated).toBeCalledWith(Configs.GOOGLE)
await config.deleteConfig(Configs.GOOGLE)
})
})
})
})
describe("oidc", () => {
const saveOIDCConfig = async (conf, _id, _rev) => {
const saveOIDCConfig = async (
conf?: any,
_id?: string,
_rev?: string
) => {
const oidcConfig = structures.configs.oidc(conf)
return saveConfig(oidcConfig, _id, _rev)
}
describe("create", () => {
it ("should create activated OIDC config", async () => {
it("should create activated OIDC config", async () => {
await saveOIDCConfig()
expect(events.auth.SSOCreated).toBeCalledTimes(1)
expect(events.auth.SSOCreated).toBeCalledWith(Configs.OIDC)
@ -112,7 +123,7 @@ describe("configs", () => {
await config.deleteConfig(Configs.OIDC)
})
it ("should create deactivated OIDC config", async () => {
it("should create deactivated OIDC config", async () => {
await saveOIDCConfig({ activated: false })
expect(events.auth.SSOCreated).toBeCalledTimes(1)
expect(events.auth.SSOCreated).toBeCalledWith(Configs.OIDC)
@ -123,10 +134,14 @@ describe("configs", () => {
})
describe("update", () => {
it ("should update OIDC config to deactivated", async () => {
it("should update OIDC config to deactivated", async () => {
const oidcConf = await saveOIDCConfig()
jest.clearAllMocks()
await saveOIDCConfig({ ...oidcConf.config.configs[0], activated: false }, oidcConf._id, oidcConf._rev)
await saveOIDCConfig(
{ ...oidcConf.config.configs[0], activated: false },
oidcConf._id,
oidcConf._rev
)
expect(events.auth.SSOUpdated).toBeCalledTimes(1)
expect(events.auth.SSOUpdated).toBeCalledWith(Configs.OIDC)
expect(events.auth.SSOActivated).not.toBeCalled()
@ -135,10 +150,14 @@ describe("configs", () => {
await config.deleteConfig(Configs.OIDC)
})
it ("should update OIDC config to activated", async () => {
it("should update OIDC config to activated", async () => {
const oidcConf = await saveOIDCConfig({ activated: false })
jest.clearAllMocks()
await saveOIDCConfig({ ...oidcConf.config.configs[0], activated: true}, oidcConf._id, oidcConf._rev)
await saveOIDCConfig(
{ ...oidcConf.config.configs[0], activated: true },
oidcConf._id,
oidcConf._rev
)
expect(events.auth.SSOUpdated).toBeCalledTimes(1)
expect(events.auth.SSOUpdated).toBeCalledWith(Configs.OIDC)
expect(events.auth.SSODeactivated).not.toBeCalled()
@ -147,17 +166,20 @@ describe("configs", () => {
await config.deleteConfig(Configs.OIDC)
})
})
})
describe("smtp", () => {
const saveSMTPConfig = async (conf, _id, _rev) => {
const saveSMTPConfig = async (
conf?: any,
_id?: string,
_rev?: string
) => {
const smtpConfig = structures.configs.smtp(conf)
return saveConfig(smtpConfig, _id, _rev)
}
describe("create", () => {
it ("should create SMTP config", async () => {
it("should create SMTP config", async () => {
await config.deleteConfig(Configs.SMTP)
await saveSMTPConfig()
expect(events.email.SMTPUpdated).not.toBeCalled()
@ -167,7 +189,7 @@ describe("configs", () => {
})
describe("update", () => {
it ("should update SMTP config", async () => {
it("should update SMTP config", async () => {
const smtpConf = await saveSMTPConfig()
jest.clearAllMocks()
await saveSMTPConfig(smtpConf.config, smtpConf._id, smtpConf._rev)
@ -179,15 +201,19 @@ describe("configs", () => {
})
describe("settings", () => {
const saveSettingsConfig = async (conf, _id, _rev) => {
const saveSettingsConfig = async (
conf?: any,
_id?: string,
_rev?: string
) => {
const settingsConfig = structures.configs.settings(conf)
return saveConfig(settingsConfig, _id, _rev)
}
describe("create", () => {
it ("should create settings config with default settings", async () => {
it("should create settings config with default settings", async () => {
await config.deleteConfig(Configs.SETTINGS)
await saveSettingsConfig()
expect(events.org.nameUpdated).not.toBeCalled()
@ -195,35 +221,43 @@ describe("configs", () => {
expect(events.org.platformURLUpdated).not.toBeCalled()
})
it ("should create settings config with non-default settings", async () => {
it("should create settings config with non-default settings", async () => {
config.modeSelf()
await config.deleteConfig(Configs.SETTINGS)
const conf = {
company: "acme",
logoUrl: "http://example.com",
platformUrl: "http://example.com"
platformUrl: "http://example.com",
}
await saveSettingsConfig(conf)
expect(events.org.nameUpdated).toBeCalledTimes(1)
expect(events.org.logoUpdated).toBeCalledTimes(1)
expect(events.org.platformURLUpdated).toBeCalledTimes(1)
config.modeAccount()
})
})
describe("update", () => {
it ("should update settings config", async () => {
it("should update settings config", async () => {
config.modeSelf()
await config.deleteConfig(Configs.SETTINGS)
const settingsConfig = await saveSettingsConfig()
settingsConfig.config.company = "acme"
settingsConfig.config.logoUrl = "http://example.com"
settingsConfig.config.platformUrl = "http://example.com"
await saveSettingsConfig(settingsConfig.config, settingsConfig._id, settingsConfig._rev)
await saveSettingsConfig(
settingsConfig.config,
settingsConfig._id,
settingsConfig._rev
)
expect(events.org.nameUpdated).toBeCalledTimes(1)
expect(events.org.logoUpdated).toBeCalledTimes(1)
expect(events.org.platformURLUpdated).toBeCalledTimes(1)
config.modeAccount()
})
})
})
@ -232,12 +266,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 request
.get(`/api/global/configs/checklist`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
const res = await api.configs.getConfigChecklist()
const checklist = res.body
expect(checklist.apps.checked).toBeFalsy()

View file

@ -1,12 +1,11 @@
jest.mock("nodemailer")
const { config, mocks, structures, request } = require("../../../tests")
import { TestConfiguration, mocks, API } from "../../../../tests"
const sendMailMock = mocks.email.mock()
const { EmailTemplatePurpose } = require("../../../constants")
const TENANT_ID = structures.TENANT_ID
import { EmailTemplatePurpose } from "../../../../constants"
describe("/api/global/email", () => {
const config = new TestConfiguration()
const api = new API(config)
beforeAll(async () => {
await config.beforeAll()
@ -20,16 +19,9 @@ describe("/api/global/email", () => {
// initially configure settings
await config.saveSmtpConfig()
await config.saveSettingsConfig()
const res = await request
.post(`/api/global/email/send`)
.send({
email: "test@test.com",
purpose: EmailTemplatePurpose.INVITATION,
tenantId: TENANT_ID,
})
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
const res = await api.emails.sendEmail(EmailTemplatePurpose.INVITATION)
expect(res.body.message).toBeDefined()
expect(sendMailMock).toHaveBeenCalled()
const emailCall = sendMailMock.mock.calls[0][0]

View file

@ -1,5 +1,5 @@
const { config, request } = require("../../../tests")
const { EmailTemplatePurpose } = require("../../../constants")
import { TestConfiguration, API } from "../../../../tests"
import { EmailTemplatePurpose } from "../../../../constants"
const nodemailer = require("nodemailer")
const fetch = require("node-fetch")
@ -7,6 +7,8 @@ const fetch = require("node-fetch")
jest.setTimeout(30000)
describe("/api/global/email", () => {
const config = new TestConfiguration()
const api = new API(config)
beforeAll(async () => {
await config.beforeAll()
@ -16,27 +18,24 @@ describe("/api/global/email", () => {
await config.afterAll()
})
async function sendRealEmail(purpose) {
async function sendRealEmail(purpose: string) {
let response, text
try {
const timeout = () => new Promise((resolve, reject) =>
setTimeout(() => reject({
status: 301,
errno: "ETIME"
}), 20000)
)
const timeout = () =>
new Promise((resolve, reject) =>
setTimeout(
() =>
reject({
status: 301,
errno: "ETIME",
}),
20000
)
)
await Promise.race([config.saveEtherealSmtpConfig(), timeout()])
await Promise.race([config.saveSettingsConfig(), timeout()])
const user = await config.getUser("test@test.com")
const res = await request
.post(`/api/global/email/send`)
.send({
email: "test@test.com",
purpose,
userId: user._id,
})
.set(config.defaultHeaders())
.timeout(20000)
const res = await api.emails.sendEmail(purpose).timeout(20000)
// ethereal hiccup, can't test right now
if (res.status >= 300) {
return
@ -47,7 +46,7 @@ describe("/api/global/email", () => {
expect(testUrl).toBeDefined()
response = await fetch(testUrl)
text = await response.text()
} catch (err) {
} catch (err: any) {
// ethereal hiccup, can't test right now
if (parseInt(err.status) >= 300 || (err && err.errno === "ETIME")) {
return
@ -81,4 +80,4 @@ describe("/api/global/email", () => {
it("should be able to send a password recovery email", async () => {
await sendRealEmail(EmailTemplatePurpose.PASSWORD_RECOVERY)
})
})
})

View file

@ -1,8 +1,10 @@
jest.mock("nodemailer")
const { config, request } = require("../../../tests")
const { events } = require("@budibase/backend-core")
import { TestConfiguration, API } 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()
@ -16,23 +18,13 @@ describe("/api/global/self", () => {
jest.clearAllMocks()
})
const updateSelf = async (user) => {
const res = await request
.post(`/api/global/self`)
.send(user)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
return res
}
describe("update", () => {
it("should update self", async () => {
const user = await config.createUser()
await config.createSession(user)
delete user.password
const res = await updateSelf(user)
const res = await api.self.updateSelf(user)
expect(res.body._id).toBe(user._id)
expect(events.user.updated).toBeCalledTimes(1)
@ -42,10 +34,10 @@ describe("/api/global/self", () => {
it("should update password", async () => {
const user = await config.createUser()
const password = "newPassword"
user.password = password
await config.createSession(user)
const res = await updateSelf(user)
user.password = "newPassword"
const res = await api.self.updateSelf(user)
delete user.password
expect(res.body._id).toBe(user._id)
@ -55,4 +47,4 @@ describe("/api/global/self", () => {
expect(events.user.passwordUpdated).toBeCalledWith(user)
})
})
})
})

View file

@ -0,0 +1,470 @@
jest.mock("nodemailer")
import {
TestConfiguration,
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()
})
afterAll(async () => {
await config.afterAll()
})
beforeEach(() => {
jest.clearAllMocks()
})
describe("invite", () => {
it("should be able to generate an invitation", async () => {
const { code, res } = await api.users.sendUserInvite(sendMailMock)
expect(res.body).toEqual({ message: "Invitation has been sent." })
expect(sendMailMock).toHaveBeenCalled()
expect(code).toBeDefined()
expect(events.user.invited).toBeCalledTimes(1)
})
it("should be able to create new user from invite", async () => {
const { code } = await api.users.sendUserInvite(sendMailMock)
const res = await api.users.acceptInvite(code)
expect(res.body._id).toBeDefined()
const user = await config.getUser("invite@test.com")
expect(user).toBeDefined()
expect(user._id).toEqual(res.body._id)
expect(events.user.inviteAccepted).toBeCalledTimes(1)
expect(events.user.inviteAccepted).toBeCalledWith(user)
})
})
describe("bulkCreate", () => {
it("should ignore users existing in the same tenant", async () => {
const user = await config.createUser()
jest.clearAllMocks()
const response = await api.users.bulkCreateUsers([user])
expect(response.successful.length).toBe(0)
expect(response.unsuccessful.length).toBe(1)
expect(response.unsuccessful[0].email).toBe(user.email)
expect(events.user.created).toBeCalledTimes(0)
})
it("should ignore users existing in other tenants", async () => {
const user = await config.createUser()
jest.resetAllMocks()
await tenancy.doInTenant(TENANT_1, async () => {
const response = await api.users.bulkCreateUsers([user])
expect(response.successful.length).toBe(0)
expect(response.unsuccessful.length).toBe(1)
expect(response.unsuccessful[0].email).toBe(user.email)
expect(events.user.created).toBeCalledTimes(0)
})
})
it("should ignore accounts using the same email", async () => {
const account = structures.accounts.account()
const resp = await api.accounts.saveMetadata(account)
const user = structures.users.user({ email: resp.email })
jest.clearAllMocks()
const response = await api.users.bulkCreateUsers([user])
expect(response.successful.length).toBe(0)
expect(response.unsuccessful.length).toBe(1)
expect(response.unsuccessful[0].email).toBe(user.email)
expect(events.user.created).toBeCalledTimes(0)
})
it("should be able to bulkCreate users", async () => {
const builder = structures.users.builderUser()
const admin = structures.users.adminUser()
const user = structures.users.user()
const response = await api.users.bulkCreateUsers([builder, admin, user])
expect(response.successful.length).toBe(3)
expect(response.successful[0].email).toBe(builder.email)
expect(response.successful[1].email).toBe(admin.email)
expect(response.successful[2].email).toBe(user.email)
expect(response.unsuccessful.length).toBe(0)
expect(events.user.created).toBeCalledTimes(3)
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1)
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(2)
})
})
describe("create", () => {
it("should be able to create a basic user", async () => {
const user = structures.users.user()
await api.users.saveUser(user)
expect(events.user.created).toBeCalledTimes(1)
expect(events.user.updated).not.toBeCalled()
expect(events.user.permissionBuilderAssigned).not.toBeCalled()
expect(events.user.permissionAdminAssigned).not.toBeCalled()
})
it("should be able to create an admin user", async () => {
const user = structures.users.adminUser()
await api.users.saveUser(user)
expect(events.user.created).toBeCalledTimes(1)
expect(events.user.updated).not.toBeCalled()
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1)
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1)
})
it("should be able to create a builder user", async () => {
const user = structures.users.builderUser()
await api.users.saveUser(user)
expect(events.user.created).toBeCalledTimes(1)
expect(events.user.updated).not.toBeCalled()
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1)
expect(events.user.permissionAdminAssigned).not.toBeCalled()
})
it("should be able to assign app roles", async () => {
const user = structures.users.user()
user.roles = {
app_123: "role1",
app_456: "role2",
}
await api.users.saveUser(user)
const savedUser = await config.getUser(user.email)
expect(events.user.created).toBeCalledTimes(1)
expect(events.user.updated).not.toBeCalled()
expect(events.role.assigned).toBeCalledTimes(2)
expect(events.role.assigned).toBeCalledWith(savedUser, "role1")
expect(events.role.assigned).toBeCalledWith(savedUser, "role2")
})
it("should not be able to create user that exists in same tenant", async () => {
const user = await config.createUser()
jest.clearAllMocks()
delete user._id
delete user._rev
const response = await api.users.saveUser(user, 400)
expect(response.body.message).toBe(
`Email address ${user.email} already in use.`
)
expect(events.user.created).toBeCalledTimes(0)
})
it("should not be able to create user that exists in other tenant", async () => {
const user = await config.createUser()
jest.resetAllMocks()
await tenancy.doInTenant(TENANT_1, async () => {
delete user._id
const response = await api.users.saveUser(user, 400)
expect(response.body.message).toBe(
`Email address ${user.email} already in use.`
)
expect(events.user.created).toBeCalledTimes(0)
})
})
it("should not be able to create user with the same email as an account", async () => {
const user = structures.users.user()
const account = structures.accounts.cloudAccount()
mocks.accounts.getAccount.mockReturnValueOnce(account)
const response = await api.users.saveUser(user, 400)
expect(response.body.message).toBe(
`Email address ${user.email} already in use.`
)
expect(events.user.created).toBeCalledTimes(0)
})
})
describe("update", () => {
it("should be able to update a basic user", async () => {
const user = await config.createUser()
jest.clearAllMocks()
await api.users.saveUser(user)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.user.permissionBuilderAssigned).not.toBeCalled()
expect(events.user.permissionAdminAssigned).not.toBeCalled()
expect(events.user.passwordForceReset).not.toBeCalled()
})
it("should be able to force reset password", async () => {
const user = await config.createUser()
jest.clearAllMocks()
user.forceResetPassword = true
user.password = "tempPassword"
await api.users.saveUser(user)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.user.permissionBuilderAssigned).not.toBeCalled()
expect(events.user.permissionAdminAssigned).not.toBeCalled()
expect(events.user.passwordForceReset).toBeCalledTimes(1)
})
it("should be able to update a basic user to an admin user", async () => {
const user = await config.createUser()
jest.clearAllMocks()
await api.users.saveUser(structures.users.adminUser(user))
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1)
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1)
})
it("should be able to update a basic user to a builder user", async () => {
const user = await config.createUser()
jest.clearAllMocks()
await api.users.saveUser(structures.users.builderUser(user))
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1)
expect(events.user.permissionAdminAssigned).not.toBeCalled()
})
it("should be able to update an admin user to a basic user", async () => {
const user = await config.createUser(structures.users.adminUser())
jest.clearAllMocks()
user.admin!.global = false
user.builder!.global = false
await api.users.saveUser(user)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1)
})
it("should be able to update an builder user to a basic user", async () => {
const user = await config.createUser(structures.users.builderUser())
jest.clearAllMocks()
user.builder!.global = false
await api.users.saveUser(user)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1)
expect(events.user.permissionAdminRemoved).not.toBeCalled()
})
it("should be able to assign app roles", async () => {
const user = await config.createUser()
jest.clearAllMocks()
user.roles = {
app_123: "role1",
app_456: "role2",
}
await api.users.saveUser(user)
const savedUser = await config.getUser(user.email)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.role.assigned).toBeCalledTimes(2)
expect(events.role.assigned).toBeCalledWith(savedUser, "role1")
expect(events.role.assigned).toBeCalledWith(savedUser, "role2")
})
it("should be able to unassign app roles", async () => {
let user = structures.users.user()
user.roles = {
app_123: "role1",
app_456: "role2",
}
user = await config.createUser(user)
jest.clearAllMocks()
user.roles = {}
await api.users.saveUser(user)
const savedUser = await config.getUser(user.email)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.role.unassigned).toBeCalledTimes(2)
expect(events.role.unassigned).toBeCalledWith(savedUser, "role1")
expect(events.role.unassigned).toBeCalledWith(savedUser, "role2")
})
it("should be able to update existing app roles", async () => {
let user = structures.users.user()
user.roles = {
app_123: "role1",
app_456: "role2",
}
user = await config.createUser(user)
jest.clearAllMocks()
user.roles = {
app_123: "role1",
app_456: "role2-edit",
}
await api.users.saveUser(user)
const savedUser = await config.getUser(user.email)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.role.unassigned).toBeCalledTimes(1)
expect(events.role.unassigned).toBeCalledWith(savedUser, "role2")
expect(events.role.assigned).toBeCalledTimes(1)
expect(events.role.assigned).toBeCalledWith(savedUser, "role2-edit")
})
it("should not be able to update email address", async () => {
const email = "email@test.com"
const user = await config.createUser(structures.users.user({ email }))
user.email = "new@test.com"
const response = await api.users.saveUser(user, 400)
const dbUser = await config.getUser(email)
user.email = email
expect(user).toStrictEqual(dbUser)
expect(response.body.message).toBe("Email address cannot be changed")
})
})
describe("bulkDelete", () => {
it("should not be able to bulkDelete current user", async () => {
const user = await config.defaultUser!
const request = { userIds: [user._id!] }
const response = await api.users.bulkDeleteUsers(request, 400)
expect(response.body.message).toBe("Unable to delete self.")
expect(events.user.deleted).not.toBeCalled()
})
it("should not be able to bulkDelete account owner", async () => {
const user = await config.createUser()
const account = structures.accounts.cloudAccount()
account.budibaseUserId = user._id!
mocks.accounts.getAccountByTenantId.mockReturnValue(account)
const request = { userIds: [user._id!] }
const response = await api.users.bulkDeleteUsers(request)
expect(response.body.successful.length).toBe(0)
expect(response.body.unsuccessful.length).toBe(1)
expect(response.body.unsuccessful[0].reason).toBe(
"Account holder cannot be deleted"
)
expect(response.body.unsuccessful[0]._id).toBe(user._id)
expect(events.user.deleted).not.toBeCalled()
})
it("should be able to bulk delete users", async () => {
const account = structures.accounts.cloudAccount()
mocks.accounts.getAccountByTenantId.mockReturnValue(account)
const builder = structures.users.builderUser()
const admin = structures.users.adminUser()
const user = structures.users.user()
const createdUsers = await api.users.bulkCreateUsers([
builder,
admin,
user,
])
const request = { userIds: createdUsers.successful.map(u => u._id!) }
const response = await api.users.bulkDeleteUsers(request)
expect(response.body.successful.length).toBe(3)
expect(response.body.unsuccessful.length).toBe(0)
expect(events.user.deleted).toBeCalledTimes(3)
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(2)
})
})
describe("destroy", () => {
it("should be able to destroy a basic user", async () => {
const user = await config.createUser()
jest.clearAllMocks()
await api.users.deleteUser(user._id!)
expect(events.user.deleted).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).not.toBeCalled()
expect(events.user.permissionAdminRemoved).not.toBeCalled()
})
it("should be able to destroy an admin user", async () => {
const user = await config.createUser(structures.users.adminUser())
jest.clearAllMocks()
await api.users.deleteUser(user._id!)
expect(events.user.deleted).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1)
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1)
})
it("should be able to destroy a builder user", async () => {
const user = await config.createUser(structures.users.builderUser())
jest.clearAllMocks()
await api.users.deleteUser(user._id!)
expect(events.user.deleted).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1)
expect(events.user.permissionAdminRemoved).not.toBeCalled()
})
it("should not be able to destroy account owner", async () => {
const user = await config.createUser()
const account = structures.accounts.cloudAccount()
mocks.accounts.getAccount.mockReturnValueOnce(account)
const response = await api.users.deleteUser(user._id!, 400)
expect(response.body.message).toBe("Account holder cannot be deleted")
})
it("should not be able to destroy account owner as account owner", async () => {
const user = await config.defaultUser!
const account = structures.accounts.cloudAccount()
account.email = user.email
mocks.accounts.getAccount.mockReturnValueOnce(account)
const response = await api.users.deleteUser(user._id!, 400)
expect(response.body.message).toBe("Unable to delete self.")
})
})
})

View file

@ -12,6 +12,7 @@ const statusRoutes = require("./system/status")
const selfRoutes = require("./global/self")
const licenseRoutes = require("./global/license")
const migrationRoutes = require("./system/migrations")
const accountRoutes = require("./system/accounts")
let userGroupRoutes = api.groups
exports.routes = [
@ -29,4 +30,5 @@ exports.routes = [
licenseRoutes,
userGroupRoutes,
migrationRoutes,
accountRoutes,
]

View file

@ -0,0 +1,19 @@
import Router from "@koa/router"
import * as controller from "../../controllers/system/accounts"
import { middleware } from "@budibase/backend-core"
const router = new Router()
router
.put(
"/api/system/accounts/:accountId/metadata",
middleware.internalApi,
controller.save
)
.delete(
"/api/system/accounts/:accountId/metadata",
middleware.internalApi,
controller.destroy
)
export = router

View file

@ -0,0 +1,57 @@
import { accounts } from "../../../../sdk"
import { TestConfiguration, structures, API } from "../../../../tests"
import { v4 as uuid } from "uuid"
describe("accounts", () => {
const config = new TestConfiguration()
const api = new API(config)
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
beforeEach(() => {
jest.clearAllMocks()
})
describe("metadata", () => {
describe("saveMetadata", () => {
it("saves account metadata", async () => {
let account = structures.accounts.account()
const response = await api.accounts.saveMetadata(account)
const id = accounts.formatAccountMetadataId(account.accountId)
const metadata = await accounts.getMetadata(id)
expect(response).toStrictEqual(metadata)
})
})
describe("destroyMetadata", () => {
it("destroys account metadata", async () => {
const account = structures.accounts.account()
await api.accounts.saveMetadata(account)
await api.accounts.destroyMetadata(account.accountId)
const deleted = await accounts.getMetadata(account.accountId)
expect(deleted).toBe(undefined)
})
it("destroys account metadata that does not exist", async () => {
const id = uuid()
const response = await api.accounts.destroyMetadata(id)
expect(response.status).toBe(404)
expect(response.body.message).toBe(
`id=${accounts.formatAccountMetadataId(id)} does not exist`
)
})
})
})
})

View file

@ -1,424 +0,0 @@
jest.mock("nodemailer")
import { config, request, mocks, structures } from "../../../tests"
const sendMailMock = mocks.email.mock()
import { events } from "@budibase/backend-core"
import { User, BulkCreateUsersRequest, BulkDeleteUsersRequest } from "@budibase/types"
describe("/api/global/users", () => {
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
beforeEach(() => {
jest.clearAllMocks()
})
const sendUserInvite = async () => {
await config.saveSmtpConfig()
await config.saveSettingsConfig()
const res = await request
.post(`/api/global/users/invite`)
.send({
email: "invite@test.com",
})
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
const emailCall = sendMailMock.mock.calls[0][0]
// after this URL there should be a code
const parts = emailCall.html.split("http://localhost:10000/builder/invite?code=")
const code = parts[1].split("\"")[0].split("&")[0]
return { code, res }
}
describe("invite", () => {
it("should be able to generate an invitation", async () => {
const { code, res } = await sendUserInvite()
expect(res.body).toEqual({ message: "Invitation has been sent." })
expect(sendMailMock).toHaveBeenCalled()
expect(code).toBeDefined()
expect(events.user.invited).toBeCalledTimes(1)
})
it("should be able to create new user from invite", async () => {
const { code } = await sendUserInvite()
const res = await request
.post(`/api/global/users/invite/accept`)
.send({
password: "newpassword",
inviteCode: code,
})
.expect("Content-Type", /json/)
.expect(200)
expect(res.body._id).toBeDefined()
const user = await config.getUser("invite@test.com")
expect(user).toBeDefined()
expect(user._id).toEqual(res.body._id)
expect(events.user.inviteAccepted).toBeCalledTimes(1)
expect(events.user.inviteAccepted).toBeCalledWith(user)
})
})
const bulkCreateUsers = async (users: User[], groups: any[] = []) => {
const body: BulkCreateUsersRequest = { users, groups }
const res = await request
.post(`/api/global/users/bulkCreate`)
.send(body)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
return res.body
}
describe("bulkCreate", () => {
it("should ignore users existing in the same tenant", async () => {
await bulkCreateUsers(toCreate)
})
it("should ignore users existing in other tenants", async () => {
await bulkCreateUsers(toCreate)
})
it("should ignore accounts using the same email", async () => {
await bulkCreateUsers(toCreate)
})
it("should be able to bulkCreate users with different permissions", async () => {
const builder = structures.users.builderUser({ email: "bulkbasic@test.com" })
const admin = structures.users.adminUser({ email: "bulkadmin@test.com" })
const user = structures.users.user({ email: "bulkuser@test.com" })
await bulkCreateUsers([builder, admin, user])
expect(events.user.created).toBeCalledTimes(3)
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1)
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1)
})
})
const createUser = async (user: User) => {
const existing = await config.getUser(user.email)
if (existing) {
await deleteUser(existing._id)
}
return saveUser(user)
}
const updateUser = async (user: User) => {
const existing = await config.getUser(user.email)
user._id = existing._id
return saveUser(user)
}
const saveUser = async (user: User) => {
const res = await request
.post(`/api/global/users`)
.send(user)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
return res.body
}
const bulkDeleteUsers = async (users: User[]) => {
const body: BulkDeleteUsersRequest = {
userIds: users.map(u => u._id!)
}
const res = await request
.post(`/api/global/users/bulkDelete`)
.send(body)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
return res.body
}
const deleteUser = async (email: string) => {
const user = await config.getUser(email)
if (user) {
await request
.delete(`/api/global/users/${user._id}`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
}
}
describe("create", () => {
it("should be able to create a basic user", async () => {
const user = structures.users.user({ email: "basic@test.com" })
await createUser(user)
expect(events.user.created).toBeCalledTimes(1)
expect(events.user.updated).not.toBeCalled()
expect(events.user.permissionBuilderAssigned).not.toBeCalled()
expect(events.user.permissionAdminAssigned).not.toBeCalled()
})
it("should be able to create an admin user", async () => {
const user = structures.users.adminUser({ email: "admin@test.com" })
await createUser(user)
expect(events.user.created).toBeCalledTimes(1)
expect(events.user.updated).not.toBeCalled()
expect(events.user.permissionBuilderAssigned).not.toBeCalled()
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1)
})
it("should be able to create a builder user", async () => {
const user = structures.users.builderUser({ email: "builder@test.com" })
await createUser(user)
expect(events.user.created).toBeCalledTimes(1)
expect(events.user.updated).not.toBeCalled()
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1)
expect(events.user.permissionAdminAssigned).not.toBeCalled()
})
it("should be able to assign app roles", async () => {
const user = structures.users.user({ email: "assign-roles@test.com" })
user.roles = {
"app_123": "role1",
"app_456": "role2",
}
await createUser(user)
const savedUser = await config.getUser(user.email)
expect(events.user.created).toBeCalledTimes(1)
expect(events.user.updated).not.toBeCalled()
expect(events.role.assigned).toBeCalledTimes(2)
expect(events.role.assigned).toBeCalledWith(savedUser, "role1")
expect(events.role.assigned).toBeCalledWith(savedUser, "role2")
})
})
describe("update", () => {
it("should be able to update a basic user", async () => {
let user = structures.users.user({ email: "basic-update@test.com" })
await createUser(user)
jest.clearAllMocks()
await updateUser(user)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.user.permissionBuilderAssigned).not.toBeCalled()
expect(events.user.permissionAdminAssigned).not.toBeCalled()
expect(events.user.passwordForceReset).not.toBeCalled()
})
it("should be able to force reset password", async () => {
let user = structures.users.user({ email: "basic-password-update@test.com" })
await createUser(user)
jest.clearAllMocks()
user.forceResetPassword = true
user.password = "tempPassword"
await updateUser(user)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.user.permissionBuilderAssigned).not.toBeCalled()
expect(events.user.permissionAdminAssigned).not.toBeCalled()
expect(events.user.passwordForceReset).toBeCalledTimes(1)
})
it("should be able to update a basic user to an admin user", async () => {
let user = structures.users.user({ email: "basic-update-admin@test.com" })
await createUser(user)
jest.clearAllMocks()
await updateUser(structures.users.adminUser(user))
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.user.permissionBuilderAssigned).not.toBeCalled()
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1)
})
it("should be able to update a basic user to a builder user", async () => {
const user = structures.users.user({ email: "basic-update-builder@test.com" })
await createUser(user)
jest.clearAllMocks()
await updateUser(structures.users.builderUser(user))
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1)
expect(events.user.permissionAdminAssigned).not.toBeCalled()
})
it("should be able to update an admin user to a basic user", async () => {
const user = structures.users.adminUser({ email: "admin-update-basic@test.com" })
await createUser(user)
jest.clearAllMocks()
user.admin.global = false
await updateUser(user)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).not.toBeCalled()
})
it("should be able to update an builder user to a basic user", async () => {
const user = structures.users.builderUser({ email: "builder-update-basic@test.com" })
await createUser(user)
jest.clearAllMocks()
user.builder.global = false
await updateUser(user)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1)
expect(events.user.permissionAdminRemoved).not.toBeCalled()
})
it("should be able to assign app roles", async () => {
const user = structures.users.user({ email: "assign-roles-update@test.com" })
await createUser(user)
jest.clearAllMocks()
user.roles = {
"app_123": "role1",
"app_456": "role2",
}
await updateUser(user)
const savedUser = await config.getUser(user.email)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.role.assigned).toBeCalledTimes(2)
expect(events.role.assigned).toBeCalledWith(savedUser, "role1")
expect(events.role.assigned).toBeCalledWith(savedUser, "role2")
})
it("should be able to unassign app roles", async () => {
const user = structures.users.user({ email: "unassign-roles@test.com" })
user.roles = {
"app_123": "role1",
"app_456": "role2",
}
await createUser(user)
jest.clearAllMocks()
user.roles = {}
await updateUser(user)
const savedUser = await config.getUser(user.email)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.role.unassigned).toBeCalledTimes(2)
expect(events.role.unassigned).toBeCalledWith(savedUser, "role1")
expect(events.role.unassigned).toBeCalledWith(savedUser, "role2")
})
it("should be able to update existing app roles", async () => {
const user = structures.users.user({ email: "update-roles@test.com" })
user.roles = {
"app_123": "role1",
"app_456": "role2",
}
await createUser(user)
jest.clearAllMocks()
user.roles = {
"app_123": "role1",
"app_456": "role2-edit",
}
await updateUser(user)
const savedUser = await config.getUser(user.email)
expect(events.user.created).not.toBeCalled()
expect(events.user.updated).toBeCalledTimes(1)
expect(events.role.unassigned).toBeCalledTimes(1)
expect(events.role.unassigned).toBeCalledWith(savedUser, "role2")
expect(events.role.assigned).toBeCalledTimes(1)
expect(events.role.assigned).toBeCalledWith(savedUser, "role2-edit")
})
})
describe("bulkDelete", () => {
it("should not be able to bulkDelete account admin as admin", async () => {
})
it("should not be able to bulkDelete account owner as account owner", async () => {
})
it("should be able to bulk delete users with different permissions", async () => {
const builder = structures.users.builderUser({ email: "basic@test.com" })
const admin = structures.users.adminUser({ email: "admin@test.com" })
const user = structures.users.user({ email: "user@test.com" })
const createdUsers = await bulkCreateUsers([builder, admin, user])
await bulkDeleteUsers(createdUsers)
expect(events.user.deleted).toBeCalledTimes(3)
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1)
})
})
describe("destroy", () => {
it("should be able to destroy a basic user", async () => {
let user = structures.users.user({ email: "destroy@test.com" })
await createUser(user)
jest.clearAllMocks()
await deleteUser(user.email)
expect(events.user.deleted).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).not.toBeCalled()
expect(events.user.permissionAdminRemoved).not.toBeCalled()
})
it("should be able to destroy an admin user", async () => {
let user = structures.users.adminUser({ email: "destroy-admin@test.com" })
await createUser(user)
jest.clearAllMocks()
await deleteUser(user.email)
expect(events.user.deleted).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).not.toBeCalled()
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1)
})
it("should be able to destroy a builder user", async () => {
let user = structures.users.builderUser({ email: "destroy-admin@test.com" })
await createUser(user)
jest.clearAllMocks()
await deleteUser(user.email)
expect(events.user.deleted).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1)
expect(events.user.permissionAdminRemoved).not.toBeCalled()
})
it("should not be able to destroy account admin as admin", async () => {
})
it("should not be able to destroy account owner as account owner", async () => {
})
})
})

View file

@ -20,13 +20,13 @@ if (!LOADED && isDev() && !isTest()) {
LOADED = true
}
function parseIntSafe(number) {
function parseIntSafe(number: any) {
if (number) {
return parseInt(number)
}
}
module.exports = {
const env = {
// auth
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
@ -47,7 +47,7 @@ module.exports = {
CLUSTER_PORT: process.env.CLUSTER_PORT,
// flags
NODE_ENV: process.env.NODE_ENV,
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
LOG_LEVEL: process.env.LOG_LEVEL,
MULTI_TENANCY: process.env.MULTI_TENANCY,
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
@ -62,7 +62,7 @@ module.exports = {
// other
CHECKLIST_CACHE_TTL: parseIntSafe(process.env.CHECKLIST_CACHE_TTL) || 3600,
SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD,
_set(key, value) {
_set(key: any, value: any) {
process.env[key] = value
module.exports[key] = value
},
@ -74,16 +74,17 @@ module.exports = {
}
// if some var haven't been set, define them
if (!module.exports.APPS_URL) {
module.exports.APPS_URL = isDev()
? "http://localhost:4001"
: "http://app-service:4002"
if (!env.APPS_URL) {
env.APPS_URL = isDev() ? "http://localhost:4001" : "http://app-service:4002"
}
// clean up any environment variable edge cases
for (let [key, value] of Object.entries(module.exports)) {
// handle the edge case of "0" to disable an environment variable
if (value === "0") {
module.exports[key] = 0
// @ts-ignore
env[key] = 0
}
}
export = env

View file

@ -78,7 +78,7 @@ const shutdown = () => {
server.destroy()
}
module.exports = server.listen(parseInt(env.PORT || 4002), async () => {
export = server.listen(parseInt(env.PORT || 4002), async () => {
console.log(`Worker running on ${JSON.stringify(server.address())}`)
await redis.init()
})
@ -92,3 +92,7 @@ process.on("uncaughtException", err => {
process.on("SIGTERM", () => {
shutdown()
})
process.on("SIGINT", () => {
shutdown()
})

View file

@ -0,0 +1,53 @@
import { AccountMetadata } from "@budibase/types"
import {
db,
StaticDatabases,
HTTPError,
DocumentType,
SEPARATOR,
} from "@budibase/backend-core"
export const formatAccountMetadataId = (accountId: string) => {
return `${DocumentType.ACCOUNT_METADATA}${SEPARATOR}${accountId}`
}
export const saveMetadata = async (
metadata: AccountMetadata
): Promise<AccountMetadata> => {
return db.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => {
const existing = await getMetadata(metadata._id!)
if (existing) {
metadata._rev = existing._rev
}
const res = await db.put(metadata)
metadata._rev = res.rev
return metadata
})
}
export const getMetadata = async (
accountId: string
): Promise<AccountMetadata | undefined> => {
return db.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => {
try {
return await db.get(accountId)
} catch (e: any) {
if (e.status === 404) {
// do nothing
return
} else {
throw e
}
}
})
}
export const destroyMetadata = async (accountId: string) => {
await db.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => {
const metadata = await getMetadata(accountId)
if (!metadata) {
throw new HTTPError(`id=${accountId} does not exist`, 404)
}
await db.remove(accountId, metadata._rev)
})
}

View file

@ -0,0 +1 @@
export * from "./accounts"

View file

@ -1 +1,2 @@
export * as users from "./users"
export * as accounts from "./accounts"

View file

@ -15,9 +15,21 @@ import {
accounts,
migrations,
StaticDatabases,
ViewName
ViewName,
} from "@budibase/backend-core"
import { MigrationType, PlatformUserByEmail, User, Account } from "@budibase/types"
import {
MigrationType,
PlatformUserByEmail,
User,
Account,
BulkCreateUsersResponse,
CreateUserResponse,
BulkDeleteUsersResponse,
CloudAccount,
AllDocsResponse,
RowResponse,
BulkDocsResponse,
} from "@budibase/types"
import { groups as groupUtils } from "@budibase/pro"
const PAGE_LIMIT = 8
@ -100,7 +112,6 @@ export const getUser = async (userId: string) => {
interface SaveUserOpts {
hashPassword?: boolean
requirePassword?: boolean
bulkCreate?: boolean
}
const buildUser = async (
@ -111,7 +122,7 @@ const buildUser = async (
},
tenantId: string,
dbUser?: any
) => {
): Promise<User> => {
let { password, _id } = user
let hashedPassword
@ -145,62 +156,63 @@ const buildUser = async (
return user
}
const validateUniqueUser = async (email: string, tenantId: string) => {
// check budibase users in other tenants
if (env.MULTI_TENANCY) {
const tenantUser = await tenancy.getTenantUser(email)
if (tenantUser != null && tenantUser.tenantId !== tenantId) {
throw `Email address ${email} already in use.`
}
}
// check root account users in account portal
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
const account = await accounts.getAccount(email)
if (account && account.verified && account.tenantId !== tenantId) {
throw `Email address ${email} already in use.`
}
}
}
export const save = async (
user: any,
user: User,
opts: SaveUserOpts = {
hashPassword: true,
requirePassword: true,
bulkCreate: false,
}
) => {
): Promise<CreateUserResponse> => {
const tenantId = tenancy.getTenantId()
const db = tenancy.getGlobalDB()
let { email, _id } = user
// make sure another user isn't using the same email
let dbUser: any
if (opts.bulkCreate) {
dbUser = null
let dbUser: User | undefined
if (_id) {
// try to get existing user from db
dbUser = (await db.get(_id)) as User
if (email && dbUser.email !== email) {
throw "Email address cannot be changed"
}
email = dbUser.email
} else if (email) {
// check budibase users inside the tenant
// no id was specified - load from email instead
dbUser = await usersCore.getGlobalUserByEmail(email)
if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
if (dbUser && dbUser._id !== _id) {
throw `Email address ${email} already in use.`
}
// check budibase users in other tenants
if (env.MULTI_TENANCY) {
const tenantUser = await tenancy.getTenantUser(email)
if (tenantUser != null && tenantUser.tenantId !== tenantId) {
throw `Email address ${email} already in use.`
}
}
// check root account users in account portal
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
const account = await accounts.getAccount(email)
if (account && account.verified && account.tenantId !== tenantId) {
throw `Email address ${email} already in use.`
}
}
} else if (_id) {
dbUser = await db.get(_id)
} else {
throw new Error("_id or email is required")
}
await validateUniqueUser(email, tenantId)
let builtUser = await buildUser(user, opts, tenantId, dbUser)
// make sure we set the _id field for a new user
if (!_id) {
_id = builtUser._id
_id = builtUser._id!
}
try {
const putOpts = {
password: builtUser.password,
...user,
}
if (opts.bulkCreate) {
return putOpts
}
// save the user to db
let response
const putUserFn = () => {
@ -253,25 +265,32 @@ const getExistingTenantUsers = async (emails: string[]): Promise<User[]> => {
return dbUtils.queryGlobalView(ViewName.USER_BY_EMAIL, {
keys: emails,
include_docs: true,
arrayResponse: true
arrayResponse: true,
})
}
const getExistingPlatformUsers = async (emails: string[]): Promise<PlatformUserByEmail[]> => {
return dbUtils.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (infoDb: any) => {
const response = await infoDb.allDocs({
keys: emails,
include_docs: true,
})
return response.rows.map((row: any) => row.doc)
})
const getExistingPlatformUsers = async (
emails: string[]
): Promise<PlatformUserByEmail[]> => {
return dbUtils.doWithDB(
StaticDatabases.PLATFORM_INFO.name,
async (infoDb: any) => {
const response = await infoDb.allDocs({
keys: emails,
include_docs: true,
})
return response.rows
.filter((row: any) => row.error !== "not_found")
.map((row: any) => row.doc)
}
)
}
const getExistingAccounts = async (emails: string[]): Promise<Account[]> => {
return dbUtils.queryPlatformView(ViewName.ACCOUNT_BY_EMAIL, {
keys: emails,
include_docs: true,
arrayResponse: true
arrayResponse: true,
})
}
@ -289,18 +308,22 @@ const searchExistingEmails = async (emails: string[]) => {
matchedEmails.push(...existingTenantUsers.map((user: User) => user.email))
const existingPlatformUsers = await getExistingPlatformUsers(emails)
matchedEmails.push(...existingPlatformUsers.map((user: PlatformUserByEmail) => user._id!))
matchedEmails.push(
...existingPlatformUsers.map((user: PlatformUserByEmail) => user._id!)
)
const existingAccounts = await getExistingAccounts(emails)
matchedEmails.push(...existingAccounts.map((account: Account) => account.email))
matchedEmails.push(
...existingAccounts.map((account: Account) => account.email)
)
return matchedEmails
return [...new Set(matchedEmails)]
}
export const bulkCreate = async (
newUsersRequested: User[],
groups: string[]
) => {
): Promise<BulkCreateUsersResponse> => {
const db = tenancy.getGlobalDB()
const tenantId = tenancy.getTenantId()
@ -309,14 +332,17 @@ export const bulkCreate = async (
const emails = newUsersRequested.map((user: User) => user.email)
const existingEmails = await searchExistingEmails(emails)
const unsuccessful: { email: string, reason: string }[] = []
const unsuccessful: { email: string; reason: string }[] = []
for (const newUser of newUsersRequested) {
if (
newUsers.find((x: any) => x.email === newUser.email) ||
existingEmails.includes(newUser.email)
) {
unsuccessful.push({ email: newUser.email, reason: `Email address ${newUser.email} already in use.` })
unsuccessful.push({
email: newUser.email,
reason: `Email address ${newUser.email} already in use.`,
})
continue
}
newUser.userGroups = groups
@ -363,59 +389,121 @@ export const bulkCreate = async (
return {
successful: saved,
unsuccessful
unsuccessful,
}
}
export const bulkDelete = async (userIds: any) => {
/**
* For the given user id's, return the account holder if it is in the ids.
*/
const getAccountHolderFromUserIds = async (
userIds: string[]
): Promise<CloudAccount | undefined> => {
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
const tenantId = tenancy.getTenantId()
const account = await accounts.getAccountByTenantId(tenantId)
if (!account) {
throw new Error(`Account not found for tenantId=${tenantId}`)
}
const budibaseUserId = account.budibaseUserId
if (userIds.includes(budibaseUserId)) {
return account
}
}
}
export const bulkDelete = async (
userIds: string[]
): Promise<BulkDeleteUsersResponse> => {
const db = tenancy.getGlobalDB()
const response: BulkDeleteUsersResponse = {
successful: [],
unsuccessful: [],
}
// remove the account holder from the delete request if present
const account = await getAccountHolderFromUserIds(userIds)
if (account) {
userIds = userIds.filter(u => u !== account.budibaseUserId)
// mark user as unsuccessful
response.unsuccessful.push({
_id: account.budibaseUserId,
email: account.email,
reason: "Account holder cannot be deleted",
})
}
let groupsToModify: any = {}
let builderCount = 0
// Get users and delete
let usersToDelete = (
await db.allDocs({
include_docs: true,
keys: userIds,
})
).rows.map((user: any) => {
// if we find a user that has an associated group, add it to
// an array so we can easily use allDocs on them later.
// This prevents us having to re-loop over all the users
if (user.doc.userGroups) {
for (let groupId of user.doc.userGroups) {
if (!Object.keys(groupsToModify).includes(groupId)) {
groupsToModify[groupId] = [user.id]
} else {
groupsToModify[groupId] = [...groupsToModify[groupId], user.id]
const allDocsResponse: AllDocsResponse<User> = await db.allDocs({
include_docs: true,
keys: userIds,
})
const usersToDelete: User[] = allDocsResponse.rows.map(
(user: RowResponse<User>) => {
// if we find a user that has an associated group, add it to
// an array so we can easily use allDocs on them later.
// This prevents us having to re-loop over all the users
if (user.doc.userGroups) {
for (let groupId of user.doc.userGroups) {
if (!Object.keys(groupsToModify).includes(groupId)) {
groupsToModify[groupId] = [user.id]
} else {
groupsToModify[groupId] = [...groupsToModify[groupId], user.id]
}
}
}
// Also figure out how many builders are being deleted
if (eventHelpers.isAddingBuilder(user.doc, null)) {
builderCount++
}
return user.doc
}
)
// Also figure out how many builders are being deleted
if (eventHelpers.isAddingBuilder(user.doc, null)) {
builderCount++
}
return user.doc
})
const response = await db.bulkDocs(
usersToDelete.map((user: any) => ({
// Delete from DB
const dbResponse: BulkDocsResponse = await db.bulkDocs(
usersToDelete.map(user => ({
...user,
_deleted: true,
}))
)
// Deletion post processing
await groupUtils.bulkDeleteGroupUsers(groupsToModify)
//Deletion post processing
for (let user of usersToDelete) {
await bulkDeleteProcessing(user)
}
await quotas.removeDevelopers(builderCount)
// Build Response
// index users by id
const userIndex: { [key: string]: User } = {}
usersToDelete.reduce((prev, current) => {
prev[current._id!] = current
return prev
}, userIndex)
// add the successful and unsuccessful users to response
dbResponse.forEach(item => {
const email = userIndex[item.id].email
if (item.ok) {
response.successful.push({ _id: item.id, email })
} else {
response.unsuccessful.push({
_id: item.id,
email,
reason: "Database error",
})
}
})
return response
}

View file

@ -1,231 +0,0 @@
require("./mocks")
require("../db").init()
const env = require("../environment")
const controllers = require("./controllers")
const supertest = require("supertest")
const { jwt } = require("@budibase/backend-core/auth")
const { Cookies, Headers } = require("@budibase/backend-core/constants")
const { Configs } = require("../constants")
const { users } = require("@budibase/backend-core")
const { createASession } = require("@budibase/backend-core/sessions")
const { TENANT_ID, CSRF_TOKEN } = require("./structures")
const structures = require("./structures")
const { doInTenant } = require("@budibase/backend-core/tenancy")
const { groups } = require("@budibase/pro")
class TestConfiguration {
constructor(openServer = true) {
if (openServer) {
env.PORT = "0" // random port
this.server = require("../index")
// we need the request for logging in, involves cookies, hard to fake
this.request = supertest(this.server)
}
}
getRequest() {
return this.request
}
// UTILS
async _req(config, params, controlFunc) {
const request = {}
// fake cookies, we don't need them
request.cookies = { set: () => {}, get: () => {} }
request.config = { jwtSecret: env.JWT_SECRET }
request.appId = this.appId
request.user = { appId: this.appId, tenantId: TENANT_ID }
request.query = {}
request.request = {
body: config,
}
request.throw = (status, err) => {
throw { status, message: err }
}
if (params) {
request.params = params
}
await doInTenant(TENANT_ID, () => {
return controlFunc(request)
})
return request.body
}
// SETUP / TEARDOWN
async beforeAll() {
await this.login()
}
async afterAll() {
if (this.server) {
await this.server.close()
}
}
// USER / AUTH
async login() {
// create a test user
await this._req(
{
email: "test@test.com",
password: "test",
_id: "us_uuid1",
builder: {
global: true,
},
admin: {
global: true,
},
},
null,
controllers.users.save
)
await createASession("us_uuid1", {
sessionId: "sessionid",
tenantId: TENANT_ID,
csrfToken: CSRF_TOKEN,
})
}
cookieHeader(cookies) {
return {
Cookie: [cookies],
}
}
defaultHeaders() {
const user = {
_id: "us_uuid1",
userId: "us_uuid1",
sessionId: "sessionid",
tenantId: TENANT_ID,
}
const authToken = jwt.sign(user, env.JWT_SECRET)
return {
Accept: "application/json",
...this.cookieHeader([`${Cookies.Auth}=${authToken}`]),
[Headers.CSRF_TOKEN]: CSRF_TOKEN,
}
}
async getUser(email) {
return doInTenant(TENANT_ID, () => {
return users.getGlobalUserByEmail(email)
})
}
async getGroup(id) {
return doInTenant(TENANT_ID, () => {
return groups.get(id)
})
}
async saveGroup(group) {
const res = await this.getRequest()
.post(`/api/global/groups`)
.send(group)
.set(this.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
return res.body
}
async createUser(email, password) {
const user = await this.getUser(structures.users.email)
if (user) {
return user
}
await this._req(
structures.users.user({ email, password }),
null,
controllers.users.save
)
}
async saveAdminUser() {
await this._req(
structures.users.user({ tenantId: TENANT_ID }),
null,
controllers.users.adminUser
)
}
// CONFIGS
async deleteConfig(type) {
try {
const cfg = await this._req(
null,
{
type,
},
controllers.config.find
)
if (cfg) {
await this._req(
null,
{
id: cfg._id,
rev: cfg._rev,
},
controllers.config.destroy
)
}
} catch (err) {
// don't need to handle error
}
}
// CONFIGS - SETTINGS
async saveSettingsConfig() {
await this.deleteConfig(Configs.SETTINGS)
await this._req(
structures.configs.settings(),
null,
controllers.config.save
)
}
// CONFIGS - GOOGLE
async saveGoogleConfig() {
await this.deleteConfig(Configs.GOOGLE)
await this._req(structures.configs.google(), null, controllers.config.save)
}
// CONFIGS - OIDC
getOIDConfigCookie(configId) {
const token = jwt.sign(configId, env.JWT_SECRET)
return this.cookieHeader([[`${Cookies.OIDC_CONFIG}=${token}`]])
}
async saveOIDCConfig() {
await this.deleteConfig(Configs.OIDC)
const config = structures.configs.oidc()
await this._req(config, null, controllers.config.save)
return config
}
// CONFIGS - SMTP
async saveSmtpConfig() {
await this.deleteConfig(Configs.SMTP)
await this._req(structures.configs.smtp(), null, controllers.config.save)
}
async saveEtherealSmtpConfig() {
await this.deleteConfig(Configs.SMTP)
await this._req(
structures.configs.smtpEthereal(),
null,
controllers.config.save
)
}
}
module.exports = TestConfiguration

View file

@ -0,0 +1,269 @@
import "./mocks"
import dbConfig from "../db"
dbConfig.init()
import env from "../environment"
import controllers from "./controllers"
const supertest = require("supertest")
import { jwt } from "@budibase/backend-core/auth"
import { Cookies, Headers } from "@budibase/backend-core/constants"
import { Configs } from "../constants"
import { users, tenancy } from "@budibase/backend-core"
import { createASession } from "@budibase/backend-core/sessions"
import { TENANT_ID, TENANT_1, CSRF_TOKEN } from "./structures"
import structures from "./structures"
import { CreateUserResponse, User, AuthToken } from "@budibase/types"
enum Mode {
ACCOUNT = "account",
SELF = "self",
}
class TestConfiguration {
server: any
request: any
defaultUser?: User
tenant1User?: User
constructor(
opts: { openServer: boolean; mode: Mode } = {
openServer: true,
mode: Mode.ACCOUNT,
}
) {
if (opts.mode === Mode.ACCOUNT) {
this.modeAccount()
} else if (opts.mode === Mode.SELF) {
this.modeSelf()
}
if (opts.openServer) {
env.PORT = "0" // random port
this.server = require("../index")
// we need the request for logging in, involves cookies, hard to fake
this.request = supertest(this.server)
}
}
getRequest() {
return this.request
}
// MODES
modeAccount = () => {
env.SELF_HOSTED = false
// @ts-ignore
env.MULTI_TENANCY = true
// @ts-ignore
env.DISABLE_ACCOUNT_PORTAL = false
}
modeSelf = () => {
env.SELF_HOSTED = true
// @ts-ignore
env.MULTI_TENANCY = false
// @ts-ignore
env.DISABLE_ACCOUNT_PORTAL = true
}
// UTILS
async _req(config: any, params: any, controlFunc: any) {
const request: any = {}
// fake cookies, we don't need them
request.cookies = { set: () => {}, get: () => {} }
request.config = { jwtSecret: env.JWT_SECRET }
request.user = { tenantId: this.getTenantId() }
request.query = {}
request.request = {
body: config,
}
request.throw = (status: any, err: any) => {
throw { status, message: err }
}
if (params) {
request.params = params
}
await tenancy.doInTenant(this.getTenantId(), () => {
return controlFunc(request)
})
return request.body
}
// SETUP / TEARDOWN
async beforeAll() {
await this.createDefaultUser()
await this.createSession(this.defaultUser!)
await tenancy.doInTenant(TENANT_1, async () => {
await this.createTenant1User()
await this.createSession(this.tenant1User!)
})
}
async afterAll() {
if (this.server) {
await this.server.close()
}
}
// TENANCY
getTenantId() {
try {
return tenancy.getTenantId()
} catch (e: any) {
return TENANT_ID
}
}
// USER / AUTH
async createDefaultUser() {
const user = structures.users.adminUser({
email: "test@test.com",
password: "test",
})
this.defaultUser = await this.createUser(user)
}
async createTenant1User() {
const user = structures.users.adminUser({
email: "tenant1@test.com",
password: "test",
})
this.tenant1User = await this.createUser(user)
}
async createSession(user: User) {
await 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 = jwt.sign(authToken, env.JWT_SECRET)
return {
Accept: "application/json",
...this.cookieHeader([`${Cookies.Auth}=${authCookie}`]),
[Headers.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)
})
}
async createUser(user?: User) {
if (!user) {
user = structures.users.user()
}
const response = await this._req(user, null, controllers.users.save)
const body = response as CreateUserResponse
return this.getUser(body.email)
}
// CONFIGS
async deleteConfig(type: any) {
try {
const cfg = await this._req(
null,
{
type,
},
controllers.config.find
)
if (cfg) {
await this._req(
null,
{
id: cfg._id,
rev: cfg._rev,
},
controllers.config.destroy
)
}
} catch (err) {
// don't need to handle error
}
}
// CONFIGS - SETTINGS
async saveSettingsConfig() {
await this.deleteConfig(Configs.SETTINGS)
await this._req(
structures.configs.settings(),
null,
controllers.config.save
)
}
// CONFIGS - GOOGLE
async saveGoogleConfig() {
await this.deleteConfig(Configs.GOOGLE)
await this._req(structures.configs.google(), null, controllers.config.save)
}
// CONFIGS - OIDC
getOIDConfigCookie(configId: string) {
const token = jwt.sign(configId, env.JWT_SECRET)
return this.cookieHeader([[`${Cookies.OIDC_CONFIG}=${token}`]])
}
async saveOIDCConfig() {
await this.deleteConfig(Configs.OIDC)
const config = structures.configs.oidc()
await this._req(config, null, controllers.config.save)
return config
}
// CONFIGS - SMTP
async saveSmtpConfig() {
await this.deleteConfig(Configs.SMTP)
await this._req(structures.configs.smtp(), null, controllers.config.save)
}
async saveEtherealSmtpConfig() {
await this.deleteConfig(Configs.SMTP)
await this._req(
structures.configs.smtpEthereal(),
null,
controllers.config.save
)
}
}
export = TestConfiguration

View file

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

View file

@ -0,0 +1,48 @@
import TestConfiguration from "../TestConfiguration"
export class AuthAPI {
config: TestConfiguration
request: any
constructor(config: TestConfiguration) {
this.config = config
this.request = config.request
}
updatePassword = (code: string) => {
return this.request
.post(`/api/global/auth/${this.config.getTenantId()}/reset/update`)
.send({
password: "newpassword",
resetCode: code,
})
.expect("Content-Type", /json/)
.expect(200)
}
logout = () => {
return this.request
.post("/api/global/auth/logout")
.set(this.config.defaultHeaders())
.expect(200)
}
requestPasswordReset = async (sendMailMock: any) => {
await this.config.saveSmtpConfig()
await this.config.saveSettingsConfig()
await this.config.createUser()
const res = await this.request
.post(`/api/global/auth/${this.config.getTenantId()}/reset`)
.send({
email: "test@test.com",
})
.expect("Content-Type", /json/)
.expect(200)
const emailCall = sendMailMock.mock.calls[0][0]
const parts = emailCall.html.split(
`http://localhost:10000/builder/auth/reset?code=`
)
const code = parts[1].split('"')[0].split("&")[0]
return { code, res }
}
}

View file

@ -0,0 +1,40 @@
import TestConfiguration from "../TestConfiguration"
export class ConfigAPI {
config: TestConfiguration
request: any
constructor(config: TestConfiguration) {
this.config = config
this.request = config.request
}
getConfigChecklist = () => {
return this.request
.get(`/api/global/configs/checklist`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
}
saveConfig = (data: any) => {
return this.request
.post(`/api/global/configs`)
.send(data)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
}
OIDCCallback = (configId: string) => {
return this.request
.get(`/api/global/auth/${this.config.getTenantId()}/oidc/callback`)
.set(this.config.getOIDConfigCookie(configId))
}
getOIDCConfig = (configId: string) => {
return this.request.get(
`/api/global/auth/${this.config.getTenantId()}/oidc/configs/${configId}`
)
}
}

View file

@ -0,0 +1,24 @@
import TestConfiguration from "../TestConfiguration"
export class EmailAPI {
config: TestConfiguration
request: any
constructor(config: TestConfiguration) {
this.config = config
this.request = config.request
}
sendEmail = (purpose: string) => {
return this.request
.post(`/api/global/email/send`)
.send({
email: "test@test.com",
purpose,
tenantId: this.config.getTenantId(),
})
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
}
}

View file

@ -0,0 +1,25 @@
import TestConfiguration from "../TestConfiguration"
import { AccountAPI } from "./accounts"
import { AuthAPI } from "./auth"
import { ConfigAPI } from "./configs"
import { EmailAPI } from "./email"
import { SelfAPI } from "./self"
import { UserAPI } from "./users"
export default class API {
accounts: AccountAPI
auth: AuthAPI
configs: ConfigAPI
emails: EmailAPI
self: SelfAPI
users: UserAPI
constructor(config: TestConfiguration) {
this.accounts = new AccountAPI(config)
this.auth = new AuthAPI(config)
this.configs = new ConfigAPI(config)
this.emails = new EmailAPI(config)
this.self = new SelfAPI(config)
this.users = new UserAPI(config)
}
}

View file

@ -0,0 +1,21 @@
import TestConfiguration from "../TestConfiguration"
import { User } from "@budibase/types"
export class SelfAPI {
config: TestConfiguration
request: any
constructor(config: TestConfiguration) {
this.config = config
this.request = config.request
}
updateSelf = (user: User) => {
return this.request
.post(`/api/global/self`)
.send(user)
.set(this.config.authHeaders(user))
.expect("Content-Type", /json/)
.expect(200)
}
}

View file

@ -0,0 +1,95 @@
import {
BulkCreateUsersRequest,
BulkCreateUsersResponse,
BulkDeleteUsersRequest,
CreateUserResponse,
User,
UserDetails,
} from "@budibase/types"
import TestConfiguration from "../TestConfiguration"
export class UserAPI {
config: TestConfiguration
request: any
constructor(config: TestConfiguration) {
this.config = config
this.request = config.request
}
// INVITE
sendUserInvite = async (sendMailMock: any) => {
await this.config.saveSmtpConfig()
await this.config.saveSettingsConfig()
const res = await this.request
.post(`/api/global/users/invite`)
.send({
email: "invite@test.com",
})
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
const emailCall = sendMailMock.mock.calls[0][0]
// after this URL there should be a code
const parts = emailCall.html.split(
"http://localhost:10000/builder/invite?code="
)
const code = parts[1].split('"')[0].split("&")[0]
return { code, res }
}
acceptInvite = (code: string) => {
return this.request
.post(`/api/global/users/invite/accept`)
.send({
password: "newpassword",
inviteCode: code,
})
.expect("Content-Type", /json/)
.expect(200)
}
// BULK
bulkCreateUsers = async (users: User[], groups: any[] = []) => {
const body: BulkCreateUsersRequest = { users, groups }
const res = await this.request
.post(`/api/global/users/bulkCreate`)
.send(body)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
return res.body as BulkCreateUsersResponse
}
bulkDeleteUsers = (body: BulkDeleteUsersRequest, status?: number) => {
return this.request
.post(`/api/global/users/bulkDelete`)
.send(body)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(status ? status : 200)
}
// USER
saveUser = (user: User, status?: number) => {
return this.request
.post(`/api/global/users`)
.send(user)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(status ? status : 200)
}
deleteUser = (userId: string, status?: number) => {
return this.request
.delete(`/api/global/users/${userId}`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(status ? status : 200)
}
}

View file

@ -1,15 +1,14 @@
import TestConfiguration from "./TestConfiguration"
import structures from "./structures"
import mocks from "./mocks"
import API from "./api"
const config = new TestConfiguration()
const request = config.getRequest()
const pkg = {
const pkg = {
structures,
TENANT_1: structures.TENANT_1,
mocks,
config,
request,
TestConfiguration,
API,
}
export = pkg

View file

@ -1,5 +0,0 @@
const email = require("./email")
module.exports = {
email,
}

View file

@ -0,0 +1,7 @@
const email = require("./email")
import { mocks as coreMocks } from "@budibase/backend-core/tests"
export = {
email,
...coreMocks,
}

View file

@ -0,0 +1,24 @@
import { Account, AuthType, Hosting, CloudAccount } from "@budibase/types"
import { v4 as uuid } from "uuid"
import { utils } from "@budibase/backend-core"
export const account = (): Account => {
return {
email: `${uuid()}@test.com`,
tenantId: utils.newid(),
hosting: Hosting.SELF,
authType: AuthType.SSO,
accountId: uuid(),
createdAt: Date.now(),
verified: true,
verificationSent: true,
tier: "FREE",
}
}
export const cloudAccount = (): CloudAccount => {
return {
...account(),
budibaseUserId: uuid(),
}
}

View file

@ -1,14 +1,20 @@
import configs from "./configs"
import * as users from "./users"
import * as groups from "./groups"
import * as accounts from "./accounts"
const TENANT_ID = "default"
const TENANT_1 = "tenant1"
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
export = {
const pkg = {
configs,
users,
accounts,
TENANT_ID,
TENANT_1,
CSRF_TOKEN,
groups,
}
export = pkg

View file

@ -1,28 +1,32 @@
export const email = "test@test.com"
import { AdminUser, BuilderUser, User } from "@budibase/types"
import { v4 as uuid } from "uuid"
export const user = (userProps: any): User => {
export const newEmail = () => {
return `${uuid()}@test.com`
}
export const user = (userProps?: any): User => {
return {
email: "test@test.com",
email: newEmail(),
password: "test",
roles: {},
...userProps,
}
}
export const adminUser = (userProps: any): AdminUser => {
export const adminUser = (userProps?: any): AdminUser => {
return {
...user(userProps),
admin: {
global: true,
},
builder: {
global: true
}
global: true,
},
}
}
export const builderUser = (userProps: any): BuilderUser => {
export const builderUser = (userProps?: any): BuilderUser => {
return {
...user(userProps),
builder: {

View file

@ -55,6 +55,7 @@ exports.init = async () => {
exports.shutdown = async () => {
if (pwResetClient) await pwResetClient.finish()
if (invitationClient) await invitationClient.finish()
console.log("Redis shutdown")
}
/**

View file

@ -1074,6 +1074,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
"@types/uuid@8.3.4":
version "8.3.4"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
"@types/yargs-parser@*":
version "21.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"