courrier redis

This commit is contained in:
alma 2025-04-27 13:39:05 +02:00
parent de728b9139
commit 973c6e54c1
11 changed files with 827 additions and 36 deletions

View File

@ -10,6 +10,7 @@ import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth'; import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { getEmailContent, markEmailReadStatus } from '@/lib/services/email-service'; import { getEmailContent, markEmailReadStatus } from '@/lib/services/email-service';
import { getCachedEmailContent, invalidateEmailContentCache } from '@/lib/redis';
export async function GET( export async function GET(
request: Request, request: Request,
@ -36,10 +37,19 @@ export async function GET(
const folder = searchParams.get("folder") || "INBOX"; const folder = searchParams.get("folder") || "INBOX";
try { try {
// Try to get email from Redis cache first
const cachedEmail = await getCachedEmailContent(session.user.id, id);
if (cachedEmail) {
console.log(`Using cached email content for ${session.user.id}:${id}`);
return NextResponse.json(cachedEmail);
}
console.log(`Cache miss for email content ${session.user.id}:${id}, fetching from IMAP`);
// Use the email service to fetch the email content // Use the email service to fetch the email content
const email = await getEmailContent(session.user.id, id, folder); const email = await getEmailContent(session.user.id, id, folder);
// Return the complete email object instead of just partial data // Return the complete email object
return NextResponse.json(email); return NextResponse.json(email);
} catch (error: any) { } catch (error: any) {
console.error("Error fetching email content:", error); console.error("Error fetching email content:", error);
@ -106,6 +116,9 @@ export async function POST(
); );
} }
// Invalidate cache for this email
await invalidateEmailContentCache(session.user.id, id);
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
} catch (error: any) { } catch (error: any) {
console.error("Error in POST:", error); console.error("Error in POST:", error);

View File

@ -6,6 +6,7 @@ import {
getUserEmailCredentials, getUserEmailCredentials,
testEmailConnection testEmailConnection
} from '@/lib/services/email-service'; } from '@/lib/services/email-service';
import { cacheEmailCredentials, invalidateUserEmailCache } from '@/lib/redis';
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
@ -43,8 +44,11 @@ export async function POST(request: Request) {
{ status: 401 } { status: 401 }
); );
} }
// Invalidate all cached data for this user as they are changing their credentials
await invalidateUserEmailCache(session.user.id);
// Save credentials in the database // Save credentials in the database and Redis
await saveUserEmailCredentials(session.user.id, { await saveUserEmailCredentials(session.user.id, {
email, email,
password, password,

View File

@ -2,6 +2,11 @@ import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth'; import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { getEmails } from '@/lib/services/email-service'; import { getEmails } from '@/lib/services/email-service';
import {
getCachedEmailList,
cacheEmailList,
invalidateFolderCache
} from '@/lib/redis';
// Simple in-memory cache (will be removed in a future update) // Simple in-memory cache (will be removed in a future update)
interface EmailCacheEntry { interface EmailCacheEntry {
@ -31,17 +36,16 @@ export async function GET(request: Request) {
const folder = searchParams.get("folder") || "INBOX"; const folder = searchParams.get("folder") || "INBOX";
const searchQuery = searchParams.get("search") || ""; const searchQuery = searchParams.get("search") || "";
// Check cache - temporary until we implement a proper server-side cache // Try to get from Redis cache first, but only if it's not a search query
const cacheKey = `${session.user.id}:${folder}:${page}:${perPage}:${searchQuery}`; if (!searchQuery) {
const now = Date.now(); const cachedEmails = await getCachedEmailList(session.user.id, folder, page, perPage);
const cachedEmails = emailListCache[cacheKey]; if (cachedEmails) {
console.log(`Using Redis cached emails for ${session.user.id}:${folder}:${page}:${perPage}`);
if (cachedEmails && now - cachedEmails.timestamp < CACHE_TTL) { return NextResponse.json(cachedEmails);
console.log(`Using cached emails for ${cacheKey}`); }
return NextResponse.json(cachedEmails.data);
} }
console.log(`Cache miss for ${cacheKey}, fetching emails`); console.log(`Redis cache miss for ${session.user.id}:${folder}:${page}:${perPage}, fetching emails from IMAP`);
// Use the email service to fetch emails // Use the email service to fetch emails
const emailsResult = await getEmails( const emailsResult = await getEmails(
@ -52,12 +56,7 @@ export async function GET(request: Request) {
searchQuery searchQuery
); );
// Cache the results // The result is already cached in the getEmails function
emailListCache[cacheKey] = {
data: emailsResult,
timestamp: now
};
return NextResponse.json(emailsResult); return NextResponse.json(emailsResult);
} catch (error: any) { } catch (error: any) {
console.error("Error fetching emails:", error); console.error("Error fetching emails:", error);
@ -81,19 +80,16 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Missing emailId parameter' }, { status: 400 }); return NextResponse.json({ error: 'Missing emailId parameter' }, { status: 400 });
} }
// Invalidate cache entries for this folder or all folders if none specified // Invalidate Redis cache for the folder
const userId = session.user.id; if (folderName) {
Object.keys(emailListCache).forEach(key => { await invalidateFolderCache(session.user.id, folderName);
if (folderName) { } else {
if (key.includes(`${userId}:${folderName}`)) { // If no folder specified, invalidate all folders (using a wildcard pattern)
delete emailListCache[key]; const folders = ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk'];
} for (const folder of folders) {
} else { await invalidateFolderCache(session.user.id, folder);
if (key.startsWith(`${userId}:`)) {
delete emailListCache[key];
}
} }
}); }
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
} catch (error) { } catch (error) {

View File

@ -12,6 +12,16 @@ services:
volumes: volumes:
- db:/var/lib/postgresql/data - db:/var/lib/postgresql/data
redis:
image: redis:latest
command: redis-server --requirepass mySecretPassword
container_name: redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes: volumes:
db: db:
driver: local driver: local
redis_data:

302
lib/redis.ts Normal file
View File

@ -0,0 +1,302 @@
import Redis from 'ioredis';
import CryptoJS from 'crypto-js';
// Initialize Redis client
let redisClient: Redis | null = null;
/**
* Get a Redis client instance (singleton pattern)
*/
export function getRedisClient(): Redis {
if (!redisClient) {
const redisUrl = process.env.REDIS_URL || 'redis://:mySecretPassword@localhost:6379';
redisClient = new Redis(redisUrl, {
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
},
enableOfflineQueue: true,
maxRetriesPerRequest: 3
});
redisClient.on('error', (err) => {
console.error('Redis connection error:', err);
});
redisClient.on('connect', () => {
console.log('Successfully connected to Redis');
});
}
return redisClient;
}
/**
* Close Redis connection (useful for serverless environments)
*/
export async function closeRedisConnection(): Promise<void> {
if (redisClient) {
await redisClient.quit();
redisClient = null;
}
}
// Encryption key from environment variable or fallback
const getEncryptionKey = () => {
return process.env.REDIS_ENCRYPTION_KEY || 'default-encryption-key-change-in-production';
};
/**
* Encrypt sensitive data before storing in Redis
*/
export function encryptData(data: string): string {
return CryptoJS.AES.encrypt(data, getEncryptionKey()).toString();
}
/**
* Decrypt sensitive data retrieved from Redis
*/
export function decryptData(encryptedData: string): string {
const bytes = CryptoJS.AES.decrypt(encryptedData, getEncryptionKey());
return bytes.toString(CryptoJS.enc.Utf8);
}
// Cache key definitions
export const KEYS = {
CREDENTIALS: (userId: string) => `email:credentials:${userId}`,
SESSION: (userId: string) => `email:session:${userId}`,
EMAIL_LIST: (userId: string, folder: string, page: number, perPage: number) =>
`email:list:${userId}:${folder}:${page}:${perPage}`,
EMAIL_CONTENT: (userId: string, emailId: string) =>
`email:content:${userId}:${emailId}`
};
// TTL constants in seconds
export const TTL = {
CREDENTIALS: 60 * 60 * 24, // 24 hours
SESSION: 60 * 30, // 30 minutes
EMAIL_LIST: 60 * 5, // 5 minutes
EMAIL_CONTENT: 60 * 15 // 15 minutes
};
interface EmailCredentials {
email: string;
password?: string;
host: string;
port: number;
secure?: boolean;
encryptedPassword?: string;
}
interface ImapSessionData {
connectionId?: string;
lastActive: number;
mailboxes?: string[];
}
/**
* Cache email credentials in Redis
*/
export async function cacheEmailCredentials(
userId: string,
credentials: EmailCredentials
): Promise<void> {
const redis = getRedisClient();
const key = KEYS.CREDENTIALS(userId);
// Create a copy without the password to store
const secureCredentials: EmailCredentials = {
email: credentials.email,
host: credentials.host,
port: credentials.port,
secure: credentials.secure ?? true
};
// Encrypt password separately if it exists
if (credentials.password) {
secureCredentials.encryptedPassword = encryptData(credentials.password);
}
await redis.set(key, JSON.stringify(secureCredentials), 'EX', TTL.CREDENTIALS);
}
/**
* Get cached email credentials from Redis
*/
export async function getCachedEmailCredentials(
userId: string
): Promise<EmailCredentials | null> {
const redis = getRedisClient();
const key = KEYS.CREDENTIALS(userId);
const cachedData = await redis.get(key);
if (!cachedData) return null;
const credentials = JSON.parse(cachedData) as EmailCredentials;
// Decrypt password if it was encrypted
if (credentials.encryptedPassword) {
credentials.password = decryptData(credentials.encryptedPassword);
delete credentials.encryptedPassword;
}
return credentials;
}
/**
* Cache IMAP session data for quick reconnection
*/
export async function cacheImapSession(
userId: string,
sessionData: ImapSessionData
): Promise<void> {
const redis = getRedisClient();
const key = KEYS.SESSION(userId);
// Always update the lastActive timestamp
sessionData.lastActive = Date.now();
await redis.set(key, JSON.stringify(sessionData), 'EX', TTL.SESSION);
}
/**
* Get cached IMAP session data
*/
export async function getCachedImapSession(
userId: string
): Promise<ImapSessionData | null> {
const redis = getRedisClient();
const key = KEYS.SESSION(userId);
const cachedData = await redis.get(key);
if (!cachedData) return null;
return JSON.parse(cachedData) as ImapSessionData;
}
/**
* Cache email list in Redis
*/
export async function cacheEmailList(
userId: string,
folder: string,
page: number,
perPage: number,
data: any
): Promise<void> {
const redis = getRedisClient();
const key = KEYS.EMAIL_LIST(userId, folder, page, perPage);
await redis.set(key, JSON.stringify(data), 'EX', TTL.EMAIL_LIST);
}
/**
* Get cached email list from Redis
*/
export async function getCachedEmailList(
userId: string,
folder: string,
page: number,
perPage: number
): Promise<any | null> {
const redis = getRedisClient();
const key = KEYS.EMAIL_LIST(userId, folder, page, perPage);
const cachedData = await redis.get(key);
if (!cachedData) return null;
return JSON.parse(cachedData);
}
/**
* Cache email content in Redis
*/
export async function cacheEmailContent(
userId: string,
emailId: string,
data: any
): Promise<void> {
const redis = getRedisClient();
const key = KEYS.EMAIL_CONTENT(userId, emailId);
await redis.set(key, JSON.stringify(data), 'EX', TTL.EMAIL_CONTENT);
}
/**
* Get cached email content from Redis
*/
export async function getCachedEmailContent(
userId: string,
emailId: string
): Promise<any | null> {
const redis = getRedisClient();
const key = KEYS.EMAIL_CONTENT(userId, emailId);
const cachedData = await redis.get(key);
if (!cachedData) return null;
return JSON.parse(cachedData);
}
/**
* Invalidate all email caches for a folder
*/
export async function invalidateFolderCache(
userId: string,
folder: string
): Promise<void> {
const redis = getRedisClient();
const pattern = `email:list:${userId}:${folder}:*`;
// Use SCAN to find and delete keys matching the pattern
let cursor = '0';
do {
const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
cursor = nextCursor;
if (keys.length > 0) {
await redis.del(...keys);
}
} while (cursor !== '0');
}
/**
* Invalidate email content cache
*/
export async function invalidateEmailContentCache(
userId: string,
emailId: string
): Promise<void> {
const redis = getRedisClient();
const key = KEYS.EMAIL_CONTENT(userId, emailId);
await redis.del(key);
}
/**
* Invalidate all user email caches (email lists and content)
*/
export async function invalidateUserEmailCache(
userId: string
): Promise<void> {
const redis = getRedisClient();
// Patterns to delete
const patterns = [
`email:list:${userId}:*`,
`email:content:${userId}:*`
];
for (const pattern of patterns) {
let cursor = '0';
do {
const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
cursor = nextCursor;
if (keys.length > 0) {
await redis.del(...keys);
}
} while (cursor !== '0');
}
}

View File

@ -5,6 +5,18 @@ import { ImapFlow } from 'imapflow';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
import { simpleParser } from 'mailparser'; import { simpleParser } from 'mailparser';
import {
cacheEmailCredentials,
getCachedEmailCredentials,
cacheEmailList,
getCachedEmailList,
cacheEmailContent,
getCachedEmailContent,
cacheImapSession,
getCachedImapSession,
invalidateFolderCache,
invalidateEmailContentCache
} from '@/lib/redis';
// Types for the email service // Types for the email service
export interface EmailCredentials { export interface EmailCredentials {
@ -88,20 +100,40 @@ setInterval(() => {
* Get IMAP connection for a user, reusing existing connections when possible * Get IMAP connection for a user, reusing existing connections when possible
*/ */
export async function getImapConnection(userId: string): Promise<ImapFlow> { export async function getImapConnection(userId: string): Promise<ImapFlow> {
// Get credentials from database // First try to get credentials from Redis cache
const credentials = await getUserEmailCredentials(userId); let credentials = await getCachedEmailCredentials(userId);
// If not in cache, get from database and cache them
if (!credentials) { if (!credentials) {
throw new Error('No email credentials found'); credentials = await getUserEmailCredentials(userId);
if (!credentials) {
throw new Error('No email credentials found');
}
// Cache credentials for future use
await cacheEmailCredentials(userId, credentials);
} }
const connectionKey = `${userId}:${credentials.email}`; const connectionKey = `${userId}:${credentials.email}`;
const existingConnection = connectionPool[connectionKey]; const existingConnection = connectionPool[connectionKey];
// Try to get session data from Redis
const sessionData = await getCachedImapSession(userId);
// Return existing connection if available and connected // Return existing connection if available and connected
if (existingConnection) { if (existingConnection) {
try { try {
if (existingConnection.client.usable) { if (existingConnection.client.usable) {
existingConnection.lastUsed = Date.now(); existingConnection.lastUsed = Date.now();
// Update session data in Redis
if (sessionData) {
await cacheImapSession(userId, {
...sessionData,
lastActive: Date.now()
});
}
return existingConnection.client; return existingConnection.client;
} }
} catch (error) { } catch (error) {
@ -169,6 +201,7 @@ export async function saveUserEmailCredentials(
userId: string, userId: string,
credentials: EmailCredentials credentials: EmailCredentials
): Promise<void> { ): Promise<void> {
// Save to database
await prisma.mailCredentials.upsert({ await prisma.mailCredentials.upsert({
where: { userId }, where: { userId },
update: { update: {
@ -185,6 +218,9 @@ export async function saveUserEmailCredentials(
port: credentials.port port: credentials.port
} }
}); });
// Also cache in Redis
await cacheEmailCredentials(userId, credentials);
} }
// Helper type for IMAP fetch options // Helper type for IMAP fetch options
@ -207,6 +243,17 @@ export async function getEmails(
perPage: number = 20, perPage: number = 20,
searchQuery: string = '' searchQuery: string = ''
): Promise<EmailListResult> { ): Promise<EmailListResult> {
// Try to get from cache first
if (!searchQuery) {
const cachedResult = await getCachedEmailList(userId, folder, page, perPage);
if (cachedResult) {
console.log(`Using cached email list for ${userId}:${folder}:${page}:${perPage}`);
return cachedResult;
}
}
console.log(`Cache miss for emails ${userId}:${folder}:${page}:${perPage}, fetching from IMAP`);
const client = await getImapConnection(userId); const client = await getImapConnection(userId);
try { try {
@ -223,7 +270,14 @@ export async function getEmails(
// Empty result if no messages // Empty result if no messages
if (totalMessages === 0 || from > to) { if (totalMessages === 0 || from > to) {
const mailboxes = await getMailboxes(client); const mailboxes = await getMailboxes(client);
return {
// Cache mailbox list in session data
await cacheImapSession(userId, {
lastActive: Date.now(),
mailboxes
});
const result = {
emails: [], emails: [],
totalEmails: 0, totalEmails: 0,
page, page,
@ -232,6 +286,13 @@ export async function getEmails(
folder, folder,
mailboxes mailboxes
}; };
// Cache even empty results
if (!searchQuery) {
await cacheEmailList(userId, folder, page, perPage, result);
}
return result;
} }
// Search if needed // Search if needed
@ -362,7 +423,13 @@ export async function getEmails(
const mailboxes = await getMailboxes(client); const mailboxes = await getMailboxes(client);
return { // Cache mailbox list in session data
await cacheImapSession(userId, {
lastActive: Date.now(),
mailboxes
});
const result = {
emails, emails,
totalEmails: totalMessages, totalEmails: totalMessages,
page, page,
@ -371,6 +438,13 @@ export async function getEmails(
folder, folder,
mailboxes mailboxes
}; };
// Cache the result if it's not a search query
if (!searchQuery) {
await cacheEmailList(userId, folder, page, perPage, result);
}
return result;
} finally { } finally {
// Don't logout, keep connection in pool // Don't logout, keep connection in pool
if (folder !== 'INBOX') { if (folder !== 'INBOX') {
@ -391,6 +465,15 @@ export async function getEmailContent(
emailId: string, emailId: string,
folder: string = 'INBOX' folder: string = 'INBOX'
): Promise<EmailMessage> { ): Promise<EmailMessage> {
// Try to get from cache first
const cachedEmail = await getCachedEmailContent(userId, emailId);
if (cachedEmail) {
console.log(`Using cached email content for ${userId}:${emailId}`);
return cachedEmail;
}
console.log(`Cache miss for email content ${userId}:${emailId}, fetching from IMAP`);
const client = await getImapConnection(userId); const client = await getImapConnection(userId);
try { try {
@ -420,7 +503,7 @@ export async function getEmailContent(
// Preserve the raw HTML exactly as it was in the original email // Preserve the raw HTML exactly as it was in the original email
const rawHtml = parsedEmail.html || ''; const rawHtml = parsedEmail.html || '';
return { const email = {
id: emailId, id: emailId,
messageId: envelope.messageId, messageId: envelope.messageId,
subject: envelope.subject || "(No Subject)", subject: envelope.subject || "(No Subject)",
@ -462,6 +545,11 @@ export async function getEmailContent(
folder, folder,
contentFetched: true contentFetched: true
}; };
// Cache the email content
await cacheEmailContent(userId, emailId, email);
return email;
} finally { } finally {
try { try {
await client.mailboxClose(); await client.mailboxClose();
@ -491,6 +579,12 @@ export async function markEmailReadStatus(
await client.messageFlagsRemove(emailId, ['\\Seen']); await client.messageFlagsRemove(emailId, ['\\Seen']);
} }
// Invalidate content cache since the flags changed
await invalidateEmailContentCache(userId, emailId);
// Also invalidate folder cache because unread counts may have changed
await invalidateFolderCache(userId, folder);
return true; return true;
} catch (error) { } catch (error) {
console.error(`Error marking email ${emailId} as ${isRead ? 'read' : 'unread'}:`, error); console.error(`Error marking email ${emailId} as ${isRead ? 'read' : 'unread'}:`, error);

112
node_modules/.package-lock.json generated vendored
View File

@ -351,6 +351,12 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
}, },
"node_modules/@ioredis/commands": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==",
"license": "MIT"
},
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -2331,6 +2337,13 @@
"tslib": "^2.8.0" "tslib": "^2.8.0"
} }
}, },
"node_modules/@types/crypto-js": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-array": { "node_modules/@types/d3-array": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
@ -2950,6 +2963,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/cmdk": { "node_modules/cmdk": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.4.tgz", "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.4.tgz",
@ -3084,6 +3106,12 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/cssesc": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@ -3310,6 +3338,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@ -3408,6 +3445,18 @@
"url": "https://github.com/fb55/domutils?sponsor=1" "url": "https://github.com/fb55/domutils?sponsor=1"
} }
}, },
"node_modules/dotenv": {
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
"integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -4119,6 +4168,30 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/ioredis": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz",
"integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/ip-address": { "node_modules/ip-address": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
@ -4490,6 +4563,12 @@
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"license": "MIT"
},
"node_modules/lodash.get": { "node_modules/lodash.get": {
"version": "4.4.2", "version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
@ -4497,6 +4576,12 @@
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
"license": "MIT"
},
"node_modules/lodash.isequal": { "node_modules/lodash.isequal": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
@ -5938,6 +6023,27 @@
"decimal.js-light": "^2.4.1" "decimal.js-light": "^2.4.1"
} }
}, },
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
"license": "MIT",
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/regenerator-runtime": { "node_modules/regenerator-runtime": {
"version": "0.14.1", "version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
@ -6277,6 +6383,12 @@
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
"license": "MIT"
},
"node_modules/stream-browserify": { "node_modules/stream-browserify": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz",

116
package-lock.json generated
View File

@ -53,12 +53,15 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "1.0.4", "cmdk": "1.0.4",
"cookies-next": "^5.1.0", "cookies-next": "^5.1.0",
"crypto-js": "^4.2.0",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"dotenv": "^16.5.0",
"embla-carousel-react": "8.5.1", "embla-carousel-react": "8.5.1",
"fullcalendar": "^6.1.15", "fullcalendar": "^6.1.15",
"imap": "^0.8.19", "imap": "^0.8.19",
"imapflow": "^1.0.184", "imapflow": "^1.0.184",
"input-otp": "1.4.1", "input-otp": "1.4.1",
"ioredis": "^5.6.1",
"isomorphic-dompurify": "^2.24.0", "isomorphic-dompurify": "^2.24.0",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"libmime": "^5.3.6", "libmime": "^5.3.6",
@ -90,6 +93,7 @@
"webdav": "^5.8.0" "webdav": "^5.8.0"
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/imapflow": "^1.0.20", "@types/imapflow": "^1.0.20",
"@types/jsdom": "^21.1.7", "@types/jsdom": "^21.1.7",
"@types/mime": "^3.0.4", "@types/mime": "^3.0.4",
@ -1210,6 +1214,12 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
}, },
"node_modules/@ioredis/commands": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==",
"license": "MIT"
},
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -3302,6 +3312,13 @@
"tslib": "^2.8.0" "tslib": "^2.8.0"
} }
}, },
"node_modules/@types/crypto-js": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-array": { "node_modules/@types/d3-array": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
@ -3921,6 +3938,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/cmdk": { "node_modules/cmdk": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.4.tgz", "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.4.tgz",
@ -4055,6 +4081,12 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/cssesc": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@ -4281,6 +4313,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@ -4379,6 +4420,18 @@
"url": "https://github.com/fb55/domutils?sponsor=1" "url": "https://github.com/fb55/domutils?sponsor=1"
} }
}, },
"node_modules/dotenv": {
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
"integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -5090,6 +5143,30 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/ioredis": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz",
"integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/ip-address": { "node_modules/ip-address": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
@ -5461,6 +5538,12 @@
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"license": "MIT"
},
"node_modules/lodash.get": { "node_modules/lodash.get": {
"version": "4.4.2", "version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
@ -5468,6 +5551,12 @@
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
"license": "MIT"
},
"node_modules/lodash.isequal": { "node_modules/lodash.isequal": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
@ -6909,6 +6998,27 @@
"decimal.js-light": "^2.4.1" "decimal.js-light": "^2.4.1"
} }
}, },
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
"license": "MIT",
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/regenerator-runtime": { "node_modules/regenerator-runtime": {
"version": "0.14.1", "version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
@ -7248,6 +7358,12 @@
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
"license": "MIT"
},
"node_modules/stream-browserify": { "node_modules/stream-browserify": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz",

View File

@ -54,12 +54,15 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "1.0.4", "cmdk": "1.0.4",
"cookies-next": "^5.1.0", "cookies-next": "^5.1.0",
"crypto-js": "^4.2.0",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"dotenv": "^16.5.0",
"embla-carousel-react": "8.5.1", "embla-carousel-react": "8.5.1",
"fullcalendar": "^6.1.15", "fullcalendar": "^6.1.15",
"imap": "^0.8.19", "imap": "^0.8.19",
"imapflow": "^1.0.184", "imapflow": "^1.0.184",
"input-otp": "1.4.1", "input-otp": "1.4.1",
"ioredis": "^5.6.1",
"isomorphic-dompurify": "^2.24.0", "isomorphic-dompurify": "^2.24.0",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"libmime": "^5.3.6", "libmime": "^5.3.6",
@ -91,6 +94,7 @@
"webdav": "^5.8.0" "webdav": "^5.8.0"
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/imapflow": "^1.0.20", "@types/imapflow": "^1.0.20",
"@types/jsdom": "^21.1.7", "@types/jsdom": "^21.1.7",
"@types/mime": "^3.0.4", "@types/mime": "^3.0.4",

68
scripts/test-redis.js Normal file
View File

@ -0,0 +1,68 @@
#!/usr/bin/env node
const Redis = require('ioredis');
const dotenv = require('dotenv');
// Load environment variables
dotenv.config({ path: '.env.local' });
const redisUrl = process.env.REDIS_URL || 'redis://:mySecretPassword@localhost:6379';
// Connect to Redis
const redis = new Redis(redisUrl, {
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
}
});
// Test functions
async function testRedisConnection() {
try {
// Test basic connection
console.log('Testing Redis connection...');
await redis.ping();
console.log('✅ Redis connection successful!');
// Test setting a key
console.log('\nTesting setting a key...');
await redis.set('test-key', 'Hello from Redis test script');
console.log('✅ Successfully set test-key');
// Test getting a key
console.log('\nTesting getting a key...');
const value = await redis.get('test-key');
console.log(`✅ Successfully retrieved test-key: "${value}"`);
// Test expiry
console.log('\nTesting key expiration...');
await redis.set('expiring-key', 'This will expire in 5 seconds', 'EX', 5);
console.log('✅ Set key with 5 second expiration');
console.log('Waiting for key to expire...');
// Wait for expiration
await new Promise(resolve => setTimeout(resolve, 6000));
const expiredValue = await redis.get('expiring-key');
if (expiredValue === null) {
console.log('✅ Key successfully expired');
} else {
console.log('❌ Key did not expire as expected');
}
// Clean up
console.log('\nCleaning up...');
await redis.del('test-key');
console.log('✅ Removed test keys');
console.log('\n🎉 All Redis tests passed!');
} catch (error) {
console.error('❌ Redis test failed:', error);
} finally {
// Close connection
redis.disconnect();
}
}
// Run the test
testRedisConnection();

View File

@ -168,6 +168,11 @@
resolved "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz" resolved "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz"
integrity sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA== integrity sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==
"@ioredis/commands@^1.1.1":
version "1.2.0"
resolved "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz"
integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==
"@isaacs/cliui@^8.0.2": "@isaacs/cliui@^8.0.2":
version "8.0.2" version "8.0.2"
resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz"
@ -1029,6 +1034,11 @@
dependencies: dependencies:
tslib "^2.8.0" tslib "^2.8.0"
"@types/crypto-js@^4.2.2":
version "4.2.2"
resolved "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz"
integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==
"@types/d3-array@^3.0.3": "@types/d3-array@^3.0.3":
version "3.2.1" version "3.2.1"
resolved "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz" resolved "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz"
@ -1416,6 +1426,11 @@ clsx@^2.0.0, clsx@^2.1.1:
resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz" resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
cluster-key-slot@^1.1.0:
version "1.1.2"
resolved "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz"
integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==
cmdk@1.0.4: cmdk@1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.npmjs.org/cmdk/-/cmdk-1.0.4.tgz" resolved "https://registry.npmjs.org/cmdk/-/cmdk-1.0.4.tgz"
@ -1500,6 +1515,11 @@ crypt@0.0.2:
resolved "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz" resolved "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz"
integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==
crypto-js@^4.2.0:
version "4.2.0"
resolved "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz"
integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
cssesc@^3.0.0: cssesc@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz"
@ -1643,6 +1663,11 @@ define-data-property@^1.1.4:
es-errors "^1.3.0" es-errors "^1.3.0"
gopd "^1.0.1" gopd "^1.0.1"
denque@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz"
integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
detect-libc@^2.0.3: detect-libc@^2.0.3:
version "2.0.4" version "2.0.4"
resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz" resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz"
@ -1708,6 +1733,11 @@ domutils@^3.0.1:
domelementtype "^2.3.0" domelementtype "^2.3.0"
domhandler "^5.0.3" domhandler "^5.0.3"
dotenv@^16.5.0:
version "16.5.0"
resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz"
integrity sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==
dunder-proto@^1.0.1: dunder-proto@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz"
@ -2141,6 +2171,21 @@ input-otp@1.4.1:
resolved "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz" resolved "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz"
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
ioredis@^5.6.1:
version "5.6.1"
resolved "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz"
integrity sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==
dependencies:
"@ioredis/commands" "^1.1.1"
cluster-key-slot "^1.1.0"
debug "^4.3.4"
denque "^2.1.0"
lodash.defaults "^4.2.0"
lodash.isarguments "^3.1.0"
redis-errors "^1.2.0"
redis-parser "^3.0.0"
standard-as-callback "^2.1.0"
ip-address@^9.0.5: ip-address@^9.0.5:
version "9.0.5" version "9.0.5"
resolved "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz" resolved "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz"
@ -2375,11 +2420,21 @@ lodash.clonedeep@^4.5.0:
resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz" resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz"
integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
lodash.defaults@^4.2.0:
version "4.2.0"
resolved "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz"
integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==
lodash.get@^4.4.2: lodash.get@^4.4.2:
version "4.4.2" version "4.4.2"
resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz" resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz"
integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
lodash.isarguments@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz"
integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==
lodash.isequal@^4.5.0: lodash.isequal@^4.5.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz" resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz"
@ -3221,6 +3276,18 @@ recharts@2.15.0:
tiny-invariant "^1.3.1" tiny-invariant "^1.3.1"
victory-vendor "^36.6.8" victory-vendor "^36.6.8"
redis-errors@^1.0.0, redis-errors@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz"
integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==
redis-parser@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz"
integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==
dependencies:
redis-errors "^1.0.0"
regenerator-runtime@^0.14.0: regenerator-runtime@^0.14.0:
version "0.14.1" version "0.14.1"
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz"
@ -3418,6 +3485,11 @@ sprintf-js@^1.1.3:
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz" resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz"
integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==
standard-as-callback@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz"
integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==
stream-browserify@^3.0.0: stream-browserify@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz" resolved "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz"