diff --git a/application/src/App.tsx b/application/src/App.tsx index 022f211..2068c31 100644 --- a/application/src/App.tsx +++ b/application/src/App.tsx @@ -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() { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/application/src/api/index.ts b/application/src/api/index.ts index 16bf896..c4f7f62 100644 --- a/application/src/api/index.ts +++ b/application/src/api/index.ts @@ -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 diff --git a/application/src/api/settings/actions/getSettings.ts b/application/src/api/settings/actions/getSettings.ts new file mode 100644 index 0000000..bf83662 --- /dev/null +++ b/application/src/api/settings/actions/getSettings.ts @@ -0,0 +1,26 @@ + +import { getAuthHeaders, getBaseUrl } from '../utils'; +import { SettingsApiResponse } from '../types'; + +export const getSettings = async (): Promise => { + 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' }, + }; + } +}; \ No newline at end of file diff --git a/application/src/api/settings/actions/sendTestEmail.ts b/application/src/api/settings/actions/sendTestEmail.ts new file mode 100644 index 0000000..5617cea --- /dev/null +++ b/application/src/api/settings/actions/sendTestEmail.ts @@ -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 = ` + + +
+

Test Email

+

This is a test email from your monitoring system.

+

If you received this email, your SMTP configuration is working correctly.

+
+

+ Sent from ReamStack Monitoring System
+ Template: ${template}
+ ${data.collection ? `Collection: ${data.collection}` : ''} +

+
+ + + `; + + switch (template) { + case 'verification': + subject = 'Email Verification Test - ReamStack'; + htmlBody = ` + + +
+

Email Verification Test

+

This is a test of the email verification template.

+

If you received this email, your SMTP configuration is working correctly.

+
+

Template: Verification Email

+

Collection: ${data.collection || '_superusers'}

+
+
+

Sent from ReamStack Monitoring System

+
+ + + `; + break; + case 'password-reset': + subject = 'Password Reset Test - ReamStack'; + htmlBody = ` + + +
+

Password Reset Test

+

This is a test of the password reset template.

+

If you received this email, your SMTP configuration is working correctly.

+
+

Template: Password Reset Email

+

Collection: ${data.collection || '_superusers'}

+
+
+

Sent from ReamStack Monitoring System

+
+ + + `; + break; + case 'email-change': + subject = 'Email Change Confirmation Test - ReamStack'; + htmlBody = ` + + +
+

Email Change Confirmation Test

+

This is a test of the email change confirmation template.

+

If you received this email, your SMTP configuration is working correctly.

+
+

Template: Email Change Confirmation

+
+
+

Sent from ReamStack Monitoring System

+
+ + + `; + break; + } + + return { subject, htmlBody }; +}; + +export const sendTestEmail = async (data: any): Promise => { + 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.' + }, + }; + } +}; \ No newline at end of file diff --git a/application/src/api/settings/actions/testEmail.ts b/application/src/api/settings/actions/testEmail.ts new file mode 100644 index 0000000..c3263aa --- /dev/null +++ b/application/src/api/settings/actions/testEmail.ts @@ -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 = ` + + +
+

Test Email

+

This is a test email from your monitoring system.

+

If you received this email, your SMTP configuration is working correctly.

+
+

+ Sent from ReamStack Monitoring System
+ Template: ${template}
+ ${data.collection ? `Collection: ${data.collection}` : ''} +

+
+ + + `; + + switch (template) { + case 'verification': + subject = 'Email Verification Test - ReamStack'; + htmlBody = ` + + +
+

Email Verification Test

+

This is a test of the email verification template.

+

If you received this email, your SMTP configuration is working correctly.

+
+

Template: Verification Email

+

Collection: ${data.collection || '_superusers'}

+
+
+

Sent from ReamStack Monitoring System

+
+ + + `; + break; + case 'password-reset': + subject = 'Password Reset Test - ReamStack'; + htmlBody = ` + + +
+

Password Reset Test

+

This is a test of the password reset template.

+

If you received this email, your SMTP configuration is working correctly.

+
+

Template: Password Reset Email

+

Collection: ${data.collection || '_superusers'}

+
+
+

Sent from ReamStack Monitoring System

+
+ + + `; + break; + case 'email-change': + subject = 'Email Change Confirmation Test - ReamStack'; + htmlBody = ` + + +
+

Email Change Confirmation Test

+

This is a test of the email change confirmation template.

+

If you received this email, your SMTP configuration is working correctly.

+
+

Template: Email Change Confirmation

+
+
+

Sent from ReamStack Monitoring System

+
+ + + `; + break; + } + + return { subject, htmlBody }; +}; + +export const testEmail = async (data: any): Promise => { + 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.' + }, + }; + } +}; \ No newline at end of file diff --git a/application/src/api/settings/actions/testEmailConnection.ts b/application/src/api/settings/actions/testEmailConnection.ts new file mode 100644 index 0000000..41b6876 --- /dev/null +++ b/application/src/api/settings/actions/testEmailConnection.ts @@ -0,0 +1,31 @@ + +import { getAuthHeaders, getBaseUrl } from '../utils'; +import { SettingsApiResponse } from '../types'; + +export const testEmailConnection = async (data: any): Promise => { + 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' }, + }; + } +}; \ No newline at end of file diff --git a/application/src/api/settings/actions/updateSettings.ts b/application/src/api/settings/actions/updateSettings.ts new file mode 100644 index 0000000..0c88abc --- /dev/null +++ b/application/src/api/settings/actions/updateSettings.ts @@ -0,0 +1,38 @@ + +import { getAuthHeaders, getBaseUrl } from '../utils'; +import { SettingsApiResponse } from '../types'; + +export const updateSettings = async (data: any): Promise => { + 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' }, + }; + } +}; \ No newline at end of file diff --git a/application/src/api/settings/index.ts b/application/src/api/settings/index.ts index 94a7b0c..1be26bc 100644 --- a/application/src/api/settings/index.ts +++ b/application/src/api/settings/index.ts @@ -1,199 +1,41 @@ -import { pb, getCurrentEndpoint } from '@/lib/pocketbase'; - -const settingsApi = async (body: any) => { - try { - const { action, data } = body; - console.log('Settings API called with action:', action, 'data:', data); - - const authToken = pb.authStore.token; - const headers: Record = { - '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' }, - }; +import { getSettings } from './actions/getSettings'; +import { updateSettings } from './actions/updateSettings'; +import { testEmailConnection } from './actions/testEmailConnection'; +import { testEmail } from './actions/testEmail'; + +/** + * 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); + + 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' }, + }; } }; diff --git a/application/src/api/settings/types.ts b/application/src/api/settings/types.ts new file mode 100644 index 0000000..01ef5f1 --- /dev/null +++ b/application/src/api/settings/types.ts @@ -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; +} \ No newline at end of file diff --git a/application/src/api/settings/utils.ts b/application/src/api/settings/utils.ts new file mode 100644 index 0000000..0039a1b --- /dev/null +++ b/application/src/api/settings/utils.ts @@ -0,0 +1,24 @@ + +import { pb, getCurrentEndpoint } from '@/lib/pocketbase'; + +export const getAuthHeaders = (): Record => { + const authToken = pb.authStore.token; + const headers: Record = { + '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); +}; \ No newline at end of file diff --git a/application/src/components/services/add-service/ServiceTypeField.tsx b/application/src/components/services/add-service/ServiceTypeField.tsx index b0c8933..73c9848 100644 --- a/application/src/components/services/add-service/ServiceTypeField.tsx +++ b/application/src/components/services/add-service/ServiceTypeField.tsx @@ -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 ; + case "ping": + return ; + case "tcp": + return ; + case "dns": + return ; + default: + return ; + } + }; + return ( - {field.value === "http" && ( + {field.value && (
- - HTTP/S + {getServiceIcon(field.value)} + {field.value.toUpperCase()}
)} - {field.value !== "http" && "Select a service type"} + {!field.value && "Select a service type"}
@@ -41,13 +56,43 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) { HTTP/S

- Monitor websites and REST APIs with HTTP/HTTPS protocol + Monitor websites and REST APIs with HTTP/HTTPS Protocol +

+ + + +
+
+ + PING +
+

+ Monitor host availability with PING Protocol +

+
+
+ +
+
+ + TCP +
+

+ Monitor TCP port connectivity with TCP Protocol +

+
+
+ +
+
+ + DNS +
+

+ Monitor DNS resolution

- PING - TCP - DNS
diff --git a/application/src/components/settings/general/MailSettingsTab.tsx b/application/src/components/settings/general/MailSettingsTab.tsx index 45f3ff7..1e02c95 100644 --- a/application/src/components/settings/general/MailSettingsTab.tsx +++ b/application/src/components/settings/general/MailSettingsTab.tsx @@ -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 = ({ 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 = ({ )} /> - - {/* Only show Test Email button, removed Test Connection button */} - {isEditing && form.watch('smtp.enabled') && ( -
+
+ + {/* Test Email button - outside the form area and always visible when SMTP is enabled */} + {form.watch('smtp.enabled') && ( +
+
+
+

{t("testEmailSettings", "settings")}

+

{t("testEmailDescription", "settings")}

+
- )} -
+ + )} = ({ ); }; -export default MailSettingsTab; +export default MailSettingsTab; \ No newline at end of file diff --git a/application/src/components/settings/general/TestEmailDialog.tsx b/application/src/components/settings/general/TestEmailDialog.tsx index 08be7b6..03afddf 100644 --- a/application/src/components/settings/general/TestEmailDialog.tsx +++ b/application/src/components/settings/general/TestEmailDialog.tsx @@ -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 = ({ 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 = ({ 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 ( @@ -90,16 +131,31 @@ const TestEmailDialog: React.FC = ({ size="icon" className="absolute right-4 top-4" onClick={handleClose} + disabled={isLoading} >
+ {/* Show last result */} + {lastResult && ( + + {lastResult.success ? ( + + ) : ( + + )} + + {lastResult.message} + + + )} + {/* Template Selection */}
- +
@@ -112,22 +168,14 @@ const TestEmailDialog: React.FC = ({
-
- - -
-
- - -
- {/* Auth Collection - only show for verification template */} - {template === 'verification' && ( + {/* Auth Collection - show for verification and password-reset templates */} + {(template === 'verification' || template === 'password-reset') && (
- @@ -148,21 +196,34 @@ const TestEmailDialog: React.FC = ({ onChange={(e) => setEmail(e.target.value)} placeholder={t("enterEmailAddress", "settings")} required + disabled={isLoading} />
+ + {/* Info message */} + + + + This will send a test email using your configured SMTP settings. Make sure SMTP is properly configured first. + +
-
diff --git a/application/src/services/monitoring/index.ts b/application/src/services/monitoring/index.ts index 7ef629c..2e36988 100644 --- a/application/src/services/monitoring/index.ts +++ b/application/src/services/monitoring/index.ts @@ -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 -}; +}; \ No newline at end of file diff --git a/application/src/services/monitoring/service-status/startMonitoring.ts b/application/src/services/monitoring/service-status/startMonitoring.ts index 4d4f870..3a156ad 100644 --- a/application/src/services/monitoring/service-status/startMonitoring.ts +++ b/application/src/services/monitoring/service-status/startMonitoring.ts @@ -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 { 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); } -} +} \ No newline at end of file diff --git a/application/src/services/monitoring/utils/httpUtils.ts b/application/src/services/monitoring/utils/httpUtils.ts index b2c24c6..08f959d 100644 --- a/application/src/services/monitoring/utils/httpUtils.ts +++ b/application/src/services/monitoring/utils/httpUtils.ts @@ -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 }; -} +} \ No newline at end of file diff --git a/application/src/services/serviceService.ts b/application/src/services/serviceService.ts index 4757135..9de9850 100644 --- a/application/src/services/serviceService.ts +++ b/application/src/services/serviceService.ts @@ -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 -}; +}; \ No newline at end of file