Merge pull request #1414 from Budibase/feature/smtp-templates
System for templating and sending SMTP emails
This commit is contained in:
commit
e08df4110a
|
@ -14,6 +14,8 @@ const DocumentTypes = {
|
||||||
USER: "us",
|
USER: "us",
|
||||||
APP: "app",
|
APP: "app",
|
||||||
GROUP: "group",
|
GROUP: "group",
|
||||||
|
CONFIG: "config",
|
||||||
|
TEMPLATE: "template",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.DocumentTypes = DocumentTypes
|
exports.DocumentTypes = DocumentTypes
|
||||||
|
@ -46,14 +48,14 @@ exports.getGroupParams = (id = "", otherProps = {}) => {
|
||||||
* Generates a new global user ID.
|
* Generates a new global user ID.
|
||||||
* @returns {string} The new user ID which the user doc can be stored under.
|
* @returns {string} The new user ID which the user doc can be stored under.
|
||||||
*/
|
*/
|
||||||
exports.generateGlobalUserID = () => {
|
exports.generateGlobalUserID = id => {
|
||||||
return `${DocumentTypes.USER}${SEPARATOR}${newid()}`
|
return `${DocumentTypes.USER}${SEPARATOR}${id || newid()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets parameters for retrieving users.
|
* Gets parameters for retrieving users.
|
||||||
*/
|
*/
|
||||||
exports.getGlobalUserParams = (globalId = "", otherProps = {}) => {
|
exports.getGlobalUserParams = (globalId, otherProps = {}) => {
|
||||||
if (!globalId) {
|
if (!globalId) {
|
||||||
globalId = ""
|
globalId = ""
|
||||||
}
|
}
|
||||||
|
@ -63,3 +65,98 @@ exports.getGlobalUserParams = (globalId = "", otherProps = {}) => {
|
||||||
endkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}${UNICODE_MAX}`,
|
endkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}${UNICODE_MAX}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a template ID.
|
||||||
|
* @param ownerId The owner/user of the template, this could be global or a group level.
|
||||||
|
*/
|
||||||
|
exports.generateTemplateID = ownerId => {
|
||||||
|
return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${newid()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets parameters for retrieving templates. Owner ID must be specified, either global or a group level.
|
||||||
|
*/
|
||||||
|
exports.getTemplateParams = (ownerId, templateId, otherProps = {}) => {
|
||||||
|
if (!templateId) {
|
||||||
|
templateId = ""
|
||||||
|
}
|
||||||
|
let final
|
||||||
|
if (templateId) {
|
||||||
|
final = templateId
|
||||||
|
} else {
|
||||||
|
final = `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}`
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...otherProps,
|
||||||
|
startkey: final,
|
||||||
|
endkey: `${final}${UNICODE_MAX}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new configuration ID.
|
||||||
|
* @returns {string} The new configuration ID which the config doc can be stored under.
|
||||||
|
*/
|
||||||
|
const generateConfigID = ({ type, group, user }) => {
|
||||||
|
const scope = [type, group, user].filter(Boolean).join(SEPARATOR)
|
||||||
|
|
||||||
|
return `${DocumentTypes.CONFIG}${SEPARATOR}${scope}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets parameters for retrieving configurations.
|
||||||
|
*/
|
||||||
|
const getConfigParams = ({ type, group, user }, otherProps = {}) => {
|
||||||
|
const scope = [type, group, user].filter(Boolean).join(SEPARATOR)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...otherProps,
|
||||||
|
startkey: `${DocumentTypes.CONFIG}${SEPARATOR}${scope}`,
|
||||||
|
endkey: `${DocumentTypes.CONFIG}${SEPARATOR}${scope}${UNICODE_MAX}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the most granular configuration document from the DB based on the type, group and userID passed.
|
||||||
|
* @param {Object} db - db instance to query
|
||||||
|
* @param {Object} scopes - the type, group and userID scopes of the configuration.
|
||||||
|
* @returns The most granular configuration document based on the scope.
|
||||||
|
*/
|
||||||
|
const determineScopedConfig = async function(db, { type, user, group }) {
|
||||||
|
const response = await db.allDocs(
|
||||||
|
getConfigParams(
|
||||||
|
{ type, user, group },
|
||||||
|
{
|
||||||
|
include_docs: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const configs = response.rows.map(row => {
|
||||||
|
const config = row.doc
|
||||||
|
|
||||||
|
// Config is specific to a user and a group
|
||||||
|
if (config._id.includes(generateConfigID({ type, user, group }))) {
|
||||||
|
config.score = 4
|
||||||
|
} else if (config._id.includes(generateConfigID({ type, user }))) {
|
||||||
|
// Config is specific to a user only
|
||||||
|
config.score = 3
|
||||||
|
} else if (config._id.includes(generateConfigID({ type, group }))) {
|
||||||
|
// Config is specific to a group only
|
||||||
|
config.score = 2
|
||||||
|
} else if (config._id.includes(generateConfigID({ type }))) {
|
||||||
|
// Config is specific to a type only
|
||||||
|
config.score = 1
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
|
// Find the config with the most granular scope based on context
|
||||||
|
const scopedConfig = configs.sort((a, b) => b.score - a.score)[0]
|
||||||
|
|
||||||
|
return scopedConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.generateConfigID = generateConfigID
|
||||||
|
exports.getConfigParams = getConfigParams
|
||||||
|
exports.determineScopedConfig = determineScopedConfig
|
||||||
|
|
|
@ -2,7 +2,4 @@ module.exports = {
|
||||||
JWT_SECRET: process.env.JWT_SECRET,
|
JWT_SECRET: process.env.JWT_SECRET,
|
||||||
COUCH_DB_URL: process.env.COUCH_DB_URL,
|
COUCH_DB_URL: process.env.COUCH_DB_URL,
|
||||||
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
||||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
|
||||||
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
|
|
||||||
GOOGLE_AUTH_CALLBACK_URL: process.env.GOOGLE_AUTH_CALLBACK_URL,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,13 @@
|
||||||
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 GoogleStrategy = require("passport-google-oauth").Strategy
|
|
||||||
const { setDB, getDB } = require("./db")
|
|
||||||
const { StaticDatabases } = require("./db/utils")
|
const { StaticDatabases } = require("./db/utils")
|
||||||
const { jwt, local, authenticated } = require("./middleware")
|
const { jwt, local, authenticated, google } = require("./middleware")
|
||||||
const { Cookies, UserStatus } = require("./constants")
|
const { setDB, getDB } = require("./db")
|
||||||
const { hash, compare } = require("./hashing")
|
|
||||||
const {
|
|
||||||
getAppId,
|
|
||||||
setCookie,
|
|
||||||
getCookie,
|
|
||||||
clearCookie,
|
|
||||||
isClient,
|
|
||||||
getGlobalUserByEmail,
|
|
||||||
} = require("./utils")
|
|
||||||
const {
|
|
||||||
generateGlobalUserID,
|
|
||||||
getGlobalUserParams,
|
|
||||||
generateGroupID,
|
|
||||||
getGroupParams,
|
|
||||||
} = require("./db/utils")
|
|
||||||
|
|
||||||
// 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.use(new GoogleStrategy(google.options, google.authenticate))
|
|
||||||
|
|
||||||
passport.serializeUser((user, done) => done(null, user))
|
passport.serializeUser((user, done) => done(null, user))
|
||||||
|
|
||||||
|
@ -45,21 +27,17 @@ module.exports = {
|
||||||
init(pouch) {
|
init(pouch) {
|
||||||
setDB(pouch)
|
setDB(pouch)
|
||||||
},
|
},
|
||||||
|
db: require("./db/utils"),
|
||||||
|
utils: {
|
||||||
|
...require("./utils"),
|
||||||
|
...require("./hashing"),
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
buildAuthMiddleware: authenticated,
|
||||||
passport,
|
passport,
|
||||||
Cookies,
|
google,
|
||||||
UserStatus,
|
jwt: require("jsonwebtoken"),
|
||||||
|
},
|
||||||
StaticDatabases,
|
StaticDatabases,
|
||||||
generateGlobalUserID,
|
constants: require("./constants"),
|
||||||
getGlobalUserParams,
|
|
||||||
generateGroupID,
|
|
||||||
getGroupParams,
|
|
||||||
hash,
|
|
||||||
compare,
|
|
||||||
getAppId,
|
|
||||||
setCookie,
|
|
||||||
getCookie,
|
|
||||||
clearCookie,
|
|
||||||
authenticated,
|
|
||||||
isClient,
|
|
||||||
getGlobalUserByEmail,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,39 @@
|
||||||
const { Cookies } = require("../constants")
|
const { Cookies } = require("../constants")
|
||||||
const { getCookie } = require("../utils")
|
const database = require("../db")
|
||||||
|
const { getCookie, clearCookie } = require("../utils")
|
||||||
|
const { StaticDatabases } = require("../db/utils")
|
||||||
|
|
||||||
module.exports = async (ctx, next) => {
|
module.exports = (noAuthPatterns = []) => {
|
||||||
|
const regex = new RegExp(noAuthPatterns.join("|"))
|
||||||
|
return async (ctx, next) => {
|
||||||
|
// the path is not authenticated
|
||||||
|
if (regex.test(ctx.request.url)) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
// check the actual user is authenticated first
|
// check the actual user is authenticated first
|
||||||
const authCookie = getCookie(ctx, Cookies.Auth)
|
const authCookie = getCookie(ctx, Cookies.Auth)
|
||||||
|
|
||||||
if (authCookie) {
|
if (authCookie) {
|
||||||
|
try {
|
||||||
|
const db = database.getDB(StaticDatabases.GLOBAL.name)
|
||||||
|
const user = await db.get(authCookie.userId)
|
||||||
|
delete user.password
|
||||||
ctx.isAuthenticated = true
|
ctx.isAuthenticated = true
|
||||||
ctx.user = authCookie
|
ctx.user = user
|
||||||
|
} catch (err) {
|
||||||
|
// remove the cookie as the use does not exist anymore
|
||||||
|
clearCookie(ctx, Cookies.Auth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// be explicit
|
||||||
|
if (ctx.isAuthenticated !== true) {
|
||||||
|
ctx.isAuthenticated = false
|
||||||
}
|
}
|
||||||
|
|
||||||
await next()
|
return next()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.throw(err.status || 403, err)
|
ctx.throw(err.status || 403, err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,76 @@
|
||||||
const env = require("../../environment")
|
const env = require("../../environment")
|
||||||
|
const jwt = require("jsonwebtoken")
|
||||||
|
const database = require("../../db")
|
||||||
|
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
||||||
|
const { StaticDatabases, generateGlobalUserID } = require("../../db/utils")
|
||||||
|
|
||||||
exports.options = {
|
async function authenticate(token, tokenSecret, profile, done) {
|
||||||
clientId: env.GOOGLE_CLIENT_ID,
|
// Check the user exists in the instance DB by email
|
||||||
clientSecret: env.GOOGLE_CLIENT_SECRET,
|
const db = database.getDB(StaticDatabases.GLOBAL.name)
|
||||||
callbackURL: env.GOOGLE_AUTH_CALLBACK_URL,
|
|
||||||
|
let dbUser
|
||||||
|
const userId = generateGlobalUserID(profile.id)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// use the google profile id
|
||||||
|
dbUser = await db.get(userId)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Google user not found. Creating..")
|
||||||
|
// create the user
|
||||||
|
const user = {
|
||||||
|
_id: userId,
|
||||||
|
provider: profile.provider,
|
||||||
|
roles: {},
|
||||||
|
builder: {
|
||||||
|
global: true,
|
||||||
|
},
|
||||||
|
...profile._json,
|
||||||
|
}
|
||||||
|
const response = await db.post(user)
|
||||||
|
|
||||||
|
dbUser = user
|
||||||
|
dbUser._rev = response.rev
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticate
|
||||||
|
const payload = {
|
||||||
|
userId: dbUser._id,
|
||||||
|
builder: dbUser.builder,
|
||||||
|
email: dbUser.email,
|
||||||
|
}
|
||||||
|
|
||||||
|
dbUser.token = jwt.sign(payload, env.JWT_SECRET, {
|
||||||
|
expiresIn: "1 day",
|
||||||
|
})
|
||||||
|
|
||||||
|
return done(null, dbUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
// exports.authenticate = async function(token, tokenSecret, profile, done) {
|
/**
|
||||||
// // retrieve user ...
|
* Create an instance of the google passport strategy. This wrapper fetches the configuration
|
||||||
// fetchUser().then(user => done(null, user))
|
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
|
||||||
// }
|
* @returns Dynamically configured Passport Google Strategy
|
||||||
|
*/
|
||||||
|
exports.strategyFactory = async function(config) {
|
||||||
|
try {
|
||||||
|
const { clientID, clientSecret, callbackURL } = config
|
||||||
|
|
||||||
|
if (!clientID || !clientSecret || !callbackURL) {
|
||||||
|
throw new Error(
|
||||||
|
"Configuration invalid. Must contain google clientID, clientSecret and callbackURL"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GoogleStrategy(
|
||||||
|
{
|
||||||
|
clientID: config.clientID,
|
||||||
|
clientSecret: config.clientSecret,
|
||||||
|
callbackURL: config.callbackURL,
|
||||||
|
},
|
||||||
|
authenticate
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
throw new Error("Error constructing google authentication strategy", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -33,8 +33,6 @@ exports.authenticate = async function(email, password, done) {
|
||||||
if (await compare(password, dbUser.password)) {
|
if (await compare(password, dbUser.password)) {
|
||||||
const payload = {
|
const payload = {
|
||||||
userId: dbUser._id,
|
userId: dbUser._id,
|
||||||
builder: dbUser.builder,
|
|
||||||
email: dbUser.email,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dbUser.token = jwt.sign(payload, env.JWT_SECRET, {
|
dbUser.token = jwt.sign(payload, env.JWT_SECRET, {
|
||||||
|
|
|
@ -92,7 +92,7 @@ exports.setCookie = (ctx, value, name = "builder") => {
|
||||||
* Utility function, simply calls setCookie with an empty string for value
|
* Utility function, simply calls setCookie with an empty string for value
|
||||||
*/
|
*/
|
||||||
exports.clearCookie = (ctx, name) => {
|
exports.clearCookie = (ctx, name) => {
|
||||||
exports.setCookie(ctx, "", name)
|
exports.setCookie(ctx, null, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
<Input outline type="password" on:change bind:value={password} />
|
<Input outline type="password" on:change bind:value={password} />
|
||||||
<Spacer large />
|
<Spacer large />
|
||||||
<Button primary on:click={login}>Login</Button>
|
<Button primary on:click={login}>Login</Button>
|
||||||
|
<a target="_blank" href="/api/admin/auth/google">Sign In With Google</a>
|
||||||
<Button secondary on:click={createTestUser}>Create Test User</Button>
|
<Button secondary on:click={createTestUser}>Create Test User</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
|
@ -103,7 +103,6 @@
|
||||||
"jimp": "0.16.1",
|
"jimp": "0.16.1",
|
||||||
"joi": "17.2.1",
|
"joi": "17.2.1",
|
||||||
"jsonschema": "1.4.0",
|
"jsonschema": "1.4.0",
|
||||||
"jsonwebtoken": "8.5.1",
|
|
||||||
"koa": "2.7.0",
|
"koa": "2.7.0",
|
||||||
"koa-body": "4.2.0",
|
"koa-body": "4.2.0",
|
||||||
"koa-compress": "4.0.1",
|
"koa-compress": "4.0.1",
|
||||||
|
|
|
@ -72,7 +72,7 @@ exports.createMetadata = async function(ctx) {
|
||||||
|
|
||||||
exports.updateSelfMetadata = async function(ctx) {
|
exports.updateSelfMetadata = async function(ctx) {
|
||||||
// overwrite the ID with current users
|
// overwrite the ID with current users
|
||||||
ctx.request.body._id = ctx.user.userId
|
ctx.request.body._id = ctx.user._id
|
||||||
// make sure no stale rev
|
// make sure no stale rev
|
||||||
delete ctx.request.body._rev
|
delete ctx.request.body._rev
|
||||||
await exports.updateMetadata(ctx)
|
await exports.updateMetadata(ctx)
|
||||||
|
|
|
@ -1,14 +1,21 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const { authenticated } = require("@budibase/auth")
|
const { buildAuthMiddleware } = require("@budibase/auth").auth
|
||||||
const currentApp = require("../middleware/currentapp")
|
const currentApp = require("../middleware/currentapp")
|
||||||
const compress = require("koa-compress")
|
const compress = require("koa-compress")
|
||||||
const zlib = require("zlib")
|
const zlib = require("zlib")
|
||||||
const { mainRoutes, authRoutes, staticRoutes } = require("./routes")
|
const { mainRoutes, staticRoutes } = require("./routes")
|
||||||
const pkg = require("../../package.json")
|
const pkg = require("../../package.json")
|
||||||
|
|
||||||
const router = new Router()
|
const router = new Router()
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
|
||||||
|
const NO_AUTH_ENDPOINTS = [
|
||||||
|
"/health",
|
||||||
|
"/version",
|
||||||
|
"webhooks/trigger",
|
||||||
|
"webhooks/schema",
|
||||||
|
]
|
||||||
|
|
||||||
router
|
router
|
||||||
.use(
|
.use(
|
||||||
compress({
|
compress({
|
||||||
|
@ -31,7 +38,7 @@ router
|
||||||
})
|
})
|
||||||
.use("/health", ctx => (ctx.status = 200))
|
.use("/health", ctx => (ctx.status = 200))
|
||||||
.use("/version", ctx => (ctx.body = pkg.version))
|
.use("/version", ctx => (ctx.body = pkg.version))
|
||||||
.use(authenticated)
|
.use(buildAuthMiddleware(NO_AUTH_ENDPOINTS))
|
||||||
.use(currentApp)
|
.use(currentApp)
|
||||||
|
|
||||||
// error handling middleware
|
// error handling middleware
|
||||||
|
@ -53,9 +60,6 @@ router.use(async (ctx, next) => {
|
||||||
|
|
||||||
router.get("/health", ctx => (ctx.status = 200))
|
router.get("/health", ctx => (ctx.status = 200))
|
||||||
|
|
||||||
router.use(authRoutes.routes())
|
|
||||||
router.use(authRoutes.allowedMethods())
|
|
||||||
|
|
||||||
// authenticated routes
|
// authenticated routes
|
||||||
for (let route of mainRoutes) {
|
for (let route of mainRoutes) {
|
||||||
router.use(route.routes())
|
router.use(route.routes())
|
||||||
|
|
|
@ -25,6 +25,7 @@ const backupRoutes = require("./backup")
|
||||||
const devRoutes = require("./dev")
|
const devRoutes = require("./dev")
|
||||||
|
|
||||||
exports.mainRoutes = [
|
exports.mainRoutes = [
|
||||||
|
authRoutes,
|
||||||
deployRoutes,
|
deployRoutes,
|
||||||
layoutRoutes,
|
layoutRoutes,
|
||||||
screenRoutes,
|
screenRoutes,
|
||||||
|
@ -52,5 +53,4 @@ exports.mainRoutes = [
|
||||||
rowRoutes,
|
rowRoutes,
|
||||||
]
|
]
|
||||||
|
|
||||||
exports.authRoutes = authRoutes
|
|
||||||
exports.staticRoutes = staticRoutes
|
exports.staticRoutes = staticRoutes
|
||||||
|
|
|
@ -93,7 +93,7 @@ describe("/queries", () => {
|
||||||
const query = await config.createQuery()
|
const query = await config.createQuery()
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/queries/${query._id}`)
|
.get(`/api/queries/${query._id}`)
|
||||||
.set(await config.roleHeaders())
|
.set(await config.roleHeaders({}))
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(res.body.fields).toBeUndefined()
|
expect(res.body.fields).toBeUndefined()
|
||||||
|
|
|
@ -35,7 +35,11 @@ describe("/routing", () => {
|
||||||
})
|
})
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/routing/client`)
|
.get(`/api/routing/client`)
|
||||||
.set(await config.roleHeaders("basic@test.com", BUILTIN_ROLE_IDS.BASIC))
|
.set(await config.roleHeaders({
|
||||||
|
email: "basic@test.com",
|
||||||
|
roleId: BUILTIN_ROLE_IDS.BASIC,
|
||||||
|
builder: false
|
||||||
|
}))
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(res.body.routes).toBeDefined()
|
expect(res.body.routes).toBeDefined()
|
||||||
|
@ -59,7 +63,11 @@ describe("/routing", () => {
|
||||||
})
|
})
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/routing/client`)
|
.get(`/api/routing/client`)
|
||||||
.set(await config.roleHeaders("basic@test.com", BUILTIN_ROLE_IDS.POWER))
|
.set(await config.roleHeaders({
|
||||||
|
email: "basic@test.com",
|
||||||
|
roleId: BUILTIN_ROLE_IDS.POWER,
|
||||||
|
builder: false,
|
||||||
|
}))
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(res.body.routes).toBeDefined()
|
expect(res.body.routes).toBeDefined()
|
||||||
|
|
|
@ -5,7 +5,9 @@ const { basicUser } = setup.structures
|
||||||
const workerRequests = require("../../../utilities/workerRequests")
|
const workerRequests = require("../../../utilities/workerRequests")
|
||||||
|
|
||||||
jest.mock("../../../utilities/workerRequests", () => ({
|
jest.mock("../../../utilities/workerRequests", () => ({
|
||||||
getGlobalUsers: jest.fn(),
|
getGlobalUsers: jest.fn(() => {
|
||||||
|
return {}
|
||||||
|
}),
|
||||||
saveGlobalUser: jest.fn(() => {
|
saveGlobalUser: jest.fn(() => {
|
||||||
const uuid = require("uuid/v4")
|
const uuid = require("uuid/v4")
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -46,7 +46,10 @@ exports.createRequest = (request, method, url, body) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.checkBuilderEndpoint = async ({ config, method, url, body }) => {
|
exports.checkBuilderEndpoint = async ({ config, method, url, body }) => {
|
||||||
const headers = await config.login()
|
const headers = await config.login("test@test.com", "test", {
|
||||||
|
userId: "us_fail",
|
||||||
|
builder: false,
|
||||||
|
})
|
||||||
await exports
|
await exports
|
||||||
.createRequest(config.request, method, url, body)
|
.createRequest(config.request, method, url, body)
|
||||||
.set(headers)
|
.set(headers)
|
||||||
|
@ -62,9 +65,10 @@ exports.checkPermissionsEndpoint = async ({
|
||||||
failRole,
|
failRole,
|
||||||
}) => {
|
}) => {
|
||||||
const password = "PASSWORD"
|
const password = "PASSWORD"
|
||||||
await config.createUser("passUser@budibase.com", password, passRole)
|
let user = await config.createUser("pass@budibase.com", password, passRole)
|
||||||
const passHeader = await config.login("passUser@budibase.com", password, {
|
const passHeader = await config.login("pass@budibase.com", password, {
|
||||||
roleId: passRole,
|
roleId: passRole,
|
||||||
|
userId: user.globalId,
|
||||||
})
|
})
|
||||||
|
|
||||||
await exports
|
await exports
|
||||||
|
@ -72,9 +76,10 @@ exports.checkPermissionsEndpoint = async ({
|
||||||
.set(passHeader)
|
.set(passHeader)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
await config.createUser("failUser@budibase.com", password, failRole)
|
user = await config.createUser("fail@budibase.com", password, failRole)
|
||||||
const failHeader = await config.login("failUser@budibase.com", password, {
|
const failHeader = await config.login("fail@budibase.com", password, {
|
||||||
roleId: failRole,
|
roleId: failRole,
|
||||||
|
userId: user.globalId,
|
||||||
})
|
})
|
||||||
|
|
||||||
await exports
|
await exports
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles")
|
const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles")
|
||||||
const { UserStatus } = require("@budibase/auth")
|
const { UserStatus } = require("@budibase/auth").constants
|
||||||
|
|
||||||
exports.LOGO_URL =
|
exports.LOGO_URL =
|
||||||
"https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg"
|
"https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg"
|
||||||
|
|
|
@ -107,8 +107,7 @@ exports.getRowParams = (tableId = null, rowId = null, otherProps = {}) => {
|
||||||
return getDocParams(DocumentTypes.ROW, null, otherProps)
|
return getDocParams(DocumentTypes.ROW, null, otherProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
const endOfKey =
|
const endOfKey = rowId == null ? `${tableId}${SEPARATOR}` : rowId
|
||||||
rowId == null ? `${tableId}${SEPARATOR}` : `${tableId}${SEPARATOR}${rowId}`
|
|
||||||
|
|
||||||
return getDocParams(DocumentTypes.ROW, endOfKey, otherProps)
|
return getDocParams(DocumentTypes.ROW, endOfKey, otherProps)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
const { getAppId, setCookie, getCookie, Cookies } = require("@budibase/auth")
|
const { getAppId, setCookie, getCookie } = require("@budibase/auth").utils
|
||||||
|
const { Cookies } = require("@budibase/auth").constants
|
||||||
const { getRole } = require("../utilities/security/roles")
|
const { getRole } = require("../utilities/security/roles")
|
||||||
const { getGlobalUsers } = require("../utilities/workerRequests")
|
const { getGlobalUsers } = require("../utilities/workerRequests")
|
||||||
const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles")
|
const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles")
|
||||||
const {
|
const { generateUserMetadataID } = require("../db/utils")
|
||||||
getGlobalIDFromUserMetadataID,
|
|
||||||
generateUserMetadataID,
|
|
||||||
} = require("../db/utils")
|
|
||||||
|
|
||||||
module.exports = async (ctx, next) => {
|
module.exports = async (ctx, next) => {
|
||||||
// try to get the appID from the request
|
// try to get the appID from the request
|
||||||
|
@ -30,8 +28,7 @@ module.exports = async (ctx, next) => {
|
||||||
appCookie.roleId === BUILTIN_ROLE_IDS.PUBLIC)
|
appCookie.roleId === BUILTIN_ROLE_IDS.PUBLIC)
|
||||||
) {
|
) {
|
||||||
// Different App ID means cookie needs reset, or if the same public user has logged in
|
// Different App ID means cookie needs reset, or if the same public user has logged in
|
||||||
const globalId = getGlobalIDFromUserMetadataID(ctx.user.userId)
|
const globalUser = await getGlobalUsers(ctx, requestAppId, ctx.user._id)
|
||||||
const globalUser = await getGlobalUsers(ctx, requestAppId, globalId)
|
|
||||||
updateCookie = true
|
updateCookie = true
|
||||||
appId = requestAppId
|
appId = requestAppId
|
||||||
if (globalUser.roles && globalUser.roles[requestAppId]) {
|
if (globalUser.roles && globalUser.roles[requestAppId]) {
|
||||||
|
@ -49,7 +46,7 @@ module.exports = async (ctx, next) => {
|
||||||
ctx.appId = appId
|
ctx.appId = appId
|
||||||
if (roleId) {
|
if (roleId) {
|
||||||
ctx.roleId = roleId
|
ctx.roleId = roleId
|
||||||
const userId = ctx.user ? generateUserMetadataID(ctx.user.userId) : null
|
const userId = ctx.user ? generateUserMetadataID(ctx.user._id) : null
|
||||||
ctx.user = {
|
ctx.user = {
|
||||||
...ctx.user,
|
...ctx.user,
|
||||||
// override userID with metadata one
|
// override userID with metadata one
|
||||||
|
|
|
@ -5,7 +5,7 @@ function mockWorker() {
|
||||||
jest.mock("../../utilities/workerRequests", () => ({
|
jest.mock("../../utilities/workerRequests", () => ({
|
||||||
getGlobalUsers: () => {
|
getGlobalUsers: () => {
|
||||||
return {
|
return {
|
||||||
email: "us_uuid1",
|
_id: "us_uuid1",
|
||||||
roles: {
|
roles: {
|
||||||
"app_test": "BASIC",
|
"app_test": "BASIC",
|
||||||
}
|
}
|
||||||
|
@ -23,10 +23,14 @@ function mockAuthWithNoCookie() {
|
||||||
jest.resetModules()
|
jest.resetModules()
|
||||||
mockWorker()
|
mockWorker()
|
||||||
jest.mock("@budibase/auth", () => ({
|
jest.mock("@budibase/auth", () => ({
|
||||||
|
utils: {
|
||||||
getAppId: jest.fn(),
|
getAppId: jest.fn(),
|
||||||
setCookie: jest.fn(),
|
setCookie: jest.fn(),
|
||||||
getCookie: jest.fn(),
|
getCookie: jest.fn(),
|
||||||
|
},
|
||||||
|
constants: {
|
||||||
Cookies: {},
|
Cookies: {},
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,15 +38,19 @@ function mockAuthWithCookie() {
|
||||||
jest.resetModules()
|
jest.resetModules()
|
||||||
mockWorker()
|
mockWorker()
|
||||||
jest.mock("@budibase/auth", () => ({
|
jest.mock("@budibase/auth", () => ({
|
||||||
|
utils: {
|
||||||
getAppId: () => {
|
getAppId: () => {
|
||||||
return "app_test"
|
return "app_test"
|
||||||
},
|
},
|
||||||
setCookie: jest.fn(),
|
setCookie: jest.fn(),
|
||||||
getCookie: () => ({ appId: "app_different", roleId: "PUBLIC" }),
|
getCookie: () => ({appId: "app_different", roleId: "PUBLIC"}),
|
||||||
|
},
|
||||||
|
constants: {
|
||||||
Cookies: {
|
Cookies: {
|
||||||
Auth: "auth",
|
Auth: "auth",
|
||||||
CurrentApp: "currentapp",
|
CurrentApp: "currentapp",
|
||||||
}
|
},
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +67,8 @@ class TestConfiguration {
|
||||||
|
|
||||||
setUser() {
|
setUser() {
|
||||||
this.ctx.user = {
|
this.ctx.user = {
|
||||||
userId: "ro_ta_user_us_uuid1",
|
userId: "us_uuid1",
|
||||||
|
_id: "us_uuid1",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +111,7 @@ describe("Current app middleware", () => {
|
||||||
async function checkExpected(setCookie) {
|
async function checkExpected(setCookie) {
|
||||||
config.setUser()
|
config.setUser()
|
||||||
await config.executeMiddleware()
|
await config.executeMiddleware()
|
||||||
const cookieFn = require("@budibase/auth").setCookie
|
const cookieFn = require("@budibase/auth").utils.setCookie
|
||||||
if (setCookie) {
|
if (setCookie) {
|
||||||
expect(cookieFn).toHaveBeenCalled()
|
expect(cookieFn).toHaveBeenCalled()
|
||||||
} else {
|
} else {
|
||||||
|
@ -122,12 +131,16 @@ describe("Current app middleware", () => {
|
||||||
it("should perform correct when no cookie exists", async () => {
|
it("should perform correct when no cookie exists", async () => {
|
||||||
mockReset()
|
mockReset()
|
||||||
jest.mock("@budibase/auth", () => ({
|
jest.mock("@budibase/auth", () => ({
|
||||||
|
utils: {
|
||||||
getAppId: () => {
|
getAppId: () => {
|
||||||
return "app_test"
|
return "app_test"
|
||||||
},
|
},
|
||||||
setCookie: jest.fn(),
|
setCookie: jest.fn(),
|
||||||
getCookie: jest.fn(),
|
getCookie: jest.fn(),
|
||||||
|
},
|
||||||
|
constants: {
|
||||||
Cookies: {},
|
Cookies: {},
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
await checkExpected(true)
|
await checkExpected(true)
|
||||||
})
|
})
|
||||||
|
@ -135,15 +148,16 @@ describe("Current app middleware", () => {
|
||||||
it("lastly check what occurs when cookie doesn't need updated", async () => {
|
it("lastly check what occurs when cookie doesn't need updated", async () => {
|
||||||
mockReset()
|
mockReset()
|
||||||
jest.mock("@budibase/auth", () => ({
|
jest.mock("@budibase/auth", () => ({
|
||||||
|
utils: {
|
||||||
getAppId: () => {
|
getAppId: () => {
|
||||||
return "app_test"
|
return "app_test"
|
||||||
},
|
},
|
||||||
setCookie: jest.fn(),
|
setCookie: jest.fn(),
|
||||||
getCookie: () => ({ appId: "app_test", roleId: "BASIC" }),
|
getCookie: () => ({appId: "app_test", roleId: "BASIC"}),
|
||||||
Cookies: {},
|
},
|
||||||
|
constants: { Cookies: {} },
|
||||||
}))
|
}))
|
||||||
await checkExpected(false)
|
await checkExpected(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
|
@ -1,5 +1,4 @@
|
||||||
const { BUILTIN_ROLE_IDS } = require("../../utilities/security/roles")
|
const { BUILTIN_ROLE_IDS } = require("../../utilities/security/roles")
|
||||||
const jwt = require("jsonwebtoken")
|
|
||||||
const env = require("../../environment")
|
const env = require("../../environment")
|
||||||
const {
|
const {
|
||||||
basicTable,
|
basicTable,
|
||||||
|
@ -15,8 +14,12 @@ const {
|
||||||
const controllers = require("./controllers")
|
const controllers = require("./controllers")
|
||||||
const supertest = require("supertest")
|
const supertest = require("supertest")
|
||||||
const { cleanup } = require("../../utilities/fileSystem")
|
const { cleanup } = require("../../utilities/fileSystem")
|
||||||
const { Cookies } = require("@budibase/auth")
|
const { Cookies } = require("@budibase/auth").constants
|
||||||
|
const { jwt } = require("@budibase/auth").auth
|
||||||
|
const { StaticDatabases } = require("@budibase/auth").db
|
||||||
|
const CouchDB = require("../../db")
|
||||||
|
|
||||||
|
const GLOBAL_USER_ID = "us_uuid1"
|
||||||
const EMAIL = "babs@babs.com"
|
const EMAIL = "babs@babs.com"
|
||||||
const PASSWORD = "babs_password"
|
const PASSWORD = "babs_password"
|
||||||
|
|
||||||
|
@ -47,6 +50,7 @@ class TestConfiguration {
|
||||||
request.config = { jwtSecret: env.JWT_SECRET }
|
request.config = { jwtSecret: env.JWT_SECRET }
|
||||||
request.appId = this.appId
|
request.appId = this.appId
|
||||||
request.user = { appId: this.appId }
|
request.user = { appId: this.appId }
|
||||||
|
request.query = {}
|
||||||
request.request = {
|
request.request = {
|
||||||
body: config,
|
body: config,
|
||||||
}
|
}
|
||||||
|
@ -57,7 +61,27 @@ class TestConfiguration {
|
||||||
return request.body
|
return request.body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async globalUser(id = GLOBAL_USER_ID, builder = true) {
|
||||||
|
const db = new CouchDB(StaticDatabases.GLOBAL.name)
|
||||||
|
let existing
|
||||||
|
try {
|
||||||
|
existing = await db.get(id)
|
||||||
|
} catch (err) {
|
||||||
|
existing = {}
|
||||||
|
}
|
||||||
|
const user = {
|
||||||
|
_id: id,
|
||||||
|
...existing,
|
||||||
|
roles: {},
|
||||||
|
}
|
||||||
|
if (builder) {
|
||||||
|
user.builder = { global: true }
|
||||||
|
}
|
||||||
|
await db.put(user)
|
||||||
|
}
|
||||||
|
|
||||||
async init(appName = "test_application") {
|
async init(appName = "test_application") {
|
||||||
|
await this.globalUser()
|
||||||
return this.createApp(appName)
|
return this.createApp(appName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,17 +93,14 @@ class TestConfiguration {
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultHeaders() {
|
defaultHeaders() {
|
||||||
const user = {
|
const auth = {
|
||||||
userId: "ro_ta_user_us_uuid1",
|
userId: GLOBAL_USER_ID,
|
||||||
builder: {
|
|
||||||
global: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
const app = {
|
const app = {
|
||||||
roleId: BUILTIN_ROLE_IDS.BUILDER,
|
roleId: BUILTIN_ROLE_IDS.BUILDER,
|
||||||
appId: this.appId,
|
appId: this.appId,
|
||||||
}
|
}
|
||||||
const authToken = jwt.sign(user, env.JWT_SECRET)
|
const authToken = jwt.sign(auth, env.JWT_SECRET)
|
||||||
const appToken = jwt.sign(app, env.JWT_SECRET)
|
const appToken = jwt.sign(app, env.JWT_SECRET)
|
||||||
const headers = {
|
const headers = {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
|
@ -104,14 +125,18 @@ class TestConfiguration {
|
||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
||||||
async roleHeaders(email = EMAIL, roleId = BUILTIN_ROLE_IDS.ADMIN) {
|
async roleHeaders({
|
||||||
|
email = EMAIL,
|
||||||
|
roleId = BUILTIN_ROLE_IDS.ADMIN,
|
||||||
|
builder = false,
|
||||||
|
}) {
|
||||||
let user
|
let user
|
||||||
try {
|
try {
|
||||||
user = await this.createUser(email, PASSWORD, roleId)
|
user = await this.createUser(email, PASSWORD, roleId)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// allow errors here
|
// allow errors here
|
||||||
}
|
}
|
||||||
return this.login(email, PASSWORD, { roleId, userId: user._id })
|
return this.login(email, PASSWORD, { roleId, userId: user._id, builder })
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApp(appName) {
|
async createApp(appName) {
|
||||||
|
@ -282,7 +307,9 @@ class TestConfiguration {
|
||||||
password = PASSWORD,
|
password = PASSWORD,
|
||||||
roleId = BUILTIN_ROLE_IDS.POWER
|
roleId = BUILTIN_ROLE_IDS.POWER
|
||||||
) {
|
) {
|
||||||
return this._req(
|
const globalId = `us_${Math.random()}`
|
||||||
|
await this.globalUser(globalId, roleId === BUILTIN_ROLE_IDS.BUILDER)
|
||||||
|
const user = await this._req(
|
||||||
{
|
{
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
|
@ -291,28 +318,34 @@ class TestConfiguration {
|
||||||
null,
|
null,
|
||||||
controllers.user.createMetadata
|
controllers.user.createMetadata
|
||||||
)
|
)
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
globalId,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(email, password, { roleId, userId } = {}) {
|
async login(email, password, { roleId, userId, builder } = {}) {
|
||||||
if (!roleId) {
|
roleId = !roleId ? BUILTIN_ROLE_IDS.BUILDER : roleId
|
||||||
roleId = BUILTIN_ROLE_IDS.BUILDER
|
userId = !userId ? `us_uuid1` : userId
|
||||||
}
|
|
||||||
if (!this.request) {
|
if (!this.request) {
|
||||||
throw "Server has not been opened, cannot login."
|
throw "Server has not been opened, cannot login."
|
||||||
}
|
}
|
||||||
|
// make sure the user exists in the global DB
|
||||||
|
if (roleId !== BUILTIN_ROLE_IDS.PUBLIC) {
|
||||||
|
await this.globalUser(userId, builder)
|
||||||
|
}
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
await this.createUser()
|
await this.createUser()
|
||||||
}
|
}
|
||||||
// have to fake this
|
// have to fake this
|
||||||
const user = {
|
const auth = {
|
||||||
userId: userId || `us_uuid1`,
|
userId,
|
||||||
email: email || EMAIL,
|
|
||||||
}
|
}
|
||||||
const app = {
|
const app = {
|
||||||
roleId: roleId,
|
roleId: roleId,
|
||||||
appId: this.appId,
|
appId: this.appId,
|
||||||
}
|
}
|
||||||
const authToken = jwt.sign(user, env.JWT_SECRET)
|
const authToken = jwt.sign(auth, env.JWT_SECRET)
|
||||||
const appToken = jwt.sign(app, env.JWT_SECRET)
|
const appToken = jwt.sign(app, env.JWT_SECRET)
|
||||||
|
|
||||||
// returning necessary request headers
|
// returning necessary request headers
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Start Server",
|
||||||
|
"program": "${workspaceFolder}/src/index.js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Jest - All",
|
||||||
|
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||||
|
"args": [],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen",
|
||||||
|
"disableOptimisticBPs": true,
|
||||||
|
"windows": {
|
||||||
|
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Jest - Users",
|
||||||
|
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||||
|
"args": ["user.spec", "--runInBand"],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen",
|
||||||
|
"disableOptimisticBPs": true,
|
||||||
|
"windows": {
|
||||||
|
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Jest - Instances",
|
||||||
|
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||||
|
"args": ["instance.spec", "--runInBand"],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen",
|
||||||
|
"disableOptimisticBPs": true,
|
||||||
|
"windows": {
|
||||||
|
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Jest - Roles",
|
||||||
|
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||||
|
"args": ["role.spec", "--runInBand"],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen",
|
||||||
|
"disableOptimisticBPs": true,
|
||||||
|
"windows": {
|
||||||
|
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Jest - Records",
|
||||||
|
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||||
|
"args": ["record.spec", "--runInBand"],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen",
|
||||||
|
"disableOptimisticBPs": true,
|
||||||
|
"windows": {
|
||||||
|
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Jest - Models",
|
||||||
|
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||||
|
"args": ["table.spec", "--runInBand"],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen",
|
||||||
|
"disableOptimisticBPs": true,
|
||||||
|
"windows": {
|
||||||
|
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Jest - Views",
|
||||||
|
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||||
|
"args": ["view.spec", "--runInBand"],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen",
|
||||||
|
"disableOptimisticBPs": true,
|
||||||
|
"windows": {
|
||||||
|
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Jest - Applications",
|
||||||
|
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||||
|
"args": ["application.spec", "--runInBand"],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen",
|
||||||
|
"disableOptimisticBPs": true,
|
||||||
|
"windows": {
|
||||||
|
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Jest Builder",
|
||||||
|
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||||
|
"args": ["builder", "--runInBand"],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen",
|
||||||
|
"disableOptimisticBPs": true,
|
||||||
|
"windows": {
|
||||||
|
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Initialise Budibase",
|
||||||
|
"program": "yarn",
|
||||||
|
"args": ["run", "initialise"],
|
||||||
|
"console": "externalTerminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -20,6 +20,7 @@
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/auth": "0.0.1",
|
"@budibase/auth": "0.0.1",
|
||||||
|
"@budibase/string-templates": "^0.8.16",
|
||||||
"@koa/router": "^8.0.0",
|
"@koa/router": "^8.0.0",
|
||||||
"aws-sdk": "^2.811.0",
|
"aws-sdk": "^2.811.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
"koa-session": "^5.12.0",
|
"koa-session": "^5.12.0",
|
||||||
"koa-static": "^5.0.0",
|
"koa-static": "^5.0.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
|
"nodemailer": "^6.5.0",
|
||||||
"passport-google-oauth": "^2.0.0",
|
"passport-google-oauth": "^2.0.0",
|
||||||
"passport-jwt": "^4.0.0",
|
"passport-jwt": "^4.0.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
|
@ -44,7 +46,9 @@
|
||||||
"server-destroy": "^1.0.1"
|
"server-destroy": "^1.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"jest": "^26.6.3",
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.7",
|
||||||
"pouchdb-adapter-memory": "^7.2.2"
|
"pouchdb-adapter-memory": "^7.2.2",
|
||||||
|
"supertest": "^6.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
const CouchDB = require("../../../db")
|
||||||
|
const {
|
||||||
|
generateConfigID,
|
||||||
|
StaticDatabases,
|
||||||
|
getConfigParams,
|
||||||
|
determineScopedConfig,
|
||||||
|
} = require("@budibase/auth").db
|
||||||
|
const { Configs } = require("../../../constants")
|
||||||
|
const email = require("../../../utilities/email")
|
||||||
|
|
||||||
|
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
||||||
|
|
||||||
|
exports.save = async function(ctx) {
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
const { type, config } = ctx.request.body
|
||||||
|
const { group, user } = config
|
||||||
|
// insert the type into the doc
|
||||||
|
config.type = type
|
||||||
|
|
||||||
|
// Config does not exist yet
|
||||||
|
if (!config._id) {
|
||||||
|
config._id = generateConfigID({
|
||||||
|
type,
|
||||||
|
group,
|
||||||
|
user,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify the configuration
|
||||||
|
switch (type) {
|
||||||
|
case Configs.SMTP:
|
||||||
|
await email.verifyConfig(config)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await db.put(config)
|
||||||
|
ctx.body = {
|
||||||
|
type,
|
||||||
|
_id: response.id,
|
||||||
|
_rev: response.rev,
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
ctx.throw(err.status, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.fetch = async function(ctx) {
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
const response = await db.allDocs(
|
||||||
|
getConfigParams(undefined, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
ctx.body = response.rows.map(row => row.doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the most granular config for a particular configuration type.
|
||||||
|
* The hierarchy is type -> group -> user.
|
||||||
|
*/
|
||||||
|
exports.find = async function(ctx) {
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
const userId = ctx.params.user && ctx.params.user._id
|
||||||
|
|
||||||
|
const { group } = ctx.query
|
||||||
|
if (group) {
|
||||||
|
const group = await db.get(group)
|
||||||
|
const userInGroup = group.users.some(groupUser => groupUser === userId)
|
||||||
|
if (!ctx.user.admin && !userInGroup) {
|
||||||
|
ctx.throw(400, `User is not in specified group: ${group}.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the config with the most granular scope based on context
|
||||||
|
const scopedConfig = await determineScopedConfig(db, {
|
||||||
|
type: ctx.params.type,
|
||||||
|
user: userId,
|
||||||
|
group,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (scopedConfig) {
|
||||||
|
ctx.body = scopedConfig
|
||||||
|
} else {
|
||||||
|
ctx.throw(400, "No configuration exists.")
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
ctx.throw(err.status, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.destroy = async function(ctx) {
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
const { id, rev } = ctx.params
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.remove(id, rev)
|
||||||
|
ctx.body = { message: "Config deleted successfully" }
|
||||||
|
} catch (err) {
|
||||||
|
ctx.throw(err.status, err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
const CouchDB = require("../../../db")
|
||||||
|
const { StaticDatabases, determineScopedConfig } = require("@budibase/auth").db
|
||||||
|
const {
|
||||||
|
EmailTemplatePurpose,
|
||||||
|
TemplateTypes,
|
||||||
|
Configs,
|
||||||
|
} = require("../../../constants")
|
||||||
|
const { getTemplateByPurpose } = require("../../../constants/templates")
|
||||||
|
const { getSettingsTemplateContext } = require("../../../utilities/templates")
|
||||||
|
const { processString } = require("@budibase/string-templates")
|
||||||
|
const { createSMTPTransport } = require("../../../utilities/email")
|
||||||
|
|
||||||
|
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
||||||
|
const TYPE = TemplateTypes.EMAIL
|
||||||
|
|
||||||
|
const FULL_EMAIL_PURPOSES = [
|
||||||
|
EmailTemplatePurpose.INVITATION,
|
||||||
|
EmailTemplatePurpose.PASSWORD_RECOVERY,
|
||||||
|
EmailTemplatePurpose.WELCOME,
|
||||||
|
]
|
||||||
|
|
||||||
|
async function buildEmail(purpose, email, user) {
|
||||||
|
// this isn't a full email
|
||||||
|
if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) {
|
||||||
|
throw `Unable to build an email of type ${purpose}`
|
||||||
|
}
|
||||||
|
let [base, styles, body] = await Promise.all([
|
||||||
|
getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE),
|
||||||
|
getTemplateByPurpose(TYPE, EmailTemplatePurpose.STYLES),
|
||||||
|
getTemplateByPurpose(TYPE, purpose),
|
||||||
|
])
|
||||||
|
if (!base || !styles || !body) {
|
||||||
|
throw "Unable to build email, missing base components"
|
||||||
|
}
|
||||||
|
base = base.contents
|
||||||
|
styles = styles.contents
|
||||||
|
body = body.contents
|
||||||
|
|
||||||
|
// TODO: need to extend the context as much as possible
|
||||||
|
const context = {
|
||||||
|
...(await getSettingsTemplateContext()),
|
||||||
|
email,
|
||||||
|
user: user || {},
|
||||||
|
}
|
||||||
|
|
||||||
|
body = await processString(body, context)
|
||||||
|
styles = await processString(styles, context)
|
||||||
|
// this should now be the complete email HTML
|
||||||
|
return processString(base, {
|
||||||
|
...context,
|
||||||
|
styles,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.sendEmail = async ctx => {
|
||||||
|
const { groupId, email, userId, purpose } = ctx.request.body
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
const params = {}
|
||||||
|
if (groupId) {
|
||||||
|
params.group = groupId
|
||||||
|
}
|
||||||
|
params.type = Configs.SMTP
|
||||||
|
let user = {}
|
||||||
|
if (userId) {
|
||||||
|
user = db.get(userId)
|
||||||
|
}
|
||||||
|
const config = await determineScopedConfig(db, params)
|
||||||
|
if (!config) {
|
||||||
|
ctx.throw(400, "Unable to find SMTP configuration")
|
||||||
|
}
|
||||||
|
const transport = createSMTPTransport(config)
|
||||||
|
const message = {
|
||||||
|
from: config.from,
|
||||||
|
subject: config.subject,
|
||||||
|
to: email,
|
||||||
|
html: await buildEmail(purpose, email, user),
|
||||||
|
}
|
||||||
|
const response = await transport.sendMail(message)
|
||||||
|
ctx.body = {
|
||||||
|
...response,
|
||||||
|
message: `Email sent to ${email}.`,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
const CouchDB = require("../../../db")
|
const CouchDB = require("../../../db")
|
||||||
const { getGroupParams, StaticDatabases } = require("@budibase/auth")
|
const {
|
||||||
const { generateGroupID } = require("@budibase/auth")
|
getGroupParams,
|
||||||
|
generateGroupID,
|
||||||
|
StaticDatabases,
|
||||||
|
} = require("@budibase/auth").db
|
||||||
|
|
||||||
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
||||||
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
const users = require("./users")
|
|
||||||
const groups = require("./groups")
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
users,
|
|
||||||
groups,
|
|
||||||
}
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
const { generateTemplateID, StaticDatabases } = require("@budibase/auth").db
|
||||||
|
const { CouchDB } = require("../../../db")
|
||||||
|
const {
|
||||||
|
TemplateMetadata,
|
||||||
|
TemplateBindings,
|
||||||
|
GLOBAL_OWNER,
|
||||||
|
} = require("../../../constants")
|
||||||
|
const { getTemplates } = require("../../../constants/templates")
|
||||||
|
|
||||||
|
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
||||||
|
|
||||||
|
exports.save = async ctx => {
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
const type = ctx.params.type
|
||||||
|
let template = ctx.request.body
|
||||||
|
if (!template.ownerId) {
|
||||||
|
template.ownerId = GLOBAL_OWNER
|
||||||
|
}
|
||||||
|
if (!template._id) {
|
||||||
|
template._id = generateTemplateID(template.ownerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await db.put({
|
||||||
|
...template,
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
ctx.body = {
|
||||||
|
...template,
|
||||||
|
_rev: response.rev,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.definitions = async ctx => {
|
||||||
|
ctx.body = {
|
||||||
|
purpose: TemplateMetadata,
|
||||||
|
bindings: Object.values(TemplateBindings),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.fetch = async ctx => {
|
||||||
|
ctx.body = await getTemplates()
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.fetchByType = async ctx => {
|
||||||
|
ctx.body = await getTemplates({
|
||||||
|
type: ctx.params.type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.fetchByOwner = async ctx => {
|
||||||
|
ctx.body = await getTemplates({
|
||||||
|
ownerId: ctx.params.ownerId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.find = async ctx => {
|
||||||
|
ctx.body = await getTemplates({
|
||||||
|
id: ctx.params.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.destroy = async ctx => {
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
await db.remove(ctx.params.id, ctx.params.rev)
|
||||||
|
ctx.message = `Template ${ctx.params.id} deleted.`
|
||||||
|
ctx.status = 200
|
||||||
|
}
|
|
@ -1,18 +1,17 @@
|
||||||
const CouchDB = require("../../../db")
|
const CouchDB = require("../../../db")
|
||||||
const {
|
const {
|
||||||
hash,
|
|
||||||
generateGlobalUserID,
|
generateGlobalUserID,
|
||||||
getGlobalUserParams,
|
getGlobalUserParams,
|
||||||
StaticDatabases,
|
StaticDatabases,
|
||||||
getGlobalUserByEmail,
|
} = require("@budibase/auth").db
|
||||||
} = require("@budibase/auth")
|
const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils
|
||||||
const { UserStatus } = require("../../../constants")
|
const { UserStatus } = require("../../../constants")
|
||||||
|
|
||||||
const FIRST_USER_EMAIL = "test@test.com"
|
const FIRST_USER_EMAIL = "test@test.com"
|
||||||
const FIRST_USER_PASSWORD = "test"
|
const FIRST_USER_PASSWORD = "test"
|
||||||
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
||||||
|
|
||||||
exports.userSave = async ctx => {
|
exports.save = async ctx => {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
const { email, password, _id } = ctx.request.body
|
const { email, password, _id } = ctx.request.body
|
||||||
|
|
||||||
|
@ -70,10 +69,10 @@ exports.firstUser = async ctx => {
|
||||||
global: true,
|
global: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
await exports.userSave(ctx)
|
await exports.save(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.userDelete = async ctx => {
|
exports.destroy = async ctx => {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
const dbUser = await db.get(ctx.params.id)
|
const dbUser = await db.get(ctx.params.id)
|
||||||
await db.remove(dbUser._id, dbUser._rev)
|
await db.remove(dbUser._id, dbUser._rev)
|
||||||
|
@ -83,7 +82,7 @@ exports.userDelete = async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// called internally by app server user fetch
|
// called internally by app server user fetch
|
||||||
exports.userFetch = async ctx => {
|
exports.fetch = async ctx => {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
const response = await db.allDocs(
|
const response = await db.allDocs(
|
||||||
getGlobalUserParams(null, {
|
getGlobalUserParams(null, {
|
||||||
|
@ -101,7 +100,7 @@ exports.userFetch = async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// called internally by app server user find
|
// called internally by app server user find
|
||||||
exports.userFind = async ctx => {
|
exports.find = async ctx => {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
let user
|
let user
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
const { passport, Cookies, clearCookie } = require("@budibase/auth")
|
const authPkg = require("@budibase/auth")
|
||||||
|
const { google } = require("@budibase/auth/src/middleware")
|
||||||
|
const { Configs } = require("../../constants")
|
||||||
|
const CouchDB = require("../../db")
|
||||||
|
const { clearCookie } = authPkg.utils
|
||||||
|
const { Cookies } = authPkg.constants
|
||||||
|
const { passport } = authPkg.auth
|
||||||
|
|
||||||
|
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
|
||||||
|
|
||||||
exports.authenticate = async (ctx, next) => {
|
exports.authenticate = async (ctx, next) => {
|
||||||
return passport.authenticate("local", async (err, user) => {
|
return passport.authenticate("local", async (err, user) => {
|
||||||
|
@ -31,10 +39,55 @@ exports.logout = async ctx => {
|
||||||
ctx.body = { message: "User logged out" }
|
ctx.body = { message: "User logged out" }
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.googleAuth = async () => {
|
/**
|
||||||
// return passport.authenticate("google")
|
* The initial call that google authentication makes to take you to the google login screen.
|
||||||
|
* On a successful login, you will be redirected to the googleAuth callback route.
|
||||||
|
*/
|
||||||
|
exports.googlePreAuth = async (ctx, next) => {
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
const config = await authPkg.db.determineScopedConfig(db, {
|
||||||
|
type: Configs.GOOGLE,
|
||||||
|
group: ctx.query.group,
|
||||||
|
})
|
||||||
|
const strategy = await google.strategyFactory(config)
|
||||||
|
|
||||||
|
return passport.authenticate(strategy, {
|
||||||
|
scope: ["profile", "email"],
|
||||||
|
})(ctx, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.googleAuth = async () => {
|
exports.googleAuth = async (ctx, next) => {
|
||||||
// return passport.authenticate("google")
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
|
||||||
|
const config = await authPkg.db.determineScopedConfig(db, {
|
||||||
|
type: Configs.GOOGLE,
|
||||||
|
group: ctx.query.group,
|
||||||
|
})
|
||||||
|
const strategy = await google.strategyFactory(config)
|
||||||
|
|
||||||
|
return passport.authenticate(
|
||||||
|
strategy,
|
||||||
|
{ successRedirect: "/", failureRedirect: "/error" },
|
||||||
|
async (err, user) => {
|
||||||
|
if (err) {
|
||||||
|
return ctx.throw(403, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
const expires = new Date()
|
||||||
|
expires.setDate(expires.getDate() + 1)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return ctx.throw(403, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.cookies.set(Cookies.Auth, user.token, {
|
||||||
|
expires,
|
||||||
|
path: "/",
|
||||||
|
httpOnly: false,
|
||||||
|
overwrite: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.redirect("/")
|
||||||
|
}
|
||||||
|
)(ctx, next)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,14 @@ const Router = require("@koa/router")
|
||||||
const compress = require("koa-compress")
|
const compress = require("koa-compress")
|
||||||
const zlib = require("zlib")
|
const zlib = require("zlib")
|
||||||
const { routes } = require("./routes")
|
const { routes } = require("./routes")
|
||||||
|
const { buildAuthMiddleware } = require("@budibase/auth").auth
|
||||||
|
|
||||||
|
const NO_AUTH_ENDPOINTS = [
|
||||||
|
"/api/admin/users/first",
|
||||||
|
"/api/admin/auth",
|
||||||
|
"/api/admin/auth/google",
|
||||||
|
"/api/admin/auth/google/callback",
|
||||||
|
]
|
||||||
|
|
||||||
const router = new Router()
|
const router = new Router()
|
||||||
|
|
||||||
|
@ -19,6 +27,14 @@ router
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.use("/health", ctx => (ctx.status = 200))
|
.use("/health", ctx => (ctx.status = 200))
|
||||||
|
.use(buildAuthMiddleware(NO_AUTH_ENDPOINTS))
|
||||||
|
// for now no public access is allowed to worker (bar health check)
|
||||||
|
.use((ctx, next) => {
|
||||||
|
if (!ctx.isAuthenticated) {
|
||||||
|
ctx.throw(403, "Unauthorized - no public worker access")
|
||||||
|
}
|
||||||
|
return next()
|
||||||
|
})
|
||||||
|
|
||||||
// error handling middleware
|
// error handling middleware
|
||||||
router.use(async (ctx, next) => {
|
router.use(async (ctx, next) => {
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
const Router = require("@koa/router")
|
||||||
|
const controller = require("../../controllers/admin/configs")
|
||||||
|
const joiValidator = require("../../../middleware/joi-validator")
|
||||||
|
const Joi = require("joi")
|
||||||
|
const { Configs } = require("../../../constants")
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
|
||||||
|
function smtpValidation() {
|
||||||
|
// prettier-ignore
|
||||||
|
return Joi.object({
|
||||||
|
port: Joi.number().required(),
|
||||||
|
host: Joi.string().required(),
|
||||||
|
from: Joi.string().email().required(),
|
||||||
|
secure: Joi.boolean().optional(),
|
||||||
|
selfSigned: Joi.boolean().optional(),
|
||||||
|
auth: Joi.object({
|
||||||
|
type: Joi.string().valid("login", "oauth2", null),
|
||||||
|
user: Joi.string().required(),
|
||||||
|
pass: Joi.string().valid("", null),
|
||||||
|
}).optional(),
|
||||||
|
}).unknown(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function settingValidation() {
|
||||||
|
// prettier-ignore
|
||||||
|
return Joi.object({
|
||||||
|
platformUrl: Joi.string().valid("", null),
|
||||||
|
logoUrl: Joi.string().valid("", null),
|
||||||
|
docsUrl: Joi.string().valid("", null),
|
||||||
|
company: Joi.string().required(),
|
||||||
|
}).unknown(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function googleValidation() {
|
||||||
|
// prettier-ignore
|
||||||
|
return Joi.object({
|
||||||
|
clientID: Joi.string().required(),
|
||||||
|
clientSecret: Joi.string().required(),
|
||||||
|
callbackURL: Joi.string().required(),
|
||||||
|
}).unknown(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConfigSaveValidation() {
|
||||||
|
// prettier-ignore
|
||||||
|
return joiValidator.body(Joi.object({
|
||||||
|
type: Joi.string().valid(...Object.values(Configs)).required(),
|
||||||
|
config: Joi.alternatives()
|
||||||
|
.conditional("type", {
|
||||||
|
switch: [
|
||||||
|
{ is: Configs.SMTP, then: smtpValidation() },
|
||||||
|
{ is: Configs.SETTINGS, then: settingValidation() },
|
||||||
|
{ is: Configs.ACCOUNT, then: Joi.object().unknown(true) },
|
||||||
|
{ is: Configs.GOOGLE, then: googleValidation() }
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
router
|
||||||
|
.post("/api/admin/configs", buildConfigSaveValidation(), controller.save)
|
||||||
|
.delete("/api/admin/configs/:id", controller.destroy)
|
||||||
|
.get("/api/admin/configs", controller.fetch)
|
||||||
|
.get("/api/admin/configs/:type", controller.find)
|
||||||
|
|
||||||
|
module.exports = router
|
|
@ -0,0 +1,24 @@
|
||||||
|
const Router = require("@koa/router")
|
||||||
|
const controller = require("../../controllers/admin/email")
|
||||||
|
const { EmailTemplatePurpose } = require("../../../constants")
|
||||||
|
const joiValidator = require("../../../middleware/joi-validator")
|
||||||
|
const Joi = require("joi")
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
|
||||||
|
function buildEmailSendValidation() {
|
||||||
|
// prettier-ignore
|
||||||
|
return joiValidator.body(Joi.object({
|
||||||
|
email: Joi.string().email(),
|
||||||
|
groupId: Joi.string().allow("", null),
|
||||||
|
purpose: Joi.string().allow(...Object.values(EmailTemplatePurpose)),
|
||||||
|
}).required().unknown(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/api/admin/email/send",
|
||||||
|
buildEmailSendValidation(),
|
||||||
|
controller.sendEmail
|
||||||
|
)
|
||||||
|
|
||||||
|
module.exports = router
|
|
@ -1,7 +1,6 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const controller = require("../../controllers/admin/groups")
|
const controller = require("../../controllers/admin/groups")
|
||||||
const joiValidator = require("../../../middleware/joi-validator")
|
const joiValidator = require("../../../middleware/joi-validator")
|
||||||
const { authenticated } = require("@budibase/auth")
|
|
||||||
const Joi = require("joi")
|
const Joi = require("joi")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
@ -25,14 +24,9 @@ function buildGroupSaveValidation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
router
|
router
|
||||||
.post(
|
.post("/api/admin/groups", buildGroupSaveValidation(), controller.save)
|
||||||
"/api/admin/groups",
|
.get("/api/admin/groups", controller.fetch)
|
||||||
buildGroupSaveValidation(),
|
.delete("/api/admin/groups/:id", controller.destroy)
|
||||||
authenticated,
|
.get("/api/admin/groups/:id", controller.find)
|
||||||
controller.save
|
|
||||||
)
|
|
||||||
.delete("/api/admin/groups/:id", authenticated, controller.destroy)
|
|
||||||
.get("/api/admin/groups", authenticated, controller.fetch)
|
|
||||||
.get("/api/admin/groups/:id", authenticated, controller.find)
|
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
const Router = require("@koa/router")
|
||||||
|
const controller = require("../../controllers/admin/templates")
|
||||||
|
const joiValidator = require("../../../middleware/joi-validator")
|
||||||
|
const Joi = require("joi")
|
||||||
|
const { TemplatePurpose, TemplateTypes } = require("../../../constants")
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
|
||||||
|
function buildTemplateSaveValidation() {
|
||||||
|
// prettier-ignore
|
||||||
|
return joiValidator.body(Joi.object({
|
||||||
|
_id: Joi.string().allow(null, ""),
|
||||||
|
_rev: Joi.string().allow(null, ""),
|
||||||
|
ownerId: Joi.string().allow(null, ""),
|
||||||
|
name: Joi.string().allow(null, ""),
|
||||||
|
contents: Joi.string().required(),
|
||||||
|
purpose: Joi.string().required().valid(...Object.values(TemplatePurpose)),
|
||||||
|
type: Joi.string().required().valid(...Object.values(TemplateTypes)),
|
||||||
|
}).required().unknown(true).optional())
|
||||||
|
}
|
||||||
|
|
||||||
|
router
|
||||||
|
.get("/api/admin/template/definitions", controller.definitions)
|
||||||
|
.post("/api/admin/template", buildTemplateSaveValidation(), controller.save)
|
||||||
|
.get("/api/admin/template", controller.fetch)
|
||||||
|
.get("/api/admin/template/:type", controller.fetchByType)
|
||||||
|
.get("/api/admin/template/:ownerId", controller.fetchByOwner)
|
||||||
|
.get("/api/admin/template/:id", controller.find)
|
||||||
|
.delete("/api/admin/template/:id/:rev", controller.destroy)
|
||||||
|
|
||||||
|
module.exports = router
|
|
@ -1,7 +1,6 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const controller = require("../../controllers/admin/users")
|
const controller = require("../../controllers/admin/users")
|
||||||
const joiValidator = require("../../../middleware/joi-validator")
|
const joiValidator = require("../../../middleware/joi-validator")
|
||||||
const { authenticated } = require("@budibase/auth")
|
|
||||||
const Joi = require("joi")
|
const Joi = require("joi")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
@ -26,15 +25,10 @@ function buildUserSaveValidation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
router
|
router
|
||||||
.post(
|
.post("/api/admin/users", buildUserSaveValidation(), controller.save)
|
||||||
"/api/admin/users",
|
.get("/api/admin/users", controller.fetch)
|
||||||
buildUserSaveValidation(),
|
|
||||||
authenticated,
|
|
||||||
controller.userSave
|
|
||||||
)
|
|
||||||
.post("/api/admin/users/first", controller.firstUser)
|
.post("/api/admin/users/first", controller.firstUser)
|
||||||
.delete("/api/admin/users/:id", authenticated, controller.userDelete)
|
.delete("/api/admin/users/:id", controller.destroy)
|
||||||
.get("/api/admin/users", authenticated, controller.userFetch)
|
.get("/api/admin/users/:id", controller.find)
|
||||||
.get("/api/admin/users/:id", authenticated, controller.userFind)
|
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const controller = require("../controllers/app")
|
const controller = require("../controllers/app")
|
||||||
const { authenticated } = require("@budibase/auth")
|
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router.get("/api/apps", authenticated, controller.getApps)
|
router.get("/api/apps", controller.getApps)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -1,19 +1,12 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const { passport } = require("@budibase/auth")
|
|
||||||
const authController = require("../controllers/auth")
|
const authController = require("../controllers/auth")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router
|
router
|
||||||
.post("/api/admin/auth", authController.authenticate)
|
.post("/api/admin/auth", authController.authenticate)
|
||||||
|
.get("/api/admin/auth/google", authController.googlePreAuth)
|
||||||
|
.get("/api/admin/auth/google/callback", authController.googleAuth)
|
||||||
.post("/api/admin/auth/logout", authController.logout)
|
.post("/api/admin/auth/logout", authController.logout)
|
||||||
.get("/api/auth/google", passport.authenticate("google"))
|
|
||||||
.get(
|
|
||||||
"/api/auth/google/callback",
|
|
||||||
passport.authenticate("google", {
|
|
||||||
successRedirect: "/app",
|
|
||||||
failureRedirect: "/",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
const userRoutes = require("./admin/users")
|
const userRoutes = require("./admin/users")
|
||||||
|
const configRoutes = require("./admin/configs")
|
||||||
const groupRoutes = require("./admin/groups")
|
const groupRoutes = require("./admin/groups")
|
||||||
|
const templateRoutes = require("./admin/templates")
|
||||||
|
const emailRoutes = require("./admin/email")
|
||||||
const authRoutes = require("./auth")
|
const authRoutes = require("./auth")
|
||||||
const appRoutes = require("./app")
|
const appRoutes = require("./app")
|
||||||
|
|
||||||
exports.routes = [userRoutes, groupRoutes, authRoutes, appRoutes]
|
exports.routes = [
|
||||||
|
configRoutes,
|
||||||
|
userRoutes,
|
||||||
|
groupRoutes,
|
||||||
|
authRoutes,
|
||||||
|
appRoutes,
|
||||||
|
templateRoutes,
|
||||||
|
emailRoutes,
|
||||||
|
]
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
const setup = require("./utilities")
|
||||||
|
const { EmailTemplatePurpose } = require("../../../constants")
|
||||||
|
|
||||||
|
// mock the email system
|
||||||
|
const sendMailMock = jest.fn()
|
||||||
|
jest.mock("nodemailer")
|
||||||
|
const nodemailer = require("nodemailer")
|
||||||
|
nodemailer.createTransport.mockReturnValue({
|
||||||
|
sendMail: sendMailMock,
|
||||||
|
verify: jest.fn()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("/api/admin/email", () => {
|
||||||
|
let request = setup.getRequest()
|
||||||
|
let config = setup.getConfig()
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(setup.afterAll)
|
||||||
|
|
||||||
|
it("should be able to send an email (with mocking)", async () => {
|
||||||
|
// initially configure settings
|
||||||
|
await config.saveSmtpConfig()
|
||||||
|
await config.saveSettingsConfig()
|
||||||
|
const res = await request
|
||||||
|
.post(`/api/admin/email/send`)
|
||||||
|
.send({
|
||||||
|
email: "test@test.com",
|
||||||
|
purpose: EmailTemplatePurpose.INVITATION,
|
||||||
|
})
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
expect(res.body.message).toBeDefined()
|
||||||
|
expect(sendMailMock).toHaveBeenCalled()
|
||||||
|
const emailCall = sendMailMock.mock.calls[0][0]
|
||||||
|
expect(emailCall.subject).toBe("Hello!")
|
||||||
|
expect(emailCall.html).not.toContain("Invalid Binding")
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,60 @@
|
||||||
|
const setup = require("./utilities")
|
||||||
|
const { EmailTemplatePurpose } = require("../../../constants")
|
||||||
|
const nodemailer = require("nodemailer")
|
||||||
|
const fetch = require("node-fetch")
|
||||||
|
|
||||||
|
describe("/api/admin/email", () => {
|
||||||
|
let request = setup.getRequest()
|
||||||
|
let config = setup.getConfig()
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(setup.afterAll)
|
||||||
|
|
||||||
|
async function sendRealEmail(purpose) {
|
||||||
|
await config.saveEtherealSmtpConfig()
|
||||||
|
await config.saveSettingsConfig()
|
||||||
|
const res = await request
|
||||||
|
.post(`/api/admin/email/send`)
|
||||||
|
.send({
|
||||||
|
email: "test@test.com",
|
||||||
|
purpose,
|
||||||
|
})
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
expect(res.body.message).toBeDefined()
|
||||||
|
const testUrl = nodemailer.getTestMessageUrl(res.body)
|
||||||
|
expect(testUrl).toBeDefined()
|
||||||
|
const response = await fetch(testUrl)
|
||||||
|
const text = await response.text()
|
||||||
|
let toCheckFor
|
||||||
|
switch (purpose) {
|
||||||
|
case EmailTemplatePurpose.WELCOME:
|
||||||
|
toCheckFor = `Thanks for getting started with Budibase's Budibase platform.`
|
||||||
|
break
|
||||||
|
case EmailTemplatePurpose.INVITATION:
|
||||||
|
toCheckFor = `Use the button below to set up your account and get started:`
|
||||||
|
break
|
||||||
|
case EmailTemplatePurpose.PASSWORD_RECOVERY:
|
||||||
|
toCheckFor = `You recently requested to reset your password for your Budibase account in your Budibase platform`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
expect(text).toContain(toCheckFor)
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should be able to send a welcome email", async () => {
|
||||||
|
await sendRealEmail(EmailTemplatePurpose.WELCOME)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to send a invitation email", async () => {
|
||||||
|
await sendRealEmail(EmailTemplatePurpose.INVITATION)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to send a password recovery email", async () => {
|
||||||
|
const res = await sendRealEmail(EmailTemplatePurpose.PASSWORD_RECOVERY)
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,146 @@
|
||||||
|
const env = require("../../../../environment")
|
||||||
|
const controllers = require("./controllers")
|
||||||
|
const supertest = require("supertest")
|
||||||
|
const { jwt } = require("@budibase/auth").auth
|
||||||
|
const { Cookies } = require("@budibase/auth").constants
|
||||||
|
const { Configs, LOGO_URL } = require("../../../../constants")
|
||||||
|
|
||||||
|
class TestConfiguration {
|
||||||
|
constructor(openServer = true) {
|
||||||
|
if (openServer) {
|
||||||
|
env.PORT = 4003
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
request.query = {}
|
||||||
|
request.request = {
|
||||||
|
body: config,
|
||||||
|
}
|
||||||
|
if (params) {
|
||||||
|
request.params = params
|
||||||
|
}
|
||||||
|
await controlFunc(request)
|
||||||
|
return request.body
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
// create a test user
|
||||||
|
await this._req(
|
||||||
|
{
|
||||||
|
email: "test@test.com",
|
||||||
|
password: "test",
|
||||||
|
_id: "us_uuid1",
|
||||||
|
builder: {
|
||||||
|
global: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
controllers.users.save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultHeaders() {
|
||||||
|
const user = {
|
||||||
|
_id: "us_uuid1",
|
||||||
|
userId: "us_uuid1",
|
||||||
|
}
|
||||||
|
const authToken = jwt.sign(user, env.JWT_SECRET)
|
||||||
|
return {
|
||||||
|
Accept: "application/json",
|
||||||
|
Cookie: [`${Cookies.Auth}=${authToken}`],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSettingsConfig() {
|
||||||
|
await this.deleteConfig(Configs.SETTINGS)
|
||||||
|
await this._req(
|
||||||
|
{
|
||||||
|
type: Configs.SETTINGS,
|
||||||
|
config: {
|
||||||
|
platformUrl: "http://localhost:10000",
|
||||||
|
logoUrl: LOGO_URL,
|
||||||
|
company: "Budibase",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
controllers.config.save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSmtpConfig() {
|
||||||
|
await this.deleteConfig(Configs.SMTP)
|
||||||
|
await this._req(
|
||||||
|
{
|
||||||
|
type: Configs.SMTP,
|
||||||
|
config: {
|
||||||
|
port: 12345,
|
||||||
|
host: "smtptesthost.com",
|
||||||
|
from: "testfrom@test.com",
|
||||||
|
subject: "Hello!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
controllers.config.save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveEtherealSmtpConfig() {
|
||||||
|
await this.deleteConfig(Configs.SMTP)
|
||||||
|
await this._req(
|
||||||
|
{
|
||||||
|
type: Configs.SMTP,
|
||||||
|
config: {
|
||||||
|
port: 587,
|
||||||
|
host: "smtp.ethereal.email",
|
||||||
|
secure: false,
|
||||||
|
auth: {
|
||||||
|
user: "don.bahringer@ethereal.email",
|
||||||
|
pass: "yCKSH8rWyUPbnhGYk9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
controllers.config.save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TestConfiguration
|
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = {
|
||||||
|
email: require("../../../controllers/admin/email"),
|
||||||
|
groups: require("../../../controllers/admin/groups"),
|
||||||
|
config: require("../../../controllers/admin/configs"),
|
||||||
|
templates: require("../../../controllers/admin/templates"),
|
||||||
|
users: require("../../../controllers/admin/users"),
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
const TestConfig = require("./TestConfiguration")
|
||||||
|
|
||||||
|
let request, config
|
||||||
|
|
||||||
|
exports.beforeAll = () => {
|
||||||
|
config = new TestConfig()
|
||||||
|
request = config.getRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.afterAll = () => {
|
||||||
|
if (config) {
|
||||||
|
config.end()
|
||||||
|
}
|
||||||
|
request = null
|
||||||
|
config = null
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getRequest = () => {
|
||||||
|
if (!request) {
|
||||||
|
exports.beforeAll()
|
||||||
|
}
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getConfig = () => {
|
||||||
|
if (!config) {
|
||||||
|
exports.beforeAll()
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
|
@ -1,3 +1,6 @@
|
||||||
|
exports.LOGO_URL =
|
||||||
|
"https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg"
|
||||||
|
|
||||||
exports.UserStatus = {
|
exports.UserStatus = {
|
||||||
ACTIVE: "active",
|
ACTIVE: "active",
|
||||||
INACTIVE: "inactive",
|
INACTIVE: "inactive",
|
||||||
|
@ -6,3 +9,77 @@ exports.UserStatus = {
|
||||||
exports.Groups = {
|
exports.Groups = {
|
||||||
ALL_USERS: "all_users",
|
ALL_USERS: "all_users",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.Configs = {
|
||||||
|
SETTINGS: "settings",
|
||||||
|
ACCOUNT: "account",
|
||||||
|
SMTP: "smtp",
|
||||||
|
GOOGLE: "google",
|
||||||
|
}
|
||||||
|
|
||||||
|
const TemplateTypes = {
|
||||||
|
EMAIL: "email",
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmailTemplatePurpose = {
|
||||||
|
BASE: "base",
|
||||||
|
STYLES: "styles",
|
||||||
|
PASSWORD_RECOVERY: "password_recovery",
|
||||||
|
INVITATION: "invitation",
|
||||||
|
WELCOME: "welcome",
|
||||||
|
CUSTOM: "custom",
|
||||||
|
}
|
||||||
|
|
||||||
|
const TemplateBindings = {
|
||||||
|
PLATFORM_URL: "platformUrl",
|
||||||
|
COMPANY: "company",
|
||||||
|
LOGO_URL: "logoUrl",
|
||||||
|
STYLES: "styles",
|
||||||
|
BODY: "body",
|
||||||
|
REGISTRATION_URL: "registrationUrl",
|
||||||
|
EMAIL: "email",
|
||||||
|
RESET_URL: "resetUrl",
|
||||||
|
USER: "user",
|
||||||
|
REQUEST: "request",
|
||||||
|
DOCS_URL: "docsUrl",
|
||||||
|
LOGIN_URL: "loginUrl",
|
||||||
|
CURRENT_YEAR: "currentYear",
|
||||||
|
CURRENT_DATE: "currentDate",
|
||||||
|
}
|
||||||
|
|
||||||
|
const TemplateMetadata = {
|
||||||
|
[TemplateTypes.EMAIL]: [
|
||||||
|
{
|
||||||
|
name: "Styling",
|
||||||
|
purpose: EmailTemplatePurpose.STYLES,
|
||||||
|
bindings: ["url", "company", "companyUrl", "styles", "body"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Base Format",
|
||||||
|
purpose: EmailTemplatePurpose.BASE,
|
||||||
|
bindings: ["company", "registrationUrl"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Password Recovery",
|
||||||
|
purpose: EmailTemplatePurpose.PASSWORD_RECOVERY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "New User Invitation",
|
||||||
|
purpose: EmailTemplatePurpose.INVITATION,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Custom",
|
||||||
|
purpose: EmailTemplatePurpose.CUSTOM,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// all purpose combined
|
||||||
|
exports.TemplatePurpose = {
|
||||||
|
...EmailTemplatePurpose,
|
||||||
|
}
|
||||||
|
exports.TemplateTypes = TemplateTypes
|
||||||
|
exports.EmailTemplatePurpose = EmailTemplatePurpose
|
||||||
|
exports.TemplateMetadata = TemplateMetadata
|
||||||
|
exports.TemplateBindings = TemplateBindings
|
||||||
|
exports.GLOBAL_OWNER = "global"
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
<!-- Based on templates: https://github.com/wildbit/postmark-templates/blob/master/templates/plain -->
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="x-apple-disable-message-reformatting" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<meta name="color-scheme" content="light dark" />
|
||||||
|
<meta name="supported-color-schemes" content="light dark" />
|
||||||
|
<title></title>
|
||||||
|
<style type="text/css" rel="stylesheet" media="all">
|
||||||
|
{{ styles }}
|
||||||
|
</style>
|
||||||
|
<!--[if mso]>
|
||||||
|
<style type="text/css">
|
||||||
|
.f-fallback {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td class="email-masthead">
|
||||||
|
<a href="{{ platformUrl }}" class="f-fallback email-masthead_name">
|
||||||
|
{{ company }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ body }}
|
||||||
|
<tr>
|
||||||
|
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td class="content-cell" align="center">
|
||||||
|
<p class="f-fallback sub align-center">© {{ currentYear }} {{ company }}. All rights reserved.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,73 @@
|
||||||
|
const { readStaticFile } = require("../../utilities/fileSystem")
|
||||||
|
const {
|
||||||
|
EmailTemplatePurpose,
|
||||||
|
TemplateTypes,
|
||||||
|
TemplatePurpose,
|
||||||
|
GLOBAL_OWNER,
|
||||||
|
} = require("../index")
|
||||||
|
const { join } = require("path")
|
||||||
|
const CouchDB = require("../../db")
|
||||||
|
const { getTemplateParams, StaticDatabases } = require("@budibase/auth").db
|
||||||
|
|
||||||
|
exports.EmailTemplates = {
|
||||||
|
[EmailTemplatePurpose.PASSWORD_RECOVERY]: readStaticFile(
|
||||||
|
join(__dirname, "passwordRecovery.hbs")
|
||||||
|
),
|
||||||
|
[EmailTemplatePurpose.INVITATION]: readStaticFile(
|
||||||
|
join(__dirname, "invitation.hbs")
|
||||||
|
),
|
||||||
|
[EmailTemplatePurpose.BASE]: readStaticFile(join(__dirname, "base.hbs")),
|
||||||
|
[EmailTemplatePurpose.STYLES]: readStaticFile(join(__dirname, "style.hbs")),
|
||||||
|
[EmailTemplatePurpose.WELCOME]: readStaticFile(
|
||||||
|
join(__dirname, "welcome.hbs")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.addBaseTemplates = (templates, type = null) => {
|
||||||
|
let purposeList
|
||||||
|
switch (type) {
|
||||||
|
case TemplateTypes.EMAIL:
|
||||||
|
purposeList = Object.values(EmailTemplatePurpose)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
purposeList = Object.values(TemplatePurpose)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for (let purpose of purposeList) {
|
||||||
|
// check if a template exists already for purpose
|
||||||
|
if (templates.find(template => template.purpose === purpose)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (exports.EmailTemplates[purpose]) {
|
||||||
|
templates.push({
|
||||||
|
contents: exports.EmailTemplates[purpose],
|
||||||
|
purpose,
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return templates
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getTemplates = async ({ ownerId, type, id } = {}) => {
|
||||||
|
const db = new CouchDB(StaticDatabases.GLOBAL.name)
|
||||||
|
const response = await db.allDocs(
|
||||||
|
getTemplateParams(ownerId || GLOBAL_OWNER, id, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
let templates = response.rows.map(row => row.doc)
|
||||||
|
// should only be one template with ID
|
||||||
|
if (id) {
|
||||||
|
return templates[0]
|
||||||
|
}
|
||||||
|
if (type) {
|
||||||
|
templates = templates.filter(template => template.type === type)
|
||||||
|
}
|
||||||
|
return exports.addBaseTemplates(templates, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getTemplateByPurpose = async (type, purpose) => {
|
||||||
|
const templates = await exports.getTemplates({ type })
|
||||||
|
return templates.find(template => template.purpose === purpose)
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
<!-- Based on template: https://github.com/wildbit/postmark-templates/blob/master/templates/plain/user-invitation/content.html -->
|
||||||
|
<tr>
|
||||||
|
<td class="email-body" width="570" cellpadding="0" cellspacing="0">
|
||||||
|
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<!-- Body content -->
|
||||||
|
<tr>
|
||||||
|
<td class="content-cell">
|
||||||
|
<div class="f-fallback">
|
||||||
|
<h1>Hi, {{ email }}!</h1>
|
||||||
|
<p>
|
||||||
|
{{#if request}}
|
||||||
|
{{ request.inviter }} has invited you to use {{ company }}'s Budibase platform.<
|
||||||
|
{{else}}
|
||||||
|
You've been invited to use {{ company }}'s Budibase platform.
|
||||||
|
{{/if}}
|
||||||
|
Use the button below to set up your account and get started:
|
||||||
|
</p>
|
||||||
|
<!-- Action -->
|
||||||
|
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="{{ registrationUrl }}" class="f-fallback button" target="_blank">Set up account</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p>If you have any questions contact your Budibase platform administrator.</p>
|
||||||
|
<p>Welcome aboard,
|
||||||
|
<br>The {{ company }} Team</p>
|
||||||
|
<p><strong>P.S.</strong> Need help getting started? Check out our <a href="{{ docsUrl }}">help documentation</a>.</p>
|
||||||
|
<!-- Sub copy -->
|
||||||
|
<table class="body-sub" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p class="f-fallback sub">If you’re having trouble with the button above, copy and paste the URL below into your web browser.</p>
|
||||||
|
<p class="f-fallback sub">{{ registrationUrl }}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
|
@ -0,0 +1,45 @@
|
||||||
|
<!-- Based on template: https://github.com/wildbit/postmark-templates/blob/master/templates/plain/password-reset/content.html -->
|
||||||
|
<tr>
|
||||||
|
<td class="email-body" width="570" cellpadding="0" cellspacing="0">
|
||||||
|
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<!-- Body content -->
|
||||||
|
<tr>
|
||||||
|
<td class="content-cell">
|
||||||
|
<div class="f-fallback">
|
||||||
|
<h1>Hi {{ email }},</h1>
|
||||||
|
<p>You recently requested to reset your password for your {{ company }} account in your Budibase platform. Use the button below to reset it. <strong>This password reset is only valid for the next 24 hours.</strong></p>
|
||||||
|
<!-- Action -->
|
||||||
|
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="{{ resetUrl }}" class="f-fallback button button--green" target="_blank">Reset your password</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{{#if request}}
|
||||||
|
<p>For security, this request was received from a {{ request.os }} device.</p>
|
||||||
|
{{/if}}
|
||||||
|
<p>If you did not request a password reset, please ignore this email or contact support if you have questions.</p>
|
||||||
|
<p>Thanks,
|
||||||
|
<br>The {{ company }} Team</p>
|
||||||
|
<!-- Sub copy -->
|
||||||
|
<table class="body-sub" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p class="f-fallback sub">If you’re having trouble with the button above, copy and paste the URL below into your web browser.</p>
|
||||||
|
<p class="f-fallback sub">{{ resetUrl }}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
|
@ -0,0 +1,408 @@
|
||||||
|
/* Based on templates: https://github.com/wildbit/postmark-templates/blob/master/templates/plain */
|
||||||
|
/* Base ------------------------------ */
|
||||||
|
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro&display=swap');
|
||||||
|
body {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #3869D4;
|
||||||
|
}
|
||||||
|
|
||||||
|
a img {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preheader {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
font-size: 1px;
|
||||||
|
line-height: 1px;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
/* Type ------------------------------ */
|
||||||
|
|
||||||
|
body,
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
font-family: "Source Sans Pro", Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333333;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333333;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333333;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol,
|
||||||
|
blockquote {
|
||||||
|
margin: .4em 0 1.1875em;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.625;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.sub {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
/* Utilities ------------------------------ */
|
||||||
|
|
||||||
|
.align-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
/* Buttons ------------------------------ */
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background-color: #3869D4;
|
||||||
|
border-top: 10px solid #3869D4;
|
||||||
|
border-right: 18px solid #3869D4;
|
||||||
|
border-bottom: 10px solid #3869D4;
|
||||||
|
border-left: 18px solid #3869D4;
|
||||||
|
display: inline-block;
|
||||||
|
color: #FFF;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button--green {
|
||||||
|
background-color: #22BC66;
|
||||||
|
border-top: 10px solid #22BC66;
|
||||||
|
border-right: 18px solid #22BC66;
|
||||||
|
border-bottom: 10px solid #22BC66;
|
||||||
|
border-left: 18px solid #22BC66;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button--red {
|
||||||
|
background-color: #FF6136;
|
||||||
|
border-top: 10px solid #FF6136;
|
||||||
|
border-right: 18px solid #FF6136;
|
||||||
|
border-bottom: 10px solid #FF6136;
|
||||||
|
border-left: 18px solid #FF6136;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 500px) {
|
||||||
|
.button {
|
||||||
|
width: 100% !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Attribute list ------------------------------ */
|
||||||
|
|
||||||
|
.attributes {
|
||||||
|
margin: 0 0 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes_content {
|
||||||
|
background-color: #F4F4F7;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes_item {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
/* Related Items ------------------------------ */
|
||||||
|
|
||||||
|
.related {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 25px 0 0 0;
|
||||||
|
-premailer-width: 100%;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related_item {
|
||||||
|
padding: 10px 0;
|
||||||
|
color: #CBCCCF;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related_item-title {
|
||||||
|
display: block;
|
||||||
|
margin: .5em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related_item-thumb {
|
||||||
|
display: block;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related_heading {
|
||||||
|
border-top: 1px solid #CBCCCF;
|
||||||
|
text-align: center;
|
||||||
|
padding: 25px 0 10px;
|
||||||
|
}
|
||||||
|
/* Discount Code ------------------------------ */
|
||||||
|
|
||||||
|
.discount {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 24px;
|
||||||
|
-premailer-width: 100%;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
background-color: #F4F4F7;
|
||||||
|
border: 2px dashed #CBCCCF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discount_heading {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discount_body {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
/* Social Icons ------------------------------ */
|
||||||
|
|
||||||
|
.social {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social td {
|
||||||
|
padding: 0;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social_icon {
|
||||||
|
height: 20px;
|
||||||
|
margin: 0 8px 10px 8px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
/* Data table ------------------------------ */
|
||||||
|
|
||||||
|
.purchase {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 35px 0;
|
||||||
|
-premailer-width: 100%;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase_content {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 25px 0 0 0;
|
||||||
|
-premailer-width: 100%;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase_item {
|
||||||
|
padding: 10px 0;
|
||||||
|
color: #51545E;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase_heading {
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #EAEAEC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase_heading p {
|
||||||
|
margin: 0;
|
||||||
|
color: #85878E;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase_footer {
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #EAEAEC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase_total {
|
||||||
|
margin: 0;
|
||||||
|
text-align: right;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase_total--label {
|
||||||
|
padding: 0 15px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #FFF;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-premailer-width: 100%;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-content {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-premailer-width: 100%;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
}
|
||||||
|
/* Masthead ----------------------- */
|
||||||
|
|
||||||
|
.email-masthead {
|
||||||
|
padding: 25px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-masthead_logo {
|
||||||
|
width: 94px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-masthead_name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #A8AAAF;
|
||||||
|
text-decoration: none;
|
||||||
|
text-shadow: 0 1px 0 white;
|
||||||
|
}
|
||||||
|
/* Body ------------------------------ */
|
||||||
|
|
||||||
|
.email-body {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-premailer-width: 100%;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-body_inner {
|
||||||
|
width: 570px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
-premailer-width: 570px;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-footer {
|
||||||
|
width: 570px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
-premailer-width: 570px;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-footer p {
|
||||||
|
color: #A8AAAF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-action {
|
||||||
|
width: 100%;
|
||||||
|
margin: 30px auto;
|
||||||
|
padding: 0;
|
||||||
|
-premailer-width: 100%;
|
||||||
|
-premailer-cellpadding: 0;
|
||||||
|
-premailer-cellspacing: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-sub {
|
||||||
|
margin-top: 25px;
|
||||||
|
padding-top: 25px;
|
||||||
|
border-top: 1px solid #EAEAEC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-cell {
|
||||||
|
padding: 35px;
|
||||||
|
}
|
||||||
|
/*Media Queries ------------------------------ */
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.email-body_inner,
|
||||||
|
.email-footer {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: #333333 !important;
|
||||||
|
color: #FFF !important;
|
||||||
|
}
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol,
|
||||||
|
blockquote,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
span,
|
||||||
|
.purchase_item {
|
||||||
|
color: #FFF !important;
|
||||||
|
}
|
||||||
|
.attributes_content,
|
||||||
|
.discount {
|
||||||
|
background-color: #222 !important;
|
||||||
|
}
|
||||||
|
.email-masthead_name {
|
||||||
|
text-shadow: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
supported-color-schemes: light dark;
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
<!-- Based on template: https://github.com/wildbit/postmark-templates/blob/master/templates/plain/welcome/content.html -->
|
||||||
|
<tr>
|
||||||
|
<td class="email-body" width="570" cellpadding="0" cellspacing="0">
|
||||||
|
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<!-- Body content -->
|
||||||
|
<tr>
|
||||||
|
<td class="content-cell">
|
||||||
|
<div class="f-fallback">
|
||||||
|
<h1>Welcome, {{ email }}!</h1>
|
||||||
|
<p>Thanks for getting started with {{ company }}'s Budibase platform.</p>
|
||||||
|
<p>For reference, here's how to login:</p>
|
||||||
|
<table class="attributes" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td class="attributes_content">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td class="attributes_item">
|
||||||
|
<span class="f-fallback">
|
||||||
|
<strong>Login Page:</strong> <a href="{{ loginUrl }}">{{ loginUrl }}</a>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{#if request}}
|
||||||
|
<tr>
|
||||||
|
<td class="attributes_item">
|
||||||
|
<span class="f-fallback">
|
||||||
|
<strong>Username:</strong> {{ request.loginUsername }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/if}}
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p>If you have any questions get in contact with your {{ company }}'s Budibase platform administration team.</p>
|
||||||
|
<p>Thanks,
|
||||||
|
<br>The {{ company }} Team</p>
|
||||||
|
<p><strong>P.S.</strong> Need immediate help getting started? Check out our <a href="{{ docsUrl }}">help documentation</a>.</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
|
@ -5,7 +5,7 @@ require("@budibase/auth").init(CouchDB)
|
||||||
const Koa = require("koa")
|
const Koa = require("koa")
|
||||||
const destroyable = require("server-destroy")
|
const destroyable = require("server-destroy")
|
||||||
const koaBody = require("koa-body")
|
const koaBody = require("koa-body")
|
||||||
const { passport } = require("@budibase/auth")
|
const { passport } = require("@budibase/auth").auth
|
||||||
const logger = require("koa-pino-logger")
|
const logger = require("koa-pino-logger")
|
||||||
const http = require("http")
|
const http = require("http")
|
||||||
const api = require("./api")
|
const api = require("./api")
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
const nodemailer = require("nodemailer")
|
||||||
|
|
||||||
|
exports.createSMTPTransport = config => {
|
||||||
|
const options = {
|
||||||
|
port: config.port,
|
||||||
|
host: config.host,
|
||||||
|
secure: config.secure || false,
|
||||||
|
auth: config.auth,
|
||||||
|
}
|
||||||
|
if (config.selfSigned) {
|
||||||
|
options.tls = {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nodemailer.createTransport(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.verifyConfig = async config => {
|
||||||
|
const transport = exports.createSMTPTransport(config)
|
||||||
|
await transport.verify()
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
const { readFileSync } = require("fs")
|
||||||
|
|
||||||
|
exports.readStaticFile = path => {
|
||||||
|
return readFileSync(path, "utf-8")
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Makes sure that a URL has the correct number of slashes, while maintaining the
|
||||||
|
* http(s):// double slashes.
|
||||||
|
* @param {string} url The URL to test and remove any extra double slashes.
|
||||||
|
* @return {string} The updated url.
|
||||||
|
*/
|
||||||
|
exports.checkSlashesInUrl = url => {
|
||||||
|
return url.replace(/(https?:\/\/)|(\/)+/g, "$1$2")
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
const CouchDB = require("../db")
|
||||||
|
const { getConfigParams, StaticDatabases } = require("@budibase/auth").db
|
||||||
|
const { Configs, TemplateBindings, LOGO_URL } = require("../constants")
|
||||||
|
const { checkSlashesInUrl } = require("./index")
|
||||||
|
const env = require("../environment")
|
||||||
|
|
||||||
|
const LOCAL_URL = `http://localhost:${env.PORT}`
|
||||||
|
const BASE_COMPANY = "Budibase"
|
||||||
|
|
||||||
|
exports.getSettingsTemplateContext = async () => {
|
||||||
|
const db = new CouchDB(StaticDatabases.GLOBAL.name)
|
||||||
|
const response = await db.allDocs(
|
||||||
|
getConfigParams(Configs.SETTINGS, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
let settings = response.rows.map(row => row.doc)[0] || {}
|
||||||
|
if (!settings.platformUrl) {
|
||||||
|
settings.platformUrl = LOCAL_URL
|
||||||
|
}
|
||||||
|
// TODO: need to fully spec out the context
|
||||||
|
const URL = settings.platformUrl
|
||||||
|
return {
|
||||||
|
[TemplateBindings.LOGO_URL]: settings.logoUrl || LOGO_URL,
|
||||||
|
[TemplateBindings.PLATFORM_URL]: URL,
|
||||||
|
[TemplateBindings.REGISTRATION_URL]: checkSlashesInUrl(
|
||||||
|
`${URL}/registration`
|
||||||
|
),
|
||||||
|
[TemplateBindings.RESET_URL]: checkSlashesInUrl(`${URL}/reset`),
|
||||||
|
[TemplateBindings.COMPANY]: settings.company || BASE_COMPANY,
|
||||||
|
[TemplateBindings.DOCS_URL]:
|
||||||
|
settings.docsUrl || "https://docs.budibase.com/",
|
||||||
|
[TemplateBindings.LOGIN_URL]: checkSlashesInUrl(`${URL}/login`),
|
||||||
|
[TemplateBindings.CURRENT_DATE]: new Date().toISOString(),
|
||||||
|
[TemplateBindings.CURRENT_YEAR]: new Date().getFullYear(),
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue