Merge pull request #1545 from Budibase/fix/user-setup-builder-admin

Ability to configure invited users as admins/builders
This commit is contained in:
Michael Drury 2021-05-24 20:40:32 +01:00 committed by GitHub
commit b5da856c64
8 changed files with 124 additions and 27 deletions

View File

@ -5,6 +5,8 @@
Select,
ModalContent,
notifications,
Toggle,
Label,
} from "@budibase/bbui"
import { createValidationStore, emailValidator } from "helpers/validation"
import { users } from "stores/portal"
@ -13,12 +15,12 @@
const options = ["Email onboarding", "Basic onboarding"]
let selected = options[0]
let builder, admin
const [email, error, touched] = createValidationStore("", emailValidator)
async function createUserFlow() {
const res = await users.invite($email)
console.log(res)
const res = await users.invite({ email: $email, builder, admin })
if (res.status) {
notifications.error(res.message)
} else {
@ -56,4 +58,23 @@
placeholder="john@doe.com"
label="Email"
/>
<div>
<div class="toggle">
<Label size="L">Development access</Label>
<Toggle text="" bind:value={builder} />
</div>
<div class="toggle">
<Label size="L">Administration access</Label>
<Toggle text="" bind:value={admin} />
</div>
</div>
</ModalContent>
<style>
.toggle {
display: grid;
grid-template-columns: 78% 1fr;
align-items: center;
width: 50%;
}
</style>

View File

@ -1,13 +1,22 @@
<script>
import { ModalContent, Body, Input, notifications } from "@budibase/bbui"
import {
ModalContent,
Body,
Input,
notifications,
Toggle,
Label,
} from "@budibase/bbui"
import { createValidationStore, emailValidator } from "helpers/validation"
import { users } from "stores/portal"
const [email, error, touched] = createValidationStore("", emailValidator)
const password = Math.random().toString(36).substr(2, 20)
let builder = false,
admin = false
async function createUser() {
const res = await users.create({ email: $email, password })
const res = await users.create({ email: $email, password, builder, admin })
if (res.status) {
notifications.error(res.message)
} else {
@ -37,4 +46,23 @@
error={$touched && $error}
/>
<Input disabled label="Password" value={password} />
<div>
<div class="toggle">
<Label size="L">Development access</Label>
<Toggle text="" bind:value={builder} />
</div>
<div class="toggle">
<Label size="L">Administration access</Label>
<Toggle text="" bind:value={admin} />
</div>
</div>
</ModalContent>
<style>
.toggle {
display: grid;
grid-template-columns: 78% 1fr;
align-items: center;
width: 50%;
}
</style>

View File

@ -11,10 +11,22 @@ export function createUsersStore() {
set(json)
}
async function invite(email) {
const response = await api.post(`/api/admin/users/invite`, { email })
async function invite({ email, builder, admin }) {
const body = { email, userInfo: {} }
if (admin) {
body.userInfo.admin = {
global: true,
}
}
if (builder) {
body.userInfo.builder = {
global: true,
}
}
const response = await api.post(`/api/admin/users/invite`, body)
return await response.json()
}
async function acceptInvite(inviteCode, password) {
const response = await api.post("/api/admin/users/invite/accept", {
inviteCode,
@ -23,14 +35,20 @@ export function createUsersStore() {
return await response.json()
}
async function create({ email, password }) {
const response = await api.post("/api/admin/users", {
async function create({ email, password, admin, builder }) {
const body = {
email,
password,
builder: { global: true },
roles: {},
})
init()
}
if (builder) {
body.builder = { global: true }
}
if (admin) {
body.admin = { global: true }
}
const response = await api.post("/api/admin/users", body)
await init()
return await response.json()
}
@ -43,8 +61,7 @@ export function createUsersStore() {
async function save(data) {
try {
const res = await post(`/api/admin/users`, data)
const json = await res.json()
return json
return await res.json()
} catch (error) {
console.log(error)
return error

View File

@ -167,13 +167,14 @@ exports.find = async ctx => {
}
exports.invite = async ctx => {
const { email } = ctx.request.body
const { email, userInfo } = ctx.request.body
const existing = await getGlobalUserByEmail(email)
if (existing) {
ctx.throw(400, "Email address already in use.")
}
await sendEmail(email, EmailTemplatePurpose.INVITATION, {
subject: "{{ company }} platform invitation",
info: userInfo,
})
ctx.body = {
message: "Invitation has been sent.",
@ -183,13 +184,15 @@ exports.invite = async ctx => {
exports.inviteAccept = async ctx => {
const { inviteCode, password, firstName, lastName } = ctx.request.body
try {
const email = await checkInviteCode(inviteCode)
// info is an extension of the user object that was stored by admin
const { email, info } = await checkInviteCode(inviteCode)
// only pass through certain props for accepting
ctx.request.body = {
firstName,
lastName,
password,
email,
...info,
}
// this will flesh out the body response
await exports.save(ctx)

View File

@ -47,6 +47,7 @@ function buildInviteValidation() {
// prettier-ignore
return joiValidator.body(Joi.object({
email: Joi.string().required(),
userInfo: Joi.object().optional(),
}).required())
}

View File

@ -9,6 +9,7 @@ const { passport } = require("@budibase/auth").auth
const logger = require("koa-pino-logger")
const http = require("http")
const api = require("./api")
const redis = require("./utilities/redis")
const app = new Koa()
@ -34,10 +35,16 @@ app.use(api.routes())
const server = http.createServer(app.callback())
destroyable(server)
server.on("close", () => console.log("Server Closed"))
server.on("close", async () => {
if (env.isProd()) {
console.log("Server Closed")
}
await redis.shutdown()
})
module.exports = server.listen(parseInt(env.PORT || 4002), async () => {
console.log(`Worker running on ${JSON.stringify(server.address())}`)
await redis.init()
})
process.on("uncaughtException", err => {

View File

@ -46,12 +46,12 @@ function createSMTPTransport(config) {
return nodemailer.createTransport(options)
}
async function getLinkCode(purpose, email, user) {
async function getLinkCode(purpose, email, user, info = null) {
switch (purpose) {
case EmailTemplatePurpose.PASSWORD_RECOVERY:
return getResetPasswordCode(user._id)
case EmailTemplatePurpose.INVITATION:
return getInviteCode(email)
return getInviteCode(email, info)
default:
return null
}
@ -136,13 +136,14 @@ exports.isEmailConfigured = async (groupId = null) => {
* @param {string|undefined} from If sending from an address that is not what is configured in the SMTP config.
* @param {string|undefined} contents If sending a custom email then can supply contents which will be added to it.
* @param {string|undefined} subject A custom subject can be specified if the config one is not desired.
* @param {object|undefined} info Pass in a structure of information to be stored alongside the invitation.
* @return {Promise<object>} returns details about the attempt to send email, e.g. if it is successful; based on
* nodemailer response.
*/
exports.sendEmail = async (
email,
purpose,
{ groupId, user, from, contents, subject } = {}
{ groupId, user, from, contents, subject, info } = {}
) => {
const db = new CouchDB(GLOBAL_DB)
let config = (await getSmtpConfiguration(db, groupId)) || {}
@ -151,7 +152,7 @@ exports.sendEmail = async (
}
const transport = createSMTPTransport(config)
// if there is a link code needed this will retrieve it
const code = await getLinkCode(purpose, email, user)
const code = await getLinkCode(purpose, email, user, info)
const context = await getSettingsTemplateContext(purpose, code)
const message = {
from: from || config.from,

View File

@ -12,15 +12,21 @@ function getExpirySecondsForDB(db) {
}
}
async function getClient(db) {
return await new Client(db).init()
let pwResetClient, invitationClient
function getClient(db) {
switch (db) {
case utils.Databases.PW_RESETS:
return pwResetClient
case utils.Databases.INVITATIONS:
return invitationClient
}
}
async function writeACode(db, value) {
const client = await getClient(db)
const code = newid()
await client.store(code, value, getExpirySecondsForDB(db))
client.finish()
return code
}
@ -33,10 +39,22 @@ async function getACode(db, code, deleteCode = true) {
if (deleteCode) {
await client.delete(code)
}
client.finish()
return value
}
exports.init = async () => {
pwResetClient = await new Client(utils.Databases.PW_RESETS).init()
invitationClient = await new Client(utils.Databases.PW_RESETS).init()
}
/**
* make sure redis connection is closed.
*/
exports.shutdown = async () => {
await pwResetClient.finish()
await invitationClient.finish()
}
/**
* Given a user ID this will store a code (that is returned) for an hour in redis.
* The user can then return this code for resetting their password (through their reset link).
@ -64,17 +82,18 @@ exports.checkResetPasswordCode = async (resetCode, deleteCode = true) => {
/**
* Generates an invitation code and writes it to redis - which can later be checked for user creation.
* @param {string} email the email address which the code is being sent to (for use later).
* @param {object|null} info Information to be carried along with the invitation.
* @return {Promise<string>} returns the code that was stored to redis.
*/
exports.getInviteCode = async email => {
return writeACode(utils.Databases.INVITATIONS, email)
exports.getInviteCode = async (email, info) => {
return writeACode(utils.Databases.INVITATIONS, { email, info })
}
/**
* Checks that the provided invite code is valid - will return the email address of user that was invited.
* @param {string} inviteCode the invite code that was provided as part of the link.
* @param {boolean} deleteCode whether or not the code should be deleted after retrieval - defaults to true.
* @return {Promise<string>} If the code is valid then an email address will be returned.
* @return {Promise<object>} If the code is valid then an email address will be returned.
*/
exports.checkInviteCode = async (inviteCode, deleteCode = true) => {
try {