feat: implement SMTP configuration management for super admin and tenant levels

This commit is contained in:
Yashwin 2026-04-14 18:02:26 +05:30
parent 2381260190
commit cde2544cf3
7 changed files with 887 additions and 0 deletions

View File

@ -69,6 +69,14 @@ const superAdminSystemMenu: MenuItem[] = [
},
{ icon: FileText, label: "Audit Logs", path: "/audit-logs" },
{ icon: Shield, label: "Audit Resources", path: "/audit-resource-types" },
{
icon: Settings,
label: "Settings",
isGroup: true,
children: [
{ label: "SMTP Config", path: "/settings/smtp" },
],
},
];
// Tenant Admin menu items
@ -202,6 +210,10 @@ const tenantAdminSystemMenu: MenuItem[] = [
{
label: "Notification Templates",
path: "/tenant/settings/notification-templates",
},
{
label: "SMTP Settings",
path: "/tenant/settings/smtp",
}
],
requiredPermission: { resource: "tenants" },

View File

@ -0,0 +1,378 @@
import { useState, useEffect } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { Modal, PrimaryButton, SecondaryButton, FormField, FormSelect } from '@/components/shared';
import { smtpConfigService, type SmtpConfig } from '@/services/smtp-config-service';
import { tenantService } from '@/services/tenant-service';
import { showToast } from '@/utils/toast';
import { Loader2, Shield, CheckCircle, XCircle } from 'lucide-react';
const smtpObjectSchema = z.object({
id: z.string().optional(),
scope: z.enum(['super_admin', 'tenant']),
tenant_id: z.string().nullable().optional(),
provider: z.string().min(1, 'Provider is required'),
host: z.string().min(1, 'Host is required'),
port: z.number().min(1, 'Port is required'),
secure: z.boolean(),
username: z.string().min(1, 'Username is required'),
password: z.string().optional(),
from_name: z.string().nullable().optional(),
from_email: z.string().email('Invalid email').nullable().optional().or(z.literal('')),
is_active: z.boolean()
});
const smtpSchema = smtpObjectSchema.refine((data) => {
if (data.scope === 'tenant' && !data.tenant_id) {
return false;
}
return true;
}, {
message: "Tenant is required for tenant specific configuration",
path: ["tenant_id"]
});
type SmtpFormValues = z.infer<typeof smtpObjectSchema>;
interface SmtpConfigModalProps {
isOpen: boolean;
onClose: () => void;
onSubmit: () => Promise<void>;
config?: SmtpConfig | null;
}
export const SmtpConfigModal = ({ isOpen, onClose, onSubmit, config }: SmtpConfigModalProps) => {
const [tenants, setTenants] = useState<{ value: string; label: string }[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isTesting, setIsTesting] = useState(false);
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
const {
control,
handleSubmit,
reset,
watch,
setValue,
formState: { errors }
} = useForm<SmtpFormValues>({
resolver: zodResolver(smtpSchema),
defaultValues: {
scope: 'tenant',
provider: 'smtp',
host: '',
port: 587,
secure: false,
username: '',
password: '',
from_name: '',
from_email: '',
is_active: true,
tenant_id: ''
}
});
const scope = watch('scope');
useEffect(() => {
if (isOpen) {
if (config) {
reset({
...config,
password: '',
tenant_id: config.tenant_id || ''
});
} else {
reset({
scope: 'tenant',
provider: 'smtp',
host: '',
port: 587,
secure: false,
username: '',
password: '',
from_name: '',
from_email: '',
is_active: true,
tenant_id: ''
});
}
setTestResult(null);
}
}, [config, isOpen, reset]);
useEffect(() => {
if (isOpen && scope === 'tenant') {
loadTenants();
}
}, [isOpen, scope]);
const loadTenants = async () => {
try {
const res = await tenantService.getAll(1, 100);
if (res.success) {
setTenants(res.data.map((t: any) => ({ value: t.id, label: t.name })));
}
} catch (err) {
showToast.error('Failed to load tenants');
}
};
const handleTestConnection = async () => {
const data = watch();
setIsTesting(true);
setTestResult(null);
try {
const res = await smtpConfigService.adminTestConnection(data);
setTestResult({
success: res.success,
message: res.message
});
if (res.success) {
showToast.success('SMTP connection verified');
} else {
showToast.error('SMTP connection failed');
}
} catch (err: any) {
const errorData = err.response?.data?.error;
const errorMsg = typeof errorData === 'string' ? errorData : errorData?.message;
setTestResult({
success: false,
message: errorMsg || err.response?.data?.message || 'Connection test failed'
});
} finally {
setIsTesting(false);
}
};
const onFormSubmit = async (values: SmtpFormValues) => {
setIsSubmitting(true);
try {
// If it's a new config, ensure password is not empty
if (!config && (!values.password || values.password.trim() === '')) {
showToast.error('Password is required for new configurations');
setIsSubmitting(false);
return;
}
await smtpConfigService.adminUpsertConfig(values);
showToast.success('SMTP configuration saved');
await onSubmit();
onClose();
} catch (err: any) {
const errorData = err.response?.data?.error;
const errorMsg = typeof errorData === 'string' ? errorData : errorData?.message;
showToast.error(errorMsg || err.response?.data?.message || 'Failed to save configuration');
} finally {
setIsSubmitting(false);
}
};
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={config ? 'Edit SMTP Configuration' : 'New SMTP Configuration'}
maxWidth="xl"
>
<form onSubmit={handleSubmit(onFormSubmit)} className="p-5 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Controller
name="scope"
control={control}
render={({ field }) => (
<FormSelect
label="Scope"
value={field.value}
onValueChange={(val) => {
field.onChange(val);
if (val === 'super_admin') setValue('tenant_id', '');
}}
options={[
{ value: 'super_admin', label: 'Global (Super Admin)' },
{ value: 'tenant', label: 'Tenant Specific' }
]}
error={errors.scope?.message}
required
/>
)}
/>
{scope === 'tenant' && (
<Controller
name="tenant_id"
control={control}
render={({ field }) => (
<FormSelect
label="Tenant"
value={field.value || ''}
onValueChange={field.onChange}
options={tenants}
error={errors.tenant_id?.message}
required
/>
)}
/>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="md:col-span-2">
<Controller
name="host"
control={control}
render={({ field }) => (
<FormField
label="SMTP Host"
{...field}
placeholder="smtp.example.com"
error={errors.host?.message}
required
/>
)}
/>
</div>
<Controller
name="port"
control={control}
render={({ field }) => (
<FormField
label="Port"
type="number"
{...field}
value={field.value?.toString()}
onChange={(e) => field.onChange(e.target.value ? Number(e.target.value) : undefined)}
placeholder="587"
error={errors.port?.message}
required
/>
)}
/>
</div>
<div className="flex items-center space-x-4">
<Controller
name="secure"
control={control}
render={({ field }) => (
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={field.value}
onChange={(e) => field.onChange(e.target.checked)}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<span className="text-sm font-medium text-gray-700">Use SSL/TLS</span>
</label>
)}
/>
<div className="flex-1"></div>
<Controller
name="is_active"
control={control}
render={({ field }) => (
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={field.value}
onChange={(e) => field.onChange(e.target.checked)}
className="w-4 h-4 text-green-600 border-gray-300 rounded focus:ring-green-500"
/>
<span className="text-sm font-medium text-gray-700">Active</span>
</label>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2 border-t border-gray-100">
<Controller
name="username"
control={control}
render={({ field }) => (
<FormField
label="Username"
{...field}
placeholder="user@example.com"
error={errors.username?.message}
required
/>
)}
/>
<Controller
name="password"
control={control}
render={({ field }) => (
<FormField
label="Password"
type="password"
{...field}
placeholder={config ? 'Leave blank to keep current' : '••••••••'}
error={errors.password?.message}
required={!config}
/>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Controller
name="from_name"
control={control}
render={({ field }) => (
<FormField
label="Sender Name"
{...field}
value={field.value ?? ''}
placeholder="QAssure Notifications"
error={errors.from_name?.message}
/>
)}
/>
<Controller
name="from_email"
control={control}
render={({ field }) => (
<FormField
label="Sender Email"
type="email"
{...field}
value={field.value ?? ''}
placeholder="noreply@qassure.ai"
error={errors.from_email?.message}
/>
)}
/>
</div>
{testResult && (
<div className={`p-3 rounded-lg flex items-start gap-3 ${testResult.success ? 'bg-green-50 text-green-700 border border-green-100' : 'bg-red-50 text-red-700 border border-red-100'}`}>
{testResult.success ? <CheckCircle className="w-5 h-5 mt-0.5 shrink-0" /> : <XCircle className="w-5 h-5 mt-0.5 shrink-0" />}
<span className="text-sm">{testResult.message}</span>
</div>
)}
<div className="flex items-center justify-between pt-4 border-t border-gray-100">
<SecondaryButton
type="button"
onClick={handleTestConnection}
disabled={isTesting || !watch('host') || !watch('username')}
>
{isTesting ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Shield className="w-4 h-4 mr-2" />
)}
Test Connection
</SecondaryButton>
<div className="flex gap-3">
<SecondaryButton type="button" onClick={onClose}>Cancel</SecondaryButton>
<PrimaryButton type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Saving...' : (config ? 'Update Configuration' : 'Create Configuration')}
</PrimaryButton>
</div>
</div>
</form>
</Modal>
);
};

View File

@ -0,0 +1,193 @@
import { useState, useEffect } from 'react';
import { Layout } from '@/components/layout/Layout';
import {
DataTable,
Pagination,
StatusBadge,
ActionDropdown,
PrimaryButton,
DeleteConfirmationModal,
type Column
} from '@/components/shared';
import { Plus, Server, Globe, Building } from 'lucide-react';
import { smtpConfigService, type SmtpConfig } from '@/services/smtp-config-service';
import { SmtpConfigModal } from '@/components/superadmin/SmtpConfigModal';
import { showToast } from '@/utils/toast';
const SmtpConfigPage = () => {
const [configs, setConfigs] = useState<SmtpConfig[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedConfig, setSelectedConfig] = useState<SmtpConfig | null>(null);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
// Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [limit, setLimit] = useState(10);
const [totalItems, setTotalItems] = useState(0);
const fetchConfigs = async () => {
setIsLoading(true);
try {
const res = await smtpConfigService.listAll({
offset: (currentPage - 1) * limit,
limit: limit
});
if (res.success) {
setConfigs(res.data);
// Assuming the API would return total items if we had many,
// for now let's just use the length or a fixed number
setTotalItems(res.data.length);
}
} catch (err) {
showToast.error('Failed to load SMTP configurations');
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchConfigs();
}, [currentPage, limit]);
const handleEdit = (config: SmtpConfig) => {
setSelectedConfig(config);
setIsModalOpen(true);
};
const handleDelete = (config: SmtpConfig) => {
setSelectedConfig(config);
setDeleteModalOpen(true);
};
const confirmDelete = async () => {
if (!selectedConfig?.id) return;
setIsDeleting(true);
try {
await smtpConfigService.deleteConfig(selectedConfig.id);
showToast.success('Configuration deleted');
setDeleteModalOpen(false);
fetchConfigs();
} catch (err) {
showToast.error('Failed to delete configuration');
} finally {
setIsDeleting(false);
}
};
const columns: Column<SmtpConfig>[] = [
{
key: 'scope',
label: 'Scope',
render: (config) => (
<div className="flex items-center gap-2">
{config.scope === 'super_admin' ? (
<Globe className="w-4 h-4 text-blue-600" />
) : (
<Building className="w-4 h-4 text-purple-600" />
)}
<span className="text-sm font-medium">
{config.scope === 'super_admin' ? 'Global' : config.tenant_name || 'Tenant'}
</span>
</div>
)
},
{
key: 'host',
label: 'Server',
render: (config) => (
<div className="flex items-center gap-2">
<Server className="w-4 h-4 text-gray-400" />
<span className="text-sm">{config.host}:{config.port}</span>
</div>
)
},
{
key: 'from_email',
label: 'Sender',
render: (config) => (
<div className="flex flex-col">
<span className="text-sm font-medium">{config.from_name || 'N/A'}</span>
<span className="text-xs text-gray-500">{config.from_email || 'N/A'}</span>
</div>
)
},
{
key: 'is_active',
label: 'Status',
render: (config) => (
<StatusBadge variant={config.is_active ? 'success' : 'failure'}>
{config.is_active ? 'Active' : 'Inactive'}
</StatusBadge>
)
},
{
key: 'actions',
label: 'Actions',
align: 'right',
render: (config) => (
<ActionDropdown
onEdit={() => handleEdit(config)}
onDelete={() => handleDelete(config)}
/>
)
}
];
return (
<Layout
currentPage="Settings"
pageHeader={{
title: 'SMTP Configurations',
description: 'Manage email delivery settings for the entire platform and individual tenants.',
action: (
<PrimaryButton onClick={() => { setSelectedConfig(null); setIsModalOpen(true); }}>
<Plus className="w-4 h-4 mr-2" />
Add Configuration
</PrimaryButton>
)
}}
>
<div className="bg-white border border-gray-100 rounded-lg shadow-sm overflow-hidden">
<DataTable
columns={columns}
data={configs}
isLoading={isLoading}
keyExtractor={(item) => item.id!}
emptyMessage="No SMTP configurations found"
/>
{totalItems > limit && (
<Pagination
currentPage={currentPage}
totalPages={Math.ceil(totalItems / limit)}
totalItems={totalItems}
limit={limit}
onPageChange={setCurrentPage}
onLimitChange={setLimit}
/>
)}
</div>
<SmtpConfigModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onSubmit={fetchConfigs}
config={selectedConfig}
/>
<DeleteConfirmationModal
isOpen={deleteModalOpen}
onClose={() => setDeleteModalOpen(false)}
onConfirm={confirmDelete}
title="Delete SMTP Configuration"
message="Are you sure you want to delete this SMTP configuration? This action cannot be undone."
itemName={selectedConfig?.scope === 'super_admin' ? 'Global Config' : selectedConfig?.tenant_name || 'Tenant Config'}
isLoading={isDeleting}
/>
</Layout>
);
};
export default SmtpConfigPage;

View File

@ -0,0 +1,237 @@
import { useState, useEffect } from 'react';
import { Layout } from "@/components/layout/Layout";
import { Loader2, Info, Shield } from "lucide-react";
import { useAppSelector } from "@/hooks/redux-hooks";
import { smtpConfigService, type SmtpConfig } from "@/services/smtp-config-service";
import { showToast } from "@/utils/toast";
import { PrimaryButton, SecondaryButton, FormField, StatusBadge } from "@/components/shared";
const SmtpSettings = () => {
const tenantId = useAppSelector((state) => state.auth.tenantId);
const [config, setConfig] = useState<Partial<SmtpConfig>>({
host: '',
port: 587,
secure: false,
username: '',
password: '',
from_name: '',
from_email: '',
is_active: true,
scope: 'tenant'
});
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [isTesting, setIsTesting] = useState(false);
const [hasExistingConfig, setHasExistingConfig] = useState(false);
useEffect(() => {
fetchConfig();
}, [tenantId]);
const fetchConfig = async () => {
if (!tenantId) return;
setIsLoading(true);
try {
const res = await smtpConfigService.getConfig(tenantId);
if (res.success && res.data) {
setConfig({
...res.data,
password: '' // Don't show password
});
setHasExistingConfig(true);
} else {
setHasExistingConfig(false);
}
} catch (err) {
// It's fine if no config exists, it will fallback to global
} finally {
setIsLoading(false);
}
};
const handleTestConnection = async () => {
setIsTesting(true);
try {
const res = await smtpConfigService.testConnection(config);
if (res.success) {
showToast.success('SMTP connection verified successfully');
} else {
showToast.error(`Connection failed: ${res.message}`);
}
} catch (err: any) {
const errorData = err.response?.data?.error;
const errorMsg = typeof errorData === 'string' ? errorData : errorData?.message;
showToast.error(errorMsg || err.response?.data?.message || 'Connection test failed');
} finally {
setIsTesting(false);
}
};
const handleSave = async () => {
setIsSaving(true);
try {
await smtpConfigService.upsertConfig({
...config,
tenant_id: tenantId || undefined,
scope: 'tenant'
});
showToast.success('SMTP settings saved successfully');
setHasExistingConfig(true);
} catch (err: any) {
const errorData = err.response?.data?.error;
const errorMsg = typeof errorData === 'string' ? errorData : errorData?.message;
showToast.error(errorMsg || err.response?.data?.message || 'Failed to save settings');
} finally {
setIsSaving(false);
}
};
if (isLoading) {
return (
<Layout currentPage="Settings" pageHeader={{ title: "SMTP Settings", description: "Loading configurations..." }}>
<div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 text-blue-600 animate-spin" />
</div>
</Layout>
);
}
return (
<Layout
currentPage="Settings"
pageHeader={{
title: "SMTP Settings",
description: "Configure your own email delivery infrastructure for notifications.",
}}
>
<div className="flex flex-col gap-6 max-w-4xl">
{!hasExistingConfig && (
<div className="bg-blue-50 border border-blue-100 rounded-lg p-4 flex gap-4 items-start">
<Info className="w-5 h-5 text-blue-600 mt-0.5 shrink-0" />
<div>
<p className="text-sm font-medium text-blue-900">Using Platform Defaults</p>
<p className="text-sm text-blue-700 mt-1">
You haven't configured a custom SMTP. The platform is currently using the global fallback SMTP settings for sending your emails.
Configure your own settings below to send emails from your own domain.
</p>
</div>
</div>
)}
<div className="bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden">
<div className="p-4 md:p-6 border-b border-gray-100 flex items-center justify-between bg-gray-50/50">
<div>
<h3 className="text-base font-semibold text-gray-900">Custom SMTP Configuration</h3>
<p className="text-xs text-gray-500 mt-0.5">Define your outgoing mail server details</p>
</div>
{hasExistingConfig && (
<StatusBadge variant={config.is_active ? 'success' : 'failure'}>
{config.is_active ? 'Configured & Active' : 'Inactive'}
</StatusBadge>
)}
</div>
<div className="p-4 md:p-6 space-y-2">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="md:col-span-2">
<FormField
label="SMTP Host"
value={config.host}
onChange={(e) => setConfig({ ...config, host: e.target.value })}
placeholder="smtp.your-company.com"
/>
</div>
<FormField
label="Port"
type="number"
value={config.port?.toString()}
onChange={(e) => setConfig({ ...config, port: parseInt(e.target.value) })}
placeholder="587"
/>
</div>
<div className="flex items-center gap-6 py-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={config.secure}
onChange={(e) => setConfig({ ...config, secure: e.target.checked })}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<span className="text-sm font-medium text-gray-700">Use SSL/TLS</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={config.is_active}
onChange={(e) => setConfig({ ...config, is_active: e.target.checked })}
className="w-4 h-4 text-green-600 border-gray-300 rounded focus:ring-green-500"
/>
<span className="text-sm font-medium text-gray-700">Enable SMTP</span>
</label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t border-gray-50">
<FormField
label="Username / Email"
value={config.username}
onChange={(e) => setConfig({ ...config, username: e.target.value })}
placeholder="notifications@yourdomain.com"
/>
<FormField
label="Password"
type="password"
value={config.password}
onChange={(e) => setConfig({ ...config, password: e.target.value })}
placeholder={hasExistingConfig ? '••••••••' : 'Your SMTP password'}
helperText={hasExistingConfig ? "Leave blank to keep existing password" : ""}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
label="Sender Display Name"
value={config.from_name || ''}
onChange={(e) => setConfig({ ...config, from_name: e.target.value })}
placeholder="Your Company Notifications"
/>
<FormField
label="Sender Email"
type="email"
value={config.from_email || ''}
onChange={(e) => setConfig({ ...config, from_email: e.target.value })}
placeholder="noreply@yourdomain.com"
/>
</div>
</div>
<div className="px-4 md:px-6 py-4 bg-gray-50 border-t border-gray-100 flex items-center justify-between">
<SecondaryButton
type="button"
onClick={handleTestConnection}
disabled={isTesting || !config.host || !config.username}
>
{isTesting ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Shield className="w-4 h-4 mr-2" />
)}
Test Connection
</SecondaryButton>
<PrimaryButton
type="button"
onClick={handleSave}
disabled={isSaving || !config.host || !config.username}
>
{isSaving ? "Saving..." : "Save Settings"}
</PrimaryButton>
</div>
</div>
</div>
</Layout>
);
};
export default SmtpSettings;

View File

@ -17,6 +17,7 @@ const Notifications = lazy(() => import("@/pages/tenant/Notifications"));
const AuditLogResourceTypes = lazy(() => import("@/pages/superadmin/AuditLogResourceTypes"));
const NotificationMaster = lazy(() => import("@/pages/superadmin/NotificationMaster"));
const NotificationTemplateMaster = lazy(() => import("@/pages/superadmin/NotificationTemplateMaster"));
const SmtpConfig = lazy(() => import("@/pages/superadmin/SmtpConfig"));
// Loading fallback component
const RouteLoader = (): ReactElement => (
@ -95,4 +96,8 @@ export const superAdminRoutes: RouteConfig[] = [
path: "/notification-templates",
element: <LazyRoute component={NotificationTemplateMaster} />,
},
{
path: "/settings/smtp",
element: <LazyRoute component={SmtpConfig} />,
},
];

View File

@ -34,6 +34,7 @@ const NotificationTemplates = lazy(() => import("@/pages/tenant/NotificationTemp
const FilesList = lazy(() => import("@/pages/tenant/FilesList"));
const FileView = lazy(() => import("@/pages/tenant/FileView"));
const StorageDashboard = lazy(() => import("@/pages/tenant/StorageDashboard"));
const SmtpSettings = lazy(() => import("@/pages/tenant/SmtpSettings"));
// Loading fallback component
const RouteLoader = (): ReactElement => (
@ -156,4 +157,8 @@ export const tenantAdminRoutes: RouteConfig[] = [
path: "/tenant/files/storage-dashboard",
element: <LazyRoute component={StorageDashboard} />,
},
{
path: "/tenant/settings/smtp",
element: <LazyRoute component={SmtpSettings} />,
},
];

View File

@ -0,0 +1,57 @@
import apiClient from './api-client';
export interface SmtpConfig {
id?: string;
tenant_id: string | null;
scope: 'super_admin' | 'tenant';
provider: string;
host: string;
port: number;
secure: boolean;
username: string;
password?: string;
from_name: string | null;
from_email: string | null;
is_active: boolean;
tenant_name?: string;
}
class SmtpConfigService {
async getConfig(tenantId?: string) {
const params = tenantId ? { tenantId } : {};
const response = await apiClient.get('/smtp-config', { params });
return response.data;
}
async listAll(params: any = {}) {
const response = await apiClient.get('/smtp-config/all', { params });
return response.data;
}
async upsertConfig(data: Partial<SmtpConfig>) {
const response = await apiClient.post('/smtp-config', data);
return response.data;
}
async adminUpsertConfig(data: Partial<SmtpConfig>) {
const response = await apiClient.post('/smtp-config/setup', data);
return response.data;
}
async testConnection(data: any) {
const response = await apiClient.post('/smtp-config/test', data);
return response.data;
}
async adminTestConnection(data: any) {
const response = await apiClient.post('/smtp-config/admin/test', data);
return response.data;
}
async deleteConfig(id: string) {
const response = await apiClient.delete(`/smtp-config/${id}`);
return response.data;
}
}
export const smtpConfigService = new SmtpConfigService();