Email: a stdlib SMTP mailer (settings-first config, graceful no-op when unconfigured), a test-email endpoint wired to the Email settings page, forced first-login password change (users.mustchangepassword, migration 7d23, /change-password flow), new-user welcome mail, and on-demand report/alert delivery (POST /api/reports/email + Email Report buttons) with an external-cron-with-a-scoped-PAT path documented for automation. All tests patch smtplib - no network. Labels: a shared /print/asset-label/<type>/<id> view any asset detail page opens - card or plain style, QR or barcode, configurable encoding. Per-type qr_target_* templates plus label_default_style/codetype/encodes settings on the Printing page. Measuring-tool labels default to encoding their inspection-operation code (derived from the location name, e.g. 0615), so every tool in an area shares the area code - verified by decoding the rendered QR. Machine labels default to the machine number; blank-serial handled gracefully. 808 tests pass; both features verified live. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
217 lines
6.8 KiB
Vue
217 lines
6.8 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<h2>Email / SMTP</h2>
|
|
</div>
|
|
|
|
<div class="section-card">
|
|
<div class="setting-group">
|
|
<h3>Email Notifications</h3>
|
|
<p class="setting-description">
|
|
Configure SMTP settings to enable email notifications for alerts, toner reports,
|
|
and other system notifications.
|
|
</p>
|
|
|
|
<div class="setting-row">
|
|
<label class="toggle-label">
|
|
<span>Enable Email Notifications</span>
|
|
<button
|
|
class="toggle-btn"
|
|
:class="{ active: settings.smtp_enabled }"
|
|
@click="toggleSetting('smtp_enabled')"
|
|
:disabled="saving"
|
|
>
|
|
<span class="toggle-slider"></span>
|
|
</button>
|
|
</label>
|
|
</div>
|
|
|
|
<template v-if="settings.smtp_enabled">
|
|
<div class="settings-grid">
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>SMTP Host</span>
|
|
<input
|
|
type="text"
|
|
v-model="settings.smtp_host"
|
|
placeholder="smtp.example.com"
|
|
@blur="saveSetting('smtp_host', settings.smtp_host)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>SMTP Port</span>
|
|
<input
|
|
type="number"
|
|
v-model="settings.smtp_port"
|
|
placeholder="587"
|
|
@blur="saveSetting('smtp_port', settings.smtp_port)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>Username</span>
|
|
<input
|
|
type="text"
|
|
v-model="settings.smtp_username"
|
|
placeholder="username"
|
|
@blur="saveSetting('smtp_username', settings.smtp_username)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>Password</span>
|
|
<input
|
|
type="password"
|
|
v-model="settings.smtp_password"
|
|
placeholder="password"
|
|
@blur="saveSetting('smtp_password', settings.smtp_password)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label class="toggle-label">
|
|
<span>Use TLS Encryption</span>
|
|
<button
|
|
class="toggle-btn"
|
|
:class="{ active: settings.smtp_use_tls }"
|
|
@click="toggleSetting('smtp_use_tls')"
|
|
:disabled="saving"
|
|
>
|
|
<span class="toggle-slider"></span>
|
|
</button>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="settings-grid">
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>From Address</span>
|
|
<input
|
|
type="email"
|
|
v-model="settings.smtp_from_address"
|
|
placeholder="noreply@example.com"
|
|
@blur="saveSetting('smtp_from_address', settings.smtp_from_address)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>From Name</span>
|
|
<input
|
|
type="text"
|
|
v-model="settings.smtp_from_name"
|
|
placeholder="ShopDB"
|
|
@blur="saveSetting('smtp_from_name', settings.smtp_from_name)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-row full-width">
|
|
<label>
|
|
<span>Default Alert Recipients</span>
|
|
<input
|
|
type="text"
|
|
v-model="settings.alert_recipients"
|
|
placeholder="user1@example.com, user2@example.com"
|
|
@blur="saveSetting('alert_recipients', settings.alert_recipients)"
|
|
:disabled="saving"
|
|
>
|
|
<small class="input-hint">Comma-separated list of email addresses</small>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="status-indicator">
|
|
<span class="status-dot" :class="smtpStatus"></span>
|
|
<span>{{ smtpMessage }}</span>
|
|
</div>
|
|
|
|
<button
|
|
class="test-btn"
|
|
@click="testEmail"
|
|
:disabled="saving || testingEmail || !canTestEmail"
|
|
>
|
|
{{ testingEmail ? 'Sending...' : 'Send Test Email' }}
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="error" class="error-message">{{ error }}</div>
|
|
<div v-if="success" class="settings-success">{{ success }}</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { onMounted, ref, computed } from 'vue'
|
|
import { useSystemSettings } from '../../composables/systemSettings'
|
|
import { apiError } from '../../utils/apiError'
|
|
import { settingsApi } from '../../api'
|
|
|
|
const {
|
|
settings, saving, error, success,
|
|
loadSettings, saveSetting, toggleSetting,
|
|
} = useSystemSettings()
|
|
|
|
const testingEmail = ref(false)
|
|
|
|
// Per-page connection status (stays with the page, not the composable).
|
|
const smtpStatus = computed(() => {
|
|
if (!settings.smtp_enabled) return 'inactive'
|
|
if (!settings.smtp_host || !settings.smtp_from_address) return 'warning'
|
|
return 'pending'
|
|
})
|
|
|
|
const smtpMessage = computed(() => {
|
|
if (!settings.smtp_enabled) return 'Disabled'
|
|
if (!settings.smtp_host) return 'SMTP host not configured'
|
|
if (!settings.smtp_from_address) return 'From address not configured'
|
|
return 'Configured - use test button to verify'
|
|
})
|
|
|
|
const canTestEmail = computed(() => {
|
|
return settings.smtp_host && settings.smtp_from_address && settings.alert_recipients
|
|
})
|
|
|
|
async function testEmail() {
|
|
testingEmail.value = true
|
|
error.value = ''
|
|
success.value = ''
|
|
try {
|
|
// Send to the configured Alert Recipients (the endpoint splits the list).
|
|
const response = await settingsApi.testEmail(settings.alert_recipients)
|
|
const result = response.data?.data || {}
|
|
if (result.sent) {
|
|
success.value = `Test email sent to ${settings.alert_recipients}`
|
|
} else {
|
|
error.value = result.error
|
|
? `Test email failed: ${result.error}`
|
|
: (response.data?.message || 'Email is not configured')
|
|
}
|
|
} catch (e) {
|
|
error.value = apiError(e, 'Failed to send test email')
|
|
} finally {
|
|
testingEmail.value = false
|
|
setTimeout(() => { success.value = '' }, 4000)
|
|
}
|
|
}
|
|
|
|
onMounted(loadSettings)
|
|
</script>
|