Fondation

This commit is contained in:
alma 2026-01-16 21:58:17 +01:00
parent c341468996
commit c4267a0eeb
8 changed files with 91 additions and 67 deletions

View File

@ -527,7 +527,7 @@ export const authOptions: NextAuthOptions = {
session.refreshToken = token.refreshToken as string | undefined;
logger.debug('[SESSION_CALLBACK] Session created', {
userId: session.user.id,
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
hasEmail: !!session.user.email,
rolesCount: session.user.role.length,
});

View File

@ -36,7 +36,7 @@ export async function GET(req: NextRequest) {
const cachedData = await getCachedCalendarData(session.user.id);
if (cachedData) {
logger.debug('[CALENDAR] Using cached calendar data', {
userId: session.user.id,
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
calendarCount: cachedData.length,
});
return NextResponse.json(cachedData);
@ -45,7 +45,7 @@ export async function GET(req: NextRequest) {
// If no cache or forcing refresh, fetch from database
logger.debug('[CALENDAR] Fetching calendar data from database', {
userId: session.user.id,
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
});
// Ensure user has a default private calendar (created automatically if missing)
@ -70,7 +70,7 @@ export async function GET(req: NextRequest) {
}
});
logger.debug('[CALENDAR] Created default private calendar', {
userId: session.user.id,
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
});
}
@ -201,7 +201,7 @@ export async function GET(req: NextRequest) {
});
logger.debug('[CALENDAR] Fetched calendars with events', {
userId: session.user.id,
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
personalCount: filteredPersonalCalendars.length,
missionCount: missionCalendars.length,
totalCount: sortedCalendars.length,

View File

@ -58,12 +58,17 @@ async function ensureUserExists(session: any): Promise<void> {
});
if (existingUser) {
logger.debug('[COURRIER_ACCOUNT] User already exists', { userId });
logger.debug('[COURRIER_ACCOUNT] User already exists', {
userIdHash: Buffer.from(userId).toString('base64').slice(0, 12),
});
return;
}
// User doesn't exist, create it
logger.debug('[COURRIER_ACCOUNT] User not found, creating from session data', { userId, email: userEmail.substring(0, 5) + '***' });
logger.debug('[COURRIER_ACCOUNT] User not found, creating from session data', {
userIdHash: Buffer.from(userId).toString('base64').slice(0, 12),
emailHash: Buffer.from(userEmail.toLowerCase()).toString('base64').slice(0, 12),
});
// Generate a temporary random password (not used for auth, Keycloak handles that)
const tempPassword = await bcrypt.hash(Math.random().toString(36).slice(-10), 10);
@ -78,15 +83,20 @@ async function ensureUserExists(session: any): Promise<void> {
}
});
logger.debug('[COURRIER_ACCOUNT] Successfully created user', { userId, email: userEmail.substring(0, 5) + '***' });
logger.debug('[COURRIER_ACCOUNT] Successfully created user', {
userIdHash: Buffer.from(userId).toString('base64').slice(0, 12),
emailHash: Buffer.from(userEmail.toLowerCase()).toString('base64').slice(0, 12),
});
} catch (error) {
logger.error('[COURRIER_ACCOUNT] Error ensuring user exists', {
userId,
userIdHash: Buffer.from(userId).toString('base64').slice(0, 12),
error: error instanceof Error ? error.message : String(error)
});
// If it's a unique constraint error, user might have been created by another request
if (error instanceof Error && error.message.includes('Unique constraint')) {
logger.debug('[COURRIER_ACCOUNT] User may have been created by concurrent request', { userId });
logger.debug('[COURRIER_ACCOUNT] User may have been created by concurrent request', {
userIdHash: Buffer.from(userId).toString('base64').slice(0, 12),
});
return;
}
throw error;

View File

@ -46,11 +46,11 @@ export async function GET(request: Request) {
// CRITICAL FIX: Log exact parameters received by the API
logger.debug('[COURRIER_API] Received request', {
folder,
accountId,
accountIdHash: accountId ? Buffer.from(accountId).toString('base64').slice(0, 12) : null,
page,
checkOnly,
refresh,
userId: session.user.id
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
});
// CRITICAL FIX: More robust parameter normalization
@ -74,15 +74,15 @@ export async function GET(request: Request) {
// CRITICAL FIX: Enhanced logging for parameter resolution
logger.debug('[COURRIER_API] Using normalized parameters', {
folder: normalizedFolder,
accountId: effectiveAccountId,
userId: session.user.id
accountIdHash: Buffer.from(effectiveAccountId).toString('base64').slice(0, 12),
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
});
// If refresh=true, invalidate cache before fetching
if (refresh) {
logger.debug('[COURRIER_API] Refresh requested - invalidating cache', {
userId: session.user.id,
accountId: effectiveAccountId,
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
accountIdHash: Buffer.from(effectiveAccountId).toString('base64').slice(0, 12),
folder: normalizedFolder
});
await invalidateFolderCache(session.user.id, effectiveAccountId, normalizedFolder);
@ -92,8 +92,8 @@ export async function GET(request: Request) {
if (!searchQuery && !checkOnly && !refresh) {
// CRITICAL FIX: Use consistent cache key format with the correct account ID
logger.debug('[COURRIER_API] Checking Redis cache', {
userId: session.user.id,
accountId: effectiveAccountId,
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
accountIdHash: Buffer.from(effectiveAccountId).toString('base64').slice(0, 12),
folder: normalizedFolder,
page,
perPage
@ -107,8 +107,8 @@ export async function GET(request: Request) {
);
if (cachedEmails) {
logger.debug('[COURRIER_API] Using Redis cached emails', {
userId: session.user.id,
accountId: effectiveAccountId,
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
accountIdHash: Buffer.from(effectiveAccountId).toString('base64').slice(0, 12),
folder: normalizedFolder,
page,
perPage
@ -118,8 +118,8 @@ export async function GET(request: Request) {
}
logger.debug('[COURRIER_API] Redis cache miss, fetching from IMAP', {
userId: session.user.id,
accountId: effectiveAccountId,
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
accountIdHash: Buffer.from(effectiveAccountId).toString('base64').slice(0, 12),
folder: normalizedFolder,
page,
perPage
@ -139,8 +139,8 @@ export async function GET(request: Request) {
// CRITICAL FIX: Log when emails are returned from IMAP
logger.debug('[COURRIER_API] Successfully fetched emails from IMAP', {
count: emailsResult.emails.length,
accountId: effectiveAccountId,
userId: session.user.id
accountIdHash: Buffer.from(effectiveAccountId).toString('base64').slice(0, 12),
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
});
// The result is already cached in the getEmails function (if not checkOnly)
@ -148,7 +148,7 @@ export async function GET(request: Request) {
} catch (error: any) {
logger.error('[COURRIER_API] Error fetching emails', {
error: error instanceof Error ? error.message : String(error),
userId: session?.user?.id
userIdHash: session?.user?.id ? Buffer.from(session.user.id).toString('base64').slice(0, 12) : null,
});
return NextResponse.json(
{ error: "Failed to fetch emails", message: error.message },
@ -180,8 +180,8 @@ export async function POST(request: Request) {
// Log the cache invalidation operation
logger.debug('[COURRIER_API] Invalidating cache', {
userId: session.user.id,
accountId: effectiveAccountId,
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
accountIdHash: Buffer.from(effectiveAccountId).toString('base64').slice(0, 12),
folder: normalizedFolder || 'all folders'
});

View File

@ -151,7 +151,7 @@ export async function GET() {
// Get user with their accounts
logger.debug('[COURRIER_SESSION] Fetching user with ID', {
userId: session.user.id,
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
});
const user = await prisma.user.findUnique({
where: { id: session.user.id },
@ -171,7 +171,7 @@ export async function GET() {
const accounts = (user.mailCredentials || []) as MailCredentials[];
if (accounts.length === 0) {
logger.debug('[COURRIER_SESSION] No email accounts found for user', {
userId: session.user.id,
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
});
return NextResponse.json({
authenticated: true,
@ -182,9 +182,8 @@ export async function GET() {
}
logger.debug('[COURRIER_SESSION] Found accounts for user', {
userId: session.user.id,
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
count: accounts.length,
emails: accounts.map(a => a.email),
});
// Fetch folders for each account

View File

@ -92,7 +92,7 @@ export async function GET(request: Request) {
const username = session.user.email.split('@')[0];
if (!username) {
logger.error('[ROCKET_CHAT] No username found in session email', {
email: session.user.email,
emailHash: Buffer.from(session.user.email.toLowerCase()).toString('base64').slice(0, 12),
});
return NextResponse.json({ messages: [] }, { status: 200 });
}
@ -123,15 +123,13 @@ export async function GET(request: Request) {
if (!currentUser) {
logger.error('[ROCKET_CHAT] User not found in users list', {
username,
email: session.user.email,
emailHash: Buffer.from(session.user.email.toLowerCase()).toString('base64').slice(0, 12),
});
return NextResponse.json({ messages: [] }, { status: 200 });
}
logger.debug('[ROCKET_CHAT] Found user', {
username: currentUser.username,
id: currentUser._id,
emailHash: Buffer.from(session.user.email.toLowerCase()).toString('base64').slice(0, 12),
});
// Step 3: Create a token for the current user
@ -207,8 +205,7 @@ export async function GET(request: Request) {
sum + (sub.unread || 0), 0);
logger.debug('[ROCKET_CHAT] Filtered user subscriptions', {
userId: currentUser._id,
username: currentUser.username,
emailHash: Buffer.from(session.user.email.toLowerCase()).toString('base64').slice(0, 12),
totalSubscriptions: userSubscriptions.length,
totalUnreadCount: totalUnreadCount,
});

View File

@ -190,21 +190,23 @@ export async function getImapConnection(
totalConnectionRequests++;
logger.debug('[IMAP] getImapConnection called', {
userId,
accountId: accountId ?? 'default',
userIdHash: Buffer.from(userId).toString('base64').slice(0, 12),
accountIdHash: accountId ? Buffer.from(accountId).toString('base64').slice(0, 12) : 'default',
});
// Special handling for 'default' accountId - find the first available account
if (!accountId || accountId === 'default') {
logger.debug('[IMAP] Resolving default accountId', { userId });
logger.debug('[IMAP] Resolving default accountId', {
userIdHash: Buffer.from(userId).toString('base64').slice(0, 12),
});
// Try getting the account ID from cache to avoid database query
const sessionData = await getCachedImapSession(userId);
if (sessionData && sessionData.defaultAccountId) {
accountId = sessionData.defaultAccountId;
logger.debug('[IMAP] Using cached default account ID', {
userId,
accountId,
userIdHash: Buffer.from(userId).toString('base64').slice(0, 12),
accountIdHash: Buffer.from(accountId).toString('base64').slice(0, 12),
});
} else {
// Query to find all accounts for this user
@ -217,9 +219,8 @@ export async function getImapConnection(
if (accounts && accounts.length > 0) {
const firstAccount = accounts[0];
logger.debug('[IMAP] Using first available account from DB', {
userId,
accountId: firstAccount.id,
email: firstAccount.email,
userIdHash: Buffer.from(userId).toString('base64').slice(0, 12),
accountIdHash: Buffer.from(firstAccount.id).toString('base64').slice(0, 12),
});
accountId = firstAccount.id;
@ -304,21 +305,23 @@ export async function getImapConnection(
}
// If we get here, we need a new connection
logger.debug('[IMAP] Creating new connection', { connectionKey });
logger.debug('[IMAP] Creating new connection', {
connectionKeyHash: Buffer.from(connectionKey).toString('base64').slice(0, 12),
});
// First try to get credentials from Redis cache
let credentials = await getCachedEmailCredentials(userId, accountId);
logger.debug('[IMAP] Retrieved credentials from Redis cache', {
userId,
accountId,
userIdHash: Buffer.from(userId).toString('base64').slice(0, 12),
accountIdHash: accountId ? Buffer.from(accountId).toString('base64').slice(0, 12) : 'default',
found: !!credentials,
});
// If not in cache, get from database and cache them
if (!credentials) {
logger.debug('[IMAP] Credentials not found in cache, querying database', {
userId,
accountId,
userIdHash: Buffer.from(userId).toString('base64').slice(0, 12),
accountIdHash: accountId ? Buffer.from(accountId).toString('base64').slice(0, 12) : 'default',
});
// Fetch directly from database
@ -333,17 +336,16 @@ export async function getImapConnection(
if (!dbCredentials) {
logger.error('[IMAP] No credentials found for user', {
userId,
accountId,
userIdHash: Buffer.from(userId).toString('base64').slice(0, 12),
accountIdHash: accountId ? Buffer.from(accountId).toString('base64').slice(0, 12) : 'default',
});
totalConnectionErrors++;
throw new Error('Email account credentials not found');
}
logger.debug('[IMAP] Database lookup returned credentials', {
userId,
accountId,
email: dbCredentials.email,
userIdHash: Buffer.from(userId).toString('base64').slice(0, 12),
accountIdHash: accountId ? Buffer.from(accountId).toString('base64').slice(0, 12) : 'default',
hasPassword: !!dbCredentials.password,
});
@ -523,7 +525,7 @@ async function createImapConnection(credentials: EmailCredentials, connectionKey
const extendedCreds = credentials as EmailCredentialsExtended;
logger.debug('[IMAP] Creating ImapFlow client with credentials metadata', {
email: extendedCreds.email,
emailHash: Buffer.from(extendedCreds.email || '').toString('base64').slice(0, 12),
host: extendedCreds.host,
port: extendedCreds.port,
hasPassword: !!extendedCreds.password,
@ -537,7 +539,9 @@ async function createImapConnection(credentials: EmailCredentials, connectionKey
// Check if we have valid OAuth tokens
if (extendedCreds.useOAuth && extendedCreds.accessToken) {
logger.debug('[IMAP] Using XOAUTH2 authentication', { connectionKey });
logger.debug('[IMAP] Using XOAUTH2 authentication', {
connectionKeyHash: Buffer.from(connectionKey).toString('base64').slice(0, 12),
});
// Set auth parameters for ImapFlow
authParams = {
@ -545,10 +549,14 @@ async function createImapConnection(credentials: EmailCredentials, connectionKey
accessToken: extendedCreds.accessToken
};
logger.debug('[IMAP] XOAUTH2 auth configured', { connectionKey });
logger.debug('[IMAP] XOAUTH2 auth configured', {
connectionKeyHash: Buffer.from(connectionKey).toString('base64').slice(0, 12),
});
} else if (extendedCreds.password) {
// Use regular password authentication
logger.debug('[IMAP] Using password authentication', { connectionKey });
logger.debug('[IMAP] Using password authentication', {
connectionKeyHash: Buffer.from(connectionKey).toString('base64').slice(0, 12),
});
authParams = {
user: extendedCreds.email,
pass: extendedCreds.password
@ -928,11 +936,11 @@ export async function getEmails(
): Promise<EmailListResult> {
// Normalize folder name and handle account ID
logger.debug('[EMAIL] getEmails called', {
userId,
userIdHash: Buffer.from(userId).toString('base64').slice(0, 12),
folder,
page,
perPage,
accountId: accountId || 'default',
accountIdHash: accountId ? Buffer.from(accountId).toString('base64').slice(0, 12) : 'default',
checkOnly,
});

View File

@ -20,7 +20,9 @@ export async function ensureFreshToken(
): Promise<{ accessToken: string; success: boolean }> {
try {
// Try Redis first (fast path)
logger.debug('[TOKEN_REFRESH] Checking if token refresh is needed', { email: email.substring(0, 5) + '***' });
logger.debug('[TOKEN_REFRESH] Checking if token refresh is needed', {
emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12),
});
const redis = getRedisClient();
const key = KEYS.CREDENTIALS(userId, email);
let credStr = await redis.get(key);
@ -30,7 +32,9 @@ export async function ensureFreshToken(
creds = JSON.parse(credStr);
} else {
// Redis cache miss - fallback to Prisma database
logger.debug('[TOKEN_REFRESH] No credentials in Redis, checking Prisma', { email: email.substring(0, 5) + '***' });
logger.debug('[TOKEN_REFRESH] No credentials in Redis, checking Prisma', {
emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12),
});
const account = await prisma.mailCredentials.findFirst({
where: {
userId: userId,
@ -54,16 +58,22 @@ export async function ensureFreshToken(
// Re-populate Redis cache
await redis.set(key, JSON.stringify(creds), 'EX', 86400);
logger.debug('[TOKEN_REFRESH] Recovered credentials from Prisma and cached in Redis', { email: email.substring(0, 5) + '***' });
logger.debug('[TOKEN_REFRESH] Recovered credentials from Prisma and cached in Redis', {
emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12),
});
} else {
logger.debug('[TOKEN_REFRESH] No OAuth credentials found in database', { email: email.substring(0, 5) + '***' });
logger.debug('[TOKEN_REFRESH] No OAuth credentials found in database', {
emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12),
});
return { accessToken: '', success: false };
}
}
// If not OAuth or missing refresh token, return failure
if (!creds.useOAuth || !creds.refreshToken) {
logger.debug('[TOKEN_REFRESH] Account not using OAuth or missing refresh token', { email: email.substring(0, 5) + '***' });
logger.debug('[TOKEN_REFRESH] Account not using OAuth or missing refresh token', {
emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12),
});
return { accessToken: '', success: false };
}