feat(api): add endpoints for fetching reaction users and searching messages

- Implemented GET endpoint to fetch users who reacted with a specific emoji on a message.
- Added validation for messageId and emoji parameters.
- Enhanced user data retrieval with display names and avatar URLs.
- Created a search endpoint for Discord messages with support for content and embed searches.
- Included pagination and rate limiting for search results.

feat(api): introduce image proxy and link preview endpoints

- Developed an image proxy API to securely fetch images from untrusted domains.
- Implemented HMAC signing for image URLs to prevent abuse.
- Created a link preview API to fetch Open Graph metadata from URLs.
- Added support for trusted domains and safe image URL generation.

style(pages): create Discord logs page with authentication

- Added a new page for displaying archived Discord channel logs.
- Integrated authentication check to ensure user access.

refactor(utils): enhance API authentication and database connection

- Improved API authentication helper to manage user sessions and token refresh.
- Established a PostgreSQL database connection utility for Discord logs.
This commit is contained in:
2025-12-03 13:27:37 -05:00
parent c3f0197115
commit 55e4c5ff0c
20 changed files with 7066 additions and 9 deletions

117
src/utils/apiAuth.js Normal file
View File

@@ -0,0 +1,117 @@
/**
* API route authentication helper
* Validates user session for protected API endpoints
*/
import { API_URL } from '@/config';
/**
* Check if the request has a valid authentication session
* @param {Request} request - The incoming request
* @returns {Promise<{user: object|null, error: Response|null, setCookieHeader: string|null}>}
*/
export async function requireApiAuth(request) {
try {
const cookieHeader = request.headers.get('cookie') ?? '';
if (!cookieHeader) {
return {
user: null,
error: new Response(JSON.stringify({ error: 'Authentication required' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
}),
setCookieHeader: null,
};
}
// Try to get user identity
let res = await fetch(`${API_URL}/auth/id`, {
headers: { Cookie: cookieHeader },
credentials: 'include',
});
let newSetCookieHeader = null;
// If unauthorized, try to refresh the token
if (res.status === 401) {
const refreshRes = await fetch(`${API_URL}/auth/refresh`, {
method: 'POST',
headers: { Cookie: cookieHeader },
credentials: 'include',
});
if (!refreshRes.ok) {
return {
user: null,
error: new Response(JSON.stringify({ error: 'Session expired' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
}),
setCookieHeader: null,
};
}
// Capture the Set-Cookie header from the refresh response to forward to client
newSetCookieHeader = refreshRes.headers.get('set-cookie');
let newCookieHeader = cookieHeader;
if (newSetCookieHeader) {
const cookiesArray = newSetCookieHeader.split(/,(?=\s*\w+=)/);
newCookieHeader = cookiesArray.map(c => c.split(';')[0]).join('; ');
} else {
return {
user: null,
error: new Response(JSON.stringify({ error: 'Session refresh failed' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
}),
setCookieHeader: null,
};
}
res = await fetch(`${API_URL}/auth/id`, {
headers: { Cookie: newCookieHeader },
credentials: 'include',
});
}
if (!res.ok) {
return {
user: null,
error: new Response(JSON.stringify({ error: 'Authentication failed' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
}),
setCookieHeader: null,
};
}
const user = await res.json();
return { user, error: null, setCookieHeader: newSetCookieHeader };
} catch (err) {
console.error('API auth error:', err);
return {
user: null,
error: new Response(JSON.stringify({ error: 'Authentication error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
}),
setCookieHeader: null,
};
}
}
/**
* Helper to create a response with optional Set-Cookie header forwarding
* @param {any} data - Response data
* @param {number} status - HTTP status code
* @param {string|null} setCookieHeader - Set-Cookie header from auth refresh
*/
export function createApiResponse(data, status = 200, setCookieHeader = null) {
const headers = { 'Content-Type': 'application/json' };
if (setCookieHeader) {
headers['Set-Cookie'] = setCookieHeader;
}
return new Response(JSON.stringify(data), { status, headers });
}

View File

@@ -1,5 +1,7 @@
import { API_URL } from "@/config";
// Track in-flight refresh to avoid duplicate requests
let refreshPromise = null;
// Auth fetch wrapper
export const authFetch = async (url, options = {}, retry = true) => {
@@ -9,12 +11,19 @@ export const authFetch = async (url, options = {}, retry = true) => {
});
if (res.status === 401 && retry) {
// attempt refresh
// attempt refresh (non-blocking if already in progress)
try {
const refreshRes = await fetch(`${API_URL}/auth/refresh`, {
method: "POST",
credentials: "include",
});
// Reuse existing refresh promise if one is in flight
if (!refreshPromise) {
refreshPromise = fetch(`${API_URL}/auth/refresh`, {
method: "POST",
credentials: "include",
}).finally(() => {
refreshPromise = null;
});
}
const refreshRes = await refreshPromise;
if (!refreshRes.ok) throw new Error("Refresh failed");
@@ -51,6 +60,56 @@ export async function refreshAccessToken(cookieHeader) {
}
}
/**
* Ensure authentication is valid before making API requests.
* Makes a lightweight auth check against our own API and refreshes if needed.
* Returns true if auth is valid, false if user needs to log in.
*/
export async function ensureAuth() {
try {
// Try a lightweight request to our own API that requires auth
// Using HEAD or a simple endpoint to minimize overhead
const res = await fetch('/api/discord/channels', {
method: 'GET',
credentials: 'include',
});
if (res.ok) {
return true;
}
if (res.status === 401) {
// Try to refresh the token via our external auth API
if (!refreshPromise) {
refreshPromise = fetch(`${API_URL}/auth/refresh`, {
method: 'POST',
credentials: 'include',
}).finally(() => {
refreshPromise = null;
});
}
const refreshRes = await refreshPromise;
if (refreshRes.ok) {
// Retry the auth check after refresh
const retryRes = await fetch('/api/discord/channels', {
method: 'GET',
credentials: 'include',
});
return retryRes.ok;
}
return false;
}
// Other errors (500, etc.) - assume auth is OK but server issue
return true;
} catch (err) {
console.error('Auth check failed:', err);
// Network error - don't redirect, let the actual API calls handle it
return true;
}
}
export function handleLogout() {
document.cookie.split(";").forEach((cookie) => {
const name = cookie.split("=")[0].trim();

18
src/utils/db.js Normal file
View File

@@ -0,0 +1,18 @@
/**
* PostgreSQL database connection for Discord logs
*/
import postgres from 'postgres';
// Database connection configuration
const sql = postgres({
host: import.meta.env.DISCORD_DB_HOST || 'localhost',
port: parseInt(import.meta.env.DISCORD_DB_PORT || '5432', 10),
database: import.meta.env.DISCORD_DB_NAME || 'discord',
username: import.meta.env.DISCORD_DB_USER || 'discord',
password: import.meta.env.DISCORD_DB_PASSWORD || '',
max: 10, // Max connections in pool
idle_timeout: 20,
connect_timeout: 10,
});
export default sql;