Merge pull request #49 from operacle/develop

Remove HTTP service check and Refactor: Split settings API into modules
This commit is contained in:
Tola Leng
2025-06-18 22:09:40 +07:00
committed by GitHub
17 changed files with 797 additions and 420 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
@@ -41,7 +42,7 @@ function App() {
<Route path="/" element={<Index />} />
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/services/:id" element={<ServiceDetail />} />
<Route path="/service/:id" element={<ServiceDetail />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
<Route path="/operational-page" element={<OperationalPage />} />
+1 -1
View File
@@ -19,7 +19,7 @@ const api = {
return await realtime(body);
} else if (path === '/api/settings' || path.startsWith('/api/settings/')) {
console.log("Routing to settings handler");
return await settingsApi(body);
return await settingsApi(body, path);
}
// 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' },
};
}
};
+35 -193
View File
@@ -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 {
const { action, data } = body;
console.log('Settings API called with action:', action, 'data:', data);
/**
* Settings API handler
*/
const settingsApi = async (body: any, path?: string) => {
console.log('Settings API called with path:', path, 'body:', body);
// Handle test email endpoint specifically
if (path === '/api/settings/test/email') {
console.log('Handling test email request');
return await testEmail(body);
}
// Handle regular settings API with action-based routing
const action = body?.action;
console.log('Settings API called with action:', action, 'data:', body?.data);
const authToken = pb.authStore.token;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const baseUrl = getCurrentEndpoint();
switch (action) {
case 'getSettings':
try {
const response = await fetch(`${baseUrl}/api/settings`, {
method: 'GET',
headers,
});
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' },
};
}
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' },
};
switch (action) {
case 'getSettings':
return await getSettings();
case 'updateSettings':
return await updateSettings(body.data);
case 'testEmailConnection':
return await testEmailConnection(body.data);
default:
console.error('Unknown action:', action);
return {
status: 400,
json: { success: false, message: 'Unknown action' },
};
}
};
+25
View File
@@ -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;
}
+24
View File
@@ -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 { 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 { ServiceFormData } from "./types";
@@ -10,6 +10,21 @@ interface 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 (
<FormField
control={form.control}
@@ -24,13 +39,13 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
>
<SelectTrigger>
<SelectValue>
{field.value === "http" && (
{field.value && (
<div className="flex items-center gap-2">
<Globe className="w-4 h-4" />
<span>HTTP/S</span>
{getServiceIcon(field.value)}
<span>{field.value.toUpperCase()}</span>
</div>
)}
{field.value !== "http" && "Select a service type"}
{!field.value && "Select a service type"}
</SelectValue>
</SelectTrigger>
<SelectContent>
@@ -41,13 +56,43 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
<span>HTTP/S</span>
</div>
<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>
</div>
</SelectItem>
<SelectItem value="ping">PING</SelectItem>
<SelectItem value="tcp">TCP</SelectItem>
<SelectItem value="dns">DNS</SelectItem>
</SelectContent>
</Select>
</FormControl>
@@ -1,5 +1,4 @@
import React, { useState } from 'react';
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
@@ -27,18 +26,15 @@ const MailSettingsTab: React.FC<MailSettingsTabProps> = ({
setIsTestingEmail(true);
console.log('Testing email with data:', data);
const response = await fetch('/api/settings', {
const response = await fetch('/api/settings/test/email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'sendTestEmail',
data: {
email: data.email,
template: data.template,
...(data.collection && { collection: data.collection })
}
email: data.email,
template: data.template,
...(data.collection && { collection: data.collection })
})
});
@@ -257,10 +253,16 @@ const MailSettingsTab: React.FC<MailSettingsTabProps> = ({
)}
/>
</div>
{/* Only show Test Email button, removed Test Connection button */}
{isEditing && form.watch('smtp.enabled') && (
<div className="mt-4">
</div>
{/* Test Email button - outside the form area and always visible when SMTP is enabled */}
{form.watch('smtp.enabled') && (
<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
type="button"
variant="outline"
@@ -272,8 +274,8 @@ const MailSettingsTab: React.FC<MailSettingsTabProps> = ({
{t("testEmail", "settings")}
</Button>
</div>
)}
</div>
</div>
)}
<TestEmailDialog
open={showTestEmailDialog}
@@ -285,4 +287,4 @@ const MailSettingsTab: React.FC<MailSettingsTabProps> = ({
);
};
export default MailSettingsTab;
export default MailSettingsTab;
@@ -1,4 +1,3 @@
import React, { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
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 { Alert, AlertDescription } from "@/components/ui/alert";
interface TestEmailDialogProps {
open: boolean;
@@ -33,9 +33,16 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
const [email, setEmail] = useState('');
const [template, setTemplate] = useState('verification');
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 () => {
if (!email) {
if (!email.trim()) {
toast({
title: "Error",
description: "Please enter an email address",
@@ -44,39 +51,73 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
return;
}
if (!validateEmail(email)) {
toast({
title: "Error",
description: "Please enter a valid email address",
variant: "destructive",
});
return;
}
try {
setLastResult(null);
setIsInternalTesting(true);
console.log('Sending test email with data:', {
email,
template,
collection: template === 'verification' || template === 'password-reset' ? collection : undefined
});
await onSendTest({
email,
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({
title: "Success",
description: "Test email sent successfully",
description: `Test email sent successfully to ${email}`,
variant: "default",
});
// Close dialog on success
handleClose();
} catch (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({
title: "Error",
description: "Failed to send test email",
description: errorMessage,
variant: "destructive",
});
} finally {
setIsInternalTesting(false);
}
};
const handleClose = () => {
onOpenChange(false);
// Reset form
// Reset form but keep last result for reference
setEmail('');
setTemplate('verification');
setCollection('_superusers');
// Don't reset lastResult immediately to allow user to see the result
setTimeout(() => setLastResult(null), 300);
};
const isLoading = isTesting || isInternalTesting;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
@@ -90,16 +131,31 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
size="icon"
className="absolute right-4 top-4"
onClick={handleClose}
disabled={isLoading}
>
<X className="h-4 w-4" />
</Button>
</DialogHeader>
<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 */}
<div className="space-y-3">
<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">
<RadioGroupItem value="verification" id="verification" />
<Label htmlFor="verification">{t("verification", "settings")}</Label>
@@ -112,22 +168,14 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
<RadioGroupItem value="email-change" id="email-change" />
<Label htmlFor="email-change">{t("confirmEmailChange", "settings")}</Label>
</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>
</div>
{/* Auth Collection - only show for verification template */}
{template === 'verification' && (
{/* Auth Collection - show for verification and password-reset templates */}
{(template === 'verification' || template === 'password-reset') && (
<div className="space-y-2">
<Label>{t("authCollection", "settings")} *</Label>
<Select value={collection} onValueChange={setCollection}>
<Select value={collection} onValueChange={setCollection} disabled={isLoading}>
<SelectTrigger>
<SelectValue placeholder={t("selectCollection", "settings")} />
</SelectTrigger>
@@ -148,21 +196,34 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
onChange={(e) => setEmail(e.target.value)}
placeholder={t("enterEmailAddress", "settings")}
required
disabled={isLoading}
/>
</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>
<DialogFooter className="flex justify-between">
<Button variant="outline" onClick={handleClose}>
<Button variant="outline" onClick={handleClose} disabled={isLoading}>
{t("close", "common")}
</Button>
<Button
onClick={handleSend}
disabled={!email || isTesting}
disabled={!email || isLoading}
className="flex items-center gap-2"
>
<Mail className="h-4 w-4" />
{isTesting ? t("sending", "settings") : t("send", "common")}
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Mail className="h-4 w-4" />
)}
{isLoading ? t("sending", "settings") : t("send", "common")}
</Button>
</DialogFooter>
</DialogContent>
+1 -3
View File
@@ -1,6 +1,5 @@
import { monitoringIntervals } from './monitoringIntervals';
import { checkHttpService } from './httpChecker';
import {
startMonitoringService,
pauseMonitoring,
@@ -12,6 +11,5 @@ export const monitoringService = {
startMonitoringService,
pauseMonitoring,
resumeMonitoring,
checkHttpService,
startAllActiveServices
};
};
@@ -1,7 +1,6 @@
import { pb } from '@/lib/pocketbase';
import { monitoringIntervals } from '../monitoringIntervals';
import { checkHttpService } from '../httpChecker';
/**
* Start monitoring for a specific service
@@ -30,35 +29,21 @@ export async function startMonitoringService(serviceId: string): Promise<void> {
status: "up",
});
// Start with an immediate check
await checkHttpService(serviceId);
// The actual service checking is now handled by the Go microservice
// 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
const intervalMs = (service.heartbeat_interval || 60) * 1000; // Convert from seconds to milliseconds
console.log(`Setting check interval for ${service.name} to ${intervalMs}ms (${service.heartbeat_interval || 60} seconds)`);
// 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);
}
// Store a placeholder interval to track that this service is being monitored
const intervalId = window.setInterval(() => {
console.log(`Monitoring active for service ${service.name} (handled by backend)`);
}, intervalMs);
// Store the interval ID for this service
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) {
console.error("Error starting service monitoring:", error);
}
}
}
@@ -56,149 +56,4 @@ export function formatRecordId(id: string): string {
export function formatCurrentTime(): string {
const now = new Date();
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 -3
View File
@@ -1,4 +1,3 @@
import { pb } from '@/lib/pocketbase';
import { Service, CreateServiceParams, UptimeData } from '@/types/service.types';
import { monitoringService } from './monitoring';
@@ -144,10 +143,9 @@ export const serviceService = {
startMonitoringService: monitoringService.startMonitoringService,
pauseMonitoring: monitoringService.pauseMonitoring,
resumeMonitoring: monitoringService.resumeMonitoring,
checkHttpService: monitoringService.checkHttpService,
startAllActiveServices: monitoringService.startAllActiveServices,
// Re-export uptime functions
recordUptimeData: uptimeService.recordUptimeData,
getUptimeHistory: uptimeService.getUptimeHistory
};
};