This commit is contained in:
Matt 2025-06-10 15:01:04 +02:00
parent 8c0d8cae69
commit 4b6d3fd991
3 changed files with 133 additions and 12 deletions

View File

@ -14,8 +14,8 @@
<div v-else> <div v-else>
<!-- Compose New Email --> <!-- Email Controls -->
<div class="mb-4 d-flex align-center gap-2"> <div class="mb-4 d-flex align-center gap-2 flex-wrap">
<v-btn <v-btn
@click="showComposer = true" @click="showComposer = true"
color="primary" color="primary"
@ -33,6 +33,24 @@
> >
<v-tooltip activator="parent">Refresh Emails</v-tooltip> <v-tooltip activator="parent">Refresh Emails</v-tooltip>
</v-btn> </v-btn>
<v-spacer />
<v-btn
@click="reconnectEmail"
variant="text"
size="small"
prepend-icon="mdi-connection"
>
Reconnect
</v-btn>
<v-btn
@click="disconnectEmail"
variant="text"
size="small"
color="error"
prepend-icon="mdi-logout"
>
Disconnect
</v-btn>
</div> </div>
<!-- Email Thread List --> <!-- Email Thread List -->
@ -415,8 +433,14 @@ watch(() => props.interest.Id, () => {
}); });
const loadEmailThread = async () => { const loadEmailThread = async () => {
if (!sessionId.value) {
console.log('[ClientEmailSection] No sessionId available, skipping email thread load');
return;
}
isRefreshing.value = true; isRefreshing.value = true;
try { try {
console.log('[ClientEmailSection] Loading email thread for:', props.interest['Email Address']);
const response = await $fetch<{ const response = await $fetch<{
success: boolean; success: boolean;
emails: any[]; emails: any[];
@ -426,15 +450,18 @@ const loadEmailThread = async () => {
'x-tag': '094ut234' 'x-tag': '094ut234'
}, },
body: { body: {
email: props.interest['Email Address'] clientEmail: props.interest['Email Address'],
interestId: props.interest.Id.toString(),
sessionId: sessionId.value
} }
}); });
if (response.success) { if (response.success) {
console.log('[ClientEmailSection] Successfully loaded', response.emails?.length || 0, 'emails');
emailThreads.value = response.emails || []; emailThreads.value = response.emails || [];
} }
} catch (error) { } catch (error) {
console.error('Failed to load email thread:', error); console.error('[ClientEmailSection] Failed to load email thread:', error);
} finally { } finally {
isRefreshing.value = false; isRefreshing.value = false;
} }
@ -641,6 +668,24 @@ const onCredentialsSaved = (data: { sessionId: string }) => {
toast.success('Email credentials saved successfully'); toast.success('Email credentials saved successfully');
}; };
const disconnectEmail = () => {
// Clear stored credentials
localStorage.removeItem('emailSessionId');
localStorage.removeItem('emailSignature');
sessionId.value = '';
hasEmailCredentials.value = false;
emailThreads.value = [];
toast.success('Email disconnected');
};
const reconnectEmail = () => {
// Clear current session to force re-authentication
localStorage.removeItem('emailSessionId');
sessionId.value = '';
hasEmailCredentials.value = false;
toast.info('Please enter your email credentials');
};
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
if (!dateString) return ''; if (!dateString) return '';

View File

@ -6,7 +6,10 @@ import { updateInterest } from '~/server/utils/nocodb';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const xTagHeader = getRequestHeader(event, "x-tag"); const xTagHeader = getRequestHeader(event, "x-tag");
console.log('[Email Send] Request received with x-tag:', xTagHeader);
if (!xTagHeader || (xTagHeader !== "094ut234" && xTagHeader !== "pjnvü1230")) { if (!xTagHeader || (xTagHeader !== "094ut234" && xTagHeader !== "pjnvü1230")) {
console.error('[Email Send] Authentication failed - invalid x-tag');
throw createError({ statusCode: 401, statusMessage: "unauthenticated" }); throw createError({ statusCode: 401, statusMessage: "unauthenticated" });
} }
@ -23,7 +26,22 @@ export default defineEventHandler(async (event) => {
attachments = [] attachments = []
} = body; } = body;
console.log('[Email Send] Request body:', {
to,
subject,
hasBody: !!emailBody,
interestId,
hasSessionId: !!sessionId,
attachmentCount: attachments.length
});
if (!to || !subject || !emailBody || !sessionId) { if (!to || !subject || !emailBody || !sessionId) {
console.error('[Email Send] Missing required fields:', {
hasTo: !!to,
hasSubject: !!subject,
hasBody: !!emailBody,
hasSessionId: !!sessionId
});
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
statusMessage: "To, subject, body, and sessionId are required" statusMessage: "To, subject, body, and sessionId are required"
@ -31,8 +49,10 @@ export default defineEventHandler(async (event) => {
} }
// Get encrypted credentials from session // Get encrypted credentials from session
console.log('[Email Send] Getting credentials for sessionId:', sessionId);
const encryptedCredentials = getCredentialsFromSession(sessionId); const encryptedCredentials = getCredentialsFromSession(sessionId);
if (!encryptedCredentials) { if (!encryptedCredentials) {
console.error('[Email Send] No credentials found for sessionId:', sessionId);
throw createError({ throw createError({
statusCode: 401, statusCode: 401,
statusMessage: "Email credentials not found. Please reconnect." statusMessage: "Email credentials not found. Please reconnect."
@ -40,7 +60,22 @@ export default defineEventHandler(async (event) => {
} }
// Decrypt credentials // Decrypt credentials
const { email, password } = decryptCredentials(encryptedCredentials); console.log('[Email Send] Decrypting credentials');
let email: string;
let password: string;
try {
const decrypted = decryptCredentials(encryptedCredentials);
email = decrypted.email;
password = decrypted.password;
console.log('[Email Send] Successfully decrypted credentials for:', email);
} catch (decryptError) {
console.error('[Email Send] Failed to decrypt credentials:', decryptError);
throw createError({
statusCode: 401,
statusMessage: "Failed to decrypt email credentials. Please reconnect."
});
}
// Get user info for signature // Get user info for signature
const defaultName = email.split('@')[0].replace('.', ' ').replace(/\b\w/g, l => l.toUpperCase()); const defaultName = email.split('@')[0].replace('.', ' ').replace(/\b\w/g, l => l.toUpperCase());
@ -84,6 +119,7 @@ export default defineEventHandler(async (event) => {
}); });
// Prepare email attachments // Prepare email attachments
console.log('[Email Send] Processing', attachments.length, 'attachments');
const emailAttachments = []; const emailAttachments = [];
if (attachments && attachments.length > 0) { if (attachments && attachments.length > 0) {
const client = getMinioClient(); const client = getMinioClient();
@ -92,6 +128,7 @@ export default defineEventHandler(async (event) => {
try { try {
// Determine which bucket to use // Determine which bucket to use
const bucket = attachment.bucket || 'client-portal'; // Default to client-portal const bucket = attachment.bucket || 'client-portal'; // Default to client-portal
console.log('[Email Send] Processing attachment:', attachment.name, 'from bucket:', bucket, 'path:', attachment.path);
// Download file from MinIO // Download file from MinIO
const stream = await client.getObject(bucket, attachment.path); const stream = await client.getObject(bucket, attachment.path);
@ -104,13 +141,14 @@ export default defineEventHandler(async (event) => {
}); });
const content = Buffer.concat(chunks); const content = Buffer.concat(chunks);
console.log('[Email Send] Successfully downloaded attachment:', attachment.name, 'size:', content.length);
emailAttachments.push({ emailAttachments.push({
filename: attachment.name, filename: attachment.name,
content: content content: content
}); });
} catch (error) { } catch (error) {
console.error(`Failed to attach file ${attachment.name} from bucket ${attachment.bucket}:`, error); console.error(`[Email Send] Failed to attach file ${attachment.name} from bucket ${attachment.bucket}:`, error);
// Continue with other attachments // Continue with other attachments
} }
} }
@ -118,6 +156,8 @@ export default defineEventHandler(async (event) => {
// Send email // Send email
const fromName = sig.name || defaultName; const fromName = sig.name || defaultName;
console.log('[Email Send] Sending email from:', email, 'to:', to);
const info = await transporter.sendMail({ const info = await transporter.sendMail({
from: `"${fromName}" <${email}>`, from: `"${fromName}" <${email}>`,
to: to, to: to,
@ -126,6 +166,8 @@ export default defineEventHandler(async (event) => {
html: htmlBody, // HTML version with signature html: htmlBody, // HTML version with signature
attachments: emailAttachments attachments: emailAttachments
}); });
console.log('[Email Send] Email sent successfully, messageId:', info.messageId);
// Store email in MinIO for thread history and update EOI Time Sent // Store email in MinIO for thread history and update EOI Time Sent
if (interestId) { if (interestId) {
@ -175,7 +217,8 @@ export default defineEventHandler(async (event) => {
messageId: info.messageId messageId: info.messageId
}; };
} catch (error) { } catch (error) {
console.error('Failed to send email:', error); console.error('[Email Send] Failed to send email:', error);
console.error('[Email Send] Error stack:', error instanceof Error ? error.stack : 'No stack trace');
if (error instanceof Error) { if (error instanceof Error) {
throw createError({ throw createError({
statusCode: 500, statusCode: 500,

View File

@ -7,7 +7,10 @@ import mime from 'mime-types';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const xTagHeader = getRequestHeader(event, "x-tag"); const xTagHeader = getRequestHeader(event, "x-tag");
console.log('[EOI Upload] Request received with x-tag:', xTagHeader);
if (!xTagHeader || (xTagHeader !== "094ut234" && xTagHeader !== "pjnvü1230")) { if (!xTagHeader || (xTagHeader !== "094ut234" && xTagHeader !== "pjnvü1230")) {
console.error('[EOI Upload] Authentication failed - invalid x-tag');
throw createError({ statusCode: 401, statusMessage: "unauthenticated" }); throw createError({ statusCode: 401, statusMessage: "unauthenticated" });
} }
@ -16,7 +19,10 @@ export default defineEventHandler(async (event) => {
const query = getQuery(event); const query = getQuery(event);
const interestId = query.interestId as string; const interestId = query.interestId as string;
console.log('[EOI Upload] Interest ID:', interestId);
if (!interestId) { if (!interestId) {
console.error('[EOI Upload] No interest ID provided');
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
statusMessage: 'Interest ID is required', statusMessage: 'Interest ID is required',
@ -24,6 +30,7 @@ export default defineEventHandler(async (event) => {
} }
// Ensure bucket exists // Ensure bucket exists
console.log('[EOI Upload] Ensuring client-portal bucket exists');
await createBucketIfNotExists('client-portal'); await createBucketIfNotExists('client-portal');
// Parse multipart form data // Parse multipart form data
@ -32,39 +39,61 @@ export default defineEventHandler(async (event) => {
keepExtensions: true, keepExtensions: true,
}); });
console.log('[EOI Upload] Parsing multipart form data');
const [fields, files] = await form.parse(event.node.req); const [fields, files] = await form.parse(event.node.req);
// Handle the uploaded file // Handle the uploaded file
const uploadedFile = Array.isArray(files.file) ? files.file[0] : files.file; const uploadedFile = Array.isArray(files.file) ? files.file[0] : files.file;
if (!uploadedFile) { if (!uploadedFile) {
console.error('[EOI Upload] No file uploaded in request');
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
statusMessage: 'No file uploaded', statusMessage: 'No file uploaded',
}); });
} }
console.log('[EOI Upload] File received:', {
originalFilename: uploadedFile.originalFilename,
size: uploadedFile.size,
mimetype: uploadedFile.mimetype
});
// Read file buffer // Read file buffer
console.log('[EOI Upload] Reading file from disk');
const fileBuffer = await fs.readFile(uploadedFile.filepath); const fileBuffer = await fs.readFile(uploadedFile.filepath);
// Generate filename with timestamp // Generate filename with timestamp - ensure it goes to EOIs folder
const timestamp = Date.now(); const timestamp = Date.now();
const sanitizedName = uploadedFile.originalFilename?.replace(/[^a-zA-Z0-9.-]/g, '_') || 'eoi-document.pdf'; const sanitizedName = uploadedFile.originalFilename?.replace(/[^a-zA-Z0-9.-]/g, '_') || 'eoi-document.pdf';
const fileName = `EOIs/${interestId}-${timestamp}-${sanitizedName}`; const fileName = `EOIs/${interestId}-${timestamp}-${sanitizedName}`;
console.log('[EOI Upload] Generated filename:', fileName);
// Get content type // Get content type
const contentType = mime.lookup(uploadedFile.originalFilename || '') || 'application/pdf'; const contentType = mime.lookup(uploadedFile.originalFilename || '') || 'application/pdf';
console.log('[EOI Upload] Content type:', contentType);
// Upload to MinIO client-portal bucket // Upload to MinIO client-portal bucket
console.log('[EOI Upload] Uploading to MinIO client-portal bucket');
const client = getMinioClient(); const client = getMinioClient();
await client.putObject('client-portal', fileName, fileBuffer, fileBuffer.length, {
'Content-Type': contentType, try {
}); await client.putObject('client-portal', fileName, fileBuffer, fileBuffer.length, {
'Content-Type': contentType,
});
console.log('[EOI Upload] Successfully uploaded to MinIO');
} catch (minioError: any) {
console.error('[EOI Upload] MinIO upload failed:', minioError);
throw new Error(`Failed to upload to MinIO: ${minioError.message}`);
}
// Clean up temp file // Clean up temp file
console.log('[EOI Upload] Cleaning up temp file');
await fs.unlink(uploadedFile.filepath); await fs.unlink(uploadedFile.filepath);
// Get download URL for the uploaded file // Get download URL for the uploaded file
console.log('[EOI Upload] Generating presigned URL');
const url = await client.presignedGetObject('client-portal', fileName, 24 * 60 * 60); // 24 hour expiry const url = await client.presignedGetObject('client-portal', fileName, 24 * 60 * 60); // 24 hour expiry
// Prepare document data for database // Prepare document data for database
@ -77,6 +106,7 @@ export default defineEventHandler(async (event) => {
}; };
// Update interest with EOI document information // Update interest with EOI document information
console.log('[EOI Upload] Updating interest with EOI document info');
await updateInterestEOIDocument(interestId, documentData); await updateInterestEOIDocument(interestId, documentData);
// Also update the status fields // Also update the status fields
@ -84,6 +114,7 @@ export default defineEventHandler(async (event) => {
'EOI Status': 'Waiting for Signatures', 'EOI Status': 'Waiting for Signatures',
'EOI Time Sent': new Date().toISOString() 'EOI Time Sent': new Date().toISOString()
}; };
console.log('[EOI Upload] Updating interest status fields');
// Update Sales Process Level if it's below "LOI and NDA Sent" // Update Sales Process Level if it's below "LOI and NDA Sent"
const currentLevel = await getCurrentSalesLevel(interestId); const currentLevel = await getCurrentSalesLevel(interestId);
@ -103,13 +134,15 @@ export default defineEventHandler(async (event) => {
} }
}); });
console.log('[EOI Upload] Upload completed successfully');
return { return {
success: true, success: true,
document: documentData, document: documentData,
message: 'EOI document uploaded successfully', message: 'EOI document uploaded successfully',
}; };
} catch (error: any) { } catch (error: any) {
console.error('Failed to upload EOI document:', error); console.error('[EOI Upload] Failed to upload EOI document:', error);
console.error('[EOI Upload] Error stack:', error.stack);
throw createError({ throw createError({
statusCode: 500, statusCode: 500,
statusMessage: error.message || 'Failed to upload EOI document', statusMessage: error.message || 'Failed to upload EOI document',