Update user creation UI

This commit is contained in:
Rory Powell 2022-08-25 22:56:58 +01:00
parent a519eb0cdc
commit b331f47aa4
8 changed files with 150 additions and 74 deletions

View File

@ -2,24 +2,78 @@
import { Body, ModalContent, Table, Icon } from "@budibase/bbui" import { Body, ModalContent, Table, Icon } from "@budibase/bbui"
import PasswordCopyRenderer from "./PasswordCopyRenderer.svelte" import PasswordCopyRenderer from "./PasswordCopyRenderer.svelte"
import { parseToCsv } from "helpers/data/utils" import { parseToCsv } from "helpers/data/utils"
import { onMount } from "svelte"
export let userData export let userData
export let createUsersResponse
$: mappedData = userData.map(user => { let hasSuccess
let hasFailure
let title
let failureMessage
let userDataIndex
let successfulUsers
let unsuccessfulUsers
const setTitle = () => {
if (hasSuccess) {
title = "Users created!"
} else if (hasFailure) {
title = "Oops!"
}
}
const setFailureMessage = () => {
if (hasSuccess) {
failureMessage = "However there was a problem creating some users."
} else {
failureMessage = "There was a problem creating some users."
}
}
const setUsers = () => {
userDataIndex = userData.reduce((prev, current) => {
prev[current.email] = current
return prev
}, {})
successfulUsers = createUsersResponse.successful.map(user => {
return { return {
email: user.email, email: user.email,
password: user.password, password: userDataIndex[user.email].password,
} }
}) })
const schema = { unsuccessfulUsers = createUsersResponse.unsuccessful.map(user => {
return {
email: user.email,
reason: user.reason,
}
})
}
onMount(() => {
hasSuccess = createUsersResponse.successful.length
hasFailure = createUsersResponse.unsuccessful.length
setTitle()
setFailureMessage()
setUsers()
})
const successSchema = {
email: {}, email: {},
password: {}, password: {},
} }
const failedSchema = {
email: {},
reason: {},
}
const downloadCsvFile = () => { const downloadCsvFile = () => {
const fileName = "passwords.csv" const fileName = "passwords.csv"
const content = parseToCsv(["email", "password"], mappedData) const content = parseToCsv(["email", "password"], successfulUsers)
download(fileName, content) download(fileName, content)
} }
@ -43,15 +97,28 @@
<ModalContent <ModalContent
size="S" size="S"
title="Accounts created!" {title}
confirmText="Done" confirmText="Done"
showCancelButton={false} showCancelButton={false}
cancelText="Cancel" cancelText="Cancel"
showCloseIcon={false} showCloseIcon={false}
> >
{#if hasFailure}
<Body size="XS"> <Body size="XS">
All your new users can be accessed through the autogenerated passwords. Take {failureMessage}
note of these passwords or download the CSV file. </Body>
<Table
schema={failedSchema}
data={unsuccessfulUsers}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
/>
{/if}
{#if hasSuccess}
<Body size="XS">
All your new users can be accessed through the autogenerated passwords.
Take note of these passwords or download the CSV file.
</Body> </Body>
<div class="container" on:click={downloadCsvFile}> <div class="container" on:click={downloadCsvFile}>
@ -65,13 +132,16 @@
</div> </div>
<Table <Table
{schema} schema={successSchema}
data={mappedData} data={successfulUsers}
allowEditColumns={false} allowEditColumns={false}
allowEditRows={false} allowEditRows={false}
allowSelectRows={false} allowSelectRows={false}
customRenderers={[{ column: "password", component: PasswordCopyRenderer }]} customRenderers={[
{ column: "password", component: PasswordCopyRenderer },
]}
/> />
{/if}
</ModalContent> </ModalContent>
<style> <style>

View File

@ -55,6 +55,7 @@
apps: {}, apps: {},
} }
$: userData = [] $: userData = []
$: createUsersResponse = { successful: [], unsuccessful: [] }
$: page = $pageInfo.page $: page = $pageInfo.page
$: fetchUsers(page, searchEmail) $: fetchUsers(page, searchEmail)
$: { $: {
@ -116,8 +117,9 @@
newUsers.push(user) newUsers.push(user)
} }
if (!newUsers.length) if (!newUsers.length) {
notifications.info("Duplicated! There is no new users to add.") notifications.info("Duplicated! There is no new users to add.")
}
return { ...userData, users: newUsers } return { ...userData, users: newUsers }
} }
@ -144,7 +146,9 @@
async function createUser() { async function createUser() {
try { try {
await users.create(await removingDuplicities(userData)) createUsersResponse = await users.create(
await removingDuplicities(userData)
)
notifications.success("Successfully created user") notifications.success("Successfully created user")
await groups.actions.init() await groups.actions.init()
passwordModal.show() passwordModal.show()
@ -284,7 +288,7 @@
</Modal> </Modal>
<Modal bind:this={passwordModal}> <Modal bind:this={passwordModal}>
<PasswordModal userData={userData.users} /> <PasswordModal {createUsersResponse} userData={userData.users} />
</Modal> </Modal>
<Modal bind:this={importUsersModal}> <Modal bind:this={importUsersModal}>

View File

@ -63,10 +63,14 @@ export function createUsersStore() {
return body return body
}) })
await API.createUsers({ users: mappedUsers, groups: data.groups }) const response = await API.createUsers({
users: mappedUsers,
groups: data.groups,
})
// re-search from first page // re-search from first page
await search() await search()
return response
} }
async function del(id) { async function del(id) {

View File

@ -1,7 +1,13 @@
export interface RowValue {
rev: string
deleted: boolean
}
export interface RowResponse<T> { export interface RowResponse<T> {
id: string id: string
key: string key: string
value: any error: string
value: RowValue
doc: T doc: T
} }

View File

@ -2,14 +2,8 @@ import Router from "@koa/router"
const compress = require("koa-compress") const compress = require("koa-compress")
const zlib = require("zlib") const zlib = require("zlib")
import { routes } from "./routes" import { routes } from "./routes"
import {
buildAuthMiddleware,
auditLog,
buildTenancyMiddleware,
buildCsrfMiddleware,
} from "@budibase/backend-core/auth"
import { middleware as pro } from "@budibase/pro" import { middleware as pro } from "@budibase/pro"
import { errors } from "@budibase/backend-core" import { errors, auth, middleware } from "@budibase/backend-core"
import { APIError } from "@budibase/types" import { APIError } from "@budibase/types"
const PUBLIC_ENDPOINTS = [ const PUBLIC_ENDPOINTS = [
@ -98,9 +92,9 @@ router
}) })
) )
.use("/health", ctx => (ctx.status = 200)) .use("/health", ctx => (ctx.status = 200))
.use(buildAuthMiddleware(PUBLIC_ENDPOINTS)) .use(auth.buildAuthMiddleware(PUBLIC_ENDPOINTS))
.use(buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS)) .use(auth.buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS))
.use(buildCsrfMiddleware({ noCsrfPatterns: NO_CSRF_ENDPOINTS })) .use(auth.buildCsrfMiddleware({ noCsrfPatterns: NO_CSRF_ENDPOINTS }))
.use(pro.licensing()) .use(pro.licensing())
// for now no public access is allowed to worker (bar health check) // for now no public access is allowed to worker (bar health check)
.use((ctx, next) => { .use((ctx, next) => {
@ -115,7 +109,7 @@ router
} }
return next() return next()
}) })
.use(auditLog) .use(middleware.auditLog)
// error handling middleware - TODO: This could be moved to backend-core // error handling middleware - TODO: This could be moved to backend-core
router.use(async (ctx, next) => { router.use(async (ctx, next) => {

View File

@ -167,9 +167,7 @@ describe("/api/global/users", () => {
const response = await api.users.saveUser(user, 400) const response = await api.users.saveUser(user, 400)
expect(response.body.message).toBe( expect(response.body.message).toBe(`Unavailable`)
`Email address ${user.email} already in use.`
)
expect(events.user.created).toBeCalledTimes(0) expect(events.user.created).toBeCalledTimes(0)
}) })
@ -181,9 +179,7 @@ describe("/api/global/users", () => {
delete user._id delete user._id
const response = await api.users.saveUser(user, 400) const response = await api.users.saveUser(user, 400)
expect(response.body.message).toBe( expect(response.body.message).toBe(`Unavailable`)
`Email address ${user.email} already in use.`
)
expect(events.user.created).toBeCalledTimes(0) expect(events.user.created).toBeCalledTimes(0)
}) })
}) })
@ -195,9 +191,7 @@ describe("/api/global/users", () => {
const response = await api.users.saveUser(user, 400) const response = await api.users.saveUser(user, 400)
expect(response.body.message).toBe( expect(response.body.message).toBe(`Unavailable`)
`Email address ${user.email} already in use.`
)
expect(events.user.created).toBeCalledTimes(0) expect(events.user.created).toBeCalledTimes(0)
}) })
}) })

View File

@ -29,6 +29,7 @@ import {
AllDocsResponse, AllDocsResponse,
RowResponse, RowResponse,
BulkDocsResponse, BulkDocsResponse,
AccountMetadata,
} from "@budibase/types" } from "@budibase/types"
import { groups as groupUtils } from "@budibase/pro" import { groups as groupUtils } from "@budibase/pro"
@ -161,7 +162,7 @@ const validateUniqueUser = async (email: string, tenantId: string) => {
if (env.MULTI_TENANCY) { if (env.MULTI_TENANCY) {
const tenantUser = await tenancy.getTenantUser(email) const tenantUser = await tenancy.getTenantUser(email)
if (tenantUser != null && tenantUser.tenantId !== tenantId) { if (tenantUser != null && tenantUser.tenantId !== tenantId) {
throw `Email address ${email} already in use.` throw `Unavailable`
} }
} }
@ -169,7 +170,7 @@ const validateUniqueUser = async (email: string, tenantId: string) => {
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
const account = await accounts.getAccount(email) const account = await accounts.getAccount(email)
if (account && account.verified && account.tenantId !== tenantId) { if (account && account.verified && account.tenantId !== tenantId) {
throw `Email address ${email} already in use.` throw `Unavailable`
} }
} }
} }
@ -197,7 +198,7 @@ export const save = async (
// no id was specified - load from email instead // no id was specified - load from email instead
dbUser = await usersCore.getGlobalUserByEmail(email) dbUser = await usersCore.getGlobalUserByEmail(email)
if (dbUser && dbUser._id !== _id) { if (dbUser && dbUser._id !== _id) {
throw `Email address ${email} already in use.` throw `Unavailable`
} }
} else { } else {
throw new Error("_id or email is required") throw new Error("_id or email is required")
@ -275,18 +276,21 @@ const getExistingPlatformUsers = async (
return dbUtils.doWithDB( return dbUtils.doWithDB(
StaticDatabases.PLATFORM_INFO.name, StaticDatabases.PLATFORM_INFO.name,
async (infoDb: any) => { async (infoDb: any) => {
const response = await infoDb.allDocs({ const response: AllDocsResponse<PlatformUserByEmail> =
await infoDb.allDocs({
keys: emails, keys: emails,
include_docs: true, include_docs: true,
}) })
return response.rows return response.rows
.filter((row: any) => row.error !== "not_found") .filter(row => row.doc && (row.error !== "not_found") !== null)
.map((row: any) => row.doc) .map((row: any) => row.doc)
} }
) )
} }
const getExistingAccounts = async (emails: string[]): Promise<Account[]> => { const getExistingAccounts = async (
emails: string[]
): Promise<AccountMetadata[]> => {
return dbUtils.queryPlatformView(ViewName.ACCOUNT_BY_EMAIL, { return dbUtils.queryPlatformView(ViewName.ACCOUNT_BY_EMAIL, {
keys: emails, keys: emails,
include_docs: true, include_docs: true,
@ -305,17 +309,13 @@ const searchExistingEmails = async (emails: string[]) => {
let matchedEmails: string[] = [] let matchedEmails: string[] = []
const existingTenantUsers = await getExistingTenantUsers(emails) const existingTenantUsers = await getExistingTenantUsers(emails)
matchedEmails.push(...existingTenantUsers.map((user: User) => user.email)) matchedEmails.push(...existingTenantUsers.map(user => user.email))
const existingPlatformUsers = await getExistingPlatformUsers(emails) const existingPlatformUsers = await getExistingPlatformUsers(emails)
matchedEmails.push( matchedEmails.push(...existingPlatformUsers.map(user => user._id!))
...existingPlatformUsers.map((user: PlatformUserByEmail) => user._id!)
)
const existingAccounts = await getExistingAccounts(emails) const existingAccounts = await getExistingAccounts(emails)
matchedEmails.push( matchedEmails.push(...existingAccounts.map(account => account.email))
...existingAccounts.map((account: Account) => account.email)
)
return [...new Set(matchedEmails)] return [...new Set(matchedEmails)]
} }
@ -341,7 +341,7 @@ export const bulkCreate = async (
) { ) {
unsuccessful.push({ unsuccessful.push({
email: newUser.email, email: newUser.email,
reason: `Email address ${newUser.email} already in use.`, reason: `Unavailable`,
}) })
continue continue
} }

View File

@ -4,11 +4,15 @@ dbConfig.init()
import env from "../environment" import env from "../environment"
import controllers from "./controllers" import controllers from "./controllers"
const supertest = require("supertest") const supertest = require("supertest")
import { jwt } from "@budibase/backend-core/auth"
import { Cookies, Headers } from "@budibase/backend-core/constants"
import { Configs } from "../constants" import { Configs } from "../constants"
import { users, tenancy } from "@budibase/backend-core" import {
import { createASession } from "@budibase/backend-core/sessions" users,
tenancy,
Cookies,
Headers,
sessions,
auth,
} from "@budibase/backend-core"
import { TENANT_ID, TENANT_1, CSRF_TOKEN } from "./structures" import { TENANT_ID, TENANT_1, CSRF_TOKEN } from "./structures"
import structures from "./structures" import structures from "./structures"
import { CreateUserResponse, User, AuthToken } from "@budibase/types" import { CreateUserResponse, User, AuthToken } from "@budibase/types"
@ -137,7 +141,7 @@ class TestConfiguration {
} }
async createSession(user: User) { async createSession(user: User) {
await createASession(user._id!, { await sessions.createASession(user._id!, {
sessionId: "sessionid", sessionId: "sessionid",
tenantId: user.tenantId, tenantId: user.tenantId,
csrfToken: CSRF_TOKEN, csrfToken: CSRF_TOKEN,
@ -156,7 +160,7 @@ class TestConfiguration {
sessionId: "sessionid", sessionId: "sessionid",
tenantId: user.tenantId, tenantId: user.tenantId,
} }
const authCookie = jwt.sign(authToken, env.JWT_SECRET) const authCookie = auth.jwt.sign(authToken, env.JWT_SECRET)
return { return {
Accept: "application/json", Accept: "application/json",
...this.cookieHeader([`${Cookies.Auth}=${authCookie}`]), ...this.cookieHeader([`${Cookies.Auth}=${authCookie}`]),
@ -237,7 +241,7 @@ class TestConfiguration {
// CONFIGS - OIDC // CONFIGS - OIDC
getOIDConfigCookie(configId: string) { getOIDConfigCookie(configId: string) {
const token = jwt.sign(configId, env.JWT_SECRET) const token = auth.jwt.sign(configId, env.JWT_SECRET)
return this.cookieHeader([[`${Cookies.OIDC_CONFIG}=${token}`]]) return this.cookieHeader([[`${Cookies.OIDC_CONFIG}=${token}`]])
} }