WP-02: encrypted settings, SMTP mail, job runner and cron scheduler
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / ui (push) Has been cancelled

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:
Dennis Nemec
2026-09-02 22:16:56 +02:00
parent 6113af0a19
commit fa9ac9a6bc
31 changed files with 1632 additions and 58 deletions

View File

@ -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' },
]

View 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>