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:
117
src/utils/apiAuth.js
Normal file
117
src/utils/apiAuth.js
Normal 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 });
|
||||
}
|
||||
@@ -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
18
src/utils/db.js
Normal 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;
|
||||
Reference in New Issue
Block a user