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",
|
||||
APP: "app",
|
||||
GROUP: "group",
|
||||
CONFIG: "config",
|
||||
TEMPLATE: "template",
|
||||
}
|
||||
|
||||
exports.DocumentTypes = DocumentTypes
|
||||
|
@ -46,14 +48,14 @@ exports.getGroupParams = (id = "", otherProps = {}) => {
|
|||
* Generates a new global user ID.
|
||||
* @returns {string} The new user ID which the user doc can be stored under.
|
||||
*/
|
||||
exports.generateGlobalUserID = () => {
|
||||
return `${DocumentTypes.USER}${SEPARATOR}${newid()}`
|
||||
exports.generateGlobalUserID = id => {
|
||||
return `${DocumentTypes.USER}${SEPARATOR}${id || newid()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets parameters for retrieving users.
|
||||
*/
|
||||
exports.getGlobalUserParams = (globalId = "", otherProps = {}) => {
|
||||
exports.getGlobalUserParams = (globalId, otherProps = {}) => {
|
||||
if (!globalId) {
|
||||
globalId = ""
|
||||
}
|
||||
|
@ -63,3 +65,98 @@ exports.getGlobalUserParams = (globalId = "", otherProps = {}) => {
|
|||
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,
|
||||
COUCH_DB_URL: process.env.COUCH_DB_URL,
|
||||
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 LocalStrategy = require("passport-local").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 { jwt, local, authenticated } = require("./middleware")
|
||||
const { Cookies, UserStatus } = require("./constants")
|
||||
const { hash, compare } = require("./hashing")
|
||||
const {
|
||||
getAppId,
|
||||
setCookie,
|
||||
getCookie,
|
||||
clearCookie,
|
||||
isClient,
|
||||
getGlobalUserByEmail,
|
||||
} = require("./utils")
|
||||
const {
|
||||
generateGlobalUserID,
|
||||
getGlobalUserParams,
|
||||
generateGroupID,
|
||||
getGroupParams,
|
||||
} = require("./db/utils")
|
||||
const { jwt, local, authenticated, google } = require("./middleware")
|
||||
const { setDB, getDB } = require("./db")
|
||||
|
||||
// Strategies
|
||||
passport.use(new LocalStrategy(local.options, local.authenticate))
|
||||
passport.use(new JwtStrategy(jwt.options, jwt.authenticate))
|
||||
// passport.use(new GoogleStrategy(google.options, google.authenticate))
|
||||
|
||||
passport.serializeUser((user, done) => done(null, user))
|
||||
|
||||
|
@ -45,21 +27,17 @@ module.exports = {
|
|||
init(pouch) {
|
||||
setDB(pouch)
|
||||
},
|
||||
passport,
|
||||
Cookies,
|
||||
UserStatus,
|
||||
db: require("./db/utils"),
|
||||
utils: {
|
||||
...require("./utils"),
|
||||
...require("./hashing"),
|
||||
},
|
||||
auth: {
|
||||
buildAuthMiddleware: authenticated,
|
||||
passport,
|
||||
google,
|
||||
jwt: require("jsonwebtoken"),
|
||||
},
|
||||
StaticDatabases,
|
||||
generateGlobalUserID,
|
||||
getGlobalUserParams,
|
||||
generateGroupID,
|
||||
getGroupParams,
|
||||
hash,
|
||||
compare,
|
||||
getAppId,
|
||||
setCookie,
|
||||
getCookie,
|
||||
clearCookie,
|
||||
authenticated,
|
||||
isClient,
|
||||
getGlobalUserByEmail,
|
||||
constants: require("./constants"),
|
||||
}
|
||||
|
|
|
@ -1,18 +1,39 @@
|
|||
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) => {
|
||||
try {
|
||||
// check the actual user is authenticated first
|
||||
const authCookie = getCookie(ctx, Cookies.Auth)
|
||||
|
||||
if (authCookie) {
|
||||
ctx.isAuthenticated = true
|
||||
ctx.user = authCookie
|
||||
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 {
|
||||
// check the actual user is authenticated first
|
||||
const authCookie = getCookie(ctx, Cookies.Auth)
|
||||
|
||||
await next()
|
||||
} catch (err) {
|
||||
ctx.throw(err.status || 403, err)
|
||||
if (authCookie) {
|
||||
try {
|
||||
const db = database.getDB(StaticDatabases.GLOBAL.name)
|
||||
const user = await db.get(authCookie.userId)
|
||||
delete user.password
|
||||
ctx.isAuthenticated = true
|
||||
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
|
||||
}
|
||||
|
||||
return next()
|
||||
} catch (err) {
|
||||
ctx.throw(err.status || 403, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,76 @@
|
|||
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 = {
|
||||
clientId: env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET,
|
||||
callbackURL: env.GOOGLE_AUTH_CALLBACK_URL,
|
||||
async function authenticate(token, tokenSecret, profile, done) {
|
||||
// Check the user exists in the instance DB by email
|
||||
const db = database.getDB(StaticDatabases.GLOBAL.name)
|
||||
|
||||
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 ...
|
||||
// fetchUser().then(user => done(null, user))
|
||||
// }
|
||||
/**
|
||||
* Create an instance of the google passport strategy. This wrapper fetches the configuration
|
||||
* 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)) {
|
||||
const payload = {
|
||||
userId: dbUser._id,
|
||||
builder: dbUser.builder,
|
||||
email: dbUser.email,
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
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} />
|
||||
<Spacer large />
|
||||
<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>
|
||||
</form>
|
||||
|
||||
|
|
|
@ -103,7 +103,6 @@
|
|||
"jimp": "0.16.1",
|
||||
"joi": "17.2.1",
|
||||
"jsonschema": "1.4.0",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"koa": "2.7.0",
|
||||
"koa-body": "4.2.0",
|
||||
"koa-compress": "4.0.1",
|
||||
|
|
|
@ -72,7 +72,7 @@ exports.createMetadata = async function(ctx) {
|
|||
|
||||
exports.updateSelfMetadata = async function(ctx) {
|
||||
// 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
|
||||
delete ctx.request.body._rev
|
||||
await exports.updateMetadata(ctx)
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
const Router = require("@koa/router")
|
||||
const { authenticated } = require("@budibase/auth")
|
||||
const { buildAuthMiddleware } = require("@budibase/auth").auth
|
||||
const currentApp = require("../middleware/currentapp")
|
||||
const compress = require("koa-compress")
|
||||
const zlib = require("zlib")
|
||||
const { mainRoutes, authRoutes, staticRoutes } = require("./routes")
|
||||
const { mainRoutes, staticRoutes } = require("./routes")
|
||||
const pkg = require("../../package.json")
|
||||
|
||||
const router = new Router()
|
||||
const env = require("../environment")
|
||||
|
||||
const NO_AUTH_ENDPOINTS = [
|
||||
"/health",
|
||||
"/version",
|
||||
"webhooks/trigger",
|
||||
"webhooks/schema",
|
||||
]
|
||||
|
||||
router
|
||||
.use(
|
||||
compress({
|
||||
|
@ -31,7 +38,7 @@ router
|
|||
})
|
||||
.use("/health", ctx => (ctx.status = 200))
|
||||
.use("/version", ctx => (ctx.body = pkg.version))
|
||||
.use(authenticated)
|
||||
.use(buildAuthMiddleware(NO_AUTH_ENDPOINTS))
|
||||
.use(currentApp)
|
||||
|
||||
// error handling middleware
|
||||
|
@ -53,9 +60,6 @@ router.use(async (ctx, next) => {
|
|||
|
||||
router.get("/health", ctx => (ctx.status = 200))
|
||||
|
||||
router.use(authRoutes.routes())
|
||||
router.use(authRoutes.allowedMethods())
|
||||
|
||||
// authenticated routes
|
||||
for (let route of mainRoutes) {
|
||||
router.use(route.routes())
|
||||
|
|
|
@ -25,6 +25,7 @@ const backupRoutes = require("./backup")
|
|||
const devRoutes = require("./dev")
|
||||
|
||||
exports.mainRoutes = [
|
||||
authRoutes,
|
||||
deployRoutes,
|
||||
layoutRoutes,
|
||||
screenRoutes,
|
||||
|
@ -52,5 +53,4 @@ exports.mainRoutes = [
|
|||
rowRoutes,
|
||||
]
|
||||
|
||||
exports.authRoutes = authRoutes
|
||||
exports.staticRoutes = staticRoutes
|
||||
|
|
|
@ -93,7 +93,7 @@ describe("/queries", () => {
|
|||
const query = await config.createQuery()
|
||||
const res = await request
|
||||
.get(`/api/queries/${query._id}`)
|
||||
.set(await config.roleHeaders())
|
||||
.set(await config.roleHeaders({}))
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(res.body.fields).toBeUndefined()
|
||||
|
|
|
@ -35,7 +35,11 @@ describe("/routing", () => {
|
|||
})
|
||||
const res = await request
|
||||
.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(200)
|
||||
expect(res.body.routes).toBeDefined()
|
||||
|
@ -59,7 +63,11 @@ describe("/routing", () => {
|
|||
})
|
||||
const res = await request
|
||||
.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(200)
|
||||
expect(res.body.routes).toBeDefined()
|
||||
|
|
|
@ -5,7 +5,9 @@ const { basicUser } = setup.structures
|
|||
const workerRequests = require("../../../utilities/workerRequests")
|
||||
|
||||
jest.mock("../../../utilities/workerRequests", () => ({
|
||||
getGlobalUsers: jest.fn(),
|
||||
getGlobalUsers: jest.fn(() => {
|
||||
return {}
|
||||
}),
|
||||
saveGlobalUser: jest.fn(() => {
|
||||
const uuid = require("uuid/v4")
|
||||
return {
|
||||
|
|
|
@ -46,7 +46,10 @@ exports.createRequest = (request, 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
|
||||
.createRequest(config.request, method, url, body)
|
||||
.set(headers)
|
||||
|
@ -62,9 +65,10 @@ exports.checkPermissionsEndpoint = async ({
|
|||
failRole,
|
||||
}) => {
|
||||
const password = "PASSWORD"
|
||||
await config.createUser("passUser@budibase.com", password, passRole)
|
||||
const passHeader = await config.login("passUser@budibase.com", password, {
|
||||
let user = await config.createUser("pass@budibase.com", password, passRole)
|
||||
const passHeader = await config.login("pass@budibase.com", password, {
|
||||
roleId: passRole,
|
||||
userId: user.globalId,
|
||||
})
|
||||
|
||||
await exports
|
||||
|
@ -72,9 +76,10 @@ exports.checkPermissionsEndpoint = async ({
|
|||
.set(passHeader)
|
||||
.expect(200)
|
||||
|
||||
await config.createUser("failUser@budibase.com", password, failRole)
|
||||
const failHeader = await config.login("failUser@budibase.com", password, {
|
||||
user = await config.createUser("fail@budibase.com", password, failRole)
|
||||
const failHeader = await config.login("fail@budibase.com", password, {
|
||||
roleId: failRole,
|
||||
userId: user.globalId,
|
||||
})
|
||||
|
||||
await exports
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles")
|
||||
const { UserStatus } = require("@budibase/auth")
|
||||
const { UserStatus } = require("@budibase/auth").constants
|
||||
|
||||
exports.LOGO_URL =
|
||||
"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)
|
||||
}
|
||||
|
||||
const endOfKey =
|
||||
rowId == null ? `${tableId}${SEPARATOR}` : `${tableId}${SEPARATOR}${rowId}`
|
||||
const endOfKey = rowId == null ? `${tableId}${SEPARATOR}` : rowId
|
||||
|
||||
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 { getGlobalUsers } = require("../utilities/workerRequests")
|
||||
const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles")
|
||||
const {
|
||||
getGlobalIDFromUserMetadataID,
|
||||
generateUserMetadataID,
|
||||
} = require("../db/utils")
|
||||
const { generateUserMetadataID } = require("../db/utils")
|
||||
|
||||
module.exports = async (ctx, next) => {
|
||||
// try to get the appID from the request
|
||||
|
@ -30,8 +28,7 @@ module.exports = async (ctx, next) => {
|
|||
appCookie.roleId === BUILTIN_ROLE_IDS.PUBLIC)
|
||||
) {
|
||||
// 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, globalId)
|
||||
const globalUser = await getGlobalUsers(ctx, requestAppId, ctx.user._id)
|
||||
updateCookie = true
|
||||
appId = requestAppId
|
||||
if (globalUser.roles && globalUser.roles[requestAppId]) {
|
||||
|
@ -49,7 +46,7 @@ module.exports = async (ctx, next) => {
|
|||
ctx.appId = appId
|
||||
if (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,
|
||||
// override userID with metadata one
|
||||
|
|
|
@ -5,7 +5,7 @@ function mockWorker() {
|
|||
jest.mock("../../utilities/workerRequests", () => ({
|
||||
getGlobalUsers: () => {
|
||||
return {
|
||||
email: "us_uuid1",
|
||||
_id: "us_uuid1",
|
||||
roles: {
|
||||
"app_test": "BASIC",
|
||||
}
|
||||
|
@ -23,10 +23,14 @@ function mockAuthWithNoCookie() {
|
|||
jest.resetModules()
|
||||
mockWorker()
|
||||
jest.mock("@budibase/auth", () => ({
|
||||
getAppId: jest.fn(),
|
||||
setCookie: jest.fn(),
|
||||
getCookie: jest.fn(),
|
||||
Cookies: {},
|
||||
utils: {
|
||||
getAppId: jest.fn(),
|
||||
setCookie: jest.fn(),
|
||||
getCookie: jest.fn(),
|
||||
},
|
||||
constants: {
|
||||
Cookies: {},
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -34,15 +38,19 @@ function mockAuthWithCookie() {
|
|||
jest.resetModules()
|
||||
mockWorker()
|
||||
jest.mock("@budibase/auth", () => ({
|
||||
getAppId: () => {
|
||||
return "app_test"
|
||||
utils: {
|
||||
getAppId: () => {
|
||||
return "app_test"
|
||||
},
|
||||
setCookie: jest.fn(),
|
||||
getCookie: () => ({appId: "app_different", roleId: "PUBLIC"}),
|
||||
},
|
||||
constants: {
|
||||
Cookies: {
|
||||
Auth: "auth",
|
||||
CurrentApp: "currentapp",
|
||||
},
|
||||
},
|
||||
setCookie: jest.fn(),
|
||||
getCookie: () => ({ appId: "app_different", roleId: "PUBLIC" }),
|
||||
Cookies: {
|
||||
Auth: "auth",
|
||||
CurrentApp: "currentapp",
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -59,7 +67,8 @@ class TestConfiguration {
|
|||
|
||||
setUser() {
|
||||
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) {
|
||||
config.setUser()
|
||||
await config.executeMiddleware()
|
||||
const cookieFn = require("@budibase/auth").setCookie
|
||||
const cookieFn = require("@budibase/auth").utils.setCookie
|
||||
if (setCookie) {
|
||||
expect(cookieFn).toHaveBeenCalled()
|
||||
} else {
|
||||
|
@ -122,12 +131,16 @@ describe("Current app middleware", () => {
|
|||
it("should perform correct when no cookie exists", async () => {
|
||||
mockReset()
|
||||
jest.mock("@budibase/auth", () => ({
|
||||
getAppId: () => {
|
||||
return "app_test"
|
||||
utils: {
|
||||
getAppId: () => {
|
||||
return "app_test"
|
||||
},
|
||||
setCookie: jest.fn(),
|
||||
getCookie: jest.fn(),
|
||||
},
|
||||
constants: {
|
||||
Cookies: {},
|
||||
},
|
||||
setCookie: jest.fn(),
|
||||
getCookie: jest.fn(),
|
||||
Cookies: {},
|
||||
}))
|
||||
await checkExpected(true)
|
||||
})
|
||||
|
@ -135,15 +148,16 @@ describe("Current app middleware", () => {
|
|||
it("lastly check what occurs when cookie doesn't need updated", async () => {
|
||||
mockReset()
|
||||
jest.mock("@budibase/auth", () => ({
|
||||
getAppId: () => {
|
||||
return "app_test"
|
||||
utils: {
|
||||
getAppId: () => {
|
||||
return "app_test"
|
||||
},
|
||||
setCookie: jest.fn(),
|
||||
getCookie: () => ({appId: "app_test", roleId: "BASIC"}),
|
||||
},
|
||||
setCookie: jest.fn(),
|
||||
getCookie: () => ({ appId: "app_test", roleId: "BASIC" }),
|
||||
Cookies: {},
|
||||
constants: { Cookies: {} },
|
||||
}))
|
||||
await checkExpected(false)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
const { BUILTIN_ROLE_IDS } = require("../../utilities/security/roles")
|
||||
const jwt = require("jsonwebtoken")
|
||||
const env = require("../../environment")
|
||||
const {
|
||||
basicTable,
|
||||
|
@ -15,8 +14,12 @@ const {
|
|||
const controllers = require("./controllers")
|
||||
const supertest = require("supertest")
|
||||
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 PASSWORD = "babs_password"
|
||||
|
||||
|
@ -47,6 +50,7 @@ class TestConfiguration {
|
|||
request.config = { jwtSecret: env.JWT_SECRET }
|
||||
request.appId = this.appId
|
||||
request.user = { appId: this.appId }
|
||||
request.query = {}
|
||||
request.request = {
|
||||
body: config,
|
||||
}
|
||||
|
@ -57,7 +61,27 @@ class TestConfiguration {
|
|||
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") {
|
||||
await this.globalUser()
|
||||
return this.createApp(appName)
|
||||
}
|
||||
|
||||
|
@ -69,17 +93,14 @@ class TestConfiguration {
|
|||
}
|
||||
|
||||
defaultHeaders() {
|
||||
const user = {
|
||||
userId: "ro_ta_user_us_uuid1",
|
||||
builder: {
|
||||
global: true,
|
||||
},
|
||||
const auth = {
|
||||
userId: GLOBAL_USER_ID,
|
||||
}
|
||||
const app = {
|
||||
roleId: BUILTIN_ROLE_IDS.BUILDER,
|
||||
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 headers = {
|
||||
Accept: "application/json",
|
||||
|
@ -104,14 +125,18 @@ class TestConfiguration {
|
|||
return headers
|
||||
}
|
||||
|
||||
async roleHeaders(email = EMAIL, roleId = BUILTIN_ROLE_IDS.ADMIN) {
|
||||
async roleHeaders({
|
||||
email = EMAIL,
|
||||
roleId = BUILTIN_ROLE_IDS.ADMIN,
|
||||
builder = false,
|
||||
}) {
|
||||
let user
|
||||
try {
|
||||
user = await this.createUser(email, PASSWORD, roleId)
|
||||
} catch (err) {
|
||||
// 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) {
|
||||
|
@ -282,7 +307,9 @@ class TestConfiguration {
|
|||
password = PASSWORD,
|
||||
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,
|
||||
password,
|
||||
|
@ -291,28 +318,34 @@ class TestConfiguration {
|
|||
null,
|
||||
controllers.user.createMetadata
|
||||
)
|
||||
return {
|
||||
...user,
|
||||
globalId,
|
||||
}
|
||||
}
|
||||
|
||||
async login(email, password, { roleId, userId } = {}) {
|
||||
if (!roleId) {
|
||||
roleId = BUILTIN_ROLE_IDS.BUILDER
|
||||
}
|
||||
async login(email, password, { roleId, userId, builder } = {}) {
|
||||
roleId = !roleId ? BUILTIN_ROLE_IDS.BUILDER : roleId
|
||||
userId = !userId ? `us_uuid1` : userId
|
||||
if (!this.request) {
|
||||
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) {
|
||||
await this.createUser()
|
||||
}
|
||||
// have to fake this
|
||||
const user = {
|
||||
userId: userId || `us_uuid1`,
|
||||
email: email || EMAIL,
|
||||
const auth = {
|
||||
userId,
|
||||
}
|
||||
const app = {
|
||||
roleId: roleId,
|
||||
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)
|
||||
|
||||
// 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",
|
||||
"dependencies": {
|
||||
"@budibase/auth": "0.0.1",
|
||||
"@budibase/string-templates": "^0.8.16",
|
||||
"@koa/router": "^8.0.0",
|
||||
"aws-sdk": "^2.811.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
|
@ -35,6 +36,7 @@
|
|||
"koa-session": "^5.12.0",
|
||||
"koa-static": "^5.0.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"nodemailer": "^6.5.0",
|
||||
"passport-google-oauth": "^2.0.0",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
|
@ -44,7 +46,9 @@
|
|||
"server-destroy": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^26.6.3",
|
||||
"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 { getGroupParams, StaticDatabases } = require("@budibase/auth")
|
||||
const { generateGroupID } = require("@budibase/auth")
|
||||
const {
|
||||
getGroupParams,
|
||||
generateGroupID,
|
||||
StaticDatabases,
|
||||
} = require("@budibase/auth").db
|
||||
|
||||
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 {
|
||||
hash,
|
||||
generateGlobalUserID,
|
||||
getGlobalUserParams,
|
||||
StaticDatabases,
|
||||
getGlobalUserByEmail,
|
||||
} = require("@budibase/auth")
|
||||
} = require("@budibase/auth").db
|
||||
const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils
|
||||
const { UserStatus } = require("../../../constants")
|
||||
|
||||
const FIRST_USER_EMAIL = "test@test.com"
|
||||
const FIRST_USER_PASSWORD = "test"
|
||||
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
||||
|
||||
exports.userSave = async ctx => {
|
||||
exports.save = async ctx => {
|
||||
const db = new CouchDB(GLOBAL_DB)
|
||||
const { email, password, _id } = ctx.request.body
|
||||
|
||||
|
@ -70,10 +69,10 @@ exports.firstUser = async ctx => {
|
|||
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 dbUser = await db.get(ctx.params.id)
|
||||
await db.remove(dbUser._id, dbUser._rev)
|
||||
|
@ -83,7 +82,7 @@ exports.userDelete = async ctx => {
|
|||
}
|
||||
|
||||
// called internally by app server user fetch
|
||||
exports.userFetch = async ctx => {
|
||||
exports.fetch = async ctx => {
|
||||
const db = new CouchDB(GLOBAL_DB)
|
||||
const response = await db.allDocs(
|
||||
getGlobalUserParams(null, {
|
||||
|
@ -101,7 +100,7 @@ exports.userFetch = async ctx => {
|
|||
}
|
||||
|
||||
// called internally by app server user find
|
||||
exports.userFind = async ctx => {
|
||||
exports.find = async ctx => {
|
||||
const db = new CouchDB(GLOBAL_DB)
|
||||
let user
|
||||
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) => {
|
||||
return passport.authenticate("local", async (err, user) => {
|
||||
|
@ -31,10 +39,55 @@ exports.logout = async ctx => {
|
|||
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 () => {
|
||||
// return passport.authenticate("google")
|
||||
exports.googleAuth = 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,
|
||||
{ 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 zlib = require("zlib")
|
||||
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()
|
||||
|
||||
|
@ -19,6 +27,14 @@ router
|
|||
})
|
||||
)
|
||||
.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
|
||||
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 controller = require("../../controllers/admin/groups")
|
||||
const joiValidator = require("../../../middleware/joi-validator")
|
||||
const { authenticated } = require("@budibase/auth")
|
||||
const Joi = require("joi")
|
||||
|
||||
const router = Router()
|
||||
|
@ -25,14 +24,9 @@ function buildGroupSaveValidation() {
|
|||
}
|
||||
|
||||
router
|
||||
.post(
|
||||
"/api/admin/groups",
|
||||
buildGroupSaveValidation(),
|
||||
authenticated,
|
||||
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)
|
||||
.post("/api/admin/groups", buildGroupSaveValidation(), controller.save)
|
||||
.get("/api/admin/groups", controller.fetch)
|
||||
.delete("/api/admin/groups/:id", controller.destroy)
|
||||
.get("/api/admin/groups/:id", controller.find)
|
||||
|
||||
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 controller = require("../../controllers/admin/users")
|
||||
const joiValidator = require("../../../middleware/joi-validator")
|
||||
const { authenticated } = require("@budibase/auth")
|
||||
const Joi = require("joi")
|
||||
|
||||
const router = Router()
|
||||
|
@ -26,15 +25,10 @@ function buildUserSaveValidation() {
|
|||
}
|
||||
|
||||
router
|
||||
.post(
|
||||
"/api/admin/users",
|
||||
buildUserSaveValidation(),
|
||||
authenticated,
|
||||
controller.userSave
|
||||
)
|
||||
.post("/api/admin/users", buildUserSaveValidation(), controller.save)
|
||||
.get("/api/admin/users", controller.fetch)
|
||||
.post("/api/admin/users/first", controller.firstUser)
|
||||
.delete("/api/admin/users/:id", authenticated, controller.userDelete)
|
||||
.get("/api/admin/users", authenticated, controller.userFetch)
|
||||
.get("/api/admin/users/:id", authenticated, controller.userFind)
|
||||
.delete("/api/admin/users/:id", controller.destroy)
|
||||
.get("/api/admin/users/:id", controller.find)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/app")
|
||||
const { authenticated } = require("@budibase/auth")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get("/api/apps", authenticated, controller.getApps)
|
||||
router.get("/api/apps", controller.getApps)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -1,19 +1,12 @@
|
|||
const Router = require("@koa/router")
|
||||
const { passport } = require("@budibase/auth")
|
||||
const authController = require("../controllers/auth")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router
|
||||
.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)
|
||||
.get("/api/auth/google", passport.authenticate("google"))
|
||||
.get(
|
||||
"/api/auth/google/callback",
|
||||
passport.authenticate("google", {
|
||||
successRedirect: "/app",
|
||||
failureRedirect: "/",
|
||||
})
|
||||
)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
const userRoutes = require("./admin/users")
|
||||
const configRoutes = require("./admin/configs")
|
||||
const groupRoutes = require("./admin/groups")
|
||||
const templateRoutes = require("./admin/templates")
|
||||
const emailRoutes = require("./admin/email")
|
||||
const authRoutes = require("./auth")
|
||||
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 = {
|
||||
ACTIVE: "active",
|
||||
INACTIVE: "inactive",
|
||||
|
@ -6,3 +9,77 @@ exports.UserStatus = {
|
|||
exports.Groups = {
|
||||
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 destroyable = require("server-destroy")
|
||||
const koaBody = require("koa-body")
|
||||
const { passport } = require("@budibase/auth")
|
||||
const { passport } = require("@budibase/auth").auth
|
||||
const logger = require("koa-pino-logger")
|
||||
const http = require("http")
|
||||
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