mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
57 changed files with 6073 additions and 244 deletions
@ -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 = []) => { |
||||
try { |
const regex = new RegExp(noAuthPatterns.join("|")) |
||||
// check the actual user is authenticated first
|
return async (ctx, next) => { |
||||
const authCookie = getCookie(ctx, Cookies.Auth) |
// the path is not authenticated
|
||||
|
if (regex.test(ctx.request.url)) { |
||||
if (authCookie) { |
return next() |
||||
ctx.isAuthenticated = true |
|
||||
ctx.user = authCookie |
|
||||
} |
} |
||||
|
try { |
||||
|
// check the actual user is authenticated first
|
||||
|
const authCookie = getCookie(ctx, Cookies.Auth) |
||||
|
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
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) |
||||
|
} |
||||
|
} |
||||
|
|||||
@ -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" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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,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 |
||||
|
} |
||||
@ -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 |
||||
@ -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,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 |
||||
|
} |
||||
@ -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> |
||||
@ -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
Loading…
Reference in new issue