WP-02: encrypted settings, SMTP mail, job runner and cron scheduler
AES-256-GCM secret storage keyed by MASTER_KEY, SMTP settings with test mail (lettre), persisted job runs with log and status, JobRunner with per-kind concurrency guard, 6-field cron schedules with defaults, scheduler loop. Settings and Jobs pages in the UI. FAKE_HOST mode for dev machines. Tests: 30 application, 8 infrastructure, 23 API, 16 Vitest, 6 Playwright. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@ -9,6 +9,7 @@ const nav = [
|
||||
{ to: '/updates', label: 'Updates' },
|
||||
{ to: '/vulnerabilities', label: 'Vulnerabilities' },
|
||||
{ to: '/backups', label: 'Backups' },
|
||||
{ to: '/jobs', label: 'Jobs' },
|
||||
{ to: '/users', label: 'Users', admin: true },
|
||||
{ to: '/settings', label: 'Settings' },
|
||||
]
|
||||
|
||||
104
frontend/src/components/SmtpForm.vue
Normal file
104
frontend/src/components/SmtpForm.vue
Normal file
@ -0,0 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { SmtpPayload, SmtpSecurity, SmtpView } from '../api/types'
|
||||
|
||||
const props = defineProps<{ current?: SmtpView | null; busy?: boolean }>()
|
||||
const emit = defineEmits<{ submit: [payload: SmtpPayload] }>()
|
||||
|
||||
const host = ref(props.current?.host ?? '')
|
||||
const port = ref(props.current?.port ?? 587)
|
||||
const security = ref<SmtpSecurity>(props.current?.security ?? 'starttls')
|
||||
const username = ref(props.current?.username ?? '')
|
||||
const password = ref('')
|
||||
const from = ref(props.current?.from ?? '')
|
||||
const notifyTo = ref(props.current?.notify_to.join(', ') ?? '')
|
||||
|
||||
function submit() {
|
||||
emit('submit', {
|
||||
host: host.value,
|
||||
port: Number(port.value),
|
||||
security: security.value,
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
from: from.value,
|
||||
notify_to: notifyTo.value
|
||||
.split(/[,;\s]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
})
|
||||
}
|
||||
const input = 'mt-1 w-full rounded-md border border-gray-300 px-3 py-2'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="grid max-w-xl gap-4" @submit.prevent="submit">
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="col-span-2">
|
||||
<label for="smtp-host" class="block text-sm font-medium">SMTP host</label>
|
||||
<input id="smtp-host" v-model="host" name="host" required :class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="smtp-port" class="block text-sm font-medium">Port</label>
|
||||
<input
|
||||
id="smtp-port"
|
||||
v-model="port"
|
||||
name="port"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
required
|
||||
:class="input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="smtp-security" class="block text-sm font-medium">Security</label>
|
||||
<select id="smtp-security" v-model="security" name="security" :class="input">
|
||||
<option value="starttls">STARTTLS</option>
|
||||
<option value="tls">TLS</option>
|
||||
<option value="none">None</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="smtp-user" class="block text-sm font-medium">Username</label>
|
||||
<input id="smtp-user" v-model="username" name="username" :class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="smtp-pass" class="block text-sm font-medium">Password</label>
|
||||
<input
|
||||
id="smtp-pass"
|
||||
v-model="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:class="input"
|
||||
/>
|
||||
<p v-if="current?.password_set" class="mt-1 text-xs text-gray-500">
|
||||
A password is stored, leave empty to keep it.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="smtp-from" class="block text-sm font-medium">From address</label>
|
||||
<input id="smtp-from" v-model="from" name="from" type="email" required :class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="smtp-to" class="block text-sm font-medium">Notification recipients</label>
|
||||
<input
|
||||
id="smtp-to"
|
||||
v-model="notifyTo"
|
||||
name="notify_to"
|
||||
placeholder="ops@example.com, dev@example.com"
|
||||
:class="input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="busy"
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
Save SMTP settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
Reference in New Issue
Block a user