Skip to content

Add anonymous doubt posting feature with UI and routing#92

Open
abinaya-manokaran wants to merge 17 commits into
durdana3105:mainfrom
abinaya-manokaran:feature/anonymous-doubts
Open

Add anonymous doubt posting feature with UI and routing#92
abinaya-manokaran wants to merge 17 commits into
durdana3105:mainfrom
abinaya-manokaran:feature/anonymous-doubts

Conversation

@abinaya-manokaran
Copy link
Copy Markdown

Anonymous Doubt Posting Feature

Implemented anonymous doubt posting system for Peer Learning platform.

Features:

  • Anonymous mode toggle
  • Subject tagging system
  • Upvote functionality
  • Reply threads structure
  • Clean responsive UI

Impact:

Helps students ask doubts without hesitation and improves engagement.

Closes #45

Abinaya M and others added 4 commits May 20, 2026 20:23
…osure

Resolves durdana3105#85

Any Vite environment variable prefixed with VITE_ is inlined into the
client-side JavaScript bundle at build time. VITE_OPENROUTER_API_KEY was
readable by any visitor who opened DevTools, inspected the network request
Authorization header, or searched the production build assets. This allows
credential theft, quota exhaustion, and unexpected billing.

Changes across four files:

src/backend/routers/chatRoutes.js
  Rewrote the route to use OpenRouter instead of the OpenAI direct endpoint.
  The OpenAI SDK supports OpenRouter via the baseURL option, so no new
  dependency is needed. The key is now read from process.env.OPENROUTER_API_KEY
  (no VITE_ prefix), which stays in the server process and is never sent to
  the browser. Added input validation to reject malformed message arrays
  before forwarding the request upstream.

src/components/Chatbot.jsx
  Removed the VITE_OPENROUTER_API_KEY constant and the direct fetch to
  openrouter.ai. The sendMessage function now posts to /api/chat (the local
  proxy route) with the message history and system prompt in the request body.
  The Authorization header is no longer present in any client-side request.

src/components/FloatingAI.tsx
  Same change as Chatbot.jsx. Removed the Authorization header containing
  import.meta.env.VITE_OPENROUTER_API_KEY and replaced the direct OpenRouter
  fetch with a call to /api/chat.

vite.config.ts
  Added a dev-server proxy rule that forwards /api requests to the Express
  backend on port 3001. This lets the frontend call /api/chat during
  development without CORS issues or hardcoded backend URLs.

.env.example
  Replaced VITE_OPENROUTER_API_KEY with OPENROUTER_API_KEY and added a
  comment explaining why the VITE_ prefix must not be used for private keys.
  Added SITE_URL for the HTTP-Referer header sent to OpenRouter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ken source

Resolves durdana3105#86

Three bugs prevented the password reset flow from working in any environment
other than a specific local setup. Two are straightforward hardcoded URL
fixes; the third is an architectural mismatch between how Supabase delivers
the reset token and how the component tried to read it.

Bug 1 -- hardcoded redirectTo in ForgotPassword.jsx
The redirectTo option passed to supabase.auth.resetPasswordForEmail was
hardcoded to http://localhost:5173/reset-password. Supabase includes this
URL verbatim in the reset email, so every email sent in production or
staging pointed users back to a developer machine. Fixed by replacing the
hardcoded string with window.location.origin so the URL is derived from
whatever host the app is running on at the time the request is made.

Bug 2 + Bug 3 -- token source mismatch in ResetPassword.jsx
The component read const { token } = useParams() and then called
http://localhost:5000/api/reset-password/${token}. Both halves of this were
wrong.

On the token source: supabase.auth.resetPasswordForEmail delivers the
recovery token inside the URL hash as #access_token=...&type=recovery, not
as a path segment. React Router useParams() only reads path segments, so
token was always undefined. Adding :token to the App.tsx route (as the
issue originally proposed) would not have helped because the token never
appears in the path.

The Supabase JS client already handles the hash automatically: when the
page loads and the hash contains type=recovery, the client establishes a
short-lived recovery session. Calling supabase.auth.updateUser({ password })
then uses that session to update the password. No manual token extraction
or forwarding is needed, and no custom backend endpoint is involved.

On the hardcoded API host: even if the path-based token approach were
correct, the fetch target http://localhost:5000/api/reset-password/${token}
would fail in every deployed environment.

Fixed by removing useParams and the fetch call entirely, importing the
Supabase client, and replacing the reset logic with supabase.auth.updateUser.
The component now works end-to-end using only the existing Supabase session
that the client sets up from the email link hash.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 23, 2026

Someone is attempting to deploy a commit to the durdana3105's projects Team on Vercel.

A member of the Team first needs to authorize it.

Your Name and others added 9 commits May 24, 2026 10:27
Resolves durdana3105#84

Two layered fixes for the privilege escalation in the admin panel:

Fix 1 -- client-side auth bypass in Admin.tsx
The useEffect unconditionally called setIsAdmin(true) for any authenticated
user, skipping role verification entirely. Replaced the synchronous flag
assignment with an async checkAdmin function that calls the existing
has_role security-definer RPC. The RPC is the canonical server-side check:
it cannot be manipulated from the client, unlike a direct user_roles query
whose result could be affected by RLS misconfiguration. Non-admins now
receive setIsAdmin(false) and the Access Denied screen. The loading state
is also correctly resolved in all code paths (previously it stayed true
forever when the user was null).

Fix 2 -- defense-in-depth at the database layer (migration)
Even after the client fix, a regular user could still call the Supabase
REST API directly to query the profiles table (profiles_select policy uses
USING(true)) and retrieve all names, emails, and session data without
going through the UI. Added admin_get_all_profiles(), a SECURITY DEFINER
function that enforces the admin role check inside the database before
returning any rows. fetchUsers now calls this RPC instead of the open
profiles query, so a direct API request by a non-admin returns an empty
result set rather than the full user list. Execution is restricted to
authenticated users via GRANT/REVOKE.

diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx
index 3debb79..f11abf8 100644
--- a/src/pages/Admin.tsx
+++ b/src/pages/Admin.tsx
@@ -1,7 +1,6 @@
 import { useEffect, useState } from "react";
 import { motion } from "framer-motion";
 import { Shield, Users, Calendar, Search } from "lucide-react";
-import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Badge } from "@/components/ui/badge";
 import { useAuth } from "@/contexts/useAuth";
@@ -32,84 +31,51 @@ const Admin = () => {
   const [loading, setLoading] = useState(true);
   const [activeTodayCount, setActiveTodayCount] = useState(0);

- useEffect(() => {
-  if (!user) return;
-
-  checkAdminRole();
-
-}, [user]);
-
-  const checkAdminRole = async () => {
-    try {
-      // Query user_roles table to check if user has admin role
-      const { data: adminRole, error } = await supabase
-        .from("user_roles")
-        .select("role")
-        .eq("user_id", user.id)
-        .eq("role", "admin")
-        .single();
-
-      if (error && error.code !== "PGRST116") {
-        // PGRST116 = no rows found, which is expected for non-admin users
-        console.error("Error checking admin role:", error);
+  useEffect(() => {
+    const checkAdmin = async () => {
+      if (!user) {
         setIsAdmin(false);
         setLoading(false);
         return;
       }

-      if (adminRole && adminRole.role === "admin") {
+      // Verify admin role server-side using the has_role security-definer function.
+      // Querying user_roles directly would rely on RLS side-effects for a security
+      // decision; the RPC call is explicit and cannot be bypassed by the client.
+      const { data, error } = await supabase.rpc("has_role", {
+        _user_id: user.id,
+        _role: "admin",
+      });
+
+      if (!error && data === true) {
         setIsAdmin(true);
         fetchUsers();
       } else {
         setIsAdmin(false);
         setLoading(false);
       }
-    } catch (err) {
-      console.error("Error checking admin role:", err);
-      setIsAdmin(false);
-      setLoading(false);
-    }
-  };
+    };
+
+    checkAdmin();
+  }, [user]);

   const fetchUsers = async () => {
-    try {
-      const { data } = await supabase
-        .from("profiles")
-        .select("id, name, email, skills, points, sessions_completed, created_at, last_active_at")
-        .order("created_at", { ascending: false });
-      if (data) {
-        setUsers(data);
-        // Calculate active users (active in the last 24 hours)
-        const activeCount = calculateActiveTodayCount(data);
-        setActiveTodayCount(activeCount);
-      }
-    } catch (err) {
-      console.error("Error fetching users:", err);
-      toast({
-        title: "Error",
-        description: "Failed to load users data",
-        variant: "destructive",
-      });
-    } finally {
-      setLoading(false);
+    // Call the admin_get_all_profiles RPC instead of querying profiles directly.
+    // The function is SECURITY DEFINER and enforces admin role server-side,
+    // so a non-admin who somehow bypasses the client check still gets no data.
+    const { data } = await supabase.rpc("admin_get_all_profiles");
+    if (data) {
+      setUsers(data);
+      // Calculate active users (active in the last 24 hours)
+      const activeCount = calculateActiveTodayCount(data);
+      setActiveTodayCount(activeCount);
     }
   };

   const calculateActiveTodayCount = (userList: UserProfile[]): number => {
     const now = new Date();
     const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
-
-    return userList.filter(u => {
-      if (!u.last_active_at) return false;
-      const lastActive = new Date(u.last_active_at);
-      return lastActive >= oneDayAgo;
-    }).length;
-  };

-  const calculateActiveTodayCount = (userList: UserProfile[]): number => {
-    const now = new Date();
-    const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
-
     return userList.filter(u => {
       if (!u.last_active_at) return false;
       const lastActive = new Date(u.last_active_at);
diff --git a/supabase/migrations/20260523_admin_profiles_rpc.sql b/supabase/migrations/20260523_admin_profiles_rpc.sql
new file mode 100644
index 0000000..6348976
--- /dev/null
+++ b/supabase/migrations/20260523_admin_profiles_rpc.sql
@@ -0,0 +1,44 @@
+-- Provide a server-side admin-only function for fetching all user profiles.
+-- Using SECURITY DEFINER lets the function bypass RLS and return all rows,
+-- but the explicit has_role check inside ensures only admins can retrieve data.
+-- This closes the direct Supabase API surface: a non-admin calling this RPC
+-- via PostgREST receives an empty array rather than all profile rows.
+
+CREATE OR REPLACE FUNCTION public.admin_get_all_profiles()
+RETURNS TABLE (
+  id UUID,
+  name TEXT,
+  email TEXT,
+  skills TEXT[],
+  points INTEGER,
+  sessions_completed INTEGER,
+  created_at TIMESTAMP WITH TIME ZONE,
+  last_active_at TIMESTAMP WITH TIME ZONE
+)
+LANGUAGE plpgsql
+SECURITY DEFINER
+SET search_path = public
+AS $$
+BEGIN
+  IF NOT public.has_role(auth.uid(), 'admin') THEN
+    RETURN;
+  END IF;
+
+  RETURN QUERY
+    SELECT
+      p.id,
+      p.name,
+      p.email,
+      p.skills,
+      p.points,
+      p.sessions_completed,
+      p.created_at,
+      p.last_active_at
+    FROM public.profiles p
+    ORDER BY p.created_at DESC;
+END;
+$$;
+
+-- Restrict execution to authenticated users only.
+REVOKE ALL ON FUNCTION public.admin_get_all_profiles() FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION public.admin_get_all_profiles() TO authenticated;
Enhanced README with demo video and improved documentation
…-broken-86

fix: repair broken password reset flow (localhost URLs and wrong token source)
…-key-exposure-85

fix: proxy OpenRouter requests through backend to prevent API key exposure
…e-escalation-84

fix: verify admin role server-side before granting access to admin panel
…or-dashboard

Build Mentor Analytics Dashboard
@durdana3105
Copy link
Copy Markdown
Owner

Anonymous Doubt Posting Feature

Implemented anonymous doubt posting system for Peer Learning platform.

Features:

  • Anonymous mode toggle
  • Subject tagging system
  • Upvote functionality
  • Reply threads structure
  • Clean responsive UI

Impact:

Helps students ask doubts without hesitation and improves engagement.

Closes #45

this pr have merge conflicts, resolve it and raise pr again

@abinaya-manokaran
Copy link
Copy Markdown
Author

@durdana3105 Merge conflicts resolved successfully ✅

  • Synced with latest upstream changes
  • Resolved conflicts in Admin.tsx and Landing.tsx
  • Verified anonymous doubt posting feature compatibility with latest codebase

PR is now ready for review and merge. Thank you!

@abinaya-manokaran
Copy link
Copy Markdown
Author

@durdana3105 Merge conflicts are resolved and the branch is updated ✅

The failing Vercel check appears to be due to deployment authorization for external contributor branches (not a code/build issue).

The feature is ready for review and merge when convenient. Thank you!

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.

Add Anonymous Doubt Posting

4 participants