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 { 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>
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}`]])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue