Files
GitLite/src/lib/components/CredentialDialog.svelte
T
Christoph Brandau 96eb8c109c feat(git): add authenticated fetch and dual sync badge
This updates the Windows taskbar overlay badge to show ahead and
behind separately with distinct colors, and clears it when both are
zero. It also adds a new authenticated fetch command wired through
the Tauri backend and UI, including a silent background fetch to keep
ahead/behind (and the badge) accurate without user interaction.

- Add Windows dual badge rendering for ahead/behind
- Implement Tauri fetch command with auth error handling
- Add UI fetch action plus periodic background fetch updates
2026-07-03 14:29:29 +02:00

225 lines
6.9 KiB
Svelte

<script lang="ts">
import {
AlertCircle,
Download,
Eye,
EyeOff,
Key,
LoaderCircle,
Lock,
ShieldCheck,
Upload,
User,
X,
} from "@lucide/svelte";
interface Props {
action: "push" | "pull" | "fetch";
error: string;
isBusy: boolean;
onSubmit: (username: string, password: string, save: boolean, expiresAt: string | null) => void;
onCancel: () => void;
}
let {
action,
error = "",
isBusy = false,
onSubmit,
onCancel,
}: Props = $props();
type Mode = "credentials" | "token";
let mode = $state<Mode>("credentials");
let username = $state("");
let password = $state("");
let showPassword = $state(false);
let saveSession = $state(false);
let expiresAt = $state("");
let canSubmit = $derived(
!isBusy &&
password.trim().length > 0 &&
(mode === "token" || username.trim().length > 0),
);
let actionLabel = $derived(action === "push" ? "Push" : action === "fetch" ? "Fetch" : "Pull");
let actionTitle = $derived(
action === "push" ? "Authenticate push" : action === "fetch" ? "Authenticate fetch" : "Authenticate pull",
);
let actionHint = $derived(action === "push"
? "The remote needs write access. Use a password or a token with the appropriate repository permissions."
: "The remote needs access to the repository. Use your Git credentials or a personal access token.");
function handleSubmit(e: SubmitEvent) {
e.preventDefault();
if (!canSubmit) return;
onSubmit(
mode === "token" ? "oauth2" : username,
password,
saveSession,
saveSession && expiresAt ? expiresAt : null,
);
}
</script>
<div
class="dialog-backdrop"
role="presentation"
onclick={(e) => { if (e.target === e.currentTarget) onCancel(); }}
>
<div class="cred-card" role="dialog" aria-modal="true" aria-label="Git credentials" tabindex="-1">
<div class="cred-hero">
<div class="cred-hero-top">
<div class="cred-hero-icon">
{#if action === "push"}
<Upload size={27} aria-hidden="true" />
{:else}
<Download size={27} aria-hidden="true" />
{/if}
</div>
<div class="cred-hero-text">
<p class="cred-hero-label">{actionLabel} Remote</p>
<h2 class="cred-hero-title">{actionTitle}</h2>
</div>
<button class="cred-close" type="button" onclick={onCancel} title="Cancel" aria-label="Cancel">
<X size={16} aria-hidden="true" />
</button>
</div>
<p class="cred-hero-copy">{actionHint}</p>
<div class="cred-security-note">
<ShieldCheck size={14} aria-hidden="true" />
<span>When saved, the token is stored encrypted in the operating system's keychain — never in plain text.</span>
</div>
</div>
<form class="cred-body" onsubmit={handleSubmit}>
<div class="cred-segment" role="group" aria-label="Authentication method">
<button
type="button"
class="cred-seg-btn"
class:active={mode === "credentials"}
onclick={() => { mode = "credentials"; }}
aria-pressed={mode === "credentials"}
>
<User size={13} aria-hidden="true" />
Username + password
</button>
<button
type="button"
class="cred-seg-btn"
class:active={mode === "token"}
onclick={() => { mode = "token"; }}
aria-pressed={mode === "token"}
>
<Key size={13} aria-hidden="true" />
Token
</button>
</div>
<div class="cred-fields">
{#if mode === "credentials"}
<div class="cred-field">
<label class="cred-field-label" for="cred-username">Username</label>
<div class="cred-input">
<User size={15} class="cred-field-icon" aria-hidden="true" />
<input
id="cred-username"
type="text"
bind:value={username}
placeholder="e.g. my-github-username"
autocomplete="username"
disabled={isBusy}
/>
</div>
</div>
{/if}
<div class="cred-field">
<label class="cred-field-label" for="cred-password">
{mode === "token" ? "Token" : "Password"}
</label>
<div class="cred-input">
<Lock size={15} class="cred-field-icon" aria-hidden="true" />
<input
id="cred-password"
type={showPassword ? "text" : "password"}
bind:value={password}
placeholder={mode === "token"
? "ghp_... or another access token"
: "Password or personal access token"}
autocomplete="current-password"
disabled={isBusy}
/>
<button
type="button"
class="cred-reveal"
onclick={() => { showPassword = !showPassword; }}
tabindex="-1"
aria-label={showPassword ? "Hide" : "Show"}
>
{#if showPassword}
<EyeOff size={14} aria-hidden="true" />
{:else}
<Eye size={14} aria-hidden="true" />
{/if}
</button>
</div>
</div>
</div>
{#if mode === "token"}
<div class="cred-token-hint">
<Key size={13} aria-hidden="true" />
<span>Username is automatically set to <code>oauth2</code>. This works with GitHub, GitLab, and Bitbucket.</span>
</div>
{/if}
{#if error}
<div class="cred-error" role="alert">
<AlertCircle size={14} aria-hidden="true" />
<span>{error}</span>
</div>
{/if}
{#if saveSession}
<div class="cred-expiry">
<label class="cred-field-label" for="cred-expiry">Expiration date (optional)</label>
<input
id="cred-expiry"
type="date"
bind:value={expiresAt}
disabled={isBusy}
/>
<span class="cred-expiry-hint">After this date you'll automatically be asked to log in again.</span>
</div>
{/if}
<div class="cred-footer">
<label class="cred-save">
<input type="checkbox" bind:checked={saveSession} disabled={isBusy} />
<span>Save in keychain</span>
</label>
<div class="cred-btns">
<button type="button" class="cred-cancel" onclick={onCancel} disabled={isBusy}>
Cancel
</button>
<button class="cred-submit" type="submit" disabled={!canSubmit}>
{#if isBusy}
<LoaderCircle class="spin" size={15} aria-hidden="true" />
{:else if action === "push"}
<Upload size={15} aria-hidden="true" />
{:else}
<Download size={15} aria-hidden="true" />
{/if}
{actionLabel}
</button>
</div>
</div>
</form>
</div>
</div>