feat(docker-watcher): phase 12 - hardening

Blue-green zero-downtime deploys, promote flow validation.
Dual auth: local (bcrypt + JWT) and OAuth2/OIDC (any provider).
Auth middleware, login page, auth settings UI.
Structured logging (slog JSON), config export to YAML.
Graceful shutdown with deploy draining.
Multi-stage Dockerfile and production docker-compose.yml.
Swap phase order: Volumes & Env before UI Polish.
This commit is contained in:
2026-03-27 23:20:56 +03:00
parent 5558396bb7
commit 32de5b26a8
30 changed files with 2134 additions and 143 deletions
+128
View File
@@ -0,0 +1,128 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { onMount } from 'svelte';
let username = $state('');
let password = $state('');
let error = $state('');
let loading = $state(false);
onMount(() => {
// Check if we got a token from OIDC callback redirect.
const urlToken = $page.url.searchParams.get('token');
if (urlToken) {
localStorage.setItem('auth_token', urlToken);
goto('/');
}
// If already logged in, redirect to dashboard.
const existingToken = localStorage.getItem('auth_token');
if (existingToken) {
goto('/');
}
});
async function handleLogin() {
error = '';
loading = true;
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const envelope = await res.json();
if (!envelope.success) {
error = envelope.error ?? 'Login failed';
return;
}
localStorage.setItem('auth_token', envelope.data.token);
goto('/');
} catch (err: unknown) {
error = err instanceof Error ? err.message : 'Network error';
} finally {
loading = false;
}
}
function handleOIDCLogin() {
window.location.href = '/api/auth/oidc/login';
}
</script>
<div class="flex min-h-screen items-center justify-center bg-gray-50">
<div class="w-full max-w-sm">
<div class="rounded-lg border border-gray-200 bg-white p-8 shadow-sm">
<div class="mb-6 text-center">
<svg class="mx-auto h-10 w-10 text-indigo-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-2.25-1.313M21 7.5v2.25m0-2.25l-2.25 1.313M3 7.5l2.25-1.313M3 7.5l2.25 1.313M3 7.5v2.25m9 3l2.25-1.313M12 12.75l-2.25-1.313M12 12.75V15m0 6.75l2.25-1.313M12 21.75V19.5m0 2.25l-2.25-1.313m0-16.875L12 2.25l2.25 1.313M21 14.25v2.25l-2.25 1.313m-13.5 0L3 16.5v-2.25" />
</svg>
<h1 class="mt-3 text-xl font-bold text-gray-900">Docker Watcher</h1>
<p class="mt-1 text-sm text-gray-500">Sign in to your account</p>
</div>
{#if error}
<div class="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700">
{error}
</div>
{/if}
<form onsubmit={(e) => { e.preventDefault(); handleLogin(); }} class="space-y-4">
<div>
<label for="username" class="block text-sm font-medium text-gray-700">Username</label>
<input
id="username"
type="text"
bind:value={username}
required
autocomplete="username"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
/>
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
<input
id="password"
type="password"
bind:value={password}
required
autocomplete="current-password"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
/>
</div>
<button
type="submit"
disabled={loading}
class="w-full rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50"
>
{loading ? 'Signing in...' : 'Sign in'}
</button>
</form>
<div class="mt-4">
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-200"></div>
</div>
<div class="relative flex justify-center text-xs">
<span class="bg-white px-2 text-gray-400">or</span>
</div>
</div>
<button
onclick={handleOIDCLogin}
class="mt-4 w-full rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Sign in with SSO (OIDC)
</button>
</div>
</div>
</div>
</div>