Merge pull request #1545 from Budibase/fix/user-setup-builder-admin
Ability to configure invited users as admins/builders
This commit is contained in:
commit
b5da856c64
|
@ -5,6 +5,8 @@
|
||||||
Select,
|
Select,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
notifications,
|
notifications,
|
||||||
|
Toggle,
|
||||||
|
Label,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { createValidationStore, emailValidator } from "helpers/validation"
|
import { createValidationStore, emailValidator } from "helpers/validation"
|
||||||
import { users } from "stores/portal"
|
import { users } from "stores/portal"
|
||||||
|
@ -13,12 +15,12 @@
|
||||||
|
|
||||||
const options = ["Email onboarding", "Basic onboarding"]
|
const options = ["Email onboarding", "Basic onboarding"]
|
||||||
let selected = options[0]
|
let selected = options[0]
|
||||||
|
let builder, admin
|
||||||
|
|
||||||
const [email, error, touched] = createValidationStore("", emailValidator)
|
const [email, error, touched] = createValidationStore("", emailValidator)
|
||||||
|
|
||||||
async function createUserFlow() {
|
async function createUserFlow() {
|
||||||
const res = await users.invite($email)
|
const res = await users.invite({ email: $email, builder, admin })
|
||||||
console.log(res)
|
|
||||||
if (res.status) {
|
if (res.status) {
|
||||||
notifications.error(res.message)
|
notifications.error(res.message)
|
||||||
} else {
|
} else {
|
||||||
|
@ -56,4 +58,23 @@
|
||||||
placeholder="john@doe.com"
|
placeholder="john@doe.com"
|
||||||
label="Email"
|
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>
|
</ModalContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.toggle {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 78% 1fr;
|
||||||
|
align-items: center;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,13 +1,22 @@
|
||||||
<script>
|
<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 { createValidationStore, emailValidator } from "helpers/validation"
|
||||||
import { users } from "stores/portal"
|
import { users } from "stores/portal"
|
||||||
|
|
||||||
const [email, error, touched] = createValidationStore("", emailValidator)
|
const [email, error, touched] = createValidationStore("", emailValidator)
|
||||||
const password = Math.random().toString(36).substr(2, 20)
|
const password = Math.random().toString(36).substr(2, 20)
|
||||||
|
let builder = false,
|
||||||
|
admin = false
|
||||||
|
|
||||||
async function createUser() {
|
async function createUser() {
|
||||||
const res = await users.create({ email: $email, password })
|
const res = await users.create({ email: $email, password, builder, admin })
|
||||||
if (res.status) {
|
if (res.status) {
|
||||||
notifications.error(res.message)
|
notifications.error(res.message)
|
||||||
} else {
|
} else {
|
||||||
|
@ -37,4 +46,23 @@
|
||||||
error={$touched && $error}
|
error={$touched && $error}
|
||||||
/>
|
/>
|
||||||
<Input disabled label="Password" value={password} />
|
<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>
|
</ModalContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.toggle {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 78% 1fr;
|
||||||
|
align-items: center;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -11,10 +11,22 @@ export function createUsersStore() {
|
||||||
set(json)
|
set(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function invite(email) {
|
async function invite({ email, builder, admin }) {
|
||||||
const response = await api.post(`/api/admin/users/invite`, { email })
|
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()
|
return await response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function acceptInvite(inviteCode, password) {
|
async function acceptInvite(inviteCode, password) {
|
||||||
const response = await api.post("/api/admin/users/invite/accept", {
|
const response = await api.post("/api/admin/users/invite/accept", {
|
||||||
inviteCode,
|
inviteCode,
|
||||||
|
@ -23,14 +35,20 @@ export function createUsersStore() {
|
||||||
return await response.json()
|
return await response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create({ email, password }) {
|
async function create({ email, password, admin, builder }) {
|
||||||
const response = await api.post("/api/admin/users", {
|
const body = {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
builder: { global: true },
|
|
||||||
roles: {},
|
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()
|
return await response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,8 +61,7 @@ export function createUsersStore() {
|
||||||
async function save(data) {
|
async function save(data) {
|
||||||
try {
|
try {
|
||||||
const res = await post(`/api/admin/users`, data)
|
const res = await post(`/api/admin/users`, data)
|
||||||
const json = await res.json()
|
return await res.json()
|
||||||
return json
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
return error
|
return error
|
||||||
|
|
|
@ -167,13 +167,14 @@ exports.find = async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.invite = async ctx => {
|
exports.invite = async ctx => {
|
||||||
const { email } = ctx.request.body
|
const { email, userInfo } = ctx.request.body
|
||||||
const existing = await getGlobalUserByEmail(email)
|
const existing = await getGlobalUserByEmail(email)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
ctx.throw(400, "Email address already in use.")
|
ctx.throw(400, "Email address already in use.")
|
||||||
}
|
}
|
||||||
await sendEmail(email, EmailTemplatePurpose.INVITATION, {
|
await sendEmail(email, EmailTemplatePurpose.INVITATION, {
|
||||||
subject: "{{ company }} platform invitation",
|
subject: "{{ company }} platform invitation",
|
||||||
|
info: userInfo,
|
||||||
})
|
})
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
message: "Invitation has been sent.",
|
message: "Invitation has been sent.",
|
||||||
|
@ -183,13 +184,15 @@ exports.invite = async ctx => {
|
||||||
exports.inviteAccept = async ctx => {
|
exports.inviteAccept = async ctx => {
|
||||||
const { inviteCode, password, firstName, lastName } = ctx.request.body
|
const { inviteCode, password, firstName, lastName } = ctx.request.body
|
||||||
try {
|
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
|
// only pass through certain props for accepting
|
||||||
ctx.request.body = {
|
ctx.request.body = {
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
password,
|
password,
|
||||||
email,
|
email,
|
||||||
|
...info,
|
||||||
}
|
}
|
||||||
// this will flesh out the body response
|
// this will flesh out the body response
|
||||||
await exports.save(ctx)
|
await exports.save(ctx)
|
||||||
|
|
|
@ -47,6 +47,7 @@ function buildInviteValidation() {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return joiValidator.body(Joi.object({
|
return joiValidator.body(Joi.object({
|
||||||
email: Joi.string().required(),
|
email: Joi.string().required(),
|
||||||
|
userInfo: Joi.object().optional(),
|
||||||
}).required())
|
}).required())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ const { passport } = require("@budibase/auth").auth
|
||||||
const logger = require("koa-pino-logger")
|
const logger = require("koa-pino-logger")
|
||||||
const http = require("http")
|
const http = require("http")
|
||||||
const api = require("./api")
|
const api = require("./api")
|
||||||
|
const redis = require("./utilities/redis")
|
||||||
|
|
||||||
const app = new Koa()
|
const app = new Koa()
|
||||||
|
|
||||||
|
@ -34,10 +35,16 @@ app.use(api.routes())
|
||||||
const server = http.createServer(app.callback())
|
const server = http.createServer(app.callback())
|
||||||
destroyable(server)
|
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 () => {
|
module.exports = server.listen(parseInt(env.PORT || 4002), async () => {
|
||||||
console.log(`Worker running on ${JSON.stringify(server.address())}`)
|
console.log(`Worker running on ${JSON.stringify(server.address())}`)
|
||||||
|
await redis.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
process.on("uncaughtException", err => {
|
process.on("uncaughtException", err => {
|
||||||
|
|
|
@ -46,12 +46,12 @@ function createSMTPTransport(config) {
|
||||||
return nodemailer.createTransport(options)
|
return nodemailer.createTransport(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLinkCode(purpose, email, user) {
|
async function getLinkCode(purpose, email, user, info = null) {
|
||||||
switch (purpose) {
|
switch (purpose) {
|
||||||
case EmailTemplatePurpose.PASSWORD_RECOVERY:
|
case EmailTemplatePurpose.PASSWORD_RECOVERY:
|
||||||
return getResetPasswordCode(user._id)
|
return getResetPasswordCode(user._id)
|
||||||
case EmailTemplatePurpose.INVITATION:
|
case EmailTemplatePurpose.INVITATION:
|
||||||
return getInviteCode(email)
|
return getInviteCode(email, info)
|
||||||
default:
|
default:
|
||||||
return null
|
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} 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} 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 {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
|
* @return {Promise<object>} returns details about the attempt to send email, e.g. if it is successful; based on
|
||||||
* nodemailer response.
|
* nodemailer response.
|
||||||
*/
|
*/
|
||||||
exports.sendEmail = async (
|
exports.sendEmail = async (
|
||||||
email,
|
email,
|
||||||
purpose,
|
purpose,
|
||||||
{ groupId, user, from, contents, subject } = {}
|
{ groupId, user, from, contents, subject, info } = {}
|
||||||
) => {
|
) => {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
let config = (await getSmtpConfiguration(db, groupId)) || {}
|
let config = (await getSmtpConfiguration(db, groupId)) || {}
|
||||||
|
@ -151,7 +152,7 @@ exports.sendEmail = async (
|
||||||
}
|
}
|
||||||
const transport = createSMTPTransport(config)
|
const transport = createSMTPTransport(config)
|
||||||
// if there is a link code needed this will retrieve it
|
// 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 context = await getSettingsTemplateContext(purpose, code)
|
||||||
const message = {
|
const message = {
|
||||||
from: from || config.from,
|
from: from || config.from,
|
||||||
|
|
|
@ -12,15 +12,21 @@ function getExpirySecondsForDB(db) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getClient(db) {
|
let pwResetClient, invitationClient
|
||||||
return await new Client(db).init()
|
|
||||||
|
function getClient(db) {
|
||||||
|
switch (db) {
|
||||||
|
case utils.Databases.PW_RESETS:
|
||||||
|
return pwResetClient
|
||||||
|
case utils.Databases.INVITATIONS:
|
||||||
|
return invitationClient
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeACode(db, value) {
|
async function writeACode(db, value) {
|
||||||
const client = await getClient(db)
|
const client = await getClient(db)
|
||||||
const code = newid()
|
const code = newid()
|
||||||
await client.store(code, value, getExpirySecondsForDB(db))
|
await client.store(code, value, getExpirySecondsForDB(db))
|
||||||
client.finish()
|
|
||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,10 +39,22 @@ async function getACode(db, code, deleteCode = true) {
|
||||||
if (deleteCode) {
|
if (deleteCode) {
|
||||||
await client.delete(code)
|
await client.delete(code)
|
||||||
}
|
}
|
||||||
client.finish()
|
|
||||||
return value
|
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.
|
* 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).
|
* 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.
|
* 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 {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.
|
* @return {Promise<string>} returns the code that was stored to redis.
|
||||||
*/
|
*/
|
||||||
exports.getInviteCode = async email => {
|
exports.getInviteCode = async (email, info) => {
|
||||||
return writeACode(utils.Databases.INVITATIONS, email)
|
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.
|
* 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 {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.
|
* @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) => {
|
exports.checkInviteCode = async (inviteCode, deleteCode = true) => {
|
||||||
try {
|
try {
|
||||||
|
|
Loading…
Reference in New Issue