Repository navigation
Refactor: Security + Performance optimisation #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 5 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
fa7b12c
Refactor: Security + Performance optimisation
Priyans00 74e25c1
Fix: Fix copilot suggestions
Priyans00 60c6338
Fix: Add centralised rate limiter
Priyans00 213f8cb
Fix: Fix copilot suggestion - 2
Priyans00 07577db
csp i did something
Priyans00 a3eb960
Fix: Fix Copilot suggestion-3
Priyans00 3ac7d55
Fix: Fix login
Priyans00 8126f8c
Fix: Fix login - 2
Priyans00 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,60 +1,127 @@ | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { createClient } from "@/lib/supabase/server"; | ||
|
|
||
| // Cache for active chats | ||
| const chatCache = new Map<string, { data: unknown[]; timestamp: number }>(); | ||
| const CACHE_TTL = 5 * 60 * 1000; // 5 minutes | ||
|
|
||
| function validateUUID(uuid: string): boolean { | ||
| const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; | ||
| return uuidRegex.test(uuid); | ||
| } | ||
|
|
||
| export async function GET(req: NextRequest) { | ||
| const supabase = await createClient(); | ||
|
|
||
| // Authenticate user | ||
| const { data: { user } } = await supabase.auth.getUser(); | ||
| if (!user) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| const { searchParams } = new URL(req.url); | ||
| const userId = searchParams.get("userId"); | ||
|
|
||
| if (!userId) { | ||
| return NextResponse.json({ error: "Missing userId" }, { status: 400 }); | ||
| } | ||
|
|
||
| try { | ||
| type Message = { sender_id: string; receiver_id: string }; | ||
| if (!validateUUID(userId)) { | ||
| return NextResponse.json({ error: "Invalid userId format" }, { status: 400 }); | ||
| } | ||
|
|
||
| // Authorization check | ||
| if (user.id !== userId) { | ||
| return NextResponse.json({ error: "Forbidden" }, { status: 403 }); | ||
| } | ||
|
|
||
| // Check cache first | ||
| const cacheKey = `active_chats_${userId}`; | ||
| const cached = chatCache.get(cacheKey); | ||
| if (cached && Date.now() - cached.timestamp < CACHE_TTL) { | ||
| return NextResponse.json(cached.data); | ||
| } | ||
|
|
||
| // Find all users that have exchanged messages with this user | ||
| const { data: messages, error } = await supabase | ||
| try { | ||
| // Optimized query: get unique conversation partners with last message time :> | ||
| const { data: conversations, error } = await supabase | ||
| .from("messages") | ||
| .select("sender_id, receiver_id") | ||
| .or(`sender_id.eq.${userId},receiver_id.eq.${userId}`); | ||
| .select(` | ||
| sender_id, | ||
| receiver_id, | ||
| created_at, | ||
| message | ||
| `) | ||
| .or(`sender_id.eq.${userId},receiver_id.eq.${userId}`) | ||
| .order("created_at", { ascending: false }) | ||
| .limit(200); // Reasonable limit for performance | ||
|
|
||
| if (error) { | ||
| console.error("Database error in active-chats:", error); | ||
| // Return empty array if there's a database error (e.g., migration not run) | ||
| return NextResponse.json([]); | ||
| return NextResponse.json({ error: "Failed to fetch conversations" }, { status: 500 }); | ||
| } | ||
|
|
||
| // Handle case where messages is null or undefined | ||
| if (!messages || !Array.isArray(messages)) { | ||
| if (!conversations || !Array.isArray(conversations)) { | ||
| return NextResponse.json([]); | ||
| } | ||
|
|
||
| // Get unique other user IDs | ||
| const otherUserIds = new Set<string>(); | ||
| messages.forEach((m: Message) => { | ||
| if (m.sender_id !== userId) otherUserIds.add(m.sender_id); | ||
| if (m.receiver_id !== userId) otherUserIds.add(m.receiver_id); | ||
| // Process conversations more efficiently | ||
| const conversationMap = new Map<string, { | ||
| userId: string; | ||
| lastMessage: string; | ||
| lastMessageTime: string | ||
| }>(); | ||
|
|
||
| conversations.forEach((msg: { sender_id: string; receiver_id: string; message: string; created_at: string }) => { | ||
| const otherUserId = msg.sender_id === userId ? msg.receiver_id : msg.sender_id; | ||
|
|
||
| // Keep only the most recent message per conversation | ||
| if (!conversationMap.has(otherUserId)) { | ||
| conversationMap.set(otherUserId, { | ||
| userId: otherUserId, | ||
| lastMessage: msg.message, | ||
| lastMessageTime: msg.created_at | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| if (otherUserIds.size === 0) return NextResponse.json([]); | ||
| if (conversationMap.size === 0) { | ||
| chatCache.set(cacheKey, { data: [], timestamp: Date.now() }); | ||
| return NextResponse.json([]); | ||
| } | ||
|
|
||
| // Fetch user details for these IDs | ||
| // Fetch user details in batch | ||
| const otherUserIds = Array.from(conversationMap.keys()); | ||
| const { data: users, error: usersError } = await supabase | ||
| .from("user_profiles") | ||
| .select("id, name") | ||
| .in("id", Array.from(otherUserIds)); | ||
| .select("id, name, srn") | ||
| .in("id", otherUserIds); | ||
|
|
||
| if (usersError) { | ||
| console.error("Database error fetching user profiles:", usersError); | ||
| // Return empty array if user_profiles table has issues | ||
| return NextResponse.json([]); | ||
| return NextResponse.json({ error: "Failed to fetch user profiles" }, { status: 500 }); | ||
| } | ||
|
|
||
| return NextResponse.json(users || []); | ||
| } catch (err) { | ||
| console.error("Unexpected error in active-chats:", err); | ||
| const message = err instanceof Error ? err.message : "Failed to fetch chats"; | ||
| return NextResponse.json({ error: message }, { status: 500 }); | ||
| // Combine user data with conversation metadata | ||
| const result = (users || []).map(user => { | ||
| const conversation = conversationMap.get(user.id); | ||
| return { | ||
| id: user.id, | ||
| name: user.name || user.srn || 'Unknown User', | ||
| lastMessage: conversation?.lastMessage?.substring(0, 100), | ||
| lastMessageTime: conversation?.lastMessageTime | ||
| }; | ||
| }).sort((a, b) => | ||
| new Date(b.lastMessageTime || 0).getTime() - new Date(a.lastMessageTime || 0).getTime() | ||
| ); | ||
|
|
||
| // Cache the result | ||
| chatCache.set(cacheKey, { data: result, timestamp: Date.now() }); | ||
|
|
||
| return NextResponse.json(result); | ||
|
|
||
| } catch (error) { | ||
| console.error("Unexpected error in active-chats:", error); | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,28 @@ | ||
| import { NextRequest, NextResponse } from 'next/server'; | ||
|
|
||
| // Rate limiting for auth attempts | ||
| const authAttempts = new Map<string, { count: number; lastAttempt: number }>(); | ||
| const MAX_AUTH_ATTEMPTS = 5; | ||
| const AUTH_WINDOW = 15 * 60 * 1000; // 15 minutes | ||
|
|
||
| function checkAuthRateLimit(identifier: string): boolean { | ||
| const now = Date.now(); | ||
| const attempts = authAttempts.get(identifier); | ||
|
|
||
| if (!attempts || now - attempts.lastAttempt > AUTH_WINDOW) { | ||
| authAttempts.set(identifier, { count: 1, lastAttempt: now }); | ||
| return true; | ||
| } | ||
|
|
||
| if (attempts.count >= MAX_AUTH_ATTEMPTS) { | ||
| return false; | ||
| } | ||
|
|
||
| attempts.count++; | ||
| attempts.lastAttempt = now; | ||
| return true; | ||
| } | ||
|
|
||
| interface PESUAuthRequest { | ||
| username: string; | ||
| password: string; | ||
|
|
@@ -26,33 +49,73 @@ interface PESUAuthResponse { | |
| timestamp: string; | ||
| } | ||
|
|
||
| function validateSRN(srn: string): boolean { | ||
| // Enhanced SRN validation | ||
| const srnPattern = /^PES\d{1}[A-Z]{2}\d{2}[A-Z]{2}\d{3}$/; | ||
| return srnPattern.test(srn.toUpperCase()); | ||
| } | ||
|
|
||
| function sanitizeInput(input: string): string { | ||
| return input.trim().replace(/[<>]/g, '').substring(0, 100); | ||
| } | ||
|
|
||
| export async function POST(request: NextRequest) { | ||
| try { | ||
| const { username, password }: PESUAuthRequest = await request.json(); | ||
| const body = await request.json(); | ||
| const { username, password }: PESUAuthRequest = body; | ||
|
|
||
| // Input validation | ||
| if (!username || !password) { | ||
| return NextResponse.json( | ||
| { error: 'Username and password are required' }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| console.log(`PESU Auth attempt for user: ${username}`); | ||
| const sanitizedUsername = sanitizeInput(username); | ||
| const sanitizedPassword = sanitizeInput(password); | ||
|
|
||
| if (!validateSRN(sanitizedUsername)) { | ||
| return NextResponse.json( | ||
| { error: 'Invalid SRN format' }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| // Rate limiting | ||
| const clientIP = request.headers.get('x-forwarded-for') || | ||
| request.headers.get('x-real-ip') || | ||
| 'unknown'; | ||
|
|
||
| if (!checkAuthRateLimit(`${clientIP}-${sanitizedUsername}`)) { | ||
| return NextResponse.json( | ||
| { error: 'Too many authentication attempts. Please try again later.' }, | ||
| { status: 429 } | ||
| ); | ||
| } | ||
|
|
||
| console.log(`PESU Auth attempt for user: ${sanitizedUsername}`); | ||
|
|
||
| // Create a fresh AbortController for each request to avoid signal conflicts | ||
| const controller = new AbortController(); | ||
| const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout | ||
|
Comment on lines
+100
to
+101
|
||
|
|
||
| // Call PESU Auth API | ||
| const response = await fetch('https://pesu-auth.onrender.com/authenticate', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| 'User-Agent': 'PesXChange/1.0', | ||
| }, | ||
| body: JSON.stringify({ | ||
| username: username.toUpperCase().trim(), | ||
| password, | ||
| username: sanitizedUsername.toUpperCase(), | ||
| password: sanitizedPassword, | ||
| profile: true, | ||
| }), | ||
| signal: controller.signal, | ||
| }); | ||
|
|
||
| // Clear timeout on successful completion | ||
| clearTimeout(timeoutId); | ||
| console.log(`PESU Auth API response status: ${response.status}`); | ||
|
|
||
| if (!response.ok) { | ||
|
|
@@ -64,7 +127,21 @@ export async function POST(request: NextRequest) { | |
| } | ||
|
|
||
| const data: PESUAuthResponse = await response.json(); | ||
| console.log(`PESU Auth API response:`, { status: data.status, message: data.message, hasProfile: !!data.profile }); | ||
|
|
||
| // Validate response data | ||
| if (!data || typeof data.status !== 'boolean') { | ||
| console.error('Invalid response format from PESU Auth API'); | ||
| return NextResponse.json( | ||
| { error: 'Invalid authentication response' }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
|
|
||
| console.log(`PESU Auth API response:`, { | ||
| status: data.status, | ||
| message: data.message, | ||
| hasProfile: !!data.profile | ||
| }); | ||
|
|
||
| if (!data.status) { | ||
| return NextResponse.json( | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This UUID regex is too restrictive as it only validates UUID v4. The pattern requires '4' in the version field and '[89ab]' in the variant field, but Supabase may use different UUID versions. Use a more generic UUID pattern:
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i