Skip to content

feat: implement room privacy system with password protection and mess…#111

Open
prajwallshetty wants to merge 2 commits into
freeman-jiang:mainfrom
prajwallshetty:main
Open

feat: implement room privacy system with password protection and mess…#111
prajwallshetty wants to merge 2 commits into
freeman-jiang:mainfrom
prajwallshetty:main

Conversation

@prajwallshetty

Copy link
Copy Markdown

Add Private Rooms with Password Protection and Message Unsend

Description

This PR introduces two privacy-focused features to improve the Beatsync room experience:

  1. Private Rooms with Password Protection
  2. Message Unsend Functionality

Changes Made

Private Rooms

  • Added support for password-protected rooms.
  • Users must enter the correct password before joining a private room.
  • Passwords are securely hashed before storage.
  • Added UI indicators for private rooms.
  • Added room settings for enabling, disabling, and updating passwords.

Message Unsend

  • Added an "Unsend Message" option for message authors.
  • Deleted messages are synchronized across all connected clients in real time.
  • Only the original sender can unsend their messages.
  • Added backend validation to prevent unauthorized message deletion.

Why This Change?

Currently, anyone with a room link can access room chats. Password protection gives hosts better control over who can join.

Additionally, users may accidentally send messages and have no way to remove them. The unsend feature provides a better user experience and aligns with modern messaging platforms.

Testing

  • Created private rooms and verified password validation.
  • Verified unauthorized users cannot join without the correct password.
  • Tested real-time message deletion across multiple devices.
  • Verified only message authors can unsend their own messages.
  • Confirmed existing public rooms continue to function normally.

Future Improvements

  • Invite-only rooms.
  • Role-based permissions (Host, Moderator, Member).
  • Time-limited message unsend.
  • Room access logs and moderation tools.

Screenshots

Add screenshots or screen recordings here.

Checklist

  • Feature implemented
  • Tested locally
  • No breaking changes
  • Documentation updated
  • Ready for review

Copilot AI review requested due to automatic review settings June 9, 2026 19:03
@vercel

vercel Bot commented Jun 9, 2026

Copy link
Copy Markdown

@prajwallshetty is attempting to deploy a commit to the freemanjiang's projects Team on Vercel.

A member of the Team first needs to authorize it.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Introduces support for private rooms protected by a 6‑digit PIN, plus “unsend” for chat messages, with shared type updates and corresponding server/client wiring.

Changes:

  • Added room privacy/PIN primitives (HTTP endpoints, WS actions/events, RoomManager password hashing & validation).
  • Added chat message soft-delete (“unsend”) flow end-to-end (schema, handler, broadcast, client UI/state).
  • Added tests and updated persisted chat fixtures to include isDeleted.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
packages/shared/types/basic.ts Adds isDeleted to chat message schema/type.
packages/shared/types/WSRequest.ts Adds WS actions + schemas for setting room PIN and unsending messages.
packages/shared/types/WSBroadcast.ts Adds broadcast event schemas for message deletion and room privacy changes.
packages/shared/types/HTTPRequest.ts Extends room preview with isPrivate + adds /room-info and /check-password request/response schemas.
bun.lock Updates lockfile for added bcrypt dependencies.
apps/server/src/websocket/registry.ts Registers new WS actions and handlers.
apps/server/src/websocket/handlers/handleUnsendMessage.ts Implements UNSEND_MESSAGE handling + broadcast.
apps/server/src/websocket/handlers/handleSetRoomPassword.ts Implements SET_ROOM_PASSWORD handling + broadcast.
apps/server/src/routes/websocket.ts Adds WS upgrade-time PIN enforcement for private rooms.
apps/server/src/routes/roomInfo.ts Adds room privacy info endpoint for pre-WS UX.
apps/server/src/routes/checkPassword.ts Adds PIN verification endpoint.
apps/server/src/managers/RoomManager.ts Adds password hashing/storage + privacy state + unsend delegation.
apps/server/src/managers/GlobalManager.ts Hides private rooms from discovery.
apps/server/src/managers/ChatManager.ts Adds soft-delete implementation for chat messages.
apps/server/src/index.ts Wires new HTTP routes and async WS upgrade handler.
apps/server/src/tests/privateRoomAndUnsend.test.ts Adds tests for PIN + unsend behavior.
apps/server/src/tests/chatPersistence.test.ts Updates chat persistence test data with isDeleted.
apps/server/package.json Adds bcryptjs + types dependency.
apps/client/src/store/chat.tsx Adds isRoomPrivate + delete message action to client chat store.
apps/client/src/lib/api.ts Adds client API calls for /room-info and /check-password.
apps/client/src/components/room/WebSocketManager.tsx Sends password on WS connect; handles new ROOM_EVENT types.
apps/client/src/components/room/TopBar.tsx Adds admin UI to set/clear room PIN via WS.
apps/client/src/components/dashboard/right/Chat.tsx Adds private badge + per-message “unsend” UI and WS request.
apps/client/src/components/PasswordModal.tsx Adds pre-join PIN entry modal + verification flow.
apps/client/src/components/NewSyncer.tsx Adds pre-WS privacy check + password gate before connecting.
apps/client/src/components/Join.tsx Adds “create private room” UI + local PIN persistence on creation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +96 to 99
const password = typeof window !== "undefined" ? sessionStorage.getItem(`room-password-${roomId}`) : null;
const passwordParam = password ? `&password=${encodeURIComponent(password)}` : "";
const SOCKET_URL = `${getWsUrl()}?roomId=${roomId}&username=${username}&clientId=${clientId}${adminParam}${creatorParam}${passwordParam}`;
console.log("Creating new WS connection to", SOCKET_URL);
}
};

const handleKeyDown = (index: number, e: React.KeyboardEvent<HTMLInputElement>) => {
}
};

const handlePaste = (e: React.ClipboardEvent) => {
Comment on lines +40 to +61
// Pre-check if the room is private before connecting the WebSocket
useEffect(() => {
let cancelled = false;
fetchRoomInfo(roomId)
.then((info) => {
if (cancelled) return;
setIsPrivate(info.isPrivate);
if (!info.isPrivate) setPasswordVerified(true);
})
.catch(() => {
if (!cancelled) setPasswordVerified(true); // Fail open — let WS handle auth
})
.finally(() => {
if (!cancelled) setRoomInfoLoaded(true);
});
return () => {
cancelled = true;
};
}, [roomId]);

const showPasswordGate = roomInfoLoaded && isPrivate && !passwordVerified;
const canConnect = roomInfoLoaded && (!isPrivate || passwordVerified);
Comment on lines +84 to 103
const handleConfirmCreate = () => {
if (makePrivate) {
if (!/^\d{6}$/.test(createPin)) {
setCreateError("PIN must be exactly 6 digits");
return;
}
}

setIsCreating(true);

// Generate a random 6-digit room ID
const newRoomId = Math.floor(100000 + Math.random() * 900000).toString();

if (makePrivate) {
sessionStorage.setItem(`room-password-${newRoomId}`, createPin);
sessionStorage.setItem(`password-verified-${newRoomId}`, "true");
}

router.push(`/room/${newRoomId}`);
};
Comment on lines +49 to +52
const MessageDeletedSchema = z.object({
type: z.literal("MESSAGE_DELETED"),
messageId: z.number(),
});
Comment on lines +21 to +27
if (!deleted) {
// Message not found or the requester is not the sender — silently ignore
console.warn(
`Room ${ws.data.roomId}: UNSEND_MESSAGE ignored for messageId=${message.messageId} by ${ws.data.clientId}`
);
return;
}
Comment on lines +136 to +148
<button
onClick={() => {
setPinDialogOpen((o) => !o);
setPinError(null);
}}
title={isRoomPrivate ? "Change room PIN" : "Set room PIN"}
className={cn(
"flex items-center gap-1 text-xs transition-colors outline-none rounded-sm",
isRoomPrivate ? "text-primary hover:text-primary/80" : "text-neutral-400 hover:text-white"
)}
>
{isRoomPrivate ? <Lock className="h-3.5 w-3.5" /> : <LockOpen className="h-3.5 w-3.5" />}
</button>
Comment on lines +259 to +269
<motion.button
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.12 }}
onClick={() => handleUnsend(msg.id)}
title="Unsend message"
className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-neutral-800 text-neutral-500 hover:bg-red-900/40 hover:text-red-400 transition-colors duration-150"
>
<Trash2 className="h-3 w-3" />
</motion.button>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants