Merge pull request #1452 from Budibase/first-time-setup
First time setup and Configuration Checklist
This commit is contained in:
commit
52d87c8267
|
@ -157,7 +157,7 @@ const getScopedFullConfig = async function (db, { type, user, group }) {
|
||||||
(a, b) => determineScore(a) - determineScore(b)
|
(a, b) => determineScore(a) - determineScore(b)
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
return scopedConfig.doc
|
return scopedConfig && scopedConfig.doc
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getScopedConfig(db, params) {
|
async function getScopedConfig(db, params) {
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
<script>
|
||||||
|
import { isActive, url, goto } from "@roxi/routify"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import {
|
||||||
|
ActionMenu,
|
||||||
|
Checkbox,
|
||||||
|
Body,
|
||||||
|
MenuItem,
|
||||||
|
Icon,
|
||||||
|
Heading,
|
||||||
|
Avatar,
|
||||||
|
Search,
|
||||||
|
Layout,
|
||||||
|
ProgressCircle,
|
||||||
|
SideNavigation as Navigation,
|
||||||
|
SideNavigationItem as Item,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
import { organisation, admin } from "stores/portal"
|
||||||
|
|
||||||
|
const MESSAGES = {
|
||||||
|
apps: "Create your first app",
|
||||||
|
smtp: "Set up email",
|
||||||
|
adminUser: "Create your first user",
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionMenu>
|
||||||
|
<div slot="control" class="icon">
|
||||||
|
<ProgressCircle size="S" value={$admin.onboardingProgress} />
|
||||||
|
</div>
|
||||||
|
<MenuItem disabled>
|
||||||
|
<header class="item">
|
||||||
|
<Heading size="XXS">Get Started Checklist</Heading>
|
||||||
|
<ProgressCircle size="S" value={$admin.onboardingProgress} />
|
||||||
|
</header>
|
||||||
|
</MenuItem>
|
||||||
|
{#each Object.keys($admin.checklist) as checklistItem, idx}
|
||||||
|
<MenuItem>
|
||||||
|
<div class="item">
|
||||||
|
<span>{idx + 1}. {MESSAGES[checklistItem]}</span>
|
||||||
|
<Checkbox value={!!$admin.checklist[checklistItem]} />
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
{/each}
|
||||||
|
</ActionMenu>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.item {
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid-template-columns: 200px 20px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import {
|
||||||
|
SideNavigation as Navigation,
|
||||||
|
SideNavigationItem as Item,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { admin } from "stores/portal"
|
||||||
|
import LoginForm from "components/login/LoginForm.svelte"
|
||||||
|
import BuilderSettingsButton from "components/start/BuilderSettingsButton.svelte"
|
||||||
|
import LogoutButton from "components/start/LogoutButton.svelte"
|
||||||
|
import Logo from "/assets/budibase-logo.svg"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
let checklist
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await admin.init()
|
||||||
|
if (!$admin?.checklist?.adminUser) {
|
||||||
|
$goto("./admin")
|
||||||
|
} else {
|
||||||
|
$goto("./portal")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $admin.checklist}
|
||||||
|
<slot />
|
||||||
|
{/if}
|
|
@ -0,0 +1,69 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Heading,
|
||||||
|
Label,
|
||||||
|
notifications,
|
||||||
|
Layout,
|
||||||
|
Input,
|
||||||
|
Body,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
let adminUser = {}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
try {
|
||||||
|
// Save the admin user
|
||||||
|
const response = await api.post(`/api/admin/users/init`, adminUser)
|
||||||
|
|
||||||
|
const json = await response.json()
|
||||||
|
if (response.status !== 200) throw new Error(json.message)
|
||||||
|
notifications.success(`Admin user created.`)
|
||||||
|
$goto("../portal")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Failed to create admin user.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<Heading size="M">Create an admin user</Heading>
|
||||||
|
<Body size="S">The admin user has access to everything in budibase.</Body>
|
||||||
|
</header>
|
||||||
|
<div class="config-form">
|
||||||
|
<Layout gap="S">
|
||||||
|
<Input label="email" bind:value={adminUser.email} />
|
||||||
|
<Input
|
||||||
|
label="password"
|
||||||
|
type="password"
|
||||||
|
bind:value={adminUser.password}
|
||||||
|
/>
|
||||||
|
<Button cta on:click={save}>Create super admin user</Button>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
width: 80%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form {
|
||||||
|
margin-bottom: 42px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -23,7 +23,6 @@
|
||||||
<div class="nav-top">
|
<div class="nav-top">
|
||||||
<Navigation>
|
<Navigation>
|
||||||
<Item href="/builder/" icon="Apps" selected>Apps</Item>
|
<Item href="/builder/" icon="Apps" selected>Apps</Item>
|
||||||
<Item href="/builder/oauth/" icon="OAuth" selected>OAuth</Item>
|
|
||||||
<Item external href="https://portal.budi.live/" icon="Servers">
|
<Item external href="https://portal.budi.live/" icon="Servers">
|
||||||
Hosting
|
Hosting
|
||||||
</Item>
|
</Item>
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { isActive, url } from "@roxi/routify"
|
import { isActive, url, goto } from "@roxi/routify"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import {
|
import {
|
||||||
|
ActionMenu,
|
||||||
|
Checkbox,
|
||||||
|
MenuItem,
|
||||||
Icon,
|
Icon,
|
||||||
|
Heading,
|
||||||
Avatar,
|
Avatar,
|
||||||
Search,
|
Search,
|
||||||
Layout,
|
Layout,
|
||||||
|
@ -10,13 +14,21 @@
|
||||||
SideNavigation as Navigation,
|
SideNavigation as Navigation,
|
||||||
SideNavigationItem as Item,
|
SideNavigationItem as Item,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { organisation } from "stores/portal"
|
import api from "builderStore/api"
|
||||||
|
import ConfigChecklist from "components/common/ConfigChecklist.svelte"
|
||||||
|
import { organisation, admin } from "stores/portal"
|
||||||
|
|
||||||
organisation.init()
|
organisation.init()
|
||||||
|
|
||||||
let onBoardingProgress, user
|
let orgName
|
||||||
|
let orgLogo
|
||||||
|
let user
|
||||||
|
|
||||||
async function getInfo() {
|
async function getInfo() {
|
||||||
onBoardingProgress = 20
|
// fetch orgInfo
|
||||||
|
orgName = "ACME Inc."
|
||||||
|
orgLogo = "https://via.placeholder.com/150"
|
||||||
|
|
||||||
user = { name: "John Doe" }
|
user = { name: "John Doe" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,15 +59,13 @@
|
||||||
<span>{$organisation?.company || "Budibase"}</span>
|
<span>{$organisation?.company || "Budibase"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="onboarding">
|
<div class="onboarding">
|
||||||
<ProgressCircle size="S" value={onBoardingProgress} />
|
<ConfigChecklist />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<Navigation>
|
<Navigation>
|
||||||
{#each menu as { title, href, heading }}
|
{#each menu as { title, href, heading }}
|
||||||
<Item selected={$isActive(href)} href={$url(href)} {heading}>
|
<Item selected={$isActive(href)} {href} {heading}>{title}</Item>
|
||||||
{title}
|
|
||||||
</Item>
|
|
||||||
{/each}
|
{/each}
|
||||||
</Navigation>
|
</Navigation>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,36 @@
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
|
||||||
const INITIAL_ADMIN_STATE = {
|
export function createAdminStore() {
|
||||||
oauth: [],
|
const { subscribe, set } = writable({})
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
try {
|
||||||
|
const response = await api.get("/api/admin/configs/checklist")
|
||||||
|
const json = await response.json()
|
||||||
|
|
||||||
|
const onboardingSteps = Object.keys(json)
|
||||||
|
|
||||||
|
const stepsComplete = onboardingSteps.reduce(
|
||||||
|
(score, step) => score + Number(!!json[step]),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
|
set({
|
||||||
|
checklist: json,
|
||||||
|
onboardingProgress: (stepsComplete / onboardingSteps.length) * 100,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
set({
|
||||||
|
checklist: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
init,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const admin = writable({ ...INITIAL_ADMIN_STATE })
|
export const admin = createAdminStore()
|
||||||
|
|
|
@ -3,10 +3,15 @@ const {
|
||||||
generateConfigID,
|
generateConfigID,
|
||||||
StaticDatabases,
|
StaticDatabases,
|
||||||
getConfigParams,
|
getConfigParams,
|
||||||
|
getGlobalUserParams,
|
||||||
getScopedFullConfig,
|
getScopedFullConfig,
|
||||||
} = require("@budibase/auth").db
|
} = require("@budibase/auth").db
|
||||||
|
const fetch = require("node-fetch")
|
||||||
const { Configs } = require("../../../constants")
|
const { Configs } = require("../../../constants")
|
||||||
const email = require("../../../utilities/email")
|
const email = require("../../../utilities/email")
|
||||||
|
const env = require("../../../environment")
|
||||||
|
|
||||||
|
const APP_PREFIX = "app_"
|
||||||
|
|
||||||
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
||||||
|
|
||||||
|
@ -100,3 +105,41 @@ exports.destroy = async function (ctx) {
|
||||||
ctx.throw(err.status, err)
|
ctx.throw(err.status, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.configChecklist = async function (ctx) {
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: Watch get started video
|
||||||
|
|
||||||
|
// Apps exist
|
||||||
|
let allDbs
|
||||||
|
if (env.COUCH_DB_URL) {
|
||||||
|
allDbs = await (await fetch(`${env.COUCH_DB_URL}/_all_dbs`)).json()
|
||||||
|
} else {
|
||||||
|
allDbs = await CouchDB.allDbs()
|
||||||
|
}
|
||||||
|
const appDbNames = allDbs.filter(dbName => dbName.startsWith(APP_PREFIX))
|
||||||
|
|
||||||
|
// They have set up SMTP
|
||||||
|
const smtpConfig = await getScopedFullConfig(db, {
|
||||||
|
type: Configs.SMTP,
|
||||||
|
})
|
||||||
|
|
||||||
|
// They have set up an admin user
|
||||||
|
const users = await db.allDocs(
|
||||||
|
getGlobalUserParams(null, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const adminUser = users.rows.some(row => row.doc.admin)
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
apps: appDbNames.length,
|
||||||
|
smtp: !!smtpConfig,
|
||||||
|
adminUser,
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
ctx.throw(err.status, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,8 +9,6 @@ const { UserStatus, EmailTemplatePurpose } = require("../../../constants")
|
||||||
const { checkInviteCode } = require("../../../utilities/redis")
|
const { checkInviteCode } = require("../../../utilities/redis")
|
||||||
const { sendEmail } = require("../../../utilities/email")
|
const { sendEmail } = require("../../../utilities/email")
|
||||||
|
|
||||||
const FIRST_USER_EMAIL = "test@test.com"
|
|
||||||
const FIRST_USER_PASSWORD = "test"
|
|
||||||
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
||||||
|
|
||||||
exports.save = async ctx => {
|
exports.save = async ctx => {
|
||||||
|
@ -62,21 +60,29 @@ exports.save = async ctx => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.firstUser = async ctx => {
|
exports.adminUser = async ctx => {
|
||||||
const existing = await getGlobalUserByEmail(FIRST_USER_EMAIL)
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
const params = {}
|
const response = await db.allDocs(
|
||||||
if (existing) {
|
getGlobalUserParams(null, {
|
||||||
params._id = existing._id
|
include_docs: true,
|
||||||
params._rev = existing._rev
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
if (response.rows.some(row => row.doc.admin)) {
|
||||||
|
ctx.throw(403, "You cannot initialise once an admin user has been created.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { email, password } = ctx.request.body
|
||||||
ctx.request.body = {
|
ctx.request.body = {
|
||||||
...params,
|
email: email,
|
||||||
email: FIRST_USER_EMAIL,
|
password: password,
|
||||||
password: FIRST_USER_PASSWORD,
|
|
||||||
roles: {},
|
roles: {},
|
||||||
builder: {
|
builder: {
|
||||||
global: true,
|
global: true,
|
||||||
},
|
},
|
||||||
|
admin: {
|
||||||
|
global: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await exports.save(ctx)
|
await exports.save(ctx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ const { buildAuthMiddleware } = require("@budibase/auth").auth
|
||||||
|
|
||||||
const PUBLIC_ENDPOINTS = [
|
const PUBLIC_ENDPOINTS = [
|
||||||
{
|
{
|
||||||
route: "/api/admin/users/first",
|
route: "/api/admin/users/init",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -29,6 +29,10 @@ const PUBLIC_ENDPOINTS = [
|
||||||
route: "/api/admin/auth/reset",
|
route: "/api/admin/auth/reset",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
route: "/api/admin/configs/checklist",
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = new Router()
|
const router = new Router()
|
||||||
|
|
|
@ -71,6 +71,8 @@ function buildConfigGetValidation() {
|
||||||
router
|
router
|
||||||
.post("/api/admin/configs", buildConfigSaveValidation(), controller.save)
|
.post("/api/admin/configs", buildConfigSaveValidation(), controller.save)
|
||||||
.delete("/api/admin/configs/:id", controller.destroy)
|
.delete("/api/admin/configs/:id", controller.destroy)
|
||||||
|
.get("/api/admin/configs", controller.fetch)
|
||||||
|
.get("/api/admin/configs/checklist", controller.configChecklist)
|
||||||
.get(
|
.get(
|
||||||
"/api/admin/configs/all/:type",
|
"/api/admin/configs/all/:type",
|
||||||
buildConfigGetValidation(),
|
buildConfigGetValidation(),
|
||||||
|
|
|
@ -42,7 +42,7 @@ function buildInviteAcceptValidation() {
|
||||||
router
|
router
|
||||||
.post("/api/admin/users", buildUserSaveValidation(), controller.save)
|
.post("/api/admin/users", buildUserSaveValidation(), controller.save)
|
||||||
.get("/api/admin/users", controller.fetch)
|
.get("/api/admin/users", controller.fetch)
|
||||||
.post("/api/admin/users/first", controller.firstUser)
|
.post("/api/admin/users/init", controller.adminUser)
|
||||||
.delete("/api/admin/users/:id", controller.destroy)
|
.delete("/api/admin/users/:id", controller.destroy)
|
||||||
.get("/api/admin/users/:id", controller.find)
|
.get("/api/admin/users/:id", controller.find)
|
||||||
.post("/api/admin/users/invite", buildInviteValidation(), controller.invite)
|
.post("/api/admin/users/invite", buildInviteValidation(), controller.invite)
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
const setup = require("./utilities")
|
||||||
|
|
||||||
|
// mock the email system
|
||||||
|
const sendMailMock = jest.fn()
|
||||||
|
jest.mock("nodemailer")
|
||||||
|
const nodemailer = require("nodemailer")
|
||||||
|
nodemailer.createTransport.mockReturnValue({
|
||||||
|
verify: jest.fn()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("/api/admin/configs/checklist", () => {
|
||||||
|
let request = setup.getRequest()
|
||||||
|
let config = setup.getConfig()
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(setup.afterAll)
|
||||||
|
|
||||||
|
it("should return the correct checklist status based on the state of the budibase installation", async () => {
|
||||||
|
// initially configure settings
|
||||||
|
await config.saveAdminUser()
|
||||||
|
await config.saveSmtpConfig()
|
||||||
|
|
||||||
|
const res = await request
|
||||||
|
.get(`/api/admin/configs/checklist`)
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
const checklist = res.body
|
||||||
|
|
||||||
|
expect(checklist.apps).toBe(0)
|
||||||
|
expect(checklist.smtp).toBe(true)
|
||||||
|
expect(checklist.adminUser).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
|
@ -131,6 +131,22 @@ class TestConfiguration {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async saveOAuthConfig() {
|
||||||
|
await this.deleteConfig(Configs.GOOGLE)
|
||||||
|
await this._req(
|
||||||
|
{
|
||||||
|
type: Configs.GOOGLE,
|
||||||
|
config: {
|
||||||
|
callbackURL: "http://somecallbackurl",
|
||||||
|
clientID: "clientId",
|
||||||
|
clientSecret: "clientSecret",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
controllers.config.save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async saveSmtpConfig() {
|
async saveSmtpConfig() {
|
||||||
await this.deleteConfig(Configs.SMTP)
|
await this.deleteConfig(Configs.SMTP)
|
||||||
await this._req(
|
await this._req(
|
||||||
|
@ -167,6 +183,17 @@ class TestConfiguration {
|
||||||
controllers.config.save
|
controllers.config.save
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async saveAdminUser() {
|
||||||
|
await this._req(
|
||||||
|
{
|
||||||
|
email: "testuser@test.com",
|
||||||
|
password: "test@test.com",
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
controllers.users.adminUser
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = TestConfiguration
|
module.exports = TestConfiguration
|
||||||
|
|
Loading…
Reference in New Issue