Files
GitLite/src/lib/components/CredentialDialog.svelte
T
Christoph Brandau 899f024a4f feat(ui): default credential dialog to save session
The credential dialog now starts with session saving enabled by
default, reducing extra user interaction for the common case. This
improves usability while keeping the option available to change.

- Set saveSession default to true in CredentialDialog
2026-07-06 21:06:58 +02:00

232 lines
7.1 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" | "clone";
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(true);
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" : action === "clone" ? "Clone" : "Pull");
let actionTitle = $derived(
action === "push"
? "Authenticate push"
: action === "fetch"
? "Authenticate fetch"
: action === "clone"
? "Authenticate clone"
: "Authenticate pull",
);
let actionHint = $derived(action === "push"
? "The remote needs write access. Use a password or a token with the appropriate repository permissions."
: action === "clone"
? "The repository needs access before it can be cloned. Use your Git credentials or a personal access token."
: "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"
>
<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>