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:
|
||||||
@@ -295,9 +314,9 @@ class Plugin:
|
|||||||
pin, pin_salt, kdf_iterations=200000
|
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(
|
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
|
# Save PIN settings
|
||||||
@@ -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
|
||||||
@@ -345,23 +379,48 @@ class Plugin:
|
|||||||
self._enc_key = user_key[:32]
|
self._enc_key = user_key[:32]
|
||||||
self._mac_key = user_key[32:]
|
self._mac_key = user_key[32:]
|
||||||
elif len(user_key) == 32:
|
elif len(user_key) == 32:
|
||||||
self._enc_key = user_key
|
# Old format: only enc_key was stored, mac_key is wrong.
|
||||||
self._mac_key = user_key
|
# 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:
|
else:
|
||||||
raise ValueError("Invalid decrypted user key length")
|
raise ValueError("Invalid decrypted user key length")
|
||||||
|
|
||||||
# Re-sync and decrypt vault
|
# Restore session from persisted tokens if no active client (restart scenario)
|
||||||
if self.client:
|
if not self.client:
|
||||||
sync_data = await self.client.sync_vault()
|
token_envelope_hex = settings.get("pin_token_envelope", "")
|
||||||
self._vault_data = sync_data
|
if token_envelope_hex:
|
||||||
self._decrypted_vault = self.client.decrypt_vault(
|
import json as _json
|
||||||
sync_data, self._enc_key, self._mac_key
|
token_envelope = bytes.fromhex(token_envelope_hex)
|
||||||
)
|
token_data_bytes = self.pin_crypto.decrypt_user_key_with_pin(
|
||||||
self._session_active = True
|
token_envelope, pin_key
|
||||||
self._pin_attempts = 0
|
)
|
||||||
return {"success": True, "vault_size": len(self._decrypted_vault.get("ciphers", []))}
|
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:
|
except Exception as e:
|
||||||
self._pin_attempts += 1
|
self._pin_attempts += 1
|
||||||
@@ -380,6 +439,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 +449,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 +478,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 =====
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "decky-vaultwarden",
|
"name": "decky-vaultwarden",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"description": "Bitwarden/Vaultwarden password manager plugin for Decky Loader",
|
"description": "Bitwarden/Vaultwarden password manager plugin for Decky Loader",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"name": "Vaultwarden - Password Manager",
|
"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.",
|
"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"],
|
"tags": ["security", "passwords", "bitwarden", "vaultwarden"],
|
||||||
"pic_value": "https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/login-logo.png"
|
"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");
|
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,81 +65,89 @@ export function VaultBrowser({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelSection title="Vault">
|
<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
|
<Focusable
|
||||||
flow-children="y"
|
onSecondaryButton={onSync}
|
||||||
style={{ maxHeight: "400px", overflowY: "auto" }}
|
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
|
<div
|
||||||
style={{
|
style={{
|
||||||
textAlign: "center",
|
display: "flex",
|
||||||
padding: "24px",
|
flexWrap: "wrap",
|
||||||
color: "#888",
|
gap: "4px",
|
||||||
|
marginBottom: "8px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{searchQuery ? "No items found" : "No items in this folder"}
|
<FolderTab
|
||||||
</div>
|
name="All"
|
||||||
) : (
|
count={vaultData.ciphers.filter((c) => !c.error).length}
|
||||||
displayedItems.map((item) => (
|
selected={selectedFolder === null}
|
||||||
<ItemCard
|
onClick={() => setSelectedFolder(null)}
|
||||||
key={item.id}
|
|
||||||
item={item}
|
|
||||||
folderMap={vaultData.folder_map}
|
|
||||||
copyStatus={copyStatus}
|
|
||||||
onCopyPassword={onCopyPassword}
|
|
||||||
onCopyUsername={onCopyUsername}
|
|
||||||
onCopyTotp={onCopyTotp}
|
|
||||||
/>
|
/>
|
||||||
))
|
{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 */}
|
{/* Items */}
|
||||||
<ButtonItem layout="below" onClick={onLock}>
|
<Focusable
|
||||||
Lock Vault
|
flow-children="y"
|
||||||
</ButtonItem>
|
style={{ maxHeight: "400px", overflowY: "auto" }}
|
||||||
<ButtonItem layout="below" onClick={onSetupPin}>
|
>
|
||||||
Set Up PIN
|
{displayedItems.length === 0 ? (
|
||||||
</ButtonItem>
|
<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>
|
</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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-24
@@ -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,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) {
|
if (vault.state === "unlocked" && vault.vaultData) {
|
||||||
return (
|
return (
|
||||||
<>
|
<VaultBrowser
|
||||||
<VaultBrowser
|
vaultData={vault.vaultData}
|
||||||
vaultData={vault.vaultData}
|
searchQuery={vault.searchQuery}
|
||||||
searchQuery={vault.searchQuery}
|
searchResults={vault.searchResults}
|
||||||
searchResults={vault.searchResults}
|
copyStatus={vault.copyStatus}
|
||||||
copyStatus={vault.copyStatus}
|
onSearch={vault.search}
|
||||||
onSearch={vault.search}
|
onCopyPassword={vault.copyPassword}
|
||||||
onCopyPassword={vault.copyPassword}
|
onCopyUsername={vault.copyUsername}
|
||||||
onCopyUsername={vault.copyUsername}
|
onCopyTotp={vault.copyTotp}
|
||||||
onCopyTotp={vault.copyTotp}
|
onLock={vault.lockVault}
|
||||||
onLock={vault.lockVault}
|
onSetupPin={vault.enterPinSetup}
|
||||||
onSetupPin={() => setShowPinSetup(true)}
|
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