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 =====
|
||||
|
||||
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:
|
||||
"""Get decrypted vault items."""
|
||||
if not self._decrypted_vault:
|
||||
@@ -295,9 +314,9 @@ class Plugin:
|
||||
pin, pin_salt, kdf_iterations=200000
|
||||
)
|
||||
|
||||
# Encrypt user key with PIN key
|
||||
# Encrypt user key with PIN key (store both enc_key + mac_key)
|
||||
envelope, iv = self.pin_crypto.encrypt_user_key_for_pin(
|
||||
self._enc_key, pin_key
|
||||
self._enc_key + self._mac_key, pin_key
|
||||
)
|
||||
|
||||
# Save PIN settings
|
||||
@@ -306,6 +325,21 @@ class Plugin:
|
||||
settings["pin_kdf_iterations"] = 200000
|
||||
settings["pin_envelope"] = envelope.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._pin_attempts = 0
|
||||
@@ -345,23 +379,48 @@ class Plugin:
|
||||
self._enc_key = user_key[:32]
|
||||
self._mac_key = user_key[32:]
|
||||
elif len(user_key) == 32:
|
||||
self._enc_key = user_key
|
||||
self._mac_key = user_key
|
||||
# Old format: only enc_key was stored, mac_key is wrong.
|
||||
# Clear stale PIN and force re-setup.
|
||||
settings["pin_enabled"] = False
|
||||
settings.pop("pin_salt", None)
|
||||
settings.pop("pin_kdf_iterations", None)
|
||||
settings.pop("pin_envelope", None)
|
||||
settings.pop("pin_envelope_iv", None)
|
||||
settings.pop("pin_token_envelope", None)
|
||||
settings.pop("pin_token_envelope_iv", None)
|
||||
self._save_settings(settings)
|
||||
return {"success": False, "error": "PIN was set with an older version. Please log in and set up PIN again."}
|
||||
else:
|
||||
raise ValueError("Invalid decrypted user key length")
|
||||
|
||||
# Re-sync and decrypt vault
|
||||
if self.client:
|
||||
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
|
||||
)
|
||||
self._session_active = True
|
||||
self._pin_attempts = 0
|
||||
return {"success": True, "vault_size": len(self._decrypted_vault.get("ciphers", []))}
|
||||
# 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"))
|
||||
|
||||
return {"success": False, "error": "No active session"}
|
||||
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
|
||||
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
|
||||
)
|
||||
self._session_active = True
|
||||
self._pin_attempts = 0
|
||||
return {"success": True, "vault_size": len(self._decrypted_vault.get("ciphers", []))}
|
||||
|
||||
except Exception as e:
|
||||
self._pin_attempts += 1
|
||||
@@ -380,6 +439,8 @@ class Plugin:
|
||||
settings.pop("pin_kdf_iterations", None)
|
||||
settings.pop("pin_envelope", None)
|
||||
settings.pop("pin_envelope_iv", None)
|
||||
settings.pop("pin_token_envelope", None)
|
||||
settings.pop("pin_token_envelope_iv", None)
|
||||
self._save_settings(settings)
|
||||
return {"success": True}
|
||||
|
||||
@@ -388,6 +449,16 @@ class Plugin:
|
||||
settings = self._load_settings()
|
||||
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 =====
|
||||
|
||||
async def lock_vault(self) -> dict:
|
||||
@@ -407,6 +478,11 @@ class Plugin:
|
||||
await self.lock_vault()
|
||||
self._master_key = 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}
|
||||
|
||||
# ===== Settings =====
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "decky-vaultwarden",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"description": "Bitwarden/Vaultwarden password manager plugin for Decky Loader",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
"content": {
|
||||
"name": "Vaultwarden - Password Manager",
|
||||
"description": "Access your Bitwarden or Vaultwarden passwords directly from the Steam Deck. Copy passwords, usernames, and TOTP codes to clipboard with a 60-second auto-clear.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"tags": ["security", "passwords", "bitwarden", "vaultwarden"],
|
||||
"pic_value": "https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/login-logo.png"
|
||||
}
|
||||
|
||||
@@ -124,6 +124,10 @@ class VaultApi {
|
||||
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[] }> {
|
||||
return this.call("search_vault", query);
|
||||
}
|
||||
@@ -188,6 +192,15 @@ class VaultApi {
|
||||
async updateSettings(settings: any): Promise<{ success: boolean }> {
|
||||
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();
|
||||
|
||||
@@ -24,6 +24,8 @@ interface LoginViewProps {
|
||||
error: string | null;
|
||||
twoFactorRequired?: boolean;
|
||||
needsMasterPassword?: boolean;
|
||||
defaultServerUrl?: string;
|
||||
defaultEmail?: string;
|
||||
}
|
||||
|
||||
export function LoginView({
|
||||
@@ -33,10 +35,12 @@ export function LoginView({
|
||||
error,
|
||||
twoFactorRequired,
|
||||
needsMasterPassword,
|
||||
defaultServerUrl,
|
||||
defaultEmail,
|
||||
}: LoginViewProps) {
|
||||
const [authMethod, setAuthMethod] = useState<"password" | "apikey">("password");
|
||||
const [serverUrl, setServerUrl] = useState("https://api.bitwarden.com");
|
||||
const [email, setEmail] = useState("");
|
||||
const [serverUrl, setServerUrl] = useState(defaultServerUrl || "https://api.bitwarden.com");
|
||||
const [email, setEmail] = useState(defaultEmail || "");
|
||||
const [password, setPassword] = useState("");
|
||||
const [twoFactorCode, setTwoFactorCode] = useState("");
|
||||
const [clientId, setClientId] = useState("");
|
||||
|
||||
@@ -3,11 +3,12 @@ import { PanelSection, TextField, ButtonItem, Spinner } from "@decky/ui";
|
||||
|
||||
interface PinSetupViewProps {
|
||||
onSetupPin: (pin: string) => Promise<boolean>;
|
||||
onBack: () => void;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export function PinSetupView({ onSetupPin, loading, error }: PinSetupViewProps) {
|
||||
export function PinSetupView({ onSetupPin, onBack, loading, error }: PinSetupViewProps) {
|
||||
const [pin, setPin] = useState("");
|
||||
const [confirmPin, setConfirmPin] = useState("");
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
@@ -29,6 +30,7 @@ export function PinSetupView({ onSetupPin, loading, error }: PinSetupViewProps)
|
||||
if (success) {
|
||||
setPin("");
|
||||
setConfirmPin("");
|
||||
onBack();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -59,6 +61,14 @@ export function PinSetupView({ onSetupPin, loading, error }: PinSetupViewProps)
|
||||
{loading ? <Spinner width={16} height={16} /> : "Set Up PIN"}
|
||||
</ButtonItem>
|
||||
|
||||
<ButtonItem
|
||||
layout="below"
|
||||
onClick={onBack}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</ButtonItem>
|
||||
|
||||
{(localError || error) && (
|
||||
<div style={{ color: "#ff4444", padding: "8px", fontSize: "12px" }}>
|
||||
{localError || error}
|
||||
|
||||
@@ -60,7 +60,7 @@ export function PinUnlockView({
|
||||
</ButtonItem>
|
||||
|
||||
<ButtonItem layout="below" onClick={onLogout} disabled={loading}>
|
||||
Login with Master Password
|
||||
Login as someone else
|
||||
</ButtonItem>
|
||||
|
||||
{error && (
|
||||
|
||||
@@ -19,6 +19,7 @@ interface VaultBrowserProps {
|
||||
onCopyTotp: (cipherId: string) => Promise<boolean>;
|
||||
onLock: () => void;
|
||||
onSetupPin: () => void;
|
||||
onSync: () => void;
|
||||
}
|
||||
|
||||
export function VaultBrowser({
|
||||
@@ -32,6 +33,7 @@ export function VaultBrowser({
|
||||
onCopyTotp,
|
||||
onLock,
|
||||
onSetupPin,
|
||||
onSync,
|
||||
}: VaultBrowserProps) {
|
||||
const [selectedFolder, setSelectedFolder] = useState<string | null>(null);
|
||||
const [localSearch, setLocalSearch] = useState(searchQuery);
|
||||
@@ -63,81 +65,89 @@ export function VaultBrowser({
|
||||
|
||||
return (
|
||||
<PanelSection title="Vault">
|
||||
{/* Search */}
|
||||
<TextField
|
||||
label="Search"
|
||||
description={`${displayedItems.length} items`}
|
||||
value={localSearch}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleSearchChange(e.target.value)}
|
||||
/>
|
||||
|
||||
{/* Folder tabs */}
|
||||
{!searchQuery && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "4px",
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
>
|
||||
<FolderTab
|
||||
name="All"
|
||||
count={vaultData.ciphers.filter((c) => !c.error).length}
|
||||
selected={selectedFolder === null}
|
||||
onClick={() => setSelectedFolder(null)}
|
||||
/>
|
||||
{vaultData.folders.map((folder) => (
|
||||
<FolderTab
|
||||
key={folder.id}
|
||||
name={folder.name}
|
||||
count={vaultData.ciphers.filter(
|
||||
(c) => c.folderId === folder.id && !c.error
|
||||
).length}
|
||||
selected={selectedFolder === folder.id}
|
||||
onClick={() => setSelectedFolder(folder.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Items */}
|
||||
<Focusable
|
||||
flow-children="y"
|
||||
style={{ maxHeight: "400px", overflowY: "auto" }}
|
||||
onSecondaryButton={onSync}
|
||||
onSecondaryActionDescription="Sync Vault"
|
||||
>
|
||||
{displayedItems.length === 0 ? (
|
||||
{/* Search */}
|
||||
<TextField
|
||||
label="Search"
|
||||
description={`${displayedItems.length} items`}
|
||||
value={localSearch}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleSearchChange(e.target.value)}
|
||||
/>
|
||||
|
||||
{/* Folder tabs */}
|
||||
{!searchQuery && (
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
padding: "24px",
|
||||
color: "#888",
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "4px",
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
>
|
||||
{searchQuery ? "No items found" : "No items in this folder"}
|
||||
</div>
|
||||
) : (
|
||||
displayedItems.map((item) => (
|
||||
<ItemCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
folderMap={vaultData.folder_map}
|
||||
copyStatus={copyStatus}
|
||||
onCopyPassword={onCopyPassword}
|
||||
onCopyUsername={onCopyUsername}
|
||||
onCopyTotp={onCopyTotp}
|
||||
<FolderTab
|
||||
name="All"
|
||||
count={vaultData.ciphers.filter((c) => !c.error).length}
|
||||
selected={selectedFolder === null}
|
||||
onClick={() => setSelectedFolder(null)}
|
||||
/>
|
||||
))
|
||||
{vaultData.folders.map((folder) => (
|
||||
<FolderTab
|
||||
key={folder.id}
|
||||
name={folder.name}
|
||||
count={vaultData.ciphers.filter(
|
||||
(c) => c.folderId === folder.id && !c.error
|
||||
).length}
|
||||
selected={selectedFolder === folder.id}
|
||||
onClick={() => setSelectedFolder(folder.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Focusable>
|
||||
|
||||
{/* Actions */}
|
||||
<ButtonItem layout="below" onClick={onLock}>
|
||||
Lock Vault
|
||||
</ButtonItem>
|
||||
<ButtonItem layout="below" onClick={onSetupPin}>
|
||||
Set Up PIN
|
||||
</ButtonItem>
|
||||
{/* Items */}
|
||||
<Focusable
|
||||
flow-children="y"
|
||||
style={{ maxHeight: "400px", overflowY: "auto" }}
|
||||
>
|
||||
{displayedItems.length === 0 ? (
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
padding: "24px",
|
||||
color: "#888",
|
||||
}}
|
||||
>
|
||||
{searchQuery ? "No items found" : "No items in this folder"}
|
||||
</div>
|
||||
) : (
|
||||
displayedItems.map((item) => (
|
||||
<ItemCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
folderMap={vaultData.folder_map}
|
||||
copyStatus={copyStatus}
|
||||
onCopyPassword={onCopyPassword}
|
||||
onCopyUsername={onCopyUsername}
|
||||
onCopyTotp={onCopyTotp}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Focusable>
|
||||
|
||||
{/* Actions */}
|
||||
<ButtonItem layout="below" onClick={onSync}>
|
||||
Sync Vault
|
||||
</ButtonItem>
|
||||
<ButtonItem layout="below" onClick={onLock}>
|
||||
Lock Vault
|
||||
</ButtonItem>
|
||||
<ButtonItem layout="below" onClick={onSetupPin}>
|
||||
Set Up PIN
|
||||
</ButtonItem>
|
||||
</Focusable>
|
||||
</PanelSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export type VaultState =
|
||||
| "two_factor_required"
|
||||
| "needs_master_password"
|
||||
| "pin_only"
|
||||
| "pin_setup"
|
||||
| "loading"
|
||||
| "unlocked";
|
||||
|
||||
@@ -22,6 +23,10 @@ export function useVault() {
|
||||
cipherId: string;
|
||||
expiresAt: number;
|
||||
} | null>(null);
|
||||
const [savedCredentials, setSavedCredentials] = useState<{
|
||||
serverUrl: string;
|
||||
email: string;
|
||||
} | null>(null);
|
||||
|
||||
// Check initial status
|
||||
useEffect(() => {
|
||||
@@ -47,6 +52,19 @@ export function useVault() {
|
||||
} else {
|
||||
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) {
|
||||
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 {
|
||||
state,
|
||||
vaultData,
|
||||
@@ -345,6 +384,7 @@ export function useVault() {
|
||||
searchQuery,
|
||||
searchResults,
|
||||
copyStatus,
|
||||
savedCredentials,
|
||||
setSelectedFolder,
|
||||
loginPassword,
|
||||
loginApiKey,
|
||||
@@ -358,6 +398,8 @@ export function useVault() {
|
||||
logout,
|
||||
setupPin,
|
||||
removePin,
|
||||
enterPinSetup,
|
||||
syncVault,
|
||||
clearError: () => setError(null),
|
||||
};
|
||||
}
|
||||
|
||||
+31
-24
@@ -6,11 +6,10 @@ import { useVault } from "./hooks/useVault";
|
||||
import { LoginView } from "./components/LoginView";
|
||||
import { PinUnlockView } from "./components/PinUnlockView";
|
||||
import { VaultBrowser } from "./components/VaultBrowser";
|
||||
import { PinSetupModal } from "./components/PinSetupModal";
|
||||
import { PinSetupView } from "./components/PinSetupView";
|
||||
|
||||
function VaultwardenPlugin() {
|
||||
const vault = useVault();
|
||||
const [showPinSetup, setShowPinSetup] = useState(false);
|
||||
|
||||
// Handle keyboard input for PIN
|
||||
useEffect(() => {
|
||||
@@ -44,6 +43,8 @@ function VaultwardenPlugin() {
|
||||
onApiKeyLogin={vault.loginApiKey}
|
||||
loading={false}
|
||||
error={vault.error}
|
||||
defaultServerUrl={vault.savedCredentials?.serverUrl}
|
||||
defaultEmail={vault.savedCredentials?.email}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -56,6 +57,8 @@ function VaultwardenPlugin() {
|
||||
loading={false}
|
||||
error={vault.error}
|
||||
twoFactorRequired
|
||||
defaultServerUrl={vault.savedCredentials?.serverUrl}
|
||||
defaultEmail={vault.savedCredentials?.email}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -81,30 +84,32 @@ 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) {
|
||||
return (
|
||||
<>
|
||||
<VaultBrowser
|
||||
vaultData={vault.vaultData}
|
||||
searchQuery={vault.searchQuery}
|
||||
searchResults={vault.searchResults}
|
||||
copyStatus={vault.copyStatus}
|
||||
onSearch={vault.search}
|
||||
onCopyPassword={vault.copyPassword}
|
||||
onCopyUsername={vault.copyUsername}
|
||||
onCopyTotp={vault.copyTotp}
|
||||
onLock={vault.lockVault}
|
||||
onSetupPin={() => setShowPinSetup(true)}
|
||||
/>
|
||||
|
||||
<PinSetupModal
|
||||
isOpen={showPinSetup}
|
||||
onClose={() => setShowPinSetup(false)}
|
||||
onSetupPin={vault.setupPin}
|
||||
loading={false}
|
||||
error={vault.error}
|
||||
/>
|
||||
</>
|
||||
<VaultBrowser
|
||||
vaultData={vault.vaultData}
|
||||
searchQuery={vault.searchQuery}
|
||||
searchResults={vault.searchResults}
|
||||
copyStatus={vault.copyStatus}
|
||||
onSearch={vault.search}
|
||||
onCopyPassword={vault.copyPassword}
|
||||
onCopyUsername={vault.copyUsername}
|
||||
onCopyTotp={vault.copyTotp}
|
||||
onLock={vault.lockVault}
|
||||
onSetupPin={vault.enterPinSetup}
|
||||
onSync={vault.syncVault}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,6 +119,8 @@ function VaultwardenPlugin() {
|
||||
onApiKeyLogin={vault.loginApiKey}
|
||||
loading={false}
|
||||
error={vault.error}
|
||||
defaultServerUrl={vault.savedCredentials?.serverUrl}
|
||||
defaultEmail={vault.savedCredentials?.email}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user