feat(auth): add PIN session persistence, vault sync, and pre-fill login credentials
This commit is contained in:
@@ -190,6 +190,25 @@ class Plugin:
|
|||||||
|
|
||||||
# ===== Vault Operations =====
|
# ===== Vault Operations =====
|
||||||
|
|
||||||
|
async def sync_vault(self) -> dict:
|
||||||
|
"""Sync vault from server and re-decrypt."""
|
||||||
|
try:
|
||||||
|
if not self.client or not self._session_active:
|
||||||
|
return {"success": False, "error": "No active session"}
|
||||||
|
|
||||||
|
if not self._enc_key or not self._mac_key:
|
||||||
|
return {"success": False, "error": "Encryption keys not available"}
|
||||||
|
|
||||||
|
sync_data = await self.client.sync_vault()
|
||||||
|
self._vault_data = sync_data
|
||||||
|
self._decrypted_vault = self.client.decrypt_vault(
|
||||||
|
sync_data, self._enc_key, self._mac_key
|
||||||
|
)
|
||||||
|
return {"success": True, "vault_size": len(self._decrypted_vault.get("ciphers", []))}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
async def get_vault_items(self) -> dict:
|
async def get_vault_items(self) -> dict:
|
||||||
"""Get decrypted vault items."""
|
"""Get decrypted vault items."""
|
||||||
if not self._decrypted_vault:
|
if not self._decrypted_vault:
|
||||||
@@ -306,6 +325,21 @@ class Plugin:
|
|||||||
settings["pin_kdf_iterations"] = 200000
|
settings["pin_kdf_iterations"] = 200000
|
||||||
settings["pin_envelope"] = envelope.hex()
|
settings["pin_envelope"] = envelope.hex()
|
||||||
settings["pin_envelope_iv"] = iv.hex()
|
settings["pin_envelope_iv"] = iv.hex()
|
||||||
|
|
||||||
|
# Persist tokens encrypted with PIN for session restore across restarts
|
||||||
|
if self.client and self.client.access_token:
|
||||||
|
import json as _json
|
||||||
|
token_data = _json.dumps({
|
||||||
|
"access_token": self.client.access_token,
|
||||||
|
"refresh_token": self.client.refresh_token or "",
|
||||||
|
"user_id": self.client.user_id or "",
|
||||||
|
}).encode("utf-8")
|
||||||
|
token_envelope, token_iv = self.pin_crypto.encrypt_user_key_for_pin(
|
||||||
|
token_data, pin_key
|
||||||
|
)
|
||||||
|
settings["pin_token_envelope"] = token_envelope.hex()
|
||||||
|
settings["pin_token_envelope_iv"] = token_iv.hex()
|
||||||
|
|
||||||
self._save_settings(settings)
|
self._save_settings(settings)
|
||||||
|
|
||||||
self._pin_attempts = 0
|
self._pin_attempts = 0
|
||||||
@@ -350,8 +384,26 @@ class Plugin:
|
|||||||
else:
|
else:
|
||||||
raise ValueError("Invalid decrypted user key length")
|
raise ValueError("Invalid decrypted user key length")
|
||||||
|
|
||||||
|
# Restore session from persisted tokens if no active client (restart scenario)
|
||||||
|
if not self.client:
|
||||||
|
token_envelope_hex = settings.get("pin_token_envelope", "")
|
||||||
|
if token_envelope_hex:
|
||||||
|
import json as _json
|
||||||
|
token_envelope = bytes.fromhex(token_envelope_hex)
|
||||||
|
token_data_bytes = self.pin_crypto.decrypt_user_key_with_pin(
|
||||||
|
token_envelope, pin_key
|
||||||
|
)
|
||||||
|
token_data = _json.loads(token_data_bytes.decode("utf-8"))
|
||||||
|
|
||||||
|
server_url = settings.get("server_url", "https://api.bitwarden.com")
|
||||||
|
self.client = self.client_class(server_url)
|
||||||
|
self.client.access_token = token_data.get("access_token", "")
|
||||||
|
self.client.refresh_token = token_data.get("refresh_token", "")
|
||||||
|
self.client.user_id = token_data.get("user_id", "")
|
||||||
|
else:
|
||||||
|
return {"success": False, "error": "No active session and no persisted tokens"}
|
||||||
|
|
||||||
# Re-sync and decrypt vault
|
# Re-sync and decrypt vault
|
||||||
if self.client:
|
|
||||||
sync_data = await self.client.sync_vault()
|
sync_data = await self.client.sync_vault()
|
||||||
self._vault_data = sync_data
|
self._vault_data = sync_data
|
||||||
self._decrypted_vault = self.client.decrypt_vault(
|
self._decrypted_vault = self.client.decrypt_vault(
|
||||||
@@ -361,8 +413,6 @@ class Plugin:
|
|||||||
self._pin_attempts = 0
|
self._pin_attempts = 0
|
||||||
return {"success": True, "vault_size": len(self._decrypted_vault.get("ciphers", []))}
|
return {"success": True, "vault_size": len(self._decrypted_vault.get("ciphers", []))}
|
||||||
|
|
||||||
return {"success": False, "error": "No active session"}
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._pin_attempts += 1
|
self._pin_attempts += 1
|
||||||
remaining = self._MAX_PIN_ATTEMPTS - self._pin_attempts
|
remaining = self._MAX_PIN_ATTEMPTS - self._pin_attempts
|
||||||
@@ -380,6 +430,8 @@ class Plugin:
|
|||||||
settings.pop("pin_kdf_iterations", None)
|
settings.pop("pin_kdf_iterations", None)
|
||||||
settings.pop("pin_envelope", None)
|
settings.pop("pin_envelope", None)
|
||||||
settings.pop("pin_envelope_iv", None)
|
settings.pop("pin_envelope_iv", None)
|
||||||
|
settings.pop("pin_token_envelope", None)
|
||||||
|
settings.pop("pin_token_envelope_iv", None)
|
||||||
self._save_settings(settings)
|
self._save_settings(settings)
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|
||||||
@@ -388,6 +440,16 @@ class Plugin:
|
|||||||
settings = self._load_settings()
|
settings = self._load_settings()
|
||||||
return {"enabled": settings.get("pin_enabled", False)}
|
return {"enabled": settings.get("pin_enabled", False)}
|
||||||
|
|
||||||
|
async def get_saved_credentials(self) -> dict:
|
||||||
|
"""Get saved server URL and email for pre-filling login form."""
|
||||||
|
settings = self._load_settings()
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"server_url": settings.get("server_url", "https://api.bitwarden.com"),
|
||||||
|
"email": settings.get("email", ""),
|
||||||
|
"pin_enabled": settings.get("pin_enabled", False),
|
||||||
|
}
|
||||||
|
|
||||||
# ===== Vault Lock =====
|
# ===== Vault Lock =====
|
||||||
|
|
||||||
async def lock_vault(self) -> dict:
|
async def lock_vault(self) -> dict:
|
||||||
@@ -407,6 +469,11 @@ class Plugin:
|
|||||||
await self.lock_vault()
|
await self.lock_vault()
|
||||||
self._master_key = None
|
self._master_key = None
|
||||||
self.client = None
|
self.client = None
|
||||||
|
# Clear persisted tokens from settings
|
||||||
|
settings = self._load_settings()
|
||||||
|
settings.pop("pin_token_envelope", None)
|
||||||
|
settings.pop("pin_token_envelope_iv", None)
|
||||||
|
self._save_settings(settings)
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|
||||||
# ===== Settings =====
|
# ===== Settings =====
|
||||||
|
|||||||
@@ -124,6 +124,10 @@ class VaultApi {
|
|||||||
return this.call("get_vault_items");
|
return this.call("get_vault_items");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async syncVault(): Promise<{ success: boolean; vault_size?: number; error?: string }> {
|
||||||
|
return this.call("sync_vault");
|
||||||
|
}
|
||||||
|
|
||||||
async searchVault(query: string): Promise<{ success: boolean; results: VaultItem[] }> {
|
async searchVault(query: string): Promise<{ success: boolean; results: VaultItem[] }> {
|
||||||
return this.call("search_vault", query);
|
return this.call("search_vault", query);
|
||||||
}
|
}
|
||||||
@@ -188,6 +192,15 @@ class VaultApi {
|
|||||||
async updateSettings(settings: any): Promise<{ success: boolean }> {
|
async updateSettings(settings: any): Promise<{ success: boolean }> {
|
||||||
return this.call("update_settings", settings);
|
return this.call("update_settings", settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSavedCredentials(): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
server_url: string;
|
||||||
|
email: string;
|
||||||
|
pin_enabled: boolean;
|
||||||
|
}> {
|
||||||
|
return this.call("get_saved_credentials");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const vaultApi = new VaultApi();
|
export const vaultApi = new VaultApi();
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ interface LoginViewProps {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
twoFactorRequired?: boolean;
|
twoFactorRequired?: boolean;
|
||||||
needsMasterPassword?: boolean;
|
needsMasterPassword?: boolean;
|
||||||
|
defaultServerUrl?: string;
|
||||||
|
defaultEmail?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LoginView({
|
export function LoginView({
|
||||||
@@ -33,10 +35,12 @@ export function LoginView({
|
|||||||
error,
|
error,
|
||||||
twoFactorRequired,
|
twoFactorRequired,
|
||||||
needsMasterPassword,
|
needsMasterPassword,
|
||||||
|
defaultServerUrl,
|
||||||
|
defaultEmail,
|
||||||
}: LoginViewProps) {
|
}: LoginViewProps) {
|
||||||
const [authMethod, setAuthMethod] = useState<"password" | "apikey">("password");
|
const [authMethod, setAuthMethod] = useState<"password" | "apikey">("password");
|
||||||
const [serverUrl, setServerUrl] = useState("https://api.bitwarden.com");
|
const [serverUrl, setServerUrl] = useState(defaultServerUrl || "https://api.bitwarden.com");
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState(defaultEmail || "");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [twoFactorCode, setTwoFactorCode] = useState("");
|
const [twoFactorCode, setTwoFactorCode] = useState("");
|
||||||
const [clientId, setClientId] = useState("");
|
const [clientId, setClientId] = useState("");
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import { PanelSection, TextField, ButtonItem, Spinner } from "@decky/ui";
|
|||||||
|
|
||||||
interface PinSetupViewProps {
|
interface PinSetupViewProps {
|
||||||
onSetupPin: (pin: string) => Promise<boolean>;
|
onSetupPin: (pin: string) => Promise<boolean>;
|
||||||
|
onBack: () => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PinSetupView({ onSetupPin, loading, error }: PinSetupViewProps) {
|
export function PinSetupView({ onSetupPin, onBack, loading, error }: PinSetupViewProps) {
|
||||||
const [pin, setPin] = useState("");
|
const [pin, setPin] = useState("");
|
||||||
const [confirmPin, setConfirmPin] = useState("");
|
const [confirmPin, setConfirmPin] = useState("");
|
||||||
const [localError, setLocalError] = useState<string | null>(null);
|
const [localError, setLocalError] = useState<string | null>(null);
|
||||||
@@ -29,6 +30,7 @@ export function PinSetupView({ onSetupPin, loading, error }: PinSetupViewProps)
|
|||||||
if (success) {
|
if (success) {
|
||||||
setPin("");
|
setPin("");
|
||||||
setConfirmPin("");
|
setConfirmPin("");
|
||||||
|
onBack();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,6 +61,14 @@ export function PinSetupView({ onSetupPin, loading, error }: PinSetupViewProps)
|
|||||||
{loading ? <Spinner width={16} height={16} /> : "Set Up PIN"}
|
{loading ? <Spinner width={16} height={16} /> : "Set Up PIN"}
|
||||||
</ButtonItem>
|
</ButtonItem>
|
||||||
|
|
||||||
|
<ButtonItem
|
||||||
|
layout="below"
|
||||||
|
onClick={onBack}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</ButtonItem>
|
||||||
|
|
||||||
{(localError || error) && (
|
{(localError || error) && (
|
||||||
<div style={{ color: "#ff4444", padding: "8px", fontSize: "12px" }}>
|
<div style={{ color: "#ff4444", padding: "8px", fontSize: "12px" }}>
|
||||||
{localError || error}
|
{localError || error}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export function PinUnlockView({
|
|||||||
</ButtonItem>
|
</ButtonItem>
|
||||||
|
|
||||||
<ButtonItem layout="below" onClick={onLogout} disabled={loading}>
|
<ButtonItem layout="below" onClick={onLogout} disabled={loading}>
|
||||||
Login with Master Password
|
Login as someone else
|
||||||
</ButtonItem>
|
</ButtonItem>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ interface VaultBrowserProps {
|
|||||||
onCopyTotp: (cipherId: string) => Promise<boolean>;
|
onCopyTotp: (cipherId: string) => Promise<boolean>;
|
||||||
onLock: () => void;
|
onLock: () => void;
|
||||||
onSetupPin: () => void;
|
onSetupPin: () => void;
|
||||||
|
onSync: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VaultBrowser({
|
export function VaultBrowser({
|
||||||
@@ -32,6 +33,7 @@ export function VaultBrowser({
|
|||||||
onCopyTotp,
|
onCopyTotp,
|
||||||
onLock,
|
onLock,
|
||||||
onSetupPin,
|
onSetupPin,
|
||||||
|
onSync,
|
||||||
}: VaultBrowserProps) {
|
}: VaultBrowserProps) {
|
||||||
const [selectedFolder, setSelectedFolder] = useState<string | null>(null);
|
const [selectedFolder, setSelectedFolder] = useState<string | null>(null);
|
||||||
const [localSearch, setLocalSearch] = useState(searchQuery);
|
const [localSearch, setLocalSearch] = useState(searchQuery);
|
||||||
@@ -63,6 +65,10 @@ export function VaultBrowser({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelSection title="Vault">
|
<PanelSection title="Vault">
|
||||||
|
<Focusable
|
||||||
|
onSecondaryButton={onSync}
|
||||||
|
onSecondaryActionDescription="Sync Vault"
|
||||||
|
>
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<TextField
|
<TextField
|
||||||
label="Search"
|
label="Search"
|
||||||
@@ -132,12 +138,16 @@ export function VaultBrowser({
|
|||||||
</Focusable>
|
</Focusable>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
|
<ButtonItem layout="below" onClick={onSync}>
|
||||||
|
Sync Vault
|
||||||
|
</ButtonItem>
|
||||||
<ButtonItem layout="below" onClick={onLock}>
|
<ButtonItem layout="below" onClick={onLock}>
|
||||||
Lock Vault
|
Lock Vault
|
||||||
</ButtonItem>
|
</ButtonItem>
|
||||||
<ButtonItem layout="below" onClick={onSetupPin}>
|
<ButtonItem layout="below" onClick={onSetupPin}>
|
||||||
Set Up PIN
|
Set Up PIN
|
||||||
</ButtonItem>
|
</ButtonItem>
|
||||||
|
</Focusable>
|
||||||
</PanelSection>
|
</PanelSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type VaultState =
|
|||||||
| "two_factor_required"
|
| "two_factor_required"
|
||||||
| "needs_master_password"
|
| "needs_master_password"
|
||||||
| "pin_only"
|
| "pin_only"
|
||||||
|
| "pin_setup"
|
||||||
| "loading"
|
| "loading"
|
||||||
| "unlocked";
|
| "unlocked";
|
||||||
|
|
||||||
@@ -22,6 +23,10 @@ export function useVault() {
|
|||||||
cipherId: string;
|
cipherId: string;
|
||||||
expiresAt: number;
|
expiresAt: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [savedCredentials, setSavedCredentials] = useState<{
|
||||||
|
serverUrl: string;
|
||||||
|
email: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
// Check initial status
|
// Check initial status
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -47,6 +52,19 @@ export function useVault() {
|
|||||||
} else {
|
} else {
|
||||||
setState("logged_out");
|
setState("logged_out");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch saved credentials for pre-filling login form
|
||||||
|
try {
|
||||||
|
const creds = await vaultApi.getSavedCredentials();
|
||||||
|
if (creds.success && (creds.email || creds.server_url)) {
|
||||||
|
setSavedCredentials({
|
||||||
|
serverUrl: creds.server_url,
|
||||||
|
email: creds.email,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors fetching saved credentials
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState("logged_out");
|
setState("logged_out");
|
||||||
}
|
}
|
||||||
@@ -336,6 +354,27 @@ export function useVault() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const enterPinSetup = useCallback(() => {
|
||||||
|
setState("pin_setup");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const syncVault = useCallback(async () => {
|
||||||
|
setState("loading");
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const syncResult = await vaultApi.syncVault();
|
||||||
|
if (syncResult.success) {
|
||||||
|
await loadVault();
|
||||||
|
} else {
|
||||||
|
setError(syncResult.error || "Sync failed");
|
||||||
|
setState("unlocked");
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.message || "Sync failed");
|
||||||
|
setState("unlocked");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
vaultData,
|
vaultData,
|
||||||
@@ -345,6 +384,7 @@ export function useVault() {
|
|||||||
searchQuery,
|
searchQuery,
|
||||||
searchResults,
|
searchResults,
|
||||||
copyStatus,
|
copyStatus,
|
||||||
|
savedCredentials,
|
||||||
setSelectedFolder,
|
setSelectedFolder,
|
||||||
loginPassword,
|
loginPassword,
|
||||||
loginApiKey,
|
loginApiKey,
|
||||||
@@ -358,6 +398,8 @@ export function useVault() {
|
|||||||
logout,
|
logout,
|
||||||
setupPin,
|
setupPin,
|
||||||
removePin,
|
removePin,
|
||||||
|
enterPinSetup,
|
||||||
|
syncVault,
|
||||||
clearError: () => setError(null),
|
clearError: () => setError(null),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-13
@@ -6,11 +6,10 @@ import { useVault } from "./hooks/useVault";
|
|||||||
import { LoginView } from "./components/LoginView";
|
import { LoginView } from "./components/LoginView";
|
||||||
import { PinUnlockView } from "./components/PinUnlockView";
|
import { PinUnlockView } from "./components/PinUnlockView";
|
||||||
import { VaultBrowser } from "./components/VaultBrowser";
|
import { VaultBrowser } from "./components/VaultBrowser";
|
||||||
import { PinSetupModal } from "./components/PinSetupModal";
|
import { PinSetupView } from "./components/PinSetupView";
|
||||||
|
|
||||||
function VaultwardenPlugin() {
|
function VaultwardenPlugin() {
|
||||||
const vault = useVault();
|
const vault = useVault();
|
||||||
const [showPinSetup, setShowPinSetup] = useState(false);
|
|
||||||
|
|
||||||
// Handle keyboard input for PIN
|
// Handle keyboard input for PIN
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -44,6 +43,8 @@ function VaultwardenPlugin() {
|
|||||||
onApiKeyLogin={vault.loginApiKey}
|
onApiKeyLogin={vault.loginApiKey}
|
||||||
loading={false}
|
loading={false}
|
||||||
error={vault.error}
|
error={vault.error}
|
||||||
|
defaultServerUrl={vault.savedCredentials?.serverUrl}
|
||||||
|
defaultEmail={vault.savedCredentials?.email}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -56,6 +57,8 @@ function VaultwardenPlugin() {
|
|||||||
loading={false}
|
loading={false}
|
||||||
error={vault.error}
|
error={vault.error}
|
||||||
twoFactorRequired
|
twoFactorRequired
|
||||||
|
defaultServerUrl={vault.savedCredentials?.serverUrl}
|
||||||
|
defaultEmail={vault.savedCredentials?.email}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -81,9 +84,19 @@ function VaultwardenPlugin() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (vault.state === "pin_setup") {
|
||||||
|
return (
|
||||||
|
<PinSetupView
|
||||||
|
onSetupPin={vault.setupPin}
|
||||||
|
onBack={() => vault.lockVault()}
|
||||||
|
loading={false}
|
||||||
|
error={vault.error}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (vault.state === "unlocked" && vault.vaultData) {
|
if (vault.state === "unlocked" && vault.vaultData) {
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<VaultBrowser
|
<VaultBrowser
|
||||||
vaultData={vault.vaultData}
|
vaultData={vault.vaultData}
|
||||||
searchQuery={vault.searchQuery}
|
searchQuery={vault.searchQuery}
|
||||||
@@ -94,17 +107,9 @@ function VaultwardenPlugin() {
|
|||||||
onCopyUsername={vault.copyUsername}
|
onCopyUsername={vault.copyUsername}
|
||||||
onCopyTotp={vault.copyTotp}
|
onCopyTotp={vault.copyTotp}
|
||||||
onLock={vault.lockVault}
|
onLock={vault.lockVault}
|
||||||
onSetupPin={() => setShowPinSetup(true)}
|
onSetupPin={vault.enterPinSetup}
|
||||||
|
onSync={vault.syncVault}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PinSetupModal
|
|
||||||
isOpen={showPinSetup}
|
|
||||||
onClose={() => setShowPinSetup(false)}
|
|
||||||
onSetupPin={vault.setupPin}
|
|
||||||
loading={false}
|
|
||||||
error={vault.error}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +119,8 @@ function VaultwardenPlugin() {
|
|||||||
onApiKeyLogin={vault.loginApiKey}
|
onApiKeyLogin={vault.loginApiKey}
|
||||||
loading={false}
|
loading={false}
|
||||||
error={vault.error}
|
error={vault.error}
|
||||||
|
defaultServerUrl={vault.savedCredentials?.serverUrl}
|
||||||
|
defaultEmail={vault.savedCredentials?.email}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user