Update user creation UI
This commit is contained in:
parent
a519eb0cdc
commit
b331f47aa4
|
@ -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>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}`]])
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue