feat(ui): Phase 8 UI/UX optimization and system documentation update
## Sidebar Enhancement - Change tabs to icon + small label layout for better space utilization - Add Teams tab with team collaboration entry point ## Settings Page Improvements - Connect theme toggle to gatewayStore.saveQuickConfig for persistence - Remove OpenFang backend download section, simplify UI - Add time range filter to UsageStats (7d/30d/all) - Add stat cards with icons (sessions, messages, input/output tokens) - Add token usage overview bar chart - Add 8 ZCLAW system skill definitions with categories ## Bug Fixes - Fix ChannelList duplicate content with deduplication logic - Integrate CreateTriggerModal in TriggersPanel - Add independent SecurityStatusPanel with 12 default enabled layers - Change workflow view to use SchedulerPanel as unified entry ## New Components - CreateTriggerModal: Event trigger creation modal - HandApprovalModal: Hand approval workflow dialog - HandParamsForm: Enhanced Hand parameter form - SecurityLayersPanel: 16-layer security status display ## Architecture - Add TOML config parsing support (toml-utils.ts, config-parser.ts) - Add request timeout and retry mechanism (request-helper.ts) - Add secure token storage (secure-storage.ts, secure_storage.rs) ## Tests - Add unit tests for config-parser, toml-utils, request-helper - Add team-client and teamStore tests ## Documentation - Update SYSTEM_ANALYSIS.md with Phase 8 completion - UI completion: 100% (30/30 components) - API coverage: 93% (63/68 endpoints) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
181
desktop/src/lib/secure-storage.ts
Normal file
181
desktop/src/lib/secure-storage.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* ZCLAW Secure Storage
|
||||
*
|
||||
* Provides secure credential storage using the OS keyring/keychain.
|
||||
* Falls back to localStorage when not running in Tauri or if keyring is unavailable.
|
||||
*
|
||||
* Platform support:
|
||||
* - Windows: DPAPI
|
||||
* - macOS: Keychain
|
||||
* - Linux: Secret Service API (gnome-keyring, kwallet, etc.)
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { isTauriRuntime } from './tauri-gateway';
|
||||
|
||||
// Cache for keyring availability check
|
||||
let keyringAvailable: boolean | null = null;
|
||||
|
||||
/**
|
||||
* Check if secure storage (keyring) is available
|
||||
*/
|
||||
export async function isSecureStorageAvailable(): Promise<boolean> {
|
||||
if (!isTauriRuntime()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use cached result if available
|
||||
if (keyringAvailable !== null) {
|
||||
return keyringAvailable;
|
||||
}
|
||||
|
||||
try {
|
||||
keyringAvailable = await invoke<boolean>('secure_store_is_available');
|
||||
return keyringAvailable;
|
||||
} catch (error) {
|
||||
console.warn('[SecureStorage] Keyring not available:', error);
|
||||
keyringAvailable = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Secure storage interface
|
||||
* Uses OS keyring when available, falls back to localStorage
|
||||
*/
|
||||
export const secureStorage = {
|
||||
/**
|
||||
* Store a value securely
|
||||
* @param key - Storage key
|
||||
* @param value - Value to store
|
||||
*/
|
||||
async set(key: string, value: string): Promise<void> {
|
||||
const trimmedValue = value.trim();
|
||||
|
||||
if (await isSecureStorageAvailable()) {
|
||||
try {
|
||||
if (trimmedValue) {
|
||||
await invoke('secure_store_set', { key, value: trimmedValue });
|
||||
} else {
|
||||
await invoke('secure_store_delete', { key });
|
||||
}
|
||||
// Also write to localStorage as backup/migration support
|
||||
writeLocalStorageBackup(key, trimmedValue);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.warn('[SecureStorage] Failed to use keyring, falling back to localStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to localStorage
|
||||
writeLocalStorageBackup(key, trimmedValue);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a value from secure storage
|
||||
* @param key - Storage key
|
||||
* @returns Stored value or null if not found
|
||||
*/
|
||||
async get(key: string): Promise<string | null> {
|
||||
if (await isSecureStorageAvailable()) {
|
||||
try {
|
||||
const value = await invoke<string>('secure_store_get', { key });
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
return value;
|
||||
}
|
||||
// If keyring returned empty, try localStorage fallback for migration
|
||||
return readLocalStorageBackup(key);
|
||||
} catch (error) {
|
||||
console.warn('[SecureStorage] Failed to read from keyring, trying localStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to localStorage
|
||||
return readLocalStorageBackup(key);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a value from secure storage
|
||||
* @param key - Storage key
|
||||
*/
|
||||
async delete(key: string): Promise<void> {
|
||||
if (await isSecureStorageAvailable()) {
|
||||
try {
|
||||
await invoke('secure_store_delete', { key });
|
||||
} catch (error) {
|
||||
console.warn('[SecureStorage] Failed to delete from keyring:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Always clear localStorage backup
|
||||
clearLocalStorageBackup(key);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if secure storage is being used (vs localStorage fallback)
|
||||
*/
|
||||
async isUsingKeyring(): Promise<boolean> {
|
||||
return isSecureStorageAvailable();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* localStorage backup functions for migration and fallback
|
||||
*/
|
||||
function writeLocalStorageBackup(key: string, value: string): void {
|
||||
try {
|
||||
if (value) {
|
||||
localStorage.setItem(key, value);
|
||||
} else {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
} catch {
|
||||
// Ignore localStorage failures
|
||||
}
|
||||
}
|
||||
|
||||
function readLocalStorageBackup(key: string): string | null {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function clearLocalStorageBackup(key: string): void {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch {
|
||||
// Ignore localStorage failures
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous versions for compatibility with existing code
|
||||
* These use localStorage only and are provided for gradual migration
|
||||
*/
|
||||
export const secureStorageSync = {
|
||||
/**
|
||||
* Synchronously get a value from localStorage (for migration only)
|
||||
* @deprecated Use async secureStorage.get() instead
|
||||
*/
|
||||
get(key: string): string | null {
|
||||
return readLocalStorageBackup(key);
|
||||
},
|
||||
|
||||
/**
|
||||
* Synchronously set a value in localStorage (for migration only)
|
||||
* @deprecated Use async secureStorage.set() instead
|
||||
*/
|
||||
set(key: string, value: string): void {
|
||||
writeLocalStorageBackup(key, value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Synchronously delete a value from localStorage (for migration only)
|
||||
* @deprecated Use async secureStorage.delete() instead
|
||||
*/
|
||||
delete(key: string): void {
|
||||
clearLocalStorageBackup(key);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user