courrier preview
This commit is contained in:
parent
3fb8266a9e
commit
a7dbd7cd85
4
.env
4
.env
@ -80,3 +80,7 @@ NEWS_API_URL="http://172.16.0.104:8000"
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=mySecretPassword
|
||||
|
||||
MICROSOFT_CLIENT_ID="afaffea5-4e10-462a-aa64-e73baf642c57"
|
||||
MICROSOFT_CLIENT_SECRET="eIx8Q~N3ZnXTjTsVM3ECZio4G7t.BO6AYlD1-b2h"
|
||||
MICROSOFT_REDIRECT_URI="https://lab.slm-lab.net/ms"
|
||||
125
app/api/courrier/microsoft/callback/route.ts
Normal file
125
app/api/courrier/microsoft/callback/route.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { exchangeCodeForTokens } from '@/lib/services/microsoft-oauth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { testEmailConnection, saveUserEmailCredentials } from '@/lib/services/email-service';
|
||||
import { invalidateFolderCache } from '@/lib/redis';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
// Authenticate user
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
const body = await request.json();
|
||||
const { code, state } = body;
|
||||
|
||||
if (!code || !state) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required parameters' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate state parameter to prevent CSRF
|
||||
try {
|
||||
const decodedState = JSON.parse(Buffer.from(state, 'base64').toString());
|
||||
|
||||
// Check if state contains valid userId and is not expired (10 minutes)
|
||||
if (decodedState.userId !== session.user.id ||
|
||||
Date.now() - decodedState.timestamp > 10 * 60 * 1000) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid or expired state parameter' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid state parameter' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Exchange code for tokens
|
||||
const tokens = await exchangeCodeForTokens(code);
|
||||
|
||||
// Extract user email from token (would require token decoding in production)
|
||||
// For this implementation, we'll use a temporary email
|
||||
const userEmail = `${session.user.email || 'user'}@microsoft.com`;
|
||||
|
||||
// Create credentials object for Microsoft account
|
||||
const credentials = {
|
||||
email: userEmail,
|
||||
// Use Microsoft's IMAP server for Outlook/Office365
|
||||
host: 'outlook.office365.com',
|
||||
port: 993,
|
||||
secure: true,
|
||||
|
||||
// OAuth specific fields
|
||||
useOAuth: true,
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: tokens.refresh_token,
|
||||
tokenExpiry: Date.now() + (tokens.expires_in * 1000),
|
||||
|
||||
// Optional fields
|
||||
display_name: `Microsoft ${userEmail}`,
|
||||
color: '#0078D4', // Microsoft blue
|
||||
|
||||
// SMTP settings for Microsoft
|
||||
smtp_host: 'smtp.office365.com',
|
||||
smtp_port: 587,
|
||||
smtp_secure: false
|
||||
};
|
||||
|
||||
// Test connection before saving
|
||||
console.log(`Testing Microsoft OAuth connection for user ${session.user.id}`);
|
||||
const testResult = await testEmailConnection(credentials);
|
||||
|
||||
if (!testResult.imap) {
|
||||
return NextResponse.json(
|
||||
{ error: `Connection test failed: ${testResult.error || 'Could not connect to Microsoft IMAP server'}` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Save credentials to database and cache
|
||||
console.log(`Saving Microsoft account for user: ${session.user.id}`);
|
||||
await saveUserEmailCredentials(session.user.id, userEmail, credentials);
|
||||
|
||||
// Fetch the created account from the database
|
||||
const createdAccount = await prisma.mailCredentials.findFirst({
|
||||
where: { userId: session.user.id, email: userEmail },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
display_name: true,
|
||||
color: true,
|
||||
}
|
||||
});
|
||||
|
||||
// Invalidate any existing folder caches
|
||||
await invalidateFolderCache(session.user.id, userEmail, '*');
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
account: createdAccount,
|
||||
message: 'Microsoft account added successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error processing Microsoft callback:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to process Microsoft authentication',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
41
app/api/courrier/microsoft/route.ts
Normal file
41
app/api/courrier/microsoft/route.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { getMicrosoftAuthUrl } from '@/lib/services/microsoft-oauth';
|
||||
|
||||
// Endpoint to initiate Microsoft OAuth flow
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
// Authenticate user
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Create a state parameter with the user's ID to prevent CSRF
|
||||
const state = Buffer.from(JSON.stringify({
|
||||
userId: session.user.id,
|
||||
timestamp: Date.now()
|
||||
})).toString('base64');
|
||||
|
||||
// Generate the authorization URL
|
||||
const authUrl = getMicrosoftAuthUrl(state);
|
||||
|
||||
return NextResponse.json({
|
||||
authUrl,
|
||||
state
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error initiating Microsoft OAuth flow:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to initiate Microsoft OAuth flow',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
87
app/ms/page.tsx
Normal file
87
app/ms/page.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function MicrosoftCallbackPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [status, setStatus] = useState<string>('Processing authentication...');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function handleCallback() {
|
||||
try {
|
||||
// Extract code and state from URL
|
||||
const code = searchParams.get('code');
|
||||
const state = searchParams.get('state');
|
||||
const errorMsg = searchParams.get('error');
|
||||
|
||||
if (errorMsg) {
|
||||
setError(`Authentication error: ${errorMsg}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!code || !state) {
|
||||
setError('Missing required parameters');
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('Authentication successful. Exchanging code for tokens...');
|
||||
|
||||
// Send code to our API to exchange for tokens
|
||||
const response = await fetch('/api/courrier/microsoft/callback', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ code, state }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setError(data.error || 'Failed to process authentication');
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('Account connected successfully!');
|
||||
|
||||
// Redirect back to email client after a short delay
|
||||
setTimeout(() => {
|
||||
router.push('/courrier');
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
setError('An unexpected error occurred');
|
||||
console.error('Callback processing error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
handleCallback();
|
||||
}, [router, searchParams]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50">
|
||||
<div className="w-full max-w-md p-8 space-y-4 bg-white rounded-xl shadow-md">
|
||||
<h1 className="text-2xl font-bold text-center">Microsoft Account Connection</h1>
|
||||
|
||||
{error ? (
|
||||
<div className="p-4 bg-red-50 text-red-700 rounded-md">
|
||||
<p className="font-medium">Error</p>
|
||||
<p>{error}</p>
|
||||
<button
|
||||
onClick={() => router.push('/courrier')}
|
||||
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
Return to Email Client
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 bg-blue-50 text-blue-700 rounded-md">
|
||||
<p>{status}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -227,6 +227,23 @@ export default function EmailSidebar({
|
||||
);
|
||||
};
|
||||
|
||||
// Add Microsoft button logic
|
||||
const handleConnectMicrosoft = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/courrier/microsoft');
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.authUrl) {
|
||||
// Redirect to Microsoft's authorization page
|
||||
window.location.href = data.authUrl;
|
||||
} else {
|
||||
console.error('Failed to initiate Microsoft authentication:', data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error connecting Microsoft account:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-60 bg-white/95 backdrop-blur-sm border-r border-gray-100 flex flex-col md:flex" style={{display: "flex !important"}}>
|
||||
{/* Courrier Title */}
|
||||
@ -441,6 +458,24 @@ export default function EmailSidebar({
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Add Microsoft option */}
|
||||
<div className="mt-2 pt-2 border-t border-gray-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleConnectMicrosoft}
|
||||
className="w-full flex items-center justify-center gap-2 h-7 text-xs bg-[#0078D4] hover:bg-[#106EBE] text-white rounded-md px-2 py-0"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 23 23">
|
||||
<path fill="#f3f3f3" d="M0 0h23v23H0z" />
|
||||
<path fill="#f35325" d="M1 1h10v10H1z" />
|
||||
<path fill="#81bc06" d="M12 1h10v10H12z" />
|
||||
<path fill="#05a6f0" d="M1 12h10v10H1z" />
|
||||
<path fill="#ffba08" d="M12 12h10v10H12z" />
|
||||
</svg>
|
||||
Connect Microsoft Account
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
invalidateEmailContentCache
|
||||
} from '@/lib/redis';
|
||||
import { EmailCredentials, EmailMessage, EmailAddress, EmailAttachment } from '@/lib/types';
|
||||
import { ensureFreshToken } from './token-refresh';
|
||||
|
||||
// Types specific to this service
|
||||
export interface EmailListResult {
|
||||
@ -250,6 +251,22 @@ export async function getImapConnection(
|
||||
await cacheEmailCredentials(userId, accountId, credentials);
|
||||
}
|
||||
|
||||
// If using OAuth, ensure we have a fresh token
|
||||
if (credentials.useOAuth) {
|
||||
try {
|
||||
console.log(`Ensuring fresh token for OAuth account ${credentials.email}`);
|
||||
const { accessToken, success } = await ensureFreshToken(userId, credentials.email);
|
||||
|
||||
if (success) {
|
||||
credentials.accessToken = accessToken;
|
||||
} else {
|
||||
console.error(`Failed to refresh token for ${credentials.email}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error refreshing token for ${credentials.email}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize connection tracking
|
||||
connectionPool[connectionKey] = {
|
||||
client: null as any,
|
||||
@ -314,14 +331,23 @@ export async function getImapConnection(
|
||||
* Helper function to create a new IMAP connection
|
||||
*/
|
||||
async function createImapConnection(credentials: EmailCredentials, connectionKey: string): Promise<ImapFlow> {
|
||||
// Configure auth based on whether we're using OAuth or password
|
||||
const auth = credentials.useOAuth
|
||||
? {
|
||||
user: credentials.email,
|
||||
// Use XOAUTH2 authentication for Microsoft accounts
|
||||
accessToken: credentials.accessToken
|
||||
}
|
||||
: {
|
||||
user: credentials.email,
|
||||
pass: credentials.password,
|
||||
};
|
||||
|
||||
const client = new ImapFlow({
|
||||
host: credentials.host,
|
||||
port: credentials.port,
|
||||
secure: credentials.secure ?? true,
|
||||
auth: {
|
||||
user: credentials.email,
|
||||
pass: credentials.password,
|
||||
},
|
||||
auth,
|
||||
logger: false,
|
||||
emitLogs: false,
|
||||
tls: {
|
||||
@ -962,15 +988,23 @@ export async function sendEmail(
|
||||
};
|
||||
}
|
||||
|
||||
// Configure SMTP auth based on OAuth or password
|
||||
const auth = credentials.useOAuth
|
||||
? {
|
||||
user: credentials.email,
|
||||
accessToken: credentials.accessToken
|
||||
}
|
||||
: {
|
||||
user: credentials.email,
|
||||
pass: credentials.password,
|
||||
};
|
||||
|
||||
// Create SMTP transporter with user's SMTP settings if available
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: credentials.smtp_host || 'smtp.infomaniak.com',
|
||||
port: credentials.smtp_port || 587,
|
||||
secure: credentials.smtp_secure || false,
|
||||
auth: {
|
||||
user: credentials.email,
|
||||
pass: credentials.password,
|
||||
},
|
||||
auth,
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
@ -1036,20 +1070,31 @@ export async function testEmailConnection(credentials: EmailCredentials): Promis
|
||||
}> {
|
||||
console.log('Testing connection with:', {
|
||||
...credentials,
|
||||
password: '***'
|
||||
password: credentials.password ? '***' : undefined,
|
||||
accessToken: credentials.accessToken ? '***' : undefined,
|
||||
refreshToken: credentials.refreshToken ? '***' : undefined
|
||||
});
|
||||
|
||||
// Test IMAP connection
|
||||
try {
|
||||
console.log(`Testing IMAP connection to ${credentials.host}:${credentials.port} for ${credentials.email}`);
|
||||
|
||||
// Configure auth based on whether we're using OAuth or password
|
||||
const auth = credentials.useOAuth
|
||||
? {
|
||||
user: credentials.email,
|
||||
accessToken: credentials.accessToken
|
||||
}
|
||||
: {
|
||||
user: credentials.email,
|
||||
pass: credentials.password,
|
||||
};
|
||||
|
||||
const client = new ImapFlow({
|
||||
host: credentials.host,
|
||||
port: credentials.port,
|
||||
secure: credentials.secure ?? true,
|
||||
auth: {
|
||||
user: credentials.email,
|
||||
pass: credentials.password,
|
||||
},
|
||||
auth,
|
||||
logger: false,
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
@ -1068,14 +1113,23 @@ export async function testEmailConnection(credentials: EmailCredentials): Promis
|
||||
if (credentials.smtp_host && credentials.smtp_port) {
|
||||
try {
|
||||
console.log(`Testing SMTP connection to ${credentials.smtp_host}:${credentials.smtp_port}`);
|
||||
|
||||
// Configure SMTP auth based on OAuth or password
|
||||
const smtpAuth = credentials.useOAuth
|
||||
? {
|
||||
user: credentials.email,
|
||||
accessToken: credentials.accessToken
|
||||
}
|
||||
: {
|
||||
user: credentials.email,
|
||||
pass: credentials.password,
|
||||
};
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: credentials.smtp_host,
|
||||
port: credentials.smtp_port,
|
||||
secure: credentials.smtp_secure ?? false,
|
||||
auth: {
|
||||
user: credentials.email,
|
||||
pass: credentials.password,
|
||||
},
|
||||
auth: smtpAuth,
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
|
||||
109
lib/services/microsoft-oauth.ts
Normal file
109
lib/services/microsoft-oauth.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import axios from 'axios';
|
||||
|
||||
// Microsoft OAuth URLs
|
||||
const MICROSOFT_AUTHORIZE_URL = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize';
|
||||
const MICROSOFT_TOKEN_URL = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
|
||||
|
||||
// Client configuration from environment variables
|
||||
const clientId = process.env.MICROSOFT_CLIENT_ID;
|
||||
const clientSecret = process.env.MICROSOFT_CLIENT_SECRET;
|
||||
const redirectUri = process.env.MICROSOFT_REDIRECT_URI;
|
||||
|
||||
// Required scopes for IMAP and SMTP access
|
||||
const REQUIRED_SCOPES = [
|
||||
'offline_access',
|
||||
'https://outlook.office.com/IMAP.AccessAsUser.All',
|
||||
'https://outlook.office.com/SMTP.Send'
|
||||
].join(' ');
|
||||
|
||||
/**
|
||||
* Generates the authorization URL for Microsoft OAuth
|
||||
*/
|
||||
export function getMicrosoftAuthUrl(state: string): string {
|
||||
const params = new URLSearchParams({
|
||||
client_id: clientId!,
|
||||
response_type: 'code',
|
||||
redirect_uri: redirectUri!,
|
||||
scope: REQUIRED_SCOPES,
|
||||
state,
|
||||
response_mode: 'query'
|
||||
});
|
||||
|
||||
return `${MICROSOFT_AUTHORIZE_URL}?${params.toString()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange authorization code for tokens
|
||||
*/
|
||||
export async function exchangeCodeForTokens(code: string): Promise<{
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
expires_in: number;
|
||||
}> {
|
||||
const params = new URLSearchParams({
|
||||
client_id: clientId!,
|
||||
client_secret: clientSecret!,
|
||||
code,
|
||||
redirect_uri: redirectUri!,
|
||||
grant_type: 'authorization_code'
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await axios.post(MICROSOFT_TOKEN_URL, params.toString(), {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
access_token: response.data.access_token,
|
||||
refresh_token: response.data.refresh_token,
|
||||
expires_in: response.data.expires_in
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error exchanging code for tokens:', error);
|
||||
throw new Error('Failed to exchange authorization code for tokens');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh an access token using a refresh token
|
||||
*/
|
||||
export async function refreshAccessToken(refreshToken: string): Promise<{
|
||||
access_token: string;
|
||||
refresh_token?: string;
|
||||
expires_in: number;
|
||||
}> {
|
||||
const params = new URLSearchParams({
|
||||
client_id: clientId!,
|
||||
client_secret: clientSecret!,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: 'refresh_token',
|
||||
scope: REQUIRED_SCOPES
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await axios.post(MICROSOFT_TOKEN_URL, params.toString(), {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
access_token: response.data.access_token,
|
||||
refresh_token: response.data.refresh_token,
|
||||
expires_in: response.data.expires_in
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error refreshing token:', error);
|
||||
throw new Error('Failed to refresh access token');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create special XOAUTH2 string for IMAP authentication
|
||||
*/
|
||||
export function createXOAuth2Token(email: string, accessToken: string): string {
|
||||
const auth = `user=${email}\x01auth=Bearer ${accessToken}\x01\x01`;
|
||||
return Buffer.from(auth).toString('base64');
|
||||
}
|
||||
79
lib/services/token-refresh.ts
Normal file
79
lib/services/token-refresh.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { refreshAccessToken } from './microsoft-oauth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { getRedisClient, KEYS } from '@/lib/redis';
|
||||
|
||||
/**
|
||||
* Check if a token is expired or about to expire (within 5 minutes)
|
||||
*/
|
||||
export function isTokenExpired(expiryTimestamp: number): boolean {
|
||||
const fiveMinutesInMs = 5 * 60 * 1000;
|
||||
return Date.now() + fiveMinutesInMs >= expiryTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh an access token if it's expired or about to expire
|
||||
*/
|
||||
export async function ensureFreshToken(
|
||||
userId: string,
|
||||
email: string
|
||||
): Promise<{ accessToken: string; success: boolean }> {
|
||||
try {
|
||||
// Get stored credentials using raw query until Prisma schema is updated
|
||||
const credentials = await prisma.$queryRaw`
|
||||
SELECT "useOAuth", "refreshToken", "accessToken", "tokenExpiry"
|
||||
FROM "MailCredentials"
|
||||
WHERE "userId" = ${userId} AND "email" = ${email}
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
const credData = Array.isArray(credentials) && credentials.length > 0
|
||||
? credentials[0]
|
||||
: null;
|
||||
|
||||
// If not OAuth or missing refresh token, return failure
|
||||
if (!credData?.useOAuth || !credData.refreshToken) {
|
||||
return { accessToken: '', success: false };
|
||||
}
|
||||
|
||||
// If token is still valid, return current token
|
||||
if (credData.tokenExpiry && credData.accessToken &&
|
||||
new Date(credData.tokenExpiry) > new Date(Date.now() + 5 * 60 * 1000)) {
|
||||
return { accessToken: credData.accessToken, success: true };
|
||||
}
|
||||
|
||||
// Token is expired or about to expire, refresh it
|
||||
console.log(`Refreshing token for user ${userId}, account ${email}`);
|
||||
const tokens = await refreshAccessToken(credData.refreshToken);
|
||||
|
||||
// Update database with new token information using raw query
|
||||
await prisma.$executeRaw`
|
||||
UPDATE "MailCredentials"
|
||||
SET
|
||||
"accessToken" = ${tokens.access_token},
|
||||
"refreshToken" = ${tokens.refresh_token || credData.refreshToken},
|
||||
"tokenExpiry" = ${new Date(Date.now() + (tokens.expires_in * 1000))}
|
||||
WHERE "userId" = ${userId} AND "email" = ${email}
|
||||
`;
|
||||
|
||||
// Update Redis cache
|
||||
const redis = getRedisClient();
|
||||
const key = KEYS.CREDENTIALS(userId, email);
|
||||
const credStr = await redis.get(key);
|
||||
|
||||
if (credStr) {
|
||||
const creds = JSON.parse(credStr);
|
||||
creds.accessToken = tokens.access_token;
|
||||
if (tokens.refresh_token) {
|
||||
creds.refreshToken = tokens.refresh_token;
|
||||
}
|
||||
creds.tokenExpiry = Date.now() + (tokens.expires_in * 1000);
|
||||
|
||||
await redis.set(key, JSON.stringify(creds), 'EX', 86400); // 24 hours
|
||||
}
|
||||
|
||||
return { accessToken: tokens.access_token, success: true };
|
||||
} catch (error) {
|
||||
console.error(`Error refreshing token for user ${userId}:`, error);
|
||||
return { accessToken: '', success: false };
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,12 @@ export interface EmailCredentials {
|
||||
secure?: boolean;
|
||||
encryptedPassword?: string;
|
||||
|
||||
// OAuth Settings (for Microsoft accounts)
|
||||
useOAuth?: boolean;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
tokenExpiry?: number;
|
||||
|
||||
// SMTP Settings
|
||||
smtp_host?: string;
|
||||
smtp_port?: number;
|
||||
|
||||
126
node_modules/.package-lock.json
generated
vendored
126
node_modules/.package-lock.json
generated
vendored
@ -2622,6 +2622,12 @@
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/atomic-sleep": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
|
||||
@ -2683,6 +2689,17 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@ -2841,7 +2858,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@ -3028,6 +3044,18 @@
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||
@ -3338,6 +3366,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
@ -3461,7 +3498,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
@ -3537,7 +3573,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -3547,7 +3582,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -3557,7 +3591,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
@ -3566,6 +3599,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
|
||||
@ -3742,6 +3790,26 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||
@ -3773,6 +3841,42 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
@ -3836,7 +3940,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
@ -3869,7 +3972,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
@ -3913,7 +4015,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -3939,7 +4040,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -3952,7 +4052,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
@ -4655,7 +4754,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -5643,6 +5741,12 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
||||
127
package-lock.json
generated
127
package-lock.json
generated
@ -49,6 +49,7 @@
|
||||
"@types/xmldom": "^0.1.34",
|
||||
"@xmldom/xmldom": "^0.9.8",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^1.9.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
@ -3597,6 +3598,12 @@
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/atomic-sleep": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
|
||||
@ -3658,6 +3665,17 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@ -3816,7 +3834,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@ -4003,6 +4020,18 @@
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||
@ -4313,6 +4342,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
@ -4436,7 +4474,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
@ -4512,7 +4549,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -4522,7 +4558,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -4532,7 +4567,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
@ -4541,6 +4575,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
|
||||
@ -4717,6 +4766,26 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||
@ -4748,6 +4817,42 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
@ -4811,7 +4916,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
@ -4844,7 +4948,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
@ -4888,7 +4991,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -4914,7 +5016,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -4927,7 +5028,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
@ -5630,7 +5730,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -6618,6 +6717,12 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
||||
@ -50,6 +50,7 @@
|
||||
"@types/xmldom": "^0.1.34",
|
||||
"@xmldom/xmldom": "^0.9.8",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^1.9.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
-- AlterTable to make password optional and add OAuth fields
|
||||
ALTER TABLE "MailCredentials"
|
||||
ALTER COLUMN "password" DROP NOT NULL,
|
||||
ADD COLUMN "useOAuth" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "refreshToken" TEXT,
|
||||
ADD COLUMN "accessToken" TEXT,
|
||||
ADD COLUMN "tokenExpiry" TIMESTAMP(3);
|
||||
@ -60,11 +60,17 @@ model MailCredentials {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
email String
|
||||
password String
|
||||
password String? // Make password optional
|
||||
host String
|
||||
port Int
|
||||
secure Boolean @default(true)
|
||||
|
||||
// OAuth Settings
|
||||
useOAuth Boolean @default(false)
|
||||
refreshToken String?
|
||||
accessToken String?
|
||||
tokenExpiry DateTime?
|
||||
|
||||
// SMTP Settings
|
||||
smtp_host String?
|
||||
smtp_port Int?
|
||||
|
||||
70
yarn.lock
70
yarn.lock
@ -1265,6 +1265,11 @@ async@^3:
|
||||
resolved "https://registry.npmjs.org/async/-/async-3.2.6.tgz"
|
||||
integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
atomic-sleep@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz"
|
||||
@ -1289,6 +1294,15 @@ available-typed-arrays@^1.0.7:
|
||||
dependencies:
|
||||
possible-typed-array-names "^1.0.0"
|
||||
|
||||
axios@^1.9.0:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz"
|
||||
integrity sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.6"
|
||||
form-data "^4.0.0"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
|
||||
@ -1469,6 +1483,13 @@ color@^4.2.3:
|
||||
color-convert "^2.0.1"
|
||||
color-string "^1.9.0"
|
||||
|
||||
combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
commander@^4.0.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz"
|
||||
@ -1663,6 +1684,11 @@ define-data-property@^1.1.4:
|
||||
es-errors "^1.3.0"
|
||||
gopd "^1.0.1"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
denque@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz"
|
||||
@ -1817,6 +1843,16 @@ es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
|
||||
es-set-tostringtag@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz"
|
||||
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
get-intrinsic "^1.2.6"
|
||||
has-tostringtag "^1.0.2"
|
||||
hasown "^2.0.2"
|
||||
|
||||
esbuild-register@3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz"
|
||||
@ -1925,6 +1961,11 @@ fill-range@^7.1.1:
|
||||
dependencies:
|
||||
to-regex-range "^5.0.1"
|
||||
|
||||
follow-redirects@^1.15.6:
|
||||
version "1.15.9"
|
||||
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz"
|
||||
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
|
||||
|
||||
for-each@^0.3.5:
|
||||
version "0.3.5"
|
||||
resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz"
|
||||
@ -1940,6 +1981,16 @@ foreground-child@^3.1.0:
|
||||
cross-spawn "^7.0.0"
|
||||
signal-exit "^4.0.1"
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz"
|
||||
integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
es-set-tostringtag "^2.1.0"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
formdata-polyfill@^4.0.10:
|
||||
version "4.0.10"
|
||||
resolved "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz"
|
||||
@ -1974,7 +2025,7 @@ function-bind@^1.1.2:
|
||||
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
|
||||
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||
|
||||
get-intrinsic@^1.2.4, get-intrinsic@^1.3.0:
|
||||
get-intrinsic@^1.2.4, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
|
||||
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
|
||||
@ -2535,6 +2586,18 @@ mime-db@^1.54.0:
|
||||
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz"
|
||||
integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
dependencies:
|
||||
mime-db "1.52.0"
|
||||
|
||||
mime-types@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz"
|
||||
@ -3061,6 +3124,11 @@ prop-types@^15.6.2, prop-types@^15.8.1:
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.13.1"
|
||||
|
||||
proxy-from-env@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
|
||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||
|
||||
punycode.js@2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user