Merge pull request #7478 from Budibase/user-fixes
User fixes and updates
This commit is contained in:
commit
faf132a82a
78 changed files with 2158 additions and 1160 deletions
|
@ -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,
|
|
@ -18,6 +18,7 @@ export enum ViewName {
|
||||||
LINK = "by_link",
|
LINK = "by_link",
|
||||||
ROUTING = "screen_routes",
|
ROUTING = "screen_routes",
|
||||||
AUTOMATION_LOGS = "automation_logs",
|
AUTOMATION_LOGS = "automation_logs",
|
||||||
|
ACCOUNT_BY_EMAIL = "account_by_email",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeprecatedViews = {
|
export const DeprecatedViews = {
|
||||||
|
@ -41,6 +42,7 @@ export enum DocumentType {
|
||||||
MIGRATIONS = "migrations",
|
MIGRATIONS = "migrations",
|
||||||
DEV_INFO = "devinfo",
|
DEV_INFO = "devinfo",
|
||||||
AUTOMATION_LOG = "log_au",
|
AUTOMATION_LOG = "log_au",
|
||||||
|
ACCOUNT_METADATA = "acc_metadata",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StaticDatabases = {
|
export const StaticDatabases = {
|
||||||
|
|
|
@ -5,6 +5,8 @@ const {
|
||||||
SEPARATOR,
|
SEPARATOR,
|
||||||
} = require("./utils")
|
} = require("./utils")
|
||||||
const { getGlobalDB } = require("../tenancy")
|
const { getGlobalDB } = require("../tenancy")
|
||||||
|
const { StaticDatabases } = require("./constants")
|
||||||
|
const { doWithDB } = require("./")
|
||||||
|
|
||||||
const DESIGN_DB = "_design/database"
|
const DESIGN_DB = "_design/database"
|
||||||
|
|
||||||
|
@ -56,6 +58,31 @@ exports.createNewUserEmailView = async () => {
|
||||||
await db.put(designDoc)
|
await db.put(designDoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.createAccountEmailView = async () => {
|
||||||
|
await doWithDB(StaticDatabases.PLATFORM_INFO.name, async db => {
|
||||||
|
let designDoc
|
||||||
|
try {
|
||||||
|
designDoc = await db.get(DESIGN_DB)
|
||||||
|
} catch (err) {
|
||||||
|
// no design doc, make one
|
||||||
|
designDoc = DesignDoc()
|
||||||
|
}
|
||||||
|
const view = {
|
||||||
|
// if using variables in a map function need to inject them before use
|
||||||
|
map: `function(doc) {
|
||||||
|
if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) {
|
||||||
|
emit(doc.email.toLowerCase(), doc._id)
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
}
|
||||||
|
designDoc.views = {
|
||||||
|
...designDoc.views,
|
||||||
|
[ViewName.ACCOUNT_BY_EMAIL]: view,
|
||||||
|
}
|
||||||
|
await db.put(designDoc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
exports.createUserAppView = async () => {
|
exports.createUserAppView = async () => {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
let designDoc
|
let designDoc
|
||||||
|
@ -128,6 +155,39 @@ exports.createUserBuildersView = async () => {
|
||||||
await db.put(designDoc)
|
await db.put(designDoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.queryView = async (viewName, params, db, CreateFuncByName) => {
|
||||||
|
try {
|
||||||
|
let response = (await db.query(`database/${viewName}`, params)).rows
|
||||||
|
response = response.map(resp =>
|
||||||
|
params.include_docs ? resp.doc : resp.value
|
||||||
|
)
|
||||||
|
if (params.arrayResponse) {
|
||||||
|
return response
|
||||||
|
} else {
|
||||||
|
return response.length <= 1 ? response[0] : response
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err != null && err.name === "not_found") {
|
||||||
|
const createFunc = CreateFuncByName[viewName]
|
||||||
|
await removeDeprecated(db, viewName)
|
||||||
|
await createFunc()
|
||||||
|
return exports.queryView(viewName, params, db, CreateFuncByName)
|
||||||
|
} else {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.queryPlatformView = async (viewName, params) => {
|
||||||
|
const CreateFuncByName = {
|
||||||
|
[ViewName.ACCOUNT_BY_EMAIL]: exports.createAccountEmailView,
|
||||||
|
}
|
||||||
|
|
||||||
|
return doWithDB(StaticDatabases.PLATFORM_INFO.name, async db => {
|
||||||
|
return exports.queryView(viewName, params, db, CreateFuncByName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
exports.queryGlobalView = async (viewName, params, db = null) => {
|
exports.queryGlobalView = async (viewName, params, db = null) => {
|
||||||
const CreateFuncByName = {
|
const CreateFuncByName = {
|
||||||
[ViewName.USER_BY_EMAIL]: exports.createNewUserEmailView,
|
[ViewName.USER_BY_EMAIL]: exports.createNewUserEmailView,
|
||||||
|
@ -139,20 +199,5 @@ exports.queryGlobalView = async (viewName, params, db = null) => {
|
||||||
if (!db) {
|
if (!db) {
|
||||||
db = getGlobalDB()
|
db = getGlobalDB()
|
||||||
}
|
}
|
||||||
try {
|
return exports.queryView(viewName, params, db, CreateFuncByName)
|
||||||
let response = (await db.query(`database/${viewName}`, params)).rows
|
|
||||||
response = response.map(resp =>
|
|
||||||
params.include_docs ? resp.doc : resp.value
|
|
||||||
)
|
|
||||||
return response.length <= 1 ? response[0] : response
|
|
||||||
} catch (err) {
|
|
||||||
if (err != null && err.name === "not_found") {
|
|
||||||
const createFunc = CreateFuncByName[viewName]
|
|
||||||
await removeDeprecated(db, viewName)
|
|
||||||
await createFunc()
|
|
||||||
return exports.queryGlobalView(viewName, params)
|
|
||||||
} else {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -13,10 +13,13 @@ function validate(schema, property) {
|
||||||
params = ctx.request[property]
|
params = ctx.request[property]
|
||||||
}
|
}
|
||||||
|
|
||||||
schema = schema.append({
|
// not all schemas have the append property e.g. array schemas
|
||||||
createdAt: Joi.any().optional(),
|
if (schema.append) {
|
||||||
updatedAt: Joi.any().optional(),
|
schema = schema.append({
|
||||||
})
|
createdAt: Joi.any().optional(),
|
||||||
|
updatedAt: Joi.any().optional(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const { error } = schema.validate(params)
|
const { error } = schema.validate(params)
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
7
packages/backend-core/tests/utilities/mocks/accounts.ts
Normal file
7
packages/backend-core/tests/utilities/mocks/accounts.ts
Normal file
|
@ -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
|
|
2
packages/backend-core/tests/utilities/mocks/date.ts
Normal file
2
packages/backend-core/tests/utilities/mocks/date.ts
Normal file
|
@ -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,
|
|
||||||
}
|
|
4
packages/backend-core/tests/utilities/mocks/index.ts
Normal file
4
packages/backend-core/tests/utilities/mocks/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import "./posthog"
|
||||||
|
import "./events"
|
||||||
|
export * as accounts from "./accounts"
|
||||||
|
export * as date from "./date"
|
|
@ -0,0 +1,73 @@
|
||||||
|
<script>
|
||||||
|
import { Body, ModalContent, Table } from "@budibase/bbui"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
export let userData
|
||||||
|
export let deleteUsersResponse
|
||||||
|
|
||||||
|
let successCount
|
||||||
|
let failureCount
|
||||||
|
let title
|
||||||
|
let unsuccessfulUsers
|
||||||
|
let message
|
||||||
|
|
||||||
|
const setTitle = () => {
|
||||||
|
if (successCount) {
|
||||||
|
title = `${successCount} users deleted`
|
||||||
|
} else {
|
||||||
|
title = "Oops!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setMessage = () => {
|
||||||
|
if (successCount) {
|
||||||
|
message = "However there was a problem deleting some users."
|
||||||
|
} else {
|
||||||
|
message = "There was a problem deleting some users."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setUsers = () => {
|
||||||
|
unsuccessfulUsers = deleteUsersResponse.unsuccessful.map(user => {
|
||||||
|
return {
|
||||||
|
email: user.email,
|
||||||
|
reason: user.reason,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
successCount = deleteUsersResponse.successful.length
|
||||||
|
failureCount = deleteUsersResponse.unsuccessful.length
|
||||||
|
setTitle()
|
||||||
|
setMessage()
|
||||||
|
setUsers()
|
||||||
|
})
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
email: {},
|
||||||
|
reason: {},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
size="M"
|
||||||
|
{title}
|
||||||
|
confirmText="Close"
|
||||||
|
showCloseIcon={false}
|
||||||
|
showCancelButton={false}
|
||||||
|
>
|
||||||
|
<Body size="XS">
|
||||||
|
{message}
|
||||||
|
</Body>
|
||||||
|
<Table
|
||||||
|
{schema}
|
||||||
|
data={unsuccessfulUsers}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={false}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
|
@ -2,24 +2,78 @@
|
||||||
import { Body, ModalContent, Table, Icon } from "@budibase/bbui"
|
import { Body, ModalContent, Table, Icon } from "@budibase/bbui"
|
||||||
import PasswordCopyRenderer from "./PasswordCopyRenderer.svelte"
|
import PasswordCopyRenderer from "./PasswordCopyRenderer.svelte"
|
||||||
import { parseToCsv } from "helpers/data/utils"
|
import { parseToCsv } from "helpers/data/utils"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
export let userData
|
export let userData
|
||||||
|
export let createUsersResponse
|
||||||
|
|
||||||
$: mappedData = userData.map(user => {
|
let hasSuccess
|
||||||
return {
|
let hasFailure
|
||||||
email: user.email,
|
let title
|
||||||
password: user.password,
|
let failureMessage
|
||||||
|
|
||||||
|
let userDataIndex
|
||||||
|
let successfulUsers
|
||||||
|
let unsuccessfulUsers
|
||||||
|
|
||||||
|
const setTitle = () => {
|
||||||
|
if (hasSuccess) {
|
||||||
|
title = "Users created!"
|
||||||
|
} else if (hasFailure) {
|
||||||
|
title = "Oops!"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFailureMessage = () => {
|
||||||
|
if (hasSuccess) {
|
||||||
|
failureMessage = "However there was a problem creating some users."
|
||||||
|
} else {
|
||||||
|
failureMessage = "There was a problem creating some users."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setUsers = () => {
|
||||||
|
userDataIndex = userData.reduce((prev, current) => {
|
||||||
|
prev[current.email] = current
|
||||||
|
return prev
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
successfulUsers = createUsersResponse.successful.map(user => {
|
||||||
|
return {
|
||||||
|
email: user.email,
|
||||||
|
password: userDataIndex[user.email].password,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
unsuccessfulUsers = createUsersResponse.unsuccessful.map(user => {
|
||||||
|
return {
|
||||||
|
email: user.email,
|
||||||
|
reason: user.reason,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
hasSuccess = createUsersResponse.successful.length
|
||||||
|
hasFailure = createUsersResponse.unsuccessful.length
|
||||||
|
setTitle()
|
||||||
|
setFailureMessage()
|
||||||
|
setUsers()
|
||||||
})
|
})
|
||||||
|
|
||||||
const schema = {
|
const successSchema = {
|
||||||
email: {},
|
email: {},
|
||||||
password: {},
|
password: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const failedSchema = {
|
||||||
|
email: {},
|
||||||
|
reason: {},
|
||||||
|
}
|
||||||
|
|
||||||
const downloadCsvFile = () => {
|
const downloadCsvFile = () => {
|
||||||
const fileName = "passwords.csv"
|
const fileName = "passwords.csv"
|
||||||
const content = parseToCsv(["email", "password"], mappedData)
|
const content = parseToCsv(["email", "password"], successfulUsers)
|
||||||
|
|
||||||
download(fileName, content)
|
download(fileName, content)
|
||||||
}
|
}
|
||||||
|
@ -42,36 +96,52 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
size="S"
|
size="M"
|
||||||
title="Accounts created!"
|
{title}
|
||||||
confirmText="Done"
|
confirmText="Done"
|
||||||
showCancelButton={false}
|
showCancelButton={false}
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
>
|
>
|
||||||
<Body size="XS">
|
{#if hasFailure}
|
||||||
All your new users can be accessed through the autogenerated passwords. Take
|
<Body size="XS">
|
||||||
note of these passwords or download the CSV file.
|
{failureMessage}
|
||||||
</Body>
|
</Body>
|
||||||
|
<Table
|
||||||
|
schema={failedSchema}
|
||||||
|
data={unsuccessfulUsers}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={false}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if hasSuccess}
|
||||||
|
<Body size="XS">
|
||||||
|
All your new users can be accessed through the autogenerated passwords.
|
||||||
|
Take note of these passwords or download the CSV file.
|
||||||
|
</Body>
|
||||||
|
|
||||||
<div class="container" on:click={downloadCsvFile}>
|
<div class="container" on:click={downloadCsvFile}>
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
<Icon name="Download" />
|
<Icon name="Download" />
|
||||||
|
|
||||||
<div style="margin-left: var(--spacing-m)">
|
<div style="margin-left: var(--spacing-m)">
|
||||||
<Body size="XS">Passwords CSV</Body>
|
<Body size="XS">Passwords CSV</Body>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
{schema}
|
schema={successSchema}
|
||||||
data={mappedData}
|
data={successfulUsers}
|
||||||
allowEditColumns={false}
|
allowEditColumns={false}
|
||||||
allowEditRows={false}
|
allowEditRows={false}
|
||||||
allowSelectRows={false}
|
allowSelectRows={false}
|
||||||
customRenderers={[{ column: "password", component: PasswordCopyRenderer }]}
|
customRenderers={[
|
||||||
/>
|
{ column: "password", component: PasswordCopyRenderer },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte"
|
import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte"
|
||||||
import PasswordModal from "./_components/PasswordModal.svelte"
|
import PasswordModal from "./_components/PasswordModal.svelte"
|
||||||
|
import DeletionFailureModal from "./_components/DeletionFailureModal.svelte"
|
||||||
import ImportUsersModal from "./_components/ImportUsersModal.svelte"
|
import ImportUsersModal from "./_components/ImportUsersModal.svelte"
|
||||||
import { createPaginationStore } from "helpers/pagination"
|
import { createPaginationStore } from "helpers/pagination"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
@ -33,7 +34,8 @@
|
||||||
inviteConfirmationModal,
|
inviteConfirmationModal,
|
||||||
onboardingTypeModal,
|
onboardingTypeModal,
|
||||||
passwordModal,
|
passwordModal,
|
||||||
importUsersModal
|
importUsersModal,
|
||||||
|
deletionFailureModal
|
||||||
let pageInfo = createPaginationStore()
|
let pageInfo = createPaginationStore()
|
||||||
let prevEmail = undefined,
|
let prevEmail = undefined,
|
||||||
searchEmail = undefined
|
searchEmail = undefined
|
||||||
|
@ -55,6 +57,8 @@
|
||||||
apps: {},
|
apps: {},
|
||||||
}
|
}
|
||||||
$: userData = []
|
$: userData = []
|
||||||
|
$: createUsersResponse = { successful: [], unsuccessful: [] }
|
||||||
|
$: deleteUsersResponse = { successful: [], unsuccessful: [] }
|
||||||
$: page = $pageInfo.page
|
$: page = $pageInfo.page
|
||||||
$: fetchUsers(page, searchEmail)
|
$: fetchUsers(page, searchEmail)
|
||||||
$: {
|
$: {
|
||||||
|
@ -116,8 +120,9 @@
|
||||||
newUsers.push(user)
|
newUsers.push(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newUsers.length)
|
if (!newUsers.length) {
|
||||||
notifications.info("Duplicated! There is no new users to add.")
|
notifications.info("Duplicated! There is no new users to add.")
|
||||||
|
}
|
||||||
return { ...userData, users: newUsers }
|
return { ...userData, users: newUsers }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,7 +149,9 @@
|
||||||
|
|
||||||
async function createUser() {
|
async function createUser() {
|
||||||
try {
|
try {
|
||||||
await users.create(await removingDuplicities(userData))
|
createUsersResponse = await users.create(
|
||||||
|
await removingDuplicities(userData)
|
||||||
|
)
|
||||||
notifications.success("Successfully created user")
|
notifications.success("Successfully created user")
|
||||||
await groups.actions.init()
|
await groups.actions.init()
|
||||||
passwordModal.show()
|
passwordModal.show()
|
||||||
|
@ -176,8 +183,15 @@
|
||||||
notifications.error("You cannot delete yourself")
|
notifications.error("You cannot delete yourself")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await users.bulkDelete(ids)
|
deleteUsersResponse = await users.bulkDelete(ids)
|
||||||
notifications.success(`Successfully deleted ${selectedRows.length} rows`)
|
if (deleteUsersResponse.unsuccessful?.length) {
|
||||||
|
deletionFailureModal.show()
|
||||||
|
} else {
|
||||||
|
notifications.success(
|
||||||
|
`Successfully deleted ${selectedRows.length} users`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
selectedRows = []
|
selectedRows = []
|
||||||
await fetchUsers(page, searchEmail)
|
await fetchUsers(page, searchEmail)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -284,7 +298,11 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={passwordModal}>
|
<Modal bind:this={passwordModal}>
|
||||||
<PasswordModal userData={userData.users} />
|
<PasswordModal {createUsersResponse} userData={userData.users} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal bind:this={deletionFailureModal}>
|
||||||
|
<DeletionFailureModal {deleteUsersResponse} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={importUsersModal}>
|
<Modal bind:this={importUsersModal}>
|
||||||
|
|
|
@ -63,10 +63,14 @@ export function createUsersStore() {
|
||||||
|
|
||||||
return body
|
return body
|
||||||
})
|
})
|
||||||
await API.createUsers({ users: mappedUsers, groups: data.groups })
|
const response = await API.createUsers({
|
||||||
|
users: mappedUsers,
|
||||||
|
groups: data.groups,
|
||||||
|
})
|
||||||
|
|
||||||
// re-search from first page
|
// re-search from first page
|
||||||
await search()
|
await search()
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
async function del(id) {
|
async function del(id) {
|
||||||
|
@ -79,7 +83,7 @@ export function createUsersStore() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bulkDelete(userIds) {
|
async function bulkDelete(userIds) {
|
||||||
await API.deleteUsers(userIds)
|
return API.deleteUsers(userIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save(user) {
|
async function save(user) {
|
||||||
|
|
|
@ -83,9 +83,7 @@ server.on("close", async () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
shuttingDown = true
|
shuttingDown = true
|
||||||
if (!env.isTest()) {
|
console.log("Server Closed")
|
||||||
console.log("Server Closed")
|
|
||||||
}
|
|
||||||
await automations.shutdown()
|
await automations.shutdown()
|
||||||
await redis.shutdown()
|
await redis.shutdown()
|
||||||
await events.shutdown()
|
await events.shutdown()
|
||||||
|
@ -167,3 +165,7 @@ process.on("uncaughtException", err => {
|
||||||
process.on("SIGTERM", () => {
|
process.on("SIGTERM", () => {
|
||||||
shutdown()
|
shutdown()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
shutdown()
|
||||||
|
})
|
||||||
|
|
|
@ -53,6 +53,7 @@ exports.shutdown = async () => {
|
||||||
await automationQueue.close()
|
await automationQueue.close()
|
||||||
automationQueue = null
|
automationQueue = null
|
||||||
}
|
}
|
||||||
|
console.log("Bull shutdown")
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.queue = automationQueue
|
exports.queue = automationQueue
|
||||||
|
|
|
@ -13,10 +13,13 @@ function validate(schema, property) {
|
||||||
params = ctx.request[property]
|
params = ctx.request[property]
|
||||||
}
|
}
|
||||||
|
|
||||||
schema = schema.append({
|
// not all schemas have the append property e.g. array schemas
|
||||||
createdAt: Joi.any().optional(),
|
if (schema.append) {
|
||||||
updatedAt: Joi.any().optional(),
|
schema = schema.append({
|
||||||
})
|
createdAt: Joi.any().optional(),
|
||||||
|
updatedAt: Joi.any().optional(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const { error } = schema.validate(params)
|
const { error } = schema.validate(params)
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
|
@ -106,5 +106,6 @@ export class Thread {
|
||||||
|
|
||||||
static async shutdown() {
|
static async shutdown() {
|
||||||
await Thread.stopThreads()
|
await Thread.stopThreads()
|
||||||
|
console.log("Threads shutdown")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ exports.shutdown = async () => {
|
||||||
if (devAppClient) await devAppClient.finish()
|
if (devAppClient) await devAppClient.finish()
|
||||||
if (debounceClient) await debounceClient.finish()
|
if (debounceClient) await debounceClient.finish()
|
||||||
if (flagClient) await flagClient.finish()
|
if (flagClient) await flagClient.finish()
|
||||||
|
console.log("Redis shutdown")
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.doesUserHaveLock = async (devAppId, user) => {
|
exports.doesUserHaveLock = async (devAppId, user) => {
|
||||||
|
|
5
packages/types/src/api/web/errors.ts
Normal file
5
packages/types/src/api/web/errors.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export interface APIError {
|
||||||
|
message: string
|
||||||
|
status: number
|
||||||
|
error?: any
|
||||||
|
}
|
|
@ -1 +1,3 @@
|
||||||
export * from "./analytics"
|
export * from "./analytics"
|
||||||
|
export * from "./user"
|
||||||
|
export * from "./errors"
|
||||||
|
|
31
packages/types/src/api/web/user.ts
Normal file
31
packages/types/src/api/web/user.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { User } from "../../documents"
|
||||||
|
|
||||||
|
export interface CreateUserResponse {
|
||||||
|
_id: string
|
||||||
|
_rev: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BulkCreateUsersRequest {
|
||||||
|
users: User[]
|
||||||
|
groups: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserDetails {
|
||||||
|
_id: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BulkCreateUsersResponse {
|
||||||
|
successful: UserDetails[]
|
||||||
|
unsuccessful: { email: string; reason: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BulkDeleteUsersRequest {
|
||||||
|
userIds: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BulkDeleteUsersResponse {
|
||||||
|
successful: UserDetails[]
|
||||||
|
unsuccessful: { _id: string; email: string; reason: string }[]
|
||||||
|
}
|
|
@ -15,8 +15,26 @@ export interface User extends Document {
|
||||||
status?: string
|
status?: string
|
||||||
createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now()
|
createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now()
|
||||||
userGroups?: string[]
|
userGroups?: string[]
|
||||||
|
forceResetPassword?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserRoles {
|
export interface UserRoles {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// utility types
|
||||||
|
|
||||||
|
export interface BuilderUser extends User {
|
||||||
|
builder: {
|
||||||
|
global: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminUser extends User {
|
||||||
|
admin: {
|
||||||
|
global: boolean
|
||||||
|
}
|
||||||
|
builder: {
|
||||||
|
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"
|
||||||
|
|
5
packages/types/src/documents/platform/accounts.ts
Normal file
5
packages/types/src/documents/platform/accounts.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { Document } from "../document"
|
||||||
|
|
||||||
|
export interface AccountMetadata extends Document {
|
||||||
|
email: string
|
||||||
|
}
|
|
@ -1 +1,3 @@
|
||||||
export * from "./info"
|
export * from "./info"
|
||||||
|
export * from "./users"
|
||||||
|
export * from "./accounts"
|
||||||
|
|
9
packages/types/src/documents/platform/users.ts
Normal file
9
packages/types/src/documents/platform/users.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { Document } from "../document"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* doc id is user email
|
||||||
|
*/
|
||||||
|
export interface PlatformUserByEmail extends Document {
|
||||||
|
tenantId: string
|
||||||
|
userId: string
|
||||||
|
}
|
26
packages/types/src/documents/pouch.ts
Normal file
26
packages/types/src/documents/pouch.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
export interface RowValue {
|
||||||
|
rev: string
|
||||||
|
deleted: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RowResponse<T> {
|
||||||
|
id: string
|
||||||
|
key: string
|
||||||
|
error: string
|
||||||
|
value: RowValue
|
||||||
|
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
|
||||||
|
}
|
5
packages/types/src/sdk/auth.ts
Normal file
5
packages/types/src/sdk/auth.ts
Normal file
|
@ -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"
|
||||||
|
|
|
@ -74,6 +74,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,
|
||||||
|
@ -46,8 +46,8 @@ export const bulkCreate = async (ctx: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let response = await users.bulkCreate(newUsersRequested, groups)
|
const response = await users.bulkCreate(newUsersRequested, groups)
|
||||||
await groupUtils.bulkSaveGroupUsers(groupsToSave, response)
|
await groupUtils.bulkSaveGroupUsers(groupsToSave, response.successful)
|
||||||
|
|
||||||
ctx.body = response
|
ctx.body = response
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
21
packages/worker/src/api/controllers/system/accounts.ts
Normal file
21
packages/worker/src/api/controllers/system/accounts.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { Account, AccountMetadata } from "@budibase/types"
|
||||||
|
import { accounts } from "../../../sdk"
|
||||||
|
|
||||||
|
export const save = async (ctx: any) => {
|
||||||
|
const account = ctx.request.body as Account
|
||||||
|
let metadata: AccountMetadata = {
|
||||||
|
_id: accounts.formatAccountMetadataId(account.accountId),
|
||||||
|
email: account.email,
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata = await accounts.saveMetadata(metadata)
|
||||||
|
|
||||||
|
ctx.body = metadata
|
||||||
|
ctx.status = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
export const destroy = async (ctx: any) => {
|
||||||
|
const accountId = accounts.formatAccountMetadataId(ctx.params.accountId)
|
||||||
|
await accounts.destroyMetadata(accountId)
|
||||||
|
ctx.status = 204
|
||||||
|
}
|
|
@ -1,15 +1,10 @@
|
||||||
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 { middleware as pro } from "@budibase/pro"
|
||||||
buildAuthMiddleware,
|
import { errors, auth, middleware } from "@budibase/backend-core"
|
||||||
auditLog,
|
import { APIError } from "@budibase/types"
|
||||||
buildTenancyMiddleware,
|
|
||||||
buildCsrfMiddleware,
|
|
||||||
} = require("@budibase/backend-core/auth")
|
|
||||||
const { middleware: pro } = require("@budibase/pro")
|
|
||||||
const { errors } = require("@budibase/backend-core")
|
|
||||||
|
|
||||||
const PUBLIC_ENDPOINTS = [
|
const PUBLIC_ENDPOINTS = [
|
||||||
// old deprecated endpoints kept for backwards compat
|
// old deprecated endpoints kept for backwards compat
|
||||||
|
@ -97,9 +92,9 @@ router
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.use("/health", ctx => (ctx.status = 200))
|
.use("/health", ctx => (ctx.status = 200))
|
||||||
.use(buildAuthMiddleware(PUBLIC_ENDPOINTS))
|
.use(auth.buildAuthMiddleware(PUBLIC_ENDPOINTS))
|
||||||
.use(buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS))
|
.use(auth.buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS))
|
||||||
.use(buildCsrfMiddleware({ noCsrfPatterns: NO_CSRF_ENDPOINTS }))
|
.use(auth.buildCsrfMiddleware({ noCsrfPatterns: NO_CSRF_ENDPOINTS }))
|
||||||
.use(pro.licensing())
|
.use(pro.licensing())
|
||||||
// for now no public access is allowed to worker (bar health check)
|
// for now no public access is allowed to worker (bar health check)
|
||||||
.use((ctx, next) => {
|
.use((ctx, next) => {
|
||||||
|
@ -114,21 +109,22 @@ router
|
||||||
}
|
}
|
||||||
return next()
|
return next()
|
||||||
})
|
})
|
||||||
.use(auditLog)
|
.use(middleware.auditLog)
|
||||||
|
|
||||||
// error handling middleware - TODO: This could be moved to backend-core
|
// error handling middleware - TODO: This could be moved to backend-core
|
||||||
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,36 +19,18 @@ 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()
|
||||||
|
@ -57,18 +39,12 @@ describe("/api/global/auth", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
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 request
|
const res = await api.auth.updatePassword(code)
|
||||||
.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,8 +55,8 @@ 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()
|
||||||
|
@ -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(mockStrategyReturn, {
|
expect(passportSpy).toBeCalledWith(
|
||||||
successRedirect: "/", failureRedirect: "/error"
|
mockStrategyReturn,
|
||||||
}, expect.anything())
|
{
|
||||||
expect(passportSpy.mock.calls.length).toBe(1);
|
successRedirect: "/",
|
||||||
|
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()
|
||||||
|
@ -96,13 +103,17 @@ describe("configs", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
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,13 +201,17 @@ 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()
|
||||||
|
@ -195,12 +221,13 @@ 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)
|
||||||
|
@ -208,22 +235,29 @@ describe("configs", () => {
|
||||||
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
|
|
@ -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)
|
464
packages/worker/src/api/routes/global/tests/users.spec.ts
Normal file
464
packages/worker/src/api/routes/global/tests/users.spec.ts
Normal file
|
@ -0,0 +1,464 @@
|
||||||
|
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(`Unavailable`)
|
||||||
|
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(`Unavailable`)
|
||||||
|
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(`Unavailable`)
|
||||||
|
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,
|
||||||
]
|
]
|
||||||
|
|
19
packages/worker/src/api/routes/system/accounts.ts
Normal file
19
packages/worker/src/api/routes/system/accounts.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import Router from "@koa/router"
|
||||||
|
import * as controller from "../../controllers/system/accounts"
|
||||||
|
import { middleware } from "@budibase/backend-core"
|
||||||
|
|
||||||
|
const router = new Router()
|
||||||
|
|
||||||
|
router
|
||||||
|
.put(
|
||||||
|
"/api/system/accounts/:accountId/metadata",
|
||||||
|
middleware.internalApi,
|
||||||
|
controller.save
|
||||||
|
)
|
||||||
|
.delete(
|
||||||
|
"/api/system/accounts/:accountId/metadata",
|
||||||
|
middleware.internalApi,
|
||||||
|
controller.destroy
|
||||||
|
)
|
||||||
|
|
||||||
|
export = router
|
57
packages/worker/src/api/routes/system/tests/accounts.spec.ts
Normal file
57
packages/worker/src/api/routes/system/tests/accounts.spec.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import { accounts } from "../../../../sdk"
|
||||||
|
import { TestConfiguration, structures, API } from "../../../../tests"
|
||||||
|
import { v4 as uuid } from "uuid"
|
||||||
|
|
||||||
|
describe("accounts", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
const api = new API(config)
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.beforeAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await config.afterAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("metadata", () => {
|
||||||
|
describe("saveMetadata", () => {
|
||||||
|
it("saves account metadata", async () => {
|
||||||
|
let account = structures.accounts.account()
|
||||||
|
|
||||||
|
const response = await api.accounts.saveMetadata(account)
|
||||||
|
|
||||||
|
const id = accounts.formatAccountMetadataId(account.accountId)
|
||||||
|
const metadata = await accounts.getMetadata(id)
|
||||||
|
expect(response).toStrictEqual(metadata)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("destroyMetadata", () => {
|
||||||
|
it("destroys account metadata", async () => {
|
||||||
|
const account = structures.accounts.account()
|
||||||
|
await api.accounts.saveMetadata(account)
|
||||||
|
|
||||||
|
await api.accounts.destroyMetadata(account.accountId)
|
||||||
|
|
||||||
|
const deleted = await accounts.getMetadata(account.accountId)
|
||||||
|
expect(deleted).toBe(undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("destroys account metadata that does not exist", async () => {
|
||||||
|
const id = uuid()
|
||||||
|
|
||||||
|
const response = await api.accounts.destroyMetadata(id)
|
||||||
|
|
||||||
|
expect(response.status).toBe(404)
|
||||||
|
expect(response.body.message).toBe(
|
||||||
|
`id=${accounts.formatAccountMetadataId(id)} does not exist`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,390 +0,0 @@
|
||||||
jest.mock("nodemailer")
|
|
||||||
const { config, request, mocks, structures } = require("../../../tests")
|
|
||||||
const sendMailMock = mocks.email.mock()
|
|
||||||
const { events } = require("@budibase/backend-core")
|
|
||||||
describe("/api/global/users", () => {
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await config.beforeAll()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await config.afterAll()
|
|
||||||
})
|
|
||||||
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
|
|
||||||
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 createUser = async (user) => {
|
|
||||||
const existing = await config.getUser(user.email)
|
|
||||||
if (existing) {
|
|
||||||
await deleteUser(existing._id)
|
|
||||||
}
|
|
||||||
return saveUser(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateUser = async (user) => {
|
|
||||||
const existing = await config.getUser(user.email)
|
|
||||||
user._id = existing._id
|
|
||||||
return saveUser(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveUser = async (user) => {
|
|
||||||
const res = await request
|
|
||||||
.post(`/api/global/users`)
|
|
||||||
.send(user)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
return res.body
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const bulkCreateUsers = async (users) => {
|
|
||||||
const res = await request
|
|
||||||
.post(`/api/global/users/bulkCreate`)
|
|
||||||
.send(users)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
return res.body
|
|
||||||
}
|
|
||||||
|
|
||||||
const bulkDeleteUsers = async (users) => {
|
|
||||||
const res = await request
|
|
||||||
.post(`/api/global/users/bulkDelete`)
|
|
||||||
.send(users)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
return res.body
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const deleteUser = async (email) => {
|
|
||||||
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 () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
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 bulkCreate users with different permissions", async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
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" })
|
|
||||||
|
|
||||||
let toCreate = { users: [builder, admin, user], groups: [] }
|
|
||||||
await bulkCreateUsers(toCreate)
|
|
||||||
|
|
||||||
expect(events.user.created).toBeCalledTimes(3)
|
|
||||||
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1)
|
|
||||||
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
it("should be able to create an admin user", async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
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 () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
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 () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
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 () => {
|
|
||||||
let 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 () => {
|
|
||||||
let 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 () => {
|
|
||||||
let 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("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 be able to bulk delete users with different permissions", async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
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" })
|
|
||||||
|
|
||||||
let toCreate = { users: [builder, admin, user], groups: [] }
|
|
||||||
let createdUsers = await bulkCreateUsers(toCreate)
|
|
||||||
await bulkDeleteUsers({ userIds: [createdUsers[0]._id, createdUsers[1]._id, createdUsers[2]._id] })
|
|
||||||
expect(events.user.deleted).toBeCalledTimes(3)
|
|
||||||
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1)
|
|
||||||
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1)
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -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
|
|
@ -71,9 +71,7 @@ server.on("close", async () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
shuttingDown = true
|
shuttingDown = true
|
||||||
if (!env.isTest()) {
|
console.log("Server Closed")
|
||||||
console.log("Server Closed")
|
|
||||||
}
|
|
||||||
await redis.shutdown()
|
await redis.shutdown()
|
||||||
await events.shutdown()
|
await events.shutdown()
|
||||||
if (!env.isTest()) {
|
if (!env.isTest()) {
|
||||||
|
@ -86,7 +84,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()
|
||||||
})
|
})
|
||||||
|
@ -100,3 +98,7 @@ process.on("uncaughtException", err => {
|
||||||
process.on("SIGTERM", () => {
|
process.on("SIGTERM", () => {
|
||||||
shutdown()
|
shutdown()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
shutdown()
|
||||||
|
})
|
||||||
|
|
53
packages/worker/src/sdk/accounts/accounts.ts
Normal file
53
packages/worker/src/sdk/accounts/accounts.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { AccountMetadata } from "@budibase/types"
|
||||||
|
import {
|
||||||
|
db,
|
||||||
|
StaticDatabases,
|
||||||
|
HTTPError,
|
||||||
|
DocumentType,
|
||||||
|
SEPARATOR,
|
||||||
|
} from "@budibase/backend-core"
|
||||||
|
|
||||||
|
export const formatAccountMetadataId = (accountId: string) => {
|
||||||
|
return `${DocumentType.ACCOUNT_METADATA}${SEPARATOR}${accountId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const saveMetadata = async (
|
||||||
|
metadata: AccountMetadata
|
||||||
|
): Promise<AccountMetadata> => {
|
||||||
|
return db.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => {
|
||||||
|
const existing = await getMetadata(metadata._id!)
|
||||||
|
if (existing) {
|
||||||
|
metadata._rev = existing._rev
|
||||||
|
}
|
||||||
|
const res = await db.put(metadata)
|
||||||
|
metadata._rev = res.rev
|
||||||
|
return metadata
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMetadata = async (
|
||||||
|
accountId: string
|
||||||
|
): Promise<AccountMetadata | undefined> => {
|
||||||
|
return db.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => {
|
||||||
|
try {
|
||||||
|
return await db.get(accountId)
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.status === 404) {
|
||||||
|
// do nothing
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const destroyMetadata = async (accountId: string) => {
|
||||||
|
await db.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => {
|
||||||
|
const metadata = await getMetadata(accountId)
|
||||||
|
if (!metadata) {
|
||||||
|
throw new HTTPError(`id=${accountId} does not exist`, 404)
|
||||||
|
}
|
||||||
|
await db.remove(accountId, metadata._rev)
|
||||||
|
})
|
||||||
|
}
|
1
packages/worker/src/sdk/accounts/index.ts
Normal file
1
packages/worker/src/sdk/accounts/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./accounts"
|
|
@ -1 +1,2 @@
|
||||||
export * as users from "./users"
|
export * as users from "./users"
|
||||||
|
export * as accounts from "./accounts"
|
||||||
|
|
|
@ -14,8 +14,23 @@ import {
|
||||||
HTTPError,
|
HTTPError,
|
||||||
accounts,
|
accounts,
|
||||||
migrations,
|
migrations,
|
||||||
|
StaticDatabases,
|
||||||
|
ViewName,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { MigrationType, User } from "@budibase/types"
|
import {
|
||||||
|
MigrationType,
|
||||||
|
PlatformUserByEmail,
|
||||||
|
User,
|
||||||
|
Account,
|
||||||
|
BulkCreateUsersResponse,
|
||||||
|
CreateUserResponse,
|
||||||
|
BulkDeleteUsersResponse,
|
||||||
|
CloudAccount,
|
||||||
|
AllDocsResponse,
|
||||||
|
RowResponse,
|
||||||
|
BulkDocsResponse,
|
||||||
|
AccountMetadata,
|
||||||
|
} 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
|
||||||
|
@ -98,7 +113,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 (
|
||||||
|
@ -109,7 +123,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
|
||||||
|
@ -143,62 +157,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 `Unavailable`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 `Unavailable`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 `Unavailable`
|
||||||
}
|
}
|
||||||
|
} 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 = () => {
|
||||||
|
@ -247,29 +262,87 @@ export const addTenant = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getExistingTenantUsers = async (emails: string[]): Promise<User[]> => {
|
||||||
|
return dbUtils.queryGlobalView(ViewName.USER_BY_EMAIL, {
|
||||||
|
keys: emails,
|
||||||
|
include_docs: true,
|
||||||
|
arrayResponse: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getExistingPlatformUsers = async (
|
||||||
|
emails: string[]
|
||||||
|
): Promise<PlatformUserByEmail[]> => {
|
||||||
|
return dbUtils.doWithDB(
|
||||||
|
StaticDatabases.PLATFORM_INFO.name,
|
||||||
|
async (infoDb: any) => {
|
||||||
|
const response: AllDocsResponse<PlatformUserByEmail> =
|
||||||
|
await infoDb.allDocs({
|
||||||
|
keys: emails,
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
return response.rows
|
||||||
|
.filter(row => row.doc && (row.error !== "not_found") !== null)
|
||||||
|
.map((row: any) => row.doc)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getExistingAccounts = async (
|
||||||
|
emails: string[]
|
||||||
|
): Promise<AccountMetadata[]> => {
|
||||||
|
return dbUtils.queryPlatformView(ViewName.ACCOUNT_BY_EMAIL, {
|
||||||
|
keys: emails,
|
||||||
|
include_docs: true,
|
||||||
|
arrayResponse: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a system-wide search on emails:
|
||||||
|
* - in tenant
|
||||||
|
* - cross tenant
|
||||||
|
* - accounts
|
||||||
|
* return an array of emails that match the supplied emails.
|
||||||
|
*/
|
||||||
|
const searchExistingEmails = async (emails: string[]) => {
|
||||||
|
let matchedEmails: string[] = []
|
||||||
|
|
||||||
|
const existingTenantUsers = await getExistingTenantUsers(emails)
|
||||||
|
matchedEmails.push(...existingTenantUsers.map(user => user.email))
|
||||||
|
|
||||||
|
const existingPlatformUsers = await getExistingPlatformUsers(emails)
|
||||||
|
matchedEmails.push(...existingPlatformUsers.map(user => user._id!))
|
||||||
|
|
||||||
|
const existingAccounts = await getExistingAccounts(emails)
|
||||||
|
matchedEmails.push(...existingAccounts.map(account => account.email))
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
let usersToSave: any[] = []
|
let usersToSave: any[] = []
|
||||||
let newUsers: any[] = []
|
let newUsers: any[] = []
|
||||||
|
|
||||||
const allUsers = await db.allDocs(
|
const emails = newUsersRequested.map((user: User) => user.email)
|
||||||
dbUtils.getGlobalUserParams(null, {
|
const existingEmails = await searchExistingEmails(emails)
|
||||||
include_docs: true,
|
const unsuccessful: { email: string; reason: string }[] = []
|
||||||
})
|
|
||||||
)
|
|
||||||
let mapped = allUsers.rows.map((row: any) => row.id)
|
|
||||||
|
|
||||||
const currentUserEmails = mapped.map((x: any) => x.email) || []
|
|
||||||
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) ||
|
||||||
currentUserEmails.includes(newUser.email)
|
existingEmails.includes(newUser.email)
|
||||||
) {
|
) {
|
||||||
|
unsuccessful.push({
|
||||||
|
email: newUser.email,
|
||||||
|
reason: `Unavailable`,
|
||||||
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
newUser.userGroups = groups
|
newUser.userGroups = groups
|
||||||
|
@ -307,63 +380,130 @@ export const bulkCreate = async (
|
||||||
await apps.syncUserInApps(user._id)
|
await apps.syncUserInApps(user._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return usersToBulkSave.map(user => {
|
const saved = usersToBulkSave.map(user => {
|
||||||
return {
|
return {
|
||||||
_id: user._id,
|
_id: user._id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
successful: saved,
|
||||||
|
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
|
|
273
packages/worker/src/tests/TestConfiguration.ts
Normal file
273
packages/worker/src/tests/TestConfiguration.ts
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
import "./mocks"
|
||||||
|
import dbConfig from "../db"
|
||||||
|
dbConfig.init()
|
||||||
|
import env from "../environment"
|
||||||
|
import controllers from "./controllers"
|
||||||
|
const supertest = require("supertest")
|
||||||
|
import { Configs } from "../constants"
|
||||||
|
import {
|
||||||
|
users,
|
||||||
|
tenancy,
|
||||||
|
Cookies,
|
||||||
|
Headers,
|
||||||
|
sessions,
|
||||||
|
auth,
|
||||||
|
} from "@budibase/backend-core"
|
||||||
|
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 sessions.createASession(user._id!, {
|
||||||
|
sessionId: "sessionid",
|
||||||
|
tenantId: user.tenantId,
|
||||||
|
csrfToken: CSRF_TOKEN,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cookieHeader(cookies: any) {
|
||||||
|
return {
|
||||||
|
Cookie: [cookies],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authHeaders(user: User) {
|
||||||
|
const authToken: AuthToken = {
|
||||||
|
userId: user._id!,
|
||||||
|
sessionId: "sessionid",
|
||||||
|
tenantId: user.tenantId,
|
||||||
|
}
|
||||||
|
const authCookie = auth.jwt.sign(authToken, env.JWT_SECRET)
|
||||||
|
return {
|
||||||
|
Accept: "application/json",
|
||||||
|
...this.cookieHeader([`${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 = auth.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
|
28
packages/worker/src/tests/api/accounts.ts
Normal file
28
packages/worker/src/tests/api/accounts.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { Account, AccountMetadata } from "@budibase/types"
|
||||||
|
import TestConfiguration from "../TestConfiguration"
|
||||||
|
|
||||||
|
export class AccountAPI {
|
||||||
|
config: TestConfiguration
|
||||||
|
request: any
|
||||||
|
|
||||||
|
constructor(config: TestConfiguration) {
|
||||||
|
this.config = config
|
||||||
|
this.request = config.request
|
||||||
|
}
|
||||||
|
|
||||||
|
saveMetadata = async (account: Account) => {
|
||||||
|
const res = await this.request
|
||||||
|
.put(`/api/system/accounts/${account.accountId}/metadata`)
|
||||||
|
.send(account)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
return res.body as AccountMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyMetadata = (accountId: string) => {
|
||||||
|
return this.request
|
||||||
|
.del(`/api/system/accounts/${accountId}/metadata`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
}
|
||||||
|
}
|
48
packages/worker/src/tests/api/auth.ts
Normal file
48
packages/worker/src/tests/api/auth.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import TestConfiguration from "../TestConfiguration"
|
||||||
|
|
||||||
|
export class AuthAPI {
|
||||||
|
config: TestConfiguration
|
||||||
|
request: any
|
||||||
|
|
||||||
|
constructor(config: TestConfiguration) {
|
||||||
|
this.config = config
|
||||||
|
this.request = config.request
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePassword = (code: string) => {
|
||||||
|
return this.request
|
||||||
|
.post(`/api/global/auth/${this.config.getTenantId()}/reset/update`)
|
||||||
|
.send({
|
||||||
|
password: "newpassword",
|
||||||
|
resetCode: code,
|
||||||
|
})
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
logout = () => {
|
||||||
|
return this.request
|
||||||
|
.post("/api/global/auth/logout")
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestPasswordReset = async (sendMailMock: any) => {
|
||||||
|
await this.config.saveSmtpConfig()
|
||||||
|
await this.config.saveSettingsConfig()
|
||||||
|
await this.config.createUser()
|
||||||
|
const res = await this.request
|
||||||
|
.post(`/api/global/auth/${this.config.getTenantId()}/reset`)
|
||||||
|
.send({
|
||||||
|
email: "test@test.com",
|
||||||
|
})
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
const emailCall = sendMailMock.mock.calls[0][0]
|
||||||
|
const parts = emailCall.html.split(
|
||||||
|
`http://localhost:10000/builder/auth/reset?code=`
|
||||||
|
)
|
||||||
|
const code = parts[1].split('"')[0].split("&")[0]
|
||||||
|
return { code, res }
|
||||||
|
}
|
||||||
|
}
|
40
packages/worker/src/tests/api/configs.ts
Normal file
40
packages/worker/src/tests/api/configs.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import TestConfiguration from "../TestConfiguration"
|
||||||
|
|
||||||
|
export class ConfigAPI {
|
||||||
|
config: TestConfiguration
|
||||||
|
request: any
|
||||||
|
|
||||||
|
constructor(config: TestConfiguration) {
|
||||||
|
this.config = config
|
||||||
|
this.request = config.request
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfigChecklist = () => {
|
||||||
|
return this.request
|
||||||
|
.get(`/api/global/configs/checklist`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
saveConfig = (data: any) => {
|
||||||
|
return this.request
|
||||||
|
.post(`/api/global/configs`)
|
||||||
|
.send(data)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
OIDCCallback = (configId: string) => {
|
||||||
|
return this.request
|
||||||
|
.get(`/api/global/auth/${this.config.getTenantId()}/oidc/callback`)
|
||||||
|
.set(this.config.getOIDConfigCookie(configId))
|
||||||
|
}
|
||||||
|
|
||||||
|
getOIDCConfig = (configId: string) => {
|
||||||
|
return this.request.get(
|
||||||
|
`/api/global/auth/${this.config.getTenantId()}/oidc/configs/${configId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
24
packages/worker/src/tests/api/email.ts
Normal file
24
packages/worker/src/tests/api/email.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import TestConfiguration from "../TestConfiguration"
|
||||||
|
|
||||||
|
export class EmailAPI {
|
||||||
|
config: TestConfiguration
|
||||||
|
request: any
|
||||||
|
|
||||||
|
constructor(config: TestConfiguration) {
|
||||||
|
this.config = config
|
||||||
|
this.request = config.request
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEmail = (purpose: string) => {
|
||||||
|
return this.request
|
||||||
|
.post(`/api/global/email/send`)
|
||||||
|
.send({
|
||||||
|
email: "test@test.com",
|
||||||
|
purpose,
|
||||||
|
tenantId: this.config.getTenantId(),
|
||||||
|
})
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
}
|
||||||
|
}
|
25
packages/worker/src/tests/api/index.ts
Normal file
25
packages/worker/src/tests/api/index.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import TestConfiguration from "../TestConfiguration"
|
||||||
|
import { AccountAPI } from "./accounts"
|
||||||
|
import { AuthAPI } from "./auth"
|
||||||
|
import { ConfigAPI } from "./configs"
|
||||||
|
import { EmailAPI } from "./email"
|
||||||
|
import { SelfAPI } from "./self"
|
||||||
|
import { UserAPI } from "./users"
|
||||||
|
|
||||||
|
export default class API {
|
||||||
|
accounts: AccountAPI
|
||||||
|
auth: AuthAPI
|
||||||
|
configs: ConfigAPI
|
||||||
|
emails: EmailAPI
|
||||||
|
self: SelfAPI
|
||||||
|
users: UserAPI
|
||||||
|
|
||||||
|
constructor(config: TestConfiguration) {
|
||||||
|
this.accounts = new AccountAPI(config)
|
||||||
|
this.auth = new AuthAPI(config)
|
||||||
|
this.configs = new ConfigAPI(config)
|
||||||
|
this.emails = new EmailAPI(config)
|
||||||
|
this.self = new SelfAPI(config)
|
||||||
|
this.users = new UserAPI(config)
|
||||||
|
}
|
||||||
|
}
|
21
packages/worker/src/tests/api/self.ts
Normal file
21
packages/worker/src/tests/api/self.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import TestConfiguration from "../TestConfiguration"
|
||||||
|
import { User } from "@budibase/types"
|
||||||
|
|
||||||
|
export class SelfAPI {
|
||||||
|
config: TestConfiguration
|
||||||
|
request: any
|
||||||
|
|
||||||
|
constructor(config: TestConfiguration) {
|
||||||
|
this.config = config
|
||||||
|
this.request = config.request
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelf = (user: User) => {
|
||||||
|
return this.request
|
||||||
|
.post(`/api/global/self`)
|
||||||
|
.send(user)
|
||||||
|
.set(this.config.authHeaders(user))
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
}
|
||||||
|
}
|
95
packages/worker/src/tests/api/users.ts
Normal file
95
packages/worker/src/tests/api/users.ts
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import {
|
||||||
|
BulkCreateUsersRequest,
|
||||||
|
BulkCreateUsersResponse,
|
||||||
|
BulkDeleteUsersRequest,
|
||||||
|
CreateUserResponse,
|
||||||
|
User,
|
||||||
|
UserDetails,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import TestConfiguration from "../TestConfiguration"
|
||||||
|
|
||||||
|
export class UserAPI {
|
||||||
|
config: TestConfiguration
|
||||||
|
request: any
|
||||||
|
|
||||||
|
constructor(config: TestConfiguration) {
|
||||||
|
this.config = config
|
||||||
|
this.request = config.request
|
||||||
|
}
|
||||||
|
|
||||||
|
// INVITE
|
||||||
|
|
||||||
|
sendUserInvite = async (sendMailMock: any) => {
|
||||||
|
await this.config.saveSmtpConfig()
|
||||||
|
await this.config.saveSettingsConfig()
|
||||||
|
const res = await this.request
|
||||||
|
.post(`/api/global/users/invite`)
|
||||||
|
.send({
|
||||||
|
email: "invite@test.com",
|
||||||
|
})
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
const emailCall = sendMailMock.mock.calls[0][0]
|
||||||
|
// after this URL there should be a code
|
||||||
|
const parts = emailCall.html.split(
|
||||||
|
"http://localhost:10000/builder/invite?code="
|
||||||
|
)
|
||||||
|
const code = parts[1].split('"')[0].split("&")[0]
|
||||||
|
return { code, res }
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptInvite = (code: string) => {
|
||||||
|
return this.request
|
||||||
|
.post(`/api/global/users/invite/accept`)
|
||||||
|
.send({
|
||||||
|
password: "newpassword",
|
||||||
|
inviteCode: code,
|
||||||
|
})
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BULK
|
||||||
|
|
||||||
|
bulkCreateUsers = async (users: User[], groups: any[] = []) => {
|
||||||
|
const body: BulkCreateUsersRequest = { users, groups }
|
||||||
|
const res = await this.request
|
||||||
|
.post(`/api/global/users/bulkCreate`)
|
||||||
|
.send(body)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
return res.body as BulkCreateUsersResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
bulkDeleteUsers = (body: BulkDeleteUsersRequest, status?: number) => {
|
||||||
|
return this.request
|
||||||
|
.post(`/api/global/users/bulkDelete`)
|
||||||
|
.send(body)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(status ? status : 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// USER
|
||||||
|
|
||||||
|
saveUser = (user: User, status?: number) => {
|
||||||
|
return this.request
|
||||||
|
.post(`/api/global/users`)
|
||||||
|
.send(user)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(status ? status : 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteUser = (userId: string, status?: number) => {
|
||||||
|
return this.request
|
||||||
|
.delete(`/api/global/users/${userId}`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(status ? status : 200)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +0,0 @@
|
||||||
const TestConfiguration = require("./TestConfiguration")
|
|
||||||
const structures = require("./structures")
|
|
||||||
const mocks = require("./mocks")
|
|
||||||
const config = new TestConfiguration()
|
|
||||||
const request = config.getRequest()
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
structures,
|
|
||||||
mocks,
|
|
||||||
config,
|
|
||||||
request,
|
|
||||||
}
|
|
14
packages/worker/src/tests/index.ts
Normal file
14
packages/worker/src/tests/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import TestConfiguration from "./TestConfiguration"
|
||||||
|
import structures from "./structures"
|
||||||
|
import mocks from "./mocks"
|
||||||
|
import API from "./api"
|
||||||
|
|
||||||
|
const pkg = {
|
||||||
|
structures,
|
||||||
|
TENANT_1: structures.TENANT_1,
|
||||||
|
mocks,
|
||||||
|
TestConfiguration,
|
||||||
|
API,
|
||||||
|
}
|
||||||
|
|
||||||
|
export = pkg
|
|
@ -1,5 +0,0 @@
|
||||||
const email = require("./email")
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
email,
|
|
||||||
}
|
|
7
packages/worker/src/tests/mocks/index.ts
Normal file
7
packages/worker/src/tests/mocks/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
const email = require("./email")
|
||||||
|
import { mocks as coreMocks } from "@budibase/backend-core/tests"
|
||||||
|
|
||||||
|
export = {
|
||||||
|
email,
|
||||||
|
...coreMocks,
|
||||||
|
}
|
24
packages/worker/src/tests/structures/accounts.ts
Normal file
24
packages/worker/src/tests/structures/accounts.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { Account, AuthType, Hosting, CloudAccount } from "@budibase/types"
|
||||||
|
import { v4 as uuid } from "uuid"
|
||||||
|
import { utils } from "@budibase/backend-core"
|
||||||
|
|
||||||
|
export const account = (): Account => {
|
||||||
|
return {
|
||||||
|
email: `${uuid()}@test.com`,
|
||||||
|
tenantId: utils.newid(),
|
||||||
|
hosting: Hosting.SELF,
|
||||||
|
authType: AuthType.SSO,
|
||||||
|
accountId: uuid(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
verified: true,
|
||||||
|
verificationSent: true,
|
||||||
|
tier: "FREE",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cloudAccount = (): CloudAccount => {
|
||||||
|
return {
|
||||||
|
...account(),
|
||||||
|
budibaseUserId: uuid(),
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
const configs = require("./configs")
|
|
||||||
const users = require("./users")
|
|
||||||
const groups = require("./groups")
|
|
||||||
|
|
||||||
const TENANT_ID = "default"
|
|
||||||
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
configs,
|
|
||||||
users,
|
|
||||||
TENANT_ID,
|
|
||||||
CSRF_TOKEN,
|
|
||||||
groups,
|
|
||||||
}
|
|
20
packages/worker/src/tests/structures/index.ts
Normal file
20
packages/worker/src/tests/structures/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import configs from "./configs"
|
||||||
|
import * as users from "./users"
|
||||||
|
import * as groups from "./groups"
|
||||||
|
import * as accounts from "./accounts"
|
||||||
|
|
||||||
|
const TENANT_ID = "default"
|
||||||
|
const TENANT_1 = "tenant1"
|
||||||
|
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
|
||||||
|
|
||||||
|
const pkg = {
|
||||||
|
configs,
|
||||||
|
users,
|
||||||
|
accounts,
|
||||||
|
TENANT_ID,
|
||||||
|
TENANT_1,
|
||||||
|
CSRF_TOKEN,
|
||||||
|
groups,
|
||||||
|
}
|
||||||
|
|
||||||
|
export = pkg
|
|
@ -1,24 +1,32 @@
|
||||||
export const email = "test@test.com"
|
export const email = "test@test.com"
|
||||||
|
import { AdminUser, BuilderUser, User } from "@budibase/types"
|
||||||
|
import { v4 as uuid } from "uuid"
|
||||||
|
|
||||||
export const user = (userProps: any) => {
|
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) => {
|
export const adminUser = (userProps?: any): AdminUser => {
|
||||||
return {
|
return {
|
||||||
...user(userProps),
|
...user(userProps),
|
||||||
admin: {
|
admin: {
|
||||||
global: true,
|
global: true,
|
||||||
},
|
},
|
||||||
|
builder: {
|
||||||
|
global: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const builderUser = (userProps: any) => {
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1094,6 +1094,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 a new issue