Merge pull request #1452 from Budibase/first-time-setup

First time setup and Configuration Checklist
This commit is contained in:
Martin McKeaveney 2021-05-06 12:37:15 +01:00 committed by GitHub
commit 52d87c8267
14 changed files with 336 additions and 26 deletions

View File

@ -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) {

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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,
})
}
} }
export const admin = writable({ ...INITIAL_ADMIN_STATE }) return {
subscribe,
init,
}
}
export const admin = createAdminStore()

View File

@ -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)
}
}

View File

@ -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)
} }

View File

@ -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()

View File

@ -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(),

View File

@ -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)

View File

@ -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)
})
})

View File

@ -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