panels-origin/web-saas/pages/smtp.vue
2026-08-21 16:55:01 -05:00

273 lines
7.6 KiB
Vue

<template>
<div class="saas-page">
<header class="saas-header">
<div>
<p class="saas-kicker">Operadores PANELS</p>
<h1>SMTP</h1>
<p class="saas-sub">Correo para enviar accesos a los administradores de cada empresa.</p>
</div>
<Button label="Empresas" icon="pi pi-building" severity="secondary" outlined @click="navigateTo('/')" />
</header>
<Message v-if="loadError" severity="error" :closable="false" class="mb-3">{{ loadError }}</Message>
<Message v-else-if="smtp?.source === 'env' && !smtp.host" severity="info" :closable="false" class="mb-3">
Aún no hay SMTP guardado. Puede configurar aquí o usar variables de entorno (SMTP_HOST / SMTP_FROM).
</Message>
<Message v-else-if="smtp?.configured" severity="success" :closable="false" class="mb-3">
SMTP activo (origen: {{ smtp.source === "db" ? "panel" : "entorno" }}).
</Message>
<Message v-else severity="warn" :closable="false" class="mb-3">
SMTP deshabilitado o incompleto: las contraseñas temporales se mostrarán en pantalla para copiarlas.
</Message>
<form class="smtp-card" @submit.prevent="save">
<div class="check-row">
<Checkbox v-model="form.enabled" input-id="smtp_enabled" binary />
<label for="smtp_enabled">Habilitar envío por SMTP</label>
</div>
<div class="detail-grid">
<FloatLabel variant="on" class="span-2">
<InputText id="host" v-model="form.host" class="w-full" autocomplete="off" />
<label for="host">Host</label>
</FloatLabel>
<FloatLabel variant="on">
<InputNumber id="port" v-model="form.port" class="w-full" :use-grouping="false" :min="1" :max="65535" />
<label for="port">Puerto</label>
</FloatLabel>
<FloatLabel variant="on">
<InputText id="from" v-model="form.from_address" class="w-full" autocomplete="off" />
<label for="from">Remitente (From)</label>
</FloatLabel>
<FloatLabel variant="on">
<InputText id="user" v-model="form.username" class="w-full" autocomplete="off" />
<label for="user">Usuario</label>
</FloatLabel>
<FloatLabel variant="on">
<Password
id="pass"
v-model="form.password"
class="w-full"
input-class="w-full"
:feedback="false"
toggle-mask
autocomplete="new-password"
/>
<label for="pass">{{ passLabel }}</label>
</FloatLabel>
</div>
<p class="hint">
Deje la contraseña vacía para conservar la actual.
Puertos habituales: 587 (STARTTLS) o 465 (SSL).
</p>
<Message v-if="formError" severity="error" :closable="false">{{ formError }}</Message>
<Message v-if="okMsg" severity="success" :closable="false">{{ okMsg }}</Message>
<div class="saas-form-actions">
<Button type="submit" label="Guardar" icon="pi pi-check" :loading="saving" />
</div>
</form>
<form class="smtp-card test-card" @submit.prevent="sendTest">
<h2 class="test-title">Probar envío</h2>
<div class="detail-grid">
<FloatLabel variant="on" class="span-2">
<InputText id="test_to" v-model="testTo" class="w-full" type="email" required />
<label for="test_to">Enviar prueba a</label>
</FloatLabel>
</div>
<Message v-if="testError" severity="error" :closable="false">{{ testError }}</Message>
<Message v-if="testOk" severity="success" :closable="false">{{ testOk }}</Message>
<div class="saas-form-actions">
<Button type="submit" label="Enviar correo de prueba" icon="pi pi-send" severity="secondary" :loading="testing" />
</div>
</form>
</div>
</template>
<script setup lang="ts">
const { api } = useApi();
const toast = useToast();
type SmtpPublic = {
host: string;
port: number;
username: string;
from_address: string;
enabled: boolean;
configured: boolean;
has_password: boolean;
updated_at: string | null;
source: "db" | "env" | "none";
};
const smtp = ref<SmtpPublic | null>(null);
const loadError = ref("");
const formError = ref("");
const okMsg = ref("");
const saving = ref(false);
const testing = ref(false);
const testTo = ref("");
const testError = ref("");
const testOk = ref("");
const form = reactive({
host: "",
port: 587 as number | null,
username: "",
password: "",
from_address: "",
enabled: true,
});
const passLabel = computed(() =>
smtp.value?.has_password ? "Contraseña (vacío = no cambiar)" : "Contraseña",
);
function applySmtp(s: SmtpPublic) {
smtp.value = s;
form.host = s.host;
form.port = s.port || 587;
form.username = s.username;
form.from_address = s.from_address;
form.enabled = s.enabled;
form.password = "";
}
async function load() {
loadError.value = "";
try {
const r = await api<{ smtp: SmtpPublic }>("/v1/saas/smtp");
applySmtp(r.smtp);
} catch (e: unknown) {
loadError.value = e instanceof Error ? e.message : "No se pudo cargar SMTP";
}
}
async function save() {
formError.value = "";
okMsg.value = "";
saving.value = true;
try {
const r = await api<{ smtp: SmtpPublic }>("/v1/saas/smtp", {
method: "PUT",
json: {
host: form.host,
port: form.port ?? 587,
username: form.username,
password: form.password,
from_address: form.from_address,
enabled: form.enabled,
},
});
applySmtp(r.smtp);
okMsg.value = "Configuración guardada";
toast.add({ severity: "success", summary: "SMTP guardado", life: 2200 });
} catch (e: unknown) {
formError.value = e instanceof Error ? e.message : "Error al guardar";
} finally {
saving.value = false;
}
}
async function sendTest() {
testError.value = "";
testOk.value = "";
testing.value = true;
try {
const r = await api<{ ok: boolean; to?: string; message_id?: string }>("/v1/saas/smtp/test", {
method: "POST",
json: { to: testTo.value },
});
testOk.value = `Servidor aceptó el envío a ${testTo.value}${r.message_id ? ` (${r.message_id})` : ""}. Revise bandeja de entrada y spam.`;
toast.add({ severity: "success", summary: "Prueba aceptada por SMTP", life: 2500 });
} catch (e: unknown) {
testError.value = e instanceof Error ? e.message : "Falló la prueba";
} finally {
testing.value = false;
}
}
onMounted(load);
</script>
<style scoped>
.saas-page {
max-width: 720px;
margin: 0 auto;
padding: 1.5rem 1.25rem 3rem;
}
.saas-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.25rem;
}
.saas-kicker {
margin: 0;
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #0f4c81;
font-weight: 700;
}
.saas-header h1 {
margin: 0.15rem 0;
font-size: 1.75rem;
}
.saas-sub {
margin: 0;
color: #64748b;
font-size: 0.95rem;
}
.smtp-card {
background: #fff;
border: 1px solid rgba(15, 23, 42, 0.08);
border-radius: 12px;
padding: 1.25rem;
margin-bottom: 1rem;
}
.test-card {
background: #f8fafc;
}
.test-title {
margin: 0 0 1rem;
font-size: 1.05rem;
}
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.1rem 0.85rem;
margin-top: 1rem;
}
.span-2 {
grid-column: span 2;
}
.check-row {
display: flex;
align-items: center;
gap: 0.55rem;
}
.hint {
margin: 0.85rem 0 0;
font-size: 0.85rem;
color: #64748b;
}
.saas-form-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1rem;
}
@media (max-width: 560px) {
.detail-grid {
grid-template-columns: 1fr;
}
.span-2 {
grid-column: span 1;
}
}
</style>