update bulk create and bulk delete backend
This commit is contained in:
parent
d591acf2d3
commit
59a53736ac
|
@ -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,
|
|
@ -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 = {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,4 +8,5 @@ import { processors } from "./processors"
|
|||
|
||||
export const shutdown = () => {
|
||||
processors.shutdown()
|
||||
console.log("Events shutdown")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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}`)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export const getAccount = jest.fn()
|
||||
export const getAccountByTenantId = jest.fn()
|
||||
|
||||
jest.mock("../../../src/cloud/accounts", () => ({
|
||||
getAccount,
|
||||
getAccountByTenantId,
|
||||
}))
|
|
@ -1,2 +0,0 @@
|
|||
exports.MOCK_DATE = new Date("2020-01-01T00:00:00.000Z")
|
||||
exports.MOCK_DATE_TIMESTAMP = 1577836800000
|
|
@ -0,0 +1,2 @@
|
|||
export const MOCK_DATE = new Date("2020-01-01T00:00:00.000Z")
|
||||
export const MOCK_DATE_TIMESTAMP = 1577836800000
|
|
@ -1,9 +0,0 @@
|
|||
const posthog = require("./posthog")
|
||||
const events = require("./events")
|
||||
const date = require("./date")
|
||||
|
||||
module.exports = {
|
||||
posthog,
|
||||
date,
|
||||
events,
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import "./posthog"
|
||||
import "./events"
|
||||
export * as accounts from "./accounts"
|
||||
export * as date from "./date"
|
|
@ -0,0 +1,5 @@
|
|||
export interface APIError {
|
||||
message: string
|
||||
status: number
|
||||
error?: any
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./analytics"
|
||||
export * from "./user"
|
||||
export * from "./user"
|
||||
export * from "./errors"
|
||||
|
|
|
@ -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 }[]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -3,3 +3,4 @@ export * from "./app"
|
|||
export * from "./global"
|
||||
export * from "./platform"
|
||||
export * from "./document"
|
||||
export * from "./pouch"
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { Document } from "../document"
|
||||
|
||||
export interface AccountMetadata extends Document {
|
||||
email: string
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./info"
|
||||
export * from "./users"
|
||||
export * from "./users"
|
||||
export * from "./accounts"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Document } from "../document";
|
||||
import { Document } from "../document"
|
||||
|
||||
/**
|
||||
* doc id is user email
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export interface AuthToken {
|
||||
userId: string
|
||||
tenantId: string
|
||||
sessionId: string
|
||||
}
|
|
@ -5,3 +5,4 @@ export * from "./licensing"
|
|||
export * from "./migrations"
|
||||
export * from "./datasources"
|
||||
export * from "./search"
|
||||
export * from "./auth"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
|
@ -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()
|
|
@ -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]
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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.")
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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,
|
||||
]
|
||||
|
|
|
@ -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
|
|
@ -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`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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 () => {
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
})
|
|
@ -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
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./accounts"
|
|
@ -1 +1,2 @@
|
|||
export * as users from "./users"
|
||||
export * as accounts from "./accounts"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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}`
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
const email = require("./email")
|
||||
|
||||
module.exports = {
|
||||
email,
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
const email = require("./email")
|
||||
import { mocks as coreMocks } from "@budibase/backend-core/tests"
|
||||
|
||||
export = {
|
||||
email,
|
||||
...coreMocks,
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -55,6 +55,7 @@ exports.init = async () => {
|
|||
exports.shutdown = async () => {
|
||||
if (pwResetClient) await pwResetClient.finish()
|
||||
if (invitationClient) await invitationClient.finish()
|
||||
console.log("Redis shutdown")
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue