courrier redis
This commit is contained in:
parent
de728b9139
commit
973c6e54c1
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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
302
lib/redis.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
112
node_modules/.package-lock.json
generated
vendored
@ -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
116
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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
68
scripts/test-redis.js
Normal 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();
|
||||||
72
yarn.lock
72
yarn.lock
@ -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"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user