From 7f48a704733bcf977860e7fb9b3d198994097800 Mon Sep 17 00:00:00 2001 From: Arjun S Date: Sun, 3 Aug 2025 00:51:07 +0530 Subject: [PATCH] clerk auth fixed --- .gitignore | 1 + ...f08e2ccc5117b4faec0dae467907fc9647b.sqlite | Bin 4096 -> 0 bytes ...2ccc5117b4faec0dae467907fc9647b.sqlite-shm | Bin 32768 -> 0 bytes ...2ccc5117b4faec0dae467907fc9647b.sqlite-wal | Bin 8272 -> 0 bytes package-lock.json | 48 +++++++ package.json | 1 + party/index.ts | 122 ++++++++++++------ partykit.json | 3 +- src/App.tsx | 13 +- src/hooks/usePartyKit.ts | 47 ++++--- 10 files changed, 171 insertions(+), 64 deletions(-) delete mode 100644 .partykit/state/party/-PartyKitDurable/9ac1723a854a77d3096a9717ff406f08e2ccc5117b4faec0dae467907fc9647b.sqlite delete mode 100644 .partykit/state/party/-PartyKitDurable/9ac1723a854a77d3096a9717ff406f08e2ccc5117b4faec0dae467907fc9647b.sqlite-shm delete mode 100644 .partykit/state/party/-PartyKitDurable/9ac1723a854a77d3096a9717ff406f08e2ccc5117b4faec0dae467907fc9647b.sqlite-wal diff --git a/.gitignore b/.gitignore index 7ceb59f..6d96f69 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ dist-ssr *.sln *.sw? .env +.partykit/ diff --git a/.partykit/state/party/-PartyKitDurable/9ac1723a854a77d3096a9717ff406f08e2ccc5117b4faec0dae467907fc9647b.sqlite b/.partykit/state/party/-PartyKitDurable/9ac1723a854a77d3096a9717ff406f08e2ccc5117b4faec0dae467907fc9647b.sqlite deleted file mode 100644 index 4ebf78cf96088c7148cb47caf804cc036de75a34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|@t|AVoG{WY8;GzzfnYK(-m98b?E5 nGz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!nC=3ArfDQ*E diff --git a/.partykit/state/party/-PartyKitDurable/9ac1723a854a77d3096a9717ff406f08e2ccc5117b4faec0dae467907fc9647b.sqlite-shm b/.partykit/state/party/-PartyKitDurable/9ac1723a854a77d3096a9717ff406f08e2ccc5117b4faec0dae467907fc9647b.sqlite-shm deleted file mode 100644 index 79b32e916d7e3d5c0032d7c98efe833cec3fa89e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI)u?fOJ6b9ha2~x%(ELRpcD3GIcd6+f`|`*4xp(w@k20>ud6@m3 zo9lK01PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB=Ch2*j@wLSUK#@h6KAAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PJ_>zz4o8 BCQ|?a diff --git a/.partykit/state/party/-PartyKitDurable/9ac1723a854a77d3096a9717ff406f08e2ccc5117b4faec0dae467907fc9647b.sqlite-wal b/.partykit/state/party/-PartyKitDurable/9ac1723a854a77d3096a9717ff406f08e2ccc5117b4faec0dae467907fc9647b.sqlite-wal deleted file mode 100644 index 0e4d3509703986f9bb8e5a8fcb85af3790ea6e70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8272 zcmXr7XKP~6eI&uaAiw|uZj*XyeRRJQVpuwd82HB~V3L&l$AqoLOp1zJjkqX|fkvdSh zvc#OyR0Ss=e=6.9.0" } }, + "node_modules/@clerk/backend": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-2.6.2.tgz", + "integrity": "sha512-IUTjLmA1QkqoJnB97S8Ay/oeFR1QtBxxzi9V2J8zncGdUUpAHRp9PfbUwe203VEZuoDD8n6PGfK4oiiq5CoKhQ==", + "license": "MIT", + "dependencies": { + "@clerk/shared": "^3.17.0", + "@clerk/types": "^4.72.0", + "cookie": "1.0.2", + "standardwebhooks": "^1.0.0", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=18.17.0" + } + }, + "node_modules/@clerk/backend/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@clerk/clerk-react": { "version": "5.38.1", "resolved": "https://registry.npmjs.org/@clerk/clerk-react/-/clerk-react-5.38.1.tgz", @@ -1358,6 +1384,12 @@ "win32" ] }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2568,6 +2600,12 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -4041,6 +4079,16 @@ "get-source": "^2.0.12" } }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/std-env": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", diff --git a/package.json b/package.json index 854d7d9..28a89d4 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@clerk/backend": "^2.6.2", "@clerk/clerk-react": "^5.38.1", "lucide-react": "^0.344.0", "partykit": "^0.0.115", diff --git a/party/index.ts b/party/index.ts index 183081a..bc724cd 100644 --- a/party/index.ts +++ b/party/index.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import type * as Party from 'partykit/server'; +import { createClerkClient, verifyToken } from '@clerk/backend'; interface GameState { totalClicks: number; @@ -12,22 +13,23 @@ interface GameState { currentClickImage: string; } -interface ClickMessage { - type: 'click'; - userId: string; - userName: string; +interface AuthenticatedMessage { + token: string; + userId: string; // Expected userId from client + userName: string; // Expected userName from client } -interface PurchaseUpgradeMessage { +interface ClickMessage extends AuthenticatedMessage { + type: 'click'; +} + +interface PurchaseUpgradeMessage extends AuthenticatedMessage { type: 'purchase-upgrade'; - userId: string; upgradeId: string; } -interface UserJoinMessage { +interface UserJoinMessage extends AuthenticatedMessage { type: 'user-join'; - userId: string; - userName: string; } type Message = ClickMessage | PurchaseUpgradeMessage | UserJoinMessage; @@ -50,7 +52,13 @@ const MILESTONES = [ ]; export default class GameServer implements Party.Server { - constructor(readonly party: Party.Party) { } + clerkClient: ReturnType; + + constructor(readonly party: Party.Party) { + this.clerkClient = createClerkClient({ + secretKey: party.env.CLERK_SECRET_KEY as string, + }); + } gameState: GameState = { totalClicks: 0, @@ -68,68 +76,104 @@ export default class GameServer implements Party.Server { autoClickInterval?: NodeJS.Timeout; - onConnect(conn: Party.Connection, _ctx: Party.ConnectionContext) { + async onConnect(conn: Party.Connection, _ctx: Party.ConnectionContext) { conn.send(JSON.stringify({ type: 'game-state', state: this.gameState })); } - onMessage(message: string, _sender: Party.Connection) { + async onMessage(message: string, sender: Party.Connection) { const data = JSON.parse(message) as Message; - switch (data.type) { - case 'user-join': - this.handleUserJoin(data); - break; - case 'click': - this.handleClick(data); - break; - case 'purchase-upgrade': - this.handlePurchaseUpgrade(data); - break; + // Verify the Clerk token for authenticated messages + if ('token' in data && data.token) { + try { + const sessionClaims = await verifyToken(data.token, { + jwtKey: this.party.env.CLERK_JWT_KEY as string, + }); + const authenticatedUserId = sessionClaims.sub; + + // Ensure the userId from the client matches the authenticated userId + if (authenticatedUserId !== data.userId) { + console.warn(`User ID mismatch: Client sent ${data.userId}, but token is for ${authenticatedUserId}`); + sender.close(); // Close connection for potential tampering + return; + } + + // Update user info on the server with authenticated data + if (!this.gameState.users[authenticatedUserId]) { + this.gameState.users[authenticatedUserId] = { + name: data.userName, // Use the name provided by the client, which comes from Clerk + clicks: 0, + lastSeen: Date.now() + }; + } + this.gameState.users[authenticatedUserId].lastSeen = Date.now(); + this.gameState.users[authenticatedUserId].name = data.userName; // Update name in case it changed + + switch (data.type) { + case 'user-join': + // User join is handled by the token verification and user state update above + break; + case 'click': + this.handleClick(data, authenticatedUserId); + break; + case 'purchase-upgrade': + this.handlePurchaseUpgrade(data, authenticatedUserId); + break; + } + } catch (error) { + console.error('Clerk token verification failed:', error); + sender.close(); // Close connection if token is invalid + return; + } + } else { + // For messages without a token (e.g., initial connection before token is sent, or unauthenticated actions) + // For this game, we only allow authenticated actions. + console.warn('Received unauthenticated message, closing connection.'); + sender.close(); + return; } this.broadcast(); } + // handleUserJoin is now largely integrated into onMessage's token verification + // Keeping it for now, but it might become redundant or simplified. handleUserJoin(data: UserJoinMessage) { - if (!this.gameState.users[data.userId]) { - this.gameState.users[data.userId] = { - name: data.userName, - clicks: 0, - lastSeen: Date.now() - }; - } - this.gameState.users[data.userId].lastSeen = Date.now(); + // This function is now mostly handled by the onMessage token verification logic + // It ensures the user exists in gameState.users and updates lastSeen/name. + // No explicit action needed here beyond what onMessage does. } - handleClick(data: ClickMessage) { + handleClick(data: ClickMessage, authenticatedUserId: string) { const clickValue = this.gameState.clickMultiplier; this.gameState.totalClicks += clickValue; - if (!this.gameState.users[data.userId]) { - this.gameState.users[data.userId] = { - name: data.userName, + // Ensure user exists (should be handled by onMessage's token verification) + if (!this.gameState.users[authenticatedUserId]) { + this.gameState.users[authenticatedUserId] = { + name: data.userName, // Use the name from the client, which comes from Clerk clicks: 0, lastSeen: Date.now() }; } - this.gameState.users[data.userId].clicks += clickValue; - this.gameState.users[data.userId].lastSeen = Date.now(); + this.gameState.users[authenticatedUserId].clicks += clickValue; + this.gameState.users[authenticatedUserId].lastSeen = Date.now(); this.checkMilestones(); } - handlePurchaseUpgrade(data: PurchaseUpgradeMessage) { + handlePurchaseUpgrade(data: PurchaseUpgradeMessage, authenticatedUserId: string) { const upgrade = UPGRADES[data.upgradeId as keyof typeof UPGRADES]; const currentUpgrade = this.gameState.upgrades[data.upgradeId]; if (!upgrade || !currentUpgrade) return; - const userClicks = this.gameState.users[data.userId]?.clicks || 0; + const userClicks = this.gameState.users[authenticatedUserId]?.clicks || 0; if (userClicks >= currentUpgrade.cost) { - this.gameState.users[data.userId].clicks -= currentUpgrade.cost; + this.gameState.users[authenticatedUserId].clicks -= currentUpgrade.cost; currentUpgrade.owned += 1; currentUpgrade.cost = Math.floor(upgrade.baseCost * Math.pow(upgrade.multiplier, currentUpgrade.owned)); diff --git a/partykit.json b/partykit.json index bce071a..469e1e9 100644 --- a/partykit.json +++ b/partykit.json @@ -1,4 +1,5 @@ { + "$schema": "https://www.partykit.io/schema.json", "name": "bozo-clicker", "main": "party/index.ts" -} \ No newline at end of file +} diff --git a/src/App.tsx b/src/App.tsx index 0448150..1202341 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,8 +8,10 @@ import { Leaderboard } from './components/Leaderboard'; import { Background } from './components/Background'; import { MILESTONES } from './config/milestones'; import { SignedIn, SignedOut, SignInButton, UserButton } from '@clerk/clerk-react'; +import { useAuth } from '@clerk/clerk-react'; function App() { + const { isSignedIn, isLoaded } = useAuth(); const { gameState, sendClick, purchaseUpgrade, userId } = usePartyKit(); const [celebrationMessage, setCelebrationMessage] = useState(null); const [previousMilestones, setPreviousMilestones] = useState>({}); @@ -18,8 +20,8 @@ function App() { if (gameState) { // Check for new milestones const newMilestones = Object.keys(gameState.milestones).filter( - (milestoneId) => - gameState.milestones[milestoneId] && + (milestoneId) => + gameState.milestones[milestoneId] && !previousMilestones[milestoneId] ); @@ -35,7 +37,7 @@ function App() { } }, [gameState, previousMilestones]); - if (!gameState) { + if (!isLoaded || !gameState) { return (
@@ -45,7 +47,8 @@ function App() { ); } - const userClicks = gameState.users[userId]?.clicks || 0; + // Only calculate userClicks if userId is defined (i.e., user is signed in) + const userClicks = isSignedIn && userId ? gameState.users[userId]?.clicks || 0 : 0; return (
@@ -99,7 +102,7 @@ function App() { {/* Right Column - Milestones and Leaderboard */}
- +
diff --git a/src/hooks/usePartyKit.ts b/src/hooks/usePartyKit.ts index d3562b4..6b58139 100644 --- a/src/hooks/usePartyKit.ts +++ b/src/hooks/usePartyKit.ts @@ -1,26 +1,31 @@ import { useState, useEffect, useCallback } from 'react'; import PartySocket from 'partysocket'; import { GameState } from '../types'; +import { useAuth, useUser } from '@clerk/clerk-react'; const PARTY_HOST = import.meta.env.DEV ? 'localhost:1999' : 'bozo-clicker.your-username.partykit.dev'; export function usePartyKit() { + const { getToken, isLoaded, isSignedIn } = useAuth(); + const { user } = useUser(); const [gameState, setGameState] = useState(null); const [socket, setSocket] = useState(null); - const [userId] = useState(() => Math.random().toString(36).substring(7)); - const [userName] = useState(() => `Bozo${Math.floor(Math.random() * 1000)}`); useEffect(() => { + if (!isLoaded || !isSignedIn || !user) return; + const ws = new PartySocket({ host: PARTY_HOST, room: 'bozo-clicker' }); - ws.onopen = () => { + ws.onopen = async () => { + const token = await getToken(); ws.send(JSON.stringify({ type: 'user-join', - userId, - userName + userId: user.id, + userName: user.username || user.fullName || user.emailAddresses[0].emailAddress, + token })); }; @@ -36,33 +41,37 @@ export function usePartyKit() { return () => { ws.close(); }; - }, [userId, userName]); + }, [isLoaded, isSignedIn, user, getToken]); - const sendClick = useCallback(() => { - if (socket) { + const sendClick = useCallback(async () => { + if (socket && isSignedIn && user) { + const token = await getToken(); socket.send(JSON.stringify({ type: 'click', - userId, - userName + userId: user.id, + userName: user.username || user.fullName || user.emailAddresses[0].emailAddress, + token })); } - }, [socket, userId, userName]); + }, [socket, isSignedIn, user, getToken]); - const purchaseUpgrade = useCallback((upgradeId: string) => { - if (socket) { + const purchaseUpgrade = useCallback(async (upgradeId: string) => { + if (socket && isSignedIn && user) { + const token = await getToken(); socket.send(JSON.stringify({ type: 'purchase-upgrade', - userId, - upgradeId + userId: user.id, + upgradeId, + token })); } - }, [socket, userId]); + }, [socket, isSignedIn, user, getToken]); return { gameState, sendClick, purchaseUpgrade, - userId, - userName + userId: user?.id, + userName: user?.username || user?.fullName || user?.emailAddresses[0]?.emailAddress }; -} \ No newline at end of file +}