Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 95 additions & 26 deletions app/api/active-chats/route.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,129 @@
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 validateUserID(id: string): boolean {
// Accept both UUID format and SRN format (PES2UG24CS453)
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const srnRegex = /^PES[0-9]{1}UG[0-9]{2}[A-Z]{2}[0-9]{3}$/;
return uuidRegex.test(id) || srnRegex.test(id);
}

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 (!validateUserID(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 });
}
}
89 changes: 83 additions & 6 deletions app/api/auth/pesu/route.ts
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;
Expand All @@ -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

Copilot AI Aug 29, 2025

Copy link

Choose a reason for hiding this comment

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

If the fetch request completes successfully before the timeout, the timeout is cleared but the AbortController signal remains aborted for subsequent requests. Consider creating a new AbortController for each request.

Copilot uses AI. Check for mistakes.

// 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) {
Expand All @@ -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(
Expand Down
Loading