diff --git a/README.md b/README.md
index 78d1ea0..298168f 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,8 @@ database:
uri: ""
```
+Replace `` with your actual connection string including the database name and any required credentials (for example: `mongodb://localhost:27017/your_database_name`). Do not commit real connection strings or credentials to version control.
+
Without a valid MongoDB URI, the backend will fail to start.
---
diff --git a/backend/config/config.prod.sample.yml b/backend/config/config.prod.sample.yml
index 48a187c..7996758 100644
--- a/backend/config/config.prod.sample.yml
+++ b/backend/config/config.prod.sample.yml
@@ -2,9 +2,9 @@ server:
port: 1313 # The port number your backend server will run on
database:
- uri: "mongodb+srv://:@/"
- # Replace with your MongoDB Atlas connection string
- # Get this from your MongoDB Atlas dashboard after creating a cluster and database
+ uri: ""
+ # Replace with your MongoDB connection string (include DB name/credentials)
+ # Example: mongodb://localhost:27017/your_database_name or a MongoDB Atlas URI
gemini:
apiKey: ""
diff --git a/backend/config/config.prod.yml b/backend/config/config.prod.yml
new file mode 100644
index 0000000..e849dd5
--- /dev/null
+++ b/backend/config/config.prod.yml
@@ -0,0 +1,43 @@
+server:
+ port: 1313 # The port number your backend server will run on
+
+database:
+ uri: "mongodb://localhost:27017"
+ # Replace with your MongoDB Atlas connection string
+ # Get this from your MongoDB Atlas dashboard after creating a cluster and database
+
+gemini:
+ apiKey: ""
+ # API key for OpenAI / Gemini model access
+ # Obtain from your OpenRouter.ai or OpenAI account dashboard
+
+jwt:
+ secret: ""
+ # A secret string used to sign JWT tokens
+ # Generate a strong random string (e.g. use `openssl rand -hex 32`)
+
+ expiry: 1440
+ # Token expiry time in minutes (e.g. 1440 = 24 hours)
+
+smtp:
+ host: "smtp.gmail.com"
+ # SMTP server host for sending emails (example is Gmail SMTP)
+
+ port: 587
+ # SMTP server port (587 for TLS)
+
+ username: ""
+ # Email username (your email address)
+
+ password: ""
+ # Password for the email or app-specific password if 2FA is enabled
+
+ senderEmail: ""
+ # The 'from' email address used when sending mails
+
+ senderName: "DebateAI Team"
+
+googleOAuth:
+ clientID: ""
+ # Google OAuth Client ID for OAuth login
+ # Obtain from Google Cloud Console (APIs & Services > Credentials > OAuth 2.0 Client IDs)
diff --git a/frontend/src/Pages/Admin/AdminDashboard.tsx b/frontend/src/Pages/Admin/AdminDashboard.tsx
index 512dca1..be5698a 100644
--- a/frontend/src/Pages/Admin/AdminDashboard.tsx
+++ b/frontend/src/Pages/Admin/AdminDashboard.tsx
@@ -18,6 +18,7 @@ import {
type Admin,
} from "@/services/adminService";
import { Button } from "@/components/ui/button";
+import { getLocalString, getLocalJSON } from "@/utils/storage";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
@@ -364,15 +365,15 @@ export default function AdminDashboard() {
}, []);
useEffect(() => {
- const adminToken = localStorage.getItem("adminToken");
- const adminData = localStorage.getItem("admin");
+ const adminToken = getLocalString("adminToken");
+ const adminData = getLocalJSON("admin");
if (!adminToken || !adminData) {
navigate("/admin/login");
return;
}
setToken(adminToken);
- setAdmin(JSON.parse(adminData));
+ setAdmin(adminData);
loadData(adminToken);
}, [loadData, navigate]);
@@ -504,8 +505,8 @@ export default function AdminDashboard() {
No analytics data available yet.
{
- const currentToken = token || localStorage.getItem("adminToken");
+ onClick={() => {
+ const currentToken = token || getLocalString("adminToken");
if (currentToken) {
loadData(currentToken);
}
diff --git a/frontend/src/Pages/CommunityFeed.tsx b/frontend/src/Pages/CommunityFeed.tsx
index 17618c7..88d961b 100644
--- a/frontend/src/Pages/CommunityFeed.tsx
+++ b/frontend/src/Pages/CommunityFeed.tsx
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useCallback } from "react";
import { useUser } from "../hooks/useUser";
+import { getLocalString } from '@/utils/storage';
import CommentTree from "../components/CommentTree";
import ProfileHover from "../components/ProfileHover";
import { Button } from "../components/ui/button";
@@ -278,7 +279,7 @@ const CommunityFeed: React.FC = () => {
const fetchFeed = useCallback(async () => {
try {
setLoading(true);
- const token = localStorage.getItem("token");
+ const token = getLocalString("token");
const userId = currentUserId;
const response = await fetch(`${baseURL}/posts/feed`, {
method: "GET",
@@ -356,7 +357,7 @@ const CommunityFeed: React.FC = () => {
}, [fetchFeed]);
const handleFollow = async (userId: string, isFollowing: boolean) => {
- const token = localStorage.getItem("token");
+ const token = getLocalString("token");
if (!token) {
alert("Please log in to follow users");
return;
@@ -402,7 +403,7 @@ const CommunityFeed: React.FC = () => {
};
const handleDeletePost = async (postId: string) => {
- const token = localStorage.getItem("token");
+ const token = getLocalString("token");
if (!token) {
alert("Please log in to delete posts");
return;
diff --git a/frontend/src/Pages/DebateRoom.tsx b/frontend/src/Pages/DebateRoom.tsx
index 5581141..9088a42 100644
--- a/frontend/src/Pages/DebateRoom.tsx
+++ b/frontend/src/Pages/DebateRoom.tsx
@@ -1,5 +1,7 @@
import React, { useState, useEffect, useRef } from "react";
+import { safeParse } from '@/utils/safeParse';
import { useLocation } from "react-router-dom";
+import { getLocalString } from '@/utils/storage';
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { sendDebateMessage, judgeDebate } from "@/services/vsbot";
@@ -224,19 +226,38 @@ const DebateRoom: React.FC = () => {
const [user] = useAtom(userAtom);
const [state, setState] = useState(() => {
- const savedState = localStorage.getItem(debateKey);
- return savedState
- ? JSON.parse(savedState)
- : {
- messages: [],
- currentPhase: 0,
- phaseStep: 0,
- isBotTurn: false,
- userStance: "",
- botStance: "",
- timer: phases[0].time,
- isDebateEnded: false,
- };
+ const savedState = getLocalString(debateKey);
+ if (savedState) {
+ try {
+ const parsed = JSON.parse(savedState) as Partial | null;
+ if (parsed && typeof parsed === 'object') {
+ return {
+ messages: parsed.messages ?? [],
+ currentPhase: parsed.currentPhase ?? 0,
+ phaseStep: parsed.phaseStep ?? 0,
+ isBotTurn: parsed.isBotTurn ?? false,
+ userStance: parsed.userStance ?? "",
+ botStance: parsed.botStance ?? "",
+ timer: parsed.timer ?? phases[0].time,
+ isDebateEnded: parsed.isDebateEnded ?? false,
+ } as DebateState;
+ }
+ } catch (err) {
+ console.warn('Failed to parse saved debate state, clearing corrupt value', err);
+ try { localStorage.removeItem(debateKey); } catch {}
+ }
+ }
+
+ return {
+ messages: [],
+ currentPhase: 0,
+ phaseStep: 0,
+ isBotTurn: false,
+ userStance: "",
+ botStance: "",
+ timer: phases[0].time,
+ isDebateEnded: false,
+ };
});
const [finalInput, setFinalInput] = useState("");
const [interimInput, setInterimInput] = useState("");
@@ -561,21 +582,17 @@ const DebateRoom: React.FC = () => {
const jsonString = extractJSON(result);
console.log("Extracted JSON string:", jsonString);
- let judgment: JudgmentData;
- try {
- judgment = JSON.parse(jsonString);
- } catch (parseError) {
- console.error("JSON parse error:", parseError, "Trying to fix JSON...");
- // Try to fix common JSON issues
+ let judgment: JudgmentData | null = safeParse(jsonString, null);
+ if (!judgment) {
+ console.warn('Initial safe parse failed, attempting to fix JSON...');
const fixedJson = jsonString
.replace(/'/g, '"') // Replace single quotes with double quotes
.replace(/(\w+):/g, '"$1":') // Add quotes to keys
.replace(/,\s*}/g, '}') // Remove trailing commas
.replace(/,\s*]/g, ']'); // Remove trailing commas in arrays
- try {
- judgment = JSON.parse(fixedJson);
- } catch (e) {
- throw new Error(`Failed to parse JSON: ${e}`);
+ judgment = safeParse(fixedJson, null);
+ if (!judgment) {
+ throw new Error('Failed to parse judgment JSON after attempts');
}
}
diff --git a/frontend/src/Pages/Game.tsx b/frontend/src/Pages/Game.tsx
index 0b7d1f3..fec1171 100644
--- a/frontend/src/Pages/Game.tsx
+++ b/frontend/src/Pages/Game.tsx
@@ -282,9 +282,17 @@ const Game: React.FC = () => {
ws.onopen = () => console.log("WebSocket connection established");
ws.onmessage = (event) => {
try {
- handleWebSocketMessage(JSON.parse(event.data));
+ const raw = event.data;
+ let parsed: any = null;
+ try {
+ parsed = JSON.parse(typeof raw === 'string' ? raw : String(raw));
+ } catch (err) {
+ console.warn('Game: failed to parse WS message', err);
+ return;
+ }
+ handleWebSocketMessage(parsed);
} catch (error) {
- console.error("Failed to parse WebSocket message:", error);
+ console.error("Failed to handle WebSocket message:", error);
}
};
ws.onerror = (error) => console.error("WebSocket error:", error);
diff --git a/frontend/src/Pages/Leaderboard.tsx b/frontend/src/Pages/Leaderboard.tsx
index d4a4d4a..d3da725 100644
--- a/frontend/src/Pages/Leaderboard.tsx
+++ b/frontend/src/Pages/Leaderboard.tsx
@@ -1,6 +1,7 @@
"use client";
import React, { useState, useEffect, useRef } from "react";
+import { getLocalString } from "@/utils/storage";
import {
Table,
TableHeader,
@@ -93,7 +94,7 @@ const Leaderboard: React.FC = () => {
const loadData = async () => {
try {
setLoading(true);
- const token = localStorage.getItem("token");
+ const token = getLocalString("token");
if (!token) return;
// Try to fetch from gamification endpoint first, fallback to old endpoint
@@ -121,7 +122,7 @@ const Leaderboard: React.FC = () => {
// Set up WebSocket connection for live updates
useEffect(() => {
- const token = localStorage.getItem("token");
+ const token = getLocalString("token");
if (!token || !user) return;
// Clean up existing connection
@@ -172,11 +173,11 @@ const Leaderboard: React.FC = () => {
// Reload full leaderboard periodically to ensure accuracy
const reloadTimer = setTimeout(async () => {
try {
- const token = localStorage.getItem("token");
- if (token) {
- const data = await fetchGamificationLeaderboard(token);
- setDebaters(data.debaters);
- }
+ const token = getLocalString("token");
+ if (token) {
+ const data = await fetchGamificationLeaderboard(token);
+ setDebaters(data.debaters);
+ }
} catch (err) {
console.error("Error reloading leaderboard:", err);
}
diff --git a/frontend/src/Pages/OnlineDebateRoom.tsx b/frontend/src/Pages/OnlineDebateRoom.tsx
index 513ddd7..7279851 100644
--- a/frontend/src/Pages/OnlineDebateRoom.tsx
+++ b/frontend/src/Pages/OnlineDebateRoom.tsx
@@ -12,6 +12,7 @@ import JudgmentPopup from "@/components/JudgementPopup";
import SpeechTranscripts from "@/components/SpeechTranscripts";
import { useUser } from "@/hooks/useUser";
import { getAuthToken } from "@/utils/auth";
+import { safeParse } from "@/utils/safeParse";
import ReconnectingWebSocket from "reconnecting-websocket";
import { useAtom } from "jotai";
import {
@@ -555,10 +556,15 @@ const OnlineDebateRoom = (): JSX.Element => {
judgePollRef.current = null;
}
const jsonString = extractJSON(pollData.result);
- const judgment: JudgmentData = JSON.parse(jsonString);
- setJudgmentData(judgment);
- setPopup({ show: false, message: "" });
- setShowJudgment(true);
+ const judgment = safeParse(jsonString, null);
+ if (judgment) {
+ setJudgmentData(judgment);
+ setPopup({ show: false, message: "" });
+ setShowJudgment(true);
+ } else {
+ console.warn('Failed to parse judgment JSON from pollData.result');
+ setPopup({ show: false, message: 'Error parsing judgment result' });
+ }
submissionStartedRef.current = false;
}
} catch (error) {
@@ -653,7 +659,10 @@ const OnlineDebateRoom = (): JSX.Element => {
result.message === "Debate already judged"
) {
const jsonString = extractJSON(result.result);
- const judgment: JudgmentData = JSON.parse(jsonString);
+ const judgment = safeParse(jsonString, null);
+ if (!judgment) {
+ console.warn('Failed to parse judgment JSON from result.result');
+ }
return judgment;
}
} catch (error) {
@@ -715,7 +724,7 @@ const OnlineDebateRoom = (): JSX.Element => {
return rolePhases.reduce((acc, phase) => {
const storageKey = `${roomId}_${phase}_${role}`;
- const stored = storageKey ? localStorage.getItem(storageKey) : null;
+ const stored = storageKey ? getLocalString(storageKey) : null;
const fromState = speechTranscripts[phase] ?? "";
const combined =
(typeof fromState === "string" && fromState.trim().length > 0
@@ -1114,7 +1123,11 @@ const OnlineDebateRoom = (): JSX.Element => {
};
rws.onmessage = async (event) => {
- const data: WSMessage = JSON.parse(event.data);
+ const data = safeParse(event.data, null);
+ if (!data) {
+ console.warn('Received invalid WS message in OnlineDebateRoom');
+ return;
+ }
switch (data.type) {
case "topicChange":
if (data.topic !== undefined) setTopic(data.topic);
diff --git a/frontend/src/Pages/StrengthenArgument.tsx b/frontend/src/Pages/StrengthenArgument.tsx
index 640edf7..87a4f09 100644
--- a/frontend/src/Pages/StrengthenArgument.tsx
+++ b/frontend/src/Pages/StrengthenArgument.tsx
@@ -33,7 +33,7 @@ interface WeakStatement {
}
interface Evaluation {
- pointsEarned: number;
+ score: number;
feedback: string;
}
@@ -125,8 +125,13 @@ const StrengthenArgument: React.FC = () => {
if (!response.ok) {
throw new Error(`Server error (Status: ${response.status}): ${text || "Unknown error"}`);
}
- const data: WeakStatement = JSON.parse(text);
- if (!data.id || !data.text || !data.topic || !data.stance) {
+ let data: WeakStatement | null = null;
+ try {
+ data = JSON.parse(text) as WeakStatement;
+ } catch (err) {
+ console.warn('Failed to parse weak statement response', err);
+ }
+ if (!data || !data.id || !data.text || !data.topic || !data.stance) {
throw new Error("Invalid response format: missing fields");
}
setWeakStatement(data);
@@ -170,9 +175,20 @@ const StrengthenArgument: React.FC = () => {
if (!response.ok) {
throw new Error(`Server error (Status: ${response.status}): ${text || "Unknown error"}`);
}
- const data: Evaluation = JSON.parse(text);
+ let data: Evaluation | null = null;
+ try {
+ data = JSON.parse(text) as Evaluation;
+ } catch (err) {
+ console.warn("Failed to parse evaluation response", err);
+ }
+
+ // Validate parsed response
+ if (!data || typeof data.score !== "number" || !data.feedback) {
+ throw new Error("Invalid evaluation result: missing score or feedback");
+ }
+
setFeedback(data.feedback);
- setScore(data.pointsEarned);
+ setScore(data.score);
setShowModal(true);
setCurrentStep(3);
} catch (err: any) {
diff --git a/frontend/src/Pages/TeamDebateRoom.tsx b/frontend/src/Pages/TeamDebateRoom.tsx
index 048853a..08a35c1 100644
--- a/frontend/src/Pages/TeamDebateRoom.tsx
+++ b/frontend/src/Pages/TeamDebateRoom.tsx
@@ -8,6 +8,8 @@ import { Button } from "@/components/ui/button";
import JudgmentPopup from "@/components/JudgementPopup";
import SpeechTranscripts from "@/components/SpeechTranscripts";
import { getAuthToken } from "@/utils/auth";
+import { safeParse } from "@/utils/safeParse";
+import { getLocalString } from '@/utils/storage';
// Define debate phases as an enum (same as OnlineDebateRoom)
enum DebatePhase {
@@ -732,7 +734,11 @@ const TeamDebateRoom: React.FC = () => {
};
ws.onmessage = async (event) => {
- const data: WSMessage = JSON.parse(event.data);
+ const data = safeParse(event.data, null);
+ if (!data) {
+ console.warn("Received invalid team-debate WS message");
+ return;
+ }
console.log("Received WebSocket message:", data);
const amTeam1 = isTeam1Ref.current;
@@ -1446,7 +1452,7 @@ const TeamDebateRoom: React.FC = () => {
phasesForRole.forEach((phase) => {
const transcript =
- localStorage.getItem(`${debateId}_${phase}_${localRole}`) ||
+ getLocalString(`${debateId}_${phase}_${localRole}`) ||
speechTranscripts[phase] ||
"No response";
debateTranscripts[phase] = transcript;
@@ -1478,10 +1484,15 @@ const TeamDebateRoom: React.FC = () => {
const result = await response.json();
if (result.message === "Debate judged" || result.message === "Debate already judged") {
const jsonString = extractJSON(result.result);
- const judgment: JudgmentData = JSON.parse(jsonString);
- setJudgmentData(judgment);
- setPopup({ show: false, message: "" });
- setShowJudgment(true);
+ const judgment = safeParse(jsonString, null);
+ if (judgment) {
+ setJudgmentData(judgment);
+ setPopup({ show: false, message: "" });
+ setShowJudgment(true);
+ } else {
+ console.warn('Failed to parse judgment JSON');
+ setPopup({ show: false, message: 'Error parsing judgment result' });
+ }
}
}
} catch (error) {
diff --git a/frontend/src/Pages/ViewDebate.tsx b/frontend/src/Pages/ViewDebate.tsx
index b326d85..a08cf5e 100644
--- a/frontend/src/Pages/ViewDebate.tsx
+++ b/frontend/src/Pages/ViewDebate.tsx
@@ -1,4 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
+import { safeParse } from "@/utils/safeParse";
import { useParams, useNavigate } from "react-router-dom";
import { useAtom } from "jotai";
import { useDebateWS } from "../hooks/useDebateWS";
@@ -274,7 +275,11 @@ export const ViewDebate: React.FC = () => {
ws.onmessage = async (event) => {
try {
- const data = JSON.parse(event.data);
+ const data: any = safeParse(event.data, null);
+ if (!data) {
+ console.warn("ViewDebate: failed to parse WS message", event.data);
+ return;
+ }
if (data.type === "roomParticipants" && data.roomParticipants) {
const roomParticipants =
diff --git a/frontend/src/components/ChatRoom.tsx b/frontend/src/components/ChatRoom.tsx
index fb834cf..e023bf1 100644
--- a/frontend/src/components/ChatRoom.tsx
+++ b/frontend/src/components/ChatRoom.tsx
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react';
+import { safeParse } from '@/utils/safeParse';
import { useParams } from 'react-router-dom';
import clsx from 'clsx';
@@ -58,7 +59,8 @@ const ChatRoom = () => {
};
wsRef.current.onmessage = (event: MessageEvent) => {
- const data = JSON.parse(event.data);
+ const data = safeParse(event.data, null);
+ if (!data) return;
switch (data.type) {
case 'chatMessage':
setMessages((prev) => [
diff --git a/frontend/src/components/Matchmaking.tsx b/frontend/src/components/Matchmaking.tsx
index aa7f4b5..bc59f3f 100644
--- a/frontend/src/components/Matchmaking.tsx
+++ b/frontend/src/components/Matchmaking.tsx
@@ -1,4 +1,5 @@
import React, { useEffect, useState, useRef } from 'react';
+import { safeParse } from '@/utils/safeParse';
import { useNavigate } from 'react-router-dom';
import Avatar from 'react-avatar';
import { useUser } from '../hooks/useUser';
@@ -69,14 +70,15 @@ const Matchmaking: React.FC = () => {
ws.onmessage = (event) => {
try {
- const message: MatchmakingMessage = JSON.parse(event.data);
+ const message = safeParse(event.data, null);
+ if (!message) return;
switch (message.type) {
case 'pool_update':
if (message.pool) {
const poolData: MatchmakingPool[] = Array.isArray(message.pool)
? message.pool
- : JSON.parse(message.pool as string);
+ : safeParse(message.pool as string, []);
setPool(poolData);
// Check if current user is in pool (only if they've started matchmaking)
diff --git a/frontend/src/components/TeamChatSidebar.tsx b/frontend/src/components/TeamChatSidebar.tsx
index ff0aa4e..da28d01 100644
--- a/frontend/src/components/TeamChatSidebar.tsx
+++ b/frontend/src/components/TeamChatSidebar.tsx
@@ -4,6 +4,7 @@ import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Send, Users } from 'lucide-react';
+import { safeParse } from '@/utils/safeParse';
interface ChatMessage {
id: string;
@@ -58,7 +59,8 @@ const TeamChatSidebar: React.FC = ({
}
ws.onmessage = (event) => {
- const data = JSON.parse(event.data);
+ const data = safeParse(event.data, null);
+ if (!data) return;
if (data.type === 'teamChatMessage') {
const newChatMessage: ChatMessage = {
diff --git a/frontend/src/context/theme-provider.tsx b/frontend/src/context/theme-provider.tsx
index 56f8ecf..2838c88 100644
--- a/frontend/src/context/theme-provider.tsx
+++ b/frontend/src/context/theme-provider.tsx
@@ -1,4 +1,5 @@
import React, { useEffect, useState } from "react";
+import { getLocalString, setLocalString } from '@/utils/storage';
export enum ThemeOptions {
Light,
@@ -27,7 +28,7 @@ function getInitialTheme() {
//get theme to browser default
let newTheme: ThemeOptions;
- let systemThemeCodeStr = localStorage.getItem("Theme");
+ let systemThemeCodeStr = getLocalString("Theme");
if (systemThemeCodeStr == null) {
let defaultBrowserTheme = window.matchMedia("(prefers-color-scheme: light)").matches ? ThemeOptions.Light : ThemeOptions.Dark;
newTheme = defaultBrowserTheme;
@@ -55,7 +56,7 @@ export function ThemeProvider({ children }: { children: any }): any {
bodyElement.classList.remove("dark", "contrast");
const className = ThemeOptions[theme].toLowerCase(); // "light", "dark", or "contrast"
bodyElement.classList.add(className);
- localStorage.setItem("Theme", String(theme));
+ setLocalString("Theme", String(theme));
}, [theme])
function toggleTheme() {
const enumCount = Object.values(ThemeOptions).filter(x => typeof x === "number").length;
diff --git a/frontend/src/hooks/useDebateWS.ts b/frontend/src/hooks/useDebateWS.ts
index 64657fb..bb38c88 100644
--- a/frontend/src/hooks/useDebateWS.ts
+++ b/frontend/src/hooks/useDebateWS.ts
@@ -13,6 +13,8 @@ import {
PollInfo,
} from '../atoms/debateAtoms';
import ReconnectingWebSocket from 'reconnecting-websocket';
+import { getLocalString, setLocalString } from '@/utils/storage';
+import { safeParse } from '@/utils/safeParse';
interface Event {
type: string;
@@ -75,10 +77,10 @@ export const useDebateWS = (debateId: string | null) => {
}
}
- let spectatorId = localStorage.getItem('spectatorId');
+ let spectatorId = getLocalString('spectatorId');
if (!spectatorId) {
spectatorId = crypto.randomUUID();
- localStorage.setItem('spectatorId', spectatorId);
+ setLocalString('spectatorId', spectatorId);
}
const wsUrl = `${protocol}//${host}/ws/debate/${debateId}${
@@ -100,8 +102,7 @@ export const useDebateWS = (debateId: string | null) => {
rws.onopen = () => {
setWsStatus('connected');
- const spectatorHashValue =
- spectatorHash || localStorage.getItem('spectatorHash') || '';
+ const spectatorHashValue = spectatorHash || getLocalString('spectatorHash') || '';
const joinMessage = {
type: 'join',
payload: {
@@ -113,7 +114,11 @@ export const useDebateWS = (debateId: string | null) => {
rws.onmessage = (event) => {
try {
- const eventData: Event = JSON.parse(event.data);
+ const eventData = safeParse(event.data, null);
+ if (!eventData) {
+ console.warn('useDebateWS: failed to parse event data');
+ return;
+ }
if (eventData.type !== 'poll_snapshot' && eventData.timestamp) {
setLastEventId(String(eventData.timestamp));
diff --git a/frontend/src/hooks/useUser.ts b/frontend/src/hooks/useUser.ts
index a9c905c..0e3ff9d 100644
--- a/frontend/src/hooks/useUser.ts
+++ b/frontend/src/hooks/useUser.ts
@@ -2,6 +2,7 @@ import { useEffect, useContext } from "react";
import { useAtom } from "jotai";
import { userAtom } from "../state/userAtom";
import { AuthContext } from "../context/authContext";
+import { getLocalString, getLocalJSON, setLocalJSON } from '@/utils/storage';
const USER_CACHE_KEY = "userProfile";
const DEFAULT_AVATAR = "https://avatar.iran.liara.run/public/10";
@@ -16,14 +17,14 @@ export const useUser = () => {
// Hydrate from localStorage if available
useEffect(() => {
if (!user) {
- const cachedUser = localStorage.getItem(USER_CACHE_KEY);
+ const cachedUser = getLocalString(USER_CACHE_KEY);
if (cachedUser) {
try {
const parsedUser = JSON.parse(cachedUser);
setUser(parsedUser);
} catch (error) {
console.error("Failed to parse cached user profile:", error);
- localStorage.removeItem(USER_CACHE_KEY);
+ try { localStorage.removeItem(USER_CACHE_KEY); } catch {}
}
}
}
@@ -87,7 +88,7 @@ export const useUser = () => {
};
setUser(normalizedUser);
- localStorage.setItem(USER_CACHE_KEY, JSON.stringify(normalizedUser));
+ setLocalJSON(USER_CACHE_KEY, normalizedUser);
} catch (error) {
console.error("Failed to fetch user data:", error);
}
@@ -106,7 +107,7 @@ export const useUser = () => {
setUser,
isLoading:
authContext?.loading ||
- (!user && !!(authContext?.token || localStorage.getItem("token"))),
- isAuthenticated: !!(authContext?.token || localStorage.getItem("token")),
+ (!user && !!(authContext?.token || getLocalString("token"))),
+ isAuthenticated: !!(authContext?.token || getLocalString("token")),
};
};
diff --git a/frontend/src/services/gamificationService.ts b/frontend/src/services/gamificationService.ts
index 16c4a36..8bb79ad 100644
--- a/frontend/src/services/gamificationService.ts
+++ b/frontend/src/services/gamificationService.ts
@@ -108,10 +108,23 @@ export const createGamificationWebSocket = (
ws.onmessage = (event) => {
try {
- const data: GamificationEvent = JSON.parse(event.data);
- onMessage(data);
+ // parse using shared safeParse utility
+ // import safeParse lazily to avoid import cycles in some builds
+ // (top-level import kept minimal) -- but here we import directly
+ // to use the utility consistently.
+ // NOTE: safeParse returns null on failure
+ // (ensures consistent parsing behavior across the app)
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const { safeParse } = require("@/utils/safeParse");
+ const raw = event.data;
+ const parsed: GamificationEvent | null = safeParse(raw, null);
+ if (!parsed) {
+ console.warn("Failed to parse gamification event, ignoring", raw);
+ return;
+ }
+ onMessage(parsed);
} catch (error) {
- console.error("Error parsing gamification event:", error);
+ console.error("Error handling gamification event:", error);
}
};
diff --git a/frontend/src/services/teamDebateService.ts b/frontend/src/services/teamDebateService.ts
index 997c97b..f2b1db3 100644
--- a/frontend/src/services/teamDebateService.ts
+++ b/frontend/src/services/teamDebateService.ts
@@ -5,8 +5,10 @@ const API_BASE_URL =
(import.meta.env.VITE_BASE_URL as string | undefined)?.replace(/\/$/, "") ??
window.location.origin;
+import { getLocalString } from '@/utils/storage';
+
function getAuthToken(): string {
- return localStorage.getItem("token") || "";
+ return getLocalString('token') || '';
}
export interface TeamDebateMember {
diff --git a/frontend/src/utils/auth.ts b/frontend/src/utils/auth.ts
index 2805953..9c2c3e6 100644
--- a/frontend/src/utils/auth.ts
+++ b/frontend/src/utils/auth.ts
@@ -1,11 +1,15 @@
+import { setLocalString, getLocalString, setLocalJSON } from './storage';
+
export const setAuthToken = (token: string) => {
- localStorage.setItem("token", token);
- };
-
- export const getAuthToken = (): string | null => {
- return localStorage.getItem("token");
- };
-
- export const clearAuthToken = () => {
- localStorage.removeItem("token");
- };
\ No newline at end of file
+ setLocalString('token', token);
+};
+
+export const getAuthToken = (): string | null => {
+ return getLocalString('token');
+};
+
+export const clearAuthToken = () => {
+ try {
+ localStorage.removeItem('token');
+ } catch {}
+};
\ No newline at end of file
diff --git a/frontend/src/utils/safeParse.ts b/frontend/src/utils/safeParse.ts
new file mode 100644
index 0000000..9b7ed1b
--- /dev/null
+++ b/frontend/src/utils/safeParse.ts
@@ -0,0 +1,13 @@
+export const safeParse = (raw: unknown, fallback: T | null = null): T | null => {
+ if (raw === null || raw === undefined) return fallback;
+ if (typeof raw === 'object') return (raw as unknown) as T;
+ if (typeof raw !== 'string') return fallback;
+ try {
+ return JSON.parse(raw) as T;
+ } catch (err) {
+ console.warn('safeParse failed to parse JSON:', err);
+ return fallback;
+ }
+};
+
+export default safeParse;
diff --git a/frontend/src/utils/storage.ts b/frontend/src/utils/storage.ts
new file mode 100644
index 0000000..27425cd
--- /dev/null
+++ b/frontend/src/utils/storage.ts
@@ -0,0 +1,44 @@
+export const getLocalString = (key: string, fallback: string | null = null): string | null => {
+ if (typeof window === 'undefined') return fallback;
+ try {
+ const v = localStorage.getItem(key);
+ return v === null ? fallback : v;
+ } catch (err) {
+ console.error('Failed to read from localStorage', key, err);
+ return fallback;
+ }
+};
+
+export const getLocalJSON = (key: string, fallback: T | null = null): T | null => {
+ const raw = getLocalString(key, null);
+ if (raw === null) return fallback;
+ try {
+ return JSON.parse(raw) as T;
+ } catch (err) {
+ console.warn(`Failed to parse JSON from localStorage key=${key}`, err);
+ try {
+ localStorage.removeItem(key);
+ } catch (e) {
+ /* ignore */
+ }
+ return fallback;
+ }
+};
+
+export const setLocalJSON = (key: string, value: unknown) => {
+ if (typeof window === 'undefined') return;
+ try {
+ localStorage.setItem(key, JSON.stringify(value));
+ } catch (err) {
+ console.error('Failed to write JSON to localStorage', key, err);
+ }
+};
+
+export const setLocalString = (key: string, value: string) => {
+ if (typeof window === 'undefined') return;
+ try {
+ localStorage.setItem(key, value);
+ } catch (err) {
+ console.error('Failed to write to localStorage', key, err);
+ }
+};