auth flow
This commit is contained in:
parent
90867df452
commit
32f77425de
@ -153,9 +153,13 @@ export const authOptions: NextAuthOptions = {
|
||||
},
|
||||
},
|
||||
},
|
||||
jwt: {
|
||||
// Add explicit max size to prevent chunking
|
||||
maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||
},
|
||||
callbacks: {
|
||||
async jwt({ token, account, profile }) {
|
||||
// Only include essential data in the JWT to reduce size
|
||||
// Drastically reduce JWT size by only storing essential info
|
||||
if (account && profile) {
|
||||
const keycloakProfile = profile as KeycloakProfile;
|
||||
const roles = keycloakProfile.realm_access?.roles || [];
|
||||
@ -163,14 +167,21 @@ export const authOptions: NextAuthOptions = {
|
||||
role.replace(/^ROLE_/, '').toLowerCase()
|
||||
);
|
||||
|
||||
// Store minimal data in the token
|
||||
token.accessToken = account.access_token ?? '';
|
||||
token.refreshToken = account.refresh_token ?? '';
|
||||
token.accessTokenExpires = account.expires_at ?? 0;
|
||||
token.sub = keycloakProfile.sub;
|
||||
token.role = cleanRoles;
|
||||
token.username = keycloakProfile.preferred_username ?? '';
|
||||
token.first_name = keycloakProfile.given_name ?? '';
|
||||
token.last_name = keycloakProfile.family_name ?? '';
|
||||
|
||||
// Only store these if they're short
|
||||
if (keycloakProfile.given_name && keycloakProfile.given_name.length < 30) {
|
||||
token.first_name = keycloakProfile.given_name;
|
||||
}
|
||||
if (keycloakProfile.family_name && keycloakProfile.family_name.length < 30) {
|
||||
token.last_name = keycloakProfile.family_name;
|
||||
}
|
||||
} else if (token.accessToken) {
|
||||
try {
|
||||
const decoded = jwtDecode<DecodedToken>(token.accessToken);
|
||||
@ -199,7 +210,7 @@ export const authOptions: NextAuthOptions = {
|
||||
|
||||
const userRoles = Array.isArray(token.role) ? token.role : [];
|
||||
|
||||
// Only include essential user data
|
||||
// Create a minimal user object
|
||||
session.user = {
|
||||
id: token.sub ?? '',
|
||||
email: token.email ?? null,
|
||||
@ -212,7 +223,7 @@ export const authOptions: NextAuthOptions = {
|
||||
nextcloudInitialized: false,
|
||||
};
|
||||
|
||||
// Only pass the access token, not the entire token
|
||||
// Only store access token, not the entire token
|
||||
session.accessToken = token.accessToken;
|
||||
|
||||
return session;
|
||||
|
||||
52
app/api/auth/session-cleanup/route.ts
Normal file
52
app/api/auth/session-cleanup/route.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { cleanupUserSessions } from '@/lib/redis';
|
||||
import { closeUserImapConnections } from '@/lib/services/email-service';
|
||||
|
||||
/**
|
||||
* API endpoint to clean up user sessions and invalidate cached data
|
||||
* Called during logout to ensure proper cleanup of all connections
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Get the user ID either from the session or request body
|
||||
const session = await getServerSession(authOptions);
|
||||
const body = await request.json().catch(() => ({}));
|
||||
|
||||
// Get user ID from session or from request body
|
||||
const userId = session?.user?.id || body.userId;
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'No user ID provided or user not authenticated'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
console.log(`Processing session cleanup for user ${userId}`);
|
||||
|
||||
// 1. Close any active IMAP connections using the dedicated function
|
||||
const closedConnections = await closeUserImapConnections(userId);
|
||||
|
||||
// 2. Clean up Redis data
|
||||
await cleanupUserSessions(userId);
|
||||
|
||||
// 3. Return success response with details
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Session cleanup completed for user ${userId}`,
|
||||
details: {
|
||||
closedConnections,
|
||||
redisCleanupPerformed: true
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in session cleanup:', error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Session cleanup failed',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
|
||||
interface ResponsiveIframeProps {
|
||||
src: string;
|
||||
@ -12,11 +13,49 @@ interface ResponsiveIframeProps {
|
||||
|
||||
export function ResponsiveIframe({ src, className = '', allow, style, token }: ResponsiveIframeProps) {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
const { data: session } = useSession();
|
||||
const [authError, setAuthError] = useState<string | null>(null);
|
||||
|
||||
// Add token parameter only if token is provided
|
||||
const fullSrc = token ?
|
||||
`${src}${src.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}` :
|
||||
src;
|
||||
|
||||
// Handle silent authentication refresh
|
||||
useEffect(() => {
|
||||
let silentRefreshTimer: NodeJS.Timeout;
|
||||
|
||||
// Set up periodic silent refresh (every 15 minutes)
|
||||
const startSilentRefresh = () => {
|
||||
silentRefreshTimer = setInterval(() => {
|
||||
console.log('Performing silent authentication check for iframes');
|
||||
|
||||
// Create a hidden iframe for silent authentication
|
||||
const refreshFrame = document.createElement('iframe');
|
||||
refreshFrame.style.display = 'none';
|
||||
refreshFrame.src = '/silent-refresh';
|
||||
document.body.appendChild(refreshFrame);
|
||||
|
||||
// Remove iframe after it has loaded (5 seconds timeout)
|
||||
setTimeout(() => {
|
||||
if (refreshFrame && refreshFrame.parentNode) {
|
||||
refreshFrame.parentNode.removeChild(refreshFrame);
|
||||
}
|
||||
}, 5000);
|
||||
}, 15 * 60 * 1000); // 15 minutes
|
||||
};
|
||||
|
||||
if (session) {
|
||||
startSilentRefresh();
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (silentRefreshTimer) {
|
||||
clearInterval(silentRefreshTimer);
|
||||
}
|
||||
};
|
||||
}, [session]);
|
||||
|
||||
useEffect(() => {
|
||||
const iframe = iframeRef.current;
|
||||
if (!iframe) return;
|
||||
@ -44,16 +83,25 @@ export function ResponsiveIframe({ src, className = '', allow, style, token }: R
|
||||
|
||||
// Handle authentication messages from iframe
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
// Only accept messages from our iframe
|
||||
if (iframe.contentWindow !== event.source) return;
|
||||
// Accept messages from our iframe or from silent auth iframe
|
||||
if (event.source !== iframe.contentWindow &&
|
||||
!event.data?.type?.startsWith('SILENT_AUTH_')) return;
|
||||
|
||||
const { type, data } = event.data || {};
|
||||
const { type, data, error } = event.data || {};
|
||||
|
||||
// Handle auth related messages
|
||||
// Handle auth-related messages
|
||||
if (type === 'AUTH_ERROR' || type === 'SESSION_EXPIRED') {
|
||||
console.log('Auth error in iframe:', data);
|
||||
// Optionally redirect to login page
|
||||
// window.location.href = '/signin';
|
||||
console.log('Auth error in iframe:', data || error);
|
||||
setAuthError(error || 'Authentication error');
|
||||
} else if (type === 'SILENT_AUTH_SUCCESS') {
|
||||
console.log('Silent authentication successful');
|
||||
setAuthError(null);
|
||||
} else if (type === 'SILENT_AUTH_FAILURE') {
|
||||
console.log('Silent authentication failed:', error);
|
||||
// Only set error if it's persistent
|
||||
if (error !== 'loading') {
|
||||
setAuthError('Session expired');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -77,19 +125,33 @@ export function ResponsiveIframe({ src, className = '', allow, style, token }: R
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
id="myFrame"
|
||||
src={fullSrc}
|
||||
className={`w-full border-none ${className}`}
|
||||
style={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
...style
|
||||
}}
|
||||
allow={allow}
|
||||
allowFullScreen
|
||||
/>
|
||||
<>
|
||||
{authError && (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 0, 0, 0.1)',
|
||||
padding: '10px',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '10px'
|
||||
}}
|
||||
>
|
||||
Authentication error: {authError}. The service might not work correctly.
|
||||
</div>
|
||||
)}
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
id="myFrame"
|
||||
src={fullSrc}
|
||||
className={`w-full border-none ${className}`}
|
||||
style={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
...style
|
||||
}}
|
||||
allow={allow}
|
||||
allowFullScreen
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -27,6 +27,59 @@ export default function LoggedOut() {
|
||||
// Additional browser storage clearing
|
||||
console.log('Performing complete browser storage cleanup');
|
||||
|
||||
// Try to get any user ID from localStorage or sessionStorage for server-side cleanup
|
||||
let userId = '';
|
||||
try {
|
||||
// Check standard localStorage locations for userId
|
||||
const possibleUserIdKeys = [
|
||||
'userId',
|
||||
'user_id',
|
||||
'currentUser',
|
||||
'user',
|
||||
'keycloak.userId',
|
||||
'auth.userId'
|
||||
];
|
||||
|
||||
for (const key of possibleUserIdKeys) {
|
||||
const value = localStorage.getItem(key) || sessionStorage.getItem(key);
|
||||
if (value) {
|
||||
try {
|
||||
// It might be a JSON object
|
||||
const parsed = JSON.parse(value);
|
||||
userId = parsed.id || parsed.userId || parsed.user_id || parsed.sub || '';
|
||||
if (userId) break;
|
||||
} catch {
|
||||
// Or it might be a plain string
|
||||
userId = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Found user ID for server cleanup:', userId || 'None found');
|
||||
} catch (e) {
|
||||
console.error('Error getting user ID from storage:', e);
|
||||
}
|
||||
|
||||
// Call the server-side cleanup if we have a user ID
|
||||
if (userId) {
|
||||
try {
|
||||
const cleanupResponse = await fetch('/api/auth/session-cleanup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ userId }),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const cleanupResult = await cleanupResponse.json();
|
||||
console.log('Server-side cleanup result:', cleanupResult);
|
||||
} catch (e) {
|
||||
console.error('Error calling server-side cleanup:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear cookies
|
||||
clearAuthCookies();
|
||||
|
||||
@ -40,7 +93,7 @@ export default function LoggedOut() {
|
||||
|
||||
// Clear local storage items related to auth
|
||||
try {
|
||||
const authLocalStoragePrefixes = ['token', 'auth', 'session', 'keycloak', 'kc', 'oidc', 'user'];
|
||||
const authLocalStoragePrefixes = ['token', 'auth', 'session', 'keycloak', 'kc', 'oidc', 'user', 'meteor'];
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
@ -68,6 +121,9 @@ export default function LoggedOut() {
|
||||
'KEYCLOAK_SESSION',
|
||||
'KEYCLOAK_IDENTITY',
|
||||
'KC_RESTART',
|
||||
'rc_token',
|
||||
'rc_uid',
|
||||
'Meteor.loginToken',
|
||||
...chunkedCookies
|
||||
];
|
||||
|
||||
|
||||
51
app/silent-refresh/page.tsx
Normal file
51
app/silent-refresh/page.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
|
||||
export default function SilentRefresh() {
|
||||
const { data: session, status } = useSession();
|
||||
const [message, setMessage] = useState('Checking authentication...');
|
||||
|
||||
useEffect(() => {
|
||||
// Notify parent window of authentication state
|
||||
const notifyParent = () => {
|
||||
try {
|
||||
if (window.parent && window.parent !== window) {
|
||||
if (status === 'authenticated' && session) {
|
||||
window.parent.postMessage({
|
||||
type: 'SILENT_AUTH_SUCCESS',
|
||||
session: {
|
||||
authenticated: true,
|
||||
userId: session.user.id,
|
||||
username: session.user.username,
|
||||
roles: session.user.role
|
||||
}
|
||||
}, '*');
|
||||
setMessage('Authentication successful. You can close this window.');
|
||||
} else if (status === 'unauthenticated') {
|
||||
window.parent.postMessage({
|
||||
type: 'SILENT_AUTH_FAILURE',
|
||||
error: 'Not authenticated'
|
||||
}, '*');
|
||||
setMessage('Not authenticated. You may need to log in again.');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error notifying parent window:', e);
|
||||
setMessage('Error communicating with parent window.');
|
||||
}
|
||||
};
|
||||
|
||||
if (status !== 'loading') {
|
||||
notifyParent();
|
||||
}
|
||||
}, [session, status]);
|
||||
|
||||
// This page is meant to be loaded in an iframe, so keep it minimal
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'sans-serif', color: '#666' }}>
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -10,9 +10,60 @@ export function SignOutHandler() {
|
||||
useEffect(() => {
|
||||
const handleSignOut = async () => {
|
||||
try {
|
||||
// First, attempt to sign out from NextAuth explicitly
|
||||
// Store the user ID before signout clears the session
|
||||
const userId = session?.user?.id;
|
||||
console.log('Starting comprehensive logout process');
|
||||
|
||||
// First trigger server-side session cleanup
|
||||
if (userId) {
|
||||
console.log('Triggering server-side session cleanup');
|
||||
try {
|
||||
const cleanupResponse = await fetch('/api/auth/session-cleanup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ userId }),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const cleanupResult = await cleanupResponse.json();
|
||||
console.log('Server cleanup result:', cleanupResult);
|
||||
} catch (cleanupError) {
|
||||
console.error('Error during server-side cleanup:', cleanupError);
|
||||
// Continue with logout even if cleanup fails
|
||||
}
|
||||
}
|
||||
|
||||
// Then, attempt to sign out from NextAuth explicitly
|
||||
await signOut({ redirect: false });
|
||||
|
||||
// Clear Rocket Chat authentication tokens
|
||||
try {
|
||||
console.log('Clearing Rocket Chat tokens');
|
||||
// Remove cookies
|
||||
document.cookie = `rc_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${window.location.hostname}; SameSite=None; Secure`;
|
||||
document.cookie = `rc_uid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${window.location.hostname}; SameSite=None; Secure`;
|
||||
|
||||
// Remove localStorage items
|
||||
localStorage.removeItem('Meteor.loginToken');
|
||||
localStorage.removeItem('Meteor.userId');
|
||||
|
||||
// Try to send logout to Rocket Chat server
|
||||
const rocketChatBaseUrl = process.env.NEXT_PUBLIC_IFRAME_PAROLE_URL?.split('/channel')[0];
|
||||
if (rocketChatBaseUrl) {
|
||||
// This is a best-effort logout - we don't wait for it to complete
|
||||
fetch(`${rocketChatBaseUrl}/api/v1/logout`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).catch(e => console.error('Failed to notify Rocket Chat server about logout:', e));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error clearing Rocket Chat tokens:', e);
|
||||
}
|
||||
|
||||
// Then clear all auth-related cookies to ensure we break any local sessions
|
||||
clearAuthCookies();
|
||||
|
||||
|
||||
108
lib/redis.ts
108
lib/redis.ts
@ -494,4 +494,112 @@ export async function getCachedEmailCredentials(
|
||||
accountId: string
|
||||
): Promise<EmailCredentials | null> {
|
||||
return getEmailCredentials(userId, accountId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up all Redis data related to a user during logout
|
||||
* This is critical to ensure proper session cleanup and prevent auth conflicts
|
||||
*/
|
||||
export async function cleanupUserSessions(userId: string): Promise<void> {
|
||||
if (!userId) {
|
||||
console.error('Cannot cleanup sessions: Missing userId');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Performing complete Redis cleanup for user ${userId}`);
|
||||
const redis = getRedisClient();
|
||||
|
||||
try {
|
||||
// 1. First get all the user's email accounts to ensure we clean everything
|
||||
const userAccountPattern = `email:credentials:${userId}:*`;
|
||||
const accountKeys: string[] = [];
|
||||
|
||||
// Use SCAN to find all account keys
|
||||
let cursor = '0';
|
||||
do {
|
||||
const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', userAccountPattern, 'COUNT', 100);
|
||||
cursor = nextCursor;
|
||||
|
||||
if (keys.length > 0) {
|
||||
accountKeys.push(...keys);
|
||||
}
|
||||
} while (cursor !== '0');
|
||||
|
||||
console.log(`Found ${accountKeys.length} email accounts for user ${userId}`);
|
||||
|
||||
// Extract accountIds from the keys
|
||||
const accountIds = accountKeys.map(key => {
|
||||
const parts = key.split(':');
|
||||
return parts[3]; // Format is email:credentials:userId:accountId
|
||||
});
|
||||
|
||||
// 2. Remove email content and email list caches for each account
|
||||
for (const accountId of accountIds) {
|
||||
// Delete email lists
|
||||
const emailListPattern = `email:list:${userId}:${accountId}:*`;
|
||||
let listCursor = '0';
|
||||
let deletedEmailLists = 0;
|
||||
|
||||
do {
|
||||
const [nextCursor, keys] = await redis.scan(listCursor, 'MATCH', emailListPattern, 'COUNT', 100);
|
||||
listCursor = nextCursor;
|
||||
|
||||
if (keys.length > 0) {
|
||||
await redis.del(...keys);
|
||||
deletedEmailLists += keys.length;
|
||||
}
|
||||
} while (listCursor !== '0');
|
||||
|
||||
// Delete email content
|
||||
const emailContentPattern = `email:content:${userId}:${accountId}:*`;
|
||||
let contentCursor = '0';
|
||||
let deletedEmailContent = 0;
|
||||
|
||||
do {
|
||||
const [nextCursor, keys] = await redis.scan(contentCursor, 'MATCH', emailContentPattern, 'COUNT', 100);
|
||||
contentCursor = nextCursor;
|
||||
|
||||
if (keys.length > 0) {
|
||||
await redis.del(...keys);
|
||||
deletedEmailContent += keys.length;
|
||||
}
|
||||
} while (contentCursor !== '0');
|
||||
|
||||
console.log(`Cleaned up ${deletedEmailLists} email lists and ${deletedEmailContent} email content items for account ${accountId}`);
|
||||
}
|
||||
|
||||
// 3. Remove credential entries
|
||||
if (accountKeys.length > 0) {
|
||||
await redis.del(...accountKeys);
|
||||
console.log(`Removed ${accountKeys.length} credential entries`);
|
||||
}
|
||||
|
||||
// 4. Remove IMAP session
|
||||
const sessionKey = KEYS.SESSION(userId);
|
||||
await redis.del(sessionKey);
|
||||
console.log(`Removed IMAP session data for user ${userId}`);
|
||||
|
||||
// 5. Remove any other user-specific data that might be present
|
||||
const otherUserDataPattern = `*:${userId}:*`;
|
||||
let otherCursor = '0';
|
||||
let deletedOtherData = 0;
|
||||
|
||||
do {
|
||||
const [nextCursor, keys] = await redis.scan(otherCursor, 'MATCH', otherUserDataPattern, 'COUNT', 100);
|
||||
otherCursor = nextCursor;
|
||||
|
||||
if (keys.length > 0) {
|
||||
await redis.del(...keys);
|
||||
deletedOtherData += keys.length;
|
||||
}
|
||||
} while (otherCursor !== '0');
|
||||
|
||||
if (deletedOtherData > 0) {
|
||||
console.log(`Removed ${deletedOtherData} additional user-specific entries`);
|
||||
}
|
||||
|
||||
console.log(`Successfully completed Redis cleanup for user ${userId}`);
|
||||
} catch (error) {
|
||||
console.error(`Error during Redis cleanup for user ${userId}:`, error);
|
||||
}
|
||||
}
|
||||
@ -1379,4 +1379,56 @@ export async function testEmailConnection(credentials: EmailCredentials): Promis
|
||||
error: `IMAP connection failed: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all IMAP connections for a user during logout
|
||||
*/
|
||||
export async function closeUserImapConnections(userId: string): Promise<number> {
|
||||
if (!userId) {
|
||||
console.error('Cannot close IMAP connections: Missing userId');
|
||||
return 0;
|
||||
}
|
||||
|
||||
console.log(`Closing all IMAP connections for user ${userId}`);
|
||||
let closedCount = 0;
|
||||
|
||||
// Get all connection keys that start with this userId
|
||||
const userConnectionKeys = Object.keys(connectionPool).filter(key =>
|
||||
key.startsWith(`${userId}:`)
|
||||
);
|
||||
|
||||
if (userConnectionKeys.length === 0) {
|
||||
console.log(`No active IMAP connections found for user ${userId}`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
console.log(`Found ${userConnectionKeys.length} active IMAP connections to close`);
|
||||
|
||||
// Close each connection
|
||||
for (const key of userConnectionKeys) {
|
||||
try {
|
||||
const connection = connectionPool[key];
|
||||
if (connection.isConnecting) {
|
||||
console.log(`Connection ${key} is still being established, marking for removal`);
|
||||
} else if (connection.client && connection.client.usable) {
|
||||
// Make a best-effort attempt to log out properly
|
||||
try {
|
||||
await connection.client.logout();
|
||||
console.log(`Successfully closed IMAP connection ${key}`);
|
||||
} catch (logoutError) {
|
||||
console.error(`Error during IMAP logout for ${key}:`, logoutError);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from the pool regardless of logout success
|
||||
delete connectionPool[key];
|
||||
closedCount++;
|
||||
} catch (error) {
|
||||
console.error(`Error closing IMAP connection ${key}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Closed ${closedCount} IMAP connections for user ${userId}`);
|
||||
return closedCount;
|
||||
}
|
||||
27
middleware.ts
Normal file
27
middleware.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
// Maximum cookie size in bytes (a bit less than 4KB to be safe)
|
||||
const MAX_COOKIE_SIZE = 3800;
|
||||
|
||||
// This middleware runs before any request
|
||||
export function middleware(request: NextRequest) {
|
||||
// Force NextAuth environment variables at runtime
|
||||
process.env.NEXTAUTH_COOKIE_SIZE_LIMIT = MAX_COOKIE_SIZE.toString();
|
||||
|
||||
// Set defaults for cookie security
|
||||
process.env.NEXTAUTH_CALLBACK = 'false';
|
||||
process.env.NEXTAUTH_SESSION_STORE_SESSION_TOKEN = 'false';
|
||||
process.env.NEXTAUTH_JWT_STORE_RAW_TOKEN = 'false';
|
||||
|
||||
// Continue with the request
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// Configure the middleware to run on specific paths
|
||||
export const config = {
|
||||
matcher: [
|
||||
// Apply to all routes except static files and api routes that aren't auth
|
||||
'/((?!_next/static|_next/image|favicon.ico|public).*)',
|
||||
],
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user