1
0
Fork 0
mirror of synced 2024-06-24 00:50:24 +12:00

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 PasswordCopyRenderer from "./PasswordCopyRenderer.svelte"
import { parseToCsv } from "helpers/data/utils"
import { onMount } from "svelte"
export let userData
export let createUsersResponse
$: mappedData = userData.map(user => {
return {
email: user.email,
password: user.password,
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 {
email: user.email,
password: userDataIndex[user.email].password,
}
})
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 schema = {
const successSchema = {
email: {},
password: {},
}
const failedSchema = {
email: {},
reason: {},
}
const downloadCsvFile = () => {
const fileName = "passwords.csv"
const content = parseToCsv(["email", "password"], mappedData)
const content = parseToCsv(["email", "password"], successfulUsers)
download(fileName, content)
}
@ -43,35 +97,51 @@
<ModalContent
size="S"
title="Accounts created!"
{title}
confirmText="Done"
showCancelButton={false}
cancelText="Cancel"
showCloseIcon={false}
>
<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>
{#if hasFailure}
<Body size="XS">
{failureMessage}
</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>
<div class="container" on:click={downloadCsvFile}>
<div class="inner">
<Icon name="Download" />
<div class="container" on:click={downloadCsvFile}>
<div class="inner">
<Icon name="Download" />
<div style="margin-left: var(--spacing-m)">
<Body size="XS">Passwords CSV</Body>
<div style="margin-left: var(--spacing-m)">
<Body size="XS">Passwords CSV</Body>
</div>
</div>
</div>
</div>
<Table
{schema}
data={mappedData}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
customRenderers={[{ column: "password", component: PasswordCopyRenderer }]}
/>
<Table
schema={successSchema}
data={successfulUsers}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
customRenderers={[
{ column: "password", component: PasswordCopyRenderer },
]}
/>
{/if}
</ModalContent>
<style>

View file

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

View file

@ -63,10 +63,14 @@ export function createUsersStore() {
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
await search()
return response
}
async function del(id) {

View file

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

View file

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

View file

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

View file

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

View file

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