@budibase/server
Version:
Budibase Web Server
179 lines (160 loc) • 5.41 kB
text/typescript
import { authorizedMiddleware as authorized } from "../middleware/authorized"
import { currentAppMiddleware as currentApp } from "../middleware/currentapp"
import { BaseSocket } from "./websocket"
import { auth, permissions, context } from "@budibase/backend-core"
import http from "http"
import Koa from "koa"
import { getSourceId } from "../api/controllers/row/utils"
import { Row, Table, View, ViewV2 } from "@budibase/types"
import { Socket } from "socket.io"
import { GridSocketEvent } from "@budibase/shared-core"
import { userAgent } from "koa-useragent"
import { createContext, runMiddlewares } from "./middleware"
import sdk from "../sdk"
import {
findHBSBlocks,
isJSBinding,
decodeJSBinding,
} from "@budibase/string-templates"
const { PermissionType, PermissionLevel } = permissions
export default class GridSocket extends BaseSocket {
constructor(app: Koa, server: http.Server) {
super(app, server, "/socket/grid")
}
// Checks if a view's query contains any current user bindings
containsCurrentUserBinding(view: ViewV2): boolean {
return findHBSBlocks(JSON.stringify(view.query))
.map(binding => {
const sanitizedBinding = binding.replace(/\\"/g, '"')
if (isJSBinding(sanitizedBinding)) {
return decodeJSBinding(sanitizedBinding)
} else {
return sanitizedBinding
}
})
.some(binding => binding?.includes("[user]"))
}
async onConnect(socket: Socket) {
// Initial identification of connected spreadsheet
socket.on(GridSocketEvent.SelectDatasource, async (payload, callback) => {
const ds = payload.datasource
const appId = payload.appId
const resourceId = ds?.type === "table" ? ds?.tableId : ds?.id
let valid = true
// Validate datasource
if (!resourceId || !appId) {
// Ignore if no table or app specified
valid = false
} else if (ds.type === "viewV2") {
// If this is a view filtered by current user, don't sync changes
try {
await context.doInAppContext(appId, async () => {
const view = await sdk.views.get(ds.id)
if (this.containsCurrentUserBinding(view)) {
valid = false
}
})
} catch (err) {
valid = false
}
}
if (!valid) {
socket.disconnect(true)
return
}
// Create context
const ctx = createContext(this.app, socket, {
resourceId,
appId,
})
// Construct full middleware chain to assess permissions
const middlewares = [
userAgent,
auth.buildAuthMiddleware([], {
publicAllowed: true,
}),
currentApp,
authorized(PermissionType.TABLE, PermissionLevel.READ),
]
// Run all koa middlewares
try {
await runMiddlewares(ctx, middlewares, async () => {
// Middlewares are finished and we have permission
// Join room for this resource
const room = `${appId}-${resourceId}`
await this.joinRoom(socket, room)
// Reply with all users in current room
const sessions = await this.getRoomSessions(room)
callback({ users: sessions })
})
} catch (error) {
socket.disconnect(true)
}
})
// Handle users selecting a new cell
socket.on(GridSocketEvent.SelectCell, ({ cellId }) => {
this.updateUser(socket, { focusedCellId: cellId })
})
}
async updateUser(socket: Socket, patch: object) {
await super.updateUser(socket, {
gridMetadata: {
...socket.data.gridMetadata,
...patch,
},
})
}
emitRowUpdate(ctx: any, row: Row) {
const source = getSourceId(ctx)
const resourceId = source.viewId ?? source.tableId
const room = `${ctx.appId}-${resourceId}`
this.emitToRoom(ctx, room, GridSocketEvent.RowChange, {
id: row._id,
row,
})
}
emitRowDeletion(ctx: any, row: Row) {
const source = getSourceId(ctx)
const resourceId = source.viewId ?? source.tableId
const room = `${ctx.appId}-${resourceId}`
this.emitToRoom(ctx, room, GridSocketEvent.RowChange, {
id: row._id,
row: null,
})
}
emitTableUpdate(ctx: any, table: Table) {
const room = `${ctx.appId}-${table._id}`
this.emitToRoom(ctx, room, GridSocketEvent.DatasourceChange, {
id: table._id,
datasource: table,
})
}
emitTableDeletion(ctx: any, table: Table) {
const room = `${ctx.appId}-${table._id}`
this.emitToRoom(ctx, room, GridSocketEvent.DatasourceChange, {
id: table._id,
datasource: null,
})
// When the table is deleted we need to notify all views that they have
// also been deleted
Object.values(table.views || {})
.filter((view: View | ViewV2) => (view as ViewV2).version === 2)
.forEach((view: View | ViewV2) => {
this.emitViewDeletion(ctx, view as ViewV2)
})
}
emitViewUpdate(ctx: any, view: ViewV2) {
const room = `${ctx.appId}-${view.id}`
this.emitToRoom(ctx, room, GridSocketEvent.DatasourceChange, {
id: view.id,
datasource: view,
})
}
emitViewDeletion(ctx: any, view: ViewV2) {
const room = `${ctx.appId}-${view.id}`
this.emitToRoom(ctx, room, GridSocketEvent.DatasourceChange, {
id: view.id,
datasource: null,
})
}
}