Updating system to allow setting builder/admin as a toggle during the invitation phase of a user.
This commit is contained in:
parent
a2917e3ffd
commit
d89c750761
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -47,6 +47,7 @@ function buildInviteValidation() {
|
|||
// prettier-ignore
|
||||
return joiValidator.body(Joi.object({
|
||||
email: Joi.string().required(),
|
||||
userInfo: Joi.object().optional(),
|
||||
}).required())
|
||||
}
|
||||
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue