Merge pull request #49 from operacle/develop
Remove HTTP service check and Refactor: Split settings API into modules
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
@@ -41,7 +42,7 @@ function App() {
|
|||||||
<Route path="/" element={<Index />} />
|
<Route path="/" element={<Index />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<Route path="/services/:id" element={<ServiceDetail />} />
|
<Route path="/service/:id" element={<ServiceDetail />} />
|
||||||
<Route path="/profile" element={<Profile />} />
|
<Route path="/profile" element={<Profile />} />
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/settings" element={<Settings />} />
|
||||||
<Route path="/operational-page" element={<OperationalPage />} />
|
<Route path="/operational-page" element={<OperationalPage />} />
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const api = {
|
|||||||
return await realtime(body);
|
return await realtime(body);
|
||||||
} else if (path === '/api/settings' || path.startsWith('/api/settings/')) {
|
} else if (path === '/api/settings' || path.startsWith('/api/settings/')) {
|
||||||
console.log("Routing to settings handler");
|
console.log("Routing to settings handler");
|
||||||
return await settingsApi(body);
|
return await settingsApi(body, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return 404 for unknown routes
|
// Return 404 for unknown routes
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
import { getAuthHeaders, getBaseUrl } from '../utils';
|
||||||
|
import { SettingsApiResponse } from '../types';
|
||||||
|
|
||||||
|
export const getSettings = async (): Promise<SettingsApiResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${getBaseUrl()}/api/settings`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
|
||||||
|
const settings = await response.json();
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: { success: true, data: settings },
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching settings:', error);
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
json: { success: false, message: 'Failed to fetch settings' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
|
||||||
|
import { getAuthHeaders, getBaseUrl, validateEmail } from '../utils';
|
||||||
|
import { SettingsApiResponse } from '../types';
|
||||||
|
|
||||||
|
const createEmailTemplate = (template: string, data: any): { subject: string; htmlBody: string } => {
|
||||||
|
let subject = 'Test Email from ReamStack';
|
||||||
|
let htmlBody = `
|
||||||
|
<html>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||||
|
<h2 style="color: #2563eb;">Test Email</h2>
|
||||||
|
<p>This is a test email from your monitoring system.</p>
|
||||||
|
<p>If you received this email, your SMTP configuration is working correctly.</p>
|
||||||
|
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||||
|
<p style="font-size: 12px; color: #666;">
|
||||||
|
Sent from ReamStack Monitoring System<br>
|
||||||
|
Template: ${template}<br>
|
||||||
|
${data.collection ? `Collection: ${data.collection}` : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
switch (template) {
|
||||||
|
case 'verification':
|
||||||
|
subject = 'Email Verification Test - ReamStack';
|
||||||
|
htmlBody = `
|
||||||
|
<html>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||||
|
<h2 style="color: #10b981;">Email Verification Test</h2>
|
||||||
|
<p>This is a test of the email verification template.</p>
|
||||||
|
<p>If you received this email, your SMTP configuration is working correctly.</p>
|
||||||
|
<div style="background: #f3f4f6; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||||
|
<p><strong>Template:</strong> Verification Email</p>
|
||||||
|
<p><strong>Collection:</strong> ${data.collection || '_superusers'}</p>
|
||||||
|
</div>
|
||||||
|
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||||
|
<p style="font-size: 12px; color: #666;">Sent from ReamStack Monitoring System</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'password-reset':
|
||||||
|
subject = 'Password Reset Test - ReamStack';
|
||||||
|
htmlBody = `
|
||||||
|
<html>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||||
|
<h2 style="color: #f59e0b;">Password Reset Test</h2>
|
||||||
|
<p>This is a test of the password reset template.</p>
|
||||||
|
<p>If you received this email, your SMTP configuration is working correctly.</p>
|
||||||
|
<div style="background: #f3f4f6; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||||
|
<p><strong>Template:</strong> Password Reset Email</p>
|
||||||
|
<p><strong>Collection:</strong> ${data.collection || '_superusers'}</p>
|
||||||
|
</div>
|
||||||
|
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||||
|
<p style="font-size: 12px; color: #666;">Sent from ReamStack Monitoring System</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'email-change':
|
||||||
|
subject = 'Email Change Confirmation Test - ReamStack';
|
||||||
|
htmlBody = `
|
||||||
|
<html>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||||
|
<h2 style="color: #8b5cf6;">Email Change Confirmation Test</h2>
|
||||||
|
<p>This is a test of the email change confirmation template.</p>
|
||||||
|
<p>If you received this email, your SMTP configuration is working correctly.</p>
|
||||||
|
<div style="background: #f3f4f6; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||||
|
<p><strong>Template:</strong> Email Change Confirmation</p>
|
||||||
|
</div>
|
||||||
|
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||||
|
<p style="font-size: 12px; color: #666;">Sent from ReamStack Monitoring System</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { subject, htmlBody };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendTestEmail = async (data: any): Promise<SettingsApiResponse> => {
|
||||||
|
console.log('sendTestEmail function called with data:', data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Validate required fields
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
|
console.log('Invalid request data - not object');
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: { success: false, message: 'Invalid request data' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.email || typeof data.email !== 'string') {
|
||||||
|
console.log('Email address missing or invalid type');
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: { success: false, message: 'Email address is required and must be a string' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateEmail(data.email)) {
|
||||||
|
console.log('Invalid email format:', data.email);
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: { success: false, message: 'Invalid email address format' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Email validation passed for:', data.email);
|
||||||
|
|
||||||
|
const headers = getAuthHeaders();
|
||||||
|
const baseUrl = getBaseUrl();
|
||||||
|
|
||||||
|
// Get current SMTP settings first
|
||||||
|
console.log('Fetching SMTP settings from:', `${baseUrl}/api/settings`);
|
||||||
|
|
||||||
|
const settingsResponse = await fetch(`${baseUrl}/api/settings`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!settingsResponse.ok) {
|
||||||
|
console.error('Failed to get SMTP settings, status:', settingsResponse.status);
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: { success: false, message: 'Failed to get SMTP settings' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsData = await settingsResponse.json();
|
||||||
|
console.log('Retrieved settings data:', settingsData);
|
||||||
|
|
||||||
|
const smtpSettings = settingsData?.smtp;
|
||||||
|
|
||||||
|
if (!smtpSettings || !smtpSettings.enabled) {
|
||||||
|
console.log('SMTP not enabled or missing');
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: { success: false, message: 'SMTP is not enabled. Please enable and configure SMTP settings first.' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!smtpSettings.host || !smtpSettings.username) {
|
||||||
|
console.log('SMTP configuration incomplete - missing host or username');
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: { success: false, message: 'SMTP configuration is incomplete. Please check host and username.' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!smtpSettings.password) {
|
||||||
|
console.log('SMTP password missing');
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: { success: false, message: 'SMTP password is required for authentication. Please configure the SMTP password.' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test email content based on template
|
||||||
|
const template = data.template || 'basic';
|
||||||
|
const { subject, htmlBody } = createEmailTemplate(template, data);
|
||||||
|
|
||||||
|
console.log('Test email prepared successfully:', {
|
||||||
|
to: data.email,
|
||||||
|
subject: subject,
|
||||||
|
template: template,
|
||||||
|
smtpHost: smtpSettings.host,
|
||||||
|
smtpPort: smtpSettings.port || 587
|
||||||
|
});
|
||||||
|
|
||||||
|
// For now, we'll simulate a successful email send
|
||||||
|
// In a real implementation, you would integrate with your email service here
|
||||||
|
// This could be nodemailer, SendGrid, or your PocketBase email system
|
||||||
|
|
||||||
|
// Simulate processing time
|
||||||
|
console.log('Simulating email send...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
console.log('Email send simulation completed');
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: {
|
||||||
|
success: true,
|
||||||
|
message: `Test email sent successfully to ${data.email}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in sendTestEmail function:', error);
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof Error ? error.message : 'Failed to send test email. Please check your SMTP configuration.'
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
import { getAuthHeaders, getBaseUrl, validateEmail } from '../utils';
|
||||||
|
import { SettingsApiResponse } from '../types';
|
||||||
|
|
||||||
|
const createEmailTemplate = (template: string, data: any): { subject: string; htmlBody: string } => {
|
||||||
|
let subject = 'Test Email from ReamStack';
|
||||||
|
let htmlBody = `
|
||||||
|
<html>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||||
|
<h2 style="color: #2563eb;">Test Email</h2>
|
||||||
|
<p>This is a test email from your monitoring system.</p>
|
||||||
|
<p>If you received this email, your SMTP configuration is working correctly.</p>
|
||||||
|
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||||
|
<p style="font-size: 12px; color: #666;">
|
||||||
|
Sent from ReamStack Monitoring System<br>
|
||||||
|
Template: ${template}<br>
|
||||||
|
${data.collection ? `Collection: ${data.collection}` : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
switch (template) {
|
||||||
|
case 'verification':
|
||||||
|
subject = 'Email Verification Test - ReamStack';
|
||||||
|
htmlBody = `
|
||||||
|
<html>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||||
|
<h2 style="color: #10b981;">Email Verification Test</h2>
|
||||||
|
<p>This is a test of the email verification template.</p>
|
||||||
|
<p>If you received this email, your SMTP configuration is working correctly.</p>
|
||||||
|
<div style="background: #f3f4f6; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||||
|
<p><strong>Template:</strong> Verification Email</p>
|
||||||
|
<p><strong>Collection:</strong> ${data.collection || '_superusers'}</p>
|
||||||
|
</div>
|
||||||
|
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||||
|
<p style="font-size: 12px; color: #666;">Sent from ReamStack Monitoring System</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'password-reset':
|
||||||
|
subject = 'Password Reset Test - ReamStack';
|
||||||
|
htmlBody = `
|
||||||
|
<html>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||||
|
<h2 style="color: #f59e0b;">Password Reset Test</h2>
|
||||||
|
<p>This is a test of the password reset template.</p>
|
||||||
|
<p>If you received this email, your SMTP configuration is working correctly.</p>
|
||||||
|
<div style="background: #f3f4f6; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||||
|
<p><strong>Template:</strong> Password Reset Email</p>
|
||||||
|
<p><strong>Collection:</strong> ${data.collection || '_superusers'}</p>
|
||||||
|
</div>
|
||||||
|
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||||
|
<p style="font-size: 12px; color: #666;">Sent from ReamStack Monitoring System</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'email-change':
|
||||||
|
subject = 'Email Change Confirmation Test - ReamStack';
|
||||||
|
htmlBody = `
|
||||||
|
<html>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||||
|
<h2 style="color: #8b5cf6;">Email Change Confirmation Test</h2>
|
||||||
|
<p>This is a test of the email change confirmation template.</p>
|
||||||
|
<p>If you received this email, your SMTP configuration is working correctly.</p>
|
||||||
|
<div style="background: #f3f4f6; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||||
|
<p><strong>Template:</strong> Email Change Confirmation</p>
|
||||||
|
</div>
|
||||||
|
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||||
|
<p style="font-size: 12px; color: #666;">Sent from ReamStack Monitoring System</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { subject, htmlBody };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const testEmail = async (data: any): Promise<SettingsApiResponse> => {
|
||||||
|
console.log('testEmail function called with data:', data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Validate required fields
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
|
console.log('Invalid request data - not object');
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: { success: false, message: 'Invalid request data' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.email || typeof data.email !== 'string') {
|
||||||
|
console.log('Email address missing or invalid type');
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: { success: false, message: 'Email address is required and must be a string' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateEmail(data.email)) {
|
||||||
|
console.log('Invalid email format:', data.email);
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: { success: false, message: 'Invalid email address format' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Email validation passed for:', data.email);
|
||||||
|
|
||||||
|
const headers = getAuthHeaders();
|
||||||
|
const baseUrl = getBaseUrl();
|
||||||
|
|
||||||
|
// Get current SMTP settings first
|
||||||
|
console.log('Fetching SMTP settings from:', `${baseUrl}/api/settings`);
|
||||||
|
|
||||||
|
const settingsResponse = await fetch(`${baseUrl}/api/settings`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!settingsResponse.ok) {
|
||||||
|
console.error('Failed to get SMTP settings, status:', settingsResponse.status);
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: { success: false, message: 'Failed to get SMTP settings' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsData = await settingsResponse.json();
|
||||||
|
console.log('Retrieved settings data:', settingsData);
|
||||||
|
|
||||||
|
const smtpSettings = settingsData?.smtp;
|
||||||
|
|
||||||
|
if (!smtpSettings || !smtpSettings.enabled) {
|
||||||
|
console.log('SMTP not enabled or missing');
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: { success: false, message: 'SMTP is not enabled. Please enable and configure SMTP settings first.' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!smtpSettings.host || !smtpSettings.username) {
|
||||||
|
console.log('SMTP configuration incomplete - missing host or username');
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: { success: false, message: 'SMTP configuration is incomplete. Please check host and username.' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test email content based on template
|
||||||
|
const template = data.template || 'basic';
|
||||||
|
const { subject, htmlBody } = createEmailTemplate(template, data);
|
||||||
|
|
||||||
|
console.log('Test email prepared successfully:', {
|
||||||
|
to: data.email,
|
||||||
|
subject: subject,
|
||||||
|
template: template,
|
||||||
|
smtpHost: smtpSettings.host,
|
||||||
|
smtpPort: smtpSettings.port || 587
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send actual email using the correct PocketBase API endpoint
|
||||||
|
console.log('Sending actual email via PocketBase...');
|
||||||
|
|
||||||
|
// Fix the payload structure to match PocketBase API expectations
|
||||||
|
const emailPayload = {
|
||||||
|
email: data.email, // Use 'email' instead of 'to'
|
||||||
|
template: template, // Add the template field
|
||||||
|
subject: subject,
|
||||||
|
html: htmlBody,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Email payload:', emailPayload);
|
||||||
|
|
||||||
|
const emailResponse = await fetch(`${baseUrl}/api/settings/test/email`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(emailPayload),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!emailResponse.ok) {
|
||||||
|
console.error('Failed to send email, status:', emailResponse.status);
|
||||||
|
const errorText = await emailResponse.text();
|
||||||
|
console.error('Email send error response:', errorText);
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: { success: false, message: 'Failed to send email. Please check your SMTP configuration.' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle 204 No Content response (successful but no body)
|
||||||
|
if (emailResponse.status === 204) {
|
||||||
|
console.log('Email sent successfully (204 No Content)');
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: {
|
||||||
|
success: true,
|
||||||
|
message: `Test email sent successfully to ${data.email}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other successful responses, try to parse JSON
|
||||||
|
const emailResult = await emailResponse.json();
|
||||||
|
console.log('Email sent successfully:', emailResult);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: {
|
||||||
|
success: true,
|
||||||
|
message: `Test email sent successfully to ${data.email}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in testEmail function:', error);
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof Error ? error.message : 'Failed to send test email. Please check your SMTP configuration.'
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
import { getAuthHeaders, getBaseUrl } from '../utils';
|
||||||
|
import { SettingsApiResponse } from '../types';
|
||||||
|
|
||||||
|
export const testEmailConnection = async (data: any): Promise<SettingsApiResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${getBaseUrl()}/api/settings/test-email`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: {
|
||||||
|
success: result.success || false,
|
||||||
|
message:
|
||||||
|
result.message || (result.success ? 'Connection successful' : 'Connection failed'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error testing email connection:', error);
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
json: { success: false, message: 'Failed to test email connection' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
|
||||||
|
import { getAuthHeaders, getBaseUrl } from '../utils';
|
||||||
|
import { SettingsApiResponse } from '../types';
|
||||||
|
|
||||||
|
export const updateSettings = async (data: any): Promise<SettingsApiResponse> => {
|
||||||
|
try {
|
||||||
|
const headers = getAuthHeaders();
|
||||||
|
const baseUrl = getBaseUrl();
|
||||||
|
|
||||||
|
let response = await fetch(`${baseUrl}/api/settings`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok && (response.status === 404 || response.status === 405)) {
|
||||||
|
response = await fetch(`${baseUrl}/api/settings`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
|
||||||
|
const updatedSettings = await response.json();
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: { success: true, data: updatedSettings },
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating settings:', error);
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
json: { success: false, message: 'Failed to update settings' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,199 +1,41 @@
|
|||||||
|
|
||||||
import { pb, getCurrentEndpoint } from '@/lib/pocketbase';
|
import { getSettings } from './actions/getSettings';
|
||||||
|
import { updateSettings } from './actions/updateSettings';
|
||||||
|
import { testEmailConnection } from './actions/testEmailConnection';
|
||||||
|
import { testEmail } from './actions/testEmail';
|
||||||
|
|
||||||
const settingsApi = async (body: any) => {
|
/**
|
||||||
try {
|
* Settings API handler
|
||||||
const { action, data } = body;
|
*/
|
||||||
console.log('Settings API called with action:', action, 'data:', data);
|
const settingsApi = async (body: any, path?: string) => {
|
||||||
|
console.log('Settings API called with path:', path, 'body:', body);
|
||||||
|
|
||||||
const authToken = pb.authStore.token;
|
// Handle test email endpoint specifically
|
||||||
const headers: Record<string, string> = {
|
if (path === '/api/settings/test/email') {
|
||||||
'Content-Type': 'application/json',
|
console.log('Handling test email request');
|
||||||
};
|
return await testEmail(body);
|
||||||
|
}
|
||||||
|
|
||||||
if (authToken) {
|
// Handle regular settings API with action-based routing
|
||||||
headers['Authorization'] = `Bearer ${authToken}`;
|
const action = body?.action;
|
||||||
}
|
console.log('Settings API called with action:', action, 'data:', body?.data);
|
||||||
|
|
||||||
const baseUrl = getCurrentEndpoint();
|
switch (action) {
|
||||||
|
case 'getSettings':
|
||||||
|
return await getSettings();
|
||||||
|
|
||||||
switch (action) {
|
case 'updateSettings':
|
||||||
case 'getSettings':
|
return await updateSettings(body.data);
|
||||||
try {
|
|
||||||
const response = await fetch(`${baseUrl}/api/settings`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
case 'testEmailConnection':
|
||||||
|
return await testEmailConnection(body.data);
|
||||||
|
|
||||||
const settings = await response.json();
|
default:
|
||||||
return {
|
console.error('Unknown action:', action);
|
||||||
status: 200,
|
return {
|
||||||
json: { success: true, data: settings },
|
status: 400,
|
||||||
};
|
json: { success: false, message: 'Unknown action' },
|
||||||
} catch (error) {
|
};
|
||||||
console.error('Error fetching settings:', error);
|
|
||||||
return {
|
|
||||||
status: 500,
|
|
||||||
json: { success: false, message: 'Failed to fetch settings' },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'updateSettings':
|
|
||||||
try {
|
|
||||||
let response = await fetch(`${baseUrl}/api/settings`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok && (response.status === 404 || response.status === 405)) {
|
|
||||||
response = await fetch(`${baseUrl}/api/settings`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
|
|
||||||
const updatedSettings = await response.json();
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
json: { success: true, data: updatedSettings },
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating settings:', error);
|
|
||||||
return {
|
|
||||||
status: 500,
|
|
||||||
json: { success: false, message: 'Failed to update settings' },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'testEmailConnection':
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${baseUrl}/api/settings/test-email`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
json: {
|
|
||||||
success: result.success || false,
|
|
||||||
message:
|
|
||||||
result.message || (result.success ? 'Connection successful' : 'Connection failed'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error testing email connection:', error);
|
|
||||||
return {
|
|
||||||
status: 500,
|
|
||||||
json: { success: false, message: 'Failed to test email connection' },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'sendTestEmail':
|
|
||||||
try {
|
|
||||||
// Try different endpoints that might be available on the PocketBase server
|
|
||||||
let response;
|
|
||||||
|
|
||||||
// First try: use admin API to send emails
|
|
||||||
try {
|
|
||||||
response = await fetch(`${baseUrl}/api/admins/auth-with-password`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify({
|
|
||||||
identity: data.email,
|
|
||||||
password: "test" // This will fail but we just need to trigger email functionality
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// Expected to fail, this is just to test email functionality
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second try: use a verification request which should trigger email
|
|
||||||
try {
|
|
||||||
const collection = data.collection || '_superusers';
|
|
||||||
response = await fetch(`${baseUrl}/api/collections/${collection}/request-verification`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: data.email
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
json: {
|
|
||||||
success: true,
|
|
||||||
message: 'Test email sent successfully (verification request)',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Verification request failed, trying password reset...');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Third try: use password reset which should trigger email
|
|
||||||
try {
|
|
||||||
const collection = data.collection || '_superusers';
|
|
||||||
response = await fetch(`${baseUrl}/api/collections/${collection}/request-password-reset`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: data.email
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
json: {
|
|
||||||
success: true,
|
|
||||||
message: 'Test email sent successfully (password reset request)',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Password reset request failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// If all specific endpoints fail, return a success message since we can't actually test
|
|
||||||
// the email without a proper test endpoint
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
json: {
|
|
||||||
success: true,
|
|
||||||
message: 'SMTP configuration validated (actual email sending requires a user to exist)',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending test email:', error);
|
|
||||||
return {
|
|
||||||
status: 500,
|
|
||||||
json: { success: false, message: 'Failed to send test email' },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
status: 400,
|
|
||||||
json: { success: false, message: 'Invalid action' },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Unexpected error in settingsApi:', error);
|
|
||||||
return {
|
|
||||||
status: 500,
|
|
||||||
json: { success: false, message: 'Internal server error' },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
export interface SettingsApiRequest {
|
||||||
|
action: string;
|
||||||
|
data?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettingsApiResponse {
|
||||||
|
status: number;
|
||||||
|
json: {
|
||||||
|
success: boolean;
|
||||||
|
data?: any;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SmtpSettings {
|
||||||
|
enabled?: boolean;
|
||||||
|
host?: string;
|
||||||
|
port?: number;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
authMethod?: string;
|
||||||
|
tls?: boolean;
|
||||||
|
localName?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
import { pb, getCurrentEndpoint } from '@/lib/pocketbase';
|
||||||
|
|
||||||
|
export const getAuthHeaders = (): Record<string, string> => {
|
||||||
|
const authToken = pb.authStore.token;
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (authToken) {
|
||||||
|
headers['Authorization'] = `Bearer ${authToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBaseUrl = (): string => {
|
||||||
|
return getCurrentEndpoint();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateEmail = (email: string): boolean => {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
return emailRegex.test(email);
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import { FormControl, FormField, FormItem, FormLabel } from "@/components/ui/form";
|
import { FormControl, FormField, FormItem, FormLabel } from "@/components/ui/form";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Globe } from "lucide-react";
|
import { Globe, Wifi, Server, Globe2 } from "lucide-react";
|
||||||
import { UseFormReturn } from "react-hook-form";
|
import { UseFormReturn } from "react-hook-form";
|
||||||
import { ServiceFormData } from "./types";
|
import { ServiceFormData } from "./types";
|
||||||
|
|
||||||
@@ -10,6 +10,21 @@ interface ServiceTypeFieldProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
|
export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
|
||||||
|
const getServiceIcon = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case "http":
|
||||||
|
return <Globe className="w-4 h-4" />;
|
||||||
|
case "ping":
|
||||||
|
return <Wifi className="w-4 h-4" />;
|
||||||
|
case "tcp":
|
||||||
|
return <Server className="w-4 h-4" />;
|
||||||
|
case "dns":
|
||||||
|
return <Globe2 className="w-4 h-4" />;
|
||||||
|
default:
|
||||||
|
return <Globe className="w-4 h-4" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -24,13 +39,13 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
|
|||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue>
|
<SelectValue>
|
||||||
{field.value === "http" && (
|
{field.value && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Globe className="w-4 h-4" />
|
{getServiceIcon(field.value)}
|
||||||
<span>HTTP/S</span>
|
<span>{field.value.toUpperCase()}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{field.value !== "http" && "Select a service type"}
|
{!field.value && "Select a service type"}
|
||||||
</SelectValue>
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -41,13 +56,43 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
|
|||||||
<span>HTTP/S</span>
|
<span>HTTP/S</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
Monitor websites and REST APIs with HTTP/HTTPS protocol
|
Monitor websites and REST APIs with HTTP/HTTPS Protocol
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="ping">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Wifi className="w-4 h-4" />
|
||||||
|
<span>PING</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Monitor host availability with PING Protocol
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="tcp">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Server className="w-4 h-4" />
|
||||||
|
<span>TCP</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Monitor TCP port connectivity with TCP Protocol
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="dns">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Globe2 className="w-4 h-4" />
|
||||||
|
<span>DNS</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Monitor DNS resolution
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="ping">PING</SelectItem>
|
|
||||||
<SelectItem value="tcp">TCP</SelectItem>
|
|
||||||
<SelectItem value="dns">DNS</SelectItem>
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
@@ -27,18 +26,15 @@ const MailSettingsTab: React.FC<MailSettingsTabProps> = ({
|
|||||||
setIsTestingEmail(true);
|
setIsTestingEmail(true);
|
||||||
console.log('Testing email with data:', data);
|
console.log('Testing email with data:', data);
|
||||||
|
|
||||||
const response = await fetch('/api/settings', {
|
const response = await fetch('/api/settings/test/email', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
action: 'sendTestEmail',
|
email: data.email,
|
||||||
data: {
|
template: data.template,
|
||||||
email: data.email,
|
...(data.collection && { collection: data.collection })
|
||||||
template: data.template,
|
|
||||||
...(data.collection && { collection: data.collection })
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -257,10 +253,16 @@ const MailSettingsTab: React.FC<MailSettingsTabProps> = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Only show Test Email button, removed Test Connection button */}
|
{/* Test Email button - outside the form area and always visible when SMTP is enabled */}
|
||||||
{isEditing && form.watch('smtp.enabled') && (
|
{form.watch('smtp.enabled') && (
|
||||||
<div className="mt-4">
|
<div className="mt-6 pt-4 border-t border-border">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium">{t("testEmailSettings", "settings")}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">{t("testEmailDescription", "settings")}</p>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -272,8 +274,8 @@ const MailSettingsTab: React.FC<MailSettingsTabProps> = ({
|
|||||||
{t("testEmail", "settings")}
|
{t("testEmail", "settings")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<TestEmailDialog
|
<TestEmailDialog
|
||||||
open={showTestEmailDialog}
|
open={showTestEmailDialog}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -7,8 +6,9 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { useLanguage } from "@/contexts/LanguageContext";
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
import { Mail, X } from "lucide-react";
|
import { Mail, X, AlertCircle, CheckCircle, Loader2 } from "lucide-react";
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
|
||||||
interface TestEmailDialogProps {
|
interface TestEmailDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -33,9 +33,16 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
|
|||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [template, setTemplate] = useState('verification');
|
const [template, setTemplate] = useState('verification');
|
||||||
const [collection, setCollection] = useState('_superusers');
|
const [collection, setCollection] = useState('_superusers');
|
||||||
|
const [lastResult, setLastResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||||
|
const [isInternalTesting, setIsInternalTesting] = useState(false);
|
||||||
|
|
||||||
|
const validateEmail = (email: string): boolean => {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
return emailRegex.test(email);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
if (!email) {
|
if (!email.trim()) {
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Please enter an email address",
|
description: "Please enter an email address",
|
||||||
@@ -44,39 +51,73 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!validateEmail(email)) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Please enter a valid email address",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setLastResult(null);
|
||||||
|
setIsInternalTesting(true);
|
||||||
|
|
||||||
|
console.log('Sending test email with data:', {
|
||||||
|
email,
|
||||||
|
template,
|
||||||
|
collection: template === 'verification' || template === 'password-reset' ? collection : undefined
|
||||||
|
});
|
||||||
|
|
||||||
await onSendTest({
|
await onSendTest({
|
||||||
email,
|
email,
|
||||||
template,
|
template,
|
||||||
collection: template === 'verification' ? collection : undefined
|
collection: template === 'verification' || template === 'password-reset' ? collection : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
setLastResult({
|
||||||
|
success: true,
|
||||||
|
message: `Test email sent successfully to ${email}`
|
||||||
});
|
});
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
description: "Test email sent successfully",
|
description: `Test email sent successfully to ${email}`,
|
||||||
variant: "default",
|
variant: "default",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close dialog on success
|
|
||||||
handleClose();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending test email:', error);
|
console.error('Error sending test email:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "Failed to send test email";
|
||||||
|
|
||||||
|
setLastResult({
|
||||||
|
success: false,
|
||||||
|
message: errorMessage
|
||||||
|
});
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Failed to send test email",
|
description: errorMessage,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setIsInternalTesting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
// Reset form
|
// Reset form but keep last result for reference
|
||||||
setEmail('');
|
setEmail('');
|
||||||
setTemplate('verification');
|
setTemplate('verification');
|
||||||
setCollection('_superusers');
|
setCollection('_superusers');
|
||||||
|
// Don't reset lastResult immediately to allow user to see the result
|
||||||
|
setTimeout(() => setLastResult(null), 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isLoading = isTesting || isInternalTesting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
@@ -90,16 +131,31 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="absolute right-4 top-4"
|
className="absolute right-4 top-4"
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
|
{/* Show last result */}
|
||||||
|
{lastResult && (
|
||||||
|
<Alert variant={lastResult.success ? "default" : "destructive"}>
|
||||||
|
{lastResult.success ? (
|
||||||
|
<CheckCircle className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
<AlertDescription>
|
||||||
|
{lastResult.message}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Template Selection */}
|
{/* Template Selection */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Label>{t("emailTemplate", "settings")}</Label>
|
<Label>{t("emailTemplate", "settings")}</Label>
|
||||||
<RadioGroup value={template} onValueChange={setTemplate}>
|
<RadioGroup value={template} onValueChange={setTemplate} disabled={isLoading}>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<RadioGroupItem value="verification" id="verification" />
|
<RadioGroupItem value="verification" id="verification" />
|
||||||
<Label htmlFor="verification">{t("verification", "settings")}</Label>
|
<Label htmlFor="verification">{t("verification", "settings")}</Label>
|
||||||
@@ -112,22 +168,14 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
|
|||||||
<RadioGroupItem value="email-change" id="email-change" />
|
<RadioGroupItem value="email-change" id="email-change" />
|
||||||
<Label htmlFor="email-change">{t("confirmEmailChange", "settings")}</Label>
|
<Label htmlFor="email-change">{t("confirmEmailChange", "settings")}</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem value="otp" id="otp" />
|
|
||||||
<Label htmlFor="otp">{t("otp", "settings")}</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem value="login-alert" id="login-alert" />
|
|
||||||
<Label htmlFor="login-alert">{t("loginAlert", "settings")}</Label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Auth Collection - only show for verification template */}
|
{/* Auth Collection - show for verification and password-reset templates */}
|
||||||
{template === 'verification' && (
|
{(template === 'verification' || template === 'password-reset') && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>{t("authCollection", "settings")} *</Label>
|
<Label>{t("authCollection", "settings")} *</Label>
|
||||||
<Select value={collection} onValueChange={setCollection}>
|
<Select value={collection} onValueChange={setCollection} disabled={isLoading}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={t("selectCollection", "settings")} />
|
<SelectValue placeholder={t("selectCollection", "settings")} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -148,21 +196,34 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
|
|||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder={t("enterEmailAddress", "settings")}
|
placeholder={t("enterEmailAddress", "settings")}
|
||||||
required
|
required
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Info message */}
|
||||||
|
<Alert>
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
This will send a test email using your configured SMTP settings. Make sure SMTP is properly configured first.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="flex justify-between">
|
<DialogFooter className="flex justify-between">
|
||||||
<Button variant="outline" onClick={handleClose}>
|
<Button variant="outline" onClick={handleClose} disabled={isLoading}>
|
||||||
{t("close", "common")}
|
{t("close", "common")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
disabled={!email || isTesting}
|
disabled={!email || isLoading}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Mail className="h-4 w-4" />
|
{isLoading ? (
|
||||||
{isTesting ? t("sending", "settings") : t("send", "common")}
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Mail className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{isLoading ? t("sending", "settings") : t("send", "common")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
import { monitoringIntervals } from './monitoringIntervals';
|
import { monitoringIntervals } from './monitoringIntervals';
|
||||||
import { checkHttpService } from './httpChecker';
|
|
||||||
import {
|
import {
|
||||||
startMonitoringService,
|
startMonitoringService,
|
||||||
pauseMonitoring,
|
pauseMonitoring,
|
||||||
@@ -12,6 +11,5 @@ export const monitoringService = {
|
|||||||
startMonitoringService,
|
startMonitoringService,
|
||||||
pauseMonitoring,
|
pauseMonitoring,
|
||||||
resumeMonitoring,
|
resumeMonitoring,
|
||||||
checkHttpService,
|
|
||||||
startAllActiveServices
|
startAllActiveServices
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
import { pb } from '@/lib/pocketbase';
|
import { pb } from '@/lib/pocketbase';
|
||||||
import { monitoringIntervals } from '../monitoringIntervals';
|
import { monitoringIntervals } from '../monitoringIntervals';
|
||||||
import { checkHttpService } from '../httpChecker';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start monitoring for a specific service
|
* Start monitoring for a specific service
|
||||||
@@ -30,34 +29,20 @@ export async function startMonitoringService(serviceId: string): Promise<void> {
|
|||||||
status: "up",
|
status: "up",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start with an immediate check
|
// The actual service checking is now handled by the Go microservice
|
||||||
await checkHttpService(serviceId);
|
// This frontend service just tracks the monitoring state
|
||||||
|
const intervalMs = (service.heartbeat_interval || 60) * 1000;
|
||||||
|
console.log(`Service ${service.name} monitoring delegated to backend service`);
|
||||||
|
|
||||||
// Then schedule regular checks based on the interval
|
// Store a placeholder interval to track that this service is being monitored
|
||||||
const intervalMs = (service.heartbeat_interval || 60) * 1000; // Convert from seconds to milliseconds
|
const intervalId = window.setInterval(() => {
|
||||||
console.log(`Setting check interval for ${service.name} to ${intervalMs}ms (${service.heartbeat_interval || 60} seconds)`);
|
console.log(`Monitoring active for service ${service.name} (handled by backend)`);
|
||||||
|
|
||||||
// Store the interval ID so we can clear it later if needed
|
|
||||||
const intervalId = window.setInterval(async () => {
|
|
||||||
try {
|
|
||||||
// Check if service has been paused since scheduling
|
|
||||||
const currentService = await pb.collection('services').getOne(serviceId);
|
|
||||||
if (currentService.status === "paused") {
|
|
||||||
console.log(`Service ${serviceId} is now paused. Skipping scheduled check.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Running scheduled check for service ${service.name}`);
|
|
||||||
await checkHttpService(serviceId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error in scheduled check for ${service.name}:`, error);
|
|
||||||
}
|
|
||||||
}, intervalMs);
|
}, intervalMs);
|
||||||
|
|
||||||
// Store the interval ID for this service
|
// Store the interval ID for this service
|
||||||
monitoringIntervals.set(serviceId, intervalId);
|
monitoringIntervals.set(serviceId, intervalId);
|
||||||
|
|
||||||
console.log(`Monitoring scheduled for service ${serviceId} every ${service.heartbeat_interval || 60} seconds`);
|
console.log(`Monitoring registered for service ${serviceId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error starting service monitoring:", error);
|
console.error("Error starting service monitoring:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,148 +57,3 @@ export function formatCurrentTime(): string {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
return now.toISOString();
|
return now.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Make an HTTP request to a service endpoint with retry logic
|
|
||||||
* Improved to better handle different response scenarios and detection issues
|
|
||||||
*/
|
|
||||||
export async function makeHttpRequest(url: string, maxRetries: number = 3): Promise<{ isUp: boolean; responseTime: number }> {
|
|
||||||
let retries = 0;
|
|
||||||
let isUp = false;
|
|
||||||
let responseTime = 0;
|
|
||||||
let lastError = null;
|
|
||||||
|
|
||||||
const startTime = performance.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Ensure URL has proper protocol
|
|
||||||
const targetUrl = ensureHttpsProtocol(url);
|
|
||||||
|
|
||||||
while (retries < maxRetries && !isUp) {
|
|
||||||
try {
|
|
||||||
// Use fetch API with a timeout to prevent long-hanging requests
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
|
||||||
|
|
||||||
console.log(`Attempt ${retries + 1}/${maxRetries} checking ${targetUrl}`);
|
|
||||||
|
|
||||||
// IMPROVED APPROACH: Try multiple detection methods in sequence
|
|
||||||
|
|
||||||
// Method 1: Try HEAD request first as it's lightweight
|
|
||||||
try {
|
|
||||||
console.log(`Trying HEAD request to ${targetUrl}`);
|
|
||||||
const headResponse = await fetch(targetUrl, {
|
|
||||||
method: 'HEAD',
|
|
||||||
signal: controller.signal,
|
|
||||||
cache: 'no-cache',
|
|
||||||
headers: {
|
|
||||||
'Accept': '*/*',
|
|
||||||
'User-Agent': 'ServiceMonitor/1.0'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If HEAD request succeeds with any status code, service is reachable
|
|
||||||
isUp = true;
|
|
||||||
console.log(`HEAD request successful with status ${headResponse.status}`);
|
|
||||||
} catch (headError) {
|
|
||||||
console.log(`HEAD request failed: ${headError.message}`);
|
|
||||||
lastError = headError;
|
|
||||||
|
|
||||||
// Method 2: Fall back to standard GET with proper error handling
|
|
||||||
try {
|
|
||||||
console.log(`Trying standard GET request to ${targetUrl}`);
|
|
||||||
const getResponse = await fetch(targetUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
signal: controller.signal,
|
|
||||||
cache: 'no-cache',
|
|
||||||
headers: {
|
|
||||||
'Accept': '*/*',
|
|
||||||
'User-Agent': 'ServiceMonitor/1.0'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If GET returns any response, consider the service up
|
|
||||||
isUp = true;
|
|
||||||
console.log(`GET request successful with status ${getResponse.status}`);
|
|
||||||
} catch (getError) {
|
|
||||||
console.log(`Standard GET request failed: ${getError.message}`);
|
|
||||||
lastError = getError;
|
|
||||||
|
|
||||||
// Method 3: Try with no-cors mode as a last resort
|
|
||||||
try {
|
|
||||||
console.log(`Trying no-cors GET request to ${targetUrl}`);
|
|
||||||
const noCorsResponse = await fetch(targetUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
mode: 'no-cors', // This allows requests to succeed even if CORS is restricted
|
|
||||||
signal: controller.signal,
|
|
||||||
cache: 'no-cache'
|
|
||||||
});
|
|
||||||
|
|
||||||
// In no-cors mode, we can't read the response, but if we get here without an exception,
|
|
||||||
// the request succeeded and the service is likely up
|
|
||||||
isUp = true;
|
|
||||||
console.log(`No-cors GET request succeeded, assuming service is UP`);
|
|
||||||
} catch (corsError) {
|
|
||||||
console.log(`No-cors GET request also failed: ${corsError.message}`);
|
|
||||||
lastError = corsError;
|
|
||||||
isUp = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 4: If all fetches fail but error is CORS-related, consider service up
|
|
||||||
if (!isUp && lastError &&
|
|
||||||
(lastError.message.includes('CORS') ||
|
|
||||||
lastError.message.includes('blocked') ||
|
|
||||||
lastError.message.includes('policy'))) {
|
|
||||||
console.log("CORS error detected, but this likely means the service is running.");
|
|
||||||
console.log("Setting service as UP despite CORS restriction.");
|
|
||||||
isUp = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
responseTime = Math.round(performance.now() - startTime);
|
|
||||||
|
|
||||||
if (isUp) {
|
|
||||||
console.log(`Service is detected as UP after ${retries + 1} attempts, response time: ${responseTime}ms`);
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
console.log(`All connection attempts failed for attempt ${retries + 1}`);
|
|
||||||
retries++;
|
|
||||||
|
|
||||||
// Add a short delay between retries
|
|
||||||
if (retries < maxRetries) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`HTTP request attempt ${retries + 1} failed with error:`, error);
|
|
||||||
lastError = error;
|
|
||||||
retries++;
|
|
||||||
|
|
||||||
if (retries < maxRetries) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Critical error in makeHttpRequest:', error);
|
|
||||||
isUp = false;
|
|
||||||
responseTime = Math.round(performance.now() - startTime);
|
|
||||||
lastError = error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final check for status
|
|
||||||
if (!isUp && lastError) {
|
|
||||||
console.log("Final connection attempt failed with error:", lastError);
|
|
||||||
// Check for specific errors that might indicate the site is actually up
|
|
||||||
if (lastError.name === 'AbortError') {
|
|
||||||
console.log("Request timed out which may indicate a slow response rather than service down");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the final result for debugging
|
|
||||||
console.log(`Final service check result - URL: ${url}, isUp: ${isUp}, responseTime: ${responseTime}ms, retries: ${retries}`);
|
|
||||||
|
|
||||||
return { isUp, responseTime };
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { pb } from '@/lib/pocketbase';
|
import { pb } from '@/lib/pocketbase';
|
||||||
import { Service, CreateServiceParams, UptimeData } from '@/types/service.types';
|
import { Service, CreateServiceParams, UptimeData } from '@/types/service.types';
|
||||||
import { monitoringService } from './monitoring';
|
import { monitoringService } from './monitoring';
|
||||||
@@ -144,7 +143,6 @@ export const serviceService = {
|
|||||||
startMonitoringService: monitoringService.startMonitoringService,
|
startMonitoringService: monitoringService.startMonitoringService,
|
||||||
pauseMonitoring: monitoringService.pauseMonitoring,
|
pauseMonitoring: monitoringService.pauseMonitoring,
|
||||||
resumeMonitoring: monitoringService.resumeMonitoring,
|
resumeMonitoring: monitoringService.resumeMonitoring,
|
||||||
checkHttpService: monitoringService.checkHttpService,
|
|
||||||
startAllActiveServices: monitoringService.startAllActiveServices,
|
startAllActiveServices: monitoringService.startAllActiveServices,
|
||||||
|
|
||||||
// Re-export uptime functions
|
// Re-export uptime functions
|
||||||
|
|||||||
Reference in New Issue
Block a user