mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 15:33:17 +00:00
273 lines
7.6 KiB
Vue
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>
|