feat(gitsync): webhook-triggad synk + helt manuellt läge
All checks were successful
build-and-push / build (push) Successful in 1m7s

- POST /api/git-hook: HMAC-SHA256-verifierad (X-Gitea-Signature),
  reagerar bara på pushar till live-grenen, svarar 202 och synkar async
- webhook-hemlighet genereras server-side (headless via config.json),
  roteras med gitRegenerateWebhookSecret
- auto_sync_minutes=0 + webhook av ⇒ ingen automatisk hämtning alls;
  webhook på ⇒ catch-up-synk vid uppstart (missade event)
- admin-UI: webhook-toggle, target-URL + secret med copy/rotate

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 23:52:01 +02:00
parent 9854d85237
commit 5819a6bdce
7 changed files with 236 additions and 7 deletions

View File

@@ -10,6 +10,7 @@ const emit = defineEmits<{
interface GitSettings {
enabled: boolean; remoteUrl: string; liveBranch: string
readOnly: boolean; autoSyncMinutes: number; publicKey: string; keySet: boolean
webhookEnabled: boolean; webhookSecret: string; webhookUrl: string
}
interface GitStatus {
enabled: boolean; currentBranch: string; liveBranch: string; readOnly: boolean
@@ -21,6 +22,7 @@ interface GitStatus {
const settings = ref<GitSettings>({
enabled: false, remoteUrl: '', liveBranch: 'main',
readOnly: false, autoSyncMinutes: 0, publicKey: '', keySet: false,
webhookEnabled: false, webhookSecret: '', webhookUrl: '',
})
const status = ref<GitStatus | null>(null)
const branches = ref<{ current: string; local: string[]; remote: string[] }>({ current: '', local: [], remote: [] })
@@ -29,6 +31,7 @@ const switchTarget = ref('')
const newBranchName = ref('')
const confirmReset = ref(false)
const confirmRegen = ref(false)
const confirmRegenHook = ref(false)
const showMergeDialog = ref(false)
const suggestReset = ref(false)
let poll: ReturnType<typeof setInterval> | undefined
@@ -41,7 +44,7 @@ const allBranches = computed(() => {
async function loadSettings() {
const d = await gql<{ gitSyncSettings: GitSettings }>(
`{ gitSyncSettings { enabled remoteUrl liveBranch readOnly autoSyncMinutes publicKey keySet } }`)
`{ gitSyncSettings { enabled remoteUrl liveBranch readOnly autoSyncMinutes publicKey keySet webhookEnabled webhookSecret webhookUrl } }`)
settings.value = d.gitSyncSettings
}
async function loadStatus() {
@@ -75,6 +78,7 @@ async function saveSettings() {
liveBranch: settings.value.liveBranch,
readOnly: settings.value.readOnly,
autoSyncMinutes: Number(settings.value.autoSyncMinutes) || 0,
webhookEnabled: settings.value.webhookEnabled,
} },
)
emit('status', 'Git sync settings saved.')
@@ -100,6 +104,22 @@ async function regenerateKey() {
} catch (err: any) { emit('status', err.message, true) } finally { busy.value = false }
}
async function copyWebhookSecret() {
await navigator.clipboard.writeText(settings.value.webhookSecret)
emit('status', 'Webhook secret copied to clipboard.')
}
async function regenerateWebhookSecret() {
confirmRegenHook.value = false
busy.value = true
try {
const d = await gql<{ gitRegenerateWebhookSecret: { secret: string } }>(
`mutation { gitRegenerateWebhookSecret { secret } }`)
settings.value.webhookSecret = d.gitRegenerateWebhookSecret.secret
emit('status', 'New webhook secret generated — update the webhook on your git host.')
} catch (err: any) { emit('status', err.message, true) } finally { busy.value = false }
}
async function pull() {
busy.value = true
suggestReset.value = false
@@ -223,8 +243,9 @@ onUnmounted(() => { if (poll) clearInterval(poll) })
<p class="text-xs text-slate-500 mt-1">The branch auto-sync keeps in sync</p>
</div>
<div>
<label class="label">Auto-sync interval (minutes, 0 = manual)</label>
<label class="label">Auto-sync interval (minutes)</label>
<input v-model.number="settings.autoSyncMinutes" type="number" min="0" class="input" />
<p class="text-xs text-slate-500 mt-1">0 = no polling. With the webhook off too, nothing is fetched automatically.</p>
</div>
<div class="sm:col-span-2">
<label class="flex items-center gap-2 cursor-pointer">
@@ -232,6 +253,37 @@ onUnmounted(() => { if (poll) clearInterval(poll) })
<span class="text-sm font-medium">Read-only (pull from remote, never push)</span>
</label>
</div>
<div class="sm:col-span-2">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="settings.webhookEnabled" class="rounded border-slate-300 text-accent-600 focus:ring-accent-500" />
<span class="text-sm font-medium">Webhook (sync immediately when the git host reports a push)</span>
</label>
</div>
</div>
<!-- Webhook details -->
<div v-if="settings.webhookEnabled && settings.webhookSecret" class="rounded-lg border border-slate-200 dark:border-slate-700 p-4 space-y-2">
<p class="text-sm text-slate-600 dark:text-slate-400">
Add a webhook on the git repository (Gitea: Settings Webhooks Add webhook Gitea)
with these values. Only pushes to the live branch trigger a sync.
</p>
<div class="text-sm flex flex-wrap items-center gap-2">
<span class="font-medium shrink-0">Target URL:</span>
<code class="font-mono text-xs bg-slate-100 dark:bg-slate-900/60 rounded px-2 py-1 break-all">{{ settings.webhookUrl || '<public URL not set>/api/git-hook' }}</code>
</div>
<div class="text-sm flex flex-wrap items-center gap-2">
<span class="font-medium shrink-0">Secret:</span>
<code class="font-mono text-xs bg-slate-100 dark:bg-slate-900/60 rounded px-2 py-1 break-all select-all">{{ settings.webhookSecret }}</code>
<button class="btn-secondary text-xs" @click="copyWebhookSecret">Copy</button>
<button class="btn-ghost text-xs" @click="confirmRegenHook = true">Rotate</button>
</div>
<div v-if="confirmRegenHook" class="rounded-lg border border-amber-300 dark:border-amber-700 bg-amber-50/50 dark:bg-amber-900/10 p-3 text-sm flex items-center justify-between gap-2">
<span>The old secret stops working immediately. Continue?</span>
<span class="flex gap-2">
<button class="btn-ghost text-xs" @click="confirmRegenHook = false">Cancel</button>
<button class="btn-primary text-xs" :disabled="busy" @click="regenerateWebhookSecret">Rotate secret</button>
</span>
</div>
</div>
<!-- Deploy key -->

View File

@@ -52,6 +52,7 @@ export default defineConfig({
proxy: {
'/graphql': { target: 'http://localhost:4000', changeOrigin: true },
'/media': { target: 'http://localhost:4000', changeOrigin: true },
'/api': { target: 'http://localhost:4000', changeOrigin: true },
},
},
})