Auth working

This commit is contained in:
Martin McKeaveney 2020-05-06 20:29:47 +01:00
parent 7da95c23a3
commit 8f5845943a
39 changed files with 40 additions and 672 deletions

View File

@ -1,8 +1,11 @@
const apiCall = method => async (url, body) => {
const jwt = localStorage.getItem("budibase:token");
const response = await fetch(url, {
method: method,
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${jwt}`
},
body: body && JSON.stringify(body),
})

View File

@ -167,7 +167,6 @@ const _saveScreenApi = (screen, s) =>
const createScreen = store => (screenName, route, layoutComponentName) => {
store.update(state => {
console.log(layoutComponentName);
const rootComponent = state.components[layoutComponentName]
const newScreen = {

View File

@ -118,9 +118,6 @@
<i class="ri-more-line" />
<div uk-dropdown="mode: click">
<ul class="uk-nav uk-dropdown-nav">
<li>
<div on:click={() => drillIntoRecord(row)}>View</div>
</li>
<li
on:click={() => {
editRecord(row)

View File

@ -1,231 +0,0 @@
<script>
import { tick } from "svelte"
import Textbox from "components/common/Textbox.svelte"
import Button from "components/common/Button.svelte"
import Select from "components/common/Select.svelte"
import ActionButton from "components/common/ActionButton.svelte"
import getIcon from "components/common/icon"
import FieldView from "../../FieldView.svelte"
import {
get,
compose,
map,
join,
filter,
some,
find,
keys,
isDate,
} from "lodash/fp"
import { store, backendUiStore } from "builderStore"
import { common, hierarchy } from "../../../../../../core/src/"
import { getNode } from "components/common/core"
import { templateApi, pipe, validate } from "components/common/core"
import ErrorsBox from "components/common/ErrorsBox.svelte"
let model
let editingField = false
let fieldToEdit
let isNewField = false
let newField
let editField
let deleteField
let onFinishedFieldEdit
let editIndex
$: parent = model && model.parent()
$: isChildModel = parent && parent.name !== "root"
$: modelExistsInHierarchy =
$store.currentNode && getNode($store.hierarchy, $store.currentNode.nodeId)
store.subscribe($store => {
model = $store.currentNode
const flattened = hierarchy.getFlattenedHierarchy($store.hierarchy)
newField = () => {
isNewField = true
fieldToEdit = templateApi($store.hierarchy).getNewField("string")
editingField = true
}
onFinishedFieldEdit = field => {
if (field) {
store.saveField(field)
}
editingField = false
}
editField = field => {
isNewField = false
fieldToEdit = field
editingField = true
}
deleteField = field => {
store.deleteField(field)
}
editIndex = index => {
store.selectExistingNode(index.nodeId)
}
})
let getTypeOptionsValueText = value => {
if (
value === Number.MAX_SAFE_INTEGER ||
value === Number.MIN_SAFE_INTEGER ||
new Date(value).getTime() === new Date(8640000000000000).getTime() ||
new Date(value).getTime() === new Date(-8640000000000000).getTime()
)
return "(any)"
if (value === null) return "(not set)"
return value
}
const nameChanged = ev => {
const pluralName = n => `${n}s`
if (model.collectionName === "") {
model.collectionName = pluralName(ev.target.value)
}
}
</script>
<heading>
{#if !editingField}
<i class="ri-list-settings-line button--toggled" />
<h3 class="budibase__title--3">Create / Edit Model</h3>
{:else}
<i class="ri-file-list-line button--toggled" />
<h3 class="budibase__title--3">Create / Edit Field</h3>
{/if}
</heading>
{#if !editingField}
<div class="padding">
<h4 class="budibase__label--big">Settings</h4>
{#if $store.errors && $store.errors.length > 0}
<ErrorsBox errors={$store.errors} />
{/if}
<form on:submit|preventDefault class="uk-form-stacked">
<Textbox label="Name" bind:text={model.name} on:change={nameChanged} />
{#if isChildModel}
<div>
<label class="uk-form-label">Parent</label>
<div class="uk-form-controls parent-name">{parent.name}</div>
</div>
{/if}
</form>
<div class="table-controls">
<span class="budibase__label--big">Fields</span>
<h4 class="hoverable new-field" on:click={newField}>Add new field</h4>
</div>
<table class="uk-table fields-table budibase__table">
<thead>
<tr>
<th>Edit</th>
<th>Name</th>
<th>Type</th>
<th>Values</th>
<th />
</tr>
</thead>
<tbody>
{#each model ? model.fields : [] as field}
<tr>
<td>
<i class="ri-more-line" on:click={() => editField(field)} />
</td>
<td>
<div>{field.name}</div>
</td>
<td>{field.type}</td>
<td>{field.typeOptions.values || ''}</td>
<td>
<i
class="ri-delete-bin-6-line hoverable"
on:click={() => deleteField(field)} />
</td>
</tr>
{/each}
</tbody>
</table>
<div class="uk-margin">
<ActionButton color="secondary" on:click={store.saveCurrentNode}>
Save
</ActionButton>
{#if modelExistsInHierarchy}
<ActionButton color="primary" on:click={store.newChildModel}>
Create Child Model on {model.name}
</ActionButton>
<ActionButton
color="primary"
on:click={async () => {
backendUiStore.actions.modals.show('VIEW')
await tick()
store.newChildIndex()
}}>
Create Child View on {model.name}
</ActionButton>
<ActionButton alert on:click={store.deleteCurrentNode}>
Delete
</ActionButton>
{/if}
</div>
</div>
{:else}
<FieldView
field={fieldToEdit}
onFinished={onFinishedFieldEdit}
allFields={model.fields}
store={$store} />
{/if}
<style>
.padding {
padding: 20px;
}
.new-field {
font-size: 16px;
font-weight: bold;
color: var(--button-text);
}
.fields-table {
margin: 1rem 1rem 0rem 0rem;
border-collapse: collapse;
}
tbody > tr:hover {
background-color: var(--primary10);
}
.table-controls {
display: flex;
justify-content: space-between;
align-items: center;
}
.ri-more-line:hover {
cursor: pointer;
}
heading {
padding: 20px 20px 0 20px;
display: flex;
align-items: center;
}
h3 {
margin: 0 0 0 10px;
}
.parent-name {
font-weight: bold;
}
</style>

View File

@ -29,7 +29,6 @@
: []
function closed() {
// editingRecord = null
onClosed()
}

View File

@ -76,7 +76,7 @@
]
}
}],
appRootPath: `/`,
appRootPath: ""
}
$: selectedComponentId = $store.currentComponentInfo ? $store.currentComponentInfo._id : ""

View File

@ -88,11 +88,6 @@ const parsePropDef = propDef => {
const type = TYPE_MAP[propDef.type]
if (!type) return error(`Type ${propDef.type} is not recognised.`)
// if (isUndefined(propDef.default)) return type.default(propDef)
// if (!type.isOfType(propDef.default))
// return error(`${propDef.default} is not of type ${type}`)
return propDef.default
}

View File

@ -1,86 +0,0 @@
const { readdir, stat, copyFile } = require("fs-extra")
const { constants } = require("fs")
const { join, basename } = require("path")
const serverConfig = require("../../../server/config")()
const packagesFolder = ".."
const jsFile = dir => join(dir, "index.js")
const jsMapFile = dir => join(dir, "index.js.map")
const sourceJs = jsFile("dist")
const sourceJsMap = jsMapFile("dist")
const componentsFile = "components.json"
const appPackages = join(
packagesFolder,
"server",
serverConfig.latestPackagesFolder
)
const publicMain = appName =>
join(
appPackages,
appName,
"public",
"main",
"lib",
"node_modules",
"@budibase",
"standard-components"
)
const publicUnauth = appName =>
join(
appPackages,
appName,
"public",
"unauthenticated",
"lib",
"node_modules",
"@budibase",
"standard-components"
)
const nodeModulesDist = appName =>
join(
appPackages,
appName,
"node_modules",
"@budibase",
"standard-components",
"dist"
)
const nodeModules = appName =>
join(appPackages, appName, "node_modules", "@budibase", "standard-components")
;(async () => {
const apps = await readdir(appPackages)
const copySource = file => async toDir => {
const dest = join(toDir, basename(file))
try {
await copyFile(file, dest, constants.COPYFILE_FICLONE)
console.log(`COPIED ${file} to ${dest}`)
} catch (e) {
console.log(`COPY FAILED ${file} to ${dest}: ${e}`)
}
}
const copySourceJs = copySource(sourceJs)
const copySourceJsMap = copySource(sourceJsMap)
const copyComponentsJson = copySource(componentsFile)
for (let app of apps) {
if (app === ".data") continue
if (!(await stat(join(appPackages, app))).isDirectory()) continue
await copySourceJs(nodeModulesDist(app))
await copySourceJsMap(nodeModulesDist(app))
await copyComponentsJson(nodeModules(app))
await copySourceJs(join(publicMain(app), "dist"))
await copySourceJsMap(join(publicMain(app), "dist"))
await copySourceJs(join(publicUnauth(app), "dist"))
await copySourceJsMap(join(publicUnauth(app), "dist"))
}
})()

View File

@ -37,10 +37,14 @@ exports.authenticate = async ctx => {
accessLevel: "",
instanceId: instanceId
};
const token = jwt.sign(payload, ctx.config.secret, {
const token = jwt.sign(payload, ctx.config.jwtSecret, {
expiresIn: "1 day"
});
ctx.cookies.set('budibase:token', token);
ctx.body = {
token,
...dbUser

View File

@ -31,18 +31,16 @@ module.exports = app => {
flush: zlib.Z_SYNC_FLUSH,
}
}))
.use(authenticated)
.use(async (ctx, next) => {
// TODO: temp dev middleware
// ctx.sessionId = ctx.session._sessCtx.externalKey
// ctx.session.accessed = true
ctx.config = {
latestPackagesFolder: resolve(homedir(), ".budibase"),
secret: "foo"
jwtSecret: "foo"
}
ctx.isDev = process.env.NODE_ENV !== "production";
await next();
});
})
.use(authenticated);
// error handling middleware
router.use(async (ctx, next) => {
@ -61,15 +59,26 @@ module.exports = app => {
router.use(authRoutes.routes());
router.use(authRoutes.allowedMethods());
router.use(pageRoutes.routes());
router.use(pageRoutes.allowedMethods());
// authenticated routes
router.use(viewRoutes.routes());
router.use(viewRoutes.allowedMethods());
router.use(modelRoutes.routes());
router.use(modelRoutes.allowedMethods());
router.use(userRoutes.routes());
router.use(userRoutes.allowedMethods());
router.use(recordRoutes.routes());
router.use(recordRoutes.allowedMethods());
router.use(instanceRoutes.routes());
router.use(instanceRoutes.allowedMethods());
// end auth routes
router.use(pageRoutes.routes());
router.use(pageRoutes.allowedMethods());
router.use(applicationRoutes.routes());
router.use(applicationRoutes.allowedMethods());
@ -79,15 +88,6 @@ module.exports = app => {
router.use(clientRoutes.routes());
router.use(clientRoutes.allowedMethods());
router.use(userRoutes.routes());
router.use(userRoutes.allowedMethods());
router.use(recordRoutes.routes());
router.use(recordRoutes.allowedMethods());
router.use(instanceRoutes.routes());
router.use(instanceRoutes.allowedMethods());
router.use(staticRoutes.routes());
router.use(staticRoutes.allowedMethods());

View File

@ -7,7 +7,7 @@ router
.param("file", async (file, ctx, next) => {
ctx.file = file && file.includes(".") ? file : "index.html";
// Serving the latest client library in dev
// Serving the client library from your local dir in dev
if (ctx.isDev && ctx.file.startsWith("budibase-client")) {
ctx.devPath = "/tmp/.budibase";
}

View File

@ -1,11 +0,0 @@
const getMasterAppInternal = require("../utilities/masterAppInternal")
module.exports = async (config, masterIsCreated) => {
const context = { config }
if (!masterIsCreated) return context
const master = await getMasterAppInternal(context)
context.master = master
return context
}

View File

@ -1,17 +1,17 @@
const jwt = require("jsonwebtoken");
module.exports = async (ctx, next) => {
if (!ctx.headers.authorization) {
const token = ctx.cookies.get("budibase:token");
console.log("TOKEN", token);
if (!token) {
ctx.isAuthenticated = false
await next();
return;
};
// if (!ctx.headers.authorization) ctx.throw(403, "No token provided");
const [_, token] = ctx.headers.authorization.split(" ");
try {
ctx.request.jwtPayload = jwt.verify(token, ctx.config.jwtSecret);
ctx.jwtPayload = jwt.verify(token, ctx.config.jwtSecret);
ctx.isAuthenticated = true;
} catch (err) {
ctx.throw(err.status || 403, err.text);

View File

@ -1,12 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
const { getRecordKey } = require("./helpers")
module.exports = async ctx => {
const indexkey = getRecordKey(ctx.params.appname, ctx.request.path)
ctx.body = await ctx.instance.indexApi.aggregates(indexkey, {
rangeStartParams: ctx.request.body.rangeStartParams,
rangeEndParams: ctx.request.body.rangeEndParams,
searchPhrase: ctx.request.body.searchPhrase,
})
ctx.response.status = StatusCodes.OK
}

View File

@ -1,21 +0,0 @@
const { getAppRelativePath } = require("./helpers")
const send = require("koa-send")
module.exports = async (ctx, next) => {
const path = getAppRelativePath(ctx.params.appname, ctx.path)
if (path.startsWith("/api/")) {
await next()
} else if (path.startsWith("/_shared/")) {
await send(ctx, path.replace(`/_shared/`, ""), { root: ctx.sharedPath })
} else if (
path.endsWith(".js") ||
path.endsWith(".map") ||
path.endsWith(".css")
) {
await send(ctx, path, { root: ctx.publicPath })
} else {
await send(ctx, "/index.html", { root: ctx.publicPath })
}
}

View File

@ -1,15 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
module.exports = async ctx => {
const user = await ctx.master.authenticate(
ctx.sessionId,
ctx.params.appname,
ctx.request.body.username,
ctx.request.body.password
)
if (!user) {
ctx.throw(StatusCodes.UNAUTHORIZED, "invalid username or password")
}
ctx.body = user.user_json
ctx.response.status = StatusCodes.OK
}

View File

@ -1,9 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
module.exports = async ctx => {
await ctx.instance.authApi.changeMyPassword(
ctx.request.body.currentPassword,
ctx.request.body.newPassword
)
ctx.response.status = StatusCodes.OK
}

View File

@ -1,17 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
module.exports = async ctx => {
const instanceApi = await ctx.master.getFullAccessInstanceApiForUsername(
ctx.params.appname,
ctx.request.body.username
)
if (!instanceApi) {
ctx.request.status = StatusCodes.OK
return
}
await instanceApi.authApi.createTemporaryAccess(ctx.request.body.username)
ctx.response.status = StatusCodes.OK
}

View File

@ -1,10 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
module.exports = async ctx => {
await ctx.instance.authApi.createUser(
ctx.request.body.user,
ctx.request.body.password
)
ctx.response.status = StatusCodes.OK
}

View File

@ -1,9 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
const { getRecordKey } = require("./helpers")
module.exports = async ctx => {
await ctx.instance.recordApi.delete(
getRecordKey(ctx.params.appname, ctx.request.path)
)
ctx.response.status = StatusCodes.OK
}

View File

@ -1,11 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
module.exports = async ctx => {
await ctx.instance.authApi.disableUser(ctx.request.body.username)
await ctx.master.removeSessionsForUser(
ctx.params.appname,
ctx.request.body.username
)
ctx.response.status = StatusCodes.OK
}

View File

@ -1,6 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
module.exports = async ctx => {
await ctx.instance.authApi.enableUser(ctx.request.body.username)
ctx.response.status = StatusCodes.OK
}

View File

@ -1,9 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
module.exports = async ctx => {
ctx.body = await ctx.instance.actionApi.execute(
ctx.request.body.actionname,
ctx.request.body.parameters
)
ctx.response.status = StatusCodes.OK
}

View File

@ -1,6 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
module.exports = async ctx => {
ctx.body = await ctx.instance.authApi.getAccessLevels()
ctx.response.status = StatusCodes.OK
}

View File

@ -1,15 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
const { getRecordKey } = require("./helpers")
module.exports = async ctx => {
try {
ctx.body = await ctx.instance.recordApi.load(
getRecordKey(ctx.params.appname, ctx.request.path)
)
ctx.response.status = StatusCodes.OK
} catch (e) {
// need to be catching for 404s here
ctx.response.status = StatusCodes.INTERAL_ERROR
ctx.response.body = e.message
}
}

View File

@ -1,6 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
module.exports = async ctx => {
ctx.body = await ctx.instance.authApi.getUsers()
ctx.response.status = StatusCodes.OK
}

View File

@ -1,15 +0,0 @@
exports.getRecordKey = (appname, wholePath) =>
this.getAppRelativePath(appname, wholePath)
.replace(`/api/files/`, "/")
.replace(`/api/lookup_field/`, "/")
.replace(`/api/record/`, "/")
.replace(`/api/listRecords/`, "/")
.replace(`/api/aggregates/`, "/")
exports.getAppRelativePath = (appname, wholePath) => {
const builderInstanceRegex = new RegExp(
`\\/_builder\\/instance\\/[^\\/]*\\/[^\\/]*\\/`
)
return wholePath.replace(builderInstanceRegex, "/").replace(`/${appname}`, "")
}

View File

@ -1,45 +0,0 @@
const authenticate = require("./authenticate")
const setPasswordFromTemporaryCode = require("./setPasswordFromTemporaryCode")
const createTemporaryAccess = require("./createTemporaryAccess")
const appDefault = require("./appDefault")
const changeMyPassword = require("./changeMyPassword")
const executeAction = require("./executeAction")
const createUser = require("./createUser")
const enableUser = require("./enableUser")
const disableUser = require("./disableUser")
const getUsers = require("./getUsers")
const getAccessLevels = require("./getAccessLevels")
const listRecordsGet = require("./listRecordsGet")
const listRecordsPost = require("./listRecordsPost")
const aggregatesPost = require("./aggregatesPost")
const postFiles = require("./postFiles")
const saveRecord = require("./saveRecord")
const lookupField = require("./lookupField")
const getRecord = require("./getRecord")
const deleteRecord = require("./deleteRecord")
const saveAppHierarchy = require("./saveAppHierarchy")
const upgradeData = require("./upgradeData")
module.exports = {
authenticate,
setPasswordFromTemporaryCode,
createTemporaryAccess,
appDefault,
changeMyPassword,
executeAction,
createUser,
enableUser,
disableUser,
getUsers,
getAccessLevels,
listRecordsGet,
listRecordsPost,
aggregatesPost,
postFiles,
saveRecord,
lookupField,
getRecord,
deleteRecord,
saveAppHierarchy,
upgradeData,
}

View File

@ -1,8 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
const { getRecordKey } = require("./helpers")
module.exports = async ctx => {
const indexkey = getRecordKey(ctx.params.appname, ctx.request.path)
ctx.body = await ctx.instance.indexApi.listItems(indexkey)
ctx.response.status = StatusCodes.OK
}

View File

@ -1,12 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
const { getRecordKey } = require("./helpers")
module.exports = async ctx => {
const indexkey = getRecordKey(ctx.params.appname, ctx.request.path)
ctx.body = await ctx.instance.indexApi.listItems(indexkey, {
rangeStartParams: ctx.request.body.rangeStartParams,
rangeEndParams: ctx.request.body.rangeEndParams,
searchPhrase: ctx.request.body.searchPhrase,
})
ctx.response.status = StatusCodes.OK
}

View File

@ -1,14 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
const { getRecordKey } = require("./helpers")
module.exports = async ctx => {
const recordKey = getRecordKey(ctx.params.appname, ctx.request.path)
const fields = ctx.query.fields.split(",")
const recordContext = await ctx.instance.recordApi.getContext(recordKey)
const allContext = []
for (let field of fields) {
allContext.push(await recordContext.referenceOptions(field))
}
ctx.body = allContext
ctx.response.status = StatusCodes.OK
}

View File

@ -1,13 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
const { getRecordKey } = require("./helpers")
const fs = require("fs")
module.exports = async ctx => {
const file = ctx.request.files.file
ctx.body = await ctx.instance.recordApi.uploadFile(
getRecordKey(ctx.params.appname, ctx.request.path),
fs.createReadStream(file.path),
file.name
)
ctx.response.status = StatusCodes.OK
}

View File

@ -1,6 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
module.exports = async ctx => {
ctx.body = await ctx.instance.templateApi.saveApplicationHierarchy(ctx.body)
ctx.response.status = StatusCodes.OK
}

View File

@ -1,6 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
module.exports = async ctx => {
ctx.body = await ctx.instance.recordApi.save(ctx.request.body)
ctx.response.status = StatusCodes.OK
}

View File

@ -1,20 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
module.exports = async ctx => {
const instanceApi = await ctx.master.getFullAccessInstanceApiForUsername(
ctx.params.appname,
ctx.request.body.username
)
if (!instanceApi) {
ctx.request.status = StatusCodes.OK
return
}
await instanceApi.authApi.setPasswordFromTemporaryCode(
ctx.request.body.tempCode,
ctx.request.body.newPassword
)
ctx.response.status = StatusCodes.OK
}

View File

@ -1,11 +0,0 @@
const StatusCodes = require("../../utilities/statusCodes")
module.exports = async ctx => {
const existingAccessLevels = await ctx.instance.authApi.loadAccessLevels()
const accessLevels = ctx.request.body.accessLevels
accessLevels.version = existingAccessLevels.version
await ctx.instance.authApi.saveAccessLevels(accessLevels)
await ctx.instance.templateApi.upgradeData(ctx.request.body.newHierarchy)
await ctx.master.clearAllSessions(ctx.params.appname)
ctx.response.status = StatusCodes.OK
}

View File

@ -265,13 +265,13 @@
"datatable": {
"description": "an HTML table that fetches data from a model or view and displays it.",
"props": {
"_viewName": "string",
"_instanceId": "string",
"model": {
"type": "options",
"default": "",
"options": [
"all_6dc86335-83b7-462c-90ca-1fe7feb08942"
"all_6dc86335-83b7-462c-90ca-1fe7feb08942",
"all_fcd00735-01f0-451c-819e-902a3ea53c26"
]
}
}

View File

@ -5,28 +5,23 @@
export let _bb
export let onLoad
export let _viewName
export let _instanceId
export let model
let cssVariables
let headers = []
let data = []
async function fetchData() {
const FETCH_RECORDS_URL = `/api/${_instanceId}/${_viewName}/records`;
const FETCH_RECORDS_URL = `/api/${_instanceId}/${model}/records`;
const response = await _bb.api.get(FETCH_RECORDS_URL);
if (response.status === 200) {
const json = await response.json();
if (json.length > 0) {
data = json;
headers = Object.keys(data[0]);
} else {
console.log("NO DATA");
}
data = json;
headers = Object.keys(data[0]).filter(key => !key.startsWith("_"));
} else {
throw new Error("Failed to fetch records..");
console.log("FAILED");
throw new Error("Failed to fetch records.", response);
}
}

View File

@ -26,7 +26,7 @@
const login = async () => {
loading = true
const response = _bb.api.post("/api/authenticate", { username, password });
const response = await _bb.api.post("/api/authenticate", { username, password });
if (response.status === 200) {
const json = await response.json();