Fondation

This commit is contained in:
alma 2026-01-17 13:44:45 +01:00
parent 672333a6db
commit 866b8fb3dc
10 changed files with 162 additions and 91 deletions

View File

@ -105,13 +105,13 @@ async function ensureUserExists(session: any): Promise<void> {
export async function POST(request: Request) { export async function POST(request: Request) {
// Authenticate user (declare outside try to access in catch) // Authenticate user (declare outside try to access in catch)
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session?.user?.id) { if (!session?.user?.id) {
return NextResponse.json( return NextResponse.json(
{ error: 'Unauthorized' }, { error: 'Unauthorized' },
{ status: 401 } { status: 401 }
); );
} }
try { try {
@ -265,10 +265,10 @@ export async function POST(request: Request) {
export async function DELETE(request: Request) { export async function DELETE(request: Request) {
// Authenticate user (declare outside try to access in catch) // Authenticate user (declare outside try to access in catch)
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session?.user?.id) { if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
} }
try { try {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
@ -351,13 +351,13 @@ export async function DELETE(request: Request) {
export async function PATCH(request: Request) { export async function PATCH(request: Request) {
// Authenticate user (declare outside try to access in catch) // Authenticate user (declare outside try to access in catch)
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session?.user?.id) { if (!session?.user?.id) {
return NextResponse.json( return NextResponse.json(
{ error: 'Unauthorized' }, { error: 'Unauthorized' },
{ status: 401 } { status: 401 }
); );
} }
try { try {

View File

@ -24,13 +24,13 @@ const emailListCache: Record<string, EmailCacheEntry> = {};
export async function GET(request: Request) { export async function GET(request: Request) {
// Authenticate user (declare outside try to access in catch) // Authenticate user (declare outside try to access in catch)
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session || !session.user?.id) { if (!session || !session.user?.id) {
return NextResponse.json( return NextResponse.json(
{ error: "Not authenticated" }, { error: "Not authenticated" },
{ status: 401 } { status: 401 }
); );
} }
try { try {

View File

@ -79,7 +79,12 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Server configuration error' }, { status: 500 }); return NextResponse.json({ error: 'Server configuration error' }, { status: 500 });
} }
logger.debug('[ROCKET_CHAT] Using Rocket.Chat base URL'); logger.debug('[ROCKET_CHAT] Using Rocket.Chat base URL', {
baseUrl,
hasToken: !!process.env.ROCKET_CHAT_TOKEN,
hasUserId: !!process.env.ROCKET_CHAT_USER_ID,
hasSecret: !!process.env.ROCKET_CHAT_CREATE_TOKEN_SECRET,
});
// Step 1: Use admin token to authenticate // Step 1: Use admin token to authenticate
const adminHeaders = { const adminHeaders = {
@ -100,15 +105,37 @@ export async function GET(request: Request) {
// Get all users to find the current user // Get all users to find the current user
let usersData; let usersData;
try { try {
usersData = await fetchJsonWithTimeout(`${baseUrl}/api/v1/users.list`, { const usersListUrl = `${baseUrl}/api/v1/users.list`;
logger.debug('[ROCKET_CHAT] Fetching users list', { url: usersListUrl });
usersData = await fetchJsonWithTimeout(usersListUrl, {
method: 'GET', method: 'GET',
timeout: 10000, // 10 seconds timeout: 10000, // 10 seconds
headers: adminHeaders headers: adminHeaders
}); });
} catch (error) {
logger.error('[ROCKET_CHAT] Error fetching users list', { logger.debug('[ROCKET_CHAT] Successfully fetched users list', {
error: error instanceof Error ? error.message : String(error), success: usersData.success,
count: usersData.count,
}); });
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('[ROCKET_CHAT] Error fetching users list', {
error: errorMessage,
baseUrl,
url: `${baseUrl}/api/v1/users.list`,
hasToken: !!process.env.ROCKET_CHAT_TOKEN,
hasUserId: !!process.env.ROCKET_CHAT_USER_ID,
});
// If it's an HTML response error, log more details
if (errorMessage.includes('text/html')) {
logger.error('[ROCKET_CHAT] RocketChat returned HTML instead of JSON - possible authentication or URL issue', {
baseUrl,
checkUrl: `${baseUrl}/api/v1/users.list`,
});
}
// Return empty messages instead of failing completely // Return empty messages instead of failing completely
return NextResponse.json({ messages: [] }, { status: 200 }); return NextResponse.json({ messages: [] }, { status: 200 });
} }
@ -412,10 +439,28 @@ export async function GET(request: Request) {
return NextResponse.json(finalResponse); return NextResponse.json(finalResponse);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('[ROCKET_CHAT] Error fetching messages', { logger.error('[ROCKET_CHAT] Error fetching messages', {
error: error instanceof Error ? error.message : String(error), error: errorMessage,
baseUrl: process.env.NEXT_PUBLIC_IFRAME_PAROLE_URL?.split('/channel')[0],
hasToken: !!process.env.ROCKET_CHAT_TOKEN,
hasUserId: !!process.env.ROCKET_CHAT_USER_ID,
hasSecret: !!process.env.ROCKET_CHAT_CREATE_TOKEN_SECRET,
}); });
return NextResponse.json({ error: 'Failed to fetch messages' }, { status: 500 });
// If it's an HTML response error, provide more helpful message
if (errorMessage.includes('text/html') || errorMessage.includes('HTML')) {
logger.error('[ROCKET_CHAT] RocketChat is returning HTML - possible causes:', {
cause1: 'Invalid or expired ROCKET_CHAT_TOKEN',
cause2: 'Invalid or expired ROCKET_CHAT_USER_ID',
cause3: 'Incorrect baseUrl (check NEXT_PUBLIC_IFRAME_PAROLE_URL)',
cause4: 'RocketChat server is down or unreachable',
cause5: 'RocketChat API endpoint changed or requires different authentication',
});
}
// Return empty messages instead of error to prevent widget from breaking
return NextResponse.json({ messages: [], total: 0, hasMore: false, totalUnreadCount: 0 }, { status: 200 });
} }
} }

View File

@ -27,6 +27,13 @@ export async function GET(request: Request) {
); );
} }
logger.debug('[ROCKET_CHAT_USER_TOKEN] Using Rocket.Chat base URL', {
baseUrl,
hasToken: !!process.env.ROCKET_CHAT_TOKEN,
hasUserId: !!process.env.ROCKET_CHAT_USER_ID,
hasSecret: !!process.env.ROCKET_CHAT_CREATE_TOKEN_SECRET,
});
// Use admin token to authenticate // Use admin token to authenticate
const adminHeaders = { const adminHeaders = {
'X-Auth-Token': process.env.ROCKET_CHAT_TOKEN!, 'X-Auth-Token': process.env.ROCKET_CHAT_TOKEN!,
@ -46,14 +53,26 @@ export async function GET(request: Request) {
// Get all users to find the current user // Get all users to find the current user
let usersResponse; let usersResponse;
const usersListUrl = `${baseUrl}/api/v1/users.list`;
try { try {
usersResponse = await fetch(`${baseUrl}/api/v1/users.list`, { logger.debug('[ROCKET_CHAT_USER_TOKEN] Fetching users list', { url: usersListUrl });
usersResponse = await fetch(usersListUrl, {
method: 'GET', method: 'GET',
headers: adminHeaders headers: adminHeaders
}); });
logger.debug('[ROCKET_CHAT_USER_TOKEN] Users list response', {
status: usersResponse.status,
statusText: usersResponse.statusText,
contentType: usersResponse.headers.get('content-type'),
});
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('[ROCKET_CHAT_USER_TOKEN] Error fetching users list', { logger.error('[ROCKET_CHAT_USER_TOKEN] Error fetching users list', {
error: error instanceof Error ? error.message : String(error), error: errorMessage,
baseUrl,
url: usersListUrl,
}); });
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to connect to RocketChat' }, { error: 'Failed to connect to RocketChat' },
@ -125,13 +144,13 @@ export async function GET(request: Request) {
let createTokenResponse; let createTokenResponse;
try { try {
createTokenResponse = await fetch(`${baseUrl}/api/v1/users.createToken`, { createTokenResponse = await fetch(`${baseUrl}/api/v1/users.createToken`, {
method: 'POST', method: 'POST',
headers: adminHeaders, headers: adminHeaders,
body: JSON.stringify({ body: JSON.stringify({
userId: currentUser._id, userId: currentUser._id,
secret: secret secret: secret
}) })
}); });
} catch (error) { } catch (error) {
logger.error('[ROCKET_CHAT_USER_TOKEN] Error creating token', { logger.error('[ROCKET_CHAT_USER_TOKEN] Error creating token', {
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),

View File

@ -52,11 +52,11 @@ function EmailSkeleton() {
} }
function ParoleSkeleton() { function ParoleSkeleton() {
return ( return (
<div className="col-span-6 animate-pulse"> <div className="col-span-6 animate-pulse">
<div className="h-96 bg-gray-200 rounded-lg"></div> <div className="h-96 bg-gray-200 rounded-lg"></div>
</div> </div>
); );
} }
export default async function Home() { export default async function Home() {
@ -70,47 +70,47 @@ export default async function Home() {
return ( return (
<> <>
<HomeLogoutCheck /> <HomeLogoutCheck />
<main className="h-screen overflow-auto"> <main className="h-screen overflow-auto">
<div className="container mx-auto p-4 mt-12"> <div className="container mx-auto p-4 mt-12">
{/* First row */} {/* First row */}
<div className="grid grid-cols-12 gap-4 mb-4"> <div className="grid grid-cols-12 gap-4 mb-4">
<Suspense fallback={<QuoteCardSkeleton />}> <Suspense fallback={<QuoteCardSkeleton />}>
<div className="col-span-3"> <div className="col-span-3">
<QuoteCard /> <QuoteCard />
</div> </div>
</Suspense> </Suspense>
<Suspense fallback={<CalendarSkeleton />}> <Suspense fallback={<CalendarSkeleton />}>
<div className="col-span-3"> <div className="col-span-3">
<Calendar /> <Calendar />
</div> </div>
</Suspense> </Suspense>
<Suspense fallback={<NewsSkeleton />}> <Suspense fallback={<NewsSkeleton />}>
<div className="col-span-3"> <div className="col-span-3">
<News /> <News />
</div> </div>
</Suspense> </Suspense>
<Suspense fallback={<DutiesSkeleton />}> <Suspense fallback={<DutiesSkeleton />}>
<div className="col-span-3"> <div className="col-span-3">
<Duties /> <Duties />
</div>
</Suspense>
</div> </div>
</Suspense>
</div>
{/* Second row */} {/* Second row */}
<div className="grid grid-cols-12 gap-4"> <div className="grid grid-cols-12 gap-4">
<Suspense fallback={<EmailSkeleton />}> <Suspense fallback={<EmailSkeleton />}>
<div className="col-span-6"> <div className="col-span-6">
<Email /> <Email />
</div> </div>
</Suspense> </Suspense>
<Suspense fallback={<ParoleSkeleton />}> <Suspense fallback={<ParoleSkeleton />}>
<div className="col-span-6"> <div className="col-span-6">
<Parole /> <Parole />
</div>
</Suspense>
</div> </div>
</Suspense>
</div> </div>
</main> </div>
</main>
</> </>
); );
} }

View File

@ -88,7 +88,7 @@ export function CalendarWidget() {
return events.map((event: any) => ({ return events.map((event: any) => ({
...event, ...event,
calendarName: getCalendarDisplayName(calendar), calendarName: getCalendarDisplayName(calendar),
calendarColor: calendar.color, calendarColor: calendar.color,
})); }));
}); });

View File

@ -70,8 +70,8 @@ export function Parole() {
// Check if response is JSON before trying to parse // Check if response is JSON before trying to parse
const contentType = response.headers.get('content-type') || ''; const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) { if (contentType.includes('application/json')) {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(errorData.error || 'Failed to fetch messages'); throw new Error(errorData.error || 'Failed to fetch messages');
} else { } else {
// Response is HTML (probably an error page) // Response is HTML (probably an error page)
const errorText = await response.text(); const errorText = await response.text();

View File

@ -78,8 +78,18 @@ export async function fetchJsonWithTimeout<T = any>(
): Promise<T> { ): Promise<T> {
const response = await fetchWithTimeout(url, options); const response = await fetchWithTimeout(url, options);
// Clone response early so we can read it multiple times if needed
const clonedResponse = response.clone();
if (!response.ok) { if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error'); const errorText = await clonedResponse.text().catch(() => 'Unknown error');
const contentType = response.headers.get('content-type') || '';
// If it's HTML, provide more context
if (contentType.includes('text/html') || errorText.trim().startsWith('<!DOCTYPE') || errorText.trim().startsWith('<html')) {
throw new Error(`HTTP ${response.status} ${response.statusText}: Server returned HTML page instead of JSON. This usually means authentication failed or URL is incorrect. Preview: ${errorText.substring(0, 300)}`);
}
throw new Error( throw new Error(
`HTTP ${response.status} ${response.statusText}: ${errorText.substring(0, 200)}` `HTTP ${response.status} ${response.statusText}: ${errorText.substring(0, 200)}`
); );
@ -89,26 +99,23 @@ export async function fetchJsonWithTimeout<T = any>(
// Check if response is JSON // Check if response is JSON
if (!contentType.includes('application/json')) { if (!contentType.includes('application/json')) {
// Try to read the response text to see what we got // Read the response text to see what we got
const responseText = await response.text().catch(() => 'Unable to read response'); const responseText = await clonedResponse.text().catch(() => 'Unable to read response');
const preview = responseText.substring(0, 200); const preview = responseText.substring(0, 500);
// If it looks like HTML, provide a more helpful error // If it looks like HTML, provide a more helpful error
if (responseText.trim().startsWith('<!DOCTYPE') || responseText.trim().startsWith('<html')) { if (responseText.trim().startsWith('<!DOCTYPE') || responseText.trim().startsWith('<html')) {
throw new Error(`Expected JSON response, got text/html. Server may be returning an error page. Preview: ${preview}`); throw new Error(`Expected JSON response, got text/html. Server may be returning an error page or login page. URL: ${url}. Preview: ${preview}`);
} }
throw new Error(`Expected JSON response, got ${contentType || 'unknown'}. Preview: ${preview}`); throw new Error(`Expected JSON response, got ${contentType || 'unknown'}. URL: ${url}. Preview: ${preview}`);
} }
// Clone the response before reading it, in case we need to read it again
const clonedResponse = response.clone();
try { try {
return await response.json(); return await response.json();
} catch (jsonError) { } catch (jsonError) {
// If JSON parsing fails, try to read the text to see what we got // If JSON parsing fails, try to read the text to see what we got
const responseText = await clonedResponse.text().catch(() => 'Unable to read response'); const responseText = await clonedResponse.text().catch(() => 'Unable to read response');
throw new Error(`Failed to parse JSON response. Content: ${responseText.substring(0, 200)}`); throw new Error(`Failed to parse JSON response from ${url}. Content: ${responseText.substring(0, 500)}`);
} }
} }