Merge branch 'master' into fix/automation-layout-styling

This commit is contained in:
Peter Clement 2023-11-08 14:50:40 +00:00 committed by GitHub
commit 4da8a3899f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 364 additions and 233 deletions

View File

@ -26,7 +26,7 @@ export async function fetchSelf(ctx: UserCtx) {
} }
const appId = context.getAppId() const appId = context.getAppId()
let user: ContextUser = await getFullUser(ctx, userId) let user: ContextUser = await getFullUser(userId)
// this shouldn't be returned by the app self // this shouldn't be returned by the app self
delete user.roles delete user.roles
// forward the csrf token from the session // forward the csrf token from the session

View File

@ -2,7 +2,6 @@ import * as linkRows from "../../../db/linkedRows"
import { import {
generateRowID, generateRowID,
getMultiIDParams, getMultiIDParams,
getTableIDFromRowID,
InternalTables, InternalTables,
} from "../../../db/utils" } from "../../../db/utils"
import * as userController from "../user" import * as userController from "../user"
@ -89,7 +88,7 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
if (isUserTable) { if (isUserTable) {
// the row has been updated, need to put it into the ctx // the row has been updated, need to put it into the ctx
ctx.request.body = row as any ctx.request.body = row as any
await userController.updateMetadata(ctx) await userController.updateMetadata(ctx as any)
return { row: ctx.body as Row, table } return { row: ctx.body as Row, table }
} }

View File

@ -1,14 +1,26 @@
import { generateUserFlagID, InternalTables } from "../../db/utils" import { generateUserFlagID, InternalTables } from "../../db/utils"
import { getFullUser } from "../../utilities/users" import { getFullUser } from "../../utilities/users"
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
import { Ctx, UserCtx } from "@budibase/types" import {
ContextUserMetadata,
Ctx,
FetchUserMetadataResponse,
FindUserMetadataResponse,
Flags,
SetFlagRequest,
UserCtx,
UserMetadata,
} from "@budibase/types"
import sdk from "../../sdk" import sdk from "../../sdk"
import { DocumentInsertResponse } from "@budibase/nano"
export async function fetchMetadata(ctx: Ctx) { export async function fetchMetadata(ctx: Ctx<void, FetchUserMetadataResponse>) {
ctx.body = await sdk.users.fetchMetadata() ctx.body = await sdk.users.fetchMetadata()
} }
export async function updateSelfMetadata(ctx: UserCtx) { export async function updateSelfMetadata(
ctx: UserCtx<UserMetadata, DocumentInsertResponse>
) {
// overwrite the ID with current users // overwrite the ID with current users
ctx.request.body._id = ctx.user?._id ctx.request.body._id = ctx.user?._id
// make sure no stale rev // make sure no stale rev
@ -18,19 +30,21 @@ export async function updateSelfMetadata(ctx: UserCtx) {
await updateMetadata(ctx) await updateMetadata(ctx)
} }
export async function updateMetadata(ctx: UserCtx) { export async function updateMetadata(
ctx: UserCtx<UserMetadata, DocumentInsertResponse>
) {
const db = context.getAppDB() const db = context.getAppDB()
const user = ctx.request.body const user = ctx.request.body
// this isn't applicable to the user const metadata: ContextUserMetadata = {
delete user.roles
const metadata = {
tableId: InternalTables.USER_METADATA, tableId: InternalTables.USER_METADATA,
...user, ...user,
} }
// this isn't applicable to the user
delete metadata.roles
ctx.body = await db.put(metadata) ctx.body = await db.put(metadata)
} }
export async function destroyMetadata(ctx: UserCtx) { export async function destroyMetadata(ctx: UserCtx<void, { message: string }>) {
const db = context.getAppDB() const db = context.getAppDB()
try { try {
const dbUser = await sdk.users.get(ctx.params.id) const dbUser = await sdk.users.get(ctx.params.id)
@ -43,11 +57,15 @@ export async function destroyMetadata(ctx: UserCtx) {
} }
} }
export async function findMetadata(ctx: UserCtx) { export async function findMetadata(
ctx.body = await getFullUser(ctx, ctx.params.id) ctx: UserCtx<void, FindUserMetadataResponse>
) {
ctx.body = await getFullUser(ctx.params.id)
} }
export async function setFlag(ctx: UserCtx) { export async function setFlag(
ctx: UserCtx<SetFlagRequest, { message: string }>
) {
const userId = ctx.user?._id const userId = ctx.user?._id
const { flag, value } = ctx.request.body const { flag, value } = ctx.request.body
if (!flag) { if (!flag) {
@ -55,9 +73,9 @@ export async function setFlag(ctx: UserCtx) {
} }
const flagDocId = generateUserFlagID(userId!) const flagDocId = generateUserFlagID(userId!)
const db = context.getAppDB() const db = context.getAppDB()
let doc let doc: Flags
try { try {
doc = await db.get<any>(flagDocId) doc = await db.get<Flags>(flagDocId)
} catch (err) { } catch (err) {
doc = { _id: flagDocId } doc = { _id: flagDocId }
} }
@ -66,13 +84,13 @@ export async function setFlag(ctx: UserCtx) {
ctx.body = { message: "Flag set successfully" } ctx.body = { message: "Flag set successfully" }
} }
export async function getFlags(ctx: UserCtx) { export async function getFlags(ctx: UserCtx<void, Flags>) {
const userId = ctx.user?._id const userId = ctx.user?._id
const docId = generateUserFlagID(userId!) const docId = generateUserFlagID(userId!)
const db = context.getAppDB() const db = context.getAppDB()
let doc let doc: Flags
try { try {
doc = await db.get(docId) doc = await db.get<Flags>(docId)
} catch (err) { } catch (err) {
doc = { _id: docId } doc = { _id: docId }
} }

View File

@ -1,208 +0,0 @@
const { roles, utils } = require("@budibase/backend-core")
const { checkPermissionsEndpoint } = require("./utilities/TestFunctions")
const setup = require("./utilities")
const { BUILTIN_ROLE_IDS } = roles
jest.setTimeout(30000)
jest.mock("../../../utilities/workerRequests", () => ({
getGlobalUsers: jest.fn(() => {
return {}
}),
getGlobalSelf: jest.fn(() => {
return {}
}),
deleteGlobalUser: jest.fn(),
}))
describe("/users", () => {
let request = setup.getRequest()
let config = setup.getConfig()
afterAll(setup.afterAll)
beforeAll(async () => {
await config.init()
})
describe("fetch", () => {
it("returns a list of users from an instance db", async () => {
await config.createUser({ id: "uuidx" })
await config.createUser({ id: "uuidy" })
const res = await request
.get(`/api/users/metadata`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.length).toBe(3)
expect(res.body.find(u => u._id === `ro_ta_users_us_uuidx`)).toBeDefined()
expect(res.body.find(u => u._id === `ro_ta_users_us_uuidy`)).toBeDefined()
})
it("should apply authorization to endpoint", async () => {
await config.createUser()
await checkPermissionsEndpoint({
config,
request,
method: "GET",
url: `/api/users/metadata`,
passRole: BUILTIN_ROLE_IDS.ADMIN,
failRole: BUILTIN_ROLE_IDS.PUBLIC,
})
})
})
describe("update", () => {
it("should be able to update the user", async () => {
const user = await config.createUser({ id: `us_update${utils.newid()}` })
user.roleId = BUILTIN_ROLE_IDS.BASIC
delete user._rev
const res = await request
.put(`/api/users/metadata`)
.set(config.defaultHeaders())
.send(user)
.expect(200)
.expect("Content-Type", /json/)
expect(res.body.ok).toEqual(true)
})
it("should be able to update the user multiple times", async () => {
const user = await config.createUser()
delete user._rev
const res1 = await request
.put(`/api/users/metadata`)
.set(config.defaultHeaders())
.send({ ...user, roleId: BUILTIN_ROLE_IDS.BASIC })
.expect(200)
.expect("Content-Type", /json/)
const res = await request
.put(`/api/users/metadata`)
.set(config.defaultHeaders())
.send({ ...user, _rev: res1.body.rev, roleId: BUILTIN_ROLE_IDS.POWER })
.expect(200)
.expect("Content-Type", /json/)
expect(res.body.ok).toEqual(true)
})
it("should require the _rev field for multiple updates", async () => {
const user = await config.createUser()
delete user._rev
await request
.put(`/api/users/metadata`)
.set(config.defaultHeaders())
.send({ ...user, roleId: BUILTIN_ROLE_IDS.BASIC })
.expect(200)
.expect("Content-Type", /json/)
await request
.put(`/api/users/metadata`)
.set(config.defaultHeaders())
.send({ ...user, roleId: BUILTIN_ROLE_IDS.POWER })
.expect(409)
.expect("Content-Type", /json/)
})
})
describe("destroy", () => {
it("should be able to delete the user", async () => {
const user = await config.createUser()
const res = await request
.delete(`/api/users/metadata/${user._id}`)
.set(config.defaultHeaders())
.expect(200)
.expect("Content-Type", /json/)
expect(res.body.message).toBeDefined()
})
})
describe("find", () => {
it("should be able to find the user", async () => {
const user = await config.createUser()
const res = await request
.get(`/api/users/metadata/${user._id}`)
.set(config.defaultHeaders())
.expect(200)
.expect("Content-Type", /json/)
expect(res.body._id).toEqual(user._id)
expect(res.body.roleId).toEqual(BUILTIN_ROLE_IDS.ADMIN)
expect(res.body.tableId).toBeDefined()
})
})
describe("setFlag", () => {
it("should throw an error if a flag is not provided", async () => {
await config.createUser()
const res = await request
.post(`/api/users/flags`)
.set(config.defaultHeaders())
.send({ value: "test" })
.expect(400)
.expect("Content-Type", /json/)
expect(res.body.message).toEqual(
"Must supply a 'flag' field in request body."
)
})
it("should be able to set a flag on the user", async () => {
await config.createUser()
const res = await request
.post(`/api/users/flags`)
.set(config.defaultHeaders())
.send({ value: "test", flag: "test" })
.expect(200)
.expect("Content-Type", /json/)
expect(res.body.message).toEqual("Flag set successfully")
})
})
describe("getFlags", () => {
it("should get flags for a specific user", async () => {
let flagData = { value: "test", flag: "test" }
await config.createUser()
await request
.post(`/api/users/flags`)
.set(config.defaultHeaders())
.send(flagData)
.expect(200)
.expect("Content-Type", /json/)
const res = await request
.get(`/api/users/flags`)
.set(config.defaultHeaders())
.expect(200)
.expect("Content-Type", /json/)
expect(res.body[flagData.value]).toEqual(flagData.flag)
})
})
describe("setFlag", () => {
it("should throw an error if a flag is not provided", async () => {
await config.createUser()
const res = await request
.post(`/api/users/flags`)
.set(config.defaultHeaders())
.send({ value: "test" })
.expect(400)
.expect("Content-Type", /json/)
expect(res.body.message).toEqual(
"Must supply a 'flag' field in request body."
)
})
it("should be able to set a flag on the user", async () => {
await config.createUser()
const res = await request
.post(`/api/users/flags`)
.set(config.defaultHeaders())
.send({ value: "test", flag: "test" })
.expect(200)
.expect("Content-Type", /json/)
expect(res.body.message).toEqual("Flag set successfully")
})
})
})

View File

@ -0,0 +1,144 @@
import { roles, utils } from "@budibase/backend-core"
import { checkPermissionsEndpoint } from "./utilities/TestFunctions"
import * as setup from "./utilities"
import { UserMetadata } from "@budibase/types"
jest.setTimeout(30000)
jest.mock("../../../utilities/workerRequests", () => ({
getGlobalUsers: jest.fn(() => {
return {}
}),
getGlobalSelf: jest.fn(() => {
return {}
}),
deleteGlobalUser: jest.fn(),
}))
describe("/users", () => {
let request = setup.getRequest()
let config = setup.getConfig()
afterAll(setup.afterAll)
beforeAll(async () => {
await config.init()
})
describe("fetch", () => {
it("returns a list of users from an instance db", async () => {
await config.createUser({ id: "uuidx" })
await config.createUser({ id: "uuidy" })
const res = await config.api.user.fetch()
expect(res.length).toBe(3)
const ids = res.map(u => u._id)
expect(ids).toContain(`ro_ta_users_us_uuidx`)
expect(ids).toContain(`ro_ta_users_us_uuidy`)
})
it("should apply authorization to endpoint", async () => {
await config.createUser()
await checkPermissionsEndpoint({
config,
request,
method: "GET",
url: `/api/users/metadata`,
passRole: roles.BUILTIN_ROLE_IDS.ADMIN,
failRole: roles.BUILTIN_ROLE_IDS.PUBLIC,
})
})
})
describe("update", () => {
it("should be able to update the user", async () => {
const user: UserMetadata = await config.createUser({
id: `us_update${utils.newid()}`,
})
user.roleId = roles.BUILTIN_ROLE_IDS.BASIC
delete user._rev
const res = await config.api.user.update(user)
expect(res.ok).toEqual(true)
})
it("should be able to update the user multiple times", async () => {
const user = await config.createUser()
delete user._rev
const res1 = await config.api.user.update({
...user,
roleId: roles.BUILTIN_ROLE_IDS.BASIC,
})
const res2 = await config.api.user.update({
...user,
_rev: res1.rev,
roleId: roles.BUILTIN_ROLE_IDS.POWER,
})
expect(res2.ok).toEqual(true)
})
it("should require the _rev field for multiple updates", async () => {
const user = await config.createUser()
delete user._rev
await config.api.user.update({
...user,
roleId: roles.BUILTIN_ROLE_IDS.BASIC,
})
await config.api.user.update(
{ ...user, roleId: roles.BUILTIN_ROLE_IDS.POWER },
{ expectStatus: 409 }
)
})
})
describe("destroy", () => {
it("should be able to delete the user", async () => {
const user = await config.createUser()
const res = await config.api.user.destroy(user._id!)
expect(res.message).toBeDefined()
})
})
describe("find", () => {
it("should be able to find the user", async () => {
const user = await config.createUser()
const res = await config.api.user.find(user._id!)
expect(res._id).toEqual(user._id)
expect(res.roleId).toEqual(roles.BUILTIN_ROLE_IDS.ADMIN)
expect(res.tableId).toBeDefined()
})
})
describe("setFlag", () => {
it("should throw an error if a flag is not provided", async () => {
await config.createUser()
const res = await request
.post(`/api/users/flags`)
.set(config.defaultHeaders())
.send({ value: "test" })
.expect(400)
.expect("Content-Type", /json/)
expect(res.body.message).toEqual(
"Must supply a 'flag' field in request body."
)
})
it("should be able to set a flag on the user", async () => {
await config.createUser()
const res = await config.api.user.setFlag("test", true)
expect(res.message).toEqual("Flag set successfully")
})
})
describe("getFlags", () => {
it("should get flags for a specific user", async () => {
await config.createUser()
await config.api.user.setFlag("test", "test")
const res = await config.api.user.getFlags()
expect(res.test).toEqual("test")
})
})
})

View File

@ -264,7 +264,7 @@ class TestConfiguration {
admin = false, admin = false,
email = this.defaultUserValues.email, email = this.defaultUserValues.email,
roles, roles,
}: any = {}) { }: any = {}): Promise<User> {
const db = tenancy.getTenantDB(this.getTenantId()) const db = tenancy.getTenantDB(this.getTenantId())
let existing let existing
try { try {

View File

@ -9,6 +9,7 @@ import { ScreenAPI } from "./screen"
import { ApplicationAPI } from "./application" import { ApplicationAPI } from "./application"
import { BackupAPI } from "./backup" import { BackupAPI } from "./backup"
import { AttachmentAPI } from "./attachment" import { AttachmentAPI } from "./attachment"
import { UserAPI } from "./user"
export default class API { export default class API {
table: TableAPI table: TableAPI
@ -21,6 +22,7 @@ export default class API {
application: ApplicationAPI application: ApplicationAPI
backup: BackupAPI backup: BackupAPI
attachment: AttachmentAPI attachment: AttachmentAPI
user: UserAPI
constructor(config: TestConfiguration) { constructor(config: TestConfiguration) {
this.table = new TableAPI(config) this.table = new TableAPI(config)
@ -33,5 +35,6 @@ export default class API {
this.application = new ApplicationAPI(config) this.application = new ApplicationAPI(config)
this.backup = new BackupAPI(config) this.backup = new BackupAPI(config)
this.attachment = new AttachmentAPI(config) this.attachment = new AttachmentAPI(config)
this.user = new UserAPI(config)
} }
} }

View File

@ -0,0 +1,157 @@
import {
FetchUserMetadataResponse,
FindUserMetadataResponse,
Flags,
UserMetadata,
} from "@budibase/types"
import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base"
import { DocumentInsertResponse } from "@budibase/nano"
export class UserAPI extends TestAPI {
constructor(config: TestConfiguration) {
super(config)
}
fetch = async (
{ expectStatus } = { expectStatus: 200 }
): Promise<FetchUserMetadataResponse> => {
const res = await this.request
.get(`/api/users/metadata`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
if (res.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
res.status
} with body ${JSON.stringify(res.body)}`
)
}
return res.body
}
find = async (
id: string,
{ expectStatus } = { expectStatus: 200 }
): Promise<FindUserMetadataResponse> => {
const res = await this.request
.get(`/api/users/metadata/${id}`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
if (res.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
res.status
} with body ${JSON.stringify(res.body)}`
)
}
return res.body
}
update = async (
user: UserMetadata,
{ expectStatus } = { expectStatus: 200 }
): Promise<DocumentInsertResponse> => {
const res = await this.request
.put(`/api/users/metadata`)
.set(this.config.defaultHeaders())
.send(user)
.expect("Content-Type", /json/)
if (res.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
res.status
} with body ${JSON.stringify(res.body)}`
)
}
return res.body as DocumentInsertResponse
}
updateSelf = async (
user: UserMetadata,
{ expectStatus } = { expectStatus: 200 }
): Promise<DocumentInsertResponse> => {
const res = await this.request
.post(`/api/users/metadata/self`)
.set(this.config.defaultHeaders())
.send(user)
.expect("Content-Type", /json/)
if (res.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
res.status
} with body ${JSON.stringify(res.body)}`
)
}
return res.body as DocumentInsertResponse
}
destroy = async (
id: string,
{ expectStatus } = { expectStatus: 200 }
): Promise<{ message: string }> => {
const res = await this.request
.delete(`/api/users/metadata/${id}`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
if (res.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
res.status
} with body ${JSON.stringify(res.body)}`
)
}
return res.body as { message: string }
}
setFlag = async (
flag: string,
value: any,
{ expectStatus } = { expectStatus: 200 }
): Promise<{ message: string }> => {
const res = await this.request
.post(`/api/users/flags`)
.set(this.config.defaultHeaders())
.send({ flag, value })
.expect("Content-Type", /json/)
if (res.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
res.status
} with body ${JSON.stringify(res.body)}`
)
}
return res.body as { message: string }
}
getFlags = async (
{ expectStatus } = { expectStatus: 200 }
): Promise<Flags> => {
const res = await this.request
.get(`/api/users/flags`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
if (res.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
res.status
} with body ${JSON.stringify(res.body)}`
)
}
return res.body as Flags
}
}

View File

@ -1,11 +1,13 @@
import { InternalTables } from "../db/utils" import { InternalTables } from "../db/utils"
import { getGlobalUser } from "./global" import { getGlobalUser } from "./global"
import { context, roles } from "@budibase/backend-core" import { context, roles } from "@budibase/backend-core"
import { UserCtx } from "@budibase/types" import { ContextUserMetadata, UserCtx, UserMetadata } from "@budibase/types"
export async function getFullUser(ctx: UserCtx, userId: string) { export async function getFullUser(
userId: string
): Promise<ContextUserMetadata> {
const global = await getGlobalUser(userId) const global = await getGlobalUser(userId)
let metadata: any = {} let metadata: UserMetadata | undefined = undefined
// always prefer the user metadata _id and _rev // always prefer the user metadata _id and _rev
delete global._id delete global._id
@ -14,11 +16,11 @@ export async function getFullUser(ctx: UserCtx, userId: string) {
try { try {
// this will throw an error if the db doesn't exist, or there is no appId // this will throw an error if the db doesn't exist, or there is no appId
const db = context.getAppDB() const db = context.getAppDB()
metadata = await db.get(userId) metadata = await db.get<UserMetadata>(userId)
delete metadata.csrfToken
} catch (err) { } catch (err) {
// it is fine if there is no user metadata yet // it is fine if there is no user metadata yet
} }
delete metadata.csrfToken
return { return {
...metadata, ...metadata,
...global, ...global,

View File

@ -6,3 +6,4 @@ export * from "./rows"
export * from "./table" export * from "./table"
export * from "./permission" export * from "./permission"
export * from "./attachment" export * from "./attachment"
export * from "./user"

View File

@ -0,0 +1,9 @@
import { ContextUserMetadata } from "../../../"
export type FetchUserMetadataResponse = ContextUserMetadata[]
export type FindUserMetadataResponse = ContextUserMetadata
export interface SetFlagRequest {
flag: string
value: any
}

View File

@ -0,0 +1,5 @@
import { Document } from "../../"
export interface Flags extends Document {
[key: string]: any
}

View File

@ -1,2 +1,3 @@
export * from "./account" export * from "./account"
export * from "./user" export * from "./user"
export * from "./flag"