refactor(credentials): remove credential expiry support and API

The credential expiry feature has been removed from both the Rust
backend and the frontend. Stored credentials now include only
username and password.

- Remove expiresAt field from StoredCredential
- Simplify API by removing expiry param from credSave
- Drop expiry UI and expiry checks across the app
This commit is contained in:
Christoph Brandau
2026-08-13 20:45:43 +02:00
parent cc0f1c076f
commit 6434a9f10c
7 changed files with 13 additions and 73 deletions
+2 -14
View File
@@ -2432,8 +2432,6 @@ const CRED_SERVICE: &str = "tauri_git_lite";
pub struct StoredCredential { pub struct StoredCredential {
pub username: String, pub username: String,
pub password: String, pub password: String,
#[serde(default, rename = "expiresAt", skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
} }
fn cred_entry(key: &str) -> Result<keyring::Entry, String> { fn cred_entry(key: &str) -> Result<keyring::Entry, String> {
@@ -2616,19 +2614,9 @@ pub fn cred_load(key: String) -> Result<Option<StoredCredential>, String> {
} }
#[tauri::command(async)] #[tauri::command(async)]
pub fn cred_save( pub fn cred_save(key: String, username: String, password: String) -> Result<(), String> {
key: String,
username: String,
password: String,
expires_at: Option<String>,
) -> Result<(), String> {
let entry = cred_entry(&key)?; let entry = cred_entry(&key)?;
let expires_at = expires_at.filter(|value| !value.trim().is_empty()); let cred = StoredCredential { username, password };
let cred = StoredCredential {
username,
password,
expires_at,
};
let json = serde_json::to_string(&cred) let json = serde_json::to_string(&cred)
.map_err(|err| format!("Could not serialize credentials: {err}"))?; .map_err(|err| format!("Could not serialize credentials: {err}"))?;
entry entry
+6 -20
View File
@@ -180,7 +180,6 @@
import { import {
orgKeyFromUrl, orgKeyFromUrl,
isCredentialExpired,
isAuthError, isAuthError,
stripAuthPrefix, stripAuthPrefix,
summarizeGitError, summarizeGitError,
@@ -2219,11 +2218,10 @@
if (!username && !password) { if (!username && !password) {
const stored = await loadStoredCredential(credentialKey); const stored = await loadStoredCredential(credentialKey);
if (stored && !isCredentialExpired(stored)) { if (stored) {
await cloneRepo(remoteUrl, parentPath, directoryName, stored.username, stored.password, credentialKey, true); await cloneRepo(remoteUrl, parentPath, directoryName, stored.username, stored.password, credentialKey, true);
return; return;
} }
if (stored && credentialKey) await credDelete(credentialKey).catch(() => {});
} }
operation = "Cloning repository"; operation = "Cloning repository";
@@ -2921,7 +2919,7 @@
if (!activeRepoPath || isBusy) return; if (!activeRepoPath || isBusy) return;
const key = await currentCredKey(); const key = await currentCredKey();
const stored = await loadStoredCredential(key); const stored = await loadStoredCredential(key);
const credential = stored && !isCredentialExpired(stored) ? stored : null; const credential = stored ?? null;
await runOperation(`Pushing tag ${tag.name}`, async () => { await runOperation(`Pushing tag ${tag.name}`, async () => {
try { try {
@@ -3028,12 +3026,7 @@
async function storedCredentialForNoteRemote(remote: string, direction: "fetch" | "push") { async function storedCredentialForNoteRemote(remote: string, direction: "fetch" | "push") {
const config = commitNoteRemotes.find((item) => item.name === remote); const config = commitNoteRemotes.find((item) => item.name === remote);
const key = orgKeyFromUrl(direction === "push" ? (config?.push_url ?? "") : (config?.fetch_url ?? "")); const key = orgKeyFromUrl(direction === "push" ? (config?.push_url ?? "") : (config?.fetch_url ?? ""));
const stored = await loadStoredCredential(key); return loadStoredCredential(key);
if (stored && isCredentialExpired(stored)) {
if (key) await credDelete(key).catch(() => {});
return null;
}
return stored;
} }
function commitNoteRemoteError(error: unknown): string { function commitNoteRemoteError(error: unknown): string {
@@ -3271,12 +3264,7 @@
handleRemoteResult("push", key, fromStore); handleRemoteResult("push", key, fromStore);
} }
async function handleCredentialSubmit( async function handleCredentialSubmit(username: string, password: string, save: boolean) {
username: string,
password: string,
save: boolean,
expiresAt: string | null,
) {
const key = credDialogKey; const key = credDialogKey;
if (credDialogAction === "pull") await doActualPull(username, password, key, false); if (credDialogAction === "pull") await doActualPull(username, password, key, false);
else if (credDialogAction === "push") await doActualPush(username, password, key, false); else if (credDialogAction === "push") await doActualPush(username, password, key, false);
@@ -3296,7 +3284,7 @@
// Only persist once the operation actually succeeded (dialog has closed). // Only persist once the operation actually succeeded (dialog has closed).
if (!credDialogOpen && save && key) { if (!credDialogOpen && save && key) {
try { try {
await credSave(key, username, password, expiresAt); await credSave(key, username, password);
} catch (error) { } catch (error) {
errorMessage = errorToMessage(error); errorMessage = errorToMessage(error);
} }
@@ -3311,15 +3299,13 @@
const key = await currentCredKey(); const key = await currentCredKey();
const stored = await loadStoredCredential(key); const stored = await loadStoredCredential(key);
if (stored && !isCredentialExpired(stored)) { if (stored) {
if (action === "pull") await doActualPull(stored.username, stored.password, key, true); if (action === "pull") await doActualPull(stored.username, stored.password, key, true);
else if (action === "fetch") await doActualFetch(stored.username, stored.password, key, true); else if (action === "fetch") await doActualFetch(stored.username, stored.password, key, true);
else await doActualPush(stored.username, stored.password, key, true); else await doActualPush(stored.username, stored.password, key, true);
return; return;
} }
// Expired entry → clean it up before prompting again.
if (stored && key) await credDelete(key).catch(() => {});
await openCredentialDialog(action, key); await openCredentialDialog(action, key);
} }
+1 -1
View File
@@ -85,7 +85,7 @@
const key = CRED_KEYS[target]; const key = CRED_KEYS[target];
const trimmed = value.trim(); const trimmed = value.trim();
if (trimmed) { if (trimmed) {
await credSave(key, "api-key", trimmed, null); await credSave(key, "api-key", trimmed);
} else { } else {
await credDelete(key); await credDelete(key);
} }
+2 -21
View File
@@ -17,7 +17,7 @@
action: "push" | "pull" | "fetch" | "clone"; action: "push" | "pull" | "fetch" | "clone";
error: string; error: string;
isBusy: boolean; isBusy: boolean;
onSubmit: (username: string, password: string, save: boolean, expiresAt: string | null) => void; onSubmit: (username: string, password: string, save: boolean) => void;
onCancel: () => void; onCancel: () => void;
} }
@@ -36,7 +36,6 @@
let password = $state(""); let password = $state("");
let showPassword = $state(false); let showPassword = $state(false);
let saveSession = $state(true); let saveSession = $state(true);
let expiresAt = $state("");
let canSubmit = $derived( let canSubmit = $derived(
!isBusy && !isBusy &&
@@ -62,12 +61,7 @@
function handleSubmit(e: SubmitEvent) { function handleSubmit(e: SubmitEvent) {
e.preventDefault(); e.preventDefault();
if (!canSubmit) return; if (!canSubmit) return;
onSubmit( onSubmit(mode === "token" ? "oauth2" : username, password, saveSession);
mode === "token" ? "oauth2" : username,
password,
saveSession,
saveSession && expiresAt ? expiresAt : null,
);
} }
</script> </script>
@@ -191,19 +185,6 @@
</div> </div>
{/if} {/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"> <div class="cred-footer">
<label class="cred-save"> <label class="cred-save">
<input type="checkbox" bind:checked={saveSession} disabled={isBusy} /> <input type="checkbox" bind:checked={saveSession} disabled={isBusy} />
-9
View File
@@ -1,5 +1,3 @@
import type { StoredCredential } from "./types";
/** /**
* Derives a credential key from a remote URL, scoped to host + organisation * Derives a credential key from a remote URL, scoped to host + organisation
* the same granularity Azure DevOps / GitHub use. Examples: * the same granularity Azure DevOps / GitHub use. Examples:
@@ -37,13 +35,6 @@ export function orgKeyFromUrl(raw: string): string | null {
return org ? `${host}/${org}` : host; return org ? `${host}/${org}` : host;
} }
/** A stored credential is expired only if it carries a past expiry date. */
export function isCredentialExpired(cred: StoredCredential): boolean {
if (!cred.expiresAt) return false;
const time = new Date(cred.expiresAt).getTime();
return !Number.isNaN(time) && time < Date.now();
}
const AUTH_PREFIX = "AUTH_FAILED:"; const AUTH_PREFIX = "AUTH_FAILED:";
export function isAuthError(message: string): boolean { export function isAuthError(message: string): boolean {
+2 -7
View File
@@ -381,13 +381,8 @@ export function credLoad(key: string): Promise<StoredCredential | null> {
return invoke<StoredCredential | null>("cred_load", { key }); return invoke<StoredCredential | null>("cred_load", { key });
} }
export function credSave( export function credSave(key: string, username: string, password: string): Promise<void> {
key: string, return invoke<void>("cred_save", { key, username, password });
username: string,
password: string,
expiresAt: string | null,
): Promise<void> {
return invoke<void>("cred_save", { key, username, password, expiresAt });
} }
export function credDelete(key: string): Promise<void> { export function credDelete(key: string): Promise<void> {
-1
View File
@@ -310,5 +310,4 @@ export interface ReflogEntry {
export interface StoredCredential { export interface StoredCredential {
username: string; username: string;
password: string; password: string;
expiresAt?: string | null;
} }