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 { 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) |
|||
|
|||
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() |
|||
} catch (err) { |
|||
ctx.throw(err.status || 403, err) |
|||
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) |
|||
} |
|||
} |
|||
|
|||
@ -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 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 |
|||
} |
|||
@ -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