From f8dc43670946910f106a849bfe14f76aad3e6e39 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Mon, 1 Jun 2026 23:36:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20client=20auth=20=E2=80=94=20login/passw?= =?UTF-8?q?ord=20flow,=20auto=20device=20reg,=20sync=20interval=20+=20impr?= =?UTF-8?q?oved=20sync=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/verstak-gui/app.go | 31 ++++++++++++- frontend/src/App.svelte | 79 +++++++++++++++++++++++++++------- internal/core/config/config.go | 1 + internal/core/sync/client.go | 34 +++++++++++++++ 4 files changed, 129 insertions(+), 16 deletions(-) diff --git a/cmd/verstak-gui/app.go b/cmd/verstak-gui/app.go index d384e53..fc61019 100644 --- a/cmd/verstak-gui/app.go +++ b/cmd/verstak-gui/app.go @@ -846,6 +846,7 @@ type SyncStatusDTO struct { DeviceID string `json:"deviceId"` UnpushedOps int `json:"unpushedOps"` LastSyncAt string `json:"lastSyncAt"` + SyncInterval int `json:"syncInterval"` } func (a *App) SyncStatus() (*SyncStatusDTO, error) { @@ -863,11 +864,23 @@ func (a *App) SyncStatus() (*SyncStatusDTO, error) { } if cfg != nil { dto.DeviceID = cfg.Sync.DeviceID + dto.SyncInterval = cfg.Sync.SyncInterval } return dto, nil } -func (a *App) SyncConfigure(serverURL, apiKey string) error { +func (a *App) SyncConfigure(serverURL, username, password string) error { + // Register device on server with user credentials. + hostname, _ := os.Hostname() + if hostname == "" { + hostname = "unknown" + } + client := syncsvc.NewClient(serverURL, "", "", a.vault) + deviceID, apiKey, err := client.RegisterDeviceWithAuth(hostname, username, password) + if err != nil { + return fmt.Errorf("register: %w", err) + } + if err := a.sync.SetState(serverURL, apiKey); err != nil { return err } @@ -878,6 +891,22 @@ func (a *App) SyncConfigure(serverURL, apiKey string) error { } cfg.Sync.ServerURL = serverURL cfg.Sync.APIKey = apiKey + cfg.Sync.DeviceID = deviceID + return config.Save(a.vault, cfg) +} + +func (a *App) SyncTestConnection(serverURL, username, password string) error { + client := syncsvc.NewClient(serverURL, "", "", a.vault) + _, _, err := client.RegisterDeviceWithAuth("test-connection", username, password) + return err +} + +func (a *App) SyncSetInterval(minutes int) error { + cfg, err := config.Load(a.vault) + if err != nil { + return err + } + cfg.Sync.SyncInterval = minutes return config.Save(a.vault, cfg) } diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 89ccf37..dd6a040 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -103,7 +103,9 @@ let syncStatus = null let syncLoading = false let syncServerUrl = '' - let syncApiKey = '' + let syncUsername = '' + let syncPassword = '' + let syncInterval = 0 let syncResult = '' const tabs = [ @@ -897,14 +899,16 @@ try { syncStatus = await wailsCall('SyncStatus') } catch (e) { - syncStatus = { configured: false, serverUrl: '', deviceId: '', unpushedOps: 0, lastSyncAt: '' } + syncStatus = { configured: false, serverUrl: '', deviceId: '', unpushedOps: 0, lastSyncAt: '', syncInterval: 0 } } } function openSettings() { showSettings = true syncServerUrl = syncStatus?.serverUrl || '' - syncApiKey = '' + syncUsername = '' + syncPassword = '' + syncInterval = syncStatus?.syncInterval || 0 syncResult = '' } @@ -917,7 +921,10 @@ syncLoading = true syncResult = '' try { - await wailsCall('SyncConfigure', syncServerUrl, syncApiKey) + await wailsCall('SyncConfigure', syncServerUrl, syncUsername, syncPassword) + if (syncInterval > 0) { + await wailsCall('SyncSetInterval', syncInterval) + } syncResult = 'ok' await loadSyncStatus() } catch (e) { @@ -926,6 +933,18 @@ syncLoading = false } + async function testConnection() { + syncLoading = true + syncResult = '' + try { + await wailsCall('SyncTestConnection', syncServerUrl, syncUsername, syncPassword) + syncResult = 'connection ok' + } catch (e) { + syncResult = 'connection failed: ' + String(e) + } + syncLoading = false + } + async function runSyncNow() { syncLoading = true syncResult = '' @@ -971,10 +990,12 @@ {/if} @@ -991,6 +1012,16 @@ Выберите раздел или дело {/if} +
+ {#if syncStatus?.configured} + + {/if} +
{#if error} @@ -1502,17 +1533,26 @@ {/if}
- +
- - + + +
+
+ + +
+
+ +
{#if syncResult}
{syncResult}
{/if} @@ -1538,12 +1578,18 @@ .nav-item:hover { background: #222233; } .nav-item.selected { background: #2a2a4a; color: #fff; font-weight: 500; } .nav-empty { padding: 8px 20px; color: #555; font-size: 12px; } -.sidebar-footer { padding: 12px 20px; border-top: 1px solid #2a2a3c; flex-shrink: 0; } -.version { font-size: 11px; color: #555; } +.sidebar-footer { padding: 8px 12px; border-top: 1px solid #2a2a3c; flex-shrink: 0; display: flex; flex-direction: column; gap: 4px; } +.version { font-size: 11px; color: #555; text-align: center; } /* Main */ .main { flex: 1; display: flex; flex-direction: column; height: 100vh; min-width: 0; overflow: hidden; background: #13131f; } .header { padding: 12px 24px; border-bottom: 1px solid #2a2a3c; display: flex; align-items: center; flex-shrink: 0; min-height: 48px; } +.header-left { display: flex; align-items: center; gap: 8px; flex: 1; } +.header-right { display: flex; align-items: center; gap: 8px; } +.header-sync-btn { background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 8px; padding: 6px 12px; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; color: #b0b0c0; font-family: inherit; font-size: 13px; position: relative; } +.header-sync-btn:hover { background: #222233; color: #e4e4ef; border-color: #6366f1; } +.header-sync-btn:disabled { opacity: 0.5; cursor: not-allowed; } +.sync-badge { background: #6366f1; color: #fff; font-size: 10px; border-radius: 50%; width: 16px; height: 16px; display: inline-flex; align-items: center; justify-content: center; position: absolute; top: -6px; right: -6px; } .crumb { font-size: 14px; font-weight: 500; } .crumb.placeholder { color: #666; } .crumb-type { font-size: 11px; color: #555; background: #1e1e2e; padding: 2px 8px; border-radius: 10px; margin-left: 8px; } @@ -1732,8 +1778,11 @@ .activity-feed-time { font-size: 11px; color: #555; } /* Sync */ -.settings-btn { background: none; border: none; color: #555; cursor: pointer; padding: 4px; display: inline-flex; align-items: center; border-radius: 4px; margin-left: auto; } -.settings-btn:hover { color: #ccc; background: #222233; } +.sidebar-sync-btn { background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 8px; padding: 8px 12px; cursor: pointer; width: 100%; display: flex; align-items: center; gap: 8px; color: #888; font-family: inherit; font-size: 13px; } +.sidebar-sync-btn:hover { background: #222233; color: #e4e4ef; border-color: #6366f1; } +.sidebar-sync-label { flex: 1; text-align: left; } +.sync-dot { width: 8px; height: 8px; border-radius: 50%; background: #4a4a4a; flex-shrink: 0; } +.sync-dot.active { background: #4ade80; box-shadow: 0 0 6px rgba(74,222,128,0.5); } .modal-sync { width: 460px; } .sync-status { background: #13131f; border-radius: 8px; padding: 12px; margin-bottom: 16px; } .sync-row { display: flex; justify-content: space-between; padding: 4px 0; font-size: 13px; } diff --git a/internal/core/config/config.go b/internal/core/config/config.go index f4644c8..c4d2fd8 100644 --- a/internal/core/config/config.go +++ b/internal/core/config/config.go @@ -27,6 +27,7 @@ type SyncConfig struct { APIKey string `yaml:"api_key"` DeviceID string `yaml:"device_id"` AutoSync bool `yaml:"auto_sync"` + SyncInterval int `yaml:"sync_interval"` } type BrowserConfig struct { diff --git a/internal/core/sync/client.go b/internal/core/sync/client.go index 1a26be9..904c0bb 100644 --- a/internal/core/sync/client.go +++ b/internal/core/sync/client.go @@ -45,6 +45,40 @@ func (c *Client) RegisterDevice(name string) (apiKey string, err error) { return resp.APIKey, nil } +// RegisterDeviceWithAuth registers a device with user credentials. +func (c *Client) RegisterDeviceWithAuth(name, username, password string) (deviceID, apiKey string, err error) { + body := map[string]string{"name": name, "username": username, "password": password} + var resp struct { + DeviceID string `json:"device_id"` + APIKey string `json:"api_key"` + } + // Temporarily clear API key for this request (server expects login/password, not API key). + savedKey := c.APIKey + c.APIKey = "" + err = c.post("/api/v1/device/register", body, &resp) + c.APIKey = savedKey + if err != nil { + return "", "", err + } + return resp.DeviceID, resp.APIKey, nil +} + +// Login authenticates with user credentials and returns a session token. +func (c *Client) Login(username, password string) (token string, err error) { + body := map[string]string{"username": username, "password": password} + var resp struct { + Token string `json:"token"` + } + savedKey := c.APIKey + c.APIKey = "" + err = c.post("/api/v1/auth/login", body, &resp) + c.APIKey = savedKey + if err != nil { + return "", err + } + return resp.Token, nil +} + // PushRequest is the payload for POST /sync/push. type PushRequest struct { DeviceID string `json:"device_id"`