add login with oskeychain

This commit is contained in:
Christoph Brandau
2026-06-29 19:13:41 +02:00
parent 8d56c3f39f
commit 4aa9a537f0
11 changed files with 1234 additions and 40 deletions
+115 -25
View File
@@ -32,6 +32,10 @@
openRepository,
pull,
push,
getRemoteUrl,
credLoad,
credSave,
credDelete,
readConflict,
resolveConflict,
resolveConflictSide,
@@ -57,8 +61,16 @@
GitSearchHit,
GitStatus,
PreparedResolution,
StoredCredential,
} from "./lib/types";
import {
orgKeyFromUrl,
isCredentialExpired,
isAuthError,
stripAuthPrefix,
} from "./lib/credentials";
// ── State ──────────────────────────────────────────────────────────────────
let repoPath = "";
@@ -95,7 +107,7 @@
let credDialogOpen = false;
let credDialogAction: "push" | "pull" | null = null;
let credDialogError = "";
let sessionCredentials: { username: string; password: string } | null = null;
let credDialogKey: string | null = null;
let lastStatusFingerprint = "";
const AUTO_REFRESH_INTERVAL = 4000;
let autoRefreshTimer: ReturnType<typeof setInterval> | undefined;
@@ -306,14 +318,69 @@
});
}
function openCredentialDialog(action: "push" | "pull") {
// Resolve the keychain key (host/org) for the active repo's remote.
async function currentCredKey(): Promise<string | null> {
if (!activeRepoPath) return null;
try {
const url = await getRemoteUrl(activeRepoPath);
return url ? orgKeyFromUrl(url) : null;
} catch {
return null;
}
}
async function loadStoredCredential(key: string | null): Promise<StoredCredential | null> {
if (!key) return null;
try {
return await credLoad(key);
} catch {
return null;
}
}
async function openCredentialDialog(action: "push" | "pull", key?: string | null) {
if (!activeRepoPath) return;
credDialogError = "";
credDialogAction = action;
credDialogKey = key === undefined ? await currentCredKey() : key;
credDialogOpen = true;
}
async function doActualPull(username: string, password: string) {
// Post-process a pull/push result: surface errors, and on rejected/expired
// credentials drop the stored entry and re-open the login dialog.
function handleRemoteResult(action: "push" | "pull", key: string | null, fromStore: boolean) {
if (!errorMessage) {
credDialogOpen = false;
credDialogAction = null;
return;
}
const auth = isAuthError(errorMessage);
const message = stripAuthPrefix(errorMessage);
errorMessage = "";
if (fromStore) {
if (auth) {
if (key) void credDelete(key).catch(() => {});
credDialogError =
"Zugangsdaten wurden abgelehnt oder sind abgelaufen. Bitte erneut anmelden.";
credDialogAction = action;
credDialogKey = key;
credDialogOpen = true;
} else {
// Non-auth failure (e.g. network) keep the stored credential, show it inline.
errorMessage = message;
}
} else {
credDialogError = message || "Anmeldung fehlgeschlagen.";
}
}
async function doActualPull(
username: string,
password: string,
key: string | null,
fromStore: boolean,
) {
errorMessage = "";
await runOperation("Pulling", async () => {
applyStatus(await pull(activeRepoPath, username, password));
@@ -322,11 +389,15 @@
await refreshExplorerFiles(activeRepoPath);
await refreshFileHistory(activeRepoPath);
});
if (errorMessage) { credDialogError = errorMessage; errorMessage = ""; }
else { credDialogOpen = false; credDialogAction = null; }
handleRemoteResult("pull", key, fromStore);
}
async function doActualPush(username: string, password: string) {
async function doActualPush(
username: string,
password: string,
key: string | null,
fromStore: boolean,
) {
errorMessage = "";
await runOperation("Pushing", async () => {
applyStatus(await push(activeRepoPath, username, password));
@@ -334,32 +405,51 @@
await refreshCommitHistory(activeRepoPath);
await refreshFileHistory(activeRepoPath);
});
if (errorMessage) { credDialogError = errorMessage; errorMessage = ""; }
else { credDialogOpen = false; credDialogAction = null; }
handleRemoteResult("push", key, fromStore);
}
async function handleCredentialSubmit(username: string, password: string, save: boolean) {
if (credDialogAction === "pull") await doActualPull(username, password);
else if (credDialogAction === "push") await doActualPush(username, password);
if (!credDialogOpen && save) sessionCredentials = { username, password };
async function handleCredentialSubmit(
username: string,
password: string,
save: boolean,
expiresAt: string | null,
) {
const key = credDialogKey;
if (credDialogAction === "pull") await doActualPull(username, password, key, false);
else if (credDialogAction === "push") await doActualPush(username, password, key, false);
// Only persist once the operation actually succeeded (dialog has closed).
if (!credDialogOpen && save && key) {
try {
await credSave(key, username, password, expiresAt);
} catch (error) {
errorMessage = errorToMessage(error);
}
}
}
async function startRemoteAction(action: "push" | "pull") {
if (!activeRepoPath) return;
const key = await currentCredKey();
const stored = await loadStoredCredential(key);
if (stored && !isCredentialExpired(stored)) {
if (action === "pull") await doActualPull(stored.username, stored.password, key, true);
else await doActualPush(stored.username, stored.password, key, true);
return;
}
// Expired entry → clean it up before prompting again.
if (stored && key) await credDelete(key).catch(() => {});
await openCredentialDialog(action, key);
}
async function pullRepo() {
if (!activeRepoPath) return;
if (sessionCredentials) {
await doActualPull(sessionCredentials.username, sessionCredentials.password);
} else {
openCredentialDialog("pull");
}
await startRemoteAction("pull");
}
async function pushRepo() {
if (!activeRepoPath) return;
if (sessionCredentials) {
await doActualPush(sessionCredentials.username, sessionCredentials.password);
} else {
openCredentialDialog("push");
}
await startRemoteAction("push");
}
// ── File staging / restore ─────────────────────────────────────────────────
@@ -881,7 +971,7 @@
error={credDialogError}
{isBusy}
onSubmit={handleCredentialSubmit}
onCancel={() => { credDialogOpen = false; credDialogAction = null; credDialogError = ""; }}
onCancel={() => { credDialogOpen = false; credDialogAction = null; credDialogError = ""; credDialogKey = null; }}
/>
{/if}