Merge remote-tracking branch 'origin/v3-ui' into feature/automation-branching-ux

This commit is contained in:
Dean 2024-10-28 09:04:17 +00:00
commit d1c21d7aeb
42 changed files with 530 additions and 216 deletions

View File

@ -27,7 +27,7 @@ export function doInUserContext(user: User, ctx: Ctx, task: any) {
hostInfo: { hostInfo: {
ipAddress: ctx.request.ip, ipAddress: ctx.request.ip,
// filled in by koa-useragent package // filled in by koa-useragent package
userAgent: ctx.userAgent._agent.source, userAgent: ctx.userAgent.source,
}, },
} }
return doInIdentityContext(userContext, task) return doInIdentityContext(userContext, task)

View File

@ -1,20 +1,26 @@
import { Cookie, Header } from "../constants" import { Cookie, Header } from "../constants"
import { import {
getCookie,
clearCookie, clearCookie,
openJwt, getCookie,
isValidInternalAPIKey, isValidInternalAPIKey,
openJwt,
} from "../utils" } from "../utils"
import { getUser } from "../cache/user" import { getUser } from "../cache/user"
import { getSession, updateSessionTTL } from "../security/sessions" import { getSession, updateSessionTTL } from "../security/sessions"
import { buildMatcherRegex, matches } from "./matchers" import { buildMatcherRegex, matches } from "./matchers"
import { SEPARATOR, queryGlobalView, ViewName } from "../db" import { queryGlobalView, SEPARATOR, ViewName } from "../db"
import { getGlobalDB, doInTenant } from "../context" import { doInTenant, getGlobalDB } from "../context"
import { decrypt } from "../security/encryption" import { decrypt } from "../security/encryption"
import * as identity from "../context/identity" import * as identity from "../context/identity"
import env from "../environment" import env from "../environment"
import { Ctx, EndpointMatcher, SessionCookie, User } from "@budibase/types" import {
import { InvalidAPIKeyError, ErrorCode } from "../errors" Ctx,
EndpointMatcher,
LoginMethod,
SessionCookie,
User,
} from "@budibase/types"
import { ErrorCode, InvalidAPIKeyError } from "../errors"
import tracer from "dd-trace" import tracer from "dd-trace"
const ONE_MINUTE = env.SESSION_UPDATE_PERIOD const ONE_MINUTE = env.SESSION_UPDATE_PERIOD
@ -26,16 +32,18 @@ interface FinaliseOpts {
internal?: boolean internal?: boolean
publicEndpoint?: boolean publicEndpoint?: boolean
version?: string version?: string
user?: any user?: User | { tenantId: string }
loginMethod?: LoginMethod
} }
function timeMinusOneMinute() { function timeMinusOneMinute() {
return new Date(Date.now() - ONE_MINUTE).toISOString() return new Date(Date.now() - ONE_MINUTE).toISOString()
} }
function finalise(ctx: any, opts: FinaliseOpts = {}) { function finalise(ctx: Ctx, opts: FinaliseOpts = {}) {
ctx.publicEndpoint = opts.publicEndpoint || false ctx.publicEndpoint = opts.publicEndpoint || false
ctx.isAuthenticated = opts.authenticated || false ctx.isAuthenticated = opts.authenticated || false
ctx.loginMethod = opts.loginMethod
ctx.user = opts.user ctx.user = opts.user
ctx.internal = opts.internal || false ctx.internal = opts.internal || false
ctx.version = opts.version ctx.version = opts.version
@ -120,9 +128,10 @@ export default function (
} }
const tenantId = ctx.request.headers[Header.TENANT_ID] const tenantId = ctx.request.headers[Header.TENANT_ID]
let authenticated = false, let authenticated: boolean = false,
user = null, user: User | { tenantId: string } | undefined = undefined,
internal = false internal: boolean = false,
loginMethod: LoginMethod | undefined = undefined
if (authCookie && !apiKey) { if (authCookie && !apiKey) {
const sessionId = authCookie.sessionId const sessionId = authCookie.sessionId
const userId = authCookie.userId const userId = authCookie.userId
@ -146,6 +155,7 @@ export default function (
} }
// @ts-ignore // @ts-ignore
user.csrfToken = session.csrfToken user.csrfToken = session.csrfToken
loginMethod = LoginMethod.COOKIE
if (session?.lastAccessedAt < timeMinusOneMinute()) { if (session?.lastAccessedAt < timeMinusOneMinute()) {
// make sure we denote that the session is still in use // make sure we denote that the session is still in use
@ -170,17 +180,16 @@ export default function (
apiKey, apiKey,
populateUser populateUser
) )
if (valid && foundUser) { if (valid) {
authenticated = true authenticated = true
loginMethod = LoginMethod.API_KEY
user = foundUser user = foundUser
} else if (valid) { internal = !foundUser
authenticated = true
internal = true
} }
} }
if (!user && tenantId) { if (!user && tenantId) {
user = { tenantId } user = { tenantId }
} else if (user) { } else if (user && "password" in user) {
delete user.password delete user.password
} }
// be explicit // be explicit
@ -204,7 +213,14 @@ export default function (
} }
// isAuthenticated is a function, so use a variable to be able to check authed state // isAuthenticated is a function, so use a variable to be able to check authed state
finalise(ctx, { authenticated, user, internal, version, publicEndpoint }) finalise(ctx, {
authenticated,
user,
internal,
version,
publicEndpoint,
loginMethod,
})
if (isUser(user)) { if (isUser(user)) {
return identity.doInUserContext(user, ctx, next) return identity.doInUserContext(user, ctx, next)

View File

@ -22,7 +22,7 @@
} from "stores/builder" } from "stores/builder"
import { themeStore } from "stores/portal" import { themeStore } from "stores/portal"
import { getContext } from "svelte" import { getContext } from "svelte"
import { Constants } from "@budibase/frontend-core" import { ThemeOptions } from "@budibase/shared-core"
const modalContext = getContext(Context.Modal) const modalContext = getContext(Context.Modal)
const commands = [ const commands = [
@ -141,13 +141,13 @@
icon: "ShareAndroid", icon: "ShareAndroid",
action: () => $goto(`./automation/${automation._id}`), action: () => $goto(`./automation/${automation._id}`),
})) ?? []), })) ?? []),
...Constants.Themes.map(theme => ({ ...ThemeOptions.map(themeMeta => ({
type: "Change Builder Theme", type: "Change Builder Theme",
name: theme.name, name: themeMeta.name,
icon: "ColorPalette", icon: "ColorPalette",
action: () => action: () =>
themeStore.update(state => { themeStore.update(state => {
state.theme = theme.class state.theme = themeMeta.id
return state return state
}), }),
})), })),

View File

@ -3,6 +3,7 @@
import { Label } from "@budibase/bbui" import { Label } from "@budibase/bbui"
import { onMount, createEventDispatcher } from "svelte" import { onMount, createEventDispatcher } from "svelte"
import { themeStore } from "stores/portal" import { themeStore } from "stores/portal"
import { Theme } from "@budibase/types"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -116,7 +117,7 @@
readOnly, readOnly,
autoCloseBrackets: true, autoCloseBrackets: true,
autoCloseTags: true, autoCloseTags: true,
theme: $themeStore.theme.includes("light") ? THEMES.LIGHT : THEMES.DARK, theme: $themeStore.theme === Theme.LIGHT ? THEMES.LIGHT : THEMES.DARK,
} }
if (!tab) if (!tab)

View File

@ -1,15 +1,15 @@
<script> <script>
import { ModalContent, Select } from "@budibase/bbui" import { ModalContent, Select } from "@budibase/bbui"
import { themeStore } from "stores/portal" import { themeStore } from "stores/portal"
import { Constants } from "@budibase/frontend-core" import { ThemeOptions } from "@budibase/shared-core"
</script> </script>
<ModalContent title="Theme"> <ModalContent title="Theme">
<Select <Select
options={Constants.Themes} options={ThemeOptions}
bind:value={$themeStore.theme} bind:value={$themeStore.theme}
placeholder={null} placeholder={null}
getOptionLabel={x => x.name} getOptionLabel={x => x.name}
getOptionValue={x => x.class} getOptionValue={x => x.id}
/> />
</ModalContent> </ModalContent>

View File

@ -1,11 +1,11 @@
<script> <script>
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { themeStore, appStore } from "stores/builder" import { themeStore, appStore } from "stores/builder"
import { Constants } from "@budibase/frontend-core" import { ThemeOptions, getThemeClassNames } from "@budibase/shared-core"
const onChangeTheme = async theme => { const onChangeTheme = async theme => {
try { try {
await themeStore.save(`spectrum--${theme}`, $appStore.appId) await themeStore.save(theme, $appStore.appId)
} catch (error) { } catch (error) {
notifications.error("Error updating theme") notifications.error("Error updating theme")
} }
@ -15,17 +15,14 @@
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="container"> <div class="container">
{#each Constants.Themes as theme} {#each ThemeOptions as themeMeta}
<div <div
class="theme" class="theme"
class:selected={`spectrum--${theme.class}` === $themeStore.theme} class:selected={themeMeta.id === $themeStore.theme}
on:click={() => onChangeTheme(theme.class)} on:click={() => onChangeTheme(themeMeta.id)}
> >
<div <div class="color {getThemeClassNames(themeMeta.id)}" />
style="background: {theme.preview}" {themeMeta.name}
class="color spectrum--{theme.class}"
/>
{theme.name}
</div> </div>
{/each} {/each}
</div> </div>

View File

@ -19,6 +19,7 @@
import { findComponent, findComponentPath } from "helpers/components" import { findComponent, findComponentPath } from "helpers/components"
import { isActive, goto } from "@roxi/routify" import { isActive, goto } from "@roxi/routify"
import { ClientAppSkeleton } from "@budibase/frontend-core" import { ClientAppSkeleton } from "@budibase/frontend-core"
import { getThemeClassNames, ThemeClassPrefix } from "@budibase/shared-core"
let iframe let iframe
let layout let layout
@ -47,7 +48,9 @@
layout, layout,
screen, screen,
selectedComponentId, selectedComponentId,
theme: $themeStore.theme, theme: $appStore.clientFeatures.unifiedThemes
? $themeStore.theme
: `${ThemeClassPrefix}${$themeStore.theme}`,
customTheme: $themeStore.customTheme, customTheme: $themeStore.customTheme,
previewDevice: $previewStore.previewDevice, previewDevice: $previewStore.previewDevice,
messagePassing: $appStore.clientFeatures.messagePassing, messagePassing: $appStore.clientFeatures.messagePassing,
@ -257,7 +260,7 @@
class:mobile={$previewStore.previewDevice === "mobile"} class:mobile={$previewStore.previewDevice === "mobile"}
> >
{#if loading} {#if loading}
<div class={`loading ${$themeStore.baseTheme} ${$themeStore.theme}`}> <div class={`loading ${getThemeClassNames($themeStore.theme)}`}>
<ClientAppSkeleton <ClientAppSkeleton
sideNav={$navigationStore?.navigation === "Left"} sideNav={$navigationStore?.navigation === "Left"}
hideFooter hideFooter

View File

@ -18,10 +18,10 @@
TooltipPosition, TooltipPosition,
TooltipType, TooltipType,
} from "@budibase/bbui" } from "@budibase/bbui"
import { sdk } from "@budibase/shared-core" import { sdk, getThemeClassNames } from "@budibase/shared-core"
import { API } from "api" import { API } from "api"
import ErrorSVG from "./ErrorSVG.svelte" import ErrorSVG from "./ErrorSVG.svelte"
import { getBaseTheme, ClientAppSkeleton } from "@budibase/frontend-core" import { ClientAppSkeleton } from "@budibase/frontend-core"
import { contextMenuStore } from "stores/builder" import { contextMenuStore } from "stores/builder"
$: app = $enrichedApps.find(app => app.appId === $params.appId) $: app = $enrichedApps.find(app => app.appId === $params.appId)
@ -163,9 +163,7 @@
class:hide={!loading || !app?.features?.skeletonLoader} class:hide={!loading || !app?.features?.skeletonLoader}
class="loading" class="loading"
> >
<div <div class="loadingThemeWrapper {getThemeClassNames(app.theme)}">
class={`loadingThemeWrapper ${getBaseTheme(app.theme)} ${app.theme}`}
>
<ClientAppSkeleton <ClientAppSkeleton
noAnimation noAnimation
hideDevTools={app?.status === "published"} hideDevTools={app?.status === "published"}

View File

@ -1,24 +1,19 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { API } from "api" import { API } from "api"
import { getBaseTheme } from "@budibase/frontend-core" import { ensureValidTheme, DefaultAppTheme } from "@budibase/shared-core"
const INITIAL_THEMES_STATE = { export const createThemeStore = () => {
theme: "",
customTheme: {},
}
export const themes = () => {
const store = writable({ const store = writable({
...INITIAL_THEMES_STATE, theme: DefaultAppTheme,
customTheme: {},
}) })
const syncAppTheme = app => { const syncAppTheme = app => {
store.update(state => { store.update(state => {
const theme = app.theme || "spectrum--light" const theme = ensureValidTheme(app.theme, DefaultAppTheme)
return { return {
...state, ...state,
theme, theme,
baseTheme: getBaseTheme(theme),
customTheme: app.customTheme, customTheme: app.customTheme,
} }
}) })
@ -51,7 +46,7 @@ export const themes = () => {
const { theme, customTheme } = metadata const { theme, customTheme } = metadata
store.update(state => ({ store.update(state => ({
...state, ...state,
theme, theme: ensureValidTheme(theme, DefaultAppTheme),
customTheme, customTheme,
})) }))
} }
@ -66,4 +61,4 @@ export const themes = () => {
} }
} }
export const themeStore = themes() export const themeStore = createThemeStore()

View File

@ -1,38 +1,37 @@
import { Constants, createLocalStorageStore } from "@budibase/frontend-core" import { createLocalStorageStore } from "@budibase/frontend-core"
import { derived } from "svelte/store"
import {
DefaultBuilderTheme,
ensureValidTheme,
getThemeClassNames,
ThemeOptions,
ThemeClassPrefix,
} from "@budibase/shared-core"
export const getThemeStore = () => { export const getThemeStore = () => {
const themeElement = document.documentElement const themeElement = document.documentElement
const initialValue = { const initialValue = {
theme: "darkest", theme: DefaultBuilderTheme,
} }
const store = createLocalStorageStore("bb-theme", initialValue) const store = createLocalStorageStore("bb-theme", initialValue)
const derivedStore = derived(store, $store => ({
...$store,
theme: ensureValidTheme($store.theme, DefaultBuilderTheme),
}))
// Update theme class when store changes // Update theme class when store changes
store.subscribe(state => { derivedStore.subscribe(({ theme }) => {
// Handle any old local storage values - this can be removed after the update const classNames = getThemeClassNames(theme).split(" ")
if (state.darkMode !== undefined) { ThemeOptions.forEach(option => {
store.set(initialValue) const className = `${ThemeClassPrefix}${option.id}`
return themeElement.classList.toggle(className, classNames.includes(className))
}
// Update global class names to use the new theme and remove others
Constants.Themes.forEach(option => {
themeElement.classList.toggle(
`spectrum--${option.class}`,
option.class === state.theme
)
}) })
// Add base theme if required
const selectedTheme = Constants.Themes.find(x => x.class === state.theme)
if (selectedTheme?.base) {
themeElement.classList.add(`spectrum--${selectedTheme.base}`)
}
}) })
return store return {
...store,
subscribe: derivedStore.subscribe,
}
} }
// ?? confusion
export const themeStore = getThemeStore() export const themeStore = getThemeStore()

View File

@ -1,6 +1,7 @@
{ {
"features": { "features": {
"spectrumThemes": true, "spectrumThemes": true,
"unifiedTheme": true,
"intelligentLoading": true, "intelligentLoading": true,
"deviceAwareness": true, "deviceAwareness": true,
"state": true, "state": true,

View File

@ -4,6 +4,7 @@
import { Layout, Heading, Body } from "@budibase/bbui" import { Layout, Heading, Body } from "@budibase/bbui"
import ErrorSVG from "@budibase/frontend-core/assets/error.svg" import ErrorSVG from "@budibase/frontend-core/assets/error.svg"
import { Constants, CookieUtils } from "@budibase/frontend-core" import { Constants, CookieUtils } from "@budibase/frontend-core"
import { getThemeClassNames } from "@budibase/shared-core"
import Component from "./Component.svelte" import Component from "./Component.svelte"
import SDK from "sdk" import SDK from "sdk"
import { import {
@ -154,7 +155,7 @@
id="spectrum-root" id="spectrum-root"
lang="en" lang="en"
dir="ltr" dir="ltr"
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}" class="spectrum spectrum--medium {getThemeClassNames($themeStore.theme)}"
class:builder={$builderStore.inBuilder} class:builder={$builderStore.inBuilder}
class:show={fontsLoaded && dataLoaded} class:show={fontsLoaded && dataLoaded}
> >

View File

@ -1,12 +1,11 @@
import { derived } from "svelte/store" import { derived } from "svelte/store"
import { appStore } from "./app" import { appStore } from "./app"
import { builderStore } from "./builder" import { builderStore } from "./builder"
import { getBaseTheme } from "@budibase/frontend-core" import { ensureValidTheme, DefaultAppTheme } from "@budibase/shared-core"
// This is the good old acorn bug where having the word "g l o b a l" makes it // This is the good old acorn bug where having the word "g l o b a l" makes it
// think that this is not ES6 compatible and starts throwing errors when using // think that this is not ES6 compatible and starts throwing errors when using
// optional chaining. Piss off acorn. // optional chaining. Piss off acorn.
const defaultTheme = "spectrum--light"
const defaultCustomTheme = { const defaultCustomTheme = {
primaryColor: "var(--spectrum-glo" + "bal-color-blue-600)", primaryColor: "var(--spectrum-glo" + "bal-color-blue-600)",
primaryColorHover: "var(--spectrum-glo" + "bal-color-blue-500)", primaryColorHover: "var(--spectrum-glo" + "bal-color-blue-500)",
@ -27,7 +26,7 @@ const createThemeStore = () => {
} }
// Ensure theme is set // Ensure theme is set
theme = theme || defaultTheme theme = ensureValidTheme(theme, DefaultAppTheme)
// Delete and nullish keys from the custom theme // Delete and nullish keys from the custom theme
if (customTheme) { if (customTheme) {
@ -52,7 +51,6 @@ const createThemeStore = () => {
return { return {
theme, theme,
baseTheme: getBaseTheme(theme),
customTheme, customTheme,
customThemeCss, customThemeCss,
} }

View File

@ -108,35 +108,6 @@ export const Roles = {
CREATOR: "CREATOR", CREATOR: "CREATOR",
} }
export const Themes = [
{
class: "lightest",
name: "Lightest",
},
{
class: "light",
name: "Light",
},
{
class: "dark",
name: "Dark",
},
{
class: "darkest",
name: "Darkest",
},
{
class: "nord",
name: "Nord",
base: "darkest",
},
{
class: "midnight",
name: "Midnight",
base: "darkest",
},
]
export const EventPublishType = { export const EventPublishType = {
ENV_VAR_UPGRADE_PANEL_OPENED: "environment_variable_upgrade_panel_opened", ENV_VAR_UPGRADE_PANEL_OPENED: "environment_variable_upgrade_panel_opened",
} }

View File

@ -9,7 +9,6 @@ export * as SchemaUtils from "./schema"
export { memo, derivedMemo } from "./memo" export { memo, derivedMemo } from "./memo"
export { createWebsocket } from "./websocket" export { createWebsocket } from "./websocket"
export * from "./download" export * from "./download"
export * from "./theme"
export * from "./settings" export * from "./settings"
export * from "./relatedColumns" export * from "./relatedColumns"
export * from "./table" export * from "./table"

View File

@ -1,12 +0,0 @@
import { Themes } from "../constants.js"
export const getBaseTheme = theme => {
if (!theme) {
return ""
}
let base = Themes.find(x => `spectrum--${x.class}` === theme)?.base || ""
if (base) {
base = `spectrum--${base}`
}
return base
}

View File

@ -20,19 +20,15 @@ const options = {
{ {
url: "https://budibase.app/api/public/v1", url: "https://budibase.app/api/public/v1",
description: "Budibase Cloud API", description: "Budibase Cloud API",
},
{
url: "{protocol}://{hostname}/api/public/v1",
description: "Budibase self hosted API",
variables: { variables: {
protocol: { apiKey: {
default: "http", default: "<user API key>",
description: description: "The API key of the user to assume for API call.",
"Whether HTTP or HTTPS should be used to communicate with your Budibase instance.",
}, },
hostname: { appId: {
default: "localhost:10000", default: "<App ID>",
description: "The URL of your Budibase instance.", description:
"The ID of the app the calls will be executed within the context of, this should start with app_ (production) or app_dev (development).",
}, },
}, },
}, },

View File

@ -8,19 +8,15 @@
"servers": [ "servers": [
{ {
"url": "https://budibase.app/api/public/v1", "url": "https://budibase.app/api/public/v1",
"description": "Budibase Cloud API" "description": "Budibase Cloud API",
},
{
"url": "{protocol}://{hostname}/api/public/v1",
"description": "Budibase self hosted API",
"variables": { "variables": {
"protocol": { "apiKey": {
"default": "http", "default": "<user API key>",
"description": "Whether HTTP or HTTPS should be used to communicate with your Budibase instance." "description": "The API key of the user to assume for API call."
}, },
"hostname": { "appId": {
"default": "localhost:10000", "default": "<App ID>",
"description": "The URL of your Budibase instance." "description": "The ID of the app the calls will be executed within the context of, this should start with app_ (production) or app_dev (development)."
} }
} }
} }
@ -51,6 +47,7 @@
"required": true, "required": true,
"description": "The ID of the app which this request is targeting.", "description": "The ID of the app which this request is targeting.",
"schema": { "schema": {
"default": "{{ appId }}",
"type": "string" "type": "string"
} }
}, },
@ -60,6 +57,7 @@
"required": true, "required": true,
"description": "The ID of the app which this request is targeting.", "description": "The ID of the app which this request is targeting.",
"schema": { "schema": {
"default": "{{ appId }}",
"type": "string" "type": "string"
} }
}, },

View File

@ -6,16 +6,14 @@ info:
servers: servers:
- url: https://budibase.app/api/public/v1 - url: https://budibase.app/api/public/v1
description: Budibase Cloud API description: Budibase Cloud API
- url: "{protocol}://{hostname}/api/public/v1"
description: Budibase self hosted API
variables: variables:
protocol: apiKey:
default: http default: <user API key>
description: Whether HTTP or HTTPS should be used to communicate with your description: The API key of the user to assume for API call.
Budibase instance. appId:
hostname: default: <App ID>
default: localhost:10000 description: The ID of the app the calls will be executed within the context of,
description: The URL of your Budibase instance. this should start with app_ (production) or app_dev (development).
components: components:
parameters: parameters:
tableId: tableId:
@ -38,6 +36,7 @@ components:
required: true required: true
description: The ID of the app which this request is targeting. description: The ID of the app which this request is targeting.
schema: schema:
default: "{{ appId }}"
type: string type: string
appIdUrl: appIdUrl:
in: path in: path
@ -45,6 +44,7 @@ components:
required: true required: true
description: The ID of the app which this request is targeting. description: The ID of the app which this request is targeting.
schema: schema:
default: "{{ appId }}"
type: string type: string
queryId: queryId:
in: path in: path

View File

@ -24,6 +24,7 @@ export const appId = {
required: true, required: true,
description: "The ID of the app which this request is targeting.", description: "The ID of the app which this request is targeting.",
schema: { schema: {
default: "{{ appId }}",
type: "string", type: "string",
}, },
} }
@ -34,6 +35,7 @@ export const appIdUrl = {
required: true, required: true,
description: "The ID of the app which this request is targeting.", description: "The ID of the app which this request is targeting.",
schema: { schema: {
default: "{{ appId }}",
type: "string", type: "string",
}, },
} }

View File

@ -138,7 +138,7 @@ const tableSchema = {
}, },
formulaType: { formulaType: {
type: "string", type: "string",
enum: Object.values(FormulaType), enum: [FormulaType.STATIC, FormulaType.DYNAMIC],
description: description:
"Defines whether this is a static or dynamic formula.", "Defines whether this is a static or dynamic formula.",
}, },

View File

@ -63,7 +63,7 @@ import {
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
import sdk from "../../sdk" import sdk from "../../sdk"
import { builderSocket } from "../../websockets" import { builderSocket } from "../../websockets"
import { sdk as sharedCoreSDK } from "@budibase/shared-core" import { DefaultAppTheme, sdk as sharedCoreSDK } from "@budibase/shared-core"
import * as appMigrations from "../../appMigrations" import * as appMigrations from "../../appMigrations"
// utility function, need to do away with this // utility function, need to do away with this
@ -302,7 +302,7 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
navBackground: "var(--spectrum-global-color-gray-100)", navBackground: "var(--spectrum-global-color-gray-100)",
links: [], links: [],
}, },
theme: "spectrum--light", theme: DefaultAppTheme,
customTheme: { customTheme: {
buttonBorderRadius: "16px", buttonBorderRadius: "16px",
}, },

View File

@ -0,0 +1,102 @@
import { User, Table, SearchFilters, Row } from "@budibase/types"
import { HttpMethod, MakeRequestResponse, generateMakeRequest } from "./utils"
import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
import { Expectations } from "../../../../tests/utilities/api/base"
type RequestOpts = { internal?: boolean; appId?: string }
export interface PublicAPIExpectations {
status?: number
body?: Record<string, any>
}
export class PublicAPIRequest {
private makeRequest: MakeRequestResponse
private appId: string | undefined
tables: PublicTableAPI
rows: PublicRowAPI
apiKey: string
private constructor(
apiKey: string,
makeRequest: MakeRequestResponse,
appId?: string
) {
this.apiKey = apiKey
this.makeRequest = makeRequest
this.appId = appId
this.tables = new PublicTableAPI(this)
this.rows = new PublicRowAPI(this)
}
static async init(config: TestConfiguration, user: User, opts?: RequestOpts) {
const apiKey = await config.generateApiKey(user._id)
const makeRequest = generateMakeRequest(apiKey, opts)
return new this(apiKey, makeRequest, opts?.appId)
}
opts(opts: RequestOpts) {
if (opts.appId) {
this.appId = opts.appId
}
this.makeRequest = generateMakeRequest(this.apiKey, opts)
}
async send(
method: HttpMethod,
endpoint: string,
body?: any,
expectations?: PublicAPIExpectations
) {
if (!this.makeRequest) {
throw new Error("Init has not been called")
}
const res = await this.makeRequest(method, endpoint, body, this.appId)
if (expectations?.status) {
expect(res.status).toEqual(expectations.status)
}
if (expectations?.body) {
expect(res.body).toEqual(expectations?.body)
}
return res.body
}
}
export class PublicTableAPI {
request: PublicAPIRequest
constructor(request: PublicAPIRequest) {
this.request = request
}
async create(
table: Table,
expectations?: PublicAPIExpectations
): Promise<{ data: Table }> {
return this.request.send("post", "/tables", table, expectations)
}
}
export class PublicRowAPI {
request: PublicAPIRequest
constructor(request: PublicAPIRequest) {
this.request = request
}
async search(
tableId: string,
query: SearchFilters,
expectations?: PublicAPIExpectations
): Promise<{ data: Row[] }> {
return this.request.send(
"post",
`/tables/${tableId}/rows/search`,
{
query,
},
expectations
)
}
}

View File

@ -1,4 +1,4 @@
const setup = require("../../tests/utilities") import * as setup from "../../tests/utilities"
describe("/metrics", () => { describe("/metrics", () => {
let request = setup.getRequest() let request = setup.getRequest()

View File

@ -0,0 +1,71 @@
import * as setup from "../../tests/utilities"
import { roles } from "@budibase/backend-core"
import { basicTable } from "../../../../tests/utilities/structures"
import { Table, User } from "@budibase/types"
import { PublicAPIRequest } from "./Request"
describe("check public API security", () => {
const config = setup.getConfig()
let builderRequest: PublicAPIRequest,
appUserRequest: PublicAPIRequest,
table: Table,
appUser: User
beforeAll(async () => {
await config.init()
const builderUser = await config.globalUser()
appUser = await config.globalUser({
builder: { global: false },
roles: {
[config.getProdAppId()]: roles.BUILTIN_ROLE_IDS.BASIC,
},
})
builderRequest = await PublicAPIRequest.init(config, builderUser)
appUserRequest = await PublicAPIRequest.init(config, appUser)
table = (await builderRequest.tables.create(basicTable())).data
})
it("should allow with builder API key", async () => {
const res = await builderRequest.rows.search(
table._id!,
{},
{
status: 200,
}
)
expect(res.data.length).toEqual(0)
})
it("should 403 when from browser, but API key", async () => {
await appUserRequest.rows.search(
table._id!,
{},
{
status: 403,
}
)
})
it("should re-direct when using cookie", async () => {
const headers = await config.login({
userId: appUser._id!,
builder: false,
prodApp: false,
})
await config.withHeaders(
{
...headers,
"User-Agent": config.browserUserAgent(),
},
async () => {
await config.api.row.search(
table._id!,
{ query: {} },
{
status: 302,
}
)
}
)
})
})

View File

@ -21,17 +21,19 @@ export type MakeRequestWithFormDataResponse = (
function base( function base(
apiKey: string, apiKey: string,
endpoint: string, endpoint: string,
intAppId: string | null, opts?: {
isInternal: boolean intAppId?: string
internal?: boolean
}
) { ) {
const extraHeaders: any = { const extraHeaders: any = {
"x-budibase-api-key": apiKey, "x-budibase-api-key": apiKey,
} }
if (intAppId) { if (opts?.intAppId) {
extraHeaders["x-budibase-app-id"] = intAppId extraHeaders["x-budibase-app-id"] = opts.intAppId
} }
const url = isInternal const url = opts?.internal
? endpoint ? endpoint
: checkSlashesInUrl(`/api/public/v1/${endpoint}`) : checkSlashesInUrl(`/api/public/v1/${endpoint}`)
return { headers: extraHeaders, url } return { headers: extraHeaders, url }
@ -39,7 +41,7 @@ function base(
export function generateMakeRequest( export function generateMakeRequest(
apiKey: string, apiKey: string,
isInternal = false opts?: { internal?: boolean }
): MakeRequestResponse { ): MakeRequestResponse {
const request = setup.getRequest()! const request = setup.getRequest()!
const config = setup.getConfig()! const config = setup.getConfig()!
@ -47,9 +49,12 @@ export function generateMakeRequest(
method: HttpMethod, method: HttpMethod,
endpoint: string, endpoint: string,
body?: any, body?: any,
intAppId: string | null = config.getAppId() intAppId: string | undefined = config.getAppId()
) => { ) => {
const { headers, url } = base(apiKey, endpoint, intAppId, isInternal) const { headers, url } = base(apiKey, endpoint, { ...opts, intAppId })
if (body && typeof body !== "string") {
headers["Content-Type"] = "application/json"
}
const req = request[method](url).set(config.defaultHeaders(headers)) const req = request[method](url).set(config.defaultHeaders(headers))
if (body) { if (body) {
req.send(body) req.send(body)
@ -62,7 +67,7 @@ export function generateMakeRequest(
export function generateMakeRequestWithFormData( export function generateMakeRequestWithFormData(
apiKey: string, apiKey: string,
isInternal = false opts?: { internal?: boolean; browser?: boolean }
): MakeRequestWithFormDataResponse { ): MakeRequestWithFormDataResponse {
const request = setup.getRequest()! const request = setup.getRequest()!
const config = setup.getConfig()! const config = setup.getConfig()!
@ -70,9 +75,9 @@ export function generateMakeRequestWithFormData(
method: HttpMethod, method: HttpMethod,
endpoint: string, endpoint: string,
fields: Record<string, string | { path: string }>, fields: Record<string, string | { path: string }>,
intAppId: string | null = config.getAppId() intAppId: string | undefined = config.getAppId()
) => { ) => {
const { headers, url } = base(apiKey, endpoint, intAppId, isInternal) const { headers, url } = base(apiKey, endpoint, { ...opts, intAppId })
const req = request[method](url).set(config.defaultHeaders(headers)) const req = request[method](url).set(config.defaultHeaders(headers))
for (let [field, value] of Object.entries(fields)) { for (let [field, value] of Object.entries(fields)) {
if (typeof value === "string") { if (typeof value === "string") {

View File

@ -1,9 +1,10 @@
const setup = require("./utilities") import * as setup from "./utilities"
const { basicScreen, powerScreen } = setup.structures import { checkBuilderEndpoint, runInProd } from "./utilities/TestFunctions"
const { checkBuilderEndpoint, runInProd } = require("./utilities/TestFunctions") import { roles } from "@budibase/backend-core"
const { roles } = require("@budibase/backend-core") import { Screen } from "@budibase/types"
const { BUILTIN_ROLE_IDS } = roles
const { BUILTIN_ROLE_IDS } = roles
const { basicScreen, powerScreen } = setup.structures
const route = "/test" const route = "/test"
// there are checks which are disabled in test env, // there are checks which are disabled in test env,
@ -12,7 +13,7 @@ const route = "/test"
describe("/routing", () => { describe("/routing", () => {
let request = setup.getRequest() let request = setup.getRequest()
let config = setup.getConfig() let config = setup.getConfig()
let basic, power let basic: Screen, power: Screen
afterAll(setup.afterAll) afterAll(setup.afterAll)
@ -25,26 +26,40 @@ describe("/routing", () => {
describe("fetch", () => { describe("fetch", () => {
it("prevents a public user from accessing development app", async () => { it("prevents a public user from accessing development app", async () => {
await runInProd(() => { await config.withHeaders(
return request {
.get(`/api/routing/client`) "User-Agent": config.browserUserAgent(),
.set(config.publicHeaders({ prodApp: false })) },
.expect(302) async () => {
}) await runInProd(() => {
return request
.get(`/api/routing/client`)
.set(config.publicHeaders({ prodApp: false }))
.expect(302)
})
}
)
}) })
it("prevents a non builder from accessing development app", async () => { it("prevents a non builder from accessing development app", async () => {
await runInProd(async () => { await config.withHeaders(
return request {
.get(`/api/routing/client`) "User-Agent": config.browserUserAgent(),
.set( },
await config.roleHeaders({ async () => {
roleId: BUILTIN_ROLE_IDS.BASIC, await runInProd(async () => {
prodApp: false, return request
}) .get(`/api/routing/client`)
) .set(
.expect(302) await config.roleHeaders({
}) roleId: BUILTIN_ROLE_IDS.BASIC,
prodApp: false,
})
)
.expect(302)
})
}
)
}) })
it("returns the correct routing for basic user", async () => { it("returns the correct routing for basic user", async () => {
const res = await request const res = await request

View File

@ -1,5 +1,9 @@
export const getThemeVariables = (theme: string) => { import { ensureValidTheme } from "@budibase/shared-core"
if (theme === "spectrum--lightest") { import { Theme } from "@budibase/types"
export const getThemeVariables = (theme: Theme) => {
theme = ensureValidTheme(theme, Theme.LIGHT)
if (theme === Theme.LIGHTEST) {
return ` return `
--spectrum-global-color-gray-50: rgb(255, 255, 255); --spectrum-global-color-gray-50: rgb(255, 255, 255);
--spectrum-global-color-gray-200: rgb(244, 244, 244); --spectrum-global-color-gray-200: rgb(244, 244, 244);
@ -7,16 +11,15 @@ export const getThemeVariables = (theme: string) => {
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-50); --spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-50);
` `
} }
if (theme === "spectrum--light") { if (theme === Theme.LIGHT) {
return ` return `
--spectrum-global-color-gray-50: rgb(255, 255, 255); --spectrum-global-color-gray-50: rgb(255, 255, 255);
--spectrum-global-color-gray-200: rgb(234, 234, 234); --spectrum-global-color-gray-200: rgb(234, 234, 234);
--spectrum-global-color-gray-300: rgb(225, 225, 225); --spectrum-global-color-gray-300: rgb(225, 225, 225);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-50); --spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-50);
` `
} }
if (theme === "spectrum--dark") { if (theme === Theme.DARK) {
return ` return `
--spectrum-global-color-gray-100: rgb(50, 50, 50); --spectrum-global-color-gray-100: rgb(50, 50, 50);
--spectrum-global-color-gray-200: rgb(62, 62, 62); --spectrum-global-color-gray-200: rgb(62, 62, 62);
@ -24,7 +27,7 @@ export const getThemeVariables = (theme: string) => {
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100); --spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
` `
} }
if (theme === "spectrum--darkest") { if (theme === Theme.DARKEST) {
return ` return `
--spectrum-global-color-gray-100: rgb(30, 30, 30); --spectrum-global-color-gray-100: rgb(30, 30, 30);
--spectrum-global-color-gray-200: rgb(44, 44, 44); --spectrum-global-color-gray-200: rgb(44, 44, 44);
@ -32,7 +35,7 @@ export const getThemeVariables = (theme: string) => {
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100); --spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
` `
} }
if (theme === "spectrum--nord") { if (theme === Theme.NORD) {
return ` return `
--spectrum-global-color-gray-100: #3b4252; --spectrum-global-color-gray-100: #3b4252;
@ -41,7 +44,7 @@ export const getThemeVariables = (theme: string) => {
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100); --spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
` `
} }
if (theme === "spectrum--midnight") { if (theme === Theme.MIDNIGHT) {
return ` return `
--hue: 220; --hue: 220;
--sat: 10%; --sat: 10%;

View File

@ -277,11 +277,14 @@ export interface components {
| "link" | "link"
| "formula" | "formula"
| "auto" | "auto"
| "ai"
| "json" | "json"
| "internal" | "internal"
| "barcodeqr" | "barcodeqr"
| "signature_single"
| "bigint" | "bigint"
| "bb_reference"; | "bb_reference"
| "bb_reference_single";
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
constraints?: { constraints?: {
/** @enum {string} */ /** @enum {string} */
@ -386,11 +389,14 @@ export interface components {
| "link" | "link"
| "formula" | "formula"
| "auto" | "auto"
| "ai"
| "json" | "json"
| "internal" | "internal"
| "barcodeqr" | "barcodeqr"
| "signature_single"
| "bigint" | "bigint"
| "bb_reference"; | "bb_reference"
| "bb_reference_single";
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
constraints?: { constraints?: {
/** @enum {string} */ /** @enum {string} */
@ -497,11 +503,14 @@ export interface components {
| "link" | "link"
| "formula" | "formula"
| "auto" | "auto"
| "ai"
| "json" | "json"
| "internal" | "internal"
| "barcodeqr" | "barcodeqr"
| "signature_single"
| "bigint" | "bigint"
| "bb_reference"; | "bb_reference"
| "bb_reference_single";
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
constraints?: { constraints?: {
/** @enum {string} */ /** @enum {string} */

View File

@ -10,7 +10,7 @@ import {
import { generateUserMetadataID, isDevAppID } from "../db/utils" import { generateUserMetadataID, isDevAppID } from "../db/utils"
import { getCachedSelf } from "../utilities/global" import { getCachedSelf } from "../utilities/global"
import env from "../environment" import env from "../environment"
import { isWebhookEndpoint } from "./utils" import { isWebhookEndpoint, isBrowser, isApiKey } from "./utils"
import { UserCtx, ContextUser } from "@budibase/types" import { UserCtx, ContextUser } from "@budibase/types"
import tracer from "dd-trace" import tracer from "dd-trace"
@ -27,7 +27,7 @@ export default async (ctx: UserCtx, next: any) => {
} }
// deny access to application preview // deny access to application preview
if (!env.isTest()) { if (isBrowser(ctx) && !isApiKey(ctx)) {
if ( if (
isDevAppID(requestAppId) && isDevAppID(requestAppId) &&
!isWebhookEndpoint(ctx) && !isWebhookEndpoint(ctx) &&

View File

@ -1,4 +1,6 @@
require("../../db").init() import * as db from "../../db"
db.init()
mockAuthWithNoCookie() mockAuthWithNoCookie()
mockWorker() mockWorker()
mockUserGroups() mockUserGroups()
@ -45,7 +47,7 @@ function mockAuthWithNoCookie() {
}, },
cache: { cache: {
user: { user: {
getUser: async id => { getUser: async () => {
return { return {
_id: "us_uuid1", _id: "us_uuid1",
} }
@ -82,7 +84,7 @@ function mockAuthWithCookie() {
}, },
cache: { cache: {
user: { user: {
getUser: async id => { getUser: async () => {
return { return {
_id: "us_uuid1", _id: "us_uuid1",
} }
@ -94,6 +96,10 @@ function mockAuthWithCookie() {
} }
class TestConfiguration { class TestConfiguration {
next: jest.MockedFunction<any>
throw: jest.MockedFunction<any>
ctx: any
constructor() { constructor() {
this.next = jest.fn() this.next = jest.fn()
this.throw = jest.fn() this.throw = jest.fn()
@ -130,7 +136,7 @@ class TestConfiguration {
} }
describe("Current app middleware", () => { describe("Current app middleware", () => {
let config let config: TestConfiguration
beforeEach(() => { beforeEach(() => {
config = new TestConfiguration() config = new TestConfiguration()
@ -192,7 +198,7 @@ describe("Current app middleware", () => {
}, },
cache: { cache: {
user: { user: {
getUser: async id => { getUser: async () => {
return { return {
_id: "us_uuid1", _id: "us_uuid1",
} }

View File

@ -1,9 +1,18 @@
import { BBContext } from "@budibase/types" import { LoginMethod, UserCtx } from "@budibase/types"
const WEBHOOK_ENDPOINTS = new RegExp( const WEBHOOK_ENDPOINTS = new RegExp(
["webhooks/trigger", "webhooks/schema"].join("|") ["webhooks/trigger", "webhooks/schema"].join("|")
) )
export function isWebhookEndpoint(ctx: BBContext) { export function isWebhookEndpoint(ctx: UserCtx) {
return WEBHOOK_ENDPOINTS.test(ctx.request.url) return WEBHOOK_ENDPOINTS.test(ctx.request.url)
} }
export function isBrowser(ctx: UserCtx) {
const browser = ctx.userAgent?.browser
return browser && browser !== "unknown"
}
export function isApiKey(ctx: UserCtx) {
return ctx.loginMethod === LoginMethod.API_KEY
}

View File

@ -423,6 +423,7 @@ export default class TestConfiguration {
Accept: "application/json", Accept: "application/json",
Cookie: [`${constants.Cookie.Auth}=${authToken}`], Cookie: [`${constants.Cookie.Auth}=${authToken}`],
[constants.Header.APP_ID]: appId, [constants.Header.APP_ID]: appId,
...this.temporaryHeaders,
} }
}) })
} }
@ -527,6 +528,10 @@ export default class TestConfiguration {
return this.login({ userId: email, roleId, builder, prodApp }) return this.login({ userId: email, roleId, builder, prodApp })
} }
browserUserAgent() {
return "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
}
// TENANCY // TENANCY
tenantHost() { tenantHost() {

View File

@ -3,6 +3,7 @@ export * from "./api"
export * from "./fields" export * from "./fields"
export * from "./rows" export * from "./rows"
export * from "./colors" export * from "./colors"
export * from "./themes"
export const OperatorOptions = { export const OperatorOptions = {
Equals: { Equals: {

View File

@ -0,0 +1,28 @@
import { ThemeMeta, Theme } from "@budibase/types"
export const ThemeClassPrefix = "spectrum--"
export const DefaultBuilderTheme = Theme.DARKEST
export const DefaultAppTheme = Theme.LIGHT
// Currently available theme options for builder and apps
export const ThemeOptions: ThemeMeta[] = [
{
id: Theme.LIGHT,
name: "Light",
},
{
// We call this dark for simplicity, but we want to use the spectrum darkest style
id: Theme.DARKEST,
name: "Dark",
},
{
id: Theme.NORD,
name: "Nord",
base: Theme.DARKEST,
},
{
id: Theme.MIDNIGHT,
name: "Midnight",
base: Theme.DARKEST,
},
]

View File

@ -4,3 +4,4 @@ export * as helpers from "./helpers"
export * as utils from "./utils" export * as utils from "./utils"
export * as sdk from "./sdk" export * as sdk from "./sdk"
export * from "./table" export * from "./table"
export * from "./themes"

View File

@ -0,0 +1,28 @@
import { getThemeClassNames, ensureValidTheme } from "../themes"
import { Theme } from "@budibase/types"
describe("theme class names", () => {
it("generates class names for a theme without base theme", () => {
expect(getThemeClassNames(Theme.LIGHT)).toStrictEqual("spectrum--light")
})
it("generates class names for a theme with base theme", () => {
expect(getThemeClassNames(Theme.NORD)).toStrictEqual(
"spectrum--darkest spectrum--nord"
)
})
})
describe("theme validity checking", () => {
it("handles no theme", () => {
expect(ensureValidTheme(undefined)).toStrictEqual(Theme.DARKEST)
})
it("allows specifiying a fallback", () => {
expect(ensureValidTheme(undefined, Theme.NORD)).toStrictEqual(Theme.NORD)
})
it("migrates lightest to light", () => {
expect(ensureValidTheme(Theme.LIGHTEST)).toStrictEqual(Theme.LIGHT)
})
it("migrates dark to darkest", () => {
expect(ensureValidTheme(Theme.DARK)).toStrictEqual(Theme.DARKEST)
})
})

View File

@ -0,0 +1,44 @@
import { ThemeOptions, ThemeClassPrefix } from "./constants/themes"
import { Theme } from "@budibase/types"
// Gets the CSS class names for the specified theme
export const getThemeClassNames = (theme: Theme): string => {
theme = ensureValidTheme(theme)
let classNames = `${ThemeClassPrefix}${theme}`
// Prefix with base class if required
const base = ThemeOptions.find(x => x.id === theme)?.base
if (base) {
classNames = `${ThemeClassPrefix}${base} ${classNames}`
}
return classNames
}
// Ensures a theme value is a valid option
export const ensureValidTheme = (
theme?: Theme,
fallback: Theme = Theme.DARKEST
): Theme => {
if (!theme) {
return fallback
}
// Ensure we aren't using the spectrum prefix
if (theme.startsWith(ThemeClassPrefix)) {
theme = theme.split(ThemeClassPrefix)[1] as Theme
}
// Check we aren't using a deprecated theme, and migrate
// to the nearest valid theme if we are
if (!ThemeOptions.some(x => x.id === theme)) {
if (theme === Theme.LIGHTEST) {
return Theme.LIGHT
} else if (theme === Theme.DARK) {
return Theme.DARKEST
} else {
return fallback
}
}
return theme
}

View File

@ -19,7 +19,8 @@
"@types/koa": "2.13.4", "@types/koa": "2.13.4",
"@types/redlock": "4.0.7", "@types/redlock": "4.0.7",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"typescript": "5.5.2" "typescript": "5.5.2",
"koa-useragent": "^4.1.0"
}, },
"dependencies": { "dependencies": {
"scim-patch": "^0.8.1" "scim-patch": "^0.8.1"

View File

@ -17,3 +17,4 @@ export * from "./component"
export * from "./sqlite" export * from "./sqlite"
export * from "./snippet" export * from "./snippet"
export * from "./rowAction" export * from "./rowAction"
export * from "./theme"

View File

@ -0,0 +1,14 @@
export enum Theme {
LIGHTEST = "lightest",
LIGHT = "light",
DARK = "dark",
DARKEST = "darkest",
NORD = "nord",
MIDNIGHT = "midnight",
}
export type ThemeMeta = {
id: string
name: string
base?: Theme
}

View File

@ -12,6 +12,12 @@ import {
import { FeatureFlag, License } from "../sdk" import { FeatureFlag, License } from "../sdk"
import { Files } from "formidable" import { Files } from "formidable"
import { EventType } from "../core" import { EventType } from "../core"
import { UserAgentContext } from "koa-useragent"
export enum LoginMethod {
API_KEY = "api_key",
COOKIE = "cookie",
}
export interface ContextUser extends Omit<User, "roles"> { export interface ContextUser extends Omit<User, "roles"> {
globalId?: string globalId?: string
@ -41,6 +47,7 @@ export interface BBRequest<RequestBody> extends Request {
export interface Ctx<RequestBody = any, ResponseBody = any> extends Context { export interface Ctx<RequestBody = any, ResponseBody = any> extends Context {
request: BBRequest<RequestBody> request: BBRequest<RequestBody>
body: ResponseBody body: ResponseBody
userAgent: UserAgentContext["userAgent"]
} }
/** /**
@ -51,6 +58,7 @@ export interface UserCtx<RequestBody = any, ResponseBody = any>
user: ContextUser user: ContextUser
roleId?: string roleId?: string
eventEmitter?: ContextEmitter eventEmitter?: ContextEmitter
loginMethod?: LoginMethod
} }
/** /**