diff --git a/.github/workflows/deploy-featurebranch.yml b/.github/workflows/deploy-featurebranch.yml index 463074e836..d86d301507 100644 --- a/.github/workflows/deploy-featurebranch.yml +++ b/.github/workflows/deploy-featurebranch.yml @@ -23,7 +23,6 @@ jobs: PAYLOAD_BRANCH: ${{ github.head_ref }} PAYLOAD_PR_NUMBER: ${{ github.event.pull_request.number }} PAYLOAD_LICENSE_TYPE: "free" - PAYLOAD_DEPLOY: true with: repository: budibase/budibase-deploys event: featurebranch-qa-deploy diff --git a/charts/budibase/templates/app-service-deployment.yaml b/charts/budibase/templates/app-service-deployment.yaml index 2b099d01f5..4d0560312f 100644 --- a/charts/budibase/templates/app-service-deployment.yaml +++ b/charts/budibase/templates/app-service-deployment.yaml @@ -184,6 +184,10 @@ spec: - name: NODE_DEBUG value: {{ .Values.services.apps.nodeDebug | quote }} {{ end }} + {{ if .Values.services.apps.xssSafeMode }} + - name: XSS_SAFE_MODE + value: {{ .Values.services.apps.xssSafeMode | quote }} + {{ end }} {{ if .Values.globals.datadogApmEnabled }} - name: DD_LOGS_INJECTION value: {{ .Values.globals.datadogApmEnabled | quote }} diff --git a/lerna.json b/lerna.json index a4bcb56d38..faed02052e 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "2.32.11", + "version": "2.32.13", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/packages/account-portal b/packages/account-portal index 3e24f6293f..8cd052ce82 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit 3e24f6293ff5ee5f9b42822e001504e3bbf19cc0 +Subproject commit 8cd052ce8288f343812a514d06c5a9459b3ba1a8 diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index 472e0f44ad..2b37526dde 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -375,11 +375,21 @@ export class DatabaseImpl implements Database { return this.performCall(() => { return async () => { const response = await directCouchUrlCall(args) - const json = await response.json() + const text = await response.text() if (response.status > 300) { + let json + try { + json = JSON.parse(text) + } catch (err) { + console.error(`SQS error: ${text}`) + throw new CouchDBError( + "error while running SQS query, please try again later", + { name: "sqs_error", status: response.status } + ) + } throw json } - return json as T + return JSON.parse(text) as T } }) } diff --git a/packages/backend-core/src/features/features.ts b/packages/backend-core/src/features/features.ts new file mode 100644 index 0000000000..90a395d52a --- /dev/null +++ b/packages/backend-core/src/features/features.ts @@ -0,0 +1,300 @@ +import env from "../environment" +import * as context from "../context" +import { PostHog, PostHogOptions } from "posthog-node" +import { FeatureFlag, IdentityType, UserCtx } from "@budibase/types" +import tracer from "dd-trace" +import { Duration } from "../utils" + +let posthog: PostHog | undefined +export function init(opts?: PostHogOptions) { + if ( + env.POSTHOG_TOKEN && + env.POSTHOG_API_HOST && + !env.SELF_HOSTED && + env.POSTHOG_FEATURE_FLAGS_ENABLED + ) { + console.log("initializing posthog client...") + posthog = new PostHog(env.POSTHOG_TOKEN, { + host: env.POSTHOG_API_HOST, + personalApiKey: env.POSTHOG_PERSONAL_TOKEN, + featureFlagsPollingInterval: Duration.fromMinutes(3).toMs(), + ...opts, + }) + } else { + console.log("posthog disabled") + } +} + +export function shutdown() { + posthog?.shutdown() +} + +export abstract class Flag { + static boolean(defaultValue: boolean): Flag { + return new BooleanFlag(defaultValue) + } + + static string(defaultValue: string): Flag { + return new StringFlag(defaultValue) + } + + static number(defaultValue: number): Flag { + return new NumberFlag(defaultValue) + } + + protected constructor(public defaultValue: T) {} + + abstract parse(value: any): T +} + +type UnwrapFlag = F extends Flag ? U : never + +export type FlagValues = { + [K in keyof T]: UnwrapFlag +} + +type KeysOfType = { + [K in keyof T]: T[K] extends Flag ? K : never +}[keyof T] + +class BooleanFlag extends Flag { + parse(value: any) { + if (typeof value === "string") { + return ["true", "t", "1"].includes(value.toLowerCase()) + } + + if (typeof value === "boolean") { + return value + } + + throw new Error(`could not parse value "${value}" as boolean`) + } +} + +class StringFlag extends Flag { + parse(value: any) { + if (typeof value === "string") { + return value + } + throw new Error(`could not parse value "${value}" as string`) + } +} + +class NumberFlag extends Flag { + parse(value: any) { + if (typeof value === "number") { + return value + } + + if (typeof value === "string") { + const parsed = parseFloat(value) + if (!isNaN(parsed)) { + return parsed + } + } + + throw new Error(`could not parse value "${value}" as number`) + } +} + +export interface EnvFlagEntry { + tenantId: string + key: string + value: boolean +} + +export function parseEnvFlags(flags: string): EnvFlagEntry[] { + const split = flags.split(",").map(x => x.split(":")) + const result: EnvFlagEntry[] = [] + for (const [tenantId, ...features] of split) { + for (let feature of features) { + let value = true + if (feature.startsWith("!")) { + feature = feature.slice(1) + value = false + } + result.push({ tenantId, key: feature, value }) + } + } + return result +} + +export class FlagSet, T extends { [key: string]: V }> { + // This is used to safely cache flags sets in the current request context. + // Because multiple sets could theoretically exist, we don't want the cache of + // one to leak into another. + private readonly setId: string + + constructor(private readonly flagSchema: T) { + this.setId = crypto.randomUUID() + } + + defaults(): FlagValues { + return Object.keys(this.flagSchema).reduce((acc, key) => { + const typedKey = key as keyof T + acc[typedKey] = this.flagSchema[key].defaultValue + return acc + }, {} as FlagValues) + } + + isFlagName(name: string | number | symbol): name is keyof T { + return this.flagSchema[name as keyof T] !== undefined + } + + async get( + key: K, + ctx?: UserCtx + ): Promise[K]> { + const flags = await this.fetch(ctx) + return flags[key] + } + + async isEnabled>( + key: K, + ctx?: UserCtx + ): Promise { + const flags = await this.fetch(ctx) + return flags[key] + } + + async fetch(ctx?: UserCtx): Promise> { + return await tracer.trace("features.fetch", async span => { + const cachedFlags = context.getFeatureFlags>(this.setId) + if (cachedFlags) { + span?.addTags({ fromCache: true }) + return cachedFlags + } + + const tags: Record = {} + const flagValues = this.defaults() + const currentTenantId = context.getTenantId() + const specificallySetFalse = new Set() + + for (const { tenantId, key, value } of parseEnvFlags( + env.TENANT_FEATURE_FLAGS || "" + )) { + if (!tenantId || (tenantId !== "*" && tenantId !== currentTenantId)) { + continue + } + + tags[`readFromEnvironmentVars`] = true + + if (value === false) { + specificallySetFalse.add(key) + } + + // ignore unknown flags + if (!this.isFlagName(key)) { + continue + } + + if (typeof flagValues[key] !== "boolean") { + throw new Error(`Feature: ${key} is not a boolean`) + } + + // @ts-expect-error - TS does not like you writing into a generic type, + // but we know that it's okay in this case because it's just an object. + flagValues[key as keyof FlagValues] = value + tags[`flags.${key}.source`] = "environment" + } + + const license = ctx?.user?.license + if (license) { + tags[`readFromLicense`] = true + + for (const feature of license.features) { + if (!this.isFlagName(feature)) { + continue + } + + if ( + flagValues[feature] === true || + specificallySetFalse.has(feature) + ) { + // If the flag is already set to through environment variables, we + // don't want to override it back to false here. + continue + } + + // @ts-expect-error - TS does not like you writing into a generic type, + // but we know that it's okay in this case because it's just an object. + flagValues[feature] = true + tags[`flags.${feature}.source`] = "license" + } + } + + const identity = context.getIdentity() + tags[`identity.type`] = identity?.type + tags[`identity.tenantId`] = identity?.tenantId + tags[`identity._id`] = identity?._id + + if (posthog && identity?.type === IdentityType.USER) { + tags[`readFromPostHog`] = true + + const personProperties: Record = {} + if (identity.tenantId) { + personProperties.tenantId = identity.tenantId + } + + const posthogFlags = await posthog.getAllFlagsAndPayloads( + identity._id, + { + personProperties, + } + ) + + for (const [name, value] of Object.entries(posthogFlags.featureFlags)) { + if (!this.isFlagName(name)) { + // We don't want an unexpected PostHog flag to break the app, so we + // just log it and continue. + console.warn(`Unexpected posthog flag "${name}": ${value}`) + continue + } + + if (flagValues[name] === true || specificallySetFalse.has(name)) { + // If the flag is already set to through environment variables, we + // don't want to override it back to false here. + continue + } + + const payload = posthogFlags.featureFlagPayloads?.[name] + const flag = this.flagSchema[name] + try { + // @ts-expect-error - TS does not like you writing into a generic + // type, but we know that it's okay in this case because it's just + // an object. + flagValues[name] = flag.parse(payload || value) + tags[`flags.${name}.source`] = "posthog" + } catch (err) { + // We don't want an invalid PostHog flag to break the app, so we just + // log it and continue. + console.warn(`Error parsing posthog flag "${name}": ${value}`, err) + } + } + } + + context.setFeatureFlags(this.setId, flagValues) + for (const [key, value] of Object.entries(flagValues)) { + tags[`flags.${key}.value`] = value + } + span?.addTags(tags) + + return flagValues + }) + } +} + +// This is the primary source of truth for feature flags. If you want to add a +// new flag, add it here and use the `fetch` and `get` functions to access it. +// All of the machinery in this file is to make sure that flags have their +// default values set correctly and their types flow through the system. +export const flags = new FlagSet({ + DEFAULT_VALUES: Flag.boolean(env.isDev()), + AUTOMATION_BRANCHING: Flag.boolean(env.isDev()), + SQS: Flag.boolean(env.isDev()), + [FeatureFlag.AI_CUSTOM_CONFIGS]: Flag.boolean(env.isDev()), + [FeatureFlag.ENRICHED_RELATIONSHIPS]: Flag.boolean(env.isDev()), +}) + +type UnwrapPromise = T extends Promise ? U : T +export type FeatureFlags = UnwrapPromise> diff --git a/packages/backend-core/src/features/index.ts b/packages/backend-core/src/features/index.ts index 2b915e5689..f77a62fd4d 100644 --- a/packages/backend-core/src/features/index.ts +++ b/packages/backend-core/src/features/index.ts @@ -1,281 +1,2 @@ -import env from "../environment" -import * as context from "../context" -import { PostHog, PostHogOptions } from "posthog-node" -import { FeatureFlag, IdentityType, UserCtx } from "@budibase/types" -import tracer from "dd-trace" -import { Duration } from "../utils" - -let posthog: PostHog | undefined -export function init(opts?: PostHogOptions) { - if ( - env.POSTHOG_TOKEN && - env.POSTHOG_API_HOST && - !env.SELF_HOSTED && - env.POSTHOG_FEATURE_FLAGS_ENABLED - ) { - console.log("initializing posthog client...") - posthog = new PostHog(env.POSTHOG_TOKEN, { - host: env.POSTHOG_API_HOST, - personalApiKey: env.POSTHOG_PERSONAL_TOKEN, - featureFlagsPollingInterval: Duration.fromMinutes(3).toMs(), - ...opts, - }) - } else { - console.log("posthog disabled") - } -} - -export function shutdown() { - posthog?.shutdown() -} - -export abstract class Flag { - static boolean(defaultValue: boolean): Flag { - return new BooleanFlag(defaultValue) - } - - static string(defaultValue: string): Flag { - return new StringFlag(defaultValue) - } - - static number(defaultValue: number): Flag { - return new NumberFlag(defaultValue) - } - - protected constructor(public defaultValue: T) {} - - abstract parse(value: any): T -} - -type UnwrapFlag = F extends Flag ? U : never - -export type FlagValues = { - [K in keyof T]: UnwrapFlag -} - -type KeysOfType = { - [K in keyof T]: T[K] extends Flag ? K : never -}[keyof T] - -class BooleanFlag extends Flag { - parse(value: any) { - if (typeof value === "string") { - return ["true", "t", "1"].includes(value.toLowerCase()) - } - - if (typeof value === "boolean") { - return value - } - - throw new Error(`could not parse value "${value}" as boolean`) - } -} - -class StringFlag extends Flag { - parse(value: any) { - if (typeof value === "string") { - return value - } - throw new Error(`could not parse value "${value}" as string`) - } -} - -class NumberFlag extends Flag { - parse(value: any) { - if (typeof value === "number") { - return value - } - - if (typeof value === "string") { - const parsed = parseFloat(value) - if (!isNaN(parsed)) { - return parsed - } - } - - throw new Error(`could not parse value "${value}" as number`) - } -} - -export class FlagSet, T extends { [key: string]: V }> { - // This is used to safely cache flags sets in the current request context. - // Because multiple sets could theoretically exist, we don't want the cache of - // one to leak into another. - private readonly setId: string - - constructor(private readonly flagSchema: T) { - this.setId = crypto.randomUUID() - } - - defaults(): FlagValues { - return Object.keys(this.flagSchema).reduce((acc, key) => { - const typedKey = key as keyof T - acc[typedKey] = this.flagSchema[key].defaultValue - return acc - }, {} as FlagValues) - } - - isFlagName(name: string | number | symbol): name is keyof T { - return this.flagSchema[name as keyof T] !== undefined - } - - async get( - key: K, - ctx?: UserCtx - ): Promise[K]> { - const flags = await this.fetch(ctx) - return flags[key] - } - - async isEnabled>( - key: K, - ctx?: UserCtx - ): Promise { - const flags = await this.fetch(ctx) - return flags[key] - } - - async fetch(ctx?: UserCtx): Promise> { - return await tracer.trace("features.fetch", async span => { - const cachedFlags = context.getFeatureFlags>(this.setId) - if (cachedFlags) { - span?.addTags({ fromCache: true }) - return cachedFlags - } - - const tags: Record = {} - const flagValues = this.defaults() - const currentTenantId = context.getTenantId() - const specificallySetFalse = new Set() - - const split = (env.TENANT_FEATURE_FLAGS || "") - .split(",") - .map(x => x.split(":")) - for (const [tenantId, ...features] of split) { - if (!tenantId || (tenantId !== "*" && tenantId !== currentTenantId)) { - continue - } - - tags[`readFromEnvironmentVars`] = true - - for (let feature of features) { - let value = true - if (feature.startsWith("!")) { - feature = feature.slice(1) - value = false - specificallySetFalse.add(feature) - } - - // ignore unknown flags - if (!this.isFlagName(feature)) { - continue - } - - if (typeof flagValues[feature] !== "boolean") { - throw new Error(`Feature: ${feature} is not a boolean`) - } - - // @ts-expect-error - TS does not like you writing into a generic type, - // but we know that it's okay in this case because it's just an object. - flagValues[feature as keyof FlagValues] = value - tags[`flags.${feature}.source`] = "environment" - } - } - - const license = ctx?.user?.license - if (license) { - tags[`readFromLicense`] = true - - for (const feature of license.features) { - if (!this.isFlagName(feature)) { - continue - } - - if ( - flagValues[feature] === true || - specificallySetFalse.has(feature) - ) { - // If the flag is already set to through environment variables, we - // don't want to override it back to false here. - continue - } - - // @ts-expect-error - TS does not like you writing into a generic type, - // but we know that it's okay in this case because it's just an object. - flagValues[feature] = true - tags[`flags.${feature}.source`] = "license" - } - } - - const identity = context.getIdentity() - tags[`identity.type`] = identity?.type - tags[`identity.tenantId`] = identity?.tenantId - tags[`identity._id`] = identity?._id - - if (posthog && identity?.type === IdentityType.USER) { - tags[`readFromPostHog`] = true - - const personProperties: Record = {} - if (identity.tenantId) { - personProperties.tenantId = identity.tenantId - } - - const posthogFlags = await posthog.getAllFlagsAndPayloads( - identity._id, - { - personProperties, - } - ) - - for (const [name, value] of Object.entries(posthogFlags.featureFlags)) { - if (!this.isFlagName(name)) { - // We don't want an unexpected PostHog flag to break the app, so we - // just log it and continue. - console.warn(`Unexpected posthog flag "${name}": ${value}`) - continue - } - - if (flagValues[name] === true || specificallySetFalse.has(name)) { - // If the flag is already set to through environment variables, we - // don't want to override it back to false here. - continue - } - - const payload = posthogFlags.featureFlagPayloads?.[name] - const flag = this.flagSchema[name] - try { - // @ts-expect-error - TS does not like you writing into a generic - // type, but we know that it's okay in this case because it's just - // an object. - flagValues[name] = flag.parse(payload || value) - tags[`flags.${name}.source`] = "posthog" - } catch (err) { - // We don't want an invalid PostHog flag to break the app, so we just - // log it and continue. - console.warn(`Error parsing posthog flag "${name}": ${value}`, err) - } - } - } - - context.setFeatureFlags(this.setId, flagValues) - for (const [key, value] of Object.entries(flagValues)) { - tags[`flags.${key}.value`] = value - } - span?.addTags(tags) - - return flagValues - }) - } -} - -// This is the primary source of truth for feature flags. If you want to add a -// new flag, add it here and use the `fetch` and `get` functions to access it. -// All of the machinery in this file is to make sure that flags have their -// default values set correctly and their types flow through the system. -export const flags = new FlagSet({ - DEFAULT_VALUES: Flag.boolean(env.isDev()), - AUTOMATION_BRANCHING: Flag.boolean(env.isDev()), - SQS: Flag.boolean(env.isDev()), - [FeatureFlag.AI_CUSTOM_CONFIGS]: Flag.boolean(env.isDev()), - [FeatureFlag.ENRICHED_RELATIONSHIPS]: Flag.boolean(env.isDev()), -}) +export * from "./features" +export * as testutils from "./tests/utils" diff --git a/packages/backend-core/src/features/tests/utils.ts b/packages/backend-core/src/features/tests/utils.ts new file mode 100644 index 0000000000..cc633c083d --- /dev/null +++ b/packages/backend-core/src/features/tests/utils.ts @@ -0,0 +1,64 @@ +import { FeatureFlags, parseEnvFlags } from ".." +import { setEnv } from "../../environment" + +function getCurrentFlags(): Record> { + const result: Record> = {} + for (const { tenantId, key, value } of parseEnvFlags( + process.env.TENANT_FEATURE_FLAGS || "" + )) { + const tenantFlags = result[tenantId] || {} + // Don't allow overwriting specifically false flags, to match the beheaviour + // of FlagSet. + if (tenantFlags[key] === false) { + continue + } + tenantFlags[key] = value + result[tenantId] = tenantFlags + } + return result +} + +function buildFlagString( + flags: Record> +): string { + const parts: string[] = [] + for (const [tenantId, tenantFlags] of Object.entries(flags)) { + for (const [key, value] of Object.entries(tenantFlags)) { + if (value === false) { + parts.push(`${tenantId}:!${key}`) + } else { + parts.push(`${tenantId}:${key}`) + } + } + } + return parts.join(",") +} + +export function setFeatureFlags( + tenantId: string, + flags: Partial +): () => void { + const current = getCurrentFlags() + for (const [key, value] of Object.entries(flags)) { + const tenantFlags = current[tenantId] || {} + tenantFlags[key] = value + current[tenantId] = tenantFlags + } + const flagString = buildFlagString(current) + return setEnv({ TENANT_FEATURE_FLAGS: flagString }) +} + +export function withFeatureFlags( + tenantId: string, + flags: Partial, + f: () => T +) { + const cleanup = setFeatureFlags(tenantId, flags) + const result = f() + if (result instanceof Promise) { + return result.finally(cleanup) + } else { + cleanup() + return result + } +} diff --git a/packages/backend-core/src/middleware/passport/sso/sso.ts b/packages/backend-core/src/middleware/passport/sso/sso.ts index ee84f03dae..8901fcc56f 100644 --- a/packages/backend-core/src/middleware/passport/sso/sso.ts +++ b/packages/backend-core/src/middleware/passport/sso/sso.ts @@ -2,7 +2,6 @@ import { generateGlobalUserID } from "../../../db" import { authError } from "../utils" import * as users from "../../../users" import * as context from "../../../context" -import fetch from "node-fetch" import { SaveSSOUserFunction, SSOAuthDetails, @@ -97,28 +96,13 @@ export async function authenticate( return done(null, ssoUser) } -async function getProfilePictureUrl(user: User, details: SSOAuthDetails) { - const pictureUrl = details.profile?._json.picture - if (pictureUrl) { - const response = await fetch(pictureUrl) - if (response.status === 200) { - const type = response.headers.get("content-type") as string - if (type.startsWith("image/")) { - return pictureUrl - } - } - } -} - /** * @returns a user that has been sync'd with third party information */ async function syncUser(user: User, details: SSOAuthDetails): Promise { let firstName let lastName - let pictureUrl let oauth2 - let thirdPartyProfile if (details.profile) { const profile = details.profile @@ -134,12 +118,6 @@ async function syncUser(user: User, details: SSOAuthDetails): Promise { lastName = name.familyName } } - - pictureUrl = await getProfilePictureUrl(user, details) - - thirdPartyProfile = { - ...profile._json, - } } // oauth tokens for future use @@ -155,8 +133,6 @@ async function syncUser(user: User, details: SSOAuthDetails): Promise { providerType: details.providerType, firstName, lastName, - thirdPartyProfile, - pictureUrl, oauth2, } } diff --git a/packages/backend-core/src/users/users.ts b/packages/backend-core/src/users/users.ts index d8546afa8b..f4838597b6 100644 --- a/packages/backend-core/src/users/users.ts +++ b/packages/backend-core/src/users/users.ts @@ -24,6 +24,7 @@ import * as context from "../context" import { getGlobalDB } from "../context" import { isCreator } from "./utils" import { UserDB } from "./db" +import { dataFilters } from "@budibase/shared-core" type GetOpts = { cleanup?: boolean } @@ -262,10 +263,17 @@ export async function paginatedUsers({ userList = await bulkGetGlobalUsersById(query?.oneOf?._id, { cleanup: true, }) + } else if (query) { + // TODO: this should use SQS search, but the logic is built in the 'server' package. Using the in-memory filtering to get this working meanwhile + const response = await db.allDocs( + getGlobalUserParams(null, { ...opts, limit: undefined }) + ) + userList = response.rows.map(row => row.doc!) + userList = dataFilters.search(userList, { query, limit: opts.limit }).rows } else { // no search, query allDocs - const response = await db.allDocs(getGlobalUserParams(null, opts)) - userList = response.rows.map((row: any) => row.doc) + const response = await db.allDocs(getGlobalUserParams(null, opts)) + userList = response.rows.map(row => row.doc!) } return pagination(userList, pageSize, { paginate: true, diff --git a/packages/backend-core/tests/core/utilities/structures/accounts.ts b/packages/backend-core/tests/core/utilities/structures/accounts.ts index 29453ad60a..7910f3c423 100644 --- a/packages/backend-core/tests/core/utilities/structures/accounts.ts +++ b/packages/backend-core/tests/core/utilities/structures/accounts.ts @@ -6,9 +6,6 @@ import { AccountSSOProviderType, AuthType, CloudAccount, - CreateAccount, - CreatePassswordAccount, - CreateVerifiableSSOAccount, Hosting, SSOAccount, } from "@budibase/types" @@ -19,6 +16,7 @@ export const account = (partial: Partial = {}): Account => { accountId: uuid(), tenantId: generator.word(), email: generator.email({ domain: "example.com" }), + accountName: generator.word(), tenantName: generator.word(), hosting: Hosting.SELF, createdAt: Date.now(), @@ -61,10 +59,8 @@ export function ssoAccount(account: Account = cloudAccount()): SSOAccount { accessToken: generator.string(), refreshToken: generator.string(), }, - pictureUrl: generator.url(), provider: provider(), providerType: providerType(), - thirdPartyProfile: {}, } } @@ -78,68 +74,7 @@ export function verifiableSsoAccount( accessToken: generator.string(), refreshToken: generator.string(), }, - pictureUrl: generator.url(), provider: AccountSSOProvider.MICROSOFT, providerType: AccountSSOProviderType.MICROSOFT, - thirdPartyProfile: { id: "abc123" }, } } - -export const cloudCreateAccount: CreatePassswordAccount = { - email: "cloud@budibase.com", - tenantId: "cloud", - hosting: Hosting.CLOUD, - authType: AuthType.PASSWORD, - password: "Password123!", - tenantName: "cloud", - name: "Budi Armstrong", - size: "10+", - profession: "Software Engineer", -} - -export const cloudSSOCreateAccount: CreateAccount = { - email: "cloud-sso@budibase.com", - tenantId: "cloud-sso", - hosting: Hosting.CLOUD, - authType: AuthType.SSO, - tenantName: "cloudsso", - name: "Budi Armstrong", - size: "10+", - profession: "Software Engineer", -} - -export const cloudVerifiableSSOCreateAccount: CreateVerifiableSSOAccount = { - email: "cloud-sso@budibase.com", - tenantId: "cloud-sso", - hosting: Hosting.CLOUD, - authType: AuthType.SSO, - tenantName: "cloudsso", - name: "Budi Armstrong", - size: "10+", - profession: "Software Engineer", - provider: AccountSSOProvider.MICROSOFT, - thirdPartyProfile: { id: "abc123" }, -} - -export const selfCreateAccount: CreatePassswordAccount = { - email: "self@budibase.com", - tenantId: "self", - hosting: Hosting.SELF, - authType: AuthType.PASSWORD, - password: "Password123!", - tenantName: "self", - name: "Budi Armstrong", - size: "10+", - profession: "Software Engineer", -} - -export const selfSSOCreateAccount: CreateAccount = { - email: "self-sso@budibase.com", - tenantId: "self-sso", - hosting: Hosting.SELF, - authType: AuthType.SSO, - tenantName: "selfsso", - name: "Budi Armstrong", - size: "10+", - profession: "Software Engineer", -} diff --git a/packages/backend-core/tests/core/utilities/structures/users.ts b/packages/backend-core/tests/core/utilities/structures/users.ts index 0171353e23..ffddae663b 100644 --- a/packages/backend-core/tests/core/utilities/structures/users.ts +++ b/packages/backend-core/tests/core/utilities/structures/users.ts @@ -25,7 +25,6 @@ export const user = (userProps?: Partial>): User => { roles: { app_test: "admin" }, firstName: generator.first(), lastName: generator.last(), - pictureUrl: "http://example.com", tenantId: tenant.id(), ...userProps, } @@ -86,9 +85,5 @@ export function ssoUser( oauth2: opts.details?.oauth2, provider: opts.details?.provider!, providerType: opts.details?.providerType!, - thirdPartyProfile: { - email: base.email, - picture: base.pictureUrl, - }, } } diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte index 9a073d041f..ddd37dc4a3 100644 --- a/packages/builder/src/pages/builder/portal/apps/index.svelte +++ b/packages/builder/src/pages/builder/portal/apps/index.svelte @@ -26,6 +26,7 @@ licensing, environment, enrichedApps, + sortBy, } from "stores/portal" import { goto } from "@roxi/routify" import AppRow from "components/start/AppRow.svelte" @@ -247,7 +248,7 @@