Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 16 additions & 24 deletions live/src/core/extensions/index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
// Third-party libraries
import { Redis } from "ioredis";

// Hocuspocus extensions and core
import { Database } from "@hocuspocus/extension-database";
import { Extension } from "@hocuspocus/server";
import { Logger } from "@hocuspocus/extension-logger";
import { Redis as HocusPocusRedis } from "@hocuspocus/extension-redis";

// Core helpers and utilities
// core helpers and utilities
import { manualLogger } from "@/core/helpers/logger.js";
import { getRedisUrl } from "@/core/lib/utils/redis-url.js";

// Core libraries
// core libraries
import {
fetchPageDescriptionBinary,
updatePageDescription,
} from "@/core/lib/page.js";

// Core types
import { TDocumentTypes } from "@/core/types/common.js";

// Plane live libraries
// plane live libraries
import { fetchDocument } from "@/plane-live/lib/fetch-document.js";
import { updateDocument } from "@/plane-live/lib/update-document.js";
// types
import {
type HocusPocusServerContext,
type TDocumentTypes,
} from "@/core/types/common.js";

export const getExtensions: () => Promise<Extension[]> = async () => {
const extensions: Extension[] = [
Expand All @@ -33,13 +31,8 @@ export const getExtensions: () => Promise<Extension[]> = async () => {
},
}),
new Database({
fetch: async ({
documentName: pageId,
requestHeaders,
requestParameters,
}) => {
// request headers
const cookie = requestHeaders.cookie?.toString();
fetch: async ({ context, documentName: pageId, requestParameters }) => {
const cookie = (context as HocusPocusServerContext).cookie;
// query params
const params = requestParameters;
const documentType = params.get("documentType")?.toString() as
Expand All @@ -54,7 +47,7 @@ export const getExtensions: () => Promise<Extension[]> = async () => {
fetchedData = await fetchPageDescriptionBinary(
params,
pageId,
cookie
cookie,
);
} else {
fetchedData = await fetchDocument({
Expand All @@ -71,13 +64,12 @@ export const getExtensions: () => Promise<Extension[]> = async () => {
});
},
store: async ({
context,
state,
documentName: pageId,
requestHeaders,
requestParameters,
}) => {
// request headers
const cookie = requestHeaders.cookie?.toString();
const cookie = (context as HocusPocusServerContext).cookie;
// query params
const params = requestParameters;
const documentType = params.get("documentType")?.toString() as
Expand Down Expand Up @@ -124,7 +116,7 @@ export const getExtensions: () => Promise<Extension[]> = async () => {
}
manualLogger.warn(
`Redis Client wasn't able to connect, continuing without Redis (you won't be able to sync data between multiple plane live servers)`,
error
error,
);
reject(error);
});
Expand All @@ -138,12 +130,12 @@ export const getExtensions: () => Promise<Extension[]> = async () => {
} catch (error) {
manualLogger.warn(
`Redis Client wasn't able to connect, continuing without Redis (you won't be able to sync data between multiple plane live servers)`,
error
error,
);
}
} else {
manualLogger.warn(
"Redis URL is not set, continuing without Redis (you won't be able to sync data between multiple plane live servers)"
"Redis URL is not set, continuing without Redis (you won't be able to sync data between multiple plane live servers)",
);
}

Expand Down
34 changes: 29 additions & 5 deletions live/src/core/hocuspocus-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { v4 as uuidv4 } from "uuid";
import { handleAuthentication } from "@/core/lib/authentication.js";
// extensions
import { getExtensions } from "@/core/extensions/index.js";
// editor types
import { TUserDetails } from "@plane/editor";
// types
import { type HocusPocusServerContext } from "@/core/types/common.js";

export const getHocusPocusServer = async () => {
const extensions = await getExtensions();
Expand All @@ -12,20 +16,40 @@ export const getHocusPocusServer = async () => {
name: serverName,
onAuthenticate: async ({
requestHeaders,
context,
// user id used as token for authentication
token,
}) => {
// request headers
const cookie = requestHeaders.cookie?.toString();
let cookie: string | undefined = undefined;
let userId: string | undefined = undefined;

if (!cookie) {
throw Error("Credentials not provided");
// Extract cookie (fallback to request headers) and userId from token (for scenarios where
// the cookies are not passed in the request headers)
try {
const parsedToken = JSON.parse(token) as TUserDetails;
userId = parsedToken.id;
cookie = parsedToken.cookie;
} catch (error) {
// If token parsing fails, fallback to request headers
console.error("Token parsing failed, using request headers:", error);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid logging raw errors to prevent sensitive data exposure

Logging the raw error object may expose sensitive information if the error contains details about user input. To prevent potential leakage of sensitive data, consider logging only the error message or a generic message.

Apply this diff to modify the logging statement:

- console.error("Token parsing failed, using request headers:", error);
+ console.error("Token parsing failed, using request headers. Error:", error.message);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.error("Token parsing failed, using request headers:", error);
console.error("Token parsing failed, using request headers. Error:", error.message);

} finally {
// If cookie is still not found, fallback to request headers
if (!cookie) {
cookie = requestHeaders.cookie?.toString();
}
Comment on lines +28 to +39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure userId is obtained when token parsing fails

When the parsing of token fails, userId remains undefined because it is only assigned within the try block. As a result, even if cookie is obtained from requestHeaders, the authentication will fail due to the missing userId.

Consider adding a fallback mechanism to obtain userId when token parsing fails. This ensures that authentication can proceed if userId can be retrieved from another source.

}

if (!cookie || !userId) {
throw new Error("Credentials not provided");
}

// set cookie in context, so it can be used throughout the ws connection
(context as HocusPocusServerContext).cookie = cookie;

try {
await handleAuthentication({
cookie,
token,
userId,
});
} catch (error) {
throw Error("Authentication unsuccessful!");
Expand Down
6 changes: 3 additions & 3 deletions live/src/core/lib/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ const userService = new UserService();

type Props = {
cookie: string;
token: string;
userId: string;
};

export const handleAuthentication = async (props: Props) => {
const { cookie, token } = props;
const { cookie, userId } = props;
// fetch current user info
let response;
try {
Expand All @@ -20,7 +20,7 @@ export const handleAuthentication = async (props: Props) => {
manualLogger.error("Failed to fetch current user:", error);
throw error;
}
if (response.id !== token) {
if (response.id !== userId) {
throw Error("Authentication failed: Token doesn't match the current user.");
Comment on lines +23 to 24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Update error message to reflect userId instead of token.

The error message still mentions "Token" despite the parameter being changed to userId. This could be confusing for debugging.

-    throw Error("Authentication failed: Token doesn't match the current user.");
+    throw Error("Authentication failed: User ID doesn't match the current user.");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (response.id !== userId) {
throw Error("Authentication failed: Token doesn't match the current user.");
if (response.id !== userId) {
throw Error("Authentication failed: User ID doesn't match the current user.");

}

Expand Down
4 changes: 4 additions & 0 deletions live/src/core/types/common.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
import { TAdditionalDocumentTypes } from "@/plane-live/types/common.js";

export type TDocumentTypes = "project_page" | TAdditionalDocumentTypes;

export type HocusPocusServerContext = {
cookie: string;
};
2 changes: 1 addition & 1 deletion packages/editor/src/core/hooks/use-collaborative-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
name: id,
parameters: realtimeConfig.queryParams,
// using user id as a token to verify the user on the server
token: user.id,
token: JSON.stringify(user),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Update useMemo dependencies to match usage.

The dependency array includes user.id, but the hook now depends on the entire user object due to JSON.stringify(user). This could lead to missed updates if other user properties change.

-[id, realtimeConfig, serverHandler, user.id]
+[id, realtimeConfig, serverHandler, user]

Also applies to: 64-64


⚠️ Potential issue

Reconsider sending the entire user object as token.

Sending the complete user object as a token raises several concerns:

  1. Security: Exposing more user data than necessary could lead to potential information leakage
  2. Performance: Increased payload size due to serializing the entire user object
  3. Type safety: No validation on the stringified user structure

Consider creating a minimal token object with only the required fields (e.g., id and cookie).

-token: JSON.stringify(user),
+token: JSON.stringify({ id: user.id, cookie: user.cookie }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
token: JSON.stringify(user),
token: JSON.stringify({ id: user.id, cookie: user.cookie }),

url: realtimeConfig.url,
onAuthenticationFailed: () => {
serverHandler?.onServerError?.();
Expand Down
1 change: 1 addition & 0 deletions packages/editor/src/core/types/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export type TUserDetails = {
color: string;
id: string;
name: string;
cookie?: string;
};

export type TRealtimeConfig = {
Expand Down