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 passport = require("koa-passport")
|
||||||
const LocalStrategy = require("passport-local").Strategy
|
const LocalStrategy = require("passport-local").Strategy
|
||||||
const JwtStrategy = require("passport-jwt").Strategy
|
const JwtStrategy = require("passport-jwt").Strategy
|
||||||
const { getGlobalDB } = require("./tenancy")
|
import { getGlobalDB } from "./tenancy"
|
||||||
const refresh = require("passport-oauth2-refresh")
|
const refresh = require("passport-oauth2-refresh")
|
||||||
const { Configs } = require("./constants")
|
import { Configs } from "./constants"
|
||||||
const { getScopedConfig } = require("./db/utils")
|
import { getScopedConfig } from "./db/utils"
|
||||||
const {
|
import {
|
||||||
jwt,
|
jwt,
|
||||||
local,
|
local,
|
||||||
authenticated,
|
authenticated,
|
||||||
|
@ -13,7 +13,6 @@ const {
|
||||||
oidc,
|
oidc,
|
||||||
auditLog,
|
auditLog,
|
||||||
tenancy,
|
tenancy,
|
||||||
appTenancy,
|
|
||||||
authError,
|
authError,
|
||||||
ssoCallbackUrl,
|
ssoCallbackUrl,
|
||||||
csrf,
|
csrf,
|
||||||
|
@ -22,32 +21,36 @@ const {
|
||||||
builderOnly,
|
builderOnly,
|
||||||
builderOrAdmin,
|
builderOrAdmin,
|
||||||
joiValidator,
|
joiValidator,
|
||||||
} = require("./middleware")
|
} from "./middleware"
|
||||||
|
import { invalidateUser } from "./cache/user"
|
||||||
const { invalidateUser } = require("./cache/user")
|
import { User } from "@budibase/types"
|
||||||
|
|
||||||
// Strategies
|
// Strategies
|
||||||
passport.use(new LocalStrategy(local.options, local.authenticate))
|
passport.use(new LocalStrategy(local.options, local.authenticate))
|
||||||
passport.use(new JwtStrategy(jwt.options, jwt.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()
|
const db = getGlobalDB()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await db.get(user._id)
|
const dbUser = await db.get(user._id)
|
||||||
return done(null, user)
|
return done(null, dbUser)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`User not found`, err)
|
console.error(`User not found`, err)
|
||||||
return done(null, false, { message: "User not found" })
|
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)
|
const callbackUrl = await oidc.getCallbackUrl(db, chosenConfig)
|
||||||
let enrichedConfig
|
let enrichedConfig: any
|
||||||
let strategy
|
let strategy: any
|
||||||
|
|
||||||
try {
|
try {
|
||||||
enrichedConfig = await oidc.fetchStrategyConfig(chosenConfig, callbackUrl)
|
enrichedConfig = await oidc.fetchStrategyConfig(chosenConfig, callbackUrl)
|
||||||
|
@ -70,22 +73,28 @@ async function refreshOIDCAccessToken(db, chosenConfig, refreshToken) {
|
||||||
refresh.requestNewAccessToken(
|
refresh.requestNewAccessToken(
|
||||||
Configs.OIDC,
|
Configs.OIDC,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
(err, accessToken, refreshToken, params) => {
|
(err: any, accessToken: string, refreshToken: any, params: any) => {
|
||||||
resolve({ err, accessToken, refreshToken, params })
|
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 callbackUrl = await google.getCallbackUrl(db, config)
|
||||||
|
|
||||||
let strategy
|
let strategy
|
||||||
try {
|
try {
|
||||||
strategy = await google.strategyFactory(config, callbackUrl)
|
strategy = await google.strategyFactory(config, callbackUrl)
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.error(err)
|
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)
|
refresh.use(strategy)
|
||||||
|
@ -94,14 +103,18 @@ async function refreshGoogleAccessToken(db, config, refreshToken) {
|
||||||
refresh.requestNewAccessToken(
|
refresh.requestNewAccessToken(
|
||||||
Configs.GOOGLE,
|
Configs.GOOGLE,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
(err, accessToken, refreshToken, params) => {
|
(err: any, accessToken: string, refreshToken: string, params: any) => {
|
||||||
resolve({ err, accessToken, refreshToken, params })
|
resolve({ err, accessToken, refreshToken, params })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshOAuthToken(refreshToken, configType, configId) {
|
async function refreshOAuthToken(
|
||||||
|
refreshToken: string,
|
||||||
|
configType: string,
|
||||||
|
configId: string
|
||||||
|
) {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
|
|
||||||
const config = await getScopedConfig(db, {
|
const config = await getScopedConfig(db, {
|
||||||
|
@ -113,7 +126,7 @@ async function refreshOAuthToken(refreshToken, configType, configId) {
|
||||||
let refreshResponse
|
let refreshResponse
|
||||||
if (configType === Configs.OIDC) {
|
if (configType === Configs.OIDC) {
|
||||||
// configId - retrieved from cookie.
|
// 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) {
|
if (!chosenConfig) {
|
||||||
throw new Error("Invalid OIDC configuration")
|
throw new Error("Invalid OIDC configuration")
|
||||||
}
|
}
|
||||||
|
@ -134,7 +147,7 @@ async function refreshOAuthToken(refreshToken, configType, configId) {
|
||||||
return refreshResponse
|
return refreshResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUserOAuth(userId, oAuthConfig) {
|
async function updateUserOAuth(userId: string, oAuthConfig: any) {
|
||||||
const details = {
|
const details = {
|
||||||
accessToken: oAuthConfig.accessToken,
|
accessToken: oAuthConfig.accessToken,
|
||||||
refreshToken: oAuthConfig.refreshToken,
|
refreshToken: oAuthConfig.refreshToken,
|
||||||
|
@ -162,14 +175,13 @@ async function updateUserOAuth(userId, oAuthConfig) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
export = {
|
||||||
buildAuthMiddleware: authenticated,
|
buildAuthMiddleware: authenticated,
|
||||||
passport,
|
passport,
|
||||||
google,
|
google,
|
||||||
oidc,
|
oidc,
|
||||||
jwt: require("jsonwebtoken"),
|
jwt: require("jsonwebtoken"),
|
||||||
buildTenancyMiddleware: tenancy,
|
buildTenancyMiddleware: tenancy,
|
||||||
buildAppTenancyMiddleware: appTenancy,
|
|
||||||
auditLog,
|
auditLog,
|
||||||
authError,
|
authError,
|
||||||
buildCsrfMiddleware: csrf,
|
buildCsrfMiddleware: csrf,
|
|
@ -42,7 +42,7 @@ export enum DocumentType {
|
||||||
MIGRATIONS = "migrations",
|
MIGRATIONS = "migrations",
|
||||||
DEV_INFO = "devinfo",
|
DEV_INFO = "devinfo",
|
||||||
AUTOMATION_LOG = "log_au",
|
AUTOMATION_LOG = "log_au",
|
||||||
ACCOUNT = "acc",
|
ACCOUNT_METADATA = "acc_metadata",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StaticDatabases = {
|
export const StaticDatabases = {
|
||||||
|
|
|
@ -6,7 +6,7 @@ const {
|
||||||
} = require("./utils")
|
} = require("./utils")
|
||||||
const { getGlobalDB } = require("../tenancy")
|
const { getGlobalDB } = require("../tenancy")
|
||||||
const { StaticDatabases } = require("./constants")
|
const { StaticDatabases } = require("./constants")
|
||||||
const { doWithDB } = require("./");
|
const { doWithDB } = require("./")
|
||||||
|
|
||||||
const DESIGN_DB = "_design/database"
|
const DESIGN_DB = "_design/database"
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ exports.createNewUserEmailView = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createAccountEmailView = async () => {
|
exports.createAccountEmailView = async () => {
|
||||||
await doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db) => {
|
await doWithDB(StaticDatabases.PLATFORM_INFO.name, async db => {
|
||||||
let designDoc
|
let designDoc
|
||||||
try {
|
try {
|
||||||
designDoc = await db.get(DESIGN_DB)
|
designDoc = await db.get(DESIGN_DB)
|
||||||
|
@ -70,8 +70,8 @@ exports.createAccountEmailView = async () => {
|
||||||
const view = {
|
const view = {
|
||||||
// if using variables in a map function need to inject them before use
|
// if using variables in a map function need to inject them before use
|
||||||
map: `function(doc) {
|
map: `function(doc) {
|
||||||
if (doc._id.startsWith("${DocumentType.ACCOUNT}${SEPARATOR}")) {
|
if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) {
|
||||||
emit(doc.email.toLowerCase(), doc.tenantId)
|
emit(doc.email.toLowerCase(), doc._id)
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
}
|
}
|
||||||
|
@ -171,7 +171,7 @@ exports.queryView = async (viewName, params, db, CreateFuncByName) => {
|
||||||
const createFunc = CreateFuncByName[viewName]
|
const createFunc = CreateFuncByName[viewName]
|
||||||
await removeDeprecated(db, viewName)
|
await removeDeprecated(db, viewName)
|
||||||
await createFunc()
|
await createFunc()
|
||||||
return exports.queryGlobalView(viewName, params)
|
return exports.queryView(viewName, params, db, CreateFuncByName)
|
||||||
} else {
|
} else {
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
@ -183,7 +183,7 @@ exports.queryPlatformView = async (viewName, params) => {
|
||||||
[ViewName.ACCOUNT_BY_EMAIL]: exports.createAccountEmailView,
|
[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)
|
return exports.queryView(viewName, params, db, CreateFuncByName)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,4 +8,5 @@ import { processors } from "./processors"
|
||||||
|
|
||||||
export const shutdown = () => {
|
export const shutdown = () => {
|
||||||
processors.shutdown()
|
processors.shutdown()
|
||||||
|
console.log("Events shutdown")
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import constants from "./constants"
|
||||||
import * as dbConstants from "./db/constants"
|
import * as dbConstants from "./db/constants"
|
||||||
import logging from "./logging"
|
import logging from "./logging"
|
||||||
import pino from "./pino"
|
import pino from "./pino"
|
||||||
|
import * as middleware from "./middleware"
|
||||||
|
|
||||||
// mimic the outer package exports
|
// mimic the outer package exports
|
||||||
import * as db from "./pkg/db"
|
import * as db from "./pkg/db"
|
||||||
|
@ -57,6 +58,7 @@ const core = {
|
||||||
roles,
|
roles,
|
||||||
...pino,
|
...pino,
|
||||||
...errorClasses,
|
...errorClasses,
|
||||||
|
middleware,
|
||||||
}
|
}
|
||||||
|
|
||||||
export = core
|
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
|
* The tenancy modules should not be used here and it should be assumed that the tenancy context
|
||||||
* has not yet been populated.
|
* has not yet been populated.
|
||||||
*/
|
*/
|
||||||
module.exports = (
|
export = (
|
||||||
noAuthPatterns = [],
|
noAuthPatterns = [],
|
||||||
opts: { publicAllowed: boolean; populateUser?: Function } = {
|
opts: { publicAllowed: boolean; populateUser?: Function } = {
|
||||||
publicAllowed: false,
|
publicAllowed: false,
|
||||||
|
|
|
@ -13,7 +13,8 @@ const adminOnly = require("./adminOnly")
|
||||||
const builderOrAdmin = require("./builderOrAdmin")
|
const builderOrAdmin = require("./builderOrAdmin")
|
||||||
const builderOnly = require("./builderOnly")
|
const builderOnly = require("./builderOnly")
|
||||||
const joiValidator = require("./joi-validator")
|
const joiValidator = require("./joi-validator")
|
||||||
module.exports = {
|
|
||||||
|
const pkg = {
|
||||||
google,
|
google,
|
||||||
oidc,
|
oidc,
|
||||||
jwt,
|
jwt,
|
||||||
|
@ -33,3 +34,5 @@ module.exports = {
|
||||||
builderOrAdmin,
|
builderOrAdmin,
|
||||||
joiValidator,
|
joiValidator,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export = pkg
|
|
@ -3,17 +3,27 @@ const { v4: uuidv4 } = require("uuid")
|
||||||
const { logWarn } = require("../logging")
|
const { logWarn } = require("../logging")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
|
||||||
interface Session {
|
interface CreateSession {
|
||||||
key: string
|
|
||||||
userId: string
|
|
||||||
sessionId: string
|
sessionId: string
|
||||||
lastAccessedAt: string
|
tenantId: string
|
||||||
createdAt: string
|
|
||||||
csrfToken?: 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
|
// a week in seconds
|
||||||
const EXPIRY_SECONDS = 86400 * 7
|
const EXPIRY_SECONDS = 86400 * 7
|
||||||
|
@ -22,14 +32,14 @@ function makeSessionID(userId: string, sessionId: string) {
|
||||||
return `${userId}/${sessionId}`
|
return `${userId}/${sessionId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSessionsForUser(userId: string) {
|
export async function getSessionsForUser(userId: string): Promise<Session[]> {
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
console.trace("Cannot get sessions for undefined userId")
|
console.trace("Cannot get sessions for undefined userId")
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
const client = await redis.getSessionClient()
|
const client = await redis.getSessionClient()
|
||||||
const sessions = await client.scan(userId)
|
const sessions: ScannedSession[] = await client.scan(userId)
|
||||||
return sessions.map((session: Session) => session.value)
|
return sessions.map(session => session.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function invalidateSessions(
|
export async function invalidateSessions(
|
||||||
|
@ -39,33 +49,32 @@ export async function invalidateSessions(
|
||||||
try {
|
try {
|
||||||
const reason = opts?.reason || "unknown"
|
const reason = opts?.reason || "unknown"
|
||||||
let sessionIds: string[] = opts.sessionIds || []
|
let sessionIds: string[] = opts.sessionIds || []
|
||||||
let sessions: SessionKey
|
let sessionKeys: SessionKey[]
|
||||||
|
|
||||||
// If no sessionIds, get all the sessions for the user
|
// If no sessionIds, get all the sessions for the user
|
||||||
if (sessionIds.length === 0) {
|
if (sessionIds.length === 0) {
|
||||||
sessions = await getSessionsForUser(userId)
|
const sessions = await getSessionsForUser(userId)
|
||||||
sessions.forEach(
|
sessionKeys = sessions.map(session => ({
|
||||||
(session: any) =>
|
key: makeSessionID(session.userId, session.sessionId),
|
||||||
(session.key = makeSessionID(session.userId, session.sessionId))
|
}))
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
// use the passed array of sessionIds
|
// use the passed array of sessionIds
|
||||||
sessionIds = Array.isArray(sessionIds) ? sessionIds : [sessionIds]
|
sessionIds = Array.isArray(sessionIds) ? sessionIds : [sessionIds]
|
||||||
sessions = sessionIds.map((sessionId: string) => ({
|
sessionKeys = sessionIds.map(sessionId => ({
|
||||||
key: makeSessionID(userId, sessionId),
|
key: makeSessionID(userId, sessionId),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessions && sessions.length > 0) {
|
if (sessionKeys && sessionKeys.length > 0) {
|
||||||
const client = await redis.getSessionClient()
|
const client = await redis.getSessionClient()
|
||||||
const promises = []
|
const promises = []
|
||||||
for (let session of sessions) {
|
for (let sessionKey of sessionKeys) {
|
||||||
promises.push(client.delete(session.key))
|
promises.push(client.delete(sessionKey.key))
|
||||||
}
|
}
|
||||||
if (!env.isTest()) {
|
if (!env.isTest()) {
|
||||||
logWarn(
|
logWarn(
|
||||||
`Invalidating sessions for ${userId} (reason: ${reason}) - ${sessions
|
`Invalidating sessions for ${userId} (reason: ${reason}) - ${sessionKeys
|
||||||
.map(session => session.key)
|
.map(sessionKey => sessionKey.key)
|
||||||
.join(", ")}`
|
.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
|
// invalidate all other sessions
|
||||||
await invalidateSessions(userId, { reason: "creation" })
|
await invalidateSessions(userId, { reason: "creation" })
|
||||||
|
|
||||||
const client = await redis.getSessionClient()
|
const client = await redis.getSessionClient()
|
||||||
const sessionId = session.sessionId
|
const sessionId = createSession.sessionId
|
||||||
if (!session.csrfToken) {
|
const csrfToken = createSession.csrfToken ? createSession.csrfToken : uuidv4()
|
||||||
session.csrfToken = uuidv4()
|
const key = makeSessionID(userId, sessionId)
|
||||||
}
|
|
||||||
session = {
|
const session: Session = {
|
||||||
...session,
|
...createSession,
|
||||||
|
csrfToken,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
lastAccessedAt: new Date().toISOString(),
|
lastAccessedAt: new Date().toISOString(),
|
||||||
userId,
|
userId,
|
||||||
}
|
}
|
||||||
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
|
await client.store(key, session, EXPIRY_SECONDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateSessionTTL(session: Session) {
|
export async function updateSessionTTL(session: Session) {
|
||||||
|
@ -106,7 +119,10 @@ export async function endSession(userId: string, sessionId: string) {
|
||||||
await client.delete(makeSessionID(userId, sessionId))
|
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) {
|
if (!userId || !sessionId) {
|
||||||
throw new Error(`Invalid session details - ${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
|
* Given an email address this will use a view to search through
|
||||||
* all the users to find one with this email address.
|
* all the users to find one with this email address.
|
||||||
* @param {string} email the email to lookup the user by.
|
* @param {string} email the email to lookup the user by.
|
||||||
* @return {Promise<object|null>}
|
|
||||||
*/
|
*/
|
||||||
exports.getGlobalUserByEmail = async email => {
|
exports.getGlobalUserByEmail = async email => {
|
||||||
if (email == null) {
|
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 "./analytics"
|
||||||
export * from "./user"
|
export * from "./user"
|
||||||
|
export * from "./errors"
|
||||||
|
|
|
@ -1,10 +1,31 @@
|
||||||
import { User } from "../../documents"
|
import { User } from "../../documents"
|
||||||
|
|
||||||
|
export interface CreateUserResponse {
|
||||||
|
_id: string
|
||||||
|
_rev: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface BulkCreateUsersRequest {
|
export interface BulkCreateUsersRequest {
|
||||||
users: User[]
|
users: User[]
|
||||||
groups: any[]
|
groups: any[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserDetails {
|
||||||
|
_id: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BulkCreateUsersResponse {
|
||||||
|
successful: UserDetails[]
|
||||||
|
unsuccessful: { email: string; reason: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface BulkDeleteUsersRequest {
|
export interface BulkDeleteUsersRequest {
|
||||||
userIds: string[]
|
userIds: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BulkDeleteUsersResponse {
|
||||||
|
successful: UserDetails[]
|
||||||
|
unsuccessful: { _id: string; email: string; reason: string }[]
|
||||||
|
}
|
||||||
|
|
|
@ -22,7 +22,6 @@ export interface UserRoles {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// utility types
|
// utility types
|
||||||
|
|
||||||
export interface BuilderUser extends User {
|
export interface BuilderUser extends User {
|
||||||
|
@ -34,8 +33,8 @@ export interface BuilderUser extends User {
|
||||||
export interface AdminUser extends User {
|
export interface AdminUser extends User {
|
||||||
admin: {
|
admin: {
|
||||||
global: boolean
|
global: boolean
|
||||||
},
|
}
|
||||||
builder: {
|
builder: {
|
||||||
global: boolean
|
global: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
import { Document } from "../document"
|
import { Document } from "../document"
|
||||||
import { User } from "./user"
|
|
||||||
export interface UserGroup extends Document {
|
export interface UserGroup extends Document {
|
||||||
name: string
|
name: string
|
||||||
icon: string
|
icon: string
|
||||||
color: string
|
color: string
|
||||||
users: groupUser[]
|
users: GroupUser[]
|
||||||
apps: string[]
|
apps: string[]
|
||||||
roles: UserGroupRoles
|
roles: UserGroupRoles
|
||||||
createdAt?: number
|
createdAt?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface groupUser {
|
export interface GroupUser {
|
||||||
_id: string
|
_id: string
|
||||||
email: string[]
|
email: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserGroupRoles {
|
export interface UserGroupRoles {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,3 +3,4 @@ export * from "./app"
|
||||||
export * from "./global"
|
export * from "./global"
|
||||||
export * from "./platform"
|
export * from "./platform"
|
||||||
export * from "./document"
|
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 "./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
|
* 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 "./migrations"
|
||||||
export * from "./datasources"
|
export * from "./datasources"
|
||||||
export * from "./search"
|
export * from "./search"
|
||||||
|
export * from "./auth"
|
||||||
|
|
|
@ -73,6 +73,7 @@
|
||||||
"@types/koa-router": "7.4.4",
|
"@types/koa-router": "7.4.4",
|
||||||
"@types/koa__router": "8.0.11",
|
"@types/koa__router": "8.0.11",
|
||||||
"@types/node": "14.18.20",
|
"@types/node": "14.18.20",
|
||||||
|
"@types/uuid": "8.3.4",
|
||||||
"@typescript-eslint/parser": "5.12.0",
|
"@typescript-eslint/parser": "5.12.0",
|
||||||
"copyfiles": "2.4.1",
|
"copyfiles": "2.4.1",
|
||||||
"eslint": "6.8.0",
|
"eslint": "6.8.0",
|
||||||
|
|
|
@ -14,3 +14,9 @@ const tk = require("timekeeper")
|
||||||
tk.freeze(mocks.date.MOCK_DATE)
|
tk.freeze(mocks.date.MOCK_DATE)
|
||||||
|
|
||||||
global.console.log = jest.fn() // console.log are ignored in tests
|
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 { sendEmail } from "../../../utilities/email"
|
||||||
import { users } from "../../../sdk"
|
import { users } from "../../../sdk"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
import { CloudAccount, User } from "@budibase/types"
|
import { BulkDeleteUsersRequest, CloudAccount, User } from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
accounts,
|
accounts,
|
||||||
cache,
|
cache,
|
||||||
|
@ -138,17 +138,15 @@ export const destroy = async (ctx: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bulkDelete = 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) {
|
if (userIds?.indexOf(ctx.user._id) !== -1) {
|
||||||
ctx.throw(400, "Unable to delete self.")
|
ctx.throw(400, "Unable to delete self.")
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let usersResponse = await users.bulkDelete(userIds)
|
let response = await users.bulkDelete(userIds)
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = response
|
||||||
message: `${usersResponse.length} user(s) deleted`,
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.throw(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 compress = require("koa-compress")
|
||||||
const zlib = require("zlib")
|
const zlib = require("zlib")
|
||||||
const { routes } = require("./routes")
|
import { routes } from "./routes"
|
||||||
const {
|
import {
|
||||||
buildAuthMiddleware,
|
buildAuthMiddleware,
|
||||||
auditLog,
|
auditLog,
|
||||||
buildTenancyMiddleware,
|
buildTenancyMiddleware,
|
||||||
buildCsrfMiddleware,
|
buildCsrfMiddleware,
|
||||||
} = require("@budibase/backend-core/auth")
|
} from "@budibase/backend-core/auth"
|
||||||
const { middleware: pro } = require("@budibase/pro")
|
import { middleware as pro } from "@budibase/pro"
|
||||||
const { errors } = require("@budibase/backend-core")
|
import { errors } from "@budibase/backend-core"
|
||||||
|
import { APIError } from "@budibase/types"
|
||||||
|
|
||||||
const PUBLIC_ENDPOINTS = [
|
const PUBLIC_ENDPOINTS = [
|
||||||
// old deprecated endpoints kept for backwards compat
|
// old deprecated endpoints kept for backwards compat
|
||||||
|
@ -120,15 +121,16 @@ router
|
||||||
router.use(async (ctx, next) => {
|
router.use(async (ctx, next) => {
|
||||||
try {
|
try {
|
||||||
await next()
|
await next()
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
ctx.log.error(err)
|
ctx.log.error(err)
|
||||||
ctx.status = err.status || err.statusCode || 500
|
ctx.status = err.status || err.statusCode || 500
|
||||||
const error = errors.getPublicError(err)
|
const error = errors.getPublicError(err)
|
||||||
ctx.body = {
|
const body: APIError = {
|
||||||
message: err.message,
|
message: err.message,
|
||||||
status: ctx.status,
|
status: ctx.status,
|
||||||
error,
|
error,
|
||||||
}
|
}
|
||||||
|
ctx.body = body
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
jest.mock("nodemailer")
|
jest.mock("nodemailer")
|
||||||
const { config, request, mocks, structures } = require("../../../tests")
|
import { TestConfiguration, mocks, API } from "../../../../tests"
|
||||||
const sendMailMock = mocks.email.mock()
|
const sendMailMock = mocks.email.mock()
|
||||||
const { events } = require("@budibase/backend-core")
|
import { events } from "@budibase/backend-core"
|
||||||
|
|
||||||
const TENANT_ID = structures.TENANT_ID
|
|
||||||
|
|
||||||
describe("/api/global/auth", () => {
|
describe("/api/global/auth", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
const api = new API(config)
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await config.beforeAll()
|
await config.beforeAll()
|
||||||
|
@ -19,56 +19,32 @@ describe("/api/global/auth", () => {
|
||||||
jest.clearAllMocks()
|
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 () => {
|
it("should logout", async () => {
|
||||||
await request
|
await api.auth.logout()
|
||||||
.post("/api/global/auth/logout")
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect(200)
|
|
||||||
expect(events.auth.logout).toBeCalledTimes(1)
|
expect(events.auth.logout).toBeCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be able to generate password reset email", async () => {
|
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")
|
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(sendMailMock).toHaveBeenCalled()
|
||||||
|
|
||||||
expect(code).toBeDefined()
|
expect(code).toBeDefined()
|
||||||
expect(events.user.passwordResetRequested).toBeCalledTimes(1)
|
expect(events.user.passwordResetRequested).toBeCalledTimes(1)
|
||||||
expect(events.user.passwordResetRequested).toBeCalledWith(user)
|
expect(events.user.passwordResetRequested).toBeCalledWith(user)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should allow resetting user password with code", async () => {
|
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")
|
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(res.body).toEqual({ message: "password reset successfully." })
|
||||||
expect(events.user.passwordReset).toBeCalledTimes(1)
|
expect(events.user.passwordReset).toBeCalledTimes(1)
|
||||||
expect(events.user.passwordReset).toBeCalledWith(user)
|
expect(events.user.passwordReset).toBeCalledWith(user)
|
||||||
|
@ -79,15 +55,15 @@ describe("/api/global/auth", () => {
|
||||||
|
|
||||||
const passportSpy = jest.spyOn(auth.passport, "authenticate")
|
const passportSpy = jest.spyOn(auth.passport, "authenticate")
|
||||||
let oidcConf
|
let oidcConf
|
||||||
let chosenConfig
|
let chosenConfig: any
|
||||||
let configId
|
let configId: string
|
||||||
|
|
||||||
// mock the oidc strategy implementation and return value
|
// mock the oidc strategy implementation and return value
|
||||||
let strategyFactory = jest.fn()
|
let strategyFactory = jest.fn()
|
||||||
let mockStrategyReturn = jest.fn()
|
let mockStrategyReturn = jest.fn()
|
||||||
let mockStrategyConfig = jest.fn()
|
let mockStrategyConfig = jest.fn()
|
||||||
auth.oidc.fetchStrategyConfig = mockStrategyConfig
|
auth.oidc.fetchStrategyConfig = mockStrategyConfig
|
||||||
|
|
||||||
strategyFactory.mockReturnValue(mockStrategyReturn)
|
strategyFactory.mockReturnValue(mockStrategyReturn)
|
||||||
auth.oidc.strategyFactory = strategyFactory
|
auth.oidc.strategyFactory = strategyFactory
|
||||||
|
|
||||||
|
@ -99,34 +75,34 @@ describe("/api/global/auth", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
expect(strategyFactory).toBeCalledWith(
|
expect(strategyFactory).toBeCalledWith(chosenConfig, expect.any(Function))
|
||||||
chosenConfig,
|
|
||||||
expect.any(Function)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("oidc configs", () => {
|
describe("oidc configs", () => {
|
||||||
it("should load strategy and delegate to passport", async () => {
|
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, {
|
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", () => {
|
describe("oidc callback", () => {
|
||||||
it("should load strategy and delegate to passport", async () => {
|
it("should load strategy and delegate to passport", async () => {
|
||||||
await request.get(`/api/global/auth/${TENANT_ID}/oidc/callback`)
|
await api.configs.OIDCCallback(configId)
|
||||||
.set(config.getOIDConfigCookie(configId))
|
|
||||||
|
expect(passportSpy).toBeCalledWith(
|
||||||
expect(passportSpy).toBeCalledWith(mockStrategyReturn, {
|
mockStrategyReturn,
|
||||||
successRedirect: "/", failureRedirect: "/error"
|
{
|
||||||
}, expect.anything())
|
successRedirect: "/",
|
||||||
expect(passportSpy.mock.calls.length).toBe(1);
|
failureRedirect: "/error",
|
||||||
|
},
|
||||||
|
expect.anything()
|
||||||
|
)
|
||||||
|
expect(passportSpy.mock.calls.length).toBe(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
|
@ -1,11 +1,12 @@
|
||||||
// mock the email system
|
// mock the email system
|
||||||
jest.mock("nodemailer")
|
jest.mock("nodemailer")
|
||||||
const { config, structures, mocks, request } = require("../../../tests")
|
import { TestConfiguration, structures, mocks, API } from "../../../../tests"
|
||||||
mocks.email.mock()
|
mocks.email.mock()
|
||||||
const { Configs } = require("@budibase/backend-core/constants")
|
import { Configs, events } from "@budibase/backend-core"
|
||||||
const { events } = require("@budibase/backend-core")
|
|
||||||
|
|
||||||
describe("configs", () => {
|
describe("configs", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
const api = new API(config)
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await config.beforeAll()
|
await config.beforeAll()
|
||||||
|
@ -20,35 +21,33 @@ describe("configs", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("post /api/global/configs", () => {
|
describe("post /api/global/configs", () => {
|
||||||
|
const saveConfig = async (conf: any, _id?: string, _rev?: string) => {
|
||||||
const saveConfig = async (conf, _id, _rev) => {
|
|
||||||
const data = {
|
const data = {
|
||||||
...conf,
|
...conf,
|
||||||
_id,
|
_id,
|
||||||
_rev
|
_rev,
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await request
|
const res = await api.configs.saveConfig(data)
|
||||||
.post(`/api/global/configs`)
|
|
||||||
.send(data)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
...res.body
|
...res.body,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("google", () => {
|
describe("google", () => {
|
||||||
const saveGoogleConfig = async (conf, _id, _rev) => {
|
const saveGoogleConfig = async (
|
||||||
|
conf?: any,
|
||||||
|
_id?: string,
|
||||||
|
_rev?: string
|
||||||
|
) => {
|
||||||
const googleConfig = structures.configs.google(conf)
|
const googleConfig = structures.configs.google(conf)
|
||||||
return saveConfig(googleConfig, _id, _rev)
|
return saveConfig(googleConfig, _id, _rev)
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
it ("should create activated google config", async () => {
|
it("should create activated google config", async () => {
|
||||||
await saveGoogleConfig()
|
await saveGoogleConfig()
|
||||||
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOCreated).toBeCalledWith(Configs.GOOGLE)
|
expect(events.auth.SSOCreated).toBeCalledWith(Configs.GOOGLE)
|
||||||
|
@ -58,7 +57,7 @@ describe("configs", () => {
|
||||||
await config.deleteConfig(Configs.GOOGLE)
|
await config.deleteConfig(Configs.GOOGLE)
|
||||||
})
|
})
|
||||||
|
|
||||||
it ("should create deactivated google config", async () => {
|
it("should create deactivated google config", async () => {
|
||||||
await saveGoogleConfig({ activated: false })
|
await saveGoogleConfig({ activated: false })
|
||||||
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOCreated).toBeCalledWith(Configs.GOOGLE)
|
expect(events.auth.SSOCreated).toBeCalledWith(Configs.GOOGLE)
|
||||||
|
@ -69,10 +68,14 @@ describe("configs", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
it ("should update google config to deactivated", async () => {
|
it("should update google config to deactivated", async () => {
|
||||||
const googleConf = await saveGoogleConfig()
|
const googleConf = await saveGoogleConfig()
|
||||||
jest.clearAllMocks()
|
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).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOUpdated).toBeCalledWith(Configs.GOOGLE)
|
expect(events.auth.SSOUpdated).toBeCalledWith(Configs.GOOGLE)
|
||||||
expect(events.auth.SSOActivated).not.toBeCalled()
|
expect(events.auth.SSOActivated).not.toBeCalled()
|
||||||
|
@ -81,10 +84,14 @@ describe("configs", () => {
|
||||||
await config.deleteConfig(Configs.GOOGLE)
|
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 })
|
const googleConf = await saveGoogleConfig({ activated: false })
|
||||||
jest.clearAllMocks()
|
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).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOUpdated).toBeCalledWith(Configs.GOOGLE)
|
expect(events.auth.SSOUpdated).toBeCalledWith(Configs.GOOGLE)
|
||||||
expect(events.auth.SSODeactivated).not.toBeCalled()
|
expect(events.auth.SSODeactivated).not.toBeCalled()
|
||||||
|
@ -92,17 +99,21 @@ describe("configs", () => {
|
||||||
expect(events.auth.SSOActivated).toBeCalledWith(Configs.GOOGLE)
|
expect(events.auth.SSOActivated).toBeCalledWith(Configs.GOOGLE)
|
||||||
await config.deleteConfig(Configs.GOOGLE)
|
await config.deleteConfig(Configs.GOOGLE)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("oidc", () => {
|
describe("oidc", () => {
|
||||||
const saveOIDCConfig = async (conf, _id, _rev) => {
|
const saveOIDCConfig = async (
|
||||||
|
conf?: any,
|
||||||
|
_id?: string,
|
||||||
|
_rev?: string
|
||||||
|
) => {
|
||||||
const oidcConfig = structures.configs.oidc(conf)
|
const oidcConfig = structures.configs.oidc(conf)
|
||||||
return saveConfig(oidcConfig, _id, _rev)
|
return saveConfig(oidcConfig, _id, _rev)
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
it ("should create activated OIDC config", async () => {
|
it("should create activated OIDC config", async () => {
|
||||||
await saveOIDCConfig()
|
await saveOIDCConfig()
|
||||||
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOCreated).toBeCalledWith(Configs.OIDC)
|
expect(events.auth.SSOCreated).toBeCalledWith(Configs.OIDC)
|
||||||
|
@ -112,7 +123,7 @@ describe("configs", () => {
|
||||||
await config.deleteConfig(Configs.OIDC)
|
await config.deleteConfig(Configs.OIDC)
|
||||||
})
|
})
|
||||||
|
|
||||||
it ("should create deactivated OIDC config", async () => {
|
it("should create deactivated OIDC config", async () => {
|
||||||
await saveOIDCConfig({ activated: false })
|
await saveOIDCConfig({ activated: false })
|
||||||
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
expect(events.auth.SSOCreated).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOCreated).toBeCalledWith(Configs.OIDC)
|
expect(events.auth.SSOCreated).toBeCalledWith(Configs.OIDC)
|
||||||
|
@ -123,10 +134,14 @@ describe("configs", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
it ("should update OIDC config to deactivated", async () => {
|
it("should update OIDC config to deactivated", async () => {
|
||||||
const oidcConf = await saveOIDCConfig()
|
const oidcConf = await saveOIDCConfig()
|
||||||
jest.clearAllMocks()
|
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).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOUpdated).toBeCalledWith(Configs.OIDC)
|
expect(events.auth.SSOUpdated).toBeCalledWith(Configs.OIDC)
|
||||||
expect(events.auth.SSOActivated).not.toBeCalled()
|
expect(events.auth.SSOActivated).not.toBeCalled()
|
||||||
|
@ -135,10 +150,14 @@ describe("configs", () => {
|
||||||
await config.deleteConfig(Configs.OIDC)
|
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 })
|
const oidcConf = await saveOIDCConfig({ activated: false })
|
||||||
jest.clearAllMocks()
|
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).toBeCalledTimes(1)
|
||||||
expect(events.auth.SSOUpdated).toBeCalledWith(Configs.OIDC)
|
expect(events.auth.SSOUpdated).toBeCalledWith(Configs.OIDC)
|
||||||
expect(events.auth.SSODeactivated).not.toBeCalled()
|
expect(events.auth.SSODeactivated).not.toBeCalled()
|
||||||
|
@ -147,17 +166,20 @@ describe("configs", () => {
|
||||||
await config.deleteConfig(Configs.OIDC)
|
await config.deleteConfig(Configs.OIDC)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("smtp", () => {
|
describe("smtp", () => {
|
||||||
const saveSMTPConfig = async (conf, _id, _rev) => {
|
const saveSMTPConfig = async (
|
||||||
|
conf?: any,
|
||||||
|
_id?: string,
|
||||||
|
_rev?: string
|
||||||
|
) => {
|
||||||
const smtpConfig = structures.configs.smtp(conf)
|
const smtpConfig = structures.configs.smtp(conf)
|
||||||
return saveConfig(smtpConfig, _id, _rev)
|
return saveConfig(smtpConfig, _id, _rev)
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
it ("should create SMTP config", async () => {
|
it("should create SMTP config", async () => {
|
||||||
await config.deleteConfig(Configs.SMTP)
|
await config.deleteConfig(Configs.SMTP)
|
||||||
await saveSMTPConfig()
|
await saveSMTPConfig()
|
||||||
expect(events.email.SMTPUpdated).not.toBeCalled()
|
expect(events.email.SMTPUpdated).not.toBeCalled()
|
||||||
|
@ -167,7 +189,7 @@ describe("configs", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
it ("should update SMTP config", async () => {
|
it("should update SMTP config", async () => {
|
||||||
const smtpConf = await saveSMTPConfig()
|
const smtpConf = await saveSMTPConfig()
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
await saveSMTPConfig(smtpConf.config, smtpConf._id, smtpConf._rev)
|
await saveSMTPConfig(smtpConf.config, smtpConf._id, smtpConf._rev)
|
||||||
|
@ -179,15 +201,19 @@ describe("configs", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("settings", () => {
|
describe("settings", () => {
|
||||||
const saveSettingsConfig = async (conf, _id, _rev) => {
|
const saveSettingsConfig = async (
|
||||||
|
conf?: any,
|
||||||
|
_id?: string,
|
||||||
|
_rev?: string
|
||||||
|
) => {
|
||||||
const settingsConfig = structures.configs.settings(conf)
|
const settingsConfig = structures.configs.settings(conf)
|
||||||
return saveConfig(settingsConfig, _id, _rev)
|
return saveConfig(settingsConfig, _id, _rev)
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("create", () => {
|
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 config.deleteConfig(Configs.SETTINGS)
|
||||||
|
|
||||||
await saveSettingsConfig()
|
await saveSettingsConfig()
|
||||||
|
|
||||||
expect(events.org.nameUpdated).not.toBeCalled()
|
expect(events.org.nameUpdated).not.toBeCalled()
|
||||||
|
@ -195,35 +221,43 @@ describe("configs", () => {
|
||||||
expect(events.org.platformURLUpdated).not.toBeCalled()
|
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)
|
await config.deleteConfig(Configs.SETTINGS)
|
||||||
const conf = {
|
const conf = {
|
||||||
company: "acme",
|
company: "acme",
|
||||||
logoUrl: "http://example.com",
|
logoUrl: "http://example.com",
|
||||||
platformUrl: "http://example.com"
|
platformUrl: "http://example.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
await saveSettingsConfig(conf)
|
await saveSettingsConfig(conf)
|
||||||
|
|
||||||
expect(events.org.nameUpdated).toBeCalledTimes(1)
|
expect(events.org.nameUpdated).toBeCalledTimes(1)
|
||||||
expect(events.org.logoUpdated).toBeCalledTimes(1)
|
expect(events.org.logoUpdated).toBeCalledTimes(1)
|
||||||
expect(events.org.platformURLUpdated).toBeCalledTimes(1)
|
expect(events.org.platformURLUpdated).toBeCalledTimes(1)
|
||||||
|
config.modeAccount()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
it ("should update settings config", async () => {
|
it("should update settings config", async () => {
|
||||||
|
config.modeSelf()
|
||||||
await config.deleteConfig(Configs.SETTINGS)
|
await config.deleteConfig(Configs.SETTINGS)
|
||||||
const settingsConfig = await saveSettingsConfig()
|
const settingsConfig = await saveSettingsConfig()
|
||||||
settingsConfig.config.company = "acme"
|
settingsConfig.config.company = "acme"
|
||||||
settingsConfig.config.logoUrl = "http://example.com"
|
settingsConfig.config.logoUrl = "http://example.com"
|
||||||
settingsConfig.config.platformUrl = "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.nameUpdated).toBeCalledTimes(1)
|
||||||
expect(events.org.logoUpdated).toBeCalledTimes(1)
|
expect(events.org.logoUpdated).toBeCalledTimes(1)
|
||||||
expect(events.org.platformURLUpdated).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 () => {
|
it("should return the correct checklist status based on the state of the budibase installation", async () => {
|
||||||
await config.saveSmtpConfig()
|
await config.saveSmtpConfig()
|
||||||
|
|
||||||
const res = await request
|
const res = await api.configs.getConfigChecklist()
|
||||||
.get(`/api/global/configs/checklist`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
const checklist = res.body
|
const checklist = res.body
|
||||||
|
|
||||||
expect(checklist.apps.checked).toBeFalsy()
|
expect(checklist.apps.checked).toBeFalsy()
|
|
@ -1,12 +1,11 @@
|
||||||
jest.mock("nodemailer")
|
jest.mock("nodemailer")
|
||||||
const { config, mocks, structures, request } = require("../../../tests")
|
import { TestConfiguration, mocks, API } from "../../../../tests"
|
||||||
const sendMailMock = mocks.email.mock()
|
const sendMailMock = mocks.email.mock()
|
||||||
|
import { EmailTemplatePurpose } from "../../../../constants"
|
||||||
const { EmailTemplatePurpose } = require("../../../constants")
|
|
||||||
|
|
||||||
const TENANT_ID = structures.TENANT_ID
|
|
||||||
|
|
||||||
describe("/api/global/email", () => {
|
describe("/api/global/email", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
const api = new API(config)
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await config.beforeAll()
|
await config.beforeAll()
|
||||||
|
@ -20,16 +19,9 @@ describe("/api/global/email", () => {
|
||||||
// initially configure settings
|
// initially configure settings
|
||||||
await config.saveSmtpConfig()
|
await config.saveSmtpConfig()
|
||||||
await config.saveSettingsConfig()
|
await config.saveSettingsConfig()
|
||||||
const res = await request
|
|
||||||
.post(`/api/global/email/send`)
|
const res = await api.emails.sendEmail(EmailTemplatePurpose.INVITATION)
|
||||||
.send({
|
|
||||||
email: "test@test.com",
|
|
||||||
purpose: EmailTemplatePurpose.INVITATION,
|
|
||||||
tenantId: TENANT_ID,
|
|
||||||
})
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(res.body.message).toBeDefined()
|
expect(res.body.message).toBeDefined()
|
||||||
expect(sendMailMock).toHaveBeenCalled()
|
expect(sendMailMock).toHaveBeenCalled()
|
||||||
const emailCall = sendMailMock.mock.calls[0][0]
|
const emailCall = sendMailMock.mock.calls[0][0]
|
|
@ -1,5 +1,5 @@
|
||||||
const { config, request } = require("../../../tests")
|
import { TestConfiguration, API } from "../../../../tests"
|
||||||
const { EmailTemplatePurpose } = require("../../../constants")
|
import { EmailTemplatePurpose } from "../../../../constants"
|
||||||
const nodemailer = require("nodemailer")
|
const nodemailer = require("nodemailer")
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ const fetch = require("node-fetch")
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
describe("/api/global/email", () => {
|
describe("/api/global/email", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
const api = new API(config)
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await config.beforeAll()
|
await config.beforeAll()
|
||||||
|
@ -16,27 +18,24 @@ describe("/api/global/email", () => {
|
||||||
await config.afterAll()
|
await config.afterAll()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function sendRealEmail(purpose) {
|
async function sendRealEmail(purpose: string) {
|
||||||
let response, text
|
let response, text
|
||||||
try {
|
try {
|
||||||
const timeout = () => new Promise((resolve, reject) =>
|
const timeout = () =>
|
||||||
setTimeout(() => reject({
|
new Promise((resolve, reject) =>
|
||||||
status: 301,
|
setTimeout(
|
||||||
errno: "ETIME"
|
() =>
|
||||||
}), 20000)
|
reject({
|
||||||
)
|
status: 301,
|
||||||
|
errno: "ETIME",
|
||||||
|
}),
|
||||||
|
20000
|
||||||
|
)
|
||||||
|
)
|
||||||
await Promise.race([config.saveEtherealSmtpConfig(), timeout()])
|
await Promise.race([config.saveEtherealSmtpConfig(), timeout()])
|
||||||
await Promise.race([config.saveSettingsConfig(), timeout()])
|
await Promise.race([config.saveSettingsConfig(), timeout()])
|
||||||
const user = await config.getUser("test@test.com")
|
|
||||||
const res = await request
|
const res = await api.emails.sendEmail(purpose).timeout(20000)
|
||||||
.post(`/api/global/email/send`)
|
|
||||||
.send({
|
|
||||||
email: "test@test.com",
|
|
||||||
purpose,
|
|
||||||
userId: user._id,
|
|
||||||
})
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.timeout(20000)
|
|
||||||
// ethereal hiccup, can't test right now
|
// ethereal hiccup, can't test right now
|
||||||
if (res.status >= 300) {
|
if (res.status >= 300) {
|
||||||
return
|
return
|
||||||
|
@ -47,7 +46,7 @@ describe("/api/global/email", () => {
|
||||||
expect(testUrl).toBeDefined()
|
expect(testUrl).toBeDefined()
|
||||||
response = await fetch(testUrl)
|
response = await fetch(testUrl)
|
||||||
text = await response.text()
|
text = await response.text()
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
// ethereal hiccup, can't test right now
|
// ethereal hiccup, can't test right now
|
||||||
if (parseInt(err.status) >= 300 || (err && err.errno === "ETIME")) {
|
if (parseInt(err.status) >= 300 || (err && err.errno === "ETIME")) {
|
||||||
return
|
return
|
||||||
|
@ -81,4 +80,4 @@ describe("/api/global/email", () => {
|
||||||
it("should be able to send a password recovery email", async () => {
|
it("should be able to send a password recovery email", async () => {
|
||||||
await sendRealEmail(EmailTemplatePurpose.PASSWORD_RECOVERY)
|
await sendRealEmail(EmailTemplatePurpose.PASSWORD_RECOVERY)
|
||||||
})
|
})
|
||||||
})
|
})
|
|
@ -1,8 +1,10 @@
|
||||||
jest.mock("nodemailer")
|
jest.mock("nodemailer")
|
||||||
const { config, request } = require("../../../tests")
|
import { TestConfiguration, API } from "../../../../tests"
|
||||||
const { events } = require("@budibase/backend-core")
|
import { events } from "@budibase/backend-core"
|
||||||
|
|
||||||
describe("/api/global/self", () => {
|
describe("/api/global/self", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
const api = new API(config)
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await config.beforeAll()
|
await config.beforeAll()
|
||||||
|
@ -16,23 +18,13 @@ describe("/api/global/self", () => {
|
||||||
jest.clearAllMocks()
|
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", () => {
|
describe("update", () => {
|
||||||
|
|
||||||
it("should update self", async () => {
|
it("should update self", async () => {
|
||||||
const user = await config.createUser()
|
const user = await config.createUser()
|
||||||
|
await config.createSession(user)
|
||||||
|
|
||||||
delete user.password
|
delete user.password
|
||||||
const res = await updateSelf(user)
|
const res = await api.self.updateSelf(user)
|
||||||
|
|
||||||
expect(res.body._id).toBe(user._id)
|
expect(res.body._id).toBe(user._id)
|
||||||
expect(events.user.updated).toBeCalledTimes(1)
|
expect(events.user.updated).toBeCalledTimes(1)
|
||||||
|
@ -42,10 +34,10 @@ describe("/api/global/self", () => {
|
||||||
|
|
||||||
it("should update password", async () => {
|
it("should update password", async () => {
|
||||||
const user = await config.createUser()
|
const user = await config.createUser()
|
||||||
const password = "newPassword"
|
await config.createSession(user)
|
||||||
user.password = password
|
|
||||||
|
|
||||||
const res = await updateSelf(user)
|
user.password = "newPassword"
|
||||||
|
const res = await api.self.updateSelf(user)
|
||||||
|
|
||||||
delete user.password
|
delete user.password
|
||||||
expect(res.body._id).toBe(user._id)
|
expect(res.body._id).toBe(user._id)
|
||||||
|
@ -55,4 +47,4 @@ describe("/api/global/self", () => {
|
||||||
expect(events.user.passwordUpdated).toBeCalledWith(user)
|
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 selfRoutes = require("./global/self")
|
||||||
const licenseRoutes = require("./global/license")
|
const licenseRoutes = require("./global/license")
|
||||||
const migrationRoutes = require("./system/migrations")
|
const migrationRoutes = require("./system/migrations")
|
||||||
|
const accountRoutes = require("./system/accounts")
|
||||||
|
|
||||||
let userGroupRoutes = api.groups
|
let userGroupRoutes = api.groups
|
||||||
exports.routes = [
|
exports.routes = [
|
||||||
|
@ -29,4 +30,5 @@ exports.routes = [
|
||||||
licenseRoutes,
|
licenseRoutes,
|
||||||
userGroupRoutes,
|
userGroupRoutes,
|
||||||
migrationRoutes,
|
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
|
LOADED = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseIntSafe(number) {
|
function parseIntSafe(number: any) {
|
||||||
if (number) {
|
if (number) {
|
||||||
return parseInt(number)
|
return parseInt(number)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
const env = {
|
||||||
// auth
|
// auth
|
||||||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
||||||
|
@ -47,7 +47,7 @@ module.exports = {
|
||||||
CLUSTER_PORT: process.env.CLUSTER_PORT,
|
CLUSTER_PORT: process.env.CLUSTER_PORT,
|
||||||
// flags
|
// flags
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
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,
|
LOG_LEVEL: process.env.LOG_LEVEL,
|
||||||
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
||||||
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
||||||
|
@ -62,7 +62,7 @@ module.exports = {
|
||||||
// other
|
// other
|
||||||
CHECKLIST_CACHE_TTL: parseIntSafe(process.env.CHECKLIST_CACHE_TTL) || 3600,
|
CHECKLIST_CACHE_TTL: parseIntSafe(process.env.CHECKLIST_CACHE_TTL) || 3600,
|
||||||
SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD,
|
SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD,
|
||||||
_set(key, value) {
|
_set(key: any, value: any) {
|
||||||
process.env[key] = value
|
process.env[key] = value
|
||||||
module.exports[key] = value
|
module.exports[key] = value
|
||||||
},
|
},
|
||||||
|
@ -74,16 +74,17 @@ module.exports = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if some var haven't been set, define them
|
// if some var haven't been set, define them
|
||||||
if (!module.exports.APPS_URL) {
|
if (!env.APPS_URL) {
|
||||||
module.exports.APPS_URL = isDev()
|
env.APPS_URL = isDev() ? "http://localhost:4001" : "http://app-service:4002"
|
||||||
? "http://localhost:4001"
|
|
||||||
: "http://app-service:4002"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean up any environment variable edge cases
|
// clean up any environment variable edge cases
|
||||||
for (let [key, value] of Object.entries(module.exports)) {
|
for (let [key, value] of Object.entries(module.exports)) {
|
||||||
// handle the edge case of "0" to disable an environment variable
|
// handle the edge case of "0" to disable an environment variable
|
||||||
if (value === "0") {
|
if (value === "0") {
|
||||||
module.exports[key] = 0
|
// @ts-ignore
|
||||||
|
env[key] = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export = env
|
|
@ -78,7 +78,7 @@ const shutdown = () => {
|
||||||
server.destroy()
|
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())}`)
|
console.log(`Worker running on ${JSON.stringify(server.address())}`)
|
||||||
await redis.init()
|
await redis.init()
|
||||||
})
|
})
|
||||||
|
@ -92,3 +92,7 @@ process.on("uncaughtException", err => {
|
||||||
process.on("SIGTERM", () => {
|
process.on("SIGTERM", () => {
|
||||||
shutdown()
|
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 users from "./users"
|
||||||
|
export * as accounts from "./accounts"
|
||||||
|
|
|
@ -15,9 +15,21 @@ import {
|
||||||
accounts,
|
accounts,
|
||||||
migrations,
|
migrations,
|
||||||
StaticDatabases,
|
StaticDatabases,
|
||||||
ViewName
|
ViewName,
|
||||||
} from "@budibase/backend-core"
|
} 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"
|
import { groups as groupUtils } from "@budibase/pro"
|
||||||
|
|
||||||
const PAGE_LIMIT = 8
|
const PAGE_LIMIT = 8
|
||||||
|
@ -100,7 +112,6 @@ export const getUser = async (userId: string) => {
|
||||||
interface SaveUserOpts {
|
interface SaveUserOpts {
|
||||||
hashPassword?: boolean
|
hashPassword?: boolean
|
||||||
requirePassword?: boolean
|
requirePassword?: boolean
|
||||||
bulkCreate?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildUser = async (
|
const buildUser = async (
|
||||||
|
@ -111,7 +122,7 @@ const buildUser = async (
|
||||||
},
|
},
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
dbUser?: any
|
dbUser?: any
|
||||||
) => {
|
): Promise<User> => {
|
||||||
let { password, _id } = user
|
let { password, _id } = user
|
||||||
|
|
||||||
let hashedPassword
|
let hashedPassword
|
||||||
|
@ -145,62 +156,63 @@ const buildUser = async (
|
||||||
return user
|
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 (
|
export const save = async (
|
||||||
user: any,
|
user: User,
|
||||||
opts: SaveUserOpts = {
|
opts: SaveUserOpts = {
|
||||||
hashPassword: true,
|
hashPassword: true,
|
||||||
requirePassword: true,
|
requirePassword: true,
|
||||||
bulkCreate: false,
|
|
||||||
}
|
}
|
||||||
) => {
|
): Promise<CreateUserResponse> => {
|
||||||
const tenantId = tenancy.getTenantId()
|
const tenantId = tenancy.getTenantId()
|
||||||
const db = tenancy.getGlobalDB()
|
const db = tenancy.getGlobalDB()
|
||||||
let { email, _id } = user
|
let { email, _id } = user
|
||||||
// make sure another user isn't using the same email
|
|
||||||
let dbUser: any
|
let dbUser: User | undefined
|
||||||
if (opts.bulkCreate) {
|
if (_id) {
|
||||||
dbUser = null
|
// 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) {
|
} else if (email) {
|
||||||
// check budibase users inside the tenant
|
// no id was specified - load from email instead
|
||||||
dbUser = await usersCore.getGlobalUserByEmail(email)
|
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.`
|
throw `Email address ${email} already in use.`
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// check budibase users in other tenants
|
throw new Error("_id or email is required")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await validateUniqueUser(email, tenantId)
|
||||||
|
|
||||||
let builtUser = await buildUser(user, opts, tenantId, dbUser)
|
let builtUser = await buildUser(user, opts, tenantId, dbUser)
|
||||||
|
|
||||||
// make sure we set the _id field for a new user
|
// make sure we set the _id field for a new user
|
||||||
if (!_id) {
|
if (!_id) {
|
||||||
_id = builtUser._id
|
_id = builtUser._id!
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const putOpts = {
|
|
||||||
password: builtUser.password,
|
|
||||||
...user,
|
|
||||||
}
|
|
||||||
if (opts.bulkCreate) {
|
|
||||||
return putOpts
|
|
||||||
}
|
|
||||||
// save the user to db
|
// save the user to db
|
||||||
let response
|
let response
|
||||||
const putUserFn = () => {
|
const putUserFn = () => {
|
||||||
|
@ -253,25 +265,32 @@ const getExistingTenantUsers = async (emails: string[]): Promise<User[]> => {
|
||||||
return dbUtils.queryGlobalView(ViewName.USER_BY_EMAIL, {
|
return dbUtils.queryGlobalView(ViewName.USER_BY_EMAIL, {
|
||||||
keys: emails,
|
keys: emails,
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
arrayResponse: true
|
arrayResponse: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getExistingPlatformUsers = async (emails: string[]): Promise<PlatformUserByEmail[]> => {
|
const getExistingPlatformUsers = async (
|
||||||
return dbUtils.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (infoDb: any) => {
|
emails: string[]
|
||||||
const response = await infoDb.allDocs({
|
): Promise<PlatformUserByEmail[]> => {
|
||||||
keys: emails,
|
return dbUtils.doWithDB(
|
||||||
include_docs: true,
|
StaticDatabases.PLATFORM_INFO.name,
|
||||||
})
|
async (infoDb: any) => {
|
||||||
return response.rows.map((row: any) => row.doc)
|
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[]> => {
|
const getExistingAccounts = async (emails: string[]): Promise<Account[]> => {
|
||||||
return dbUtils.queryPlatformView(ViewName.ACCOUNT_BY_EMAIL, {
|
return dbUtils.queryPlatformView(ViewName.ACCOUNT_BY_EMAIL, {
|
||||||
keys: emails,
|
keys: emails,
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
arrayResponse: true
|
arrayResponse: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,18 +308,22 @@ const searchExistingEmails = async (emails: string[]) => {
|
||||||
matchedEmails.push(...existingTenantUsers.map((user: User) => user.email))
|
matchedEmails.push(...existingTenantUsers.map((user: User) => user.email))
|
||||||
|
|
||||||
const existingPlatformUsers = await getExistingPlatformUsers(emails)
|
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)
|
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 (
|
export const bulkCreate = async (
|
||||||
newUsersRequested: User[],
|
newUsersRequested: User[],
|
||||||
groups: string[]
|
groups: string[]
|
||||||
) => {
|
): Promise<BulkCreateUsersResponse> => {
|
||||||
const db = tenancy.getGlobalDB()
|
const db = tenancy.getGlobalDB()
|
||||||
const tenantId = tenancy.getTenantId()
|
const tenantId = tenancy.getTenantId()
|
||||||
|
|
||||||
|
@ -309,14 +332,17 @@ export const bulkCreate = async (
|
||||||
|
|
||||||
const emails = newUsersRequested.map((user: User) => user.email)
|
const emails = newUsersRequested.map((user: User) => user.email)
|
||||||
const existingEmails = await searchExistingEmails(emails)
|
const existingEmails = await searchExistingEmails(emails)
|
||||||
const unsuccessful: { email: string, reason: string }[] = []
|
const unsuccessful: { email: string; reason: string }[] = []
|
||||||
|
|
||||||
for (const newUser of newUsersRequested) {
|
for (const newUser of newUsersRequested) {
|
||||||
if (
|
if (
|
||||||
newUsers.find((x: any) => x.email === newUser.email) ||
|
newUsers.find((x: any) => x.email === newUser.email) ||
|
||||||
existingEmails.includes(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
|
continue
|
||||||
}
|
}
|
||||||
newUser.userGroups = groups
|
newUser.userGroups = groups
|
||||||
|
@ -363,59 +389,121 @@ export const bulkCreate = async (
|
||||||
|
|
||||||
return {
|
return {
|
||||||
successful: saved,
|
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 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 groupsToModify: any = {}
|
||||||
let builderCount = 0
|
let builderCount = 0
|
||||||
|
|
||||||
// Get users and delete
|
// Get users and delete
|
||||||
let usersToDelete = (
|
const allDocsResponse: AllDocsResponse<User> = await db.allDocs({
|
||||||
await db.allDocs({
|
include_docs: true,
|
||||||
include_docs: true,
|
keys: userIds,
|
||||||
keys: userIds,
|
})
|
||||||
})
|
const usersToDelete: User[] = allDocsResponse.rows.map(
|
||||||
).rows.map((user: any) => {
|
(user: RowResponse<User>) => {
|
||||||
// if we find a user that has an associated group, add it to
|
// if we find a user that has an associated group, add it to
|
||||||
// an array so we can easily use allDocs on them later.
|
// an array so we can easily use allDocs on them later.
|
||||||
// This prevents us having to re-loop over all the users
|
// This prevents us having to re-loop over all the users
|
||||||
if (user.doc.userGroups) {
|
if (user.doc.userGroups) {
|
||||||
for (let groupId of user.doc.userGroups) {
|
for (let groupId of user.doc.userGroups) {
|
||||||
if (!Object.keys(groupsToModify).includes(groupId)) {
|
if (!Object.keys(groupsToModify).includes(groupId)) {
|
||||||
groupsToModify[groupId] = [user.id]
|
groupsToModify[groupId] = [user.id]
|
||||||
} else {
|
} else {
|
||||||
groupsToModify[groupId] = [...groupsToModify[groupId], user.id]
|
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
|
// Delete from DB
|
||||||
if (eventHelpers.isAddingBuilder(user.doc, null)) {
|
const dbResponse: BulkDocsResponse = await db.bulkDocs(
|
||||||
builderCount++
|
usersToDelete.map(user => ({
|
||||||
}
|
|
||||||
|
|
||||||
return user.doc
|
|
||||||
})
|
|
||||||
|
|
||||||
const response = await db.bulkDocs(
|
|
||||||
usersToDelete.map((user: any) => ({
|
|
||||||
...user,
|
...user,
|
||||||
_deleted: true,
|
_deleted: true,
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Deletion post processing
|
||||||
await groupUtils.bulkDeleteGroupUsers(groupsToModify)
|
await groupUtils.bulkDeleteGroupUsers(groupsToModify)
|
||||||
|
|
||||||
//Deletion post processing
|
|
||||||
for (let user of usersToDelete) {
|
for (let user of usersToDelete) {
|
||||||
await bulkDeleteProcessing(user)
|
await bulkDeleteProcessing(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
await quotas.removeDevelopers(builderCount)
|
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
|
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 TestConfiguration from "./TestConfiguration"
|
||||||
import structures from "./structures"
|
import structures from "./structures"
|
||||||
import mocks from "./mocks"
|
import mocks from "./mocks"
|
||||||
|
import API from "./api"
|
||||||
|
|
||||||
const config = new TestConfiguration()
|
const pkg = {
|
||||||
const request = config.getRequest()
|
|
||||||
|
|
||||||
const pkg = {
|
|
||||||
structures,
|
structures,
|
||||||
|
TENANT_1: structures.TENANT_1,
|
||||||
mocks,
|
mocks,
|
||||||
config,
|
TestConfiguration,
|
||||||
request,
|
API,
|
||||||
}
|
}
|
||||||
|
|
||||||
export = pkg
|
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 configs from "./configs"
|
||||||
import * as users from "./users"
|
import * as users from "./users"
|
||||||
import * as groups from "./groups"
|
import * as groups from "./groups"
|
||||||
|
import * as accounts from "./accounts"
|
||||||
|
|
||||||
const TENANT_ID = "default"
|
const TENANT_ID = "default"
|
||||||
|
const TENANT_1 = "tenant1"
|
||||||
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
|
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
|
||||||
|
|
||||||
export = {
|
const pkg = {
|
||||||
configs,
|
configs,
|
||||||
users,
|
users,
|
||||||
|
accounts,
|
||||||
TENANT_ID,
|
TENANT_ID,
|
||||||
|
TENANT_1,
|
||||||
CSRF_TOKEN,
|
CSRF_TOKEN,
|
||||||
groups,
|
groups,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export = pkg
|
||||||
|
|
|
@ -1,28 +1,32 @@
|
||||||
export const email = "test@test.com"
|
export const email = "test@test.com"
|
||||||
import { AdminUser, BuilderUser, User } from "@budibase/types"
|
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 {
|
return {
|
||||||
email: "test@test.com",
|
email: newEmail(),
|
||||||
password: "test",
|
password: "test",
|
||||||
roles: {},
|
roles: {},
|
||||||
...userProps,
|
...userProps,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const adminUser = (userProps: any): AdminUser => {
|
export const adminUser = (userProps?: any): AdminUser => {
|
||||||
return {
|
return {
|
||||||
...user(userProps),
|
...user(userProps),
|
||||||
admin: {
|
admin: {
|
||||||
global: true,
|
global: true,
|
||||||
},
|
},
|
||||||
builder: {
|
builder: {
|
||||||
global: true
|
global: true,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const builderUser = (userProps: any): BuilderUser => {
|
export const builderUser = (userProps?: any): BuilderUser => {
|
||||||
return {
|
return {
|
||||||
...user(userProps),
|
...user(userProps),
|
||||||
builder: {
|
builder: {
|
||||||
|
|
|
@ -55,6 +55,7 @@ exports.init = async () => {
|
||||||
exports.shutdown = async () => {
|
exports.shutdown = async () => {
|
||||||
if (pwResetClient) await pwResetClient.finish()
|
if (pwResetClient) await pwResetClient.finish()
|
||||||
if (invitationClient) await invitationClient.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"
|
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
|
||||||
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
|
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@*":
|
"@types/yargs-parser@*":
|
||||||
version "21.0.0"
|
version "21.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
|
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
|
||||||
|
|
Loading…
Reference in New Issue