mirror of https://github.com/Budibase/budibase.git
99 changed files with 2558 additions and 1249 deletions
@ -0,0 +1,58 @@ |
|||
<script> |
|||
import { Button, Select, Label, Heading, Input } from "@budibase/bbui" |
|||
|
|||
export let value |
|||
|
|||
let presets = false |
|||
|
|||
const CRON_EXPRESSIONS = [ |
|||
{ |
|||
label: "Every Minute", |
|||
value: "* * * * *", |
|||
}, |
|||
{ |
|||
label: "Every Hour", |
|||
value: "0 * * * *", |
|||
}, |
|||
{ |
|||
label: "Every Morning at 8AM", |
|||
value: "0 8 * * *", |
|||
}, |
|||
{ |
|||
label: "Every Night at Midnight", |
|||
value: "0 0 * * *", |
|||
}, |
|||
{ |
|||
label: "Every Budibase Reboot", |
|||
value: "@reboot", |
|||
}, |
|||
] |
|||
</script> |
|||
|
|||
<div class="block-field"> |
|||
<Input bind:value /> |
|||
|
|||
<div class="presets"> |
|||
<Button on:click={() => (presets = !presets)} |
|||
>{presets ? "Hide" : "Show"} Presets</Button |
|||
> |
|||
{#if presets} |
|||
<Select |
|||
bind:value |
|||
secondary |
|||
extraThin |
|||
label="Presets" |
|||
options={CRON_EXPRESSIONS} |
|||
/> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.presets { |
|||
margin-top: var(--spacing-m); |
|||
} |
|||
.block-field { |
|||
padding-top: var(--spacing-s); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,19 @@ |
|||
<script> |
|||
import { admin } from "stores/portal" |
|||
import { onMount } from "svelte" |
|||
import { redirect } from "@roxi/routify" |
|||
|
|||
let loaded = false |
|||
|
|||
onMount(() => { |
|||
if ($admin?.checklist?.adminUser) { |
|||
$redirect("../") |
|||
} else { |
|||
loaded = true |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
{#if loaded} |
|||
<slot /> |
|||
{/if} |
|||
@ -0,0 +1,7 @@ |
|||
<script> |
|||
import { auth } from "stores/portal" |
|||
</script> |
|||
|
|||
{#if $auth.user} |
|||
<slot /> |
|||
{/if} |
|||
@ -0,0 +1,27 @@ |
|||
<script> |
|||
import { ModalContent, Body, notifications } from "@budibase/bbui" |
|||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte" |
|||
import { auth } from "stores/portal" |
|||
|
|||
let password |
|||
let error |
|||
|
|||
const updatePassword = async () => { |
|||
try { |
|||
await auth.updateSelf({ ...$auth.user, password }) |
|||
notifications.success("Information updated successfully") |
|||
} catch (error) { |
|||
notifications.error("Failed to update password") |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent |
|||
title="Update password" |
|||
confirmText="Update password" |
|||
onConfirm={updatePassword} |
|||
disabled={error || !password} |
|||
> |
|||
<Body size="S">Enter your new password below.</Body> |
|||
<PasswordRepeatInput bind:password bind:error /> |
|||
</ModalContent> |
|||
@ -0,0 +1,31 @@ |
|||
<script> |
|||
import { ModalContent, Body, Input, notifications } from "@budibase/bbui" |
|||
import { writable } from "svelte/store" |
|||
import { auth } from "stores/portal" |
|||
|
|||
const values = writable({ |
|||
firstName: $auth.user.firstName, |
|||
lastName: $auth.user.lastName, |
|||
}) |
|||
|
|||
const updateInfo = async () => { |
|||
try { |
|||
await auth.updateSelf({ ...$auth.user, ...$values }) |
|||
notifications.success("Information updated successfully") |
|||
} catch (error) { |
|||
notifications.error("Failed to update information") |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent |
|||
title="Update user information" |
|||
confirmText="Update information" |
|||
onConfirm={updateInfo} |
|||
> |
|||
<Body size="S"> |
|||
Personalise the platform by adding your first name and last name. |
|||
</Body> |
|||
<Input bind:value={$values.firstName} label="First name" /> |
|||
<Input bind:value={$values.lastName} label="Last name" /> |
|||
</ModalContent> |
|||
@ -0,0 +1,7 @@ |
|||
<script> |
|||
import { auth } from "stores/portal" |
|||
</script> |
|||
|
|||
{#if $auth.user} |
|||
<slot /> |
|||
{/if} |
|||
@ -0,0 +1,163 @@ |
|||
<script> |
|||
import { |
|||
Heading, |
|||
Layout, |
|||
Select, |
|||
Divider, |
|||
ActionMenu, |
|||
MenuItem, |
|||
Avatar, |
|||
Page, |
|||
Icon, |
|||
Body, |
|||
Modal, |
|||
} from "@budibase/bbui" |
|||
import { onMount } from "svelte" |
|||
import { apps, organisation, auth } from "stores/portal" |
|||
import { goto } from "@roxi/routify" |
|||
import { AppStatus } from "constants" |
|||
import { gradient } from "actions" |
|||
import UpdateUserInfoModal from "./_components/UpdateUserInfoModal.svelte" |
|||
import ChangePasswordModal from "./_components/ChangePasswordModal.svelte" |
|||
|
|||
let loaded = false |
|||
let userInfoModal |
|||
let changePasswordModal |
|||
|
|||
onMount(async () => { |
|||
await organisation.init() |
|||
await apps.load(AppStatus.DEV) |
|||
loaded = true |
|||
}) |
|||
</script> |
|||
|
|||
{#if loaded} |
|||
<div class="container"> |
|||
<Page> |
|||
<div class="content"> |
|||
<Layout noPadding> |
|||
<img src={$organisation.logoUrl} /> |
|||
<div class="info-title"> |
|||
<Layout noPadding gap="XS"> |
|||
<Heading size="L"> |
|||
Hey {$auth.user.firstName || $auth.user.email} |
|||
</Heading> |
|||
<Body> |
|||
Welcome to the {$organisation.company} portal. Below you'll find |
|||
the list of apps that you have access to. |
|||
</Body> |
|||
</Layout> |
|||
<ActionMenu align="right"> |
|||
<div slot="control" class="avatar"> |
|||
<Avatar size="M" name="John Doe" /> |
|||
<Icon size="XL" name="ChevronDown" /> |
|||
</div> |
|||
<MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}> |
|||
Update user information |
|||
</MenuItem> |
|||
<MenuItem |
|||
icon="LockClosed" |
|||
on:click={() => changePasswordModal.show()} |
|||
> |
|||
Update password |
|||
</MenuItem> |
|||
<MenuItem |
|||
icon="UserDeveloper" |
|||
on:click={() => $goto("../portal")} |
|||
> |
|||
Open developer mode |
|||
</MenuItem> |
|||
<MenuItem icon="LogOut" on:click={auth.logout}>Log out</MenuItem> |
|||
</ActionMenu> |
|||
</div> |
|||
<Divider /> |
|||
{#if $apps.length} |
|||
<Heading>Apps</Heading> |
|||
<div class="group"> |
|||
<Layout gap="S" noPadding> |
|||
{#each $apps as app, idx (app.appId)} |
|||
<a class="app" target="_blank" href={`/${app.appId}`}> |
|||
<div class="preview" use:gradient={{ seed: app.name }} /> |
|||
<div class="app-info"> |
|||
<Heading size="XS">{app.name}</Heading> |
|||
<Body size="S"> |
|||
Edited {Math.round(Math.random() * 10 + 1)} months ago |
|||
</Body> |
|||
</div> |
|||
<Icon name="ChevronRight" /> |
|||
</a> |
|||
{/each} |
|||
</Layout> |
|||
</div> |
|||
{:else} |
|||
<Layout gap="XS" noPadding> |
|||
<Heading size="S">You don't have access to any apps yet.</Heading> |
|||
<Body size="S"> |
|||
The apps you have access to will be listed here. |
|||
</Body> |
|||
</Layout> |
|||
{/if} |
|||
</Layout> |
|||
</div> |
|||
</Page> |
|||
</div> |
|||
<Modal bind:this={userInfoModal}> |
|||
<UpdateUserInfoModal /> |
|||
</Modal> |
|||
<Modal bind:this={changePasswordModal}> |
|||
<ChangePasswordModal /> |
|||
</Modal> |
|||
{/if} |
|||
|
|||
<style> |
|||
.container { |
|||
height: 100%; |
|||
overflow: auto; |
|||
} |
|||
.content { |
|||
padding: 60px 0; |
|||
width: 100%; |
|||
} |
|||
img { |
|||
width: 40px; |
|||
margin-bottom: -12px; |
|||
} |
|||
.info-title { |
|||
display: grid; |
|||
grid-template-columns: 1fr auto; |
|||
grid-gap: var(--spacing-xl); |
|||
} |
|||
.avatar { |
|||
display: grid; |
|||
grid-template-columns: auto auto; |
|||
place-items: center; |
|||
grid-gap: var(--spacing-xs); |
|||
} |
|||
.avatar:hover { |
|||
cursor: pointer; |
|||
filter: brightness(110%); |
|||
} |
|||
.group { |
|||
margin-top: var(--spacing-s); |
|||
} |
|||
.app { |
|||
display: grid; |
|||
grid-template-columns: auto 1fr auto; |
|||
background-color: var(--background); |
|||
padding: var(--spacing-xl); |
|||
border-radius: var(--border-radius-s); |
|||
align-items: center; |
|||
grid-gap: var(--spacing-xl); |
|||
color: inherit; |
|||
} |
|||
.app:hover { |
|||
cursor: pointer; |
|||
background: var(--spectrum-global-color-gray-200); |
|||
transition: background-color 130ms ease-in-out; |
|||
} |
|||
.preview { |
|||
height: 40px; |
|||
width: 60px; |
|||
border-radius: var(--border-radius-s); |
|||
} |
|||
</style> |
|||
@ -1,4 +1,4 @@ |
|||
<script> |
|||
import { goto } from "@roxi/routify" |
|||
$goto("./login") |
|||
import { redirect } from "@roxi/routify" |
|||
$redirect("./login") |
|||
</script> |
|||
|
|||
@ -1,4 +1,14 @@ |
|||
<script> |
|||
import { goto } from "@roxi/routify" |
|||
$goto("./portal") |
|||
import { redirect } from "@roxi/routify" |
|||
import { auth } from "stores/portal" |
|||
|
|||
$: { |
|||
if (!$auth.user) { |
|||
$redirect("./auth/login") |
|||
} else if ($auth.user.builder?.global) { |
|||
$redirect("./portal") |
|||
} else { |
|||
$redirect("./apps") |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
@ -1,4 +1,4 @@ |
|||
<script> |
|||
import { goto } from "@roxi/routify" |
|||
$goto("./apps") |
|||
import { redirect } from "@roxi/routify" |
|||
$redirect("./apps") |
|||
</script> |
|||
|
|||
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,115 +0,0 @@ |
|||
<script> |
|||
import GoogleLogo from "./_logos/Google.svelte" |
|||
import { |
|||
Button, |
|||
Heading, |
|||
Divider, |
|||
Label, |
|||
notifications, |
|||
Layout, |
|||
Input, |
|||
Body, |
|||
Page, |
|||
} from "@budibase/bbui" |
|||
import { onMount } from "svelte" |
|||
import api from "builderStore/api" |
|||
|
|||
const ConfigTypes = { |
|||
Google: "google", |
|||
// Github: "github", |
|||
// AzureAD: "ad", |
|||
} |
|||
|
|||
const ConfigFields = { |
|||
Google: ["clientID", "clientSecret", "callbackURL"], |
|||
} |
|||
|
|||
let google |
|||
|
|||
async function save(doc) { |
|||
try { |
|||
// Save an oauth config |
|||
const response = await api.post(`/api/admin/configs`, doc) |
|||
const json = await response.json() |
|||
if (response.status !== 200) throw new Error(json.message) |
|||
google._rev = json._rev |
|||
google._id = json._id |
|||
|
|||
notifications.success(`Settings saved.`) |
|||
} catch (err) { |
|||
notifications.error(`Failed to update OAuth settings. ${err}`) |
|||
} |
|||
} |
|||
|
|||
onMount(async () => { |
|||
// fetch the configs for oauth |
|||
const googleResponse = await api.get( |
|||
`/api/admin/configs/${ConfigTypes.Google}` |
|||
) |
|||
const googleDoc = await googleResponse.json() |
|||
|
|||
if (!googleDoc._id) { |
|||
google = { |
|||
type: ConfigTypes.Google, |
|||
config: {}, |
|||
} |
|||
} else { |
|||
google = googleDoc |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<Page> |
|||
<header> |
|||
<Heading size="M">OAuth</Heading> |
|||
<Body size="S"> |
|||
Every budibase app comes with basic authentication (email/password) |
|||
included. You can add additional authentication methods from the options |
|||
below. |
|||
</Body> |
|||
</header> |
|||
<Divider /> |
|||
{#if google} |
|||
<div class="config-form"> |
|||
<Layout gap="S"> |
|||
<Heading size="S"> |
|||
<span> |
|||
<GoogleLogo /> |
|||
Google |
|||
</span> |
|||
</Heading> |
|||
{#each ConfigFields.Google as field} |
|||
<div class="form-row"> |
|||
<Label>{field}</Label> |
|||
<Input bind:value={google.config[field]} /> |
|||
</div> |
|||
{/each} |
|||
</Layout> |
|||
<Button primary on:click={() => save(google)}>Save</Button> |
|||
</div> |
|||
<Divider /> |
|||
{/if} |
|||
</Page> |
|||
|
|||
<style> |
|||
.config-form { |
|||
margin-top: 42px; |
|||
margin-bottom: 42px; |
|||
} |
|||
|
|||
.form-row { |
|||
display: grid; |
|||
grid-template-columns: 20% 1fr; |
|||
grid-gap: var(--spacing-l); |
|||
} |
|||
|
|||
span { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: var(--spacing-s); |
|||
} |
|||
|
|||
header { |
|||
margin-bottom: 42px; |
|||
} |
|||
</style> |
|||
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,114 +0,0 @@ |
|||
<script> |
|||
import GoogleLogo from "./_logos/Google.svelte" |
|||
import { |
|||
Button, |
|||
Page, |
|||
Heading, |
|||
Divider, |
|||
Label, |
|||
notifications, |
|||
Layout, |
|||
Input, |
|||
Body, |
|||
} from "@budibase/bbui" |
|||
import { onMount } from "svelte" |
|||
import api from "builderStore/api" |
|||
|
|||
const ConfigTypes = { |
|||
Google: "google", |
|||
// Github: "github", |
|||
// AzureAD: "ad", |
|||
} |
|||
|
|||
const ConfigFields = { |
|||
Google: ["clientID", "clientSecret", "callbackURL"], |
|||
} |
|||
|
|||
let google |
|||
|
|||
async function save(doc) { |
|||
try { |
|||
// Save an oauth config |
|||
const response = await api.post(`/api/admin/configs`, doc) |
|||
const json = await response.json() |
|||
if (response.status !== 200) throw new Error(json.message) |
|||
google._rev = json._rev |
|||
google._id = json._id |
|||
|
|||
notifications.success(`Settings saved.`) |
|||
} catch (err) { |
|||
notifications.error(`Failed to update OAuth settings. ${err}`) |
|||
} |
|||
} |
|||
|
|||
onMount(async () => { |
|||
// fetch the configs for oauth |
|||
const googleResponse = await api.get( |
|||
`/api/admin/configs/${ConfigTypes.Google}` |
|||
) |
|||
const googleDoc = await googleResponse.json() |
|||
|
|||
if (!googleDoc._id) { |
|||
google = { |
|||
type: ConfigTypes.Google, |
|||
config: {}, |
|||
} |
|||
} else { |
|||
google = googleDoc |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<Page> |
|||
<Layout noPadding> |
|||
<div> |
|||
<Heading size="M">OAuth</Heading> |
|||
<Body> |
|||
Every budibase app comes with basic authentication (email/password) |
|||
included. You can add additional authentication methods from the options |
|||
below. |
|||
</Body> |
|||
</div> |
|||
<Divider /> |
|||
{#if google} |
|||
<div> |
|||
<Heading size="S"> |
|||
<span> |
|||
<GoogleLogo /> |
|||
Google |
|||
</span> |
|||
</Heading> |
|||
<Body> |
|||
To allow users to authenticate using their Google accounts, fill out |
|||
the fields below. |
|||
</Body> |
|||
</div> |
|||
|
|||
{#each ConfigFields.Google as field} |
|||
<div class="form-row"> |
|||
<Label size="L">{field}</Label> |
|||
<Input bind:value={google.config[field]} /> |
|||
</div> |
|||
{/each} |
|||
<div> |
|||
<Button cta on:click={() => save(google)}>Save</Button> |
|||
</div> |
|||
<Divider /> |
|||
{/if} |
|||
</Layout> |
|||
</Page> |
|||
|
|||
<style> |
|||
.form-row { |
|||
display: grid; |
|||
grid-template-columns: 20% 1fr; |
|||
grid-gap: var(--spacing-l); |
|||
align-items: center; |
|||
} |
|||
|
|||
span { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: var(--spacing-s); |
|||
} |
|||
</style> |
|||
@ -1,4 +1,4 @@ |
|||
<script> |
|||
import { goto } from "@roxi/routify" |
|||
$goto("./builder") |
|||
import { redirect } from "@roxi/routify" |
|||
$redirect("./builder") |
|||
</script> |
|||
|
|||
@ -1,90 +0,0 @@ |
|||
const roles = require("@budibase/auth/roles") |
|||
const userController = require("../../api/controllers/user") |
|||
const env = require("../../environment") |
|||
const usage = require("../../utilities/usageQuota") |
|||
|
|||
module.exports.definition = { |
|||
description: "Create a new user", |
|||
tagline: "Create user {{inputs.email}}", |
|||
icon: "ri-user-add-line", |
|||
name: "Create User", |
|||
type: "ACTION", |
|||
stepId: "CREATE_USER", |
|||
inputs: { |
|||
roleId: roles.BUILTIN_ROLE_IDS.POWER, |
|||
}, |
|||
schema: { |
|||
inputs: { |
|||
properties: { |
|||
email: { |
|||
type: "string", |
|||
customType: "email", |
|||
title: "Email", |
|||
}, |
|||
password: { |
|||
type: "string", |
|||
title: "Password", |
|||
}, |
|||
roleId: { |
|||
type: "string", |
|||
title: "Role", |
|||
enum: roles.BUILTIN_ROLE_ID_ARRAY, |
|||
pretty: roles.BUILTIN_ROLE_NAME_ARRAY, |
|||
}, |
|||
}, |
|||
required: ["email", "password", "roleId"], |
|||
}, |
|||
outputs: { |
|||
properties: { |
|||
id: { |
|||
type: "string", |
|||
description: "The identifier of the new user", |
|||
}, |
|||
revision: { |
|||
type: "string", |
|||
description: "The revision of the new user", |
|||
}, |
|||
response: { |
|||
type: "object", |
|||
description: "The response from the user table", |
|||
}, |
|||
success: { |
|||
type: "boolean", |
|||
description: "Whether the action was successful", |
|||
}, |
|||
}, |
|||
required: ["id", "revision", "success"], |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
module.exports.run = async function ({ inputs, appId, apiKey, emitter }) { |
|||
const { email, password, roleId } = inputs |
|||
const ctx = { |
|||
appId, |
|||
request: { |
|||
body: { email, password, roleId }, |
|||
}, |
|||
eventEmitter: emitter, |
|||
} |
|||
|
|||
try { |
|||
if (env.isProd()) { |
|||
await usage.update(apiKey, usage.Properties.USER, 1) |
|||
} |
|||
await userController.createMetadata(ctx) |
|||
return { |
|||
response: ctx.body, |
|||
// internal property not returned through the API
|
|||
id: ctx.body._id, |
|||
revision: ctx.body._rev, |
|||
success: ctx.status === 200, |
|||
} |
|||
} catch (err) { |
|||
console.error(err) |
|||
return { |
|||
success: false, |
|||
response: err, |
|||
} |
|||
} |
|||
} |
|||
@ -1,42 +0,0 @@ |
|||
const usageQuota = require("../../utilities/usageQuota") |
|||
const setup = require("./utilities") |
|||
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") |
|||
const { InternalTables } = require("../../db/utils") |
|||
|
|||
jest.mock("../../utilities/usageQuota") |
|||
|
|||
describe("test the create user action", () => { |
|||
let config = setup.getConfig() |
|||
let user |
|||
|
|||
beforeEach(async () => { |
|||
await config.init() |
|||
user = { |
|||
email: "test@test.com", |
|||
password: "password", |
|||
roleId: BUILTIN_ROLE_IDS.POWER |
|||
} |
|||
}) |
|||
|
|||
afterAll(setup.afterAll) |
|||
|
|||
it("should be able to run the action", async () => { |
|||
const res = await setup.runStep(setup.actions.CREATE_USER.stepId, user) |
|||
expect(res.id).toBeDefined() |
|||
expect(res.revision).toBeDefined() |
|||
const userDoc = await config.getRow(InternalTables.USER_METADATA, res.id) |
|||
expect(userDoc).toBeDefined() |
|||
}) |
|||
|
|||
it("should return an error if no inputs provided", async () => { |
|||
const res = await setup.runStep(setup.actions.CREATE_USER.stepId, {}) |
|||
expect(res.success).toEqual(false) |
|||
}) |
|||
|
|||
it("check usage quota attempts", async () => { |
|||
await setup.runInProd(async () => { |
|||
await setup.runStep(setup.actions.CREATE_USER.stepId, user) |
|||
expect(usageQuota.update).toHaveBeenCalledWith(setup.apiKey, "users", 1) |
|||
}) |
|||
}) |
|||
}) |
|||
File diff suppressed because it is too large
@ -1,10 +1,11 @@ |
|||
const Router = require("@koa/router") |
|||
const controller = require("../../controllers/admin/roles") |
|||
const adminOnly = require("../../../middleware/adminOnly") |
|||
|
|||
const router = Router() |
|||
|
|||
router |
|||
.get("/api/admin/roles", controller.fetch) |
|||
.get("/api/admin/roles/:appId", controller.find) |
|||
.get("/api/admin/roles", adminOnly, controller.fetch) |
|||
.get("/api/admin/roles/:appId", adminOnly, controller.find) |
|||
|
|||
module.exports = router |
|||
|
|||
@ -0,0 +1,6 @@ |
|||
module.exports = async (ctx, next) => { |
|||
if (!ctx.user || !ctx.user.admin || !ctx.user.admin.global) { |
|||
ctx.throw(403, "Admin user only endpoint.") |
|||
} |
|||
return next() |
|||
} |
|||
Loading…
Reference in new issue