chore: format backend server code
This commit is contained in:
parent
87d311a5d1
commit
7ecbd02fef
@ -1,58 +1,61 @@
|
|||||||
import { Socket } from "socket.io"
|
import { Socket } from "socket.io"
|
||||||
|
|
||||||
class Counter {
|
class Counter {
|
||||||
private count: number = 0
|
private count: number = 0
|
||||||
|
|
||||||
increment() {
|
increment() {
|
||||||
this.count++
|
this.count++
|
||||||
}
|
}
|
||||||
|
|
||||||
decrement() {
|
decrement() {
|
||||||
this.count = Math.max(0, this.count - 1)
|
this.count = Math.max(0, this.count - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
getValue(): number {
|
getValue(): number {
|
||||||
return this.count
|
return this.count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Owner Connection Management
|
// Owner Connection Management
|
||||||
export class ConnectionManager {
|
export class ConnectionManager {
|
||||||
// Counts how many times the owner is connected to a sandbox
|
// Counts how many times the owner is connected to a sandbox
|
||||||
private ownerConnections: Record<string, Counter> = {}
|
private ownerConnections: Record<string, Counter> = {}
|
||||||
// Stores all sockets connected to a given sandbox
|
// Stores all sockets connected to a given sandbox
|
||||||
private sockets: Record<string, Set<Socket>> = {}
|
private sockets: Record<string, Set<Socket>> = {}
|
||||||
|
|
||||||
// Checks if the owner of a sandbox is connected
|
// Checks if the owner of a sandbox is connected
|
||||||
ownerIsConnected(sandboxId: string): boolean {
|
ownerIsConnected(sandboxId: string): boolean {
|
||||||
return this.ownerConnections[sandboxId]?.getValue() > 0
|
return this.ownerConnections[sandboxId]?.getValue() > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a connection for a sandbox
|
||||||
|
addConnectionForSandbox(socket: Socket, sandboxId: string, isOwner: boolean) {
|
||||||
|
this.sockets[sandboxId] ??= new Set()
|
||||||
|
this.sockets[sandboxId].add(socket)
|
||||||
|
|
||||||
|
// If the connection is for the owner, increments the owner connection counter
|
||||||
|
if (isOwner) {
|
||||||
|
this.ownerConnections[sandboxId] ??= new Counter()
|
||||||
|
this.ownerConnections[sandboxId].increment()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Adds a connection for a sandbox
|
// Removes a connection for a sandbox
|
||||||
addConnectionForSandbox(socket: Socket, sandboxId: string, isOwner: boolean) {
|
removeConnectionForSandbox(
|
||||||
this.sockets[sandboxId] ??= new Set()
|
socket: Socket,
|
||||||
this.sockets[sandboxId].add(socket)
|
sandboxId: string,
|
||||||
|
isOwner: boolean
|
||||||
|
) {
|
||||||
|
this.sockets[sandboxId]?.delete(socket)
|
||||||
|
|
||||||
// If the connection is for the owner, increments the owner connection counter
|
// If the connection being removed is for the owner, decrements the owner connection counter
|
||||||
if (isOwner) {
|
if (isOwner) {
|
||||||
this.ownerConnections[sandboxId] ??= new Counter()
|
this.ownerConnections[sandboxId]?.decrement()
|
||||||
this.ownerConnections[sandboxId].increment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removes a connection for a sandbox
|
|
||||||
removeConnectionForSandbox(socket: Socket, sandboxId: string, isOwner: boolean) {
|
|
||||||
this.sockets[sandboxId]?.delete(socket)
|
|
||||||
|
|
||||||
// If the connection being removed is for the owner, decrements the owner connection counter
|
|
||||||
if (isOwner) {
|
|
||||||
this.ownerConnections[sandboxId]?.decrement()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the set of sockets connected to a given sandbox
|
|
||||||
connectionsForSandbox(sandboxId: string): Set<Socket> {
|
|
||||||
return this.sockets[sandboxId] ?? new Set();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the set of sockets connected to a given sandbox
|
||||||
|
connectionsForSandbox(sandboxId: string): Set<Socket> {
|
||||||
|
return this.sockets[sandboxId] ?? new Set()
|
||||||
|
}
|
||||||
}
|
}
|
@ -23,7 +23,11 @@ function generateFileStructure(paths: string[]): (TFolder | TFile)[] {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isFile) {
|
if (isFile) {
|
||||||
const file: TFile = { id: `/${parts.join("/")}`, type: "file", name: part }
|
const file: TFile = {
|
||||||
|
id: `/${parts.join("/")}`,
|
||||||
|
type: "file",
|
||||||
|
name: part,
|
||||||
|
}
|
||||||
current.children.push(file)
|
current.children.push(file)
|
||||||
} else {
|
} else {
|
||||||
const folder: TFolder = {
|
const folder: TFolder = {
|
||||||
@ -75,7 +79,9 @@ export class FileManager {
|
|||||||
|
|
||||||
if (isFile) {
|
if (isFile) {
|
||||||
const fileId = `/${parts.join("/")}`
|
const fileId = `/${parts.join("/")}`
|
||||||
const data = await RemoteFileStorage.fetchFileContent(`projects/${this.sandboxId}${fileId}`)
|
const data = await RemoteFileStorage.fetchFileContent(
|
||||||
|
`projects/${this.sandboxId}${fileId}`
|
||||||
|
)
|
||||||
fileData.push({ id: fileId, data })
|
fileData.push({ id: fileId, data })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -91,7 +97,7 @@ export class FileManager {
|
|||||||
// Convert remote file path to local file path
|
// Convert remote file path to local file path
|
||||||
private getLocalFileId(remoteId: string): string | undefined {
|
private getLocalFileId(remoteId: string): string | undefined {
|
||||||
const allParts = remoteId.split("/")
|
const allParts = remoteId.split("/")
|
||||||
if (allParts[1] !== this.sandboxId) return undefined;
|
if (allParts[1] !== this.sandboxId) return undefined
|
||||||
return allParts.slice(2).join("/")
|
return allParts.slice(2).join("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +105,7 @@ export class FileManager {
|
|||||||
private getLocalFileIds(remoteIds: string[]): string[] {
|
private getLocalFileIds(remoteIds: string[]): string[] {
|
||||||
return remoteIds
|
return remoteIds
|
||||||
.map(this.getLocalFileId.bind(this))
|
.map(this.getLocalFileId.bind(this))
|
||||||
.filter((id) => id !== undefined);
|
.filter((id) => id !== undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download files from remote storage
|
// Download files from remote storage
|
||||||
@ -120,7 +126,6 @@ export class FileManager {
|
|||||||
|
|
||||||
// Initialize the FileManager
|
// Initialize the FileManager
|
||||||
async initialize() {
|
async initialize() {
|
||||||
|
|
||||||
// Download files from remote file storage
|
// Download files from remote file storage
|
||||||
await this.updateFileStructure()
|
await this.updateFileStructure()
|
||||||
await this.updateFileData()
|
await this.updateFileData()
|
||||||
@ -141,10 +146,14 @@ export class FileManager {
|
|||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
|
|
||||||
// Reload file list from the container to include template files
|
// Reload file list from the container to include template files
|
||||||
const result = await this.sandbox.commands.run(`find "${this.dirName}" -type f`); // List all files recursively
|
const result = await this.sandbox.commands.run(
|
||||||
const localPaths = result.stdout.split('\n').filter(path => path); // Split the output into an array and filter out empty strings
|
`find "${this.dirName}" -type f`
|
||||||
const relativePaths = localPaths.map(filePath => path.posix.relative(this.dirName, filePath)); // Convert absolute paths to relative paths
|
) // List all files recursively
|
||||||
this.files = generateFileStructure(relativePaths);
|
const localPaths = result.stdout.split("\n").filter((path) => path) // Split the output into an array and filter out empty strings
|
||||||
|
const relativePaths = localPaths.map((filePath) =>
|
||||||
|
path.posix.relative(this.dirName, filePath)
|
||||||
|
) // Convert absolute paths to relative paths
|
||||||
|
this.files = generateFileStructure(relativePaths)
|
||||||
|
|
||||||
// Make the logged in user the owner of all project files
|
// Make the logged in user the owner of all project files
|
||||||
this.fixPermissions()
|
this.fixPermissions()
|
||||||
@ -169,9 +178,7 @@ export class FileManager {
|
|||||||
// Change the owner of the project directory to user
|
// Change the owner of the project directory to user
|
||||||
private async fixPermissions() {
|
private async fixPermissions() {
|
||||||
try {
|
try {
|
||||||
await this.sandbox.commands.run(
|
await this.sandbox.commands.run(`sudo chown -R user "${this.dirName}"`)
|
||||||
`sudo chown -R user "${this.dirName}"`
|
|
||||||
)
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log("Failed to fix permissions: " + e)
|
console.log("Failed to fix permissions: " + e)
|
||||||
}
|
}
|
||||||
@ -193,7 +200,10 @@ export class FileManager {
|
|||||||
// This is the absolute file path in the container
|
// This is the absolute file path in the container
|
||||||
const containerFilePath = path.posix.join(directory, event.name)
|
const containerFilePath = path.posix.join(directory, event.name)
|
||||||
// This is the file path relative to the project directory
|
// This is the file path relative to the project directory
|
||||||
const sandboxFilePath = removeDirName(containerFilePath, this.dirName)
|
const sandboxFilePath = removeDirName(
|
||||||
|
containerFilePath,
|
||||||
|
this.dirName
|
||||||
|
)
|
||||||
// This is the directory being watched relative to the project directory
|
// This is the directory being watched relative to the project directory
|
||||||
const sandboxDirectory = removeDirName(directory, this.dirName)
|
const sandboxDirectory = removeDirName(directory, this.dirName)
|
||||||
|
|
||||||
@ -218,16 +228,16 @@ export class FileManager {
|
|||||||
|
|
||||||
const newItem = isDir
|
const newItem = isDir
|
||||||
? ({
|
? ({
|
||||||
id: sandboxFilePath,
|
id: sandboxFilePath,
|
||||||
name: event.name,
|
name: event.name,
|
||||||
type: "folder",
|
type: "folder",
|
||||||
children: [],
|
children: [],
|
||||||
} as TFolder)
|
} as TFolder)
|
||||||
: ({
|
: ({
|
||||||
id: sandboxFilePath,
|
id: sandboxFilePath,
|
||||||
name: event.name,
|
name: event.name,
|
||||||
type: "file",
|
type: "file",
|
||||||
} as TFile)
|
} as TFile)
|
||||||
|
|
||||||
if (folder) {
|
if (folder) {
|
||||||
// If the folder exists, add the new item (file/folder) as a child
|
// If the folder exists, add the new item (file/folder) as a child
|
||||||
@ -361,7 +371,9 @@ export class FileManager {
|
|||||||
|
|
||||||
// Get folder content
|
// Get folder content
|
||||||
async getFolder(folderId: string): Promise<string[]> {
|
async getFolder(folderId: string): Promise<string[]> {
|
||||||
const remotePaths = await RemoteFileStorage.getFolder(this.getRemoteFileId(folderId))
|
const remotePaths = await RemoteFileStorage.getFolder(
|
||||||
|
this.getRemoteFileId(folderId)
|
||||||
|
)
|
||||||
return this.getLocalFileIds(remotePaths)
|
return this.getLocalFileIds(remotePaths)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -400,7 +412,11 @@ export class FileManager {
|
|||||||
fileData.id = newFileId
|
fileData.id = newFileId
|
||||||
file.id = newFileId
|
file.id = newFileId
|
||||||
|
|
||||||
await RemoteFileStorage.renameFile(this.getRemoteFileId(fileId), this.getRemoteFileId(newFileId), fileData.data)
|
await RemoteFileStorage.renameFile(
|
||||||
|
this.getRemoteFileId(fileId),
|
||||||
|
this.getRemoteFileId(newFileId),
|
||||||
|
fileData.data
|
||||||
|
)
|
||||||
return this.updateFileStructure()
|
return this.updateFileStructure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -465,7 +481,11 @@ export class FileManager {
|
|||||||
|
|
||||||
await this.moveFileInContainer(fileId, newFileId)
|
await this.moveFileInContainer(fileId, newFileId)
|
||||||
await this.fixPermissions()
|
await this.fixPermissions()
|
||||||
await RemoteFileStorage.renameFile(this.getRemoteFileId(fileId), this.getRemoteFileId(newFileId), fileData.data)
|
await RemoteFileStorage.renameFile(
|
||||||
|
this.getRemoteFileId(fileId),
|
||||||
|
this.getRemoteFileId(newFileId),
|
||||||
|
fileData.data
|
||||||
|
)
|
||||||
|
|
||||||
fileData.id = newFileId
|
fileData.id = newFileId
|
||||||
file.id = newFileId
|
file.id = newFileId
|
||||||
@ -477,9 +497,7 @@ export class FileManager {
|
|||||||
if (!file) return this.files
|
if (!file) return this.files
|
||||||
|
|
||||||
await this.sandbox.files.remove(path.posix.join(this.dirName, fileId))
|
await this.sandbox.files.remove(path.posix.join(this.dirName, fileId))
|
||||||
this.fileData = this.fileData.filter(
|
this.fileData = this.fileData.filter((f) => f.id !== fileId)
|
||||||
(f) => f.id !== fileId
|
|
||||||
)
|
|
||||||
|
|
||||||
await RemoteFileStorage.deleteFile(this.getRemoteFileId(fileId))
|
await RemoteFileStorage.deleteFile(this.getRemoteFileId(fileId))
|
||||||
return this.updateFileStructure()
|
return this.updateFileStructure()
|
||||||
@ -487,14 +505,14 @@ export class FileManager {
|
|||||||
|
|
||||||
// Delete a folder
|
// Delete a folder
|
||||||
async deleteFolder(folderId: string): Promise<(TFolder | TFile)[]> {
|
async deleteFolder(folderId: string): Promise<(TFolder | TFile)[]> {
|
||||||
const files = await RemoteFileStorage.getFolder(this.getRemoteFileId(folderId))
|
const files = await RemoteFileStorage.getFolder(
|
||||||
|
this.getRemoteFileId(folderId)
|
||||||
|
)
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
files.map(async (file) => {
|
files.map(async (file) => {
|
||||||
await this.sandbox.files.remove(path.posix.join(this.dirName, file))
|
await this.sandbox.files.remove(path.posix.join(this.dirName, file))
|
||||||
this.fileData = this.fileData.filter(
|
this.fileData = this.fileData.filter((f) => f.id !== file)
|
||||||
(f) => f.id !== file
|
|
||||||
)
|
|
||||||
await RemoteFileStorage.deleteFile(this.getRemoteFileId(file))
|
await RemoteFileStorage.deleteFile(this.getRemoteFileId(file))
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -61,11 +61,7 @@ export const RemoteFileStorage = {
|
|||||||
return res.ok
|
return res.ok
|
||||||
},
|
},
|
||||||
|
|
||||||
renameFile: async (
|
renameFile: async (fileId: string, newFileId: string, data: string) => {
|
||||||
fileId: string,
|
|
||||||
newFileId: string,
|
|
||||||
data: string
|
|
||||||
) => {
|
|
||||||
const res = await fetch(`${process.env.STORAGE_WORKER_URL}/api/rename`, {
|
const res = await fetch(`${process.env.STORAGE_WORKER_URL}/api/rename`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@ -111,7 +107,7 @@ export const RemoteFileStorage = {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
return (await res.json()).size
|
return (await res.json()).size
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RemoteFileStorage
|
export default RemoteFileStorage
|
@ -5,11 +5,11 @@ import { CONTAINER_TIMEOUT } from "./constants"
|
|||||||
import { DokkuClient } from "./DokkuClient"
|
import { DokkuClient } from "./DokkuClient"
|
||||||
import { FileManager } from "./FileManager"
|
import { FileManager } from "./FileManager"
|
||||||
import {
|
import {
|
||||||
createFileRL,
|
createFileRL,
|
||||||
createFolderRL,
|
createFolderRL,
|
||||||
deleteFileRL,
|
deleteFileRL,
|
||||||
renameFileRL,
|
renameFileRL,
|
||||||
saveFileRL,
|
saveFileRL,
|
||||||
} from "./ratelimit"
|
} from "./ratelimit"
|
||||||
import { SecureGitClient } from "./SecureGitClient"
|
import { SecureGitClient } from "./SecureGitClient"
|
||||||
import { TerminalManager } from "./TerminalManager"
|
import { TerminalManager } from "./TerminalManager"
|
||||||
@ -18,245 +18,267 @@ import { LockManager } from "./utils"
|
|||||||
const lockManager = new LockManager()
|
const lockManager = new LockManager()
|
||||||
|
|
||||||
// Define a type for SocketHandler functions
|
// Define a type for SocketHandler functions
|
||||||
type SocketHandler<T = Record<string, any>> = (args: T) => any;
|
type SocketHandler<T = Record<string, any>> = (args: T) => any
|
||||||
|
|
||||||
// Extract port number from a string
|
// Extract port number from a string
|
||||||
function extractPortNumber(inputString: string): number | null {
|
function extractPortNumber(inputString: string): number | null {
|
||||||
const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, "")
|
const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, "")
|
||||||
const regex = /http:\/\/localhost:(\d+)/
|
const regex = /http:\/\/localhost:(\d+)/
|
||||||
const match = cleanedString.match(regex)
|
const match = cleanedString.match(regex)
|
||||||
return match ? parseInt(match[1]) : null
|
return match ? parseInt(match[1]) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerContext = {
|
type ServerContext = {
|
||||||
aiWorker: AIWorker;
|
aiWorker: AIWorker
|
||||||
dokkuClient: DokkuClient | null;
|
dokkuClient: DokkuClient | null
|
||||||
gitClient: SecureGitClient | null;
|
gitClient: SecureGitClient | null
|
||||||
};
|
}
|
||||||
|
|
||||||
export class Sandbox {
|
export class Sandbox {
|
||||||
|
// Sandbox properties:
|
||||||
|
sandboxId: string
|
||||||
|
type: string
|
||||||
|
fileManager: FileManager | null
|
||||||
|
terminalManager: TerminalManager | null
|
||||||
|
container: E2BSandbox | null
|
||||||
|
// Server context:
|
||||||
|
dokkuClient: DokkuClient | null
|
||||||
|
gitClient: SecureGitClient | null
|
||||||
|
aiWorker: AIWorker
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
sandboxId: string,
|
||||||
|
type: string,
|
||||||
|
{ aiWorker, dokkuClient, gitClient }: ServerContext
|
||||||
|
) {
|
||||||
// Sandbox properties:
|
// Sandbox properties:
|
||||||
sandboxId: string;
|
this.sandboxId = sandboxId
|
||||||
type: string;
|
this.type = type
|
||||||
fileManager: FileManager | null;
|
this.fileManager = null
|
||||||
terminalManager: TerminalManager | null;
|
this.terminalManager = null
|
||||||
container: E2BSandbox | null;
|
this.container = null
|
||||||
// Server context:
|
// Server context:
|
||||||
dokkuClient: DokkuClient | null;
|
this.aiWorker = aiWorker
|
||||||
gitClient: SecureGitClient | null;
|
this.dokkuClient = dokkuClient
|
||||||
aiWorker: AIWorker;
|
this.gitClient = gitClient
|
||||||
|
}
|
||||||
|
|
||||||
constructor(sandboxId: string, type: string, { aiWorker, dokkuClient, gitClient }: ServerContext) {
|
// Initializes the container for the sandbox environment
|
||||||
// Sandbox properties:
|
async initialize(
|
||||||
this.sandboxId = sandboxId;
|
fileWatchCallback: ((files: (TFolder | TFile)[]) => void) | undefined
|
||||||
this.type = type;
|
) {
|
||||||
this.fileManager = null;
|
// Acquire a lock to ensure exclusive access to the sandbox environment
|
||||||
this.terminalManager = null;
|
await lockManager.acquireLock(this.sandboxId, async () => {
|
||||||
this.container = null;
|
// Check if a container already exists and is running
|
||||||
// Server context:
|
if (this.container && (await this.container.isRunning())) {
|
||||||
this.aiWorker = aiWorker;
|
console.log(`Found existing container ${this.sandboxId}`)
|
||||||
this.dokkuClient = dokkuClient;
|
} else {
|
||||||
this.gitClient = gitClient;
|
console.log("Creating container", this.sandboxId)
|
||||||
}
|
// Create a new container with a specified template and timeout
|
||||||
|
const templateTypes = ["vanillajs", "reactjs", "nextjs", "streamlit"]
|
||||||
// Initializes the container for the sandbox environment
|
const template = templateTypes.includes(this.type)
|
||||||
async initialize(
|
? `gitwit-${this.type}`
|
||||||
fileWatchCallback: ((files: (TFolder | TFile)[]) => void) | undefined
|
: `base`
|
||||||
) {
|
this.container = await E2BSandbox.create(template, {
|
||||||
// Acquire a lock to ensure exclusive access to the sandbox environment
|
timeoutMs: CONTAINER_TIMEOUT,
|
||||||
await lockManager.acquireLock(this.sandboxId, async () => {
|
|
||||||
// Check if a container already exists and is running
|
|
||||||
if (this.container && await this.container.isRunning()) {
|
|
||||||
console.log(`Found existing container ${this.sandboxId}`)
|
|
||||||
} else {
|
|
||||||
console.log("Creating container", this.sandboxId)
|
|
||||||
// Create a new container with a specified template and timeout
|
|
||||||
const templateTypes = ["vanillajs", "reactjs", "nextjs", "streamlit"];
|
|
||||||
const template = templateTypes.includes(this.type)
|
|
||||||
? `gitwit-${this.type}`
|
|
||||||
: `base`;
|
|
||||||
this.container = await E2BSandbox.create(template, {
|
|
||||||
timeoutMs: CONTAINER_TIMEOUT,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
// Ensure a container was successfully created
|
}
|
||||||
if (!this.container) throw new Error("Failed to create container")
|
})
|
||||||
|
// Ensure a container was successfully created
|
||||||
|
if (!this.container) throw new Error("Failed to create container")
|
||||||
|
|
||||||
// Initialize the terminal manager if it hasn't been set up yet
|
// Initialize the terminal manager if it hasn't been set up yet
|
||||||
if (!this.terminalManager) {
|
if (!this.terminalManager) {
|
||||||
this.terminalManager = new TerminalManager(this.container)
|
this.terminalManager = new TerminalManager(this.container)
|
||||||
console.log(`Terminal manager set up for ${this.sandboxId}`)
|
console.log(`Terminal manager set up for ${this.sandboxId}`)
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the file manager if it hasn't been set up yet
|
|
||||||
if (!this.fileManager) {
|
|
||||||
this.fileManager = new FileManager(
|
|
||||||
this.sandboxId,
|
|
||||||
this.container,
|
|
||||||
fileWatchCallback ?? null
|
|
||||||
)
|
|
||||||
// Initialize the file manager and emit the initial files
|
|
||||||
await this.fileManager.initialize()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called when the client disconnects from the Sandbox
|
// Initialize the file manager if it hasn't been set up yet
|
||||||
async disconnect() {
|
if (!this.fileManager) {
|
||||||
// Close all terminals managed by the terminal manager
|
this.fileManager = new FileManager(
|
||||||
await this.terminalManager?.closeAllTerminals()
|
this.sandboxId,
|
||||||
// This way the terminal manager will be set up again if we reconnect
|
this.container,
|
||||||
this.terminalManager = null;
|
fileWatchCallback ?? null
|
||||||
// Close all file watchers managed by the file manager
|
)
|
||||||
await this.fileManager?.closeWatchers()
|
// Initialize the file manager and emit the initial files
|
||||||
// This way the file manager will be set up again if we reconnect
|
await this.fileManager.initialize()
|
||||||
this.fileManager = null;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when the client disconnects from the Sandbox
|
||||||
|
async disconnect() {
|
||||||
|
// Close all terminals managed by the terminal manager
|
||||||
|
await this.terminalManager?.closeAllTerminals()
|
||||||
|
// This way the terminal manager will be set up again if we reconnect
|
||||||
|
this.terminalManager = null
|
||||||
|
// Close all file watchers managed by the file manager
|
||||||
|
await this.fileManager?.closeWatchers()
|
||||||
|
// This way the file manager will be set up again if we reconnect
|
||||||
|
this.fileManager = null
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers(connection: { userId: string; isOwner: boolean; socket: Socket }) {
|
||||||
|
// Handle heartbeat from a socket connection
|
||||||
|
const handleHeartbeat: SocketHandler = (_: any) => {
|
||||||
|
// Only keep the sandbox alive if the owner is still connected
|
||||||
|
if (connection.isOwner) {
|
||||||
|
this.container?.setTimeout(CONTAINER_TIMEOUT)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handlers(connection: { userId: string, isOwner: boolean, socket: Socket }) {
|
// Handle getting a file
|
||||||
|
const handleGetFile: SocketHandler = ({ fileId }: any) => {
|
||||||
|
return this.fileManager?.getFile(fileId)
|
||||||
|
}
|
||||||
|
|
||||||
// Handle heartbeat from a socket connection
|
// Handle getting a folder
|
||||||
const handleHeartbeat: SocketHandler = (_: any) => {
|
const handleGetFolder: SocketHandler = ({ folderId }: any) => {
|
||||||
// Only keep the sandbox alive if the owner is still connected
|
return this.fileManager?.getFolder(folderId)
|
||||||
if (connection.isOwner) {
|
}
|
||||||
this.container?.setTimeout(CONTAINER_TIMEOUT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle getting a file
|
// Handle saving a file
|
||||||
const handleGetFile: SocketHandler = ({ fileId }: any) => {
|
const handleSaveFile: SocketHandler = async ({ fileId, body }: any) => {
|
||||||
return this.fileManager?.getFile(fileId)
|
await saveFileRL.consume(connection.userId, 1)
|
||||||
}
|
return this.fileManager?.saveFile(fileId, body)
|
||||||
|
}
|
||||||
|
|
||||||
// Handle getting a folder
|
// Handle moving a file
|
||||||
const handleGetFolder: SocketHandler = ({ folderId }: any) => {
|
const handleMoveFile: SocketHandler = ({ fileId, folderId }: any) => {
|
||||||
return this.fileManager?.getFolder(folderId)
|
return this.fileManager?.moveFile(fileId, folderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle saving a file
|
// Handle listing apps
|
||||||
const handleSaveFile: SocketHandler = async ({ fileId, body }: any) => {
|
const handleListApps: SocketHandler = async (_: any) => {
|
||||||
await saveFileRL.consume(connection.userId, 1);
|
if (!this.dokkuClient)
|
||||||
return this.fileManager?.saveFile(fileId, body)
|
throw Error("Failed to retrieve apps list: No Dokku client")
|
||||||
}
|
return { success: true, apps: await this.dokkuClient.listApps() }
|
||||||
|
}
|
||||||
|
|
||||||
// Handle moving a file
|
// Handle deploying code
|
||||||
const handleMoveFile: SocketHandler = ({ fileId, folderId }: any) => {
|
const handleDeploy: SocketHandler = async (_: any) => {
|
||||||
return this.fileManager?.moveFile(fileId, folderId)
|
if (!this.gitClient) throw Error("No git client")
|
||||||
}
|
if (!this.fileManager) throw Error("No file manager")
|
||||||
|
await this.gitClient.pushFiles(this.fileManager?.fileData, this.sandboxId)
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
|
||||||
// Handle listing apps
|
// Handle creating a file
|
||||||
const handleListApps: SocketHandler = async (_: any) => {
|
const handleCreateFile: SocketHandler = async ({ name }: any) => {
|
||||||
if (!this.dokkuClient) throw Error("Failed to retrieve apps list: No Dokku client")
|
await createFileRL.consume(connection.userId, 1)
|
||||||
return { success: true, apps: await this.dokkuClient.listApps() }
|
return { success: await this.fileManager?.createFile(name) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle deploying code
|
// Handle creating a folder
|
||||||
const handleDeploy: SocketHandler = async (_: any) => {
|
const handleCreateFolder: SocketHandler = async ({ name }: any) => {
|
||||||
if (!this.gitClient) throw Error("No git client")
|
await createFolderRL.consume(connection.userId, 1)
|
||||||
if (!this.fileManager) throw Error("No file manager")
|
return { success: await this.fileManager?.createFolder(name) }
|
||||||
await this.gitClient.pushFiles(this.fileManager?.fileData, this.sandboxId)
|
}
|
||||||
return { success: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle creating a file
|
// Handle renaming a file
|
||||||
const handleCreateFile: SocketHandler = async ({ name }: any) => {
|
const handleRenameFile: SocketHandler = async ({
|
||||||
await createFileRL.consume(connection.userId, 1);
|
fileId,
|
||||||
return { "success": await this.fileManager?.createFile(name) }
|
newName,
|
||||||
}
|
}: any) => {
|
||||||
|
await renameFileRL.consume(connection.userId, 1)
|
||||||
|
return this.fileManager?.renameFile(fileId, newName)
|
||||||
|
}
|
||||||
|
|
||||||
// Handle creating a folder
|
// Handle deleting a file
|
||||||
const handleCreateFolder: SocketHandler = async ({ name }: any) => {
|
const handleDeleteFile: SocketHandler = async ({ fileId }: any) => {
|
||||||
await createFolderRL.consume(connection.userId, 1);
|
await deleteFileRL.consume(connection.userId, 1)
|
||||||
return { "success": await this.fileManager?.createFolder(name) }
|
return this.fileManager?.deleteFile(fileId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle renaming a file
|
// Handle deleting a folder
|
||||||
const handleRenameFile: SocketHandler = async ({ fileId, newName }: any) => {
|
const handleDeleteFolder: SocketHandler = ({ folderId }: any) => {
|
||||||
await renameFileRL.consume(connection.userId, 1)
|
return this.fileManager?.deleteFolder(folderId)
|
||||||
return this.fileManager?.renameFile(fileId, newName)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Handle deleting a file
|
// Handle creating a terminal session
|
||||||
const handleDeleteFile: SocketHandler = async ({ fileId }: any) => {
|
const handleCreateTerminal: SocketHandler = async ({ id }: any) => {
|
||||||
await deleteFileRL.consume(connection.userId, 1)
|
await lockManager.acquireLock(this.sandboxId, async () => {
|
||||||
return this.fileManager?.deleteFile(fileId)
|
await this.terminalManager?.createTerminal(
|
||||||
}
|
id,
|
||||||
|
(responseString: string) => {
|
||||||
// Handle deleting a folder
|
connection.socket.emit("terminalResponse", {
|
||||||
const handleDeleteFolder: SocketHandler = ({ folderId }: any) => {
|
id,
|
||||||
return this.fileManager?.deleteFolder(folderId)
|
data: responseString,
|
||||||
}
|
|
||||||
|
|
||||||
// Handle creating a terminal session
|
|
||||||
const handleCreateTerminal: SocketHandler = async ({ id }: any) => {
|
|
||||||
await lockManager.acquireLock(this.sandboxId, async () => {
|
|
||||||
await this.terminalManager?.createTerminal(id, (responseString: string) => {
|
|
||||||
connection.socket.emit("terminalResponse", { id, data: responseString })
|
|
||||||
const port = extractPortNumber(responseString)
|
|
||||||
if (port) {
|
|
||||||
connection.socket.emit(
|
|
||||||
"previewURL",
|
|
||||||
"https://" + this.container?.getHost(port)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
const port = extractPortNumber(responseString)
|
||||||
|
if (port) {
|
||||||
// Handle resizing a terminal
|
connection.socket.emit(
|
||||||
const handleResizeTerminal: SocketHandler = ({ dimensions }: any) => {
|
"previewURL",
|
||||||
this.terminalManager?.resizeTerminal(dimensions)
|
"https://" + this.container?.getHost(port)
|
||||||
}
|
)
|
||||||
|
}
|
||||||
// Handle sending data to a terminal
|
}
|
||||||
const handleTerminalData: SocketHandler = ({ id, data }: any) => {
|
)
|
||||||
return this.terminalManager?.sendTerminalData(id, data)
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// Handle closing a terminal
|
|
||||||
const handleCloseTerminal: SocketHandler = ({ id }: any) => {
|
|
||||||
return this.terminalManager?.closeTerminal(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle generating code
|
|
||||||
const handleGenerateCode: SocketHandler = ({ fileName, code, line, instructions }: any) => {
|
|
||||||
return this.aiWorker.generateCode(connection.userId, fileName, code, line, instructions)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle downloading files by download button
|
|
||||||
const handleDownloadFiles: SocketHandler = async () => {
|
|
||||||
if (!this.fileManager) throw Error("No file manager")
|
|
||||||
|
|
||||||
// Get all files with their data through fileManager
|
|
||||||
const files = this.fileManager.fileData.map((file: TFileData) => ({
|
|
||||||
path: file.id.startsWith('/') ? file.id.slice(1) : file.id,
|
|
||||||
content: file.data
|
|
||||||
}))
|
|
||||||
|
|
||||||
return { files }
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"heartbeat": handleHeartbeat,
|
|
||||||
"getFile": handleGetFile,
|
|
||||||
"downloadFiles": handleDownloadFiles,
|
|
||||||
"getFolder": handleGetFolder,
|
|
||||||
"saveFile": handleSaveFile,
|
|
||||||
"moveFile": handleMoveFile,
|
|
||||||
"list": handleListApps,
|
|
||||||
"deploy": handleDeploy,
|
|
||||||
"createFile": handleCreateFile,
|
|
||||||
"createFolder": handleCreateFolder,
|
|
||||||
"renameFile": handleRenameFile,
|
|
||||||
"deleteFile": handleDeleteFile,
|
|
||||||
"deleteFolder": handleDeleteFolder,
|
|
||||||
"createTerminal": handleCreateTerminal,
|
|
||||||
"resizeTerminal": handleResizeTerminal,
|
|
||||||
"terminalData": handleTerminalData,
|
|
||||||
"closeTerminal": handleCloseTerminal,
|
|
||||||
"generateCode": handleGenerateCode,
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle resizing a terminal
|
||||||
|
const handleResizeTerminal: SocketHandler = ({ dimensions }: any) => {
|
||||||
|
this.terminalManager?.resizeTerminal(dimensions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle sending data to a terminal
|
||||||
|
const handleTerminalData: SocketHandler = ({ id, data }: any) => {
|
||||||
|
return this.terminalManager?.sendTerminalData(id, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle closing a terminal
|
||||||
|
const handleCloseTerminal: SocketHandler = ({ id }: any) => {
|
||||||
|
return this.terminalManager?.closeTerminal(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle generating code
|
||||||
|
const handleGenerateCode: SocketHandler = ({
|
||||||
|
fileName,
|
||||||
|
code,
|
||||||
|
line,
|
||||||
|
instructions,
|
||||||
|
}: any) => {
|
||||||
|
return this.aiWorker.generateCode(
|
||||||
|
connection.userId,
|
||||||
|
fileName,
|
||||||
|
code,
|
||||||
|
line,
|
||||||
|
instructions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle downloading files by download button
|
||||||
|
const handleDownloadFiles: SocketHandler = async () => {
|
||||||
|
if (!this.fileManager) throw Error("No file manager")
|
||||||
|
|
||||||
|
// Get all files with their data through fileManager
|
||||||
|
const files = this.fileManager.fileData.map((file: TFileData) => ({
|
||||||
|
path: file.id.startsWith("/") ? file.id.slice(1) : file.id,
|
||||||
|
content: file.data,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return { files }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
heartbeat: handleHeartbeat,
|
||||||
|
getFile: handleGetFile,
|
||||||
|
downloadFiles: handleDownloadFiles,
|
||||||
|
getFolder: handleGetFolder,
|
||||||
|
saveFile: handleSaveFile,
|
||||||
|
moveFile: handleMoveFile,
|
||||||
|
list: handleListApps,
|
||||||
|
deploy: handleDeploy,
|
||||||
|
createFile: handleCreateFile,
|
||||||
|
createFolder: handleCreateFolder,
|
||||||
|
renameFile: handleRenameFile,
|
||||||
|
deleteFile: handleDeleteFile,
|
||||||
|
deleteFolder: handleDeleteFolder,
|
||||||
|
createTerminal: handleCreateTerminal,
|
||||||
|
resizeTerminal: handleResizeTerminal,
|
||||||
|
terminalData: handleTerminalData,
|
||||||
|
closeTerminal: handleCloseTerminal,
|
||||||
|
generateCode: handleGenerateCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -10,14 +10,14 @@ import { ConnectionManager } from "./ConnectionManager"
|
|||||||
import { DokkuClient } from "./DokkuClient"
|
import { DokkuClient } from "./DokkuClient"
|
||||||
import { Sandbox } from "./Sandbox"
|
import { Sandbox } from "./Sandbox"
|
||||||
import { SecureGitClient } from "./SecureGitClient"
|
import { SecureGitClient } from "./SecureGitClient"
|
||||||
import { socketAuth } from "./socketAuth"; // Import the new socketAuth middleware
|
import { socketAuth } from "./socketAuth" // Import the new socketAuth middleware
|
||||||
import { TFile, TFolder } from "./types"
|
import { TFile, TFolder } from "./types"
|
||||||
|
|
||||||
// Log errors and send a notification to the client
|
// Log errors and send a notification to the client
|
||||||
export const handleErrors = (message: string, error: any, socket: Socket) => {
|
export const handleErrors = (message: string, error: any, socket: Socket) => {
|
||||||
console.error(message, error);
|
console.error(message, error)
|
||||||
socket.emit("error", `${message} ${error.message ?? error}`);
|
socket.emit("error", `${message} ${error.message ?? error}`)
|
||||||
};
|
}
|
||||||
|
|
||||||
// Handle uncaught exceptions
|
// Handle uncaught exceptions
|
||||||
process.on("uncaughtException", (error) => {
|
process.on("uncaughtException", (error) => {
|
||||||
@ -64,10 +64,10 @@ if (!process.env.DOKKU_KEY)
|
|||||||
const dokkuClient =
|
const dokkuClient =
|
||||||
process.env.DOKKU_HOST && process.env.DOKKU_KEY && process.env.DOKKU_USERNAME
|
process.env.DOKKU_HOST && process.env.DOKKU_KEY && process.env.DOKKU_USERNAME
|
||||||
? new DokkuClient({
|
? new DokkuClient({
|
||||||
host: process.env.DOKKU_HOST,
|
host: process.env.DOKKU_HOST,
|
||||||
username: process.env.DOKKU_USERNAME,
|
username: process.env.DOKKU_USERNAME,
|
||||||
privateKey: fs.readFileSync(process.env.DOKKU_KEY),
|
privateKey: fs.readFileSync(process.env.DOKKU_KEY),
|
||||||
})
|
})
|
||||||
: null
|
: null
|
||||||
dokkuClient?.connect()
|
dokkuClient?.connect()
|
||||||
|
|
||||||
@ -75,9 +75,9 @@ dokkuClient?.connect()
|
|||||||
const gitClient =
|
const gitClient =
|
||||||
process.env.DOKKU_HOST && process.env.DOKKU_KEY
|
process.env.DOKKU_HOST && process.env.DOKKU_KEY
|
||||||
? new SecureGitClient(
|
? new SecureGitClient(
|
||||||
`dokku@${process.env.DOKKU_HOST}`,
|
`dokku@${process.env.DOKKU_HOST}`,
|
||||||
process.env.DOKKU_KEY
|
process.env.DOKKU_KEY
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
// Add this near the top of the file, after other initializations
|
// Add this near the top of the file, after other initializations
|
||||||
@ -110,21 +110,23 @@ io.on("connection", async (socket) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Create or retrieve the sandbox manager for the given sandbox ID
|
// Create or retrieve the sandbox manager for the given sandbox ID
|
||||||
const sandbox = sandboxes[data.sandboxId] ?? new Sandbox(
|
const sandbox =
|
||||||
data.sandboxId,
|
sandboxes[data.sandboxId] ??
|
||||||
data.type,
|
new Sandbox(data.sandboxId, data.type, {
|
||||||
{
|
aiWorker,
|
||||||
aiWorker, dokkuClient, gitClient,
|
dokkuClient,
|
||||||
}
|
gitClient,
|
||||||
)
|
})
|
||||||
sandboxes[data.sandboxId] = sandbox
|
sandboxes[data.sandboxId] = sandbox
|
||||||
|
|
||||||
// This callback recieves an update when the file list changes, and notifies all relevant connections.
|
// This callback recieves an update when the file list changes, and notifies all relevant connections.
|
||||||
const sendFileNotifications = (files: (TFolder | TFile)[]) => {
|
const sendFileNotifications = (files: (TFolder | TFile)[]) => {
|
||||||
connections.connectionsForSandbox(data.sandboxId).forEach((socket: Socket) => {
|
connections
|
||||||
socket.emit("loaded", files);
|
.connectionsForSandbox(data.sandboxId)
|
||||||
});
|
.forEach((socket: Socket) => {
|
||||||
};
|
socket.emit("loaded", files)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the sandbox container
|
// Initialize the sandbox container
|
||||||
// The file manager and terminal managers will be set up if they have been closed
|
// The file manager and terminal managers will be set up if they have been closed
|
||||||
@ -134,26 +136,35 @@ io.on("connection", async (socket) => {
|
|||||||
// Register event handlers for the sandbox
|
// Register event handlers for the sandbox
|
||||||
// For each event handler, listen on the socket for that event
|
// For each event handler, listen on the socket for that event
|
||||||
// Pass connection-specific information to the handlers
|
// Pass connection-specific information to the handlers
|
||||||
Object.entries(sandbox.handlers({
|
Object.entries(
|
||||||
userId: data.userId,
|
sandbox.handlers({
|
||||||
isOwner: data.isOwner,
|
userId: data.userId,
|
||||||
socket
|
isOwner: data.isOwner,
|
||||||
})).forEach(([event, handler]) => {
|
socket,
|
||||||
socket.on(event, async (options: any, callback?: (response: any) => void) => {
|
})
|
||||||
try {
|
).forEach(([event, handler]) => {
|
||||||
const result = await handler(options)
|
socket.on(
|
||||||
callback?.(result);
|
event,
|
||||||
} catch (e: any) {
|
async (options: any, callback?: (response: any) => void) => {
|
||||||
handleErrors(`Error processing event "${event}":`, e, socket);
|
try {
|
||||||
|
const result = await handler(options)
|
||||||
|
callback?.(result)
|
||||||
|
} catch (e: any) {
|
||||||
|
handleErrors(`Error processing event "${event}":`, e, socket)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
// Handle disconnection event
|
// Handle disconnection event
|
||||||
socket.on("disconnect", async () => {
|
socket.on("disconnect", async () => {
|
||||||
try {
|
try {
|
||||||
// Deregister the connection
|
// Deregister the connection
|
||||||
connections.removeConnectionForSandbox(socket, data.sandboxId, data.isOwner)
|
connections.removeConnectionForSandbox(
|
||||||
|
socket,
|
||||||
|
data.sandboxId,
|
||||||
|
data.isOwner
|
||||||
|
)
|
||||||
|
|
||||||
// If the owner has disconnected from all sockets, close open terminals and file watchers.o
|
// If the owner has disconnected from all sockets, close open terminals and file watchers.o
|
||||||
// The sandbox itself will timeout after the heartbeat stops.
|
// The sandbox itself will timeout after the heartbeat stops.
|
||||||
@ -165,16 +176,14 @@ io.on("connection", async (socket) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
handleErrors("Error disconnecting:", e, socket);
|
handleErrors("Error disconnecting:", e, socket)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
handleErrors(`Error initializing sandbox ${data.sandboxId}:`, e, socket);
|
handleErrors(`Error initializing sandbox ${data.sandboxId}:`, e, socket)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
handleErrors("Error connecting:", e, socket);
|
handleErrors("Error connecting:", e, socket)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -4,72 +4,72 @@ import { Sandbox, User } from "./types"
|
|||||||
|
|
||||||
// Middleware for socket authentication
|
// Middleware for socket authentication
|
||||||
export const socketAuth = async (socket: Socket, next: Function) => {
|
export const socketAuth = async (socket: Socket, next: Function) => {
|
||||||
// Define the schema for handshake query validation
|
// Define the schema for handshake query validation
|
||||||
const handshakeSchema = z.object({
|
const handshakeSchema = z.object({
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
sandboxId: z.string(),
|
sandboxId: z.string(),
|
||||||
EIO: z.string(),
|
EIO: z.string(),
|
||||||
transport: z.string(),
|
transport: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const q = socket.handshake.query
|
const q = socket.handshake.query
|
||||||
const parseQuery = handshakeSchema.safeParse(q)
|
const parseQuery = handshakeSchema.safeParse(q)
|
||||||
|
|
||||||
// Check if the query is valid according to the schema
|
// Check if the query is valid according to the schema
|
||||||
if (!parseQuery.success) {
|
if (!parseQuery.success) {
|
||||||
next(new Error("Invalid request."))
|
next(new Error("Invalid request."))
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sandboxId, userId } = parseQuery.data
|
||||||
|
// Fetch user data from the database
|
||||||
|
const dbUser = await fetch(
|
||||||
|
`${process.env.DATABASE_WORKER_URL}/api/user?id=${userId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `${process.env.WORKERS_KEY}`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
const dbUserJSON = (await dbUser.json()) as User
|
||||||
|
|
||||||
const { sandboxId, userId } = parseQuery.data
|
// Fetch sandbox data from the database
|
||||||
// Fetch user data from the database
|
const dbSandbox = await fetch(
|
||||||
const dbUser = await fetch(
|
`${process.env.DATABASE_WORKER_URL}/api/sandbox?id=${sandboxId}`,
|
||||||
`${process.env.DATABASE_WORKER_URL}/api/user?id=${userId}`,
|
{
|
||||||
{
|
headers: {
|
||||||
headers: {
|
Authorization: `${process.env.WORKERS_KEY}`,
|
||||||
Authorization: `${process.env.WORKERS_KEY}`,
|
},
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const dbUserJSON = (await dbUser.json()) as User
|
|
||||||
|
|
||||||
// Fetch sandbox data from the database
|
|
||||||
const dbSandbox = await fetch(
|
|
||||||
`${process.env.DATABASE_WORKER_URL}/api/sandbox?id=${sandboxId}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `${process.env.WORKERS_KEY}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const dbSandboxJSON = (await dbSandbox.json()) as Sandbox
|
|
||||||
|
|
||||||
// Check if user data was retrieved successfully
|
|
||||||
if (!dbUserJSON) {
|
|
||||||
next(new Error("DB error."))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
const dbSandboxJSON = (await dbSandbox.json()) as Sandbox
|
||||||
|
|
||||||
// Check if the user owns the sandbox or has shared access
|
// Check if user data was retrieved successfully
|
||||||
const sandbox = dbUserJSON.sandbox.find((s) => s.id === sandboxId)
|
if (!dbUserJSON) {
|
||||||
const sharedSandboxes = dbUserJSON.usersToSandboxes.find(
|
next(new Error("DB error."))
|
||||||
(uts) => uts.sandboxId === sandboxId
|
return
|
||||||
)
|
}
|
||||||
|
|
||||||
// If user doesn't own or have shared access to the sandbox, deny access
|
// Check if the user owns the sandbox or has shared access
|
||||||
if (!sandbox && !sharedSandboxes) {
|
const sandbox = dbUserJSON.sandbox.find((s) => s.id === sandboxId)
|
||||||
next(new Error("Invalid credentials."))
|
const sharedSandboxes = dbUserJSON.usersToSandboxes.find(
|
||||||
return
|
(uts) => uts.sandboxId === sandboxId
|
||||||
}
|
)
|
||||||
|
|
||||||
// Set socket data with user information
|
// If user doesn't own or have shared access to the sandbox, deny access
|
||||||
socket.data = {
|
if (!sandbox && !sharedSandboxes) {
|
||||||
userId,
|
next(new Error("Invalid credentials."))
|
||||||
sandboxId: sandboxId,
|
return
|
||||||
isOwner: sandbox !== undefined,
|
}
|
||||||
type: dbSandboxJSON.type
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow the connection
|
// Set socket data with user information
|
||||||
next()
|
socket.data = {
|
||||||
|
userId,
|
||||||
|
sandboxId: sandboxId,
|
||||||
|
isOwner: sandbox !== undefined,
|
||||||
|
type: dbSandboxJSON.type,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the connection
|
||||||
|
next()
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user