diff --git a/backend/server/dist/index.js b/backend/server/dist/index.js index 2134505..747f7ae 100644 --- a/backend/server/dist/index.js +++ b/backend/server/dist/index.js @@ -22,6 +22,7 @@ const socket_io_1 = require("socket.io"); const zod_1 = require("zod"); const utils_1 = require("./utils"); const node_pty_1 = require("node-pty"); +const ratelimit_1 = require("./ratelimit"); dotenv_1.default.config(); const app = (0, express_1.default)(); const port = process.env.PORT || 4000; @@ -89,60 +90,97 @@ io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () { }); // todo: send diffs + debounce for efficiency socket.on("saveFile", (fileId, body) => __awaiter(void 0, void 0, void 0, function* () { - const file = sandboxFiles.fileData.find((f) => f.id === fileId); - if (!file) - return; - file.data = body; - fs_1.default.writeFile(path_1.default.join(dirName, file.id), body, function (err) { - if (err) - throw err; - }); - yield (0, utils_1.saveFile)(fileId, body); + try { + yield ratelimit_1.saveFileRL.consume(data.userId, 1); + if (Buffer.byteLength(body, "utf-8") > ratelimit_1.MAX_BODY_SIZE) { + socket.emit("rateLimit", "Rate limited: file size too large. Please reduce the file size."); + return; + } + const file = sandboxFiles.fileData.find((f) => f.id === fileId); + if (!file) + return; + file.data = body; + fs_1.default.writeFile(path_1.default.join(dirName, file.id), body, function (err) { + if (err) + throw err; + }); + yield (0, utils_1.saveFile)(fileId, body); + } + catch (e) { + socket.emit("rateLimit", "Rate limited: file saving. Please slow down."); + } })); socket.on("createFile", (name) => __awaiter(void 0, void 0, void 0, function* () { - const id = `projects/${data.id}/${name}`; - fs_1.default.writeFile(path_1.default.join(dirName, id), "", function (err) { - if (err) - throw err; - }); - sandboxFiles.files.push({ - id, - name, - type: "file", - }); - sandboxFiles.fileData.push({ - id, - data: "", - }); - yield (0, utils_1.createFile)(id); + try { + yield ratelimit_1.createFileRL.consume(data.userId, 1); + const id = `projects/${data.id}/${name}`; + fs_1.default.writeFile(path_1.default.join(dirName, id), "", function (err) { + if (err) + throw err; + }); + sandboxFiles.files.push({ + id, + name, + type: "file", + }); + sandboxFiles.fileData.push({ + id, + data: "", + }); + yield (0, utils_1.createFile)(id); + } + catch (e) { + socket.emit("rateLimit", "Rate limited: file creation. Please slow down."); + } })); socket.on("renameFile", (fileId, newName) => __awaiter(void 0, void 0, void 0, function* () { - const file = sandboxFiles.fileData.find((f) => f.id === fileId); - if (!file) + try { + yield ratelimit_1.renameFileRL.consume(data.userId, 1); + const file = sandboxFiles.fileData.find((f) => f.id === fileId); + if (!file) + return; + file.id = newName; + const parts = fileId.split("/"); + const newFileId = parts.slice(0, parts.length - 1).join("/") + "/" + newName; + fs_1.default.rename(path_1.default.join(dirName, fileId), path_1.default.join(dirName, newFileId), function (err) { + if (err) + throw err; + }); + yield (0, utils_1.renameFile)(fileId, newFileId, file.data); + } + catch (e) { + socket.emit("rateLimit", "Rate limited: file renaming. Please slow down."); return; - file.id = newName; - const parts = fileId.split("/"); - const newFileId = parts.slice(0, parts.length - 1).join("/") + "/" + newName; - fs_1.default.rename(path_1.default.join(dirName, fileId), path_1.default.join(dirName, newFileId), function (err) { - if (err) - throw err; - }); - yield (0, utils_1.renameFile)(fileId, newFileId, file.data); + } })); socket.on("deleteFile", (fileId, callback) => __awaiter(void 0, void 0, void 0, function* () { - const file = sandboxFiles.fileData.find((f) => f.id === fileId); - if (!file) - return; - fs_1.default.unlink(path_1.default.join(dirName, fileId), function (err) { - if (err) - throw err; - }); - sandboxFiles.fileData = sandboxFiles.fileData.filter((f) => f.id !== fileId); - yield (0, utils_1.deleteFile)(fileId); - const newFiles = yield (0, utils_1.getSandboxFiles)(data.id); - callback(newFiles.files); + try { + yield ratelimit_1.deleteFileRL.consume(data.userId, 1); + const file = sandboxFiles.fileData.find((f) => f.id === fileId); + if (!file) + return; + fs_1.default.unlink(path_1.default.join(dirName, fileId), function (err) { + if (err) + throw err; + }); + sandboxFiles.fileData = sandboxFiles.fileData.filter((f) => f.id !== fileId); + yield (0, utils_1.deleteFile)(fileId); + const newFiles = yield (0, utils_1.getSandboxFiles)(data.id); + callback(newFiles.files); + } + catch (e) { + socket.emit("rateLimit", "Rate limited: file deletion. Please slow down."); + } })); socket.on("createTerminal", ({ id }) => { + if (terminals[id]) { + console.log("Terminal already exists."); + return; + } + if (Object.keys(terminals).length >= 4) { + console.log("Too many terminals."); + return; + } const pty = (0, node_pty_1.spawn)(os_1.default.platform() === "win32" ? "cmd.exe" : "bash", [], { name: "xterm", cols: 100, diff --git a/frontend/app/api/lb-auth/route.ts b/frontend/app/api/lb-auth/route.ts index ad480bc..08dfcfb 100644 --- a/frontend/app/api/lb-auth/route.ts +++ b/frontend/app/api/lb-auth/route.ts @@ -1,3 +1,4 @@ +import { colors } from "@/lib/colors" import { User } from "@/lib/types" import { currentUser } from "@clerk/nextjs" import { Liveblocks } from "@liveblocks/node" @@ -19,12 +20,19 @@ export async function POST(request: NextRequest) { const res = await fetch(`http://localhost:8787/api/user?id=${clerkUser.id}`) const user = (await res.json()) as User + const colorNames = Object.keys(colors) + const randomColor = colorNames[ + Math.floor(Math.random() * colorNames.length) + ] as keyof typeof colors + const code = colors[randomColor] + // Create a session for the current user // userInfo is made available in Liveblocks presence hooks, e.g. useOthers const session = liveblocks.prepareSession(user.id, { userInfo: { name: user.name, email: user.email, + color: randomColor, }, }) diff --git a/frontend/app/globals.css b/frontend/app/globals.css index abbbb13..aa82857 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -99,13 +99,23 @@ background: radial-gradient(circle at bottom right, #312e81 -75%, hsl(0 0% 3.9%) 60%); /* violet 900 -> bg */ } +.inline-decoration::before { + content: 'Generate'; + color: #525252; + /* border: 1px solid #525252; */ + /* padding: 2px 4px; */ + /* border-radius: 4px; */ + margin-left: 36px; +} .inline-decoration::after { - content: 'Generate ⌘G'; + content: '⌘G'; color: #525252; border: 1px solid #525252; - padding: 2px 4px; + border-bottom-width: 2px; + padding: 0 4px; border-radius: 4px; - margin-left: 56px; + margin-left: 6px; + line-height: 0; } .yRemoteSelection { diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index 3bf7b7b..908d58b 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -11,12 +11,7 @@ import * as Y from "yjs" import LiveblocksProvider from "@liveblocks/yjs" import { MonacoBinding } from "y-monaco" import { Awareness } from "y-protocols/awareness" -import { - TypedLiveblocksProvider, - useRoom, - AwarenessList, - useSelf, -} from "@/liveblocks.config" +import { TypedLiveblocksProvider, useRoom } from "@/liveblocks.config" import { ResizableHandle, @@ -27,6 +22,7 @@ import { ChevronLeft, ChevronRight, FileJson, + Loader2, Plus, RotateCw, Shell, @@ -355,15 +351,20 @@ export default function CodeEditor({ setFiles(files) } - socket.on("connect", onConnect) + const onRateLimit = (message: string) => { + toast.error(message) + } + socket.on("connect", onConnect) socket.on("disconnect", onDisconnect) socket.on("loaded", onLoadedEvent) + socket.on("rateLimit", onRateLimit) return () => { socket.off("connect", onConnect) socket.off("disconnect", onDisconnect) socket.off("loaded", onLoadedEvent) + socket.off("rateLimit", onRateLimit) } }, []) @@ -547,7 +548,7 @@ export default function CodeEditor({ > {!activeId ? ( <> -
+
No file selected.
@@ -591,7 +592,12 @@ export default function CodeEditor({ value={activeFile ?? ""} /> - ) : null} + ) : ( +
+ + Waiting for Clerk to load... +
+ )}
@@ -629,27 +635,36 @@ export default function CodeEditor({ minSize={20} className="p-2 flex flex-col" > -
- - - Shell - - -
-
- {socket ? : null} -
+ {isOwner ? ( + <> +
+ + + Shell + + +
+
+ {socket ? : null} +
+ + ) : ( +
+ + No terminal access. +
+ )} diff --git a/frontend/components/editor/live/avatars.tsx b/frontend/components/editor/live/avatars.tsx index 92c72ff..f52bc14 100644 --- a/frontend/components/editor/live/avatars.tsx +++ b/frontend/components/editor/live/avatars.tsx @@ -1,33 +1,39 @@ "use client" -import { colorClasses, colors } from "@/lib/colors" import { useOthers } from "@/liveblocks.config" -import { useState } from "react" + +const classNames = { + red: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-red-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-red-950 to-red-600 flex items-center justify-center text-xs font-medium", + orange: + "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-orange-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-orange-950 to-orange-600 flex items-center justify-center text-xs font-medium", + yellow: + "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-yellow-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-yellow-950 to-yellow-600 flex items-center justify-center text-xs font-medium", + green: + "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-green-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-green-950 to-green-600 flex items-center justify-center text-xs font-medium", + blue: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-blue-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-blue-950 to-blue-600 flex items-center justify-center text-xs font-medium", + purple: + "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-purple-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-purple-950 to-purple-600 flex items-center justify-center text-xs font-medium", + pink: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-pink-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-pink-950 to-pink-600 flex items-center justify-center text-xs font-medium", +} export function Avatars() { const users = useOthers() - const colorNames = Object.keys(colors) - const [activeColors, setActiveColors] = useState([]) - return ( -
- {users.map(({ connectionId, info }) => { - const c = colorNames[ - connectionId % colorNames.length - ] as keyof typeof colors - - return ( -
- {info.name - .split(" ") - .slice(0, 2) - .map((letter) => letter[0].toUpperCase())} -
- ) - })} -
+ <> +
+ {users.map(({ connectionId, info }) => { + return ( +
+ {info.name + .split(" ") + .slice(0, 2) + .map((letter) => letter[0].toUpperCase())} +
+ ) + })} +
+
+ ) } diff --git a/frontend/components/editor/live/cursors.tsx b/frontend/components/editor/live/cursors.tsx index c86dd47..18d3fa0 100644 --- a/frontend/components/editor/live/cursors.tsx +++ b/frontend/components/editor/live/cursors.tsx @@ -5,11 +5,14 @@ import { UserAwareness, useSelf, } from "@/liveblocks.config" +import { colors } from "@/lib/colors" export function Cursors({ yProvider }: { yProvider: TypedLiveblocksProvider }) { // Get user info from Liveblocks authentication endpoint const userInfo = useSelf((me) => me.info) + if (!userInfo) return null + const [awarenessUsers, setAwarenessUsers] = useState([]) useEffect(() => { @@ -40,7 +43,7 @@ export function Cursors({ yProvider }: { yProvider: TypedLiveblocksProvider }) { cursorStyles += ` .yRemoteSelection-${clientId}, .yRemoteSelectionHead-${clientId} { - --user-color: ${"#FF0000"}; + --user-color: ${colors[client.user.color]}; } .yRemoteSelectionHead-${clientId}::after { diff --git a/frontend/components/editor/live/room.tsx b/frontend/components/editor/live/room.tsx index 5ba17c5..a59aaf3 100644 --- a/frontend/components/editor/live/room.tsx +++ b/frontend/components/editor/live/room.tsx @@ -2,6 +2,7 @@ import { RoomProvider } from "@/liveblocks.config" import { ClientSideSuspense } from "@liveblocks/react" +import Loading from "../loading" export function Room({ id, @@ -17,9 +18,9 @@ export function Room({ cursor: null, }} > - Loading!!!!
}> - {() => children} - + {/* }> */} + {children} + {/* */} ) } diff --git a/frontend/components/editor/loading.tsx b/frontend/components/editor/loading.tsx new file mode 100644 index 0000000..6780701 --- /dev/null +++ b/frontend/components/editor/loading.tsx @@ -0,0 +1,41 @@ +import Image from "next/image" +import Logo from "@/assets/logo.svg" +import { Skeleton } from "../ui/skeleton" +import { Loader, Loader2 } from "lucide-react" + +export default function Loading() { + return ( +
+
+
+ Logo + +
+
+ + +
+
+
+
+
+
Explorer
+
+ + +
+
+
+
+ +
+
+
+
+ + Loading... +
{" "} +
+
+ ) +} diff --git a/frontend/components/editor/navbar/index.tsx b/frontend/components/editor/navbar/index.tsx index 014c0a1..b5cfede 100644 --- a/frontend/components/editor/navbar/index.tsx +++ b/frontend/components/editor/navbar/index.tsx @@ -62,7 +62,7 @@ export default function Navbar({ ) : null} -
+
{isOwner ? ( diff --git a/frontend/components/editor/sidebar/index.tsx b/frontend/components/editor/sidebar/index.tsx index e80984e..e569e61 100644 --- a/frontend/components/editor/sidebar/index.tsx +++ b/frontend/components/editor/sidebar/index.tsx @@ -8,7 +8,7 @@ import { useState } from "react" import New from "./new" import { Socket } from "socket.io-client" import Button from "@/components/ui/customButton" -import Toggle from "@/components/ui/customToggle" +import { Switch } from "@/components/ui/switch" export default function Sidebar({ files, @@ -104,12 +104,20 @@ export default function Sidebar({ )}
- {/*
*/} - - - AI Copilot - - {/*
*/} +
+
+ + Copilot{" "} + + ⌘G + +
+ +
) } diff --git a/frontend/components/ui/customToggle.tsx b/frontend/components/ui/customToggle.tsx deleted file mode 100644 index 776b6bf..0000000 --- a/frontend/components/ui/customToggle.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import * as React from "react" -import { Plus } from "lucide-react" -import { cn } from "@/lib/utils" -import { Button } from "./button" - -const Toggle = ({ - children, - className, - value, - setValue, -}: { - children: React.ReactNode - className?: string - value: boolean - setValue: React.Dispatch> -}) => { - if (value) - return ( - - ) - else - return ( - - ) -} - -export default Toggle diff --git a/frontend/components/ui/skeleton.tsx b/frontend/components/ui/skeleton.tsx new file mode 100644 index 0000000..d7e45f7 --- /dev/null +++ b/frontend/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/frontend/components/ui/switch.tsx b/frontend/components/ui/switch.tsx new file mode 100644 index 0000000..5f4117f --- /dev/null +++ b/frontend/components/ui/switch.tsx @@ -0,0 +1,29 @@ +"use client" + +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/frontend/lib/colors.ts b/frontend/lib/colors.ts index 8967c38..e30f754 100644 --- a/frontend/lib/colors.ts +++ b/frontend/lib/colors.ts @@ -7,34 +7,3 @@ export const colors = { purple: "#7c3aed", pink: "#db2777", } - -export const colorClasses = { - red: { - ring: "ring-red-700", - bg: "from-red-950 to-red-600", - }, - orange: { - ring: "ring-orange-700", - bg: "from-orange-950 to-orange-600", - }, - yellow: { - ring: "ring-yellow-700", - bg: "from-yellow-950 to-yellow-600", - }, - green: { - ring: "ring-green-700", - bg: "from-green-950 to-green-600", - }, - blue: { - ring: "ring-blue-700", - bg: "from-blue-950 to-blue-600", - }, - purple: { - ring: "ring-purple-700", - bg: "from-purple-950 to-purple-600", - }, - pink: { - ring: "ring-pink-700", - bg: "from-pink-950 to-pink-600", - }, -} diff --git a/frontend/liveblocks.config.ts b/frontend/liveblocks.config.ts index b898d9e..df22c32 100644 --- a/frontend/liveblocks.config.ts +++ b/frontend/liveblocks.config.ts @@ -1,6 +1,7 @@ import { createClient } from "@liveblocks/client" import { createRoomContext, createLiveblocksContext } from "@liveblocks/react" import YLiveblocksProvider from "@liveblocks/yjs" +import { colors } from "./lib/colors" const client = createClient({ // publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY!, @@ -73,6 +74,7 @@ type UserMeta = { info: { name: string email: string + color: keyof typeof colors } } @@ -99,60 +101,56 @@ export type AwarenessList = [number, UserAwareness][] // Room-level hooks, use inside `RoomProvider` export const { - suspense: { - RoomProvider, - useRoom, - useMyPresence, - useUpdateMyPresence, - useSelf, - useOthers, - useOthersMapped, - useOthersListener, - useOthersConnectionIds, - useOther, - useBroadcastEvent, - useEventListener, - useErrorListener, - useStorage, - useBatch, - useHistory, - useUndo, - useRedo, - useCanUndo, - useCanRedo, - useMutation, - useStatus, - useLostConnectionListener, - useThreads, - useCreateThread, - useEditThreadMetadata, - useCreateComment, - useEditComment, - useDeleteComment, - useAddReaction, - useRemoveReaction, - useThreadSubscription, - useMarkThreadAsRead, - useRoomNotificationSettings, - useUpdateRoomNotificationSettings, + RoomProvider, + useRoom, + useMyPresence, + useUpdateMyPresence, + useSelf, + useOthers, + useOthersMapped, + useOthersListener, + useOthersConnectionIds, + useOther, + useBroadcastEvent, + useEventListener, + useErrorListener, + useStorage, + useBatch, + useHistory, + useUndo, + useRedo, + useCanUndo, + useCanRedo, + useMutation, + useStatus, + useLostConnectionListener, + useThreads, + useCreateThread, + useEditThreadMetadata, + useCreateComment, + useEditComment, + useDeleteComment, + useAddReaction, + useRemoveReaction, + useThreadSubscription, + useMarkThreadAsRead, + useRoomNotificationSettings, + useUpdateRoomNotificationSettings, - // These hooks can be exported from either context - useUser, - useRoomInfo, - }, + // These hooks can be exported from either context + useUser, + useRoomInfo, } = createRoomContext( client ) // Project-level hooks, use inside `LiveblocksProvider` export const { - suspense: { - LiveblocksProvider, - useMarkInboxNotificationAsRead, - useMarkAllInboxNotificationsAsRead, - useInboxNotifications, - useUnreadInboxNotificationsCount, - }, + LiveblocksProvider, + useMarkInboxNotificationAsRead, + useMarkAllInboxNotificationsAsRead, + useInboxNotifications, + useUnreadInboxNotificationsCount, } = createLiveblocksContext(client) export type TypedLiveblocksProvider = YLiveblocksProvider< diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ac33b95..8061baa 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,6 +24,7 @@ "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "class-variance-authority": "^0.7.0", @@ -1228,6 +1229,35 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz", + "integrity": "sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index e201b66..016b772 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,6 +25,7 @@ "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "class-variance-authority": "^0.7.0",