feat(gitsync): webhook-triggad synk + helt manuellt läge
All checks were successful
build-and-push / build (push) Successful in 1m7s
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:
@@ -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 -->
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user