From 40b3154a343e9c24fefddaeb526ce710c61e94e7 Mon Sep 17 00:00:00 2001 From: Zaki <18zaki18@gmail.com> Date: Mon, 8 Jun 2026 21:31:39 +0100 Subject: [PATCH] feat: MFA tools, single-instance, tray polish, prefs persistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Session highlights: - feat(prefs): DPAPI-backed key/value store (PM.UserPrefs) — fixes rememberedUsername being lost across reboots due to the random ephemeral HTTP port changing the localStorage origin every launch. Bridge cmd://prefs/{get,set} round-trips through Delphi. - feat(tray): icon visible from startup (NIM_ADD at constructor, not at first minimize). Tray context menu themed via uxtheme!135 SetPreferredAppMode so it follows the app's dark/light setting. - feat(single-instance): named mutex + RegisterWindowMessage broadcast. Second launch posts WM_PMSHOW to HWND_BROADCAST and exits; the running bridge restores the window from tray. Mutex lives in Local\ namespace so distinct Windows users can still each run one. - feat(mfa): Authenticator sidebar view (live TOTP codes for every entry with a secret) + standalone TOTP generator modal (paste base32 / otpauth:// URI, or generate a random 20-byte secret). - feat(sidebar): Folders / Tags / Tools sections collapsible with chevron toggle. Badge counts stay visible when collapsed. State persisted in settings_json (synced across devices). - feat(autofill): hotkey when vault is locked now restores the app and focuses the master password input instead of no-op'ing silently. Cleaner UX for the common "I hit Ctrl+Shift+L but the vault was locked" path. - feat(quick-unlock): when enabled, skip lockVault on Windows lock / sleep. Rationale: the DPAPI blob already gates access via the Windows account, so re-locking on top of the OS lock is redundant. Idle auto-lock still fires (separate opt-in). - fix(quick-unlock): re-sync state.quickUnlockEnabled from DPAPI source-of-truth at boot, instead of trusting (now-volatile) localStorage. - docs: CLAUDE.md updated with all new modules, bridge commands, and the port-ephemeral pitfall. Co-Authored-By: Claude Opus 4.7 --- CLAUDE.md | 440 ++++ css/style-legacy.css | 343 +++ css/style.css | 447 +++- delphi-backend/Handlers/PM.Handler.Audit.pas | 65 + delphi-backend/Handlers/PM.Handler.Auth.pas | 23 +- .../Handlers/PM.Handler.Entries.pas | 72 +- .../Handlers/PM.Handler.Recovery.pas | 52 +- .../Handlers/PM.Handler.Settings.pas | 113 + delphi-backend/PMServer.dpr | 13 +- delphi-backend/PMServer.dproj | 1522 ++++++++++--- delphi-backend/PMServer.res | Bin 154792 -> 105096 bytes delphi-backend/PMServer_Icon.ico | Bin 0 -> 102390 bytes delphi-backend/PMServer_Icon1.ico | Bin 0 -> 102390 bytes delphi-backend/Source/PM.Bridge.pas | 533 ++++- delphi-backend/Source/PM.Database.pas | 10 + delphi-backend/Source/PM.HTTPServer.pas | 125 +- delphi-backend/Source/PM.JSON.pas | 1 - delphi-backend/Source/PM.ProcessLockdown.pas | 116 + delphi-backend/Source/PM.RateLimit.pas | 2 +- delphi-backend/Source/PM.Session.pas | 2 +- delphi-backend/Source/PM.SingleInstance.pas | 76 + delphi-backend/Source/PM.UserPrefs.pas | 180 ++ delphi-backend/UMainForm.fmx | 4 +- delphi-backend/UMainForm.pas | 311 ++- delphi-backend/app.ico | Bin 0 -> 102390 bytes delphi-backend/app.png | Bin 0 -> 7752 bytes delphi-backend/assets/assets.res | Bin 146968 -> 325848 bytes icon-hex-16.svg | 43 + icon-hex.svg | 61 + icon-key.svg | 49 + icon-shield.svg | 41 + icon-vault.svg | 49 + icon.svg | 29 + index-legacy.html | 218 ++ index.html | 191 +- js/app-legacy.js | 1550 +++++++++++++ js/app.js | 1981 +++++++++++++++-- test_pbkdf2.php | 51 + 38 files changed, 8165 insertions(+), 548 deletions(-) create mode 100644 CLAUDE.md create mode 100644 css/style-legacy.css create mode 100644 delphi-backend/Handlers/PM.Handler.Audit.pas create mode 100644 delphi-backend/Handlers/PM.Handler.Settings.pas create mode 100644 delphi-backend/PMServer_Icon.ico create mode 100644 delphi-backend/PMServer_Icon1.ico create mode 100644 delphi-backend/Source/PM.ProcessLockdown.pas create mode 100644 delphi-backend/Source/PM.SingleInstance.pas create mode 100644 delphi-backend/Source/PM.UserPrefs.pas create mode 100644 delphi-backend/app.ico create mode 100644 delphi-backend/app.png create mode 100644 icon-hex-16.svg create mode 100644 icon-hex.svg create mode 100644 icon-key.svg create mode 100644 icon-shield.svg create mode 100644 icon-vault.svg create mode 100644 icon.svg create mode 100644 index-legacy.html create mode 100644 js/app-legacy.js create mode 100644 test_pbkdf2.php diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8aaa0f3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,440 @@ +# Password Manager — Architecture éclair + +Desktop password vault Windows. Exe Delphi FMX embarquant WebView2 qui +charge un frontend HTML/JS via Indy HTTP loopback (127.0.0.1:8765). +100 % offline, multi-utilisateur, chiffrement client-side (AES-GCM, +clé dérivée PBKDF2-SHA256 600 k iter). + +## Stack + +- **Backend** : Delphi 12, FMX, TMS FNC WebBrowser (Edge WebView2) +- **HTTP** : Indy `TIdHTTPServer` bindé 127.0.0.1 only +- **DB** : SQLite via FireDAC (`vault.db`, gitignored) +- **Frontend** : vanilla JS + CSS, pas de framework (`js/app.js` monofile ~4000 lignes) + +## Build pipeline (CRITIQUE) + +Toute modif `index.html` / `js/` / `css/` nécessite : + +1. `delphi-backend\assets\BuildAssets.cmd` → régénère `assets.res` +2. Build Delphi (F9 ou MSBuild `PMServer.dproj`) + +Sans étape 1, l'exe embarque l'ancienne version des assets — le bug le +plus courant après modif frontend. + +## Carte des fichiers + +| Rôle | Path | +|---|---| +| Form principale, wire bridge, dispatch cmd:// | `delphi-backend/UMainForm.pas` | +| Bridge JS↔Delphi (clipboard, tray, hotkeys, autofill, focus, debug) | `delphi-backend/Source/PM.Bridge.pas` | +| HTTP server (ordre dispatch : router → embedded → static → 404) | `delphi-backend/Source/PM.HTTPServer.pas` | +| Routes API (regex match) | `delphi-backend/Source/PM.Router.pas` | +| Auth + sessions + CSRF | `delphi-backend/Source/PM.Session.pas` | +| Migrations DB idempotentes (`AddColumnIfMissing`) | `delphi-backend/Source/PM.Database.pas` | +| Static files disque (mort en prod, embedded gagne) | `delphi-backend/Source/PM.StaticFiles.pas` | +| Assets embedded dans .res | `delphi-backend/Source/PM.EmbeddedAssets.pas` | +| Quick unlock DPAPI (device-bound) | `delphi-backend/Source/PM.QuickUnlock.pas` | +| Prefs key/value DPAPI (`prefs.bin`) | `delphi-backend/Source/PM.UserPrefs.pas` | +| Single-instance mutex + broadcast | `delphi-backend/Source/PM.SingleInstance.pas` | +| Handlers REST | `delphi-backend/Handlers/PM.Handler.*.pas` | +| Frontend complet | `js/app.js` | +| HTML racine | `index.html` | +| Styles | `css/style.css` | + +## Bridge cmd:// (JS → Delphi) + +JS navigue vers `cmd://action/sub?params` → intercepté par +`WebBrowserBeforeNavigate` → `HandleBridgeCommand`. + +Commandes connues : +- `clipboard/copy`, `clipboard/clear`, `clipboard/read` (Paste custom menu) +- `quickunlock/{store,get,clear,status}` +- `prefs/{get,set}?key=...` (device-bound DPAPI key/value, voir plus bas) +- `autofill/{configure,hotkeys,execute,cancel}` +- `app/focus` (ramène la fenêtre au premier plan, pour le picker) +- `app/ready` (page chargée → SetFocus WebBrowser + DOM focus auth input) +- `app/theme?mode=dark|light` (sync title bar + popup menus via `SetPreferredAppMode`) +- `audit` (POST log d'action user-visible) +- `entry/new-from-title` (Ctrl+Shift+A → modal pré-rempli) + +Retour Delphi → JS via `WebBrowser.ExecuteJavaScript('Bridge.onX(...)')`. + +## Hotkeys globaux Win32 + +- `Ctrl+Shift+L` : autofill complet (user + Tab + password) +- `Ctrl+Shift+P` : autofill password seul (forms step-2, unlock screens) +- `Ctrl+Shift+A` : new entry pré-rempli avec le titre de la fenêtre foreground +- `Ctrl+Shift+D` : toggle debug panel (uniquement si `config.txt` présent) + +Combos autofill configurables depuis Settings (synced en DB +via `settings_json`). + +## Debug mode + +Fichier `config.txt` à côté de l'exe (tous champs optionnels) : +``` +port=8765 # 0 ou absent = port aléatoire ephemeral (49152-65535) +debug=true # affiche PanelTop (Start/Stop, log memo) +require_token=false # désactive le token validation (curl/Postman direct) +require_process_check=false # désactive le PID-on-socket check +``` +Sans `config.txt`, Ctrl+Shift+D ne fait rien (sécurité, panel jamais +exposé en prod). + +## HTTP server lockdown (anti-browser-direct) + +Le serveur Indy n'accepte une requête que si elle a : +- Query param `?pmt=` (sur la 1ʳᵉ navigation) → Set-Cookie +- OU cookie `pm_token=; HttpOnly; SameSite=Strict; Path=/` + +Au start : +- Port aléatoire dans `[49152, 65535]` si pas configuré +- Token random 32-chars hex généré +- WebView2 navigate vers `http://127.0.0.1:{port}/index.html?pmt={token}` +- JS strip le `?pmt=...` du URL bar via `history.replaceState` + +Requête sans token valide → `404` (pas 403 pour ne pas révéler que le +serveur existe). Override via `require_token=false`. + +### Couche 2 : PID-on-socket process check + +Sur chaque request, `PM.ProcessLockdown` : +- `GetExtendedTcpTable` (winapi `iphlpapi.dll`) lookup le PID qui possède + la connexion entrante (match sur Chrome's `localPort` + `remotePort = our port`) +- `IsDescendantOfCurrentProcess` walk parent tree via `Toolhelp32` → + notre exe est-il ancêtre ? +- Si non (curl externe, autre browser, etc.) → 404 + +Defense in depth : si quelqu'un connaît le token (leaked logs, mémoire), +il faut AUSSI exécuter le code dans notre lignée de process pour passer. +Override via `require_process_check=false`. Notre WebView2 (children +`msedgewebview2.exe`) passent naturellement. + +### Token masqué dans les logs + +`MaskAccessToken(url)` dans UMainForm remplace `?pmt=<32 chars>` par +`?pmt=***` avant `LogLine`. Évite le leak via copie/upload du log. + +## Title bar + popup menus dark mode + +`PM.Bridge.ApplyTitleBarTheme(ADark: Boolean)` fait 2 choses : + +1. `DwmSetWindowAttribute(DWMWA_USE_IMMERSIVE_DARK_MODE=20)` → title bar. + No-op sur Win < 19044. +2. `uxtheme!SetPreferredAppMode` (ordinal #135) + `FlushMenuThemes` (#136) + — API privée stable depuis Win10 1809 utilisée par Explorer/Edge/Office. + Themea automatiquement les popup menus (tray context menu inclus), + scrollbars, tooltips. Valeurs : `ForceDark=2`, `ForceLight=3`. + +Appelé au FormCreate + via `cmd://app/theme?mode=dark|light` quand JS toggle. + +## Single instance + +Au démarrage, `PM.SingleInstance.AcquireOrSignal` : +- Crée un mutex nommé `Local\PMServer.SingleInstance.Mutex` (namespace + `Local\` = per-session, donc deux users Windows distincts peuvent + chacun lancer le leur) +- Si `ERROR_ALREADY_EXISTS` : `PostMessage(HWND_BROADCAST, WM_PMSHOW, 0, 0)` + où `WM_PMSHOW = RegisterWindowMessage('PMServer_ShowExisting')` est un + ID system-unique. Puis exit immédiat avant `Application.Initialize`. + +L'instance existante reçoit `WM_PMSHOW` dans son message-only window +(`PM.Bridge.MsgWindowHandler`) → déclenche `FOnTrayRestore` → +`RestoreFromTray`. `AllocateHWnd` crée une fenêtre top-level (pas +`HWND_MESSAGE`) donc elle reçoit bien le broadcast. + +## Tray icon + +`Shell_NotifyIcon(NIM_ADD)` au **constructor** de `TPMBridge`, pas +au premier minimize → ic ône visible dès le démarrage, même si la +fenêtre est ouverte. `NIM_DELETE` uniquement au destructor. + +`MinimizeToTray` ne touche plus à l'icône, juste à la visibilité de +la fenêtre + clipboard clear + balloon first-time. + +Menu : Open / Lock vault / Quit (via `TrackPopupMenu`, themé par +`SetPreferredAppMode` ci-dessus). + +## Device-bound prefs (`PM.UserPrefs`) + +**Problème résolu** : le serveur HTTP bind un port éphémère aléatoire +(49152-65535) sur chaque start. `localStorage` est keyed par origin +(scheme+host+**port**) → reboot = nouvelle origine = `localStorage` +wipé. Pour les prefs qui doivent survivre (`rememberedUsername`, +etc.), on persiste via DPAPI. + +- `%LOCALAPPDATA%\PMServer\prefs.bin` — DPAPI-encrypted UTF-8 JSON + `{"key":"value",...}` +- `PM.UserPrefs.GetPref(key) / SetPref(key, value)` Delphi-side +- Bridge JS : `await Bridge.getPref(key)` (Promise) / `Bridge.setPref(key, value)` +- Callback : `Bridge.onPrefResult(key, value)` posé par Delphi via + `ExecuteJavaScript`. Resolvers stockés dans `prefResolvers[key]` + avec timeout 2 s. + +`LoadAll` retourne toujours un `TJSONObject` valide (jamais nil) — +défault = objet vide, remplacé seulement sur parse success. + +## Quick unlock — interactions + +- Au boot, `init()` call `bridgeQuickUnlockStatus()` et synchronise + `state.quickUnlockEnabled` + `localStorage` depuis la source of truth + DPAPI (corrige le décalage localStorage quand le port change). +- Sur Windows lock / sleep (`BridgeSystemLock` → `Bridge.onSystemLock`) : + - Si `state.quickUnlockEnabled` → **pas de lockVault**. Le blob DPAPI + gate déjà l'accès via le compte Windows, re-locker est redondant. + Toast informatif affiché. + - Sinon → comportement d'origine (`lockVault()`). +- L'auto-lock par inactivité (`autoLockTimer`) reste indépendant — il + ignore le flag QU (opt-in utilisateur explicite via Settings). + +## Authenticator + TOTP tool + +Sidebar Tools expose 2 items MFA : + +- **Authenticator** (`state.view = 'authenticator'`) : grille de cards + (chaque entry avec TOTP secret) — code 6 chiffres en gros + barre + countdown. `renderAuthenticatorGrid()` décrypte tous les secrets + upfront puis tick 1 s. `authTickTimer` clear sur lock / view change. + Snap instantané à 100% sur reset de période (sinon CSS transition + anime le saut backward → effet "freeze"). +- **TOTP generator** (`openTotpTool()`) : modal standalone. Paste + base32 ou `otpauth://` URI → code live. Bouton "Generate" génère un + secret base32 aléatoire 20 bytes (`randomBase32Secret()`). Rien + n'est sauvegardé. + +Code TOTP retourne `{ code, period, secondsLeft }` — attention au nom, +**pas `remaining`** (utiliser `t.secondsLeft`). + +## Sidebar sections collapsibles + +Sections `Folders`, `Tags`, `Tools` ont chacune un `.section-toggle` +button (chevron + label + badge count) qui toggle `.is-collapsed` sur +la section parent. Body masqué via `display: none` sous `.is-collapsed`. + +État persisté dans `state.sidebarCollapsed = { folders, tags, tools }`, +synced via `SYNCED_SETTING_KEYS` (settings_json). Badges (`#countFolders`, +`#countTags`) restent visibles quand replié. + +## Hotkey verrouillé (UX) + +`autofillHandleRequest` : si `state.locked` ou pas de cryptoKey → +`Bridge.cancelAutofill() + Bridge.focusApp()` puis focus le master +password input via DOM. Remplace l'ancien no-op silencieux qui laissait +l'utilisateur perplexe. + +## WebView2 / TMS settings — pièges majeurs + +`TTMSFNCWebBrowser` expose des props qui semblent settables au FormCreate +mais ne sont effectivement appliquées qu'après l'init async du WebView2 +sous-jacent. **Set ces props dans `WebBrowser.OnInitialized`, pas FormCreate** : + +- `EnableContextMenu := False` → kill le menu Edge natif (Inspecter, Importer mots de passe…) +- `EnableShowDebugConsole := False` → kill DevTools +- `EnableAcceleratorKeys := False` ← **NE PAS METTRE** : casse aussi Ctrl+C/V/X/Z/A/K dans les inputs + +Le blocking F12 / Ctrl+Shift+I/J / Ctrl+U se fait côté JS (keydown +preventDefault) en complément. + +## Custom right-click menu + +Le menu natif est désactivé via TMS. On gère un mini-menu JS dédié sur +right-click dans les inputs : `installCustomContextMenu()` → Cut / Copy / +Paste / Select All. Paste utilise `Bridge.readClipboard()` (sans prompt +WebView2 contrairement à `navigator.clipboard.readText()`). `stopPropagation` +sur les click items sinon le slideover se ferme (chip retiré du DOM +avant que le doc click handler check `.closest('.slideover')`). + +## Browser shortcuts bloqués (keydown JS) + +Bloqués via `preventDefault` : +- `F12`, `Ctrl+Shift+I/J`, `Ctrl+U` — DevTools / View source +- `Ctrl+J` — Downloads overlay (le moins évident, déclenche un popup Edge) +- `Ctrl+H/S/P/T/N/R`, `F5` — History / Save / Print / New tab+win / Reload +- `Ctrl+Shift+N/W/Delete` — Incognito / Close window / Clear data + +**Conservés** : `Ctrl+C/V/X/A/Z` (édition), `Ctrl+F` (find in page), `Ctrl+0/+/-` +(zoom accessibility), `Ctrl+K` (notre command palette), `Ctrl+Shift+L/P/A/D` +(nos hotkeys). + +## Pagination + +State : `pageSize` (10/25/50/100, default 25, **synced via settings_json**) ++ `currentPage` runtime-only. Active sur les 3 view modes (cards/list/ +table) si > 10 items. Position **top** (au-dessus du grid/table). + +CSS : `.pagination { grid-column: 1 / -1 }` pour span full width en cards +view (sinon ça occupe une slot de card). + +Reset `currentPage = 1` sur : search, sort, view (folder/fav/tag), +viewMode change, pageSize change. Render via `renderPagination(total, totalPages)` ++ `computePageList(current, total)` avec ellipsis intelligente +(`1 … 4 5 6 … 12`). + +## Flash animation (nouvelle entry) + +`flashEntry(id)` appelée après `saveEntry` success + `duplicateEntry` : +`setTimeout(0)` → `querySelector('.entry-card[data-id],.entry-row[data-id]')` +→ `scrollIntoView({block:'center'})` + classe `is-flash` 2.5s +(`@keyframes entryFlash` pulse cyan). + +Résout le cas "j'ai ajouté un mot de passe avec tri A-Z, où se loge-t-il ?" +— scroll + pulse trouvent la nouvelle entry dans la grille triée. + +## Settings sync + +Per-user blob JSON dans `users.settings_json`, exposé via `GET/PUT +/settings`. Synced : theme, autoLock, viewMode, sortBy/sortDir, hibp, +hotkeys autofill, etc. **Device-only** (localStorage seulement) : +`quickUnlockEnabled` (DPAPI lié au compte Windows), `autofillEnabled` +(toggle hotkey Win32), `rememberedUsername` (auth screen autofill local). + +## Quick Unlock + +DPAPI blob à `%LOCALAPPDATA%\PMServer\quickunlock.bin` (tied to Windows +user, **pas** à l'emplacement de l'exe — survit au déplacement). + +**Cold-start restore** : `tryQuickUnlock` ne check pas `localStorage` +comme source of truth (WebView2 user-data folder est relatif à l'exe → +move = localStorage perdu). Le fichier DPAPI seul gouverne. Si présent : +- Décrypte blob → restore key +- **Re-login fresh** via `/login` avec `verifier = bytesToHex(rawKey)` + (le token stocké dans le blob peut être expiré ; on en récupère un + nouveau à chaque cold-start) +- Resync `localStorage` à `'1'` pour que Settings affiche "Disable" + +## Recovery code (refactor 2026-05) + +- **5 uses par code** (compteur `remaining_uses` dans `recovery_keys`) +- Décrémenté à chaque redeem. Si tombe à 0 → row deletée +- Row deletée aussi sur `successful change-master-password` +- Workflow attendu : recover → set new master pw (consomme le code) → + regenerate un nouveau code + +**State `justRecovered`** : flag JS posé après redeem, cleared sur +- successful master pw change (le code est consumed) +- `lockVault` (lock = perte de contexte recovery) +- `doLogin` success (login normal avec pw = sortie du mode recovery) + +En mode recovery, le modal "Change master password" masque le champ +"Current master password" ET retire son `required` (sinon HTML5 form +validation bloque silencieusement le submit). Le verifier "current" est +dérivé du `state.cryptoKey` en mémoire (qu'on vient de recover). + +## Autofill pipeline + +### Workflow utilisateur (important) + +**L'utilisateur doit cliquer le champ cible AVANT de presser le hotkey.** +Le code ne fait plus de "click au centre de la fenêtre" pour deviner le +champ — ça détruisait le focus sur les forms qui n'étaient pas centrés +(Gitea login, etc.). + +- `Ctrl+Shift+L` (full) : click sur **username field** → hotkey → user+Tab+pwd +- `Ctrl+Shift+P` (password only) : click sur **password field** → hotkey → pwd seul + +### Pipeline Delphi + +1. `RegisterHotKey` Ctrl+Shift+L/P sur message-only window +2. `WM_HOTKEY` → capture `GetForegroundWindow` + titre, fire callback +3. JS reçoit `Bridge.onAutofillRequest(title, kind)` → strip browser + suffix (`- Google Chrome`, etc.) → matching `state.entries.site` +4. Single match → `Bridge.executeAutofill(user, pwd)` direct + Multi-match → `Bridge.focusApp()` + picker modal → user pick → execute + Note : `closeAutofillPicker(false)` quand user pick (skip cancel + cmd qui clearrait `FAutofillTargetHWND`) +5. Delphi `ExecuteAutofill` : + - Si app foreground → `SW_MINIMIZE` (laisse la place au target) + - `ForceForegroundWindow(target)` avec `AttachThreadInput` + poll + jusqu'à `GetForegroundWindow == target` (max 600ms) + - `WaitForModifierRelease` (sinon Ctrl+Shift restent down → toutes + les frappes deviennent Ctrl+Shift+X) + - `Sleep(120)` settle + - **Pas de click parasite** — le user a déjà focusé le bon champ + - `Ctrl+A + Del` avant chaque champ (clear contenu existant) + - SendInput : `Ctrl+A+Del → username → 200ms → Tab → 200ms → Ctrl+A+Del → password` + +### Matching titre → entry + +- Strip suffixe browser via `BROWSER_SUFFIX_RE` (Chrome, Firefox, Edge, + Brave, Opera, Vivaldi, Safari, Tor, Arc) avant toute comparaison +- Score = max sur 3 champs : `entry.title` (display name, score 1500+), + `entry.site` hostname full (1000+), SLD (500+) +- Pour SLD courts (`x`, `qq`), match avec word boundaries +- `entry.title` priorisé → si utilisateur stocke `site = "git.example.com"` + (hostname), mettre aussi `title = "Gitea"` (brand) couvre le browser + title sans ambiguïté + +### Contraintes connues autofill + +- **UIPI** : SendInput vers process élevé bloqué. Notepad++ admin, + regedit → autofill silencieusement no-op. Tester avec apps non-élevées. +- **Chrome password manager** : si Chrome a des credentials sauvegardés + pour le domaine, il peut racer avec notre SendInput et écraser notre + username pendant le Tab. Workaround : user désactive Chrome PM par + site (clic-droit password field → "Never save passwords for this site") +- **Gitea-like forms** : tabindex explicite + `autocomplete="current-password"` + → Chrome PM s'active. Désactiver pour ce domaine ou utiliser Ctrl+Shift+P +- **Title-based matching** : si l'utilisateur stocke `site = "git.ai-agents4you.com"` + (hostname) mais le titre browser n'a que la marque ("Gitea"), pas de + match. Convention : préférer `site = "Gitea"` (brand) pour les sites + brandés. URL hostname-only fonctionne pour les sites dont le titre + contient le hostname (rare). + +## Contraintes / pièges connus + +- **UIPI** : process non-élevé ne peut pas SendInput vers process élevé + (Notepad++ admin, regedit, …). Solution future : manifest + `uiAccess=true` + exe signé EV + installé dans `C:\Program Files\`. +- **Assets embedded > static** dans le dispatch HTTP : modifier JS sans + rebuild `assets.res` n'a aucun effet sur l'exe. +- **WebView2 mange tous les keydown** : ne pas utiliser `FormKeyDown` + pour des raccourcis globaux → toujours `RegisterHotKey`. +- **FMX `FormCloseQuery`** : minimize-to-tray sauf si `FQuitting=True` + (set par le tray menu "Quit" uniquement). +- **`SetForegroundWindow` Win10/11** : règles strictes anti-stealing → + nécessite `AttachThreadInput` pour cross-process. +- **`updated_at`** est bumpé à chaque PUT entry → sort par défaut = + `name` asc pour que modifier une entry ne change pas sa position. +- **TMS render-time vs DOM-attachment** : pattern courant — fonctions + qui rendent un widget (`soTagsField`, `soTotpField`) appellent un + helper (`renderSoChips`, `startTotpTick`) qui fait `$('#id')` sur un + élément pas encore dans le DOM. Solution : render inline OU + `setTimeout(fn, 0)` pour deferrer jusqu'au tick suivant. +- **HTML5 `required` + `display:none`** : un input `required` caché + bloque silencieusement le form submit. Toggle aussi `.required = false` + quand on cache (cf. modal recovery). +- **WebView2 user-data folder est relatif à l'exe** : déplacer l'exe = + nouveau folder = `localStorage`/`sessionStorage` perdu. Pour ce qui + doit survivre au move, stocker via Delphi (DPAPI, fichiers user). +- **Port éphémère = nouvelle origin = localStorage wipé chaque launch** : + cause distincte du point précédent (l'exe ne bouge pas, mais le port + change). Mêmes conséquences : pour tout pref qui doit survivre au + reboot, passer par `PM.UserPrefs` via `Bridge.getPref/setPref`. Le + fix port-fixe est possible (`port=N` dans `config.txt`) mais on perd + l'aléatoire qui complique le scan externe. +- **Modifier les `Enable*` props TMS en `OnInitialized`**, pas FormCreate. + +## Conventions code (héritage sessions précédentes) + +- **Pas** de commentaires multi-lignes pour chaque fix : préférer noms + de variables/fonctions descriptifs (`OwnFormHwnd`, + `EnsureTargetThreadHasKeyboardFocus`, `FocusSettleDelayMs`) +- Migrations DB : toujours via `AddColumnIfMissing` (idempotent) +- Nouveau setting UI : ajouter à `SYNCED_SETTING_KEYS` + handler dans + `loadServerSettings` switch case + listener change qui appelle + `saveServerSettings()` +- Tout new bridge cmd : handler dans `UMainForm.HandleBridgeCommand`, + log via `LogLine` pour traçabilité + +## Audit / docs + +- `security-issues.md` à la racine : audit sécu legacy (api.php) +- Ce CLAUDE.md = source de vérité pour l'architecture courante + +## Git + +- Pas de remote configuré (dev local) +- `vault.db`, `*.db-shm`, `*.db-wal`, `delphi-backend/Win32/`, `*.dcu`, + `assets.res` gitignored +- Messages commit : `type(scope): description` style conventional diff --git a/css/style-legacy.css b/css/style-legacy.css new file mode 100644 index 0000000..f15b100 --- /dev/null +++ b/css/style-legacy.css @@ -0,0 +1,343 @@ +:root { + --bg: #0e1015; --bg2: #191d27; --text: #e2e6ea; --text2: #7f8a98; + --accent: #6b7280; --accent-rgb: 107,114,128; + --danger: #ef4444; --success: #22c55e; --warning: #f59e0b; + --border: rgba(255,255,255,0.12); --card: #212734; + --input: #11141a; --vault-bg: rgba(255,255,255,0.06); +} +.light { + --bg: #eef0f4; --bg2: #e2e5eb; --text: #161b24; --text2: #5f6a7a; + --accent: #6b7280; --accent-rgb: 107,114,128; + --card: #ffffff; --input: #ffffff; + --border: rgba(0,0,0,0.16); --vault-bg: rgba(255,255,255,0.7); +} + +* { margin:0; padding:0; box-sizing:border-box; } +body { + background: linear-gradient(145deg, var(--bg) 0%, var(--bg2) 100%); + font-family: 'Segoe UI', system-ui, sans-serif; + min-height: 100vh; display: flex; justify-content: center; + align-items: flex-start; padding: 1.2rem; color: var(--text); + transition: background 0.3s, color 0.3s; +} + +.toast-container { position:fixed; top:1rem; right:1rem; z-index:9999; display:flex; flex-direction:column; gap:0.5rem; } +.toast { padding:0.7rem 1.2rem; border-radius:0.8rem; font-size:0.85rem; animation:slideIn 0.3s ease; color:#fff; } +.toast.error { background:var(--danger); } +.toast.success { background:var(--success); } +.toast { display:flex; align-items:center; gap:0.6rem; max-width:400px; } +.toast-action { background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.4); color:#fff; font-weight:700; font-size:0.8rem; padding:0.25rem 0.7rem; border-radius:1rem; cursor:pointer; white-space:nowrap; flex-shrink:0; transition:0.15s; } +.toast-action:hover { background:rgba(0,0,0,0.5); } +@keyframes slideIn { from { transform:translateX(100%); opacity:0; } to { transform:translateX(0); opacity:1; } } + +.vault { + width:96%; max-width:1700px; background:var(--vault-bg); + backdrop-filter:blur(20px); border:1px solid var(--border); + border-radius:2rem; padding:1.8rem; margin:0.5rem auto; + box-shadow:0 30px 50px rgba(0,0,0,0.4); + user-select:none; -webkit-user-drag:none; +} +h1 { font-size:2rem; margin-bottom:0.6rem; display:flex; align-items:center; gap:0.6rem; flex-wrap:wrap; } +h1 span { background:var(--accent); padding:0.2rem 0.7rem; border-radius:3rem; font-size:0.8rem; color:#fff; } + +/* Auth */ +.auth-section { background:rgba(0,0,0,0.25); border-radius:1.5rem; padding:1.3rem; margin-bottom:1rem; border:1px solid var(--border); } +.auth-tabs { display:flex; gap:1rem; margin-bottom:0.8rem; } +.auth-tab { background:none; border:none; color:var(--text2); padding:0.4rem 0.8rem; cursor:pointer; border-bottom:2px solid transparent; font-size:0.9rem; } +.auth-tab.active { color:var(--accent); border-bottom-color:var(--accent); } + +input, select, textarea { + flex:1; min-width:130px; background:var(--input); border:1px solid #2d3748; + padding:0.65rem 1rem; border-radius:2rem; color:var(--text); font-size:0.85rem; + outline:none; font-family:inherit; +} +input:focus, select:focus { border-color:var(--accent); } + +.btn { + background:linear-gradient(135deg, #6b7280, #4b5563); border:none; color:#fff; + font-weight:600; padding:0.65rem 1.3rem; border-radius:2rem; cursor:pointer; + font-size:0.85rem; transition:0.2s; white-space:nowrap; +} +.btn:hover { filter:brightness(1.15); transform:scale(1.02); } +.btn:disabled { opacity:0.5; cursor:not-allowed; transform:none; } +.btn-outline { background:transparent; border:1px solid #475569; color:var(--text2); } +.btn-sm { padding:0.35rem 0.9rem; font-size:0.75rem; } +.btn-xs { padding:0.2rem 0.5rem; font-size:0.7rem; } +.btn-danger { background:var(--danger); } + +.input-group { display:flex; gap:0.5rem; margin:0.7rem 0; flex-wrap:wrap; align-items:center; } + +/* Toolbar */ +.toolbar { display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:0.5rem; margin-bottom:0.7rem; } +.view-dropdown { position:relative; } +.view-dropdown-menu { position:absolute; top:100%; right:0; margin-top:4px; min-width:155px; background:var(--bg2); border:1px solid var(--border); border-radius:0.6rem; padding:0.3rem; box-shadow:0 8px 24px rgba(0,0,0,0.3); z-index:100; display:flex; flex-direction:column; gap:2px; } +.view-opt { background:none; border:none; color:var(--text); padding:0.45rem 0.8rem; border-radius:0.4rem; cursor:pointer; font-size:0.8rem; text-align:left; transition:0.12s; white-space:nowrap; } +.view-opt:hover { background:var(--accent); color:#fff; } +.view-opt.active { background:var(--accent); color:#fff; font-weight:600; } + +.toggles-row { display:flex; gap:1.2rem; align-items:center; flex-wrap:wrap; } +.toggle-item { display:flex; align-items:center; gap:0.4rem; font-size:0.75rem; color:var(--text2); } +.toggle-switch { position:relative; width:36px; height:20px; background:#334155; border-radius:10px; cursor:pointer; transition:0.2s; } +.toggle-switch.active { background:var(--accent); } +.toggle-switch::after { content:''; position:absolute; top:2px; left:2px; width:16px; height:16px; background:#fff; border-radius:50%; transition:0.2s; } +.toggle-switch.active::after { left:18px; } + +.status-badge { background:var(--bg2); padding:0.25rem 0.8rem; border-radius:2rem; font-size:0.75rem; white-space:nowrap; } +.hidden { display:none !important; } + +/* Strength */ +.strength-bar { height:4px; border-radius:2px; transition:0.3s; margin-top:0.2rem; } +.s0 { background:var(--danger); width:20%; } +.s1 { background:var(--warning); width:40%; } +.s2 { background:#eab308; width:60%; } +.s3 { background:#84cc16; width:80%; } +.s4 { background:var(--success); width:100%; } + +/* ========== SEARCH BOX WITH CLEAR BUTTON (FIXED) ========== */ +.search-box { + position: relative; + width: 240px; /* fixed width – adjust if needed */ + flex-shrink: 0; +} +.search-box input { + width: 100%; + box-sizing: border-box; + padding-right: 30px; /* space for the ✕ button */ +} +.clear-search-btn { + position: absolute; + right: 6px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--text2); + cursor: pointer; + font-size: 0.9rem; + line-height: 1; + padding: 6px; + display: none; +} + +/* Folders */ +.folders-bar { display:flex; gap:0.4rem; margin-bottom:0.8rem; flex-wrap:wrap; align-items:center; padding:0.4rem 0.6rem; background:rgba(0,0,0,0.2); border-radius:1rem; } +.folder-chip { background:var(--bg2); border:1px solid var(--border); color:var(--text2); padding:0.3rem 0.8rem; border-radius:2rem; cursor:pointer; font-size:0.78rem; transition:0.2s; white-space:nowrap; } +.folder-chip:hover { background:var(--chip-color, var(--accent)); color:#fff; border-color:var(--chip-color, var(--accent)); } +.folder-chip.active { background:var(--chip-color, var(--accent)); color:#fff; border-color:var(--chip-color, var(--accent)); } +.folder-chip.drag-over { border-color:var(--accent)!important; box-shadow:0 0 12px rgba(var(--accent-rgb),0.5); transform:scale(1.05); } +.pw-display.pw-hover { cursor:pointer; } +.folder-count { background:rgba(0,0,0,0.3); padding:0.1rem 0.4rem; border-radius:1rem; margin-left:0.3rem; font-size:0.7rem; } +.folder-delete-btn { background:transparent; border:none; color:var(--danger); cursor:pointer; font-size:0.7rem; margin-left:0.2rem; opacity:0.7; } +.folder-delete-btn:hover { opacity:1; } +.folder-add-btn { background:transparent; border:1px dashed #475569; color:var(--text2); padding:0.3rem 0.6rem; border-radius:2rem; cursor:pointer; font-size:0.75rem; transition:0.2s; } +.folder-add-btn:hover { border-color:var(--accent); color:var(--accent); } + +.light .entry-card, .light .entry-row, .light .entry-compact { box-shadow:0 1px 4px rgba(0,0,0,0.08); } +#addFolderSelect { min-width:110px; max-width:150px; background:var(--input); border:1px solid #2d3748; color:var(--text); padding:0.5rem 0.8rem; border-radius:2rem; font-size:0.8rem; cursor:pointer; } + +/* Entries */ +#entriesContainer { user-select:none; -webkit-user-drag:none; } +#entriesContainer.grid-view { display:grid; grid-template-columns: repeat(auto-fill, minmax(180px,1fr)); gap:0.6rem; } +#entriesContainer.list-view { display:flex; flex-direction:column; gap:0.4rem; } +#entriesContainer.compact-view { display:flex; flex-direction:column; gap:0.2rem; } +#entriesContainer.table-view { overflow-x:auto; } +#entriesContainer.table-view table { width:100%; border-collapse:collapse; } +#entriesContainer.table-view th { text-align:left; padding:0.4rem 0.6rem; color:var(--text2); font-size:0.75rem; border-bottom:1px solid var(--border); } +#entriesContainer.table-view td { padding:0.4rem 0.6rem; font-size:0.8rem; border-bottom:1px solid rgba(255,255,255,0.03); } + +.entry-card, .entry-row, .entry-compact, .table-row-drag { cursor:pointer; user-select:none; } +.entry-card.drag-over, .entry-row.drag-over, .entry-compact.drag-over, .table-row-drag.drag-over { border-color:var(--accent)!important; box-shadow:0 0 15px rgba(var(--accent-rgb),0.3); } +.entry-card.selected { border-color:var(--accent)!important; box-shadow:0 0 0 2px var(--accent),0 0 18px rgba(var(--accent-rgb),0.35); background:rgba(var(--accent-rgb),0.18); } +.entry-row.selected, .entry-compact.selected { border-color:var(--accent)!important; box-shadow:0 0 0 2px var(--accent),0 0 18px rgba(var(--accent-rgb),0.35); background:rgba(var(--accent-rgb),0.14); border-left:3px solid var(--accent); } +.table-row-drag.selected td { background:rgba(var(--accent-rgb),0.14)!important; box-shadow:inset 0 0 0 1px var(--accent); border-bottom:1px solid var(--accent); } +.entry-card { background:var(--card); border:1px solid var(--border); border-radius:0.8rem; padding:0.9rem 2.8rem 0.9rem 0.9rem; transition:0.2s; position:relative; word-break:break-word; box-shadow:0 2px 6px rgba(0,0,0,0.2); } +.entry-card:hover { border-color:rgba(255,255,255,0.15); transform:translateY(-2px); box-shadow:0 4px 12px rgba(0,0,0,0.3); } +.card-site { font-weight:700; font-size:0.9rem; color:var(--text); margin-bottom:0.2rem; } +.card-user { color:var(--text2); font-size:0.75rem; margin-bottom:0.3rem; } +.card-folder { font-size:0.65rem; color:var(--accent); margin-bottom:0.3rem; background:rgba(var(--accent-rgb),0.15); display:inline-block; padding:0.1rem 0.5rem; border-radius:1rem; } +.card-password { background:var(--bg2); padding:0.3rem 0.5rem; border-radius:0.6rem; display:flex; align-items:center; justify-content:space-between; font-family:monospace; font-size:0.75rem; gap:0.2rem; } +#entriesContainer.card-view .entry-card { padding:1.2rem 3rem 1.2rem 1.2rem; } +#entriesContainer.card-view .card-site { font-size:1.05rem; } +#entriesContainer.card-view .card-password { font-size:0.9rem; } +#entriesContainer.card-view .entry-card:hover { transform:translateY(-3px); } +.entry-row { background:var(--card); border:1px solid var(--border); border-radius:0.8rem; padding:0.7rem 2.8rem 0.7rem 0.9rem; display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:0.4rem; position:relative; box-shadow:0 2px 6px rgba(0,0,0,0.15); } +.entry-compact { display:flex; align-items:center; gap:0.5rem; padding:0.35rem 2.8rem 0.35rem 0.7rem; background:var(--card); border-radius:0.5rem; border:1px solid var(--border); font-size:0.8rem; position:relative; box-shadow:0 1px 4px rgba(0,0,0,0.12); } +.action-btns { position:absolute; top:4px; right:6px; display:flex; gap:4px; z-index:1; } +.delete-btn { width:20px; height:20px; background:transparent; color:var(--danger); border:none; cursor:pointer; font-size:0.9rem; font-weight:700; display:flex; align-items:center; justify-content:center; } +.delete-btn:hover { color:#fff; transform:scale(1.2); } +.light .delete-btn:hover { color:#000!important; } +.edit-btn { width:20px; height:20px; background:transparent; color:var(--accent); border:none; cursor:pointer; font-size:0.75rem; display:flex; align-items:center; justify-content:center; } +.edit-btn:hover { color:#fff; transform:scale(1.2); } +.light .edit-btn:hover { color:#000!important; } +.star-btn { width:20px; height:20px; background:transparent; border:none; cursor:pointer; font-size:0.85rem; display:flex; align-items:center; justify-content:center; color:var(--text2); } +.star-btn:hover { transform:scale(1.3); } +.entry-card.favorite { border-color:rgba(255,200,0,0.3); background:rgba(255,200,0,0.05); } +.entry-row.favorite { border-color:rgba(255,200,0,0.3); background:rgba(255,200,0,0.05); } +.entry-compact.favorite { border-color:rgba(255,200,0,0.3); background:rgba(255,200,0,0.05); } +.table-row-drag.favorite td { background:rgba(255,200,0,0.05); } + +/* Grouped view */ +#entriesContainer.grouped-view { display:flex; flex-direction:column; gap:0.15rem; } +.grouped-header { position:sticky; top:0; z-index:2; background:var(--bg); padding:0.55rem 0.8rem; border-radius:0.5rem; font-weight:600; font-size:0.9rem; color:var(--accent); display:flex; align-items:center; gap:0.5rem; border-bottom:2px solid var(--accent); margin-top:0.4rem; } +.grouped-header:first-child { margin-top:0; } +#entriesContainer.grouped-view .entry-row { border-left:3px solid transparent; transition:0.15s; padding-left:1rem; } +#entriesContainer.grouped-view .entry-row:hover { border-left-color:var(--accent); } + +/* Detail view */ +#entriesContainer.detail-view { display:flex; flex-direction:column; gap:1rem; } +.detail-nav { display:flex; align-items:center; justify-content:center; gap:1rem; padding:0.5rem 0; position:sticky; top:0; z-index:2; background:var(--bg); } +.detail-nav button { background:var(--bg2); border:1px solid var(--border); color:var(--text); padding:0.4rem 1rem; border-radius:0.5rem; cursor:pointer; transition:0.15s; font-size:0.85rem; } +.detail-nav button:hover { border-color:var(--accent); color:var(--accent); } +.detail-nav button:disabled { opacity:0.4; cursor:default; } +.detail-nav .detail-counter { font-size:0.8rem; color:var(--text2); min-width:80px; text-align:center; } +.detail-card { background:var(--bg2); border:1px solid var(--border); border-radius:1rem; padding:1.5rem; display:flex; flex-direction:column; gap:0.9rem; max-width:520px; margin:0 auto; width:100%; } +.detail-field { display:flex; flex-direction:column; gap:0.15rem; } +.detail-label { font-size:0.7rem; color:var(--text2); text-transform:uppercase; letter-spacing:0.5px; } +.detail-value { font-size:1.2rem; word-break:break-all; } +.detail-value.pw-display { font-family:monospace; letter-spacing:2px; font-size:1.3rem; cursor:default; } +.detail-folder { display:flex; gap:0.5rem; align-items:center; } +.detail-actions { display:flex; gap:0.5rem; margin-top:0.3rem; } +.detail-actions button { flex:1; padding:0.5rem; border-radius:0.5rem; border:1px solid var(--border); background:var(--bg); cursor:pointer; transition:0.15s; font-size:0.85rem; } +.detail-actions button:hover { border-color:var(--accent); background:var(--bg2); } + + +.entry-info { display:flex; gap:0.5rem; align-items:center; flex-wrap:wrap; flex:1; } +.entry-site { font-weight:700; color:var(--text); } +.entry-user { color:var(--text2); } +.entry-folder { font-size:0.7rem; color:var(--accent); background:rgba(var(--accent-rgb),0.15); padding:0.1rem 0.5rem; border-radius:1rem; } +.password-field { display:flex; align-items:center; gap:0.3rem; background:var(--bg2); padding:0.2rem 0.5rem; border-radius:2rem; } +.password-text { font-family:monospace; color:var(--text2); font-size:0.8rem; } +.icon-btn { background:none; border:1px solid #475569; color:var(--text2); border-radius:2rem; padding:0.2rem 0.5rem; font-size:0.65rem; cursor:pointer; } +.icon-btn:hover { background:rgba(var(--accent-rgb),0.2); } + +/* Settings Dropdown */ +.settings-dropdown { position:relative; display:inline-block; } +.settings-menu { position:absolute; top:100%; right:0; background:var(--bg2); border:1px solid var(--border); border-radius:0.8rem; padding:0.5rem; min-width:200px; z-index:1000; box-shadow:0 10px 25px rgba(0,0,0,0.4); } +.settings-item { display:flex; justify-content:space-between; align-items:center; padding:0.4rem 0.6rem; border-radius:0.5rem; font-size:0.8rem; color:var(--text2); cursor:pointer; } +.settings-item:hover { background:rgba(255,255,255,0.05); } +.settings-item select { background:var(--input); border:1px solid #334155; color:var(--text); padding:0.2rem 0.5rem; border-radius:1rem; font-size:0.75rem; } + +/* Trash */ +.trash-badge { background:var(--danger); color:#fff; padding:0.15rem 0.5rem; border-radius:1rem; font-size:0.65rem; margin-left:0.3rem; } +.trash-info { font-size:0.7rem; color:var(--text2); margin-top:0.2rem; } +.restore-btn { background:none; color:#fff; border:none; padding:0.2rem 0.6rem; border-radius:1.5rem; cursor:pointer; font-size:0.7rem; } +.restore-btn:hover { filter:brightness(1.2); } +.empty-trash-btn { background:var(--danger); color:#fff; border:none; padding:0.3rem 0.8rem; border-radius:1.5rem; cursor:pointer; font-size:0.75rem; } +.empty-trash-btn:hover { filter:brightness(1.2); } + +/* Edit Modal */ +.edit-modal { position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.7); display:none; justify-content:center; align-items:center; z-index:1001; } +.edit-modal.show { display:flex; } +.edit-box { background:var(--bg2); border-radius:1.5rem; padding:1.5rem; min-width:380px; max-width:90%; } +.edit-box h3 { margin-bottom:1rem; color:var(--text); } +.edit-box label { color:var(--text2); font-size:0.8rem; display:block; margin-bottom:0.2rem; } +.edit-box input, .edit-box select { width:100%; margin-bottom:0.5rem; } + +/* Custom modals */ +.custom-modal-overlay { position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.6); display:flex; justify-content:center; align-items:center; z-index:10001; display:none; } +.custom-modal-overlay.show { display:flex; } +.custom-modal { background:var(--bg2); border:1px solid var(--border); border-radius:1.2rem; padding:1.5rem; min-width:300px; max-width:90%; } +.custom-modal h3 { margin-bottom:1rem; color:var(--text); } +.custom-modal input { width:100%; margin-bottom:1rem; } +.custom-modal .modal-actions { display:flex; gap:0.5rem; justify-content:flex-end; } + +/* Confirm popup near element */ +.custom-confirm { position:fixed; background:var(--bg2); border:1px solid var(--accent); border-radius:0.8rem; padding:0.7rem 1rem; z-index:9999; box-shadow:0 10px 30px rgba(0,0,0,0.5); display:none; font-size:0.8rem; color:var(--text); white-space:nowrap; } +.custom-confirm.show { display:block; } +.custom-confirm .confirm-text { margin-bottom:0.5rem; } +.custom-confirm .confirm-btns { display:flex; gap:0.4rem; } +.custom-confirm .confirm-yes { background:var(--danger); color:#fff; border:none; padding:0.3rem 0.8rem; border-radius:1.5rem; cursor:pointer; font-size:0.75rem; } +.custom-confirm .confirm-no { background:#334155; color:#fff; border:none; padding:0.3rem 0.8rem; border-radius:1.5rem; cursor:pointer; font-size:0.75rem; } + +/* Zigzag toast */ +.toast-zigzag { position:fixed; z-index:9998; padding:0.4rem 0.7rem; border-radius:0.5rem; font-size:0.72rem; font-weight:600; pointer-events:none; animation:zigzagUp 1.2s ease-out forwards; white-space:nowrap; } +.toast-zigzag.success { background:rgba(34,197,94,0.95); color:#fff; } +.toast-zigzag.error { background:rgba(239,68,68,0.95); color:#fff; } +@keyframes zigzagUp { 0%{opacity:1;transform:translate(0,0) scale(1)} 20%{opacity:0.9;transform:translate(-10px,-14px) scale(0.9)} 40%{opacity:0.65;transform:translate(12px,-28px) scale(0.75)} 70%{opacity:0.3;transform:translate(-8px,-42px) scale(0.6)} 100%{opacity:0;transform:translate(0,-60px) scale(0.4)} } + +.idle-warning { position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background:rgba(0,0,0,0.95); color:#fff; padding:2rem; border-radius:2rem; z-index:10000; text-align:center; display:none; } +.idle-warning.show { display:block; } + +@media(max-width:700px) { + .vault { padding:1rem; border-radius:1.5rem; width:98%; } + .entry-row { flex-direction:column; } + #entriesContainer.grid-view { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } + .toolbar { flex-direction:column; } + .folders-bar { flex-direction:column; align-items:stretch; } +} + +/* Floating action button */ +.fab { + position: fixed; bottom: 2rem; right: 2rem; + width: 56px; height: 56px; border-radius: 50%; + background: var(--accent); color: #fff; border: none; + font-size: 1.8rem; cursor: pointer; box-shadow: 0 8px 20px rgba(0,0,0,0.4); + display: flex; align-items: center; justify-content: center; + transition: 0.2s; z-index: 100; +} +.fab:hover { transform: scale(1.1); filter: brightness(1.1); } +.fab-trash { left: 2rem; right: auto; font-size:1.4rem; background:transparent; box-shadow:none; } +.fab-trash.active { box-shadow:0 0 18px rgba(var(--accent-rgb),0.5); } + +/* Generic modal overlay (if you don’t already have it) */ +.modal-overlay { + position: fixed; top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.7); display: none; + justify-content: center; align-items: center; z-index: 1001; +} +.modal-overlay.show { display: flex; } + +/* Modal box (used by both edit and add modals) */ +.modal-box { + background: var(--bg2); border-radius: 1.5rem; padding: 1.5rem; + min-width: 380px; max-width: 90%; +} +.modal-box h3 { margin-bottom: 1rem; color: var(--text); } +.modal-box label { color: var(--text2); font-size: 0.8rem; display: block; margin-bottom: 0.2rem; } +.modal-box input, .modal-box select { width: 100%; margin-bottom: 0.5rem; } +/* Batch select */ +.select-checkbox { + position: absolute; + top: 6px; + left: 6px; + z-index: 2; + accent-color: var(--accent); +} +.batch-actions { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + background: var(--bg2); + border: 1px solid var(--accent); + border-radius: 2rem; + padding: 0.5rem 1.5rem; + display: flex; + gap: 0.8rem; + align-items: center; + z-index: 1000; + box-shadow: 0 10px 25px rgba(0,0,0,0.5); + font-size: 0.85rem; + color: var(--text); +} +.batch-actions button { + font-size: 0.78rem; + padding: 0.35rem 0.8rem; +} +.selected-count { + font-weight: 600; + color: var(--accent); +} +#rectSelect { + position:fixed; pointer-events:none; z-index:999; + border:1px solid var(--accent); + background:rgba(var(--accent-rgb),0.1); + display:none; +} +#trashBtn.drag-over { + border-color:var(--danger)!important; + box-shadow:0 0 15px rgba(239,68,68,0.4); + background:rgba(239,68,68,0.15); +} +/* Batch confirm modal backdrop */ +.batch-confirm-overlay { background:rgba(0,0,0,0.3); } \ No newline at end of file diff --git a/css/style.css b/css/style.css index 7d87d38..2d02b9c 100644 --- a/css/style.css +++ b/css/style.css @@ -490,6 +490,41 @@ input[type="range"]::-webkit-slider-thumb { letter-spacing: 0.5px; color: var(--text-faint); } +.section-toggle { + flex: 1; + display: flex; align-items: center; gap: 6px; + background: none; border: none; padding: 2px 0; + font: inherit; color: inherit; text-transform: inherit; + letter-spacing: inherit; + cursor: pointer; + text-align: left; +} +.section-toggle:hover { color: var(--text-dim); } +.section-count { + min-width: 16px; + padding: 1px 6px; + border-radius: 9px; + background: var(--bg); + color: var(--text-dim); + font-size: 10px; + font-weight: 600; + text-align: center; + letter-spacing: 0; +} +/* Push everything after the count toward the right of the toggle button + so the +/etc. action button stays at the row's edge. */ +.section-toggle { gap: 6px; } +.section-chevron { + width: 12px; height: 12px; + transition: transform 0.18s ease; + flex-shrink: 0; +} +.sidebar-section.is-collapsed .section-chevron { + transform: rotate(-90deg); +} +.sidebar-section.is-collapsed .section-body { + display: none; +} .sidebar-bottom { margin-top: auto; padding-top: 12px; @@ -513,6 +548,13 @@ input[type="range"]::-webkit-slider-thumb { background: var(--bg-glass); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur); + position: relative; + /* backdrop-filter creates a stacking context, so child z-indexes (e.g. + .user-dropdown : 50) are confined here. Without an explicit z-index + the topbar lands behind .slideover (z-index 30), clipping the + dropdown when an entry detail panel is open. 40 puts the topbar + (and its children) above the slideover; modals at 200+ still win. */ + z-index: 40; } .search { flex: 1; max-width: 480px; position: relative; display: flex; align-items: center; } .search svg { @@ -615,14 +657,6 @@ input[type="range"]::-webkit-slider-thumb { } /* Marquee rectangle for rubber-band selection */ -.marquee { - position: fixed; - z-index: 5; - background: var(--accent-soft); - border: 1px solid var(--accent); - border-radius: 2px; - pointer-events: none; -} .entry-card { position: relative; @@ -714,26 +748,55 @@ input[type="range"]::-webkit-slider-thumb { restore + purge (trash view), or kebab (compact mode). Same order slot. */ .entry-grid.is-list .entry-fav, .entry-grid.is-list .entry-del, +.entry-grid.is-list .entry-dup, .entry-grid.is-list .entry-kebab-wrap, .entry-grid.is-list .entry-head .icon-btn { order: 6; flex-shrink: 0; } -.entry-avatar.is-checkable { - position: relative; - cursor: pointer; -} -.entry-avatar.is-checkable:hover::after { - content: ''; - position: absolute; inset: 0; +/* Selection checkbox overlay — nested INSIDE the avatar (.entry-avatar + is position:relative) so it covers the avatar 1:1, no overlap with the + avatar's footprint. Hidden by default; appears on card hover OR when + the entry is already checked. The avatar's initials stay visible + underneath through the checkbox's transparent-then-solid transition. */ +.entry-check { + position: absolute; + inset: 0; + display: grid; place-items: center; + background: var(--bg); + border: 1.5px solid var(--accent); border-radius: var(--radius-sm); - background: var(--accent-soft); - border: 2px solid var(--accent); + color: var(--text); + font-size: 14px; + line-height: 1; + cursor: pointer; + opacity: 0; + transition: opacity var(--t-fast), + background var(--t-fast), border-color var(--t-fast); + z-index: 2; } -.entry-card.is-checked .entry-avatar { +.entry-card:hover .entry-check, +.entry-check.is-checked { + opacity: 1; +} +.entry-check.is-checked { background: var(--accent); + border-color: var(--accent); color: white; } +/* Standalone variant used in the table view's check column. The default + rule above is absolute+inset:0 so the box sizes itself from its avatar + parent (card view). When the checkbox lives on its own in a , no + parent provides a size and the box collapses to a thin line. This + modifier restores explicit dimensions and always-on visibility. */ +.entry-check.entry-check-static { + position: static; + width: 18px; height: 18px; + border-radius: 4px; + border-width: 1.5px; + opacity: 1; + font-size: 11px; +} /* Batch action bar (appears when selection is non-empty) */ .batch-bar { @@ -766,6 +829,7 @@ input[type="range"]::-webkit-slider-thumb { .entry-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; } .entry-avatar { + position: relative; /* anchor for .entry-check overlay */ width: 32px; height: 32px; display: grid; place-items: center; background: var(--bg-elev-2); @@ -784,7 +848,7 @@ input[type="range"]::-webkit-slider-thumb { font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.entry-fav, .entry-del { +.entry-fav, .entry-del, .entry-dup { color: var(--text-faint); background: transparent; border: none; padding: 4px; @@ -793,6 +857,18 @@ input[type="range"]::-webkit-slider-thumb { } .entry-fav.is-on { color: var(--warning); } .entry-fav:hover { color: var(--warning); transform: scale(1.1); } +.entry-dup { + color: var(--text-dim); + opacity: 0; +} +.entry-card:hover .entry-dup { opacity: 0.8; } +.entry-dup:hover { + opacity: 1; + color: var(--accent); + background: var(--accent-soft); + border-radius: 6px; + transform: scale(1.1); +} /* Delete button — always visible, brightens on hover */ .entry-del { @@ -862,6 +938,10 @@ input[type="range"]::-webkit-slider-thumb { animation: modalIn var(--t-fast); } .entry-kebab-menu.is-open { display: block; } +/* Lift the card containing an open kebab menu above its siblings so the + dropdown isn't clipped by the next card in DOM order. */ +.entry-card:has(.entry-kebab-menu.is-open) { z-index: 100; } +.entry-row:has(.entry-kebab-menu.is-open) { position: relative; z-index: 100; } .kebab-item { display: flex; align-items: center; gap: 10px; @@ -966,6 +1046,62 @@ input[type="range"]::-webkit-slider-thumb { transition: width 0.8s linear, background 0.2s; } +/* ---- Authenticator view (TOTP grid) ---------------------- */ +.entry-grid.is-auth { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 12px; +} +.auth-card { + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 14px 16px; + cursor: pointer; + transition: border-color 0.15s, transform 0.05s; +} +.auth-card:hover { border-color: var(--accent); } +.auth-card:active { transform: scale(0.99); } +.auth-card-head { margin-bottom: 8px; } +.auth-card-title { + font-weight: 600; + color: var(--text); + font-size: 14px; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.auth-card-sub { + font-size: 12px; + color: var(--text-dim); + margin-top: 2px; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.auth-card-code-row { + display: flex; align-items: center; gap: 8px; +} +.auth-card-code { + flex: 1; + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 26px; + letter-spacing: 3px; + color: var(--accent); + font-weight: 600; + user-select: all; +} +.auth-card-bar-wrap { + margin-top: 8px; + height: 3px; + background: var(--bg); + border-radius: 2px; + overflow: hidden; +} +.auth-card-bar { + height: 100%; + background: var(--accent); + width: 100%; + transition: width 0.8s linear, background 0.2s; +} +.auth-card-bar.is-warning { background: #f59e0b; } + /* ---- 10. SLIDE-OVER -------------------------------------- */ .slideover { @@ -1276,3 +1412,276 @@ input[type="range"]::-webkit-slider-thumb { .content { padding: 16px; } .slideover { width: 100%; } } + +/* ---- 17. AUTOFILL PICKER -------------------------------- */ + +.autofill-pick-btn { + display: flex; + flex-direction: column; + gap: 2px; + width: 100%; + padding: 10px 16px; + background: none; + border: none; + text-align: left; + cursor: pointer; + transition: background var(--t-fast); +} +.autofill-pick-btn:hover { background: var(--bg-elev-2); } +.autofill-pick-site { + font-size: 13px; + font-weight: 500; + color: var(--text); +} +.autofill-pick-user { + font-size: 11px; + color: var(--text-dim); +} + +/* ---- Hotkey capture buttons (Settings → Autofill) ---- */ +.hotkey-capture-btn { + font-family: ui-monospace, "Cascadia Code", Menlo, monospace; + font-size: 11px; + min-width: 140px; + text-align: center; +} +.hotkey-capture-btn.is-capturing { + background: var(--accent); + color: var(--accent-fg, #fff); + animation: pulse-capture 1.2s ease-in-out infinite; +} +@keyframes pulse-capture { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } +} + +/* ---- Card display title + subtitle (when entry.title set) ---- */ +.entry-subtitle { + display: block; + font-size: 11px; + color: var(--text-faint); + margin-top: 1px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ---- Sidebar "(no folder)" pseudo-entry ---- */ +.nav-item.is-uncategorized { + color: var(--text-faint); + font-style: italic; +} +.nav-item.is-uncategorized:hover { color: var(--text-dim); } +.nav-item.is-uncategorized.is-active { color: var(--text); font-style: normal; } +/* Spacer occupies the same footprint as the SVG folder icon so labels + stay vertically aligned across real-folder rows and "(no folder)". */ +.nav-icon-spacer { + display: inline-block; + width: 16px; + height: 16px; + flex-shrink: 0; +} + +/* ============================================================ + Table view + ============================================================ */ +.entry-grid.is-table { display: block; padding: 0; } +.entry-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} +.entry-table thead th { + text-align: left; + font-weight: 500; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-faint); + padding: 10px 12px; + border-bottom: 1px solid var(--border); + background: var(--bg-elev); + user-select: none; + position: sticky; top: 0; + z-index: 1; +} +.entry-table th.is-sortable { cursor: pointer; } +.entry-table th.is-sortable:hover { color: var(--text-dim); } +.entry-table th.is-active { color: var(--accent); } +.entry-table th .sort-arrow { + margin-left: 4px; + font-size: 10px; + vertical-align: middle; +} +.entry-table tbody td { + padding: 8px 12px; + border-bottom: 1px solid var(--border-soft); + color: var(--text); + vertical-align: middle; +} +.entry-row { cursor: pointer; transition: background var(--t-fast); } +.entry-row:hover { background: var(--bg-elev); } +.entry-row.is-selected { background: var(--accent-soft); } +.entry-row.is-checked { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); } +.col-check { width: 36px; } +.col-name { min-width: 180px; } +.col-name .entry-avatar-sm { + width: 22px; height: 22px; + display: inline-grid; place-items: center; + font-size: 10px; + margin-right: 8px; + vertical-align: middle; +} +.col-name .cell-name-wrap { display: inline-flex; align-items: center; gap: 6px; } +.col-name .fav-dot { color: var(--warning); font-size: 12px; } +.col-site, .col-user { color: var(--text-dim); } +.col-site { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.col-user { max-width: 200px; } +.col-user .icon-btn { margin-left: 4px; opacity: 0; transition: opacity var(--t-fast); } +.entry-row:hover .col-user .icon-btn { opacity: 0.7; } +.col-folder { color: var(--text-dim); white-space: nowrap; } +.col-updated { color: var(--text-faint); white-space: nowrap; font-variant-numeric: tabular-nums; } +.col-actions { width: 80px; text-align: right; white-space: nowrap; } +.col-actions .icon-btn { opacity: 0; transition: opacity var(--t-fast); } +.entry-row:hover .col-actions .icon-btn { opacity: 0.7; } +.col-actions .entry-kebab-wrap { display: inline-block; vertical-align: middle; } + +/* Hide Edge/IE native password reveal eye (we ship our own toggle). */ +input[type="password"]::-ms-reveal, +input[type="password"]::-ms-clear { + display: none !important; + width: 0; + height: 0; +} + +/* Login screen pw toggle — only one action button, override the + 2-buttons layout from .input-with-action. */ +.input-with-action:has(> .icon-btn:only-of-type) input { + padding-right: 40px; +} +.input-with-action:has(> .icon-btn:only-of-type) .icon-btn:only-of-type { + right: 4px; +} + +/* Custom right-click menu (replaces native Edge contextmenu in inputs). */ +.custom-ctxmenu { + position: fixed; + z-index: 9999; + min-width: 160px; + padding: 4px; + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + box-shadow: var(--shadow); +} +.custom-ctxmenu-item { + display: block; + width: 100%; + text-align: left; + padding: 7px 12px; + font-size: 13px; + background: transparent; + border: none; + border-radius: 4px; + color: var(--text); + cursor: pointer; +} +.custom-ctxmenu-item:hover:not(.is-disabled) { background: var(--bg-elev-2); } +.custom-ctxmenu-item.is-disabled { color: var(--text-faint); cursor: default; } + +/* Folder delete X button — visible on row hover only. */ +.nav-item .folder-delete { + margin-left: auto; + width: 20px; height: 20px; + padding: 0; + line-height: 0; + display: inline-flex; align-items: center; justify-content: center; + background: transparent; border: none; + color: var(--text-faint); + border-radius: 4px; + opacity: 0; + transition: opacity var(--t-fast), color var(--t-fast), background var(--t-fast); + cursor: pointer; +} +.nav-item .folder-delete svg { width: 14px; height: 14px; display: block; } +.nav-item:hover .folder-delete { opacity: 0.7; } +.nav-item .folder-delete:hover { + opacity: 1; + color: var(--danger); + background: var(--danger-soft); +} +.nav-item:hover .nav-count { display: none; } + +/* Remember username checkbox row on auth screen. */ +.remember-row { + display: flex; align-items: center; gap: 8px; + margin: -4px 0 12px; + font-size: 12px; + color: var(--text-dim); + cursor: pointer; + user-select: none; +} +.remember-row input[type="checkbox"] { + width: 14px; height: 14px; + accent-color: var(--accent); + cursor: pointer; +} + +/* Just-added entry feedback — pulse background fades out over 2.5s. */ +@keyframes entryFlash { + 0% { background: var(--accent-soft); box-shadow: 0 0 0 2px var(--accent), 0 8px 24px rgba(6,182,212,0.3); } + 40% { background: var(--accent-soft); box-shadow: 0 0 0 2px var(--accent), 0 8px 24px rgba(6,182,212,0.2); } + 100% { background: var(--bg-elev); box-shadow: 0 0 0 0 transparent; } +} +.entry-card.is-flash { animation: entryFlash 2.5s ease-out; } +.entry-row.is-flash { animation: entryFlash 2.5s ease-out; } + +/* Pagination footer (list + table views). */ +.pagination { + display: flex; align-items: center; gap: 6px; + padding: 14px 12px 4px; + flex-wrap: wrap; + font-size: 13px; + color: var(--text-dim); + grid-column: 1 / -1; +} +.pagination-info { + margin-right: auto; + color: var(--text-faint); + font-size: 12px; +} +.pagination-btn { + min-width: 32px; + padding: 5px 9px; + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-dim); + cursor: pointer; + transition: all var(--t-fast); +} +.pagination-btn:hover:not(.is-active):not(.is-disabled) { + border-color: var(--accent); + color: var(--text); +} +.pagination-btn.is-active { + background: var(--accent); + border-color: var(--accent); + color: white; + cursor: default; +} +.pagination-btn.is-disabled { + opacity: 0.4; + cursor: not-allowed; +} +.pagination-ellipsis { padding: 0 4px; color: var(--text-faint); } +.pagination-size { + margin-left: 8px; + padding: 5px 8px; + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-dim); + font-size: 12px; + cursor: pointer; +} diff --git a/delphi-backend/Handlers/PM.Handler.Audit.pas b/delphi-backend/Handlers/PM.Handler.Audit.pas new file mode 100644 index 0000000..89d416d --- /dev/null +++ b/delphi-backend/Handlers/PM.Handler.Audit.pas @@ -0,0 +1,65 @@ +unit PM.Handler.Audit; + +(* + POST /audit body {action, site} -> {ok:true} + + Light-weight endpoint that lets the JS layer append an entry to audit_log + without going through the full entries pipeline. Used by the autofill + feature to record which site was filled (action = "autofill:"). + The bearer token identifies the user — no data beyond the action string + is stored. +*) + +interface + +implementation + +uses + System.SysUtils, System.JSON, + IdCustomHTTPServer, + PM.Router, PM.JSON, PM.Session, PM.Audit; + +function GetClientIP(ARequest: TIdHTTPRequestInfo): string; +begin + Result := ARequest.RemoteIP; + if Result = '' then Result := '127.0.0.1'; +end; + +procedure HandlePostAudit(ARequest: TIdHTTPRequestInfo; + AResponse: TIdHTTPResponseInfo; const AParams: TArray); +var + LUserId: Integer; + LBody: TJSONObject; + LAction, LSite: string; +begin + LUserId := Authenticate(ARequest, AResponse); + RequireCSRF(ARequest, AResponse, LUserId); + + LBody := TJSONHelper.ReadBody(ARequest); + try + LAction := LBody.GetValue('action', ''); + LSite := LBody.GetValue('site', ''); + finally + LBody.Free; + end; + + if LAction = '' then + begin + TJSONHelper.SendError(AResponse, 400, 'action required'); + Exit; + end; + + // Keep the log compact: "autofill:github.com" rather than repeating + // structured columns we don't have in the current schema. + if LSite <> '' then + LAction := LAction + ':' + LSite; + + LogAudit(LUserId, LAction, GetClientIP(ARequest)); + + TJSONHelper.SendOK(AResponse); +end; + +initialization + Router.Register('POST', '/audit', HandlePostAudit); + +end. diff --git a/delphi-backend/Handlers/PM.Handler.Auth.pas b/delphi-backend/Handlers/PM.Handler.Auth.pas index d8e8afa..449503a 100644 --- a/delphi-backend/Handlers/PM.Handler.Auth.pas +++ b/delphi-backend/Handlers/PM.Handler.Auth.pas @@ -19,9 +19,9 @@ interface implementation uses - System.SysUtils, System.JSON, System.Classes, - FireDAC.Comp.Client, - IdCustomHTTPServer,Data.DB, + System.SysUtils, System.JSON, System.Classes, System.Generics.Collections, + Data.DB, FireDAC.Comp.Client, FireDAC.Stan.Param, + IdCustomHTTPServer, PM.Router, PM.JSON, PM.Database, PM.Crypto, PM.Session, PM.RateLimit, PM.Audit; @@ -961,8 +961,21 @@ begin DB.Unlock; end; - // Step 5: invalidate every other session for this user. The CURRENT - // session token is still valid — caller stays logged in. + DB.Lock; + try + LQ := TFDQuery.Create(nil); + try + LQ.Connection := DB.Connection; + LQ.SQL.Text := 'DELETE FROM recovery_keys WHERE user_id = :uid'; + LQ.ParamByName('uid').AsInteger := LUserId; + LQ.ExecSQL; + finally + LQ.Free; + end; + finally + DB.Unlock; + end; + DeleteAllUserSessions(LUserId); finally LBody.Free; diff --git a/delphi-backend/Handlers/PM.Handler.Entries.pas b/delphi-backend/Handlers/PM.Handler.Entries.pas index 87cb520..1ba6218 100644 --- a/delphi-backend/Handlers/PM.Handler.Entries.pas +++ b/delphi-backend/Handlers/PM.Handler.Entries.pas @@ -16,6 +16,7 @@ implementation uses System.SysUtils, System.JSON, System.StrUtils, System.NetEncoding, + System.Generics.Collections, Data.DB, FireDAC.Comp.Client, FireDAC.Stan.Param, IdCustomHTTPServer, IdGlobalProtocols, IdURI, PM.Router, PM.JSON, PM.Database, PM.Session, PM.Audit, PM.RateLimit; @@ -91,6 +92,7 @@ begin LObj := TJSONObject.Create; LObj.AddPair('id', TJSONNumber.Create(LQ.FieldByName('id').AsInteger)); LObj.AddPair('site', LQ.FieldByName('site').AsString); + LObj.AddPair('title', LQ.FieldByName('title').AsString); LObj.AddPair('username', LQ.FieldByName('username').AsString); LObj.AddPair('encrypted_password', LQ.FieldByName('encrypted_password').AsString); LObj.AddPair('iv', LQ.FieldByName('iv').AsString); @@ -135,7 +137,7 @@ procedure HandleCreateEntry(ARequest: TIdHTTPRequestInfo; var LUserId, LNewId: Integer; LBody, LObj: TJSONObject; - LSite, LUser, LFolder, LEnc, LIV, LTags, LNow, LTotpSec, LTotpIv: string; + LSite, LTitle, LUser, LFolder, LEnc, LIV, LTags, LNow, LTotpSec, LTotpIv: string; LQ: TFDQuery; begin try @@ -148,6 +150,7 @@ begin LBody := TJSONHelper.ReadBody(ARequest); try LSite := Trim(LBody.GetValue('site', '')); + LTitle := Trim(LBody.GetValue('title', '')); LUser := Trim(LBody.GetValue('username', '')); LFolder := Trim(LBody.GetValue('folder', 'All')); LEnc := LBody.GetValue('encrypted_password', ''); @@ -175,11 +178,12 @@ begin LQ.Connection := DB.Connection; LQ.SQL.Text := 'INSERT INTO vault_entries ' + - '(user_id, site, username, encrypted_password, iv, encryption_method, ' + + '(user_id, site, title, username, encrypted_password, iv, encryption_method, ' + ' folder, tags, totp_secret, totp_iv, created_at, updated_at) ' + - 'VALUES (:uid, :s, :u, :e, :i, ''client'', :f, :t, :ts, :tiv, :c, :c2)'; + 'VALUES (:uid, :s, :tt, :u, :e, :i, ''client'', :f, :t, :ts, :tiv, :c, :c2)'; LQ.ParamByName('uid').AsInteger := LUserId; LQ.ParamByName('s').AsString := LSite; + LQ.ParamByName('tt').AsString := LTitle; LQ.ParamByName('u').AsString := LUser; LQ.ParamByName('e').AsString := LEnc; LQ.ParamByName('i').AsString := LIV; @@ -216,6 +220,7 @@ begin LObj := TJSONObject.Create; LObj.AddPair('id', TJSONNumber.Create(LNewId)); LObj.AddPair('site', LSite); + LObj.AddPair('title', LTitle); LObj.AddPair('username', LUser); LObj.AddPair('folder', LFolder); LObj.AddPair('tags', LTags); @@ -229,7 +234,7 @@ procedure HandleUpdateEntry(ARequest: TIdHTTPRequestInfo; var LUserId, LId: Integer; LBody: TJSONObject; - LSite, LUser, LFolder, LEnc, LIV, LTags, LNow, LTotpSec, LTotpIv: string; + LSite, LTitle, LUser, LFolder, LEnc, LIV, LTags, LNow, LTotpSec, LTotpIv: string; LQ: TFDQuery; begin try @@ -249,6 +254,7 @@ begin LBody := TJSONHelper.ReadBody(ARequest); try LSite := Trim(LBody.GetValue('site', '')); + LTitle := Trim(LBody.GetValue('title', '')); LUser := Trim(LBody.GetValue('username', '')); LFolder := Trim(LBody.GetValue('folder', 'All')); LEnc := LBody.GetValue('encrypted_password', ''); @@ -274,11 +280,12 @@ begin LQ.Connection := DB.Connection; LQ.SQL.Text := 'UPDATE vault_entries ' + - 'SET site=:s, username=:u, encrypted_password=:e, iv=:i, ' + + 'SET site=:s, title=:tt, username=:u, encrypted_password=:e, iv=:i, ' + ' folder=:f, tags=:t, totp_secret=:ts, totp_iv=:tiv, ' + ' updated_at=:c ' + 'WHERE id=:id AND user_id=:uid'; LQ.ParamByName('s').AsString := LSite; + LQ.ParamByName('tt').AsString := LTitle; LQ.ParamByName('u').AsString := LUser; LQ.ParamByName('e').AsString := LEnc; LQ.ParamByName('i').AsString := LIV; @@ -501,7 +508,7 @@ var LUserId, I, LImported: Integer; LBody, LObj, LEntry: TJSONObject; LArr: TJSONArray; - LSite, LUser, LFolder, LEnc, LIV, LTags, LTotpSec, LTotpIv, LNow: string; + LSite, LTitle, LUser, LFolder, LEnc, LIV, LTags, LTotpSec, LTotpIv, LNow: string; LQ: TFDQuery; begin try @@ -540,9 +547,9 @@ begin LQ.Connection := DB.Connection; LQ.SQL.Text := 'INSERT INTO vault_entries ' + - '(user_id, site, username, encrypted_password, iv, encryption_method, ' + + '(user_id, site, title, username, encrypted_password, iv, encryption_method, ' + ' folder, tags, totp_secret, totp_iv, created_at, updated_at) ' + - 'VALUES (:uid, :s, :u, :e, :i, ''client'', :f, :t, :ts, :tiv, :c, :c2)'; + 'VALUES (:uid, :s, :tt, :u, :e, :i, ''client'', :f, :t, :ts, :tiv, :c, :c2)'; // Declare optional TOTP param types ONCE — the prepared statement // is reused across every imported entry, and FireDAC needs the // type set before the first .Clear call would otherwise fail @@ -554,6 +561,7 @@ begin begin LEntry := LArr.Items[I] as TJSONObject; LSite := Trim(LEntry.GetValue('site', '')); + LTitle := Trim(LEntry.GetValue('title', '')); LUser := Trim(LEntry.GetValue('username', '')); LFolder := Trim(LEntry.GetValue('folder', 'All')); LEnc := LEntry.GetValue('encrypted_password', ''); @@ -569,6 +577,7 @@ begin LQ.ParamByName('uid').AsInteger := LUserId; LQ.ParamByName('s').AsString := LSite; + LQ.ParamByName('tt').AsString := LTitle; LQ.ParamByName('u').AsString := LUser; LQ.ParamByName('e').AsString := LEnc; LQ.ParamByName('i').AsString := LIV; @@ -604,6 +613,52 @@ begin TJSONHelper.SendJSON(AResponse, LObj); end; +procedure HandleEntriesCount(ARequest: TIdHTTPRequestInfo; + AResponse: TIdHTTPResponseInfo; const AParams: TArray); +var + LUserId, LActive, LTrashed: Integer; + LQ: TFDQuery; + LObj: TJSONObject; +begin + try + LUserId := Authenticate(ARequest, AResponse); + except + on ESessionRejected do Exit; + end; + + LActive := 0; + LTrashed := 0; + DB.Lock; + try + LQ := TFDQuery.Create(nil); + try + LQ.Connection := DB.Connection; + LQ.SQL.Text := + 'SELECT deleted, COUNT(*) AS cnt FROM vault_entries ' + + 'WHERE user_id = :uid GROUP BY deleted'; + LQ.ParamByName('uid').AsInteger := LUserId; + LQ.Open; + while not LQ.Eof do + begin + if LQ.FieldByName('deleted').AsInteger = 0 then + LActive := LQ.FieldByName('cnt').AsInteger + else + LTrashed := LQ.FieldByName('cnt').AsInteger; + LQ.Next; + end; + finally + LQ.Free; + end; + finally + DB.Unlock; + end; + + LObj := TJSONObject.Create; + LObj.AddPair('active', TJSONNumber.Create(LActive)); + LObj.AddPair('trashed', TJSONNumber.Create(LTrashed)); + TJSONHelper.SendJSON(AResponse, LObj); +end; + initialization // /entries/trash/empty must be registered BEFORE /entries/{id} to win the regex match. // Same logic for /entries/bulk-import — register before the catch-all /entries/{id}. @@ -611,6 +666,7 @@ initialization Router.Register('POST', '/entries/bulk-import', HandleBulkImport); Router.Register('POST', '/entries/(\d+)/restore', HandleRestoreEntry); Router.Register('POST', '/entries/(\d+)/favorite', HandleToggleFavorite); + Router.Register('GET', '/entries/count', HandleEntriesCount); Router.Register('GET', '/entries', HandleGetEntries); Router.Register('POST', '/entries', HandleCreateEntry); Router.Register('PUT', '/entries/(\d+)', HandleUpdateEntry); diff --git a/delphi-backend/Handlers/PM.Handler.Recovery.pas b/delphi-backend/Handlers/PM.Handler.Recovery.pas index 572e3e5..193e77d 100644 --- a/delphi-backend/Handlers/PM.Handler.Recovery.pas +++ b/delphi-backend/Handlers/PM.Handler.Recovery.pas @@ -33,7 +33,7 @@ implementation uses System.SysUtils, System.JSON, - FireDAC.Comp.Client, FireDAC.Stan.Param, + Data.DB, FireDAC.Comp.Client, FireDAC.Stan.Param, IdCustomHTTPServer, PM.Router, PM.JSON, PM.Database, PM.Crypto, PM.Session, PM.Audit, PM.RateLimit; @@ -122,19 +122,21 @@ begin LConfigured := False; LCreatedAt := ''; + var RemainingUses: Integer := 0; DB.Lock; try LQ := TFDQuery.Create(nil); try LQ.Connection := DB.Connection; LQ.SQL.Text := - 'SELECT created_at FROM recovery_keys WHERE user_id = :uid'; + 'SELECT created_at, remaining_uses FROM recovery_keys WHERE user_id = :uid'; LQ.ParamByName('uid').AsInteger := LUserId; LQ.Open; if not LQ.IsEmpty then begin LConfigured := True; LCreatedAt := LQ.FieldByName('created_at').AsString; + RemainingUses := LQ.FieldByName('remaining_uses').AsInteger; end; finally LQ.Free; @@ -146,6 +148,7 @@ begin LObj := TJSONObject.Create; LObj.AddPair('configured', TJSONBool.Create(LConfigured)); if LConfigured then LObj.AddPair('created_at', LCreatedAt); + if LConfigured then LObj.AddPair('remaining_uses', TJSONNumber.Create(RemainingUses)); TJSONHelper.SendJSON(AResponse, LObj); end; @@ -208,8 +211,8 @@ begin LQ.SQL.Text := 'INSERT INTO recovery_keys ' + - ' (user_id, code_hash, kdf_salt, wrapped_key, wrapped_iv) ' + - 'VALUES (:uid, :ch, :ks, :wk, :wi)'; + ' (user_id, code_hash, kdf_salt, wrapped_key, wrapped_iv, remaining_uses) ' + + 'VALUES (:uid, :ch, :ks, :wk, :wi, 5)'; LQ.ParamByName('uid').AsInteger := LUserId; LQ.ParamByName('ch').AsString := LCodeHash; LQ.ParamByName('ks').AsString := LKdfSalt; @@ -271,7 +274,7 @@ var LBody, LObj: TJSONObject; LUser, LCode, LCodeHash, LIP, LStoredHash, LKdfSalt, LWrappedKey, LWrappedIv, LSalt, LToken, LCSRF: string; - LUserId, LKdfIters: Integer; + LUserId, LKdfIters, LCurrentUses, LNewUses: Integer; LQ: TFDQuery; begin LIP := GetClientIP(ARequest); @@ -307,7 +310,7 @@ begin // Join to users to look up by username + verify the code in one shot. LQ.SQL.Text := 'SELECT u.id, u.salt, u.kdf_iterations, ' + - ' rk.code_hash, rk.kdf_salt, rk.wrapped_key, rk.wrapped_iv ' + + ' rk.code_hash, rk.kdf_salt, rk.wrapped_key, rk.wrapped_iv, rk.remaining_uses ' + 'FROM users u ' + 'LEFT JOIN recovery_keys rk ON rk.user_id = u.id ' + 'WHERE u.username = :u'; @@ -315,20 +318,19 @@ begin LQ.Open; if LQ.IsEmpty then begin - // User doesn't exist OR has no recovery key configured. Same error - // either way to avoid leaking which. RecordAttempt(LIP); RecordFailedAccountAttempt(LUser, LIP); TJSONHelper.SendError(AResponse, 401, 'Invalid username or recovery code'); Exit; end; - LUserId := LQ.FieldByName('id').AsInteger; - LSalt := LQ.FieldByName('salt').AsString; - LKdfIters := LQ.FieldByName('kdf_iterations').AsInteger; - LStoredHash := LQ.FieldByName('code_hash').AsString; - LKdfSalt := LQ.FieldByName('kdf_salt').AsString; - LWrappedKey := LQ.FieldByName('wrapped_key').AsString; - LWrappedIv := LQ.FieldByName('wrapped_iv').AsString; + LUserId := LQ.FieldByName('id').AsInteger; + LSalt := LQ.FieldByName('salt').AsString; + LKdfIters := LQ.FieldByName('kdf_iterations').AsInteger; + LStoredHash := LQ.FieldByName('code_hash').AsString; + LKdfSalt := LQ.FieldByName('kdf_salt').AsString; + LWrappedKey := LQ.FieldByName('wrapped_key').AsString; + LWrappedIv := LQ.FieldByName('wrapped_iv').AsString; + LCurrentUses := LQ.FieldByName('remaining_uses').AsInteger; finally LQ.Free; end; @@ -361,13 +363,24 @@ begin Exit; end; - // Code matches. Consume (delete the row) inside the same lock so the - // single-use guarantee holds even under concurrent requests. + // Code matches. Decrement remaining_uses ; if it drops to 0, delete + // the row (last use). The row is also deleted when the user + // successfully changes their master password (in PM.Handler.Auth). + LNewUses := LCurrentUses - 1; LQ := TFDQuery.Create(nil); try LQ.Connection := DB.Connection; - LQ.SQL.Text := 'DELETE FROM recovery_keys WHERE user_id = :uid'; - LQ.ParamByName('uid').AsInteger := LUserId; + if LNewUses <= 0 then + begin + LQ.SQL.Text := 'DELETE FROM recovery_keys WHERE user_id = :uid'; + LQ.ParamByName('uid').AsInteger := LUserId; + end + else + begin + LQ.SQL.Text := 'UPDATE recovery_keys SET remaining_uses = :u WHERE user_id = :uid'; + LQ.ParamByName('u').AsInteger := LNewUses; + LQ.ParamByName('uid').AsInteger := LUserId; + end; LQ.ExecSQL; finally LQ.Free; @@ -391,6 +404,7 @@ begin LObj.AddPair('wrappedKey', LWrappedKey); LObj.AddPair('wrappedIv', LWrappedIv); LObj.AddPair('kdfSalt', LKdfSalt); + LObj.AddPair('remainingUses', TJSONNumber.Create(LNewUses)); TJSONHelper.SendJSON(AResponse, LObj); end; diff --git a/delphi-backend/Handlers/PM.Handler.Settings.pas b/delphi-backend/Handlers/PM.Handler.Settings.pas new file mode 100644 index 0000000..e1d7262 --- /dev/null +++ b/delphi-backend/Handlers/PM.Handler.Settings.pas @@ -0,0 +1,113 @@ +unit PM.Handler.Settings; + +(* + GET /settings -> {} + PUT /settings body: {} -> {message:"OK"} + + Persists a per-user preferences blob (users.settings_json). The server + treats the body as opaque JSON — schema lives in the JS layer. Any client + reading it should tolerate unknown keys for forward compatibility. + + Device-specific toggles (quick-unlock DPAPI, autofill hotkey) deliberately + stay in localStorage on the client and are NOT included in this blob. +*) + +interface + +implementation + +uses + System.SysUtils, System.JSON, System.Classes, + Data.DB, FireDAC.Comp.Client, FireDAC.Stan.Param, + IdCustomHTTPServer, + PM.Router, PM.JSON, PM.Session, PM.Database; + +procedure HandleGetSettings(ARequest: TIdHTTPRequestInfo; + AResponse: TIdHTTPResponseInfo; const AParams: TArray); +var + LUserId: Integer; + LQ: TFDQuery; + LRaw: string; + LObj: TJSONValue; +begin + LUserId := Authenticate(ARequest, AResponse); + + DB.Lock; + try + LQ := TFDQuery.Create(nil); + try + LQ.Connection := DB.Connection; + LQ.SQL.Text := 'SELECT settings_json FROM users WHERE id = :uid'; + LQ.ParamByName('uid').AsInteger := LUserId; + LQ.Open; + if LQ.IsEmpty then + LRaw := '{}' + else + LRaw := LQ.FieldByName('settings_json').AsString; + finally + LQ.Free; + end; + finally + DB.Unlock; + end; + + if Trim(LRaw) = '' then LRaw := '{}'; + + // Validate so a corrupt row doesn't return malformed JSON to the client. + LObj := TJSONObject.ParseJSONValue(LRaw); + if LObj = nil then LObj := TJSONObject.Create; + TJSONHelper.SendJSON(AResponse, LObj); // SendJSON frees the object +end; + +procedure HandlePutSettings(ARequest: TIdHTTPRequestInfo; + AResponse: TIdHTTPResponseInfo; const AParams: TArray); +var + LUserId: Integer; + LBody: TJSONObject; + LSerialized: string; + LQ: TFDQuery; +begin + LUserId := Authenticate(ARequest, AResponse); + RequireCSRF(ARequest, AResponse, LUserId); + + LBody := TJSONHelper.ReadBody(ARequest); + try + // Re-serialize to a canonical compact form (strips comments / extra + // whitespace, and guarantees what we store is valid JSON). + LSerialized := LBody.ToJSON; + finally + LBody.Free; + end; + + // Soft cap to protect the row from a runaway client (typical settings + // blob is a few hundred bytes; 16 KB leaves room for future flags). + if Length(LSerialized) > 16384 then + begin + TJSONHelper.SendError(AResponse, 413, 'Settings payload too large'); + Exit; + end; + + DB.Lock; + try + LQ := TFDQuery.Create(nil); + try + LQ.Connection := DB.Connection; + LQ.SQL.Text := 'UPDATE users SET settings_json = :s WHERE id = :uid'; + LQ.ParamByName('s').AsString := LSerialized; + LQ.ParamByName('uid').AsInteger := LUserId; + LQ.ExecSQL; + finally + LQ.Free; + end; + finally + DB.Unlock; + end; + + TJSONHelper.SendOK(AResponse); +end; + +initialization + Router.Register('GET', '/settings', HandleGetSettings); + Router.Register('PUT', '/settings', HandlePutSettings); + +end. diff --git a/delphi-backend/PMServer.dpr b/delphi-backend/PMServer.dpr index 572e081..53fef5f 100644 --- a/delphi-backend/PMServer.dpr +++ b/delphi-backend/PMServer.dpr @@ -3,6 +3,7 @@ program PMServer; uses System.StartUpCopy, FMX.Forms, + PM.SingleInstance in 'Source\PM.SingleInstance.pas', UMainForm in 'UMainForm.pas' {MainForm}, PM.JSON in 'Source\PM.JSON.pas', PM.Database in 'Source\PM.Database.pas', @@ -16,17 +17,27 @@ uses PM.HTTPServer in 'Source\PM.HTTPServer.pas', PM.Bridge in 'Source\PM.Bridge.pas', PM.QuickUnlock in 'Source\PM.QuickUnlock.pas', + PM.UserPrefs in 'Source\PM.UserPrefs.pas', + PM.ProcessLockdown in 'Source\PM.ProcessLockdown.pas', PM.Handler.Ping in 'Handlers\PM.Handler.Ping.pas', PM.Handler.Auth in 'Handlers\PM.Handler.Auth.pas', PM.Handler.Folders in 'Handlers\PM.Handler.Folders.pas', PM.Handler.Entries in 'Handlers\PM.Handler.Entries.pas', PM.Handler.Passkey in 'Handlers\PM.Handler.Passkey.pas', - PM.Handler.Recovery in 'Handlers\PM.Handler.Recovery.pas'; + PM.Handler.Recovery in 'Handlers\PM.Handler.Recovery.pas', + PM.Handler.Audit in 'Handlers\PM.Handler.Audit.pas', + PM.Handler.Settings in 'Handlers\PM.Handler.Settings.pas'; {$R *.res} {$R assets\assets.res} begin + // Single-instance: if another PMServer is running, bring it to front + // (it'll handle WM_PMSHOW on its message-only window) and exit. Avoids + // two icons in the tray and two HTTP servers fighting for the port. + if not PM.SingleInstance.AcquireOrSignal then + Exit; + Application.Initialize; Application.CreateForm(TMainForm, MainForm); Application.Run; diff --git a/delphi-backend/PMServer.dproj b/delphi-backend/PMServer.dproj index 4dff459..3b37da9 100644 --- a/delphi-backend/PMServer.dproj +++ b/delphi-backend/PMServer.dproj @@ -1,272 +1,1250 @@ - - - {59A8733F-111A-41EC-80BE-9848275FC80D} - PMServer.dpr - True - Debug - 693249 - Application - FMX - 20.1 - Win32 - - - true - - - true - Base - true - - - true - Base - true - - - true - Base - true - - - true - Base - true - - - true - Base - true - - - true - Base - true - - - true - Cfg_1 - true - true - - - true - Base - true - - - true - Cfg_2 - true - true - - - true - Cfg_2 - true - true - - - true - Cfg_2 - true - true - - - true - Cfg_2 - true - true - - - true - Cfg_2 - true - true - - - false - false - false - false - false - 00400000 - PMServer - 1036 - CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=;CFBundleName= - System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) - $(BDS)\bin\delphi_PROJECTICON.ico - $(BDS)\bin\delphi_PROJECTICNS.icns - - - package=com.embarcadero.$(MSBuildProjectName);label=$(MSBuildProjectName);versionCode=1;versionName=1.0.0;persistent=False;restoreAnyVersion=False;installLocation=auto;largeHeap=False;theme=TitleBar;hardwareAccelerated=true;apiKey= - Debug - true - $(BDS)\bin\Artwork\Android\FM_LauncherIcon_36x36.png - $(BDS)\bin\Artwork\Android\FM_LauncherIcon_48x48.png - $(BDS)\bin\Artwork\Android\FM_LauncherIcon_72x72.png - $(BDS)\bin\Artwork\Android\FM_LauncherIcon_96x96.png - $(BDS)\bin\Artwork\Android\FM_LauncherIcon_144x144.png - $(BDS)\bin\Artwork\Android\FM_SplashImage_426x320.png - $(BDS)\bin\Artwork\Android\FM_SplashImage_470x320.png - $(BDS)\bin\Artwork\Android\FM_SplashImage_640x480.png - $(BDS)\bin\Artwork\Android\FM_SplashImage_960x720.png - true - true - true - true - true - true - true - true - true - true - $(BDS)\bin\Artwork\Android\FM_NotificationIcon_24x24.png - $(BDS)\bin\Artwork\Android\FM_NotificationIcon_36x36.png - $(BDS)\bin\Artwork\Android\FM_NotificationIcon_48x48.png - $(BDS)\bin\Artwork\Android\FM_NotificationIcon_72x72.png - $(BDS)\bin\Artwork\Android\FM_NotificationIcon_96x96.png - $(BDS)\bin\Artwork\Android\FM_LauncherIcon_192x192.png - activity-1.7.2.dex.jar;annotation-experimental-1.3.0.dex.jar;annotation-jvm-1.6.0.dex.jar;annotations-13.0.dex.jar;appcompat-1.2.0.dex.jar;appcompat-resources-1.2.0.dex.jar;billing-6.0.1.dex.jar;biometric-1.1.0.dex.jar;browser-1.4.0.dex.jar;cloud-messaging.dex.jar;collection-1.1.0.dex.jar;concurrent-futures-1.1.0.dex.jar;core-1.10.1.dex.jar;core-common-2.2.0.dex.jar;core-ktx-1.10.1.dex.jar;core-runtime-2.2.0.dex.jar;cursoradapter-1.0.0.dex.jar;customview-1.0.0.dex.jar;documentfile-1.0.0.dex.jar;drawerlayout-1.0.0.dex.jar;error_prone_annotations-2.9.0.dex.jar;exifinterface-1.3.6.dex.jar;firebase-annotations-16.2.0.dex.jar;firebase-common-20.3.1.dex.jar;firebase-components-17.1.0.dex.jar;firebase-datatransport-18.1.7.dex.jar;firebase-encoders-17.0.0.dex.jar;firebase-encoders-json-18.0.0.dex.jar;firebase-encoders-proto-16.0.0.dex.jar;firebase-iid-interop-17.1.0.dex.jar;firebase-installations-17.1.3.dex.jar;firebase-installations-interop-17.1.0.dex.jar;firebase-measurement-connector-19.0.0.dex.jar;firebase-messaging-23.1.2.dex.jar;fragment-1.2.5.dex.jar;google-play-licensing.dex.jar;interpolator-1.0.0.dex.jar;javax.inject-1.dex.jar;kotlin-stdlib-1.8.22.dex.jar;kotlin-stdlib-common-1.8.22.dex.jar;kotlin-stdlib-jdk7-1.8.22.dex.jar;kotlin-stdlib-jdk8-1.8.22.dex.jar;kotlinx-coroutines-android-1.6.4.dex.jar;kotlinx-coroutines-core-jvm-1.6.4.dex.jar;legacy-support-core-utils-1.0.0.dex.jar;lifecycle-common-2.6.1.dex.jar;lifecycle-livedata-2.6.1.dex.jar;lifecycle-livedata-core-2.6.1.dex.jar;lifecycle-runtime-2.6.1.dex.jar;lifecycle-service-2.6.1.dex.jar;lifecycle-viewmodel-2.6.1.dex.jar;lifecycle-viewmodel-savedstate-2.6.1.dex.jar;listenablefuture-1.0.dex.jar;loader-1.0.0.dex.jar;localbroadcastmanager-1.0.0.dex.jar;okio-jvm-3.4.0.dex.jar;play-services-ads-22.2.0.dex.jar;play-services-ads-base-22.2.0.dex.jar;play-services-ads-identifier-18.0.0.dex.jar;play-services-ads-lite-22.2.0.dex.jar;play-services-appset-16.0.1.dex.jar;play-services-base-18.1.0.dex.jar;play-services-basement-18.1.0.dex.jar;play-services-cloud-messaging-17.0.1.dex.jar;play-services-location-21.0.1.dex.jar;play-services-maps-18.1.0.dex.jar;play-services-measurement-base-20.1.2.dex.jar;play-services-measurement-sdk-api-20.1.2.dex.jar;play-services-stats-17.0.2.dex.jar;play-services-tasks-18.0.2.dex.jar;print-1.0.0.dex.jar;profileinstaller-1.3.0.dex.jar;room-common-2.2.5.dex.jar;room-runtime-2.2.5.dex.jar;savedstate-1.2.1.dex.jar;sqlite-2.1.0.dex.jar;sqlite-framework-2.1.0.dex.jar;startup-runtime-1.1.1.dex.jar;tracing-1.0.0.dex.jar;transport-api-3.0.0.dex.jar;transport-backend-cct-3.1.8.dex.jar;transport-runtime-3.1.8.dex.jar;user-messaging-platform-2.0.0.dex.jar;vectordrawable-1.1.0.dex.jar;vectordrawable-animated-1.1.0.dex.jar;versionedparcelable-1.1.1.dex.jar;viewpager-1.0.0.dex.jar;work-runtime-2.7.0.dex.jar - - - $(BDS)\bin\Artwork\Android\FM_LauncherIcon_192x192.png - activity-1.7.2.dex.jar;annotation-experimental-1.3.0.dex.jar;annotation-jvm-1.6.0.dex.jar;annotations-13.0.dex.jar;appcompat-1.2.0.dex.jar;appcompat-resources-1.2.0.dex.jar;billing-6.0.1.dex.jar;biometric-1.1.0.dex.jar;browser-1.4.0.dex.jar;cloud-messaging.dex.jar;collection-1.1.0.dex.jar;concurrent-futures-1.1.0.dex.jar;core-1.10.1.dex.jar;core-common-2.2.0.dex.jar;core-ktx-1.10.1.dex.jar;core-runtime-2.2.0.dex.jar;cursoradapter-1.0.0.dex.jar;customview-1.0.0.dex.jar;documentfile-1.0.0.dex.jar;drawerlayout-1.0.0.dex.jar;error_prone_annotations-2.9.0.dex.jar;exifinterface-1.3.6.dex.jar;firebase-annotations-16.2.0.dex.jar;firebase-common-20.3.1.dex.jar;firebase-components-17.1.0.dex.jar;firebase-datatransport-18.1.7.dex.jar;firebase-encoders-17.0.0.dex.jar;firebase-encoders-json-18.0.0.dex.jar;firebase-encoders-proto-16.0.0.dex.jar;firebase-iid-interop-17.1.0.dex.jar;firebase-installations-17.1.3.dex.jar;firebase-installations-interop-17.1.0.dex.jar;firebase-measurement-connector-19.0.0.dex.jar;firebase-messaging-23.1.2.dex.jar;fragment-1.2.5.dex.jar;google-play-licensing.dex.jar;interpolator-1.0.0.dex.jar;javax.inject-1.dex.jar;kotlin-stdlib-1.8.22.dex.jar;kotlin-stdlib-common-1.8.22.dex.jar;kotlin-stdlib-jdk7-1.8.22.dex.jar;kotlin-stdlib-jdk8-1.8.22.dex.jar;kotlinx-coroutines-android-1.6.4.dex.jar;kotlinx-coroutines-core-jvm-1.6.4.dex.jar;legacy-support-core-utils-1.0.0.dex.jar;lifecycle-common-2.6.1.dex.jar;lifecycle-livedata-2.6.1.dex.jar;lifecycle-livedata-core-2.6.1.dex.jar;lifecycle-runtime-2.6.1.dex.jar;lifecycle-service-2.6.1.dex.jar;lifecycle-viewmodel-2.6.1.dex.jar;lifecycle-viewmodel-savedstate-2.6.1.dex.jar;listenablefuture-1.0.dex.jar;loader-1.0.0.dex.jar;localbroadcastmanager-1.0.0.dex.jar;okio-jvm-3.4.0.dex.jar;play-services-ads-22.2.0.dex.jar;play-services-ads-base-22.2.0.dex.jar;play-services-ads-identifier-18.0.0.dex.jar;play-services-ads-lite-22.2.0.dex.jar;play-services-appset-16.0.1.dex.jar;play-services-base-18.1.0.dex.jar;play-services-basement-18.1.0.dex.jar;play-services-cloud-messaging-17.0.1.dex.jar;play-services-location-21.0.1.dex.jar;play-services-maps-18.1.0.dex.jar;play-services-measurement-base-20.1.2.dex.jar;play-services-measurement-sdk-api-20.1.2.dex.jar;play-services-stats-17.0.2.dex.jar;play-services-tasks-18.0.2.dex.jar;print-1.0.0.dex.jar;profileinstaller-1.3.0.dex.jar;room-common-2.2.5.dex.jar;room-runtime-2.2.5.dex.jar;savedstate-1.2.1.dex.jar;sqlite-2.1.0.dex.jar;sqlite-framework-2.1.0.dex.jar;startup-runtime-1.1.1.dex.jar;tracing-1.0.0.dex.jar;transport-api-3.0.0.dex.jar;transport-backend-cct-3.1.8.dex.jar;transport-runtime-3.1.8.dex.jar;user-messaging-platform-2.0.0.dex.jar;vectordrawable-1.1.0.dex.jar;vectordrawable-animated-1.1.0.dex.jar;versionedparcelable-1.1.1.dex.jar;viewpager-1.0.0.dex.jar;work-runtime-2.7.0.dex.jar - - - $(BDS)\bin\Artwork\iOS\iPhone\FM_SettingIcon_87x87.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_180x180.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_120x120.png - $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_167x167.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_2x.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImageDark_2x.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_3x.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImageDark_3x.png - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImage_2x.png - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageDark_2x.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_1024x1024.png - - - Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) - Debug - true - CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) - 1033 - $(BDS)\bin\default_app.manifest - $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png - $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png - - - $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png - $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png - - - RELEASE;$(DCC_Define) - 0 - false - 0 - - - PerMonitorV2 - - - DEBUG;$(DCC_Define) - false - true - true - true - - - Debug - - - Debug - - - Debug - - - Debug - - - PerMonitorV2 - true - 1033 - CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) - - - - - MainSource - - -
MainForm
-
- - - - - - - - - - - - - - - - - - - - Base - - - Cfg_1 - Base - - - Cfg_2 - Base - -
- - Delphi.Personality.12 - - - - - PMServer.dpr - - - Microsoft Office 2000 Sample Automation Server Wrapper Components - Microsoft Office XP Sample Automation Server Wrapper Components - - - - False - True - True - True - True - True - True - False - - - 12 - - - - - "Z:\password-manager\delphi-backend\assets\BuildAssets.cmd" - False - - False - - False - -
+ + + {59A8733F-111A-41EC-80BE-9848275FC80D} + PMServer.dpr + True + Debug + 693249 + Application + FMX + 20.1 + Win32 + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_1 + true + true + + + true + Base + true + + + true + Cfg_2 + true + true + + + true + Cfg_2 + true + true + + + true + Cfg_2 + true + true + + + true + Cfg_2 + true + true + + + true + Cfg_2 + true + true + + + false + false + false + false + false + 00400000 + PMServer + 1036 + CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=;CFBundleName= + System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) + $(BDS)\bin\delphi_PROJECTICON.ico + $(BDS)\bin\delphi_PROJECTICNS.icns + + + package=com.embarcadero.$(MSBuildProjectName);label=$(MSBuildProjectName);versionCode=1;versionName=1.0.0;persistent=False;restoreAnyVersion=False;installLocation=auto;largeHeap=False;theme=TitleBar;hardwareAccelerated=true;apiKey= + Debug + true + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_36x36.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_48x48.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_72x72.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_96x96.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_144x144.png + $(BDS)\bin\Artwork\Android\FM_SplashImage_426x320.png + $(BDS)\bin\Artwork\Android\FM_SplashImage_470x320.png + $(BDS)\bin\Artwork\Android\FM_SplashImage_640x480.png + $(BDS)\bin\Artwork\Android\FM_SplashImage_960x720.png + true + true + true + true + true + true + true + true + true + true + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_24x24.png + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_36x36.png + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_48x48.png + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_72x72.png + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_96x96.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_192x192.png + activity-1.7.2.dex.jar;annotation-experimental-1.3.0.dex.jar;annotation-jvm-1.6.0.dex.jar;annotations-13.0.dex.jar;appcompat-1.2.0.dex.jar;appcompat-resources-1.2.0.dex.jar;billing-6.0.1.dex.jar;biometric-1.1.0.dex.jar;browser-1.4.0.dex.jar;cloud-messaging.dex.jar;collection-1.1.0.dex.jar;concurrent-futures-1.1.0.dex.jar;core-1.10.1.dex.jar;core-common-2.2.0.dex.jar;core-ktx-1.10.1.dex.jar;core-runtime-2.2.0.dex.jar;cursoradapter-1.0.0.dex.jar;customview-1.0.0.dex.jar;documentfile-1.0.0.dex.jar;drawerlayout-1.0.0.dex.jar;error_prone_annotations-2.9.0.dex.jar;exifinterface-1.3.6.dex.jar;firebase-annotations-16.2.0.dex.jar;firebase-common-20.3.1.dex.jar;firebase-components-17.1.0.dex.jar;firebase-datatransport-18.1.7.dex.jar;firebase-encoders-17.0.0.dex.jar;firebase-encoders-json-18.0.0.dex.jar;firebase-encoders-proto-16.0.0.dex.jar;firebase-iid-interop-17.1.0.dex.jar;firebase-installations-17.1.3.dex.jar;firebase-installations-interop-17.1.0.dex.jar;firebase-measurement-connector-19.0.0.dex.jar;firebase-messaging-23.1.2.dex.jar;fragment-1.2.5.dex.jar;google-play-licensing.dex.jar;interpolator-1.0.0.dex.jar;javax.inject-1.dex.jar;kotlin-stdlib-1.8.22.dex.jar;kotlin-stdlib-common-1.8.22.dex.jar;kotlin-stdlib-jdk7-1.8.22.dex.jar;kotlin-stdlib-jdk8-1.8.22.dex.jar;kotlinx-coroutines-android-1.6.4.dex.jar;kotlinx-coroutines-core-jvm-1.6.4.dex.jar;legacy-support-core-utils-1.0.0.dex.jar;lifecycle-common-2.6.1.dex.jar;lifecycle-livedata-2.6.1.dex.jar;lifecycle-livedata-core-2.6.1.dex.jar;lifecycle-runtime-2.6.1.dex.jar;lifecycle-service-2.6.1.dex.jar;lifecycle-viewmodel-2.6.1.dex.jar;lifecycle-viewmodel-savedstate-2.6.1.dex.jar;listenablefuture-1.0.dex.jar;loader-1.0.0.dex.jar;localbroadcastmanager-1.0.0.dex.jar;okio-jvm-3.4.0.dex.jar;play-services-ads-22.2.0.dex.jar;play-services-ads-base-22.2.0.dex.jar;play-services-ads-identifier-18.0.0.dex.jar;play-services-ads-lite-22.2.0.dex.jar;play-services-appset-16.0.1.dex.jar;play-services-base-18.1.0.dex.jar;play-services-basement-18.1.0.dex.jar;play-services-cloud-messaging-17.0.1.dex.jar;play-services-location-21.0.1.dex.jar;play-services-maps-18.1.0.dex.jar;play-services-measurement-base-20.1.2.dex.jar;play-services-measurement-sdk-api-20.1.2.dex.jar;play-services-stats-17.0.2.dex.jar;play-services-tasks-18.0.2.dex.jar;print-1.0.0.dex.jar;profileinstaller-1.3.0.dex.jar;room-common-2.2.5.dex.jar;room-runtime-2.2.5.dex.jar;savedstate-1.2.1.dex.jar;sqlite-2.1.0.dex.jar;sqlite-framework-2.1.0.dex.jar;startup-runtime-1.1.1.dex.jar;tracing-1.0.0.dex.jar;transport-api-3.0.0.dex.jar;transport-backend-cct-3.1.8.dex.jar;transport-runtime-3.1.8.dex.jar;user-messaging-platform-2.0.0.dex.jar;vectordrawable-1.1.0.dex.jar;vectordrawable-animated-1.1.0.dex.jar;versionedparcelable-1.1.1.dex.jar;viewpager-1.0.0.dex.jar;work-runtime-2.7.0.dex.jar + + + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_192x192.png + activity-1.7.2.dex.jar;annotation-experimental-1.3.0.dex.jar;annotation-jvm-1.6.0.dex.jar;annotations-13.0.dex.jar;appcompat-1.2.0.dex.jar;appcompat-resources-1.2.0.dex.jar;billing-6.0.1.dex.jar;biometric-1.1.0.dex.jar;browser-1.4.0.dex.jar;cloud-messaging.dex.jar;collection-1.1.0.dex.jar;concurrent-futures-1.1.0.dex.jar;core-1.10.1.dex.jar;core-common-2.2.0.dex.jar;core-ktx-1.10.1.dex.jar;core-runtime-2.2.0.dex.jar;cursoradapter-1.0.0.dex.jar;customview-1.0.0.dex.jar;documentfile-1.0.0.dex.jar;drawerlayout-1.0.0.dex.jar;error_prone_annotations-2.9.0.dex.jar;exifinterface-1.3.6.dex.jar;firebase-annotations-16.2.0.dex.jar;firebase-common-20.3.1.dex.jar;firebase-components-17.1.0.dex.jar;firebase-datatransport-18.1.7.dex.jar;firebase-encoders-17.0.0.dex.jar;firebase-encoders-json-18.0.0.dex.jar;firebase-encoders-proto-16.0.0.dex.jar;firebase-iid-interop-17.1.0.dex.jar;firebase-installations-17.1.3.dex.jar;firebase-installations-interop-17.1.0.dex.jar;firebase-measurement-connector-19.0.0.dex.jar;firebase-messaging-23.1.2.dex.jar;fragment-1.2.5.dex.jar;google-play-licensing.dex.jar;interpolator-1.0.0.dex.jar;javax.inject-1.dex.jar;kotlin-stdlib-1.8.22.dex.jar;kotlin-stdlib-common-1.8.22.dex.jar;kotlin-stdlib-jdk7-1.8.22.dex.jar;kotlin-stdlib-jdk8-1.8.22.dex.jar;kotlinx-coroutines-android-1.6.4.dex.jar;kotlinx-coroutines-core-jvm-1.6.4.dex.jar;legacy-support-core-utils-1.0.0.dex.jar;lifecycle-common-2.6.1.dex.jar;lifecycle-livedata-2.6.1.dex.jar;lifecycle-livedata-core-2.6.1.dex.jar;lifecycle-runtime-2.6.1.dex.jar;lifecycle-service-2.6.1.dex.jar;lifecycle-viewmodel-2.6.1.dex.jar;lifecycle-viewmodel-savedstate-2.6.1.dex.jar;listenablefuture-1.0.dex.jar;loader-1.0.0.dex.jar;localbroadcastmanager-1.0.0.dex.jar;okio-jvm-3.4.0.dex.jar;play-services-ads-22.2.0.dex.jar;play-services-ads-base-22.2.0.dex.jar;play-services-ads-identifier-18.0.0.dex.jar;play-services-ads-lite-22.2.0.dex.jar;play-services-appset-16.0.1.dex.jar;play-services-base-18.1.0.dex.jar;play-services-basement-18.1.0.dex.jar;play-services-cloud-messaging-17.0.1.dex.jar;play-services-location-21.0.1.dex.jar;play-services-maps-18.1.0.dex.jar;play-services-measurement-base-20.1.2.dex.jar;play-services-measurement-sdk-api-20.1.2.dex.jar;play-services-stats-17.0.2.dex.jar;play-services-tasks-18.0.2.dex.jar;print-1.0.0.dex.jar;profileinstaller-1.3.0.dex.jar;room-common-2.2.5.dex.jar;room-runtime-2.2.5.dex.jar;savedstate-1.2.1.dex.jar;sqlite-2.1.0.dex.jar;sqlite-framework-2.1.0.dex.jar;startup-runtime-1.1.1.dex.jar;tracing-1.0.0.dex.jar;transport-api-3.0.0.dex.jar;transport-backend-cct-3.1.8.dex.jar;transport-runtime-3.1.8.dex.jar;user-messaging-platform-2.0.0.dex.jar;vectordrawable-1.1.0.dex.jar;vectordrawable-animated-1.1.0.dex.jar;versionedparcelable-1.1.1.dex.jar;viewpager-1.0.0.dex.jar;work-runtime-2.7.0.dex.jar + + + $(BDS)\bin\Artwork\iOS\iPhone\FM_SettingIcon_87x87.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_180x180.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_120x120.png + $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_167x167.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_2x.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImageDark_2x.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_3x.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImageDark_3x.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImage_2x.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageDark_2x.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_1024x1024.png + + + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + Debug + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) + 1033 + $(BDS)\bin\default_app.manifest + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png + + + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png + + + RELEASE;$(DCC_Define) + 0 + false + 0 + + + PerMonitorV2 + + + DEBUG;$(DCC_Define) + false + true + true + true + + + Debug + + + Debug + + + Debug + + + Debug + + + PerMonitorV2 + true + 1033 + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) + + PMServer_Icon1.ico + app.png + app.png + + + + MainSource + + +
MainForm
+
+ + + + + + + + + + + + + + + + + + + + + + + + Base + + + Cfg_1 + Base + + + Cfg_2 + Base + +
+ + Delphi.Personality.12 + + + + + PMServer.dpr + + + Microsoft Office 2000 Sample Automation Server Wrapper Components + Microsoft Office XP Sample Automation Server Wrapper Components + + + + False + True + True + True + True + True + True + False + + + + + true + + + + + true + + + + + true + + + + + Assets\ + Logo44x44.png + true + + + + + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + classes + 64 + + + classes + 64 + + + + + res\xml + 1 + + + res\xml + 1 + + + + + library\lib\armeabi + 1 + + + library\lib\armeabi + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + library\lib\mips + 1 + + + library\lib\mips + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-anydpi-v21 + 1 + + + res\drawable-anydpi-v21 + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\values-v21 + 1 + + + res\values-v21 + 1 + + + + + res\values-v31 + 1 + + + res\values-v31 + 1 + + + + + res\drawable-anydpi-v26 + 1 + + + res\drawable-anydpi-v26 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-anydpi-v33 + 1 + + + res\drawable-anydpi-v33 + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\values-night-v21 + 1 + + + res\values-night-v21 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-ldpi + 1 + + + res\drawable-ldpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-small + 1 + + + res\drawable-small + 1 + + + + + res\drawable-normal + 1 + + + res\drawable-normal + 1 + + + + + res\drawable-large + 1 + + + res\drawable-large + 1 + + + + + res\drawable-xlarge + 1 + + + res\drawable-xlarge + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\drawable-anydpi-v24 + 1 + + + res\drawable-anydpi-v24 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-night-anydpi-v21 + 1 + + + res\drawable-night-anydpi-v21 + 1 + + + + + res\drawable-anydpi-v31 + 1 + + + res\drawable-anydpi-v31 + 1 + + + + + res\drawable-night-anydpi-v31 + 1 + + + res\drawable-night-anydpi-v31 + 1 + + + + + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + Contents\MacOS + 1 + .framework + + + Contents\MacOS + 1 + .framework + + + Contents\MacOS + 1 + .framework + + + 0 + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + 0 + .dll;.bpl + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + 0 + .bpl + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + Contents\Resources\StartUp\ + 0 + + + Contents\Resources\StartUp\ + 0 + + + Contents\Resources\StartUp\ + 0 + + + 0 + + + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + ..\ + 1 + + + ..\ + 1 + + + ..\ + 1 + + + + + Contents + 1 + + + Contents + 1 + + + Contents + 1 + + + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + Contents\MacOS + 1 + + + Contents\MacOS + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + library\lib\armeabi-v7a + 1 + + + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + ..\ + 1 + + + ..\ + 1 + + + ..\ + 1 + + + + + 1 + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).launchscreen + 64 + + + ..\$(PROJECTNAME).launchscreen + 64 + + + + + 1 + + + 1 + + + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + + + + + + + + + + + + 12 + + + + + + "Z:\password-manager\delphi-backend\assets\BuildAssets.cmd" + False + + False + + False + +
diff --git a/delphi-backend/PMServer.res b/delphi-backend/PMServer.res index 2ac980a4b2b057687f9c6fb5203521b63f14ec0a..b317cb7d93110eaf28ad0cbd77dc724e79612a07 100644 GIT binary patch literal 105096 zcmeFa34GPnng5?oJ3oai_a-;@W&;xT%>}pG+E$(aPutpdI$e|`EUvXyWrvW3giS<5 z1j4@W`wm$lAqks`3o3}nqEKs>`OR-;oX*T|>?of9`#I-(zW19;0wF-qj(NR4+?$Zx z?>Xo5oaa2}oM(xOii*krN*VCB{@35w|M&AdU(^5BE2`Bu++X^eTY1|%>X%Uu@$bD+ zzu?_{QU4J2Q@+2Cf4|Rr`P%)BfBifvO8(w_hJC$D|8G>3e4zaw=SNWw@`JDOc_X5R zMLiicDC!A5?a8PCQIGS# zdQ5-L5I*lo{&tXl%+I3!o+muc|BTfC$`5-*UF-SV-umamJ%4jk)D8TOMO4(y`dL5Y zGX_WfDk_KPjG*U_L=EIG<*T7*+^U~Z|8F9n6#1N=My1d$^EXfMG#Gs?Y6Sj=jKJUU<>{(?>-mBC*8RcP-7ozo?svT52i|Yx2m1T3@ulC`@4xYl^M4=l z`|gKEJRqV0`rB|5oNuF|Ob6U=%@54CpRo{rk{%WHu`ep>%r#L_bu@V_Yg|=vbqN%^*hV;bb1w3w%v$V^1@h`F8CK{u|az_Fv!3KX3HU<`u@J?porX za-=-rpWmqn{NL(j2~*}5`BEGGgC_pR^bv8fW%FX6er-$KiIX*c^}(Kab!J~co!*RDCw9lGQ?+sG{W_kp*F5952NKo$ zdxGkJ@)^SoL4T+B^77q!}SXxp`)9^2VUy|=5iI$j;4PV$U*Dtzi_ zrJwePpYgkwLh58~=+ph{f*UtXO}MGCk6t(?HeuV$*2VkRT>GzYm;5iqn#*ThtK=ES zD&y4lnF(s#ufY=mRXR7QKBx_#4s-S;R6+V=# zCJ$(%UKr3;jea;)6+N7+iXIKACF6tY=uZ0alJp}<{r+IGIyxBLWw%pBSzXlsd9#E1KZiP~ zF@3tK(fu-1(LdVZhY8$f=}{rr{B`L^ts6RXPS$Ero+V%5M0;?$7d32IpH zpvuWiR$Hc}t3ze!>do?WwXHZE&XZNagF#jBa6lD2>Q@zu{LJqJ=652WAJ`HpKPx82 zZ`n4>uS(|n)Pk`-_4NHdHRyqO`jE&tg;Y*cn$CfBCb4ty^X!sO_^8)bfe(YG7}_8t{N$4WbW2GkHc

skAA+sTs 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 diff --git a/delphi-backend/PMServer_Icon.ico b/delphi-backend/PMServer_Icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1aa467674ad41f311c52f53edc063e5081f22fb2 GIT binary patch literal 102390 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 literal 0 HcmV?d00001 diff --git a/delphi-backend/PMServer_Icon1.ico b/delphi-backend/PMServer_Icon1.ico new file mode 100644 index 0000000000000000000000000000000000000000..1aa467674ad41f311c52f53edc063e5081f22fb2 GIT binary patch literal 102390 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 literal 0 HcmV?d00001 diff --git a/delphi-backend/Source/PM.Bridge.pas b/delphi-backend/Source/PM.Bridge.pas index 7faedbd..5647385 100644 --- a/delphi-backend/Source/PM.Bridge.pas +++ b/delphi-backend/Source/PM.Bridge.pas @@ -31,7 +31,8 @@ interface uses System.SysUtils, System.Classes, System.Math, FMX.Types, FMX.Forms, - Winapi.Windows, Winapi.ShellAPI, Winapi.Messages; + Winapi.Windows, Winapi.ShellAPI, Winapi.Messages, + PM.SingleInstance; type // ------------------------------------------------------------------------- @@ -44,12 +45,29 @@ type public constructor Create; destructor Destroy; override; + function ReadText: string; // Copy AText to the clipboard, excluding it from Win+V history. // AClearAfterMs = 0 disables auto-clear; default is 30 seconds. procedure SetText(const AText: string; AClearAfterMs: Integer = 30000); procedure Clear; end; + // Distinguishes the "fill everything" hotkey (Ctrl+Shift+L) from the + // "password only" hotkey (Ctrl+Shift+P). The host decides what to type + // based on this kind. + TAutofillKind = (akFull, akPasswordOnly); + + // Fired when an autofill hotkey is pressed. Args are the foreground + // window HWND and its title (captured before any focus change), plus + // the kind of fill requested. + // Declared as a method pointer (not TProc<>) because Delphi has no implicit + // conversion from "procedure of object" to "reference to procedure" — the + // host wires this with a regular form method (BridgeAutofillRequest). + TAutofillRequestEvent = procedure(AKind: TAutofillKind; + ATargetHWND: HWND; const ATitle: string) of object; + + TNewEntryHotkeyEvent = procedure(const AWindowTitle: string) of object; + // ------------------------------------------------------------------------- // TPMBridge // ------------------------------------------------------------------------- @@ -64,10 +82,28 @@ type FPowerNotify: THandle; // registration handle from PowerRegisterSuspendResumeNotification FSecureClipboard: TSecureClipboard; FBalloonShown: Boolean; + // Window placement captured at MinimizeToTray time. Replayed on + // RestoreFromTray so the window comes back in the same state + // (maximised / normal + position + size) as before hiding. + FSavedPlacement: TWindowPlacement; + FHasSavedPlacement: Boolean; FOnSystemLock: TProc; FOnTrayRestore: TProc; FOnLockRequest: TProc; FOnQuit: TProc; + // Autofill: global hotkeys → inject credentials into browser. Combos + // are user-configurable from Settings; defaults are Ctrl+Shift+L / + // Ctrl+Shift+P. We track which IDs are actually live so unregister + // doesn't blindly call UnregisterHotKey on unregistered IDs (which + // would set GetLastError noise during shutdown). + FAutofillRegistered: Boolean; + FAutofillFullActive: Boolean; + FAutofillPwdActive: Boolean; + FOnAutofillRequest: TAutofillRequestEvent; + FDebugHotkeyRegistered: Boolean; + FOnDebugHotkey: TProc; + FNewEntryHotkeyRegistered: Boolean; + FOnNewEntryHotkey: TNewEntryHotkeyEvent; procedure MsgWindowHandler(var AMsg: TMessage); procedure PrepareNid; procedure ShowTrayMenu; @@ -80,8 +116,35 @@ type procedure MinimizeToTray; // Restore main window and remove tray icon. procedure RestoreFromTray; + // Apply Windows dark-mode title bar to the main form. Win10 19044+ + // / Win11 only — no-op on older builds. Safe to call repeatedly. + procedure ApplyTitleBarTheme(ADark: Boolean); + + // Register the two autofill global hotkeys (full + password-only) with + // the given Win32 modifier flags (MOD_CONTROL/MOD_SHIFT/MOD_ALT/MOD_WIN + // bitmask) and virtual-key codes. Replaces any prior registration — + // safe to call repeatedly to swap combos at runtime. + // Returns True if both hotkeys registered successfully. If one or both + // failed (clash with another app), best-effort: whichever succeeded + // stays active. + function SetAutofillHotkeys(AFullMods, AFullVk, + APwdMods, APwdVk: Word): Boolean; + // Convenience wrapper: register the historical defaults (Ctrl+Shift+L + // and Ctrl+Shift+P). Used by the host on first start; runtime changes + // go through SetAutofillHotkeys. + procedure RegisterAutofillHotkey; + // Unregister both autofill hotkeys. + procedure UnregisterAutofillHotkey; + // Simulate username + Tab + password keystrokes into ATargetHWND. + // ATargetHWND = 0 → type into whatever window has focus. + // If AUsername is empty, only the password is typed (no Tab) — matches + // the password-only hotkey path AND avoids spurious Tab on entries + // without a stored username. + procedure ExecuteAutofill(ATargetHWND: HWND; + const AUsername, APassword: string); property SecureClipboard: TSecureClipboard read FSecureClipboard; property TrayAdded: Boolean read FTrayAdded; + property AutofillRegistered: Boolean read FAutofillRegistered; // Fired on main thread when Windows locks the session (WTS_SESSION_LOCK). property OnSystemLock: TProc read FOnSystemLock write FOnSystemLock; // Fired on main thread when the user clicks the tray icon. @@ -94,6 +157,17 @@ type // bridge does not call it itself, so the host stays in control of // shutdown order (server stop, save state, etc.). property OnQuit: TProc read FOnQuit write FOnQuit; + // Fired on main thread when the autofill hotkey fires. + // Args: (ATargetHWND, AWindowTitle). Handler calls ExecuteJavaScript + // to let JS match the title against vault entries. + property OnAutofillRequest: TAutofillRequestEvent + read FOnAutofillRequest write FOnAutofillRequest; + property OnDebugHotkey: TProc + read FOnDebugHotkey write FOnDebugHotkey; + // Fires on Ctrl+Shift+A. Arg = foreground window title (stripped of + // browser suffix by the JS layer before pre-fill). + property OnNewEntryHotkey: TNewEntryHotkeyEvent + read FOnNewEntryHotkey write FOnNewEntryHotkey; end; implementation @@ -130,6 +204,17 @@ const PBT_APMRESUMEAUTOMATIC = $0012; PBT_APMRESUMESUSPEND = $0007; +// Autofill hotkeys — Ctrl+Shift+L (full) and Ctrl+Shift+P (password only). +// IDs must not clash with other RegisterHotKey calls in this process; +// 42-43 are arbitrary and well outside the range used by FMX internals. +const + AUTOFILL_HOTKEY_ID_FULL = 42; // Ctrl+Shift+L → user + Tab + password + AUTOFILL_HOTKEY_ID_PWDONLY = 43; // Ctrl+Shift+P → password only + DEBUG_HOTKEY_ID = 44; // Ctrl+Shift+D → toggle debug panel + NEW_ENTRY_HOTKEY_ID = 45; // Ctrl+Shift+A → quick-add from window title + AF_MOD_CONTROL = $0002; // same value as MOD_CONTROL + AF_MOD_SHIFT = $0004; // same value as MOD_SHIFT + // Dynamic WTS function pointers — wtsapi32.dll is not guaranteed on all // Windows SKUs (e.g. minimal Server Core without Session Services), so // we load it at runtime and tolerate absence gracefully. @@ -256,6 +341,28 @@ begin end; end; +function TSecureClipboard.ReadText: string; +var + H: THandle; + P: PChar; +begin + Result := ''; + if not OpenClipboard(0) then Exit; + try + H := GetClipboardData(CF_UNICODETEXT); + if H = 0 then Exit; + P := PChar(GlobalLock(H)); + if P <> nil then + try + Result := P; + finally + GlobalUnlock(H); + end; + finally + CloseClipboard; + end; +end; + // ============================================================================= // TPMBridge // ============================================================================= @@ -273,6 +380,14 @@ begin PrepareNid; + // Add the tray icon eagerly so it's visible from app startup, regardless + // of whether the window is shown or hidden. Without this, the tray icon + // only appears the first time the user minimizes — meaning fresh-launch + // users can't lock/quit from the tray and discover the feature only by + // accident. NIM_DELETE is now only called at shutdown. + if Shell_NotifyIcon(NIM_ADD, @FNid) then + FTrayAdded := True; + // Session-lock detection (fails silently if wtsapi32.dll is absent). LoadWtsApi; if Assigned(_WTSRegister) then @@ -286,10 +401,21 @@ begin LoadPowerApi; if Assigned(_PowerRegister) then _PowerRegister(DEVICE_NOTIFY_WINDOW_HANDLE, FMsgWindow, FPowerNotify); + + FDebugHotkeyRegistered := RegisterHotKey(FMsgWindow, DEBUG_HOTKEY_ID, + AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('D')); + FNewEntryHotkeyRegistered := RegisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID, + AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('A')); end; destructor TPMBridge.Destroy; begin + if FDebugHotkeyRegistered then + UnregisterHotKey(FMsgWindow, DEBUG_HOTKEY_ID); + + if FNewEntryHotkeyRegistered then + UnregisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID); + if (FPowerNotify <> 0) and Assigned(_PowerUnregister) then _PowerUnregister(FPowerNotify); @@ -379,11 +505,8 @@ procedure TPMBridge.MinimizeToTray; var LFormHwnd, LAppHwnd: HWND; begin - if not FTrayAdded then - begin - if Shell_NotifyIcon(NIM_ADD, @FNid) then - FTrayAdded := True; - end; + // Tray icon is added at construction time and persists for the app's + // lifetime — no NIM_ADD here. // Extra safety: clear the clipboard immediately when the user minimizes, // rather than waiting for the 30s auto-clear timer to fire. A password @@ -394,6 +517,15 @@ begin LFormHwnd := MainFormHWND(FMainForm); LAppHwnd := FindFMXAppWindow; + // 0. Snapshot the window placement BEFORE hiding so RestoreFromTray can + // replay the exact same state (maximised / normal + size + position). + // Without this, ShowWindow(SW_RESTORE) below always returns to the + // "normal" state — a window that was maximised before hiding comes + // back un-maximised. + FillChar(FSavedPlacement, SizeOf(FSavedPlacement), 0); + FSavedPlacement.length := SizeOf(FSavedPlacement); + FHasSavedPlacement := GetWindowPlacement(LFormHwnd, @FSavedPlacement); + // 1. Hide the visible form via both FMX state and Win32 ShowWindow. // Keeps the form invisible to the user. FMainForm.Hide; @@ -442,12 +574,7 @@ procedure TPMBridge.RestoreFromTray; var LFormHwnd, LAppHwnd: HWND; begin - if FTrayAdded then - begin - Shell_NotifyIcon(NIM_DELETE, @FNid); - FTrayAdded := False; - end; - + // Tray icon stays in the tray — we only show the window again. LFormHwnd := MainFormHWND(FMainForm); LAppHwnd := FindFMXAppWindow; @@ -457,8 +584,23 @@ begin ShowWindow(LAppHwnd, SW_SHOW); FMainForm.Show; - ShowWindow(LFormHwnd, SW_SHOW); - ShowWindow(LFormHwnd, SW_RESTORE); + + // Restore to the exact pre-tray state (maximised/normal + size + pos). + // Falls back to SW_RESTORE if we never captured a placement (e.g. tray + // restore was triggered without a prior MinimizeToTray call). + if FHasSavedPlacement then + begin + // showCmd governs whether the window comes back maximised or normal; + // it's what SW_RESTORE clobbers. We force it ourselves. + if FSavedPlacement.showCmd = SW_SHOWMINIMIZED then + FSavedPlacement.showCmd := SW_SHOWNORMAL; // never restore as minimised + SetWindowPlacement(LFormHwnd, @FSavedPlacement); + end + else + begin + ShowWindow(LFormHwnd, SW_SHOW); + ShowWindow(LFormHwnd, SW_RESTORE); + end; SetForegroundWindow(LFormHwnd); end; @@ -543,9 +685,372 @@ begin // suspends — fast handler required (no UI prompts, no network). if AMsg.WParam = PBT_APMSUSPEND then if Assigned(FOnSystemLock) then FOnSystemLock(); + end + + else if (AMsg.Msg <> 0) and (AMsg.Msg = WM_PMShowMessage) then + begin + // A second instance was launched and PostMessage'd HWND_BROADCAST. + // Bring our window back to the front instead of letting that second + // process spawn its own UI. + if Assigned(FOnTrayRestore) then FOnTrayRestore(); + end + + else if (AMsg.Msg = WM_HOTKEY) and (AMsg.WParam = DEBUG_HOTKEY_ID) then + begin + if Assigned(FOnDebugHotkey) then FOnDebugHotkey(); + end + + else if (AMsg.Msg = WM_HOTKEY) and (AMsg.WParam = NEW_ENTRY_HOTKEY_ID) then + begin + if Assigned(FOnNewEntryHotkey) then + begin + var LTarget := GetForegroundWindow; + var LTitle: string; + SetLength(LTitle, 512); + var LLen := GetWindowTextW(LTarget, PChar(LTitle), 512); + SetLength(LTitle, LLen); + FOnNewEntryHotkey(LTitle); + end; + end + + else if (AMsg.Msg = WM_HOTKEY) and Assigned(FOnAutofillRequest) and + ((AMsg.WParam = AUTOFILL_HOTKEY_ID_FULL) or + (AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY)) then + begin + // Capture the foreground window BEFORE any focus change, then fire the + // callback so the host can match the title against vault entries. + var LKind: TAutofillKind; + if AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY then + LKind := akPasswordOnly + else + LKind := akFull; + var LTarget := GetForegroundWindow; + var LTitle: string; + SetLength(LTitle, 512); + var LLen := GetWindowTextW(LTarget, PChar(LTitle), 512); + SetLength(LTitle, LLen); + FOnAutofillRequest(LKind, LTarget, LTitle); end; AMsg.Result := DefWindowProc(FMsgWindow, AMsg.Msg, AMsg.WParam, AMsg.LParam); end; +// ============================================================================= +// TPMBridge — Autofill hotkey + SendInput +// ============================================================================= + +function TPMBridge.SetAutofillHotkeys(AFullMods, AFullVk, + APwdMods, APwdVk: Word): Boolean; +begin + // Tear down whatever is currently registered before installing the new + // combos. RegisterHotKey would fail if the same ID is already taken. + if FAutofillFullActive then + begin + UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_FULL); + FAutofillFullActive := False; + end; + if FAutofillPwdActive then + begin + UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_PWDONLY); + FAutofillPwdActive := False; + end; + + // Best-effort registration. A failure (typically MOD_x clash with another + // app's global hotkey) is silent: the other slot can still be live. + if (AFullVk <> 0) and (AFullMods <> 0) then + FAutofillFullActive := RegisterHotKey(FMsgWindow, + AUTOFILL_HOTKEY_ID_FULL, AFullMods, AFullVk); + if (APwdVk <> 0) and (APwdMods <> 0) then + FAutofillPwdActive := RegisterHotKey(FMsgWindow, + AUTOFILL_HOTKEY_ID_PWDONLY, APwdMods, APwdVk); + + FAutofillRegistered := FAutofillFullActive or FAutofillPwdActive; + Result := FAutofillFullActive and FAutofillPwdActive; +end; + +procedure TPMBridge.ApplyTitleBarTheme(ADark: Boolean); +const + DWMWA_USE_IMMERSIVE_DARK_MODE = 20; + // uxtheme.dll private API, stable since Win10 1809. File Explorer, Edge + // and Office use this to opt their UI (including popup menus, scrollbars, + // tooltips) into dark mode. Signature changed in 1903 to take an enum: + // 0=Default 1=AllowDark 2=ForceDark 3=ForceLight 4=Max + // We use ForceDark / ForceLight for unambiguous behaviour. + APPMODE_DEFAULT = 0; + APPMODE_FORCE_DARK = 2; + APPMODE_FORCE_LIGHT = 3; +type + TDwmSetWindowAttribute = function(hwnd: HWND; dwAttribute: DWORD; + pvAttribute: Pointer; cbAttribute: DWORD): HRESULT; stdcall; + TSetPreferredAppMode = function(AppMode: Integer): Integer; stdcall; + TFlushMenuThemes = procedure; stdcall; +var + DwmLib, UxLib: HMODULE; + DwmSetWindowAttribute: TDwmSetWindowAttribute; + SetPreferredAppMode: TSetPreferredAppMode; + FlushMenuThemes: TFlushMenuThemes; + DarkFlag: BOOL; + FormHwnd: HWND; +begin + if FMainForm = nil then Exit; + FormHwnd := MainFormHWND(FMainForm); + if FormHwnd = 0 then Exit; + + // 1. Title bar (DWM immersive dark mode). + DwmLib := LoadLibrary('dwmapi.dll'); + if DwmLib <> 0 then + try + @DwmSetWindowAttribute := GetProcAddress(DwmLib, 'DwmSetWindowAttribute'); + if Assigned(DwmSetWindowAttribute) then + begin + DarkFlag := ADark; + DwmSetWindowAttribute(FormHwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, + @DarkFlag, SizeOf(DarkFlag)); + end; + finally + FreeLibrary(DwmLib); + end; + + // 2. App-wide preferred mode (themes popup menus, scrollbars, tooltips). + // Loaded by ordinal because the functions are not exported by name. + UxLib := LoadLibrary('uxtheme.dll'); + if UxLib <> 0 then + try + @SetPreferredAppMode := GetProcAddress(UxLib, MAKEINTRESOURCE(135)); + @FlushMenuThemes := GetProcAddress(UxLib, MAKEINTRESOURCE(136)); + if Assigned(SetPreferredAppMode) then + begin + if ADark then SetPreferredAppMode(APPMODE_FORCE_DARK) + else SetPreferredAppMode(APPMODE_FORCE_LIGHT); + if Assigned(FlushMenuThemes) then FlushMenuThemes; + end; + finally + FreeLibrary(UxLib); + end; +end; + +procedure TPMBridge.RegisterAutofillHotkey; +begin + // Convenience default — Ctrl+Shift+L (full) + Ctrl+Shift+P (password). + // Idempotent: calling twice with the same combos is harmless. + SetAutofillHotkeys(AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('L'), + AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('P')); +end; + +procedure TPMBridge.UnregisterAutofillHotkey; +begin + if FAutofillFullActive then + begin + UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_FULL); + FAutofillFullActive := False; + end; + if FAutofillPwdActive then + begin + UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_PWDONLY); + FAutofillPwdActive := False; + end; + FAutofillRegistered := False; +end; + +// Block until the user releases Ctrl, Shift, Alt, and Win, or until ATimeoutMs +// elapses. Without this, an autofill triggered by Ctrl+Shift+L injects +// keystrokes WHILE Ctrl+Shift are physically held — turning our Tab into +// Ctrl+Tab (next tab in Chrome), our 's' into Ctrl+S, etc. 1000 ms is a +// generous bound; typical release happens within 50-150 ms. +procedure WaitForModifierRelease(ATimeoutMs: Cardinal); +var + LStart: Cardinal; +begin + LStart := GetTickCount; + while ((GetAsyncKeyState(VK_CONTROL) and $8000) <> 0) + or ((GetAsyncKeyState(VK_SHIFT) and $8000) <> 0) + or ((GetAsyncKeyState(VK_MENU) and $8000) <> 0) // Alt + or ((GetAsyncKeyState(VK_LWIN) and $8000) <> 0) + or ((GetAsyncKeyState(VK_RWIN) and $8000) <> 0) do + begin + Sleep(15); + if GetTickCount - LStart > ATimeoutMs then Break; + end; +end; + +// Build (and immediately send) a key-down+up pair for each char in AText +// using KEYEVENTF_UNICODE. Returns nothing — best-effort. +procedure SendUnicodeString(const AText: string); +var + LInputs: TArray; + LCount, I: Integer; +begin + if AText = '' then Exit; + SetLength(LInputs, Length(AText) * 2); + LCount := 0; + for I := 1 to Length(AText) do + begin + FillChar(LInputs[LCount], SizeOf(TInput), 0); + FillChar(LInputs[LCount + 1], SizeOf(TInput), 0); + LInputs[LCount].Itype := INPUT_KEYBOARD; + LInputs[LCount].ki.wScan := Ord(AText[I]); + LInputs[LCount].ki.dwFlags := KEYEVENTF_UNICODE; + LInputs[LCount + 1] := LInputs[LCount]; + LInputs[LCount + 1].ki.dwFlags := KEYEVENTF_UNICODE or KEYEVENTF_KEYUP; + Inc(LCount, 2); + end; + SendInput(LCount, @LInputs[0], SizeOf(TInput)); +end; + +// Send one virtual-key press (down+up). +procedure SendVKey(AVk: Word); +var + LInputs: array[0..1] of TInput; +begin + FillChar(LInputs, SizeOf(LInputs), 0); + LInputs[0].Itype := INPUT_KEYBOARD; + LInputs[0].ki.wVk := AVk; + LInputs[1] := LInputs[0]; + LInputs[1].ki.dwFlags := KEYEVENTF_KEYUP; + SendInput(2, @LInputs[0], SizeOf(TInput)); +end; + +procedure SendSelectAllAndDelete; +var + LInputs: array[0..5] of TInput; +begin + FillChar(LInputs, SizeOf(LInputs), 0); + LInputs[0].Itype := INPUT_KEYBOARD; + LInputs[0].ki.wVk := VK_CONTROL; + LInputs[1].Itype := INPUT_KEYBOARD; + LInputs[1].ki.wVk := Ord('A'); + LInputs[2].Itype := INPUT_KEYBOARD; + LInputs[2].ki.wVk := Ord('A'); + LInputs[2].ki.dwFlags := KEYEVENTF_KEYUP; + LInputs[3].Itype := INPUT_KEYBOARD; + LInputs[3].ki.wVk := VK_CONTROL; + LInputs[3].ki.dwFlags := KEYEVENTF_KEYUP; + LInputs[4].Itype := INPUT_KEYBOARD; + LInputs[4].ki.wVk := VK_DELETE; + LInputs[5] := LInputs[4]; + LInputs[5].ki.dwFlags := KEYEVENTF_KEYUP; + SendInput(6, @LInputs[0], SizeOf(TInput)); +end; + +// Always-attach foreground switch. The early SetForegroundWindow shortcut +// was unreliable after the picker click — Win10/11 still refused the focus +// hand-off even when our process was foreground. Always doing the attach +// dance is slightly slower but actually works. +function ForceForegroundWindow(ATargetHwnd: HWND): Boolean; +const + ForegroundPollIntervalMs = 20; + ForegroundPollTimeoutMs = 600; +var + CallerThread, TargetThread, TargetPid: DWORD; + ThreadsAttached: Boolean; + WaitStart: Cardinal; +begin + Result := False; + if (ATargetHwnd = 0) or not IsWindow(ATargetHwnd) then Exit; + + TargetPid := 0; + TargetThread := GetWindowThreadProcessId(ATargetHwnd, TargetPid); + CallerThread := GetCurrentThreadId; + if TargetThread = 0 then Exit; + + ThreadsAttached := (TargetThread <> CallerThread) and + AttachThreadInput(CallerThread, TargetThread, True); + try + if IsIconic(ATargetHwnd) then + ShowWindow(ATargetHwnd, SW_RESTORE); + BringWindowToTop(ATargetHwnd); + SetWindowPos(ATargetHwnd, HWND_TOP, 0, 0, 0, 0, + SWP_NOMOVE or SWP_NOSIZE or SWP_NOACTIVATE); + SetForegroundWindow(ATargetHwnd); + + WaitStart := GetTickCount; + while GetForegroundWindow <> ATargetHwnd do + begin + if GetTickCount - WaitStart > ForegroundPollTimeoutMs then Break; + Sleep(ForegroundPollIntervalMs); + SetForegroundWindow(ATargetHwnd); + end; + Result := GetForegroundWindow = ATargetHwnd; + finally + if ThreadsAttached then + AttachThreadInput(CallerThread, TargetThread, False); + end; +end; + +procedure ClickTargetCenterToGrabFocus(ATargetHwnd: HWND); +const + PostClickSettleMs = 40; +var + WindowRect: TRect; + CenterAbsX, CenterAbsY, ScreenW, ScreenH: Integer; + SavedCursor: TPoint; + MouseInputs: array[0..2] of TInput; +begin + if (ATargetHwnd = 0) or not IsWindow(ATargetHwnd) then Exit; + if not GetWindowRect(ATargetHwnd, WindowRect) then Exit; + + CenterAbsX := (WindowRect.Left + WindowRect.Right) div 2; + CenterAbsY := (WindowRect.Top + WindowRect.Bottom) div 2; + ScreenW := GetSystemMetrics(SM_CXSCREEN); + ScreenH := GetSystemMetrics(SM_CYSCREEN); + if (ScreenW <= 0) or (ScreenH <= 0) then Exit; + + GetCursorPos(SavedCursor); + + FillChar(MouseInputs, SizeOf(MouseInputs), 0); + MouseInputs[0].Itype := INPUT_MOUSE; + MouseInputs[0].mi.dx := (CenterAbsX * 65535) div ScreenW; + MouseInputs[0].mi.dy := (CenterAbsY * 65535) div ScreenH; + MouseInputs[0].mi.dwFlags:= MOUSEEVENTF_ABSOLUTE or MOUSEEVENTF_MOVE or MOUSEEVENTF_LEFTDOWN; + MouseInputs[1] := MouseInputs[0]; + MouseInputs[1].mi.dwFlags:= MOUSEEVENTF_LEFTUP; + MouseInputs[2].Itype := INPUT_MOUSE; + MouseInputs[2].mi.dx := (SavedCursor.X * 65535) div ScreenW; + MouseInputs[2].mi.dy := (SavedCursor.Y * 65535) div ScreenH; + MouseInputs[2].mi.dwFlags:= MOUSEEVENTF_ABSOLUTE or MOUSEEVENTF_MOVE; + SendInput(3, @MouseInputs[0], SizeOf(TInput)); + + Sleep(PostClickSettleMs); +end; + +procedure TPMBridge.ExecuteAutofill(ATargetHWND: HWND; + const AUsername, APassword: string); +const + MinimizeSettleMs = 80; + FocusSettleDelayMs = 120; +var + OwnFormHwnd: HWND; +begin + OwnFormHwnd := MainFormHWND(FMainForm); + if GetForegroundWindow = OwnFormHwnd then + begin + ShowWindow(OwnFormHwnd, SW_MINIMIZE); + Sleep(MinimizeSettleMs); + end; + + if ATargetHWND <> 0 then + ForceForegroundWindow(ATargetHWND); + + WaitForModifierRelease(1000); + Sleep(FocusSettleDelayMs); + + if AUsername = '' then + begin + SendSelectAllAndDelete; + Sleep(60); + SendUnicodeString(APassword); + Exit; + end; + + SendSelectAllAndDelete; + Sleep(60); + SendUnicodeString(AUsername); + Sleep(200); + SendVKey(VK_TAB); + Sleep(200); + SendSelectAllAndDelete; + Sleep(60); + SendUnicodeString(APassword); +end; + end. diff --git a/delphi-backend/Source/PM.Database.pas b/delphi-backend/Source/PM.Database.pas index da4d8a6..94f8d5d 100644 --- a/delphi-backend/Source/PM.Database.pas +++ b/delphi-backend/Source/PM.Database.pas @@ -231,6 +231,10 @@ begin // UI V2: tags stored as comma-separated TEXT (e.g. "work,important,2fa"). // Simple format, search via LIKE %tag%. Frontend handles parsing/joining. AddColumnIfMissing('vault_entries', 'tags', 'TEXT DEFAULT '''''); + // Optional human-friendly display name. When empty, the UI falls back + // to `site`. Lets the user store the raw URL/host (used for autofill + // domain matching) while showing something nicer on cards/slideovers. + AddColumnIfMissing('vault_entries', 'title', 'TEXT DEFAULT '''''); // TOTP (2FA) — RFC 6238. Secret + IV are AES-GCM ciphertext / IV pair // encrypted client-side with the user's master-derived key, exactly like // encrypted_password. The server treats them as opaque blobs and never @@ -244,6 +248,12 @@ begin // (600 000 as of 2026). Login flow transparently re-hashes legacy users // and re-encrypts their entries on the client side. AddColumnIfMissing('users', 'kdf_iterations', 'INTEGER DEFAULT 100000'); + AddColumnIfMissing('recovery_keys', 'remaining_uses', 'INTEGER DEFAULT 5'); + // Server-side preferences blob (JSON). Synced across devices on login, + // saved on every change from the JS settings panel. Device-specific + // toggles (quick-unlock DPAPI, Win32 autofill hotkey) intentionally stay + // in localStorage and are NOT included here. + AddColumnIfMissing('users', 'settings_json', 'TEXT DEFAULT ''{}'''); AddColumnIfMissing('sessions', 'csrf_token', 'TEXT'); end; diff --git a/delphi-backend/Source/PM.HTTPServer.pas b/delphi-backend/Source/PM.HTTPServer.pas index 4b2949a..b119f19 100644 --- a/delphi-backend/Source/PM.HTTPServer.pas +++ b/delphi-backend/Source/PM.HTTPServer.pas @@ -1,4 +1,4 @@ -unit PM.HTTPServer; +unit PM.HTTPServer; { Indy TIdHTTPServer wrapper. @@ -12,8 +12,10 @@ interface uses System.SysUtils, System.Classes, System.IOUtils, - IdHTTPServer, IdContext, IdCustomHTTPServer, IdSocketHandle, - PM.Router, PM.JSON, PM.Database, PM.StaticFiles, PM.EmbeddedAssets; + Winapi.Windows, + IdHTTPServer, IdContext, IdCustomHTTPServer, IdSocketHandle, IdTCPConnection, + PM.Router, PM.JSON, PM.Database, PM.StaticFiles, PM.EmbeddedAssets, + PM.Crypto, PM.ProcessLockdown; type TLogProc = reference to procedure(const AMsg: string); @@ -22,6 +24,10 @@ type private FServer: TIdHTTPServer; FOnLog: TLogProc; + FAccessToken: string; + FRequireAccessToken: Boolean; + FRequireProcessCheck: Boolean; + FBoundPort: Integer; procedure HandleCommand(AContext: TIdContext; ARequest: TIdHTTPRequestInfo; AResponse: TIdHTTPResponseInfo); procedure HandleCommandOther(AContext: TIdContext; @@ -34,13 +40,23 @@ type AResponse: TIdHTTPResponseInfo); procedure Log(const AMsg: string); function GetActive: Boolean; + function ValidateAccessToken(ARequest: TIdHTTPRequestInfo; + AResponse: TIdHTTPResponseInfo): Boolean; + function ValidateConnectingProcess(AContext: TIdContext; + AResponse: TIdHTTPResponseInfo): Boolean; public constructor Create; destructor Destroy; override; - procedure Start(APort: Integer); + procedure Start(APort: Integer; SameFolder: Boolean; + ARequireAccessToken: Boolean = True; + ARequireProcessCheck: Boolean = True); procedure Stop; property Active: Boolean read GetActive; property OnLog: TLogProc read FOnLog write FOnLog; + property AccessToken: string read FAccessToken; + property RequireAccessToken: Boolean read FRequireAccessToken; + property RequireProcessCheck: Boolean read FRequireProcessCheck; + property BoundPort: Integer read FBoundPort; end; implementation @@ -96,16 +112,34 @@ begin if Assigned(FOnLog) then FOnLog(AMsg); end; -procedure TPMHTTPServer.Start(APort: Integer); +procedure TPMHTTPServer.Start(APort: Integer; SameFolder: Boolean; + ARequireAccessToken: Boolean; ARequireProcessCheck: Boolean); +const + EphemeralPortMin = 49152; + EphemeralPortMax = 65535; var LBinding: TIdSocketHandle; - LDBPath, LWebRoot: string; + LDBPath, LWebRoot, LResolvedPort: string; + LRequestedPort: Integer; begin if FServer.Active then Exit; - // Resolve vault.db AND the web root (parent of the exe = Z:\password-manager\) - LDBPath := TPath.GetFullPath(TPath.Combine(ExtractFilePath(ParamStr(0)), '..\vault.db')); - LWebRoot := TPath.GetFullPath(TPath.Combine(ExtractFilePath(ParamStr(0)), '..\')); + FRequireAccessToken := ARequireAccessToken; + FRequireProcessCheck := ARequireProcessCheck; + if FRequireAccessToken then + FAccessToken := PM.Crypto.RandomHex(32) + else + FAccessToken := ''; + + LRequestedPort := APort; + if LRequestedPort = 0 then + LRequestedPort := EphemeralPortMin + Random(EphemeralPortMax - EphemeralPortMin); + + var pathParent := '..\'; + if SameFolder then + pathParent := ''; + LDBPath := TPath.GetFullPath(TPath.Combine(ExtractFilePath(ParamStr(0)), pathParent+'vault.db')); + LWebRoot := TPath.GetFullPath(TPath.Combine(ExtractFilePath(ParamStr(0)), pathParent)); Log('Opening database: ' + LDBPath); InitDatabase(LDBPath); Log('Database ready.'); @@ -115,10 +149,15 @@ begin FServer.Bindings.Clear; LBinding := FServer.Bindings.Add; LBinding.IP := '127.0.0.1'; - LBinding.Port := APort; + LBinding.Port := LRequestedPort; FServer.Active := True; - Log('Server started on http://127.0.0.1:' + IntToStr(APort)); + FBoundPort := LRequestedPort; + LResolvedPort := IntToStr(FBoundPort); + Log(Format('Server started on http://127.0.0.1:%s (token:%s process_check:%s)', + [LResolvedPort, + BoolToStr(FRequireAccessToken, True), + BoolToStr(FRequireProcessCheck, True)])); end; procedure TPMHTTPServer.Stop; @@ -176,12 +215,70 @@ begin end; end; +function TPMHTTPServer.ValidateConnectingProcess(AContext: TIdContext; + AResponse: TIdHTTPResponseInfo): Boolean; +var + Binding: TIdSocketHandle; + ConnectingPid: DWORD; +begin + if not FRequireProcessCheck then Exit(True); + Result := False; + + Binding := AContext.Binding; + if Binding = nil then + begin + AResponse.ResponseNo := 404; + AResponse.ContentText := ''; + Exit; + end; + + ConnectingPid := GetPidOfTcpConnection(Word(Binding.PeerPort), Word(Binding.Port)); + if (ConnectingPid <> 0) and IsDescendantOfCurrentProcess(ConnectingPid) then + Exit(True); + + Log(Format('Rejected request from foreign PID %d (%s %s)', + [ConnectingPid, AContext.Connection.Socket.Binding.PeerIP, ''])); + AResponse.ResponseNo := 404; + AResponse.ContentText := ''; +end; + +function TPMHTTPServer.ValidateAccessToken(ARequest: TIdHTTPRequestInfo; + AResponse: TIdHTTPResponseInfo): Boolean; +const + CookieName = 'pm_token'; + QueryParamName = 'pmt'; + SetCookieHeader = 'Set-Cookie'; +var + CookieHeader, QueryToken: string; +begin + if not FRequireAccessToken then Exit(True); + + CookieHeader := ARequest.RawHeaders.Values['Cookie']; + if (CookieHeader <> '') and + (Pos(CookieName + '=' + FAccessToken, CookieHeader) > 0) then + Exit(True); + + QueryToken := ARequest.Params.Values[QueryParamName]; + if QueryToken = FAccessToken then + begin + AResponse.CustomHeaders.AddValue(SetCookieHeader, + CookieName + '=' + FAccessToken + + '; Path=/; HttpOnly; SameSite=Strict'); + Exit(True); + end; + + AResponse.ResponseNo := 404; + AResponse.ContentText := ''; + Result := False; +end; + procedure TPMHTTPServer.HandleCommand(AContext: TIdContext; ARequest: TIdHTTPRequestInfo; AResponse: TIdHTTPResponseInfo); begin ApplySecurityHeaders(ARequest, AResponse); + if not ValidateConnectingProcess(AContext, AResponse) then Exit; + if not ValidateAccessToken(ARequest, AResponse) then Exit; try - // Order: API route → embedded resource (production) → disk static (dev) → 404 if Router.DispatchRequest(ARequest, AResponse) then Exit; if TryServeEmbedded(ARequest, AResponse) then Exit; if Assigned(StaticServer) and StaticServer.TryServe(ARequest, AResponse) then Exit; @@ -199,14 +296,14 @@ procedure TPMHTTPServer.HandleCommandOther(AContext: TIdContext; ARequest: TIdHTTPRequestInfo; AResponse: TIdHTTPResponseInfo); begin ApplySecurityHeaders(ARequest, AResponse); - // OPTIONS preflight if SameText(ARequest.Command, 'OPTIONS') then begin AResponse.ResponseNo := 204; AResponse.ContentText := ''; Exit; end; - // Routes for PUT / DELETE go through here in Indy + if not ValidateConnectingProcess(AContext, AResponse) then Exit; + if not ValidateAccessToken(ARequest, AResponse) then Exit; try if not Router.DispatchRequest(ARequest, AResponse) then TJSONHelper.SendError(AResponse, 404, 'Not found'); diff --git a/delphi-backend/Source/PM.JSON.pas b/delphi-backend/Source/PM.JSON.pas index ae9d757..a3f294a 100644 --- a/delphi-backend/Source/PM.JSON.pas +++ b/delphi-backend/Source/PM.JSON.pas @@ -24,7 +24,6 @@ var LSS: TStringStream; LValue: TJSONValue; begin - Result := nil; if ARequest.PostStream = nil then Exit(TJSONObject.Create); LSS := TStringStream.Create('', TEncoding.UTF8); try diff --git a/delphi-backend/Source/PM.ProcessLockdown.pas b/delphi-backend/Source/PM.ProcessLockdown.pas new file mode 100644 index 0000000..729e261 --- /dev/null +++ b/delphi-backend/Source/PM.ProcessLockdown.pas @@ -0,0 +1,116 @@ +unit PM.ProcessLockdown; + +interface + +uses + Winapi.Windows; + +function GetPidOfTcpConnection(ALocalPort, ARemotePort: Word): DWORD; +function IsDescendantOfCurrentProcess(APid: DWORD): Boolean; + +implementation + +uses + System.SysUtils, System.Generics.Collections, Winapi.WinSock, + Winapi.TlHelp32; + +const + IPHLPAPI = 'iphlpapi.dll'; + AF_INET_LOCAL = 2; + TCP_TABLE_OWNER_PID_CONNECTIONS = 4; + NO_ERROR = 0; + +type + MIB_TCPROW_OWNER_PID = record + dwState: DWORD; + dwLocalAddr: DWORD; + dwLocalPort: DWORD; + dwRemoteAddr: DWORD; + dwRemotePort: DWORD; + dwOwningPid: DWORD; + end; + + MIB_TCPTABLE_OWNER_PID = record + dwNumEntries: DWORD; + table: array[0..0] of MIB_TCPROW_OWNER_PID; + end; + PMIB_TCPTABLE_OWNER_PID = ^MIB_TCPTABLE_OWNER_PID; + +function GetExtendedTcpTable(pTcpTable: Pointer; pdwSize: PDWORD; + bOrder: BOOL; ulAf: ULONG; TableClass: DWORD; Reserved: ULONG): DWORD; + stdcall; external IPHLPAPI; + +function GetPidOfTcpConnection(ALocalPort, ARemotePort: Word): DWORD; +var + Size: DWORD; + Buffer: PMIB_TCPTABLE_OWNER_PID; + i: Integer; + Row: ^MIB_TCPROW_OWNER_PID; + WantedLocal, WantedRemote: Word; +begin + Result := 0; + Size := 0; + GetExtendedTcpTable(nil, @Size, False, AF_INET_LOCAL, + TCP_TABLE_OWNER_PID_CONNECTIONS, 0); + if Size = 0 then Exit; + + GetMem(Buffer, Size); + try + if GetExtendedTcpTable(Buffer, @Size, False, AF_INET_LOCAL, + TCP_TABLE_OWNER_PID_CONNECTIONS, 0) <> NO_ERROR then Exit; + + WantedLocal := ntohs(ALocalPort); + WantedRemote := ntohs(ARemotePort); + Row := @Buffer.table[0]; + for i := 0 to Buffer.dwNumEntries - 1 do + begin + if (Word(Row.dwLocalPort) = WantedLocal) and + (Word(Row.dwRemotePort) = WantedRemote) then + Exit(Row.dwOwningPid); + Inc(Row); + end; + finally + FreeMem(Buffer); + end; +end; + +function IsDescendantOfCurrentProcess(APid: DWORD): Boolean; +const + MaxDepth = 32; +var + Snap: THandle; + Entry: TProcessEntry32W; + ParentMap: TDictionary; + Current, RootPid: DWORD; + Depth: Integer; +begin + Result := False; + if APid = 0 then Exit; + RootPid := GetCurrentProcessId; + if APid = RootPid then Exit(True); + + Snap := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if Snap = INVALID_HANDLE_VALUE then Exit; + + ParentMap := TDictionary.Create; + try + Entry.dwSize := SizeOf(Entry); + if Process32FirstW(Snap, Entry) then + repeat + ParentMap.AddOrSetValue(Entry.th32ProcessID, Entry.th32ParentProcessID); + until not Process32NextW(Snap, Entry); + + Current := APid; + for Depth := 0 to MaxDepth do + begin + if Current = RootPid then Exit(True); + if not ParentMap.TryGetValue(Current, Current) then Exit; + if (Current = 0) or (Current = 4) then Exit; + end; + finally + ParentMap.Free; + CloseHandle(Snap); + end; +end; + +end. diff --git a/delphi-backend/Source/PM.RateLimit.pas b/delphi-backend/Source/PM.RateLimit.pas index 351f036..e4b532e 100644 --- a/delphi-backend/Source/PM.RateLimit.pas +++ b/delphi-backend/Source/PM.RateLimit.pas @@ -23,6 +23,7 @@ interface uses System.SysUtils, System.JSON, + Data.DB, FireDAC.Comp.Client, FireDAC.Stan.Param, IdCustomHTTPServer, PM.Database; @@ -67,7 +68,6 @@ function CheckRateLimit(const AIP: string): Integer; var LQ: TFDQuery; begin - Result := 0; DB.Lock; try LQ := TFDQuery.Create(nil); diff --git a/delphi-backend/Source/PM.Session.pas b/delphi-backend/Source/PM.Session.pas index 6bfee85..8ea2f7c 100644 --- a/delphi-backend/Source/PM.Session.pas +++ b/delphi-backend/Source/PM.Session.pas @@ -16,6 +16,7 @@ interface uses System.SysUtils, System.Classes, System.StrUtils, + Data.DB, FireDAC.Comp.Client, FireDAC.Stan.Param, IdCustomHTTPServer, PM.Database, PM.Crypto, PM.JSON; @@ -55,7 +56,6 @@ var LQ: TFDQuery; LExpires: TDateTime; begin - Result := 0; LToken := ExtractBearerToken(ARequest); if LToken = '' then begin diff --git a/delphi-backend/Source/PM.SingleInstance.pas b/delphi-backend/Source/PM.SingleInstance.pas new file mode 100644 index 0000000..d372450 --- /dev/null +++ b/delphi-backend/Source/PM.SingleInstance.pas @@ -0,0 +1,76 @@ +unit PM.SingleInstance; + +{ + Single-instance guard. + + AcquireOrSignal: + - First instance: creates a named mutex and returns True. Caller proceeds. + - Subsequent instance: detects the mutex, broadcasts WM_PMSHOW so the + running instance restores from tray, returns False. Caller exits. + + WM_PMSHOW is a RegisterWindowMessage('PMServer_ShowExisting') — system- + unique, all processes that register the same string get the same ID. + PM.Bridge listens for it on its message-only window. +} + +interface + +uses + Winapi.Windows, Winapi.Messages; + +const + // Mutex name lives in the Local\ namespace → per-user-session, so a + // second user on the same machine (RDP, Switch User) can still launch + // their own instance. The Global\ namespace would block them. + PMSERVER_MUTEX_NAME = 'Local\PMServer.SingleInstance.Mutex'; + +// System-wide unique message ID, computed once. Bridge + .dpr both call +// this to get the same UINT. +function WM_PMShowMessage: UINT; + +// Try to become the single instance. True = we are first; False = another +// instance was already running (we have signalled it and the caller must +// exit immediately). +function AcquireOrSignal: Boolean; + +implementation + +var + _Mutex: THandle = 0; + _WmShow: UINT = 0; + +function WM_PMShowMessage: UINT; +begin + if _WmShow = 0 then + _WmShow := RegisterWindowMessage('PMServer_ShowExisting'); + Result := _WmShow; +end; + +function AcquireOrSignal: Boolean; +var + LErr: DWORD; +begin + _Mutex := CreateMutex(nil, True, PMSERVER_MUTEX_NAME); + LErr := GetLastError; + + if (_Mutex <> 0) and (LErr <> ERROR_ALREADY_EXISTS) then + begin + // We are the first instance. Keep the mutex alive for the process + // lifetime — Windows releases it automatically on exit. + Result := True; + Exit; + end; + + // Another instance is already running. Close our handle (it isn't ours) + // and broadcast the show-message to all top-level windows. The running + // bridge picks it up on its message-only window. + if _Mutex <> 0 then + begin + CloseHandle(_Mutex); + _Mutex := 0; + end; + PostMessage(HWND_BROADCAST, WM_PMShowMessage, 0, 0); + Result := False; +end; + +end. diff --git a/delphi-backend/Source/PM.UserPrefs.pas b/delphi-backend/Source/PM.UserPrefs.pas new file mode 100644 index 0000000..d0ab4c9 --- /dev/null +++ b/delphi-backend/Source/PM.UserPrefs.pas @@ -0,0 +1,180 @@ +unit PM.UserPrefs; + +{ + Device-bound key/value prefs persisted across launches. + + Problem solved: the HTTP server binds an ephemeral port that changes on + every start (49152-65535). localStorage is keyed by origin (scheme+host + +port) so a different port = a fresh localStorage = anything persisted + there is lost between launches. For prefs that must survive a reboot + (remembered username, etc.) we persist them via this unit instead. + + Storage: %LOCALAPPDATA%\PMServer\prefs.bin + Format: DPAPI-encrypted UTF-8 JSON object {"key":"value",.... + Scope: current Windows user (same threat model as PM.QuickUnlock). +} + +interface + +uses + System.SysUtils, System.Classes, System.IOUtils, System.JSON, + Winapi.Windows; + +function GetPref(const AKey: string): string; +procedure SetPref(const AKey, AValue: string); + +implementation + +type + TDataBlob = record + cbData: DWORD; + pbData: PByte; + end; + PDataBlob = ^TDataBlob; + +function CryptProtectData(pDataIn: PDataBlob; szDataDescr: PWideChar; + pOptionalEntropy: PDataBlob; pvReserved: Pointer; pPromptStruct: Pointer; + dwFlags: DWORD; pDataOut: PDataBlob): BOOL; stdcall; + external 'crypt32.dll' name 'CryptProtectData'; + +function CryptUnprotectData(pDataIn: PDataBlob; ppszDataDescr: PPWideChar; + pOptionalEntropy: PDataBlob; pvReserved: Pointer; pPromptStruct: Pointer; + dwFlags: DWORD; pDataOut: PDataBlob): BOOL; stdcall; + external 'crypt32.dll' name 'CryptUnprotectData'; + +function LocalFree(hMem: HLOCAL): HLOCAL; stdcall; + external 'kernel32.dll' name 'LocalFree'; + +function StorageDir: string; +begin + Result := TPath.Combine(GetEnvironmentVariable('LOCALAPPDATA'), 'PMServer'); +end; + +function StorageFile: string; +begin + Result := TPath.Combine(StorageDir, 'prefs.bin'); +end; + +procedure EnsureStorageDir; +begin + if not TDirectory.Exists(StorageDir) then + TDirectory.CreateDirectory(StorageDir); +end; + +function LoadAll: TJSONObject; +var + LEncrypted: TBytes; + LIn, LOut: TDataBlob; + LStream: TFileStream; + LPlain: string; + LValue: TJSONValue; +begin + // Default to an empty object; every error path just Exits with this. + // Only the success path replaces it with the parsed JSON. + Result := TJSONObject.Create; + + if not TFile.Exists(StorageFile) then Exit; + + try + LStream := TFileStream.Create(StorageFile, fmOpenRead or fmShareDenyWrite); + try + SetLength(LEncrypted, LStream.Size); + if Length(LEncrypted) > 0 then + LStream.ReadBuffer(LEncrypted[0], LStream.Size); + finally + LStream.Free; + end; + except + Exit; + end; + + if Length(LEncrypted) = 0 then Exit; + + LIn.cbData := Length(LEncrypted); + LIn.pbData := @LEncrypted[0]; + LOut.pbData := nil; + LOut.cbData := 0; + + if not CryptUnprotectData(@LIn, nil, nil, nil, nil, 0, @LOut) then Exit; + try + SetString(LPlain, PAnsiChar(LOut.pbData), LOut.cbData); + LValue := TJSONObject.ParseJSONValue(TEncoding.UTF8.GetBytes(LPlain), 0); + if LValue is TJSONObject then + begin + // Replace the default empty object with the parsed one. + Result.Free; + Result := TJSONObject(LValue); + end + else if LValue <> nil then + LValue.Free; + finally + if LOut.pbData <> nil then LocalFree(HLOCAL(LOut.pbData)); + end; +end; + +procedure SaveAll(AObj: TJSONObject); +var + LBytes: TBytes; + LIn, LOut: TDataBlob; + LStream: TFileStream; + LJsonStr: string; +begin + LJsonStr := AObj.ToJSON; + LBytes := TEncoding.UTF8.GetBytes(LJsonStr); + if Length(LBytes) = 0 then Exit; + + LIn.cbData := Length(LBytes); + LIn.pbData := @LBytes[0]; + LOut.pbData := nil; + LOut.cbData := 0; + + if not CryptProtectData(@LIn, nil, nil, nil, nil, 0, @LOut) then Exit; + try + EnsureStorageDir; + LStream := TFileStream.Create(StorageFile, fmCreate); + try + LStream.WriteBuffer(LOut.pbData^, LOut.cbData); + finally + LStream.Free; + end; + finally + if LOut.pbData <> nil then LocalFree(HLOCAL(LOut.pbData)); + end; +end; + +function GetPref(const AKey: string): string; +var + LObj: TJSONObject; + LValue: TJSONValue; +begin + Result := ''; + LObj := LoadAll; + try + if LObj = nil then Exit; + LValue := LObj.GetValue(AKey); + if LValue <> nil then + Result := LValue.Value; + finally + LObj.Free; + end; +end; + +procedure SetPref(const AKey, AValue: string); +var + LObj: TJSONObject; + LExisting: TJSONValue; +begin + LObj := LoadAll; + try + if LObj = nil then LObj := TJSONObject.Create; + LExisting := LObj.GetValue(AKey); + if LExisting <> nil then + LObj.RemovePair(AKey).Free; + LObj.AddPair(AKey, AValue); + SaveAll(LObj); + finally + LObj.Free; + end; +end; + +end. diff --git a/delphi-backend/UMainForm.fmx b/delphi-backend/UMainForm.fmx index 5ddd177..24903f3 100644 --- a/delphi-backend/UMainForm.fmx +++ b/delphi-backend/UMainForm.fmx @@ -1,15 +1,15 @@ object MainForm: TMainForm Left = 0 Top = 0 - Caption = 'Password Manager - Delphi Backend' + Caption = 'Password Manager' ClientHeight = 720 ClientWidth = 1100 FormFactor.Width = 320 FormFactor.Height = 480 FormFactor.Devices = [Desktop] OnCreate = FormCreate - OnDestroy = FormDestroy OnCloseQuery = FormCloseQuery + OnDestroy = FormDestroy DesignerMasterStyle = 0 object PanelTop: TPanel Align = Top diff --git a/delphi-backend/UMainForm.pas b/delphi-backend/UMainForm.pas index d25a492..7da371d 100644 --- a/delphi-backend/UMainForm.pas +++ b/delphi-backend/UMainForm.pas @@ -4,12 +4,14 @@ interface uses System.SysUtils, System.Classes, System.UITypes, System.NetEncoding, + System.StrUtils, + Winapi.Windows, FMX.Forms, FMX.Controls, FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types, FMX.ScrollBox, FMX.Edit, FMX.Layouts, FMX.Types, - FMX.Dialogs, + FMX.Dialogs, FMX.DialogService, FMX.TMSFNCTypes, FMX.TMSFNCUtils, FMX.TMSFNCGraphics, FMX.TMSFNCGraphicsTypes, FMX.TMSFNCCustomControl, FMX.TMSFNCWebBrowser, - PM.HTTPServer, PM.Bridge, PM.QuickUnlock; + PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.UserPrefs; type TMainForm = class(TForm) @@ -37,9 +39,17 @@ type FBridge: TPMBridge; FPendingURL: string; FNavTimer: TTimer; - FNavAttempts: Integer; + FRequireAccessToken: Boolean; + FRequireProcessCheck: Boolean; FQuitting: Boolean; // set when user picks "Quit" in tray menu — bypasses // FormCloseQuery's minimize-to-tray intercept. + FAutofillTargetHWND: HWND; // saved at hotkey time, consumed on /execute + // Pending payload for the 60ms delay timer (focus settle before SendInput). + // Cleared inside AutofillTimerTick. + FAutofillPendingHWND: HWND; + FAutofillPendingUser: string; + FAutofillPendingPass: string; + procedure AutofillTimerTick(Sender: TObject); procedure LogLine(const AMsg: string); procedure UpdateButtons; procedure NavigateToVault; @@ -52,6 +62,11 @@ type procedure BridgeTrayRestore; procedure BridgeLockRequest; procedure BridgeQuit; + procedure BridgeAutofillRequest(AKind: TAutofillKind; + ATargetHWND: HWND; const ATitle: string); + procedure BridgeDebugHotkey; + procedure BridgeNewEntryHotkey(const AWindowTitle: string); + procedure WebBrowserInitialized(Sender: TObject); end; var @@ -67,13 +82,19 @@ begin FServer.OnLog := LogLine; FBridge := TPMBridge.Create(Self); - FBridge.OnSystemLock := BridgeSystemLock; - FBridge.OnTrayRestore := BridgeTrayRestore; - FBridge.OnLockRequest := BridgeLockRequest; - FBridge.OnQuit := BridgeQuit; + FBridge.OnSystemLock := BridgeSystemLock; + FBridge.OnTrayRestore := BridgeTrayRestore; + FBridge.OnLockRequest := BridgeLockRequest; + FBridge.OnQuit := BridgeQuit; + FBridge.OnAutofillRequest := BridgeAutofillRequest; + FBridge.OnDebugHotkey := BridgeDebugHotkey; + FBridge.OnNewEntryHotkey := BridgeNewEntryHotkey; + FBridge.RegisterAutofillHotkey; // Ctrl+Shift+L active from startup + FBridge.ApplyTitleBarTheme(True); // dark by default, JS may toggle later + FAutofillTargetHWND := 0; - // Wire the cmd:// bridge before any navigation happens. WebBrowser.OnBeforeNavigate := WebBrowserBeforeNavigate; + WebBrowser.OnInitialized := WebBrowserInitialized; // Delayed-Navigate timer: TTMSFNCWebBrowser (WebView2 backend) ignores // Navigate() calls until Edge Chromium finishes its async init (~1-2s). @@ -88,6 +109,28 @@ begin UpdateButtons; LogLine('Password Manager - Delphi backend ready.'); LogLine('Click Start to launch server + embedded web vault.'); + PanelTop.Visible := False; + FRequireAccessToken := True; + FRequireProcessCheck := True; + edtPort.Text := '0'; + if FileExists('config.txt') then + begin + var configList := TStringList.Create; + configList.LoadFromFile('config.txt'); + try + var defPort := StrToIntDef(configList.Values['port'], 0); + edtPort.Text := defPort.ToString; + PanelTop.Visible := configList.Values['debug'].ToLower.Equals('true'); + if configList.Values['require_token'].ToLower.Equals('false') then + FRequireAccessToken := False; + if configList.Values['require_process_check'].ToLower.Equals('false') then + FRequireProcessCheck := False; + finally + FreeAndNil(configList); + end; + end; + btnStartClick(Nil); + btnToggleLogClick(nil); end; procedure TMainForm.FormDestroy(Sender: TObject); @@ -96,7 +139,36 @@ begin FServer.Free; end; -procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean); +procedure TMainForm.WebBrowserInitialized(Sender: TObject); +begin + WebBrowser.EnableContextMenu := False; + WebBrowser.EnableShowDebugConsole := False; +end; + +procedure TMainForm.BridgeNewEntryHotkey(const AWindowTitle: string); +var + EscapedTitle: string; +begin + if not FServer.Active then Exit; + FBridge.RestoreFromTray; + EscapedTitle := StringReplace(AWindowTitle, '\', '\\', [rfReplaceAll]); + EscapedTitle := StringReplace(EscapedTitle, '"', '\"', [rfReplaceAll]); + WebBrowser.ExecuteJavaScript( + 'if(window.Bridge&&typeof Bridge.onNewEntryFromTitle==="function")' + + 'Bridge.onNewEntryFromTitle("' + EscapedTitle + '")'); + LogLine('New entry hotkey — title: "' + AWindowTitle + '"'); +end; + +procedure TMainForm.BridgeDebugHotkey; +begin + if not FileExists('config.txt') then + Exit; + PanelTop.Visible := not PanelTop.Visible; + LogLine('Debug panel ' + IfThen(PanelTop.Visible, 'shown', 'hidden') + + ' via Ctrl+Shift+D'); +end; + +Procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean); begin // The tray-menu "Quit" handler sets FQuitting before triggering close, // so we bypass the minimize-to-tray intercept in that case. @@ -138,20 +210,31 @@ begin lblStatus.Text := 'Stopped'; end; +function MaskAccessToken(const AUrl: string): string; +var + TokenPos: Integer; +begin + Result := AUrl; + TokenPos := Pos('?pmt=', Result); + if TokenPos > 0 then + Result := Copy(Result, 1, TokenPos + 4) + '***'; +end; + procedure TMainForm.NavigateToVault; begin - FPendingURL := 'http://127.0.0.1:' + edtPort.Text + '/index.html'; - LogLine('Will navigate embedded browser in ~1.5s to: ' + FPendingURL); - // Schedule a single Navigate after Edge has had time to initialize. - FNavTimer.Enabled := False; // restart timer if already running + FPendingURL := 'http://127.0.0.1:' + FServer.BoundPort.ToString + '/index.html'; + if FServer.RequireAccessToken then + FPendingURL := FPendingURL + '?pmt=' + FServer.AccessToken; + LogLine('Will navigate embedded browser in ~1.5s to: ' + MaskAccessToken(FPendingURL)); + FNavTimer.Enabled := False; FNavTimer.Enabled := True; end; procedure TMainForm.NavTimerTick(Sender: TObject); begin - FNavTimer.Enabled := False; // one-shot + FNavTimer.Enabled := False; if FPendingURL = '' then Exit; - LogLine('Navigating to: ' + FPendingURL); + LogLine('Navigating to: ' + MaskAccessToken(FPendingURL)); WebBrowser.Navigate(FPendingURL); FPendingURL := ''; end; @@ -162,15 +245,16 @@ var begin LPort := StrToIntDef(edtPort.Text, 8765); try - FServer.Start(LPort); + FServer.Start(LPort, True, FRequireAccessToken, FRequireProcessCheck); + edtPort.Text := FServer.BoundPort.ToString; UpdateButtons; NavigateToVault; except on E: Exception do begin LogLine('ERROR starting server: ' + E.Message); - MessageDlg('Failed to start: ' + E.Message, - TMsgDlgType.mtError, [TMsgDlgBtn.mbOK], 0); + TDialogService.MessageDialog('Failed to start: ' + E.Message, + TMsgDlgType.mtError, [TMsgDlgBtn.mbOK], TMsgDlgBtn.mbOK, 0, nil); end; end; end; @@ -279,6 +363,17 @@ begin LogLine('Clipboard cleared by JS request'); end + else if ACmd = 'clipboard/read' then + begin + var ClipText := FBridge.SecureClipboard.ReadText; + var Escaped := StringReplace(ClipText, '\', '\\', [rfReplaceAll]); + Escaped := StringReplace(Escaped, '"', '\"', [rfReplaceAll]); + Escaped := StringReplace(Escaped, #13, '\r', [rfReplaceAll]); + Escaped := StringReplace(Escaped, #10, '\n', [rfReplaceAll]); + WebBrowser.ExecuteJavaScript( + 'if(window.Bridge&&Bridge.onClipboardRead)Bridge.onClipboardRead("' + Escaped + '")'); + end + // ---- Quick unlock (DPAPI persistence of the vault key) ---- // store: client provides a base64-encoded blob (UTF-8 JSON, content // opaque to us). We DPAPI-encrypt and stash on disk. @@ -332,15 +427,145 @@ begin BoolToStr(PM.QuickUnlock.HasQuickUnlock, True).ToLower + ')'); end + // ---- Autofill -------------------------------------------------------- + // configure: JS calls this on page load / settings change to sync the + // hotkey registration state with the user's localStorage preference. + // Uses the historical defaults (Ctrl+Shift+L / Ctrl+Shift+P) — for + // custom combos, JS sends cmd://autofill/hotkeys instead. + else if ACmd = 'autofill/configure' then + begin + if GetParam('enabled') = '1' then + begin + FBridge.RegisterAutofillHotkey; + LogLine('Autofill hotkeys registered (defaults)'); + end + else + begin + FBridge.UnregisterAutofillHotkey; + LogLine('Autofill hotkeys unregistered'); + end; + end + + // hotkeys: JS pushes the user-configured combos. Params: + // enabled = '1' | '0' + // full_mods = MOD_x bitmask (decimal), full_vk = VK code (decimal) + // pwd_mods, pwd_vk = same for the password-only hotkey + // If enabled=0, we just unregister and ignore the rest. If enabled=1, + // we register both with the supplied combos (replacing any prior). + else if ACmd = 'autofill/hotkeys' then + begin + if GetParam('enabled') <> '1' then + begin + FBridge.UnregisterAutofillHotkey; + LogLine('Autofill hotkeys unregistered (custom)'); + end + else + begin + var LFullMods := Word(StrToIntDef(GetParam('full_mods'), 6)); // Ctrl+Shift + var LFullVk := Word(StrToIntDef(GetParam('full_vk'), Ord('L'))); + var LPwdMods := Word(StrToIntDef(GetParam('pwd_mods'), 6)); + var LPwdVk := Word(StrToIntDef(GetParam('pwd_vk'), Ord('P'))); + var LAllOk := FBridge.SetAutofillHotkeys(LFullMods, LFullVk, + LPwdMods, LPwdVk); + LogLine(Format('Autofill hotkeys set — full=mods:%d vk:%d pwd=mods:%d vk:%d (all_ok=%s)', + [LFullMods, LFullVk, LPwdMods, LPwdVk, BoolToStr(LAllOk, True)])); + // Notify JS of the result so the UI can flag a failed-to-register combo + // (typically a clash with another app's global hotkey). + WebBrowser.ExecuteJavaScript( + 'if(window.Bridge&&Bridge.onAutofillHotkeysResult)' + + 'Bridge.onAutofillHotkeysResult(' + BoolToStr(LAllOk, True).ToLower + ')'); + end; + end + + // execute: JS has matched an entry, decrypted the password, and is + // telling Delphi to type username + Tab + password into the saved HWND. + else if ACmd = 'autofill/execute' then + begin + FAutofillPendingUser := GetParam('username'); + FAutofillPendingPass := GetParam('password'); + FAutofillPendingHWND := FAutofillTargetHWND; + FAutofillTargetHWND := 0; + + // Small timer so SetForegroundWindow has time to take effect before + // SendInput fires — avoids the first keystrokes going to our window. + // TTimer.OnTimer is a TNotifyEvent (method, not anon proc) → we use a + // dedicated method on the form and stash the payload in fields. + var LTimer := TTimer.Create(Self); + LTimer.Interval := 60; + LTimer.OnTimer := AutofillTimerTick; + LTimer.Enabled := True; + end + + // cancel: JS found no match or user dismissed the picker — nothing to type. + else if ACmd = 'autofill/cancel' then + begin + FAutofillTargetHWND := 0; + LogLine('Autofill cancelled (no match or dismissed)'); + end + + // focus: JS asks us to bring the main window to front (e.g. when the + // autofill picker opens — without this the picker is shown in the + // WebView but the user might not notice if our window was minimised + // or behind other apps). The Target HWND stays saved; ExecuteAutofill + // restores it later via ForceForegroundWindow. + else if ACmd = 'app/focus' then + begin + FBridge.RestoreFromTray; + LogLine('App brought to front (autofill picker)'); + end + + else if ACmd = 'app/ready' then + begin + WebBrowser.SetFocus; + WebBrowser.ExecuteJavaScript( + 'setTimeout(()=>{var u=document.getElementById("loginUsername"),' + + 'p=document.getElementById("loginPassword");' + + 'if(u&&u.value){p&&p.focus();}else{u&&u.focus();}},0)'); + end + + else if ACmd = 'app/theme' then + FBridge.ApplyTitleBarTheme(GetParam('mode') = 'dark') + + // ---- Device-bound prefs (DPAPI key/value) ---------------------------- + // Used for prefs that must survive the ephemeral-port reset of the + // WebView2 localStorage (rememberedUsername, etc.). + else if ACmd = 'prefs/get' then + begin + var LKey := GetParam('key'); + if LKey = '' then Exit; + var LVal := PM.UserPrefs.GetPref(LKey); + var LEscapedKey := StringReplace(LKey, '\', '\\', [rfReplaceAll]); + LEscapedKey := StringReplace(LEscapedKey, '"', '\"', [rfReplaceAll]); + var LEscapedVal := StringReplace(LVal, '\', '\\', [rfReplaceAll]); + LEscapedVal := StringReplace(LEscapedVal, '"', '\"', [rfReplaceAll]); + LEscapedVal := StringReplace(LEscapedVal, #13, '\r', [rfReplaceAll]); + LEscapedVal := StringReplace(LEscapedVal, #10, '\n', [rfReplaceAll]); + WebBrowser.ExecuteJavaScript( + 'if(window.Bridge&&Bridge.onPrefResult)' + + 'Bridge.onPrefResult("' + LEscapedKey + '","' + LEscapedVal + '")'); + end + + else if ACmd = 'prefs/set' then + begin + var LKey := GetParam('key'); + if LKey = '' then Exit; + PM.UserPrefs.SetPref(LKey, GetParam('value')); + end + else LogLine('Bridge: unknown command "' + ACmd + '"'); end; procedure TMainForm.BridgeSystemLock; begin - // Windows session locked — lock the vault in the JS layer immediately. - LogLine('Windows session locked — locking vault'); - WebBrowser.ExecuteJavaScript('if(typeof lockVault==="function")lockVault()'); + // Windows session locked or system suspending — delegate to Bridge.onSystemLock + // in JS which honours the "quick unlock" opt-out (DPAPI already gates + // access via the Windows account, so re-locking on top of Windows lock + // is redundant for users who enabled it). + LogLine('Windows session locked / suspend — notifying JS'); + WebBrowser.ExecuteJavaScript( + 'if(window.Bridge&&typeof Bridge.onSystemLock==="function")Bridge.onSystemLock();' + + 'else if(typeof lockVault==="function")lockVault()'); end; procedure TMainForm.BridgeTrayRestore; @@ -375,4 +600,48 @@ begin Application.Terminate; end; +procedure TMainForm.AutofillTimerTick(Sender: TObject); +var + TargetHwnd: HWND; + PendingUser, PendingPass: string; + ForegroundAfter: HWND; +begin + TargetHwnd := FAutofillPendingHWND; + PendingUser := FAutofillPendingUser; + PendingPass := FAutofillPendingPass; + FAutofillPendingHWND := 0; + FAutofillPendingUser := ''; + FAutofillPendingPass := ''; + + TTimer(Sender).Enabled := False; + TTimer(Sender).Free; + + FBridge.ExecuteAutofill(TargetHwnd, PendingUser, PendingPass); + ForegroundAfter := GetForegroundWindow; + LogLine(Format('Autofill executed — target=%s, foreground_after=%s, match=%s', + [IntToHex(TargetHwnd, 8), IntToHex(ForegroundAfter, 8), + BoolToStr(ForegroundAfter = TargetHwnd, True)])); +end; + +procedure TMainForm.BridgeAutofillRequest(AKind: TAutofillKind; + ATargetHWND: HWND; const ATitle: string); +var + LTitle, LKind: string; +begin + if not FServer.Active then Exit; + FAutofillTargetHWND := ATargetHWND; + + // Escape the title for safe injection into a JS string literal. + LTitle := ATitle; + LTitle := LTitle.Replace('\', '\\'); + LTitle := LTitle.Replace('"', '\"'); + + if AKind = akPasswordOnly then LKind := 'password' else LKind := 'full'; + + WebBrowser.ExecuteJavaScript( + 'if(window.Bridge&&typeof Bridge.onAutofillRequest==="function")' + + 'Bridge.onAutofillRequest("' + LTitle + '","' + LKind + '")'); + LogLine('Autofill hotkey (' + LKind + ') — foreground: "' + ATitle + '"'); +end; + end. diff --git a/delphi-backend/app.ico b/delphi-backend/app.ico new file mode 100644 index 0000000000000000000000000000000000000000..1aa467674ad41f311c52f53edc063e5081f22fb2 GIT binary patch literal 102390 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 literal 0 HcmV?d00001 diff --git a/delphi-backend/app.png b/delphi-backend/app.png new file mode 100644 index 0000000000000000000000000000000000000000..77177ff826c71f060c83780956e81bfc2f4c98c5 GIT binary patch literal 7752 zcmV-O9=G9%P);i{PDgk?P_OtXJ)70_np~(RBvsmr;p$9{XNg~drrYMNP`n^62Lex z155%TAOeJeF<=xJ1_FQ=aGMW6(|qbc5y%0HKn92bQ6LGVfO#NYSa?NVYilg70T8)O z5QTxgz%9T&6HtDj&wP}1`bizAnor3D*b(4Yz)L{f1Y5HQtc?XE74VrrdLQt1UQ7QttuF zZZJ*0xgU57a39F~OfJ>80p}%6(HR5r81NWysE|5Z=>f|k8k&5w3QPg-0X_=c1_U>m zL|+090p9_h022As(M}Io-2+tw_*cOFz_x4P@*#mUz+VFo=Tk?cJz%XLm;~@v;1j@y zfWd3wHoFYK3c&Y)hk--+)KS?3*4hB&0`>qO2R;l;T!VmG4qywwqrlgJWBJt4Ru5P! z=TO7I?*aePRLFaDF-3vD0R9wMavBE{4g=Ky@XvtH09$+5SrMHDz5skTpE_FZ0qbHe zb*BlacVf6vdbDz?13xzbcEAbT%XVN5h2A^@JP7;&hMT2FYo{de72uJ4@@V#&1=dh# z?~TBhfO~t2`MN6jj{|?0Prm%(H3h7p&|V+#fXTW?d!XtHxE6uW0)LrLzFgb%zzT-; zP67W8_!x%!rAOVPD)0^9OH0X@6ITAPYB8vGVAx+i!t3!(J?b90Fnp;u`TXMt>g9B9 zQ*o?dXs-gi3-}gBP+gDgQS2W8e!i4^S>Ny+D;V1AF(>q2d!VwF{1}e=oX!w77sTg5d=iAW zw~LN_R1mj-@c8@_&(urn%emBb29;)tzlZ4g`!e2K?amKy%Y&ZK=RLK-uS?c)ng*pWEWP*s+GEZcs2B$^Qu$+C1mkg`Av(@K*XG zGXPYg^AJ0WNTs0F+Drh;1D^uEl}o&I)tqa+ILo`t+utStl>m<$IW>*AemDNfNhU{! z@edf@r_$6Usq42>PNk5ik0TOsXf!e0a~qn5%sq?(KeL@<4Nu*$19+NEVEmMU0CLA3 zwD1HB<~O>Sd;)kdmv|{*Ik1MOZ}0(M2L27}SNFgoX(wP{02vCS zZry=@e4Me-Vf?`${nyQ)jgFx7^%?KGEQ^!q)hg}9ER|R1DJ?CLSWT zE`!`~Vp0eG8}Lvr{?eL4ZmvehdI0#%4F{^g5c=*np@kxhjgJxZ_Y;^2qndEXK-qY{#=gXM1tacjHQ_wGU*KMWEAE2D^SdDq*YSPy?q%Y-OL(} zH9WoVM&PFyC4V-CxZAQ7BG=)c3Nt+DXLw=^PiPX?)D%izA406p`H)gauU2U#5;T%2 zatj%9g%YJi45F_h=FV;;F_{70lZziY(sf`BPwyK6zKzqviG_e*5IGe=+p>+o=qMwj z!wifJ1_gu`gTAj0kLp18XTF)5o(+Dl6`Vlk?j zMe^At3b`Du`Ll@lJR*~E8W;N!@R4l%NO|3WHDZXsk6}cC+V5O;yOATK$nCrEOiU3R z872_$(;t~e3x!Yv0Z^S@bIqhqhfc9bJDH>sjZ-X^Sjy&TEF=)8P9U<2PVrCHP1Djh zvnvpZD+5+|-w1FRxXxa2=SJVY3&L9%m<%yI&_`fm4EOXj>iD>0#NE}1$xb>=Gaje7 zkReyDQcfiyHivTR1hi}i*jeDVZ0yR4yjTIKT)^ira@6b*cM~CGcnhw`bqo#-F*rO# z-^4hs=_%A;(BMvDlcXj}X>hTnC0g+W)rB;LT%JNcPb>N=A~uIuNZKhT{}}k=Z0tzu zYJs&6_#swTed+5%j!nSU9eBql7z_sS2Z!)Zg>Z!?QTqEgj<~xDF{xK7w2~?6i6o_^ zJcXqt>gfeIJB!FHK(%W1sG`7q+1TZ6TjKJdx={fC1^69H#of-`$nX~Y;}Z-G^f53t zhI=N0Iy#E*UYqfQvRa%L;8@D>IGegZ>-ctewDQ&XrzLz_p`t%IoS3F^rd#e9K6u|z92 zhnPQWgz~H9_*gzHIEpWee`AmjIQDVa9(CU2xs#JKu!=*0&J~PZ8X?1K*zA zxUS!h7#Mghx}Zm^AWcII44_R<;~5)eV4$A?my6Ee2#sphQ1aKGJV3QCF!_sGaY3#~ z6biu@tZ&DPbOEbdB1lnMv>ji18X#LV?Rd6hpt}3 zq%uxOa2}<)+DLq_ukz-j#VG}!#Uj(et$W1Cn6G=-7&Ysh+ddhn@m3Pb5why>> zG;-4ui_sT4Z{%1$jN%ZMIOXIy1+xdN?j2m~eomO!@nN(lJ`Y%eFh&z@>)16QdbsM^ z!M$MHyHERoY2d^Kz!HWSRINKvrjFH9-n*w`Nh@)MijfldI3?MAUuc>fON;|M?6?j)+DF)E&^$d$rr2{M^4Q2pMcG!7la`>yxW_pW;p{R7UKiOc6ws%{{iSLdnxuW!)FWa#^+A3+%kIt5vFU5jo3(8gwN7FL@X#QFM_18}96 zOH}^qn>3$TdxgaM5B?u|txEp`A4eJRI|EqK7FA>4*v!pBLoklCzO$)Bnl-4T?Hf@2 z+y6`R>0ey+S2cg}G-7m&z7PF#M2IW=?GESnLA7K$3MJeF%C`9 zm>7&O5-{qZWQ$1_3Q^knGWGVGEt)@i4A*UUpl-3ZSGqVqB*|Kg`+*q^Bel!ghLq++ zb-O(}ttO4XdlFeFE&FGmyPpr=^ZRTI-^{QHtXwh4nb_a+=u=;%)~sB7UZrHUi5QP+?*zqzR)Wk>#CE&qX)bsbuNp6746K*Q=(2*YV&#`XVzEdoHND7xq+kWzAbl(3#l;E(fw?dn+^e_L&<1A{sUp zsI@yZS=N^?sIhf&FaEwOi!p!SFk2`0E(LtJ1&LBo%@0lIZ09!OnM?^QcmUkkjm!eb~Mf`mum;HFPUL?H~y~KBW zZJnE9PjtgM8rBd5Y_i^=V{d4(^ek#uw&-QZoe{Kxj);J(<_RBe{|@&2!|R7zcP{7c0YKi>Z}s?B98y13o2V~=g~ zI)80#B98{xV#Vv|l8!FXI}$~l->TR`3JfA{z6G8=c+sDZ#SZbkC;o(AyW``G1!r)( zjQXa{R*lTkJU>40HIBs&E&shY--7NBU}gO!c}~5+Fgz=|MAfBT{b<~_0qfk&ML$o^ z66OgL;=;bz-J8A#ao0U4zd8)9_8V@jI?tbel4H^5*thi`a9wCG0Iw!r=E&K@1j)N5Ez3|4-uXZ+a?@fre zybZDcm&+^Y%`yj1{D^}mezdahd+XcK_uh1g8Lo0I-T)Z6#4;%CH_Ag8Fv4}2J{SU|k|^3@%by?bfj_hEEJy$lNEMZmf29IyJ++QK** zHUaCr*{>7JmY4ea6>OP45kh|Oeq5DrAkLm$+wX7PO6!C7)0qf??u6o&wu-{0g3)o{ z8l3wZE)&~!(EROBq2F@r>SC#cK)>}?8o&K1I@@+QXF~R_Z9_v)q?-k7E#g?~VgeZ* zr}>+o#QohzP@aBjWuJM+-84V)Akyn|SXTFy^n|WbK?8DD|Ejp)j_j=TFX{0j-77ck zNw*j2@j9#bnQN6HH`MG){+?p zEIMr*w<}U28Y?efEgChDy5kxg7;B4N{SPx55VL&JZF91nj^jj#sMS_i7rjWg`o*1cBSJ(LH-1|Ka16HSV-dbDAO*=qL6GmIs zDi&r!D~&qnI!dGAI58q>)zxvVY7J4VB5jOsUrwAWjfT0#b*mNl8j#d58tzyMESgPY z0~-xQyN%Rb&e@bk15sPKX;0Ma&}=x0^NM!coJZA;?yS{wz?w%;!l)8r&BR+xltvA` z(SUXbROhrg5v?XP8Y}-my@6;pouwBjJ3@z+Tpi6k~(u(o|8epvijH6R2+8SfKn(R#wHvCEOKjbL@Z?; zRgOt17C`E#$+#Uq|0{UC2IDa!ep{tM_n;?4o0Cg-W@M zXf#l>X**A$z>7s zI%+zJ4KC1fH>c5mE_6t%|yogLdouELQuT{2IhZKJSdNw_B)7IYh03 zl1kWt>t{?Xy{;4p_=$jyrR5`NHIqgk3?gmp2`$Q{wO!A39Z@MGZ4~FC z-8OXAdKE1dvm+4e$T|J$LSPv_o~KMb%d%mK{1UE(1WjK*N+C~Yc*MTVsg$k)ltpA< z&_3O&kVjN1xKas3e#wSw4^NpG;sQ~aJbkDG`~<_-V;$)_+KHoxRtse@WB9r3(orf` zaXYP)Z5y{}w~e)HHPKEUwZkY{1s*psbRn=zdKx?8vnpj=@mEnwMU*wzk=AV-)yh?T zwrU@@%4JbXMO^VYM8y`hq z>vhzH6iT&>cIvoY*nTW|`p^Z7x16T@7>JKy^!l}qTNI19=1!w~`%u#hw5P(B$7uj=mTesdt>(;xjPKC8n1eS^@*$mp^0_vGrySUXM6Ehd?o__Ao3OE)3Parsh zl`&f97Hlbt`_wBmcHfAWO41H*L8?j*Nf#VI9W9kasg!ZgzJgfF+Lb_f8p*Qt%$Eh0 z5F~)_0RP@{p)Doa>gSmBDI z5TKNcOaJinEbuXGs`#ho7R>|&0h)&NdY#wY(DJR6s7qP2g(O-wjpv2uUQea7o(jOn z6Q`cbuNYV+xQhPa=^Yp@9?Q2>v|A|UB6@HH(dmH4V-z#HR?#ITYN>#hjHBjrcwTxQ zrBtwOG{9daPCfVSOEQMbecC@fodQ09yhvOhV03v|Zff(kaluJgs*kBN)*#NiO z5Q}1yi$y6>%SBwt7)rT>=am;wRg)`RK)_?LIP1K<9+FQ161}WZTuPV{ZcM|Sx@(hj{Ma2w~YXY5Nx%3 zR{BJU`o7!H`~7rA#%Pa>Z9dK=C0aIvn$4h8N_dYPMzLS~f#fW3TjJESSB&3QSMq)S zk#HSB8F(L;HmX{qb5+Wy`5bb<4-%Ai%c%6CsG9;)qTRxkP8w!~YzEJf!zhbsCk(BQ z;13ZTDCMr4R<@$uDCOe#N5ZqfTL9Y&ZHihIHJ5?Dex#(e3ixDV_Sw@b#mlPXM_?oz zGmT^KbA+W*O4M``wNya*1JE*R7D%_pQH4e>%YB=5v~%NP)_6;~cr7pzei`_6px-$szk!xYAY=#W z>od}ynhm4&mXo-bG(ujq#WZR@hgvD(Iei?@(H9Z*n$zIF2>eET_Ssig`@uC`rAoQ@ zLSQ6P0P$`RF2`sqQle%TaV27=5!H)QDH+AxTy9Vl8=0&`v|4Dn4BBEErB=fopTqn7 zLBoTv(NtRT4uZEJ9=Ow{0cB)-V_}rZ$!Ig=q=>}(&s}WQL7cBE~lnh zZkiCSCTc#1mQABnN@)2k-dA44b^3%+w9R>>1OF5FYAF|6Q-Q6k0>t#r+oym>fcH4| z7Vq)WnVzA!bqAfkex%!Lggp-gkgD3nGa(V}4oannQYs>vb<}DF_t{z0`21BRZdgE1 z01w7b9$a&t{#CE)^v>INV5I(p9Bn3YduU(3lVzI{raJ$zN9v)j};5 z5akl0(?+RPaK~OnJAKlqvgd3{0`G{QJa|>rht}n|PVc<^-N3g1XNP(UVE}B)^|YpE z=nVKl)zJF}(EA6_J>Ip8Nxg&8sH0TMDAh7_+Nk9cuJ}CK+-U<)QaT+}3E(&4ClCJY zYQJb*l#Rehcn+f{&)wJ*ak^q`b=s)e3|b~-=v{7)S+=@{QmYy%U_xD)|4|wWxmqcr zEoD(l1tV^%n8$PKIPQ~2QB(2PyF@sf^1#0Z{-%_TUG)iAHw7cS^L7vLJHVH5L}|Mf zibzePGd)9lVv5%21X6PuV$$nF_w}Ls`pnzp`P@lIsZ|Xi)@z7P+wk!%E}$(W3>~W7 zW&@E9JOun%?Bv09xlOFA<{I94yJimXbJ*BxdhLq7hxWFew8lcT{ez&YAQW_;59#SM z7OYW4shRzAIvuo90aqr8cIG6a(O?5o6Zj9nmt!XnuG?u?_bX9&=j~o|pr6FH&aGFs zB+}zS28L))glLaV(m5|ATC|#|^%`0xWyDGq3nrlCMg&z2_;cX%v6Bbu>${3wpzzK+ z0>Ed0-`j|s>%!Zl8+~+)&WO=pLCvO($}Y~dKdqKi{VDLp*vSLM^*^3nqVTReyuk09 z+^d&y>O7t5b4b1#J9(fh2DPr4po`g9J1`nKfKj6Ptxmr!UPF{Ob>OeYP9EqQ+wH1v zqnM59fzinGrhVkCfWNn|wo<}0DSRV#;y_n3T`lqPg?HVd0Ph6;5{FypH!&rF2Y{c) zP8{fZu$QIIF#y1Nf|Yd+gb~R!4Q<=fD?`9EhISZ@YiVQQ{}E`_2L2BPPJM_VBVII&12x-;EyM zUv|o%9F><|T#PmcMclY@zC5Ksl+`@qA%q3H4b zj*ipWP#YB4eP_f3*!?&t6T8cRI*n1m`r+vD{n3pw@C~;yk==Ky7)cxN20m(Xv*0EJ z)DmzA_-o*2Kq7j4e`h02U_+nynca8xo4~pc#C<>*=-Ytvk{IrnnAw-+G2qbr@%@#J zx;L9fl+5h8%ZrgEbT4oZu+M3?-estR(V6rq3~Tw*^T&Q!-wgY)>2#EtJ$I=XvG&s@ z$li`oEN(kCl@&ja%D^d%Ztu^SV2T5^`D4HAY^HtK^tY>-J$Jc*aT91a0JoUjY!V~z zu+IeIdU3Z7Mv_7mC;=&xEBy+1$plh*{@5>@n|a-@NxRU@p1TAvhEc;lWIkaaV!l2C z44bA64@N@Bxf0C{4C8IXGy&v*ERex4TE;MZc1d6!$jl%6rM%YG`2Pd6JF_8!5GT(7 O0000J{nst_f$NGUQiDkly|f+SKPfC4~L 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 diff --git a/icon-hex-16.svg b/icon-hex-16.svg new file mode 100644 index 0000000..200b953 --- /dev/null +++ b/icon-hex-16.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icon-hex.svg b/icon-hex.svg new file mode 100644 index 0000000..e8652f3 --- /dev/null +++ b/icon-hex.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icon-key.svg b/icon-key.svg new file mode 100644 index 0000000..2bc80b2 --- /dev/null +++ b/icon-key.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icon-shield.svg b/icon-shield.svg new file mode 100644 index 0000000..e8f103a --- /dev/null +++ b/icon-shield.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icon-vault.svg b/icon-vault.svg new file mode 100644 index 0000000..6960957 --- /dev/null +++ b/icon-vault.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..b7abfc8 --- /dev/null +++ b/icon.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/index-legacy.html b/index-legacy.html new file mode 100644 index 0000000..f3e900f --- /dev/null +++ b/index-legacy.html @@ -0,0 +1,218 @@ + + + + + + 🔐 Vault — Legacy UI + + + +

+
+

⏰ Auto-lock

+

Vault locks in 30s

+ +
+ +
+
+

✏️ Edit Entry

+ + + +
+ + +
+ + + +
+ + +
+
+
+ + + + + + +
+
+

🔐 Vault XAMPP

+ +
+ + +
+
+ + +
+
+
+ + + + +
+
+ +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/index.html b/index.html index caf3f49..38b7121 100644 --- a/index.html +++ b/index.html @@ -38,7 +38,11 @@ + + + + @@ -99,7 +103,16 @@ + - + + + + + diff --git a/js/app-legacy.js b/js/app-legacy.js new file mode 100644 index 0000000..82093a6 --- /dev/null +++ b/js/app-legacy.js @@ -0,0 +1,1550 @@ +// Backend detection: Apache/XAMPP serves under /password-manager/, Delphi at root. +const API = (window.location.pathname.indexOf('/password-manager/') === 0) + ? '/password-manager/api.php' + : ''; +let token = sessionStorage.getItem('authToken'); +let csrfToken = sessionStorage.getItem('csrfToken') || ''; +let curUser = sessionStorage.getItem('currentUsername'); +function a2b64(arr) { return btoa(String.fromCharCode(...new Uint8Array(arr))).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,''); } +function b642ab(s) { return Uint8Array.from(atob(s.replace(/-/g,'+').replace(/_/g,'/')), c=>c.charCodeAt(0)).buffer; } +let view = localStorage.getItem('vaultView') || 'grid'; +let detailIndex = 0; +let showView = localStorage.getItem('showViewBtn') !== 'false'; +let showMail = localStorage.getItem('showEmail') !== 'false'; +let dark = localStorage.getItem('darkTheme') !== 'false'; +let lockMin = parseInt(localStorage.getItem('autoLockMinutes') || '5'); +let order = JSON.parse(localStorage.getItem('entryOrder') || '[]'); +let selectedFolder = localStorage.getItem('selectedFolder') || 'All'; +let entries = []; +let folders = ['All']; +let genPwdVal = ''; +let cryptoKey = null; +let searchQuery = ''; +let idleT, warnT, countT; +let _loadVer = 0; +let draggedId = null; +let showTrash = false; +let selectedIds = new Set(); +let lastSelectedId = null; +let arrowAnchor = -1; +let arrowFocus = -1; +let rectState = { active: false, startX: 0, startY: 0, el: null, started: false }; + +// ==================== SOUND ==================== +let soundEnabled = localStorage.getItem('soundEnabled') !== 'false'; +let audioCtx = null; + +function getAudioContext() { + if (!audioCtx) { + audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + } + return audioCtx; +} + +function playTone(freq, duration, type = 'sine', volume = 0.08) { + if (!soundEnabled) return; + try { + const ctx = getAudioContext(); + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = type; + osc.frequency.setValueAtTime(freq, ctx.currentTime); + gain.gain.setValueAtTime(volume, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration); + osc.connect(gain); + gain.connect(ctx.destination); + osc.start(ctx.currentTime); + osc.stop(ctx.currentTime + duration); + } catch (e) { /* ignore */ } +} + +function playSound(type) { + if (!soundEnabled) return; + switch (type) { + case 'click': playTone(800, 0.08, 'sine', 0.06); break; + case 'success': + playTone(523, 0.1, 'sine', 0.1); + setTimeout(() => playTone(659, 0.1, 'sine', 0.1), 100); + setTimeout(() => playTone(784, 0.15, 'sine', 0.1), 200); + break; + case 'error': + playTone(200, 0.2, 'square', 0.06); + setTimeout(() => playTone(150, 0.3, 'square', 0.06), 150); + break; + case 'delete': + playTone(150, 0.15, 'triangle', 0.08); + break; + case 'copy': + playTone(1200, 0.05, 'sine', 0.07); + break; + case 'generate': + playTone(440, 0.05, 'sine', 0.05); + setTimeout(() => playTone(554, 0.05, 'sine', 0.05), 60); + setTimeout(() => playTone(659, 0.05, 'sine', 0.05), 120); + setTimeout(() => playTone(880, 0.1, 'sine', 0.07), 180); + break; + case 'open': + playTone(600, 0.12, 'sine', 0.06); + setTimeout(() => playTone(800, 0.1, 'sine', 0.06), 80); + break; + case 'close': + playTone(800, 0.08, 'sine', 0.05); + setTimeout(() => playTone(600, 0.1, 'sine', 0.05), 80); + break; + case 'login': + playTone(523, 0.1, 'sine', 0.08); + setTimeout(() => playTone(659, 0.1, 'sine', 0.08), 100); + setTimeout(() => playTone(784, 0.2, 'sine', 0.1), 200); + break; + case 'register': + playTone(440, 0.1, 'sine', 0.08); + setTimeout(() => playTone(554, 0.1, 'sine', 0.08), 100); + setTimeout(() => playTone(659, 0.15, 'sine', 0.1), 200); + break; + } +} + +function toggleSound() { + soundEnabled = !soundEnabled; + localStorage.setItem('soundEnabled', soundEnabled); + if (soundEnabled) playTone(440, 0.05); + syncSettingsUI(); +} + +// ==================== TOAST ==================== +function toast(m, t, action) { t = t || 'success'; const c = document.getElementById('toastContainer'); const d = document.createElement('div'); d.className = 'toast ' + t; d.innerHTML = '' + m + ''; if (action) { const btn = document.createElement('button'); btn.className = 'toast-action'; btn.textContent = action.label; btn.onclick = function(e) { e.stopPropagation(); action.cb(); d.remove(); }; d.appendChild(btn); c.appendChild(d); } else { c.appendChild(d); setTimeout(() => d.remove(), 3000); } } +function showZigzagToast(elem, msg, type) { + const t = document.createElement('div'); + t.className = 'toast-zigzag ' + (type || 'success'); + t.textContent = msg; + document.body.appendChild(t); + const r = elem.getBoundingClientRect(); + t.style.left = r.left + 'px'; + t.style.top = r.top + 'px'; + setTimeout(() => t.remove(), 1500); +} + +// ==================== THEME ==================== +function applyTheme() { document.body.classList.toggle('light', !dark); } +function toggleTheme() { dark = !dark; localStorage.setItem('darkTheme', dark); applyTheme(); syncSettingsUI(); playSound('click'); } + +// ==================== CRYPTO ==================== +function checkStrength() { const p = document.getElementById('passwordInput').value; const b = document.getElementById('strengthBar'); let s = 0; if (p.length >= 8) s++; if (p.length >= 12) s++; if (/[A-Z]/.test(p) && /[a-z]/.test(p)) s++; if (/\d/.test(p)) s++; if (/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(p)) s++; b.className = 'strength-bar s' + Math.min(4, s); } +async function deriveKey(pwd, salt) { const enc = new TextEncoder(); const km = await crypto.subtle.importKey('raw', enc.encode(pwd), 'PBKDF2', false, ['deriveKey']); const sb = Uint8Array.from(atob(salt), c => c.charCodeAt(0)); return crypto.subtle.deriveKey({ name: 'PBKDF2', salt: sb, iterations: 100000, hash: 'SHA-256' }, km, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']); } +async function encryptPwd(plain) { const iv = crypto.getRandomValues(new Uint8Array(12)); const enc = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, cryptoKey, new TextEncoder().encode(plain)); return { encrypted: btoa(String.fromCharCode(...new Uint8Array(enc))), iv: btoa(String.fromCharCode(...iv)) }; } +async function decryptPwd(encB64, ivB64) { try { const enc = Uint8Array.from(atob(encB64), c => c.charCodeAt(0)); const iv = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0)); const dec = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, cryptoKey, enc); return new TextDecoder().decode(dec); } catch (e) { return '[ERROR]'; } } +async function persistCryptoKey() { const raw = await crypto.subtle.exportKey('raw', cryptoKey); sessionStorage.setItem('cryptoKey', btoa(String.fromCharCode(...new Uint8Array(raw)))); } +async function restoreCryptoKey() { const saved = sessionStorage.getItem('cryptoKey'); if (!saved) return false; try { const raw = Uint8Array.from(atob(saved), c => c.charCodeAt(0)); cryptoKey = await crypto.subtle.importKey('raw', raw, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']); return true; } catch (e) { return false; } } + +// ==================== AUTO-LOCK ==================== +function setAutoLock() { lockMin = parseInt(document.getElementById('autoLockTimer').value); localStorage.setItem('autoLockMinutes', lockMin); resetIdle(); } +function resetIdle() { clearTimeout(idleT); clearTimeout(warnT); clearInterval(countT); document.getElementById('idleWarning').classList.remove('show'); if (lockMin > 0 && token) { const lm = lockMin * 60000; warnT = setTimeout(() => { document.getElementById('idleWarning').classList.add('show'); let cd = 30; document.getElementById('idleCountdown').textContent = cd; countT = setInterval(() => { cd--; document.getElementById('idleCountdown').textContent = cd; if (cd <= 0) { clearInterval(countT); doLogout(); } }, 1000); }, Math.max(0, lm - 30000)); idleT = setTimeout(() => doLogout(), lm); } } + +// ==================== USERNAME ==================== +function saveUsername() { const f = document.getElementById('addUsername'); if (f && f.value.trim()) localStorage.setItem('savedUsername', f.value.trim()); } +function loadUsername() { const s = localStorage.getItem('savedUsername'); const f = document.getElementById('addUsername'); if (s && f) f.value = s; } + +// ==================== FOLDERS ==================== +async function loadFolders() { + if (!token) return; + try { + const r = await fetch(API + '/folders', { headers: { 'Authorization': 'Bearer ' + token } }); + if (r.ok) { + const data = await r.json(); + if (Array.isArray(data)) { + folders = data.filter(f => typeof f === 'string'); + if (!folders.includes('All')) folders.unshift('All'); + } else { + folders = ['All']; + } + } else { + folders = ['All']; + } + } catch (e) { + folders = ['All']; + } +} + +async function addFolderToServer(name) { + try { + const r = await fetch(API + '/folders', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken }, + body: JSON.stringify({ name }) + }); + if (r.ok) { await loadFolders(); return true; } + const d = await r.json(); + toast('❌ ' + (d.error || 'Error'), 'error'); + return false; + } catch (e) { toast('⚠️ Connection error', 'error'); return false; } +} + +async function deleteFolderFromServer(name) { + try { + const r = await fetch(API + '/folders/' + encodeURIComponent(name), { + method: 'DELETE', + headers: { 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken } + }); + if (r.ok) { + await loadFolders(); + if (selectedFolder === name) { selectedFolder = 'All'; localStorage.setItem('selectedFolder', 'All'); } + return true; + } + const d = await r.json(); + toast('❌ ' + (d.error || 'Error'), 'error'); + return false; + } catch (e) { toast('⚠️ Connection error', 'error'); return false; } +} + +function folderColor(name) { + if (name === 'All') return ''; + let hash = 0; + for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash); + const hue = ((hash % 360) + 360) % 360; + return `style="--chip-color:hsl(${hue},60%,55%)"`; +} +function renderFolders() { + const bar = document.getElementById('foldersBar'); + if (!bar) return; + entries.forEach(e => { if (!e.folder || typeof e.folder !== 'string') e.folder = 'All'; }); + const counts = {}; + entries.forEach(e => { const f = e.folder; counts[f] = (counts[f] || 0) + 1; }); + let html = ''; + folders.forEach(f => { + if (!f) return; + const count = counts[f] || 0; + const color = folderColor(f); + html += `📁 ${esc(f)}${count}${f !== 'All' ? `` : ''}`; + }); + html += ``; + bar.innerHTML = html; + // Make folders drop targets for moving entries + bar.querySelectorAll('.folder-chip[data-folder]').forEach(chip => { + chip.addEventListener('dragover', e => { e.preventDefault(); chip.classList.add('drag-over'); }); + chip.addEventListener('dragleave', () => chip.classList.remove('drag-over')); + chip.addEventListener('drop', async function(e) { + e.preventDefault(); + this.classList.remove('drag-over'); + const id = parseInt(e.dataTransfer.getData('text/plain')); + if (!id) return; + const entry = entries.find(x => x.id === id); + if (!entry) return; + const folder = this.dataset.folder; + const ids = selectedIds.has(id) && selectedIds.size > 1 ? [...selectedIds] : [id]; + let moved = 0; + for (const sid of ids) { + const e2 = entries.find(x => x.id === sid); + if (!e2 || e2.folder === folder) continue; + const enc = await encryptPwd(e2.password); + const r = await fetch(API + '/entries/' + sid, { + method: 'PUT', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken }, + body: JSON.stringify({ site: e2.site, username: e2.username, encrypted_password: enc.encrypted, iv: enc.iv, folder }) + }); + if (r.ok) { e2.folder = folder; moved++; } + } + renderFolders(); + render(); + if (moved) playSound('success'); + }); + }); +} + + + +function selectFolder(f) { + selectedFolder = f; + localStorage.setItem('selectedFolder', f); + renderFolders(); + populateFolderSelects(); + render(); + playSound('click'); +} + +function showAddFolderModal() { + const overlay = document.createElement('div'); + overlay.className = 'custom-modal-overlay show'; + overlay.innerHTML = `

📁 New Folder

`; + document.body.appendChild(overlay); + document.getElementById('cancelAddFolder').onclick = () => overlay.remove(); + document.getElementById('confirmAddFolder').onclick = async () => { + const name = document.getElementById('newFolderName').value.trim(); + if (!name) { toast('Enter a name', 'error'); return; } + const ok = await addFolderToServer(name); + if (ok) { renderFolders(); populateFolderSelects(); overlay.remove(); toast('📁 Folder created!'); playSound('success'); } + }; + overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); + playSound('open'); +} + +function showDeleteFolderConfirm(folderName) { + const overlay = document.createElement('div'); + overlay.className = 'custom-modal-overlay show'; + overlay.innerHTML = `

🗑️ Delete Folder

Delete "${folderName}"? Entries move to "All".

`; + document.body.appendChild(overlay); + document.getElementById('cancelDeleteFolder').onclick = () => overlay.remove(); + document.getElementById('confirmDeleteFolder').onclick = async () => { + const ok = await deleteFolderFromServer(folderName); + if (ok) { renderFolders(); populateFolderSelects(); render(); overlay.remove(); toast('📁 Folder deleted'); playSound('delete'); } + }; + overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); +} + +// ==================== TRASH ==================== +function toggleTrash() { + showTrash = !showTrash; + const btn = document.getElementById('trashBtn'); + const actions = document.getElementById('trashActions'); + if (btn) { btn.classList.toggle('active', showTrash); btn.title = showTrash ? 'Back to entries' : 'Trash'; } + if (actions) actions.classList.toggle('hidden', !showTrash); + loadEntries(); + playSound('click'); +} +async function restoreEntry(id, noToast) { + try { const r = await fetch(API + '/entries/' + id + '/restore', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken } }); if (r.ok) { if (!noToast) { toast('✅ Restored!'); await loadEntries(); playSound('success'); } } } catch (e) { if (!noToast) toast('⚠️ Error', 'error'); } +} +async function toggleFavorite(id) { + try { await fetch(API + '/entries/' + id + '/favorite', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken } }); const e = entries.find(x => x.id == id); if (e) e.favorite = e.favorite ? 0 : 1; renderFolders(); render(); playSound('click'); } catch (e) {} +} +async function permanentDelete(id, silent) { + const doDelete = async () => { + try { const r = await fetch(API + '/entries/' + id + '?permanent=1', { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken } }); if (r.ok) { order = order.filter(x => x != id); localStorage.setItem('entryOrder', JSON.stringify(order)); if (!silent) { toast('🗑️ Permanently deleted'); await loadEntries(); playSound('error'); } } } catch (e) { if (!silent) toast('⚠️ Error', 'error'); } + }; + if (silent) { await doDelete(); return; } + const btn = document.querySelector('.delete-btn[data-id="' + id + '"]'); + if (btn) showBatchConfirm(btn, 'Permanently delete?', doDelete); + else showBatchConfirm(document.body, 'Permanently delete?', doDelete); +} +async function emptyTrash() { + const btn = document.querySelector('.empty-trash-btn'); + showBatchConfirm(btn || document.body, 'Delete ALL trashed entries?', async () => { + try { const r = await fetch(API + '/entries/trash/empty', { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken } }); if (r.ok) { toast('🗑️ Trash emptied'); await loadEntries(); playSound('error'); } } catch (e) { toast('⚠️ Error', 'error'); } + }); +} +function timeAgo(dateStr) { + if (!dateStr) return ''; + const now = new Date(); const d = new Date(dateStr + 'Z'); + const days = 30 - Math.floor((now - d) / (1000 * 60 * 60 * 24)); + return days <= 0 ? 'Expiring' : days + 'd left'; +} + +// ==================== SETTINGS ==================== +function toggleSettings() { + document.getElementById('settingsMenu').classList.toggle('hidden'); +} +function syncSettingsUI() { + document.getElementById('soundToggleSwitch').classList.toggle('active', soundEnabled); + document.getElementById('themeToggleSwitch').classList.toggle('active', !dark); + document.getElementById('showViewBtnToggle').classList.toggle('active', showView); + document.getElementById('showEmailToggle').classList.toggle('active', showMail); + document.getElementById('autoLockTimer').value = lockMin; +} +async function registerPasskey() { + try { + const r = await fetch(API + '/passkey/register/begin', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken } + }); + if (!r.ok) { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); return; } + const opts = await r.json(); + opts.challenge = b642ab(opts.challenge); + opts.user.id = b642ab(opts.user.id); + if (!window.PublicKeyCredential) { toast('❌ Passkeys not supported', 'error'); return; } + const cred = await navigator.credentials.create({ publicKey: opts }); + const result = { + id: cred.id, + response: { + clientDataJSON: a2b64(cred.response.clientDataJSON), + attestationObject: a2b64(cred.response.attestationObject) + } + }; + const r2 = await fetch(API + '/passkey/register/complete', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken }, + body: JSON.stringify(result) + }); + if (r2.ok) { toast('✅ Passkey registered!'); playSound('success'); } + else { const d = await r2.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); } + } catch (e) { toast('⚠️ Passkey setup failed: ' + e.message, 'error'); } +} + +// ==================== INIT ==================== +function init() { + document.getElementById('autoLockTimer').value = lockMin; + applyTheme(); + // document.getElementById('usernameInput').style.display = showMail ? '' : 'none'; + loadUsername(); + const sl = localStorage.getItem('savedLoginUser'); + if (sl) document.getElementById('loginUsername').value = sl; + syncSettingsUI(); + // View dropdown + const vdd = document.getElementById('viewDropdown'); + const vBtn = document.getElementById('viewDropdownBtn'); + const vMenu = document.getElementById('viewDropdownMenu'); + const vIcons = {grid:'🟫',compact:'📝',list:'📋',table:'📊',card:'🃏',grouped:'📂',detail:'🔍'}; + const updateViewBtn = () => { vBtn.textContent = (vIcons[view] || '🟫') + ' ' + view.charAt(0).toUpperCase() + view.slice(1) + ' ▾'; }; + updateViewBtn(); + vMenu.querySelectorAll('.view-opt').forEach(o => o.classList.toggle('active', o.dataset.view === view)); + vBtn.onclick = (e) => { e.stopPropagation(); vMenu.classList.toggle('hidden'); }; + vMenu.onclick = (e) => { + const opt = e.target.closest('.view-opt'); + if (!opt) return; + vMenu.querySelectorAll('.view-opt').forEach(o => o.classList.remove('active')); + opt.classList.add('active'); + view = opt.dataset.view; + detailIndex = 0; + localStorage.setItem('vaultView', view); + updateViewBtn(); + render(); + playSound('click'); + }; + document.addEventListener('click', () => vMenu.classList.add('hidden')); + const clearBtn = document.getElementById('clearSearchBtn'); + if (clearBtn) clearBtn.style.display = 'none'; + document.getElementById('addModal').addEventListener('click', e => { + if (e.target === e.currentTarget) closeAdd(); + }); + // Close settings when clicking outside + document.addEventListener('click', (e) => { + const menu = document.getElementById('settingsMenu'); + const btn = document.getElementById('settingsBtn'); + if (menu && !menu.classList.contains('hidden') && !menu.contains(e.target) && e.target !== btn) { + menu.classList.add('hidden'); + } + }); +} + +function toggleViewBtn() { showView = !showView; localStorage.setItem('showViewBtn', showView); syncSettingsUI(); render(); playSound('click'); } +function toggleShowEmail() { showMail = !showMail; localStorage.setItem('showEmail', showMail); syncSettingsUI(); render(); playSound('click'); } + +// ==================== GENERATOR ==================== +function openGen() { document.getElementById('genModal').style.display = 'flex'; genPwd(); playSound('open'); } +function closeGen() { document.getElementById('genModal').style.display = 'none'; playSound('close'); } +function onLenChange() { document.getElementById('lenVal').textContent = document.getElementById('pwdLen').value; genPwd(); } +function genPreset(len, chars) { + document.getElementById('pwdLen').value = len; + document.getElementById('lenVal').textContent = len; + document.getElementById('useUpper').checked = chars.includes('upper'); + document.getElementById('useLower').checked = chars.includes('lower'); + document.getElementById('useNum').checked = chars.includes('num'); + document.getElementById('useSym').checked = chars.includes('sym'); + genPwd(); + playSound('click'); +} +function genPwd() { const l = parseInt(document.getElementById('pwdLen').value); let c = ''; if (document.getElementById('useUpper').checked) c += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; if (document.getElementById('useLower').checked) c += 'abcdefghijklmnopqrstuvwxyz'; if (document.getElementById('useNum').checked) c += '0123456789'; if (document.getElementById('useSym').checked) c += '!@#$%^&*()_+-=[]{}|;:,.<>?'; if (!c) { document.getElementById('genPreview').textContent = 'Select option'; return; } let p = ''; const max = 256 - (256 % c.length); const buf = new Uint8Array(1); for (let i = 0; i < l; i++) { do { crypto.getRandomValues(buf); } while (buf[0] >= max); p += c.charAt(buf[0] % c.length); } genPwdVal = p; document.getElementById('genPreview').textContent = p; } +function useGen() { + if (!genPwdVal) genPwd(); + // Put the generated password into the add‑modal’s password field + const pwdField = document.getElementById('addPassword'); + if (pwdField) { + pwdField.value = genPwdVal; + checkAddStrength(); // update the strength bar + } + navigator.clipboard.writeText(genPwdVal); + toast('🎲 Copied!'); + closeGen(); // closes the generator modal, not the add modal +} +function populateFolderSelects() { + ['addFolder', 'editFolder'].forEach(id => { + const select = document.getElementById(id); + if (!select) return; + select.innerHTML = ''; + folders.forEach(f => { + if (!f) return; + const option = document.createElement('option'); + option.value = f; + option.textContent = '📁 ' + f; + if (f === selectedFolder) option.selected = true; + select.appendChild(option); + }); + }); +} +function openAdd() { + document.getElementById('addSite').value = ''; + document.getElementById('addPassword').value = ''; + document.getElementById('addStrengthBar').className = 'strength-bar s0'; + loadUsername(); + populateFolderSelects(); + document.getElementById('addModal').classList.add('show'); + document.getElementById('addSite').focus(); + playSound('open'); +} + +function closeAdd() { + document.getElementById('addModal').classList.remove('show'); + playSound('close'); +} + +function checkRegStrength() { + const p = document.getElementById('regPassword').value; + const bar = document.getElementById('regStrengthBar'); + let s = 0; + if (p.length >= 8) s++; + if (p.length >= 12) s++; + if (/[A-Z]/.test(p) && /[a-z]/.test(p)) s++; + if (/\d/.test(p)) s++; + if (/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(p)) s++; + bar.className = 'strength-bar s' + Math.min(4, s); +} +function checkAddStrength() { + const p = document.getElementById('addPassword').value; + const bar = document.getElementById('addStrengthBar'); + let s = 0; + if (p.length >= 8) s++; + if (p.length >= 12) s++; + if (/[A-Z]/.test(p) && /[a-z]/.test(p)) s++; + if (/\d/.test(p)) s++; + if (/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(p)) s++; + bar.className = 'strength-bar s' + Math.min(4, s); +} +// ==================== AUTH ==================== +function switchTab(t) { document.querySelectorAll('.auth-tab').forEach(x => x.classList.remove('active')); event.target.classList.add('active'); document.getElementById('loginForm').classList.toggle('hidden', t !== 'login'); document.getElementById('registerForm').classList.toggle('hidden', t !== 'register'); } + +async function loginWithPasskey() { + if (!window.PublicKeyCredential) { toast('❌ Passkeys not supported', 'error'); return; } + const u = document.getElementById('loginUsername').value.trim(); + if (!u) { toast('Enter username first', 'error'); return; } + document.getElementById('loginBtn').disabled = true; + try { + const r = await fetch(API + '/passkey/login/begin', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: u }) + }); + if (!r.ok) { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); document.getElementById('loginBtn').disabled = false; return; } + const opts = await r.json(); + opts.challenge = b642ab(opts.challenge); + opts.allowCredentials.forEach(c => { c.id = b642ab(c.id); }); + const cred = await navigator.credentials.get({ publicKey: opts }); + const result = { + id: cred.id, + response: { + clientDataJSON: a2b64(cred.response.clientDataJSON), + authenticatorData: a2b64(cred.response.authenticatorData), + signature: a2b64(cred.response.signature), + userHandle: cred.response.userHandle ? a2b64(cred.response.userHandle) : null + } + }; + const r2 = await fetch(API + '/passkey/login/complete', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(result) + }); + const d = await r2.json(); + if (r2.ok) { + token = d.token; csrfToken = d.csrfToken || ''; curUser = d.username || u; + sessionStorage.setItem('authToken', token); + sessionStorage.setItem('csrfToken', csrfToken); + sessionStorage.setItem('currentUsername', curUser); + // Try to restore crypto key from sessionStorage + const restored = await restoreCryptoKey(); + if (!restored) { + // Need master password once to derive crypto key + const mp = await new Promise(resolve => { + const overlay = document.createElement('div'); + overlay.className = 'custom-modal-overlay show'; + overlay.innerHTML = `

🔑 One more step

Enter your master password to unlock the vault

`; + document.body.appendChild(overlay); + document.getElementById('passkeyTempBtn').onclick = () => resolve(document.getElementById('passkeyTempPwd').value); + overlay.addEventListener('keydown', function handler(e) { if (e.key === 'Enter') { resolve(document.getElementById('passkeyTempPwd').value); overlay.remove(); document.removeEventListener('keydown', handler); } }); + }); + const m = document.querySelector('.custom-modal-overlay.show'); + if (m) m.remove(); + cryptoKey = await deriveKey(mp, d.salt); + persistCryptoKey(); + } + await loadFolders(); + toast('✅ Biometric login!'); + playSound('login'); + showVault(); + loadEntries(); + } else { toast('❌ ' + (d.error || 'Failed'), 'error'); } + } catch (e) { toast('⚠️ Passkey login failed: ' + e.message, 'error'); } + finally { document.getElementById('loginBtn').disabled = false; } +} +async function login() { + const u = document.getElementById('loginUsername').value.trim(); + const p = document.getElementById('loginPassword').value; + if (!u || !p) { toast('Fill all fields', 'error'); return; } + document.getElementById('loginBtn').disabled = true; + localStorage.setItem('savedLoginUser', u); + try { + const r = await fetch(API + '/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: u, masterPassword: p }) }); + const d = await r.json(); + if (r.ok) { + token = d.token; csrfToken = d.csrfToken || ''; curUser = u; + cryptoKey = await deriveKey(p, d.salt); + persistCryptoKey(); + sessionStorage.setItem('authToken', token); + sessionStorage.setItem('csrfToken', csrfToken); + sessionStorage.setItem('currentUsername', u); + await loadFolders(); + toast('✅ Login!'); + playSound('login'); + showVault(); + loadEntries(); + } else { toast('❌ ' + (d.error || 'Invalid'), 'error'); document.getElementById('loginPassword').value = ''; } + } catch (e) { toast('⚠️ Connection error', 'error'); } + finally { document.getElementById('loginBtn').disabled = false; } +} + +async function register() { + const u = document.getElementById('regUsername').value.trim(); + const p = document.getElementById('regPassword').value; + if (u.length < 3) { toast('Username min 3', 'error'); return; } + if (p.length < 8) { toast('Password min 8', 'error'); return; } + document.getElementById('registerBtn').disabled = true; + try { + const r = await fetch(API + '/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: u, masterPassword: p }) }); + const d = await r.json(); + if (r.ok) { + token = d.token; csrfToken = d.csrfToken || ''; curUser = u; + cryptoKey = await deriveKey(p, d.salt); + persistCryptoKey(); + sessionStorage.setItem('authToken', token); + sessionStorage.setItem('csrfToken', csrfToken); + sessionStorage.setItem('currentUsername', u); + await loadFolders(); + toast('✅ Created!'); + playSound('register'); + showVault(); + loadEntries(); + } else { toast('❌ ' + (d.error || 'Failed'), 'error'); } + } catch (e) { toast('⚠️ Connection error', 'error'); } + finally { document.getElementById('registerBtn').disabled = false; } +} + +async function doLogout() { + saveUsername(); + if (token) { + try { await fetch(API + '/logout', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken } }); } catch (e) {} + } + clearTimeout(idleT); clearTimeout(warnT); clearInterval(countT); + document.getElementById('idleWarning').classList.remove('show'); + token = null; csrfToken = ''; curUser = null; entries = []; cryptoKey = null; folders = ['All']; showTrash = false; + sessionStorage.clear(); + document.getElementById('authSection').classList.remove('hidden'); + document.getElementById('vaultSection').classList.add('hidden'); + document.getElementById('loginPassword').value = ''; + document.getElementById('loginUsername').value = localStorage.getItem('savedLoginUser') || ''; + document.querySelectorAll('.fab').forEach(b => b.classList.add('hidden')); +} + +function showVault() { + document.getElementById('authSection').classList.add('hidden'); + document.getElementById('vaultSection').classList.remove('hidden'); + document.getElementById('currentUser').textContent = '👤 ' + curUser; + //document.getElementById('usernameInput').style.display = showMail ? '' : 'none'; + document.getElementById('autoLockTimer').value = lockMin; + loadUsername(); + renderFolders(); + populateFolderSelects(); + syncSettingsUI(); + document.querySelectorAll('.fab').forEach(b => b.classList.remove('hidden')); + resetIdle(); +} + +// ==================== ENTRIES ==================== +function applyOrder(list) { if (!list || !list.length) return []; if (!order || !order.length) return list; const map = new Map(list.filter(e => e && e.id).map(e => [e.id, e])); const ord = []; order.forEach(id => { if (map.has(id)) { ord.push(map.get(id)); map.delete(id); } }); map.forEach(e => ord.push(e)); return ord; } + +async function loadEntries(q) { + const ver = ++_loadVer; + try { + let url = API + '/entries?deleted=' + (showTrash ? '1' : '0'); + if (q) url += '&search=' + encodeURIComponent(q); + const r = await fetch(url, { headers: { 'Authorization': 'Bearer ' + token } }); + if (ver !== _loadVer) return; + if (r.ok) { + const raw = await r.json(); + if (ver !== _loadVer) return; + entries = []; + for (const e of raw) { + if (e.encryption_method === 'client') { + const pw = await decryptPwd(e.encrypted_password, e.iv); + entries.push({ id: e.id, site: e.site, username: e.username, password: pw, folder: e.folder || 'All', deleted_at: e.deleted_at, favorite: e.favorite || 0 }); + } else { + entries.push({ id: e.id, site: e.site, username: e.username, password: e.password || '', folder: e.folder || 'All', deleted_at: e.deleted_at, favorite: e.favorite || 0 }); + } + } + entries = applyOrder(entries); + entries.sort((a, b) => (b.favorite || 0) - (a.favorite || 0)); + document.getElementById('connectionStatus').textContent = '🟢 Connected'; + document.getElementById('entryCount').textContent = '(' + entries.length + ' entries)'; + renderFolders(); + populateFolderSelects(); + render(); + } else if (r.status === 401) { toast('Session expired', 'error'); doLogout(); } + } catch (e) { document.getElementById('connectionStatus').textContent = '🔴 Error'; toast('Connection error', 'error'); } +} + +function getFilteredEntries(noFolder) { + if (showTrash) return entries; + if (noFolder || selectedFolder === 'All') return entries; + return entries.filter(e => (e.folder || 'All') === selectedFolder); +} + +function getGridCols() { + const c = document.getElementById('entriesContainer'); + if (!c || !c.firstElementChild) return 1; + const w = c.firstElementChild.offsetWidth; + const gap = parseInt(getComputedStyle(c).columnGap) || 0; + return Math.max(1, Math.round(c.offsetWidth / (w + gap))); +} + +// ==================== RENDER ==================== +function render() { + const c = document.getElementById('entriesContainer'); + c.className = ''; + if (showTrash) c.classList.add('trash-view'); + c.classList.add(view + '-view'); + const filtered = getFilteredEntries(); + if (!filtered.length) { c.innerHTML = '
' + (showTrash ? '📭 Trash empty' : '📭 No entries') + '
'; return; } + if (view === 'table') { + let h = '' + (showMail ? '' : '') + '' + (!showTrash ? '' : '') + ''; + filtered.forEach(e => { + const sel = selectedIds.has(e.id); + h += '' + + '' + + '' + + (showMail ? '' : '') + + ''; + if (!showTrash) { + h += '' + + ''; + } else { + h += '' + + ''; + } + h += ''; + }); + h += '
SiteUserPasswordFolderDeletedActions
' + (e.favorite ? '⭐' : '') + '🌐 ' + highlightText(e.site, searchQuery) + '👤 ' + highlightText(e.username, searchQuery) + '••••••••📁 ' + esc(e.folder || 'All') + '' + + ' ' + + ' ' + + ' ' + + '' + + '🗑️ ' + timeAgo(e.deleted_at) + '' + + ' ' + + '' + + '
'; + c.innerHTML = h; + } else if (view === 'grouped') { + c.innerHTML = groupedC(getFilteredEntries(true)); + } else if (view === 'detail') { + c.innerHTML = detailC(getFilteredEntries(view === 'grouped')); + } else { + c.innerHTML = filtered.map(e => { + if (view === 'grid' || view === 'card') return gridC(e); + if (view === 'compact') return compC(e); + return listC(e); + }).join(''); + } + attachEvents(); + setupDrag(); +} + +function gridC(e) { + const sel = selectedIds.has(e.id); + let html = '
'; + html += '
'; + if (showTrash) { + html += ''; + html += ''; + } else { + html += ''; + html += ''; + html += ''; + } + html += '
'; + html += '
🌐 ' + highlightText(e.site, searchQuery) + '
'; + if (showMail) html += '
👤 ' + highlightText(e.username, searchQuery) + '
'; + html += '
📁 ' + esc(e.folder || 'All') + '
'; + if (!showTrash) { + html += '
••••••••
' + + '
'; + } else { + html += '
🗑️ ' + timeAgo(e.deleted_at) + '
'; + } + html += '
'; + return html; +} +function listC(e) { + const sel = selectedIds.has(e.id); + let html = '
'; + html += '
'; + if (showTrash) { + html += ''; + html += ''; + } else { + html += ''; + html += ''; + html += ''; + } + html += '
'; + html += '
'; + return html; +} +function compC(e) { + const sel = selectedIds.has(e.id); + let html = '
'; + html += '
'; + if (showTrash) { + html += ''; + html += ''; + } else { + html += ''; + html += ''; + html += ''; + } + html += '
'; + html += '🌐 ' + highlightText(e.site, searchQuery) + ''; + if (showMail) html += '👤 ' + highlightText(e.username, searchQuery) + ''; + if (!showTrash) { + html += '📁 ' + esc(e.folder || 'All') + ''; + html += '••••••••'; + html += ''; + } else { + html += '🗑️ ' + timeAgo(e.deleted_at) + ''; + } + html += '
'; + return html; +} + +function groupedC(list) { + const groups = {}; + list.forEach(e => { + const f = e.folder || 'All'; + if (!groups[f]) groups[f] = []; + groups[f].push(e); + }); + let html = ''; + for (const [folder, items] of Object.entries(groups)) { + html += '
📁 ' + esc(folder) + ' ' + items.length + '
'; + items.forEach(e => { + const sel = selectedIds.has(e.id); + html += '
'; + html += '
'; + if (showTrash) { + html += ''; + html += ''; + } else { + html += ''; + html += ''; + html += ''; + } + html += '
'; + html += '
'; + }); + } + return html; +} + +function detailC(list) { + if (detailIndex >= list.length) detailIndex = 0; + if (detailIndex < 0) detailIndex = list.length - 1; + const e = list[detailIndex]; + const hasPrev = detailIndex > 0, hasNext = detailIndex < list.length - 1; + const sel = selectedIds.has(e.id); + let html = '
'; + html += ''; + html += '' + (detailIndex + 1) + ' of ' + list.length + ''; + html += ''; + html += '
'; + html += '
'; + html += '
Site🌐 ' + highlightText(e.site, searchQuery) + '
'; + if (showMail) html += '
Username👤 ' + highlightText(e.username, searchQuery) + '
'; + if (!showTrash) { + html += '
Password••••••••
'; + html += '
Folder📁 ' + esc(e.folder || 'All') + '
'; + html += '
' + + '' + + '' + + '' + + '' + + '
'; + } else { + html += '
Deleted🗑️ ' + timeAgo(e.deleted_at) + '
'; + html += '
' + + '' + + '' + + '
'; + } + html += '
'; + return html; +} + +function goDetail(dir) { + const list = getFilteredEntries(); + detailIndex += dir; + if (detailIndex < 0) detailIndex = list.length - 1; + if (detailIndex >= list.length) detailIndex = 0; + render(); +} + +// ==================== EVENTS ==================== +function showConfirm(btn, message, callback) { + const id = btn.dataset.id; + const existing = document.querySelector('.custom-confirm'); + if (existing) existing.remove(); + + const confirm = document.createElement('div'); + confirm.className = 'custom-confirm show'; + confirm.innerHTML = + '
' + message + '
' + + '
' + + '' + + '' + + '
'; + document.body.appendChild(confirm); + + const rect = btn.getBoundingClientRect(); + confirm.style.top = (rect.top - 60) + 'px'; + let leftPos = rect.left - confirm.offsetWidth + rect.width; + if (leftPos < 10) leftPos = 10; + confirm.style.left = leftPos + 'px'; + + const yesBtn = confirm.querySelector('.confirm-yes'); + const noBtn = confirm.querySelector('.confirm-no'); + + const cleanup = () => { + confirm.remove(); + document.removeEventListener('keydown', keyHandler); + }; + + const keyHandler = (e) => { + if (e.key === 'Enter' || e.key === 'y' || e.key === 'Y') { + e.preventDefault(); + cleanup(); + callback(id); + showZigzagToast(btn, '🗑️ Deleted!', 'error'); + playSound('delete'); + } else if (e.key === 'Escape' || e.key === 'n' || e.key === 'N') { + e.preventDefault(); + cleanup(); + } + }; + + yesBtn.onclick = () => { + cleanup(); + callback(id); + showZigzagToast(btn, '🗑️ Deleted!', 'error'); + playSound('delete'); + }; + noBtn.onclick = () => cleanup(); + + // Focus the confirm box so keyboard events are captured + confirm.tabIndex = 0; + confirm.focus(); + document.addEventListener('keydown', keyHandler); + + // Close if clicking outside + setTimeout(() => { + document.addEventListener('click', function closeConfirm(e) { + if (!confirm.contains(e.target) && e.target !== btn) { + cleanup(); + document.removeEventListener('click', closeConfirm); + } + }); + }, 10); +} +function entryPw(id) { const e = entries.find(x => x.id == id); return e ? e.password : ''; } +function showBatchConfirm(btn, message, callback) { + const existing = document.querySelector('.batch-confirm-overlay'); + if (existing) existing.remove(); + const overlay = document.createElement('div'); + overlay.className = 'batch-confirm-overlay'; + overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;z-index:9999;background:transparent;'; + const confirm = document.createElement('div'); + confirm.className = 'custom-confirm show'; + confirm.style.cssText = 'position:fixed;background:var(--bg2);border:1px solid var(--accent);border-radius:0.8rem;padding:0.7rem 1rem;z-index:10000;box-shadow:0 10px 30px rgba(0,0,0,0.5);font-size:0.8rem;color:var(--text);white-space:nowrap;'; + confirm.innerHTML = + '
' + message + '
' + + '
' + + '' + + '' + + '
'; + const rect = btn.getBoundingClientRect(); + confirm.style.top = (rect.top - 60) + 'px'; + let leftPos = rect.left - 20; + if (leftPos < 10) leftPos = 10; + confirm.style.left = leftPos + 'px'; + overlay.appendChild(confirm); + document.body.appendChild(overlay); + const yesBtn = confirm.querySelector('.confirm-yes'); + const noBtn = confirm.querySelector('.confirm-no'); + const cleanup = () => { overlay.remove(); document.removeEventListener('keydown', keyHandler); }; + const keyHandler = (e) => { + if (e.key === 'Enter' || e.key === 'y' || e.key === 'Y') { e.preventDefault(); cleanup(); callback(); playSound('delete'); } + else if (e.key === 'Escape' || e.key === 'n' || e.key === 'N') { e.preventDefault(); cleanup(); } + }; + yesBtn.onclick = () => { cleanup(); callback(); playSound('delete'); }; + noBtn.onclick = () => cleanup(); + overlay.onclick = (e) => { if (e.target === overlay) cleanup(); }; + confirm.tabIndex = 0; confirm.focus(); + document.addEventListener('keydown', keyHandler); +} +function attachEvents() { + document.querySelectorAll('.delete-btn').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); const id = parseInt(this.dataset.id); if (showTrash) { permanentDelete(id); } else { showConfirm(this, 'Delete this entry?', async id2 => { await delEntry(id2, true); await loadEntries(); toast('📦 Moved to trash', 'success', { label: '↩ Undo', cb: async () => { await restoreEntry(id2, true); await loadEntries(); toast('↩ Restored'); playSound('success'); } }); playSound('delete'); }); } }); + document.querySelectorAll('.edit-btn').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); openEdit(this.dataset.id); }); + document.querySelectorAll('.star-btn').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); toggleFavorite(this.dataset.id); }); + document.querySelectorAll('.copy-p').forEach(b => b.onclick = async function(ev) { ev.stopPropagation(); const pw = entryPw(this.dataset.id); try { await navigator.clipboard.writeText(pw); this.textContent = '✓'; const btn = this; setTimeout(() => { btn.textContent = '📋'; }, 1000); showZigzagToast(this, '📋 Copied!', 'success'); playSound('copy'); } catch (e) { showZigzagToast(this, 'Failed', 'error'); } }); + // Double-click entry to edit + document.querySelectorAll('[draggable="true"], .detail-card').forEach(el => { + el.addEventListener('dblclick', function(ev) { + const id = this.dataset.id; + if (id && !showTrash) { + ev.preventDefault(); + selectedIds.clear(); + selectedIds.add(parseInt(id)); + updateBatchBar(); + render(); + openEdit(id); + } + }); + }); + if (showView) { + document.querySelectorAll('.pw-display.pw-hover').forEach(el => { + el.addEventListener('mouseenter', function() { + const pw = entryPw(this.id.replace('p-', '')); + this.textContent = pw; + }); + el.addEventListener('mouseleave', function() { + this.textContent = '••••••••'; + }); + }); + } +} +function setupDrag() { + const c = document.getElementById('entriesContainer'); if (!c) return; + c.querySelectorAll('[draggable="true"]').forEach(el => { + el.ondragstart = function(e) { draggedId = this.dataset.id; const dragIds = selectedIds.has(parseInt(draggedId)) && selectedIds.size > 1 ? [...selectedIds] : [parseInt(draggedId)]; c.querySelectorAll('[draggable="true"]').forEach(card => { card.classList.toggle('drag-dim', dragIds.includes(parseInt(card.dataset.id))); }); e.dataTransfer.setData('text/plain', this.dataset.id); e.dataTransfer.effectAllowed = 'move'; if (dragIds.length > 1) { const cv = document.createElement('canvas'); cv.width = 100; cv.height = 50; const g = cv.getContext('2d'); for (let i = dragIds.length - 1; i >= 0; i--) { const ox = i * 4, oy = i * 4; g.fillStyle = i === 0 ? 'rgba(30,40,55,0.9)' : 'rgba(59,130,246,0.15)'; g.fillRect(ox, oy, 80, 36); g.strokeStyle = 'rgba(255,255,255,0.15)'; g.strokeRect(ox, oy, 80, 36); } g.fillStyle = 'rgba(0,0,0,0.7)'; g.fillRect(0, 34, 100, 16); g.fillStyle = '#fff'; g.font = '11px sans-serif'; g.textAlign = 'center'; g.fillText(dragIds.length + ' items', 50, 46); cv.style.position = 'fixed'; cv.style.top = '-1000px'; document.body.appendChild(cv); e.dataTransfer.setDragImage(cv, 6, 10); setTimeout(() => cv.remove(), 50); } }; + el.ondragend = function(e) { c.querySelectorAll('.drag-dim').forEach(card => card.classList.remove('drag-dim')); draggedId = null; c.querySelectorAll('.drag-over').forEach(x => x.classList.remove('drag-over')); document.getElementById('trashBtn')?.classList.remove('drag-over'); }; + el.ondragover = function(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (this.dataset.id !== draggedId) this.classList.add('drag-over'); }; + el.ondragleave = function(e) { this.classList.remove('drag-over'); }; + el.ondrop = function(e) { e.preventDefault(); e.stopPropagation(); this.classList.remove('drag-over'); const fromId = parseInt(e.dataTransfer.getData('text/plain')); const toId = parseInt(this.dataset.id); if (!fromId || !toId) return; const ids = selectedIds.has(fromId) && selectedIds.size > 1 ? [...selectedIds] : [fromId]; if (ids.length === 1 && ids[0] === toId) return; const base = order.length > 0 ? order : entries.filter(e => e).map(e => e.id); const filtered = base.filter(id => !ids.includes(id)); const idx = filtered.indexOf(toId); idx > -1 ? filtered.splice(idx, 0, ...ids) : filtered.push(...ids); order = filtered; // Ensure no entries are lost from order +entries.forEach(e => { if (!order.includes(e.id)) order.push(e.id); }); localStorage.setItem('entryOrder', JSON.stringify(order)); const map = new Map(entries.filter(e => e).map(e => [e.id, e])); entries = order.map(id => map.get(id)).filter(e => e); entries.sort((a, b) => (b.favorite || 0) - (a.favorite || 0)); render(); }; + }); +} + +// ==================== EDIT ==================== +function openEdit(id) { + let e = null; + for (let i = 0; i < entries.length; i++) { if (entries[i] && entries[i].id == id) { e = entries[i]; break; } } + if (!e) return; + const folderSelect = document.getElementById('editFolder'); + folderSelect.innerHTML = ''; + folders.forEach(f => { + if (!f) return; + const option = document.createElement('option'); + option.value = f; + option.textContent = '📁 ' + f; + if (f === (e.folder || 'All')) option.selected = true; + folderSelect.appendChild(option); + }); + document.getElementById('editId').value = id; + document.getElementById('editSite').value = e.site; + document.getElementById('editUsername').value = e.username; + document.getElementById('editPassword').value = e.password; + document.getElementById('editPassword').type = 'password'; + document.getElementById('editModal').classList.add('show'); + document.getElementById('editSite').focus(); + playSound('open'); +} +function closeEdit() { document.getElementById('editModal').classList.remove('show'); render(); playSound('close'); } +function toggleEditPassword() { const f = document.getElementById('editPassword'); f.type = f.type === 'password' ? 'text' : 'password'; } +async function saveEdit() { + const id = document.getElementById('editId').value; + const site = document.getElementById('editSite').value.trim(); + const username = document.getElementById('editUsername').value.trim(); + const password = document.getElementById('editPassword').value; + const folder = document.getElementById('editFolder').value; + if (!site || !password) { toast('Site and password required', 'error'); return; } + try { + const enc = await encryptPwd(password); + const r = await fetch(API + '/entries/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ site, username, encrypted_password: enc.encrypted, iv: enc.iv, folder }) }); + if (r.ok) { toast('✅ Updated!'); closeEdit(); loadEntries(); playSound('success'); } + else { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); } + } catch (e) { toast('⚠️ Error', 'error'); } +} +//======================== batch selection ========================== +function toggleSelectEntry(id, e) { + if (e?.shiftKey && lastSelectedId !== null) { + const ids = getFilteredEntries().map(x => x.id); + const i1 = ids.indexOf(lastSelectedId); + const i2 = ids.indexOf(id); + if (i1 > -1 && i2 > -1) { + const start = Math.min(i1, i2), end = Math.max(i1, i2); + for (let i = start; i <= end; i++) selectedIds.add(ids[i]); + } + } else if (e?.ctrlKey || e?.metaKey) { + if (selectedIds.has(id)) selectedIds.delete(id); else selectedIds.add(id); + } else { + if (selectedIds.size === 1 && selectedIds.has(id)) { selectedIds.clear(); } + else { selectedIds.clear(); selectedIds.add(id); } + } + lastSelectedId = id; + arrowAnchor = -1; + arrowFocus = -1; + updateBatchBar(); + render(); +} + +function clearSelection() { + selectedIds.clear(); + lastSelectedId = null; + arrowAnchor = -1; + arrowFocus = -1; + hideBatchBar(); + render(); +} + +function updateBatchBar() { + const existing = document.getElementById('batchBar'); + if (existing) existing.remove(); + if (selectedIds.size === 0) return; + const bar = document.createElement('div'); + bar.id = 'batchBar'; + bar.className = 'batch-actions'; + bar.innerHTML = `${selectedIds.size} selected`; + if (showTrash) { + bar.innerHTML += ` + + + `; + } else { + bar.innerHTML += ` + + + + `; + } + bar.innerHTML += ``; + document.body.appendChild(bar); +} + +function hideBatchBar() { + const bar = document.getElementById('batchBar'); + if (bar) bar.remove(); +} + +async function batchDelete() { + const count = selectedIds.size; + const ids = [...selectedIds]; + const btn = document.querySelector('#batchBar .btn-danger'); + showBatchConfirm(btn || document.body, 'Move ' + count + ' entries to trash?', async () => { + for (const id of ids) await delEntry(id, true); + clearSelection(); + await loadEntries(); + toast('📦 Moved ' + count + ' entries to trash', 'success', { + label: '↩ Undo', + cb: async () => { for (const id of ids) await restoreEntry(id, true); await loadEntries(); toast('↩ Restored ' + ids.length + ' entries'); playSound('success'); }, + onExpire: null + }); + playSound('delete'); + }); +} + +async function batchPermanentDelete() { + const count = selectedIds.size; + const btn = document.querySelector('#batchBar .btn-danger'); + showBatchConfirm(btn || document.body, 'Permanently delete ' + count + ' entries?', async () => { + for (const id of selectedIds) await permanentDelete(id, true); + toast('🗑️ Permanently deleted ' + count + ' entries'); + playSound('error'); + clearSelection(); + await loadEntries(); + }); +} + +async function batchRestore() { + const count = selectedIds.size; + for (const id of selectedIds) await restoreEntry(id, true); + toast('✅ Restored ' + count + ' entries'); + playSound('success'); + clearSelection(); + await loadEntries(); +} + +async function batchMove() { + const folder = document.getElementById('batchFolder')?.value || 'All'; + for (const id of selectedIds) { + const e = entries.find(x => x.id == id); + if (e) { + e.folder = folder; + const enc = await encryptPwd(e.password); + await fetch(API + '/entries/' + id, { + method: 'PUT', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken }, + body: JSON.stringify({ site: e.site, username: e.username, encrypted_password: enc.encrypted, iv: enc.iv, folder }) + }); + } + } + clearSelection(); + loadEntries(); +} +// ==================== ADD / DELETE ==================== +async function addEntry() { + const site = document.getElementById('addSite').value.trim(); + const user = document.getElementById('addUsername').value.trim(); + const pass = document.getElementById('addPassword').value; + if (!site || !pass) { toast('❌ Site and password required', 'error'); return; } + if (user) localStorage.setItem('savedUsername', user); + const btn = document.getElementById('addEntryBtn'); + btn.disabled = true; + try { + const enc = await encryptPwd(pass); + const folder = document.getElementById('addFolder').value; + const r = await fetch(API + '/entries', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken }, + body: JSON.stringify({ site, username: user, encrypted_password: enc.encrypted, iv: enc.iv, encryption_method: 'client', folder }) + }); + if (r.ok) { + closeAdd(); + toast('✅ Saved!'); + loadEntries(); + playSound('success'); + } else { + const d = await r.json(); + toast('❌ ' + (d.error || 'Failed'), 'error'); + } + } catch (e) { toast('⚠️ Error', 'error'); } + finally { btn.disabled = false; } +} +async function delEntry(id, noToast) { + try { + const r = await fetch(API + '/entries/' + id, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken } }); + if (r.ok) { selectedIds.delete(id); order = order.filter(x => x != id); localStorage.setItem('entryOrder', JSON.stringify(order)); if (!noToast) { toast('📦 Moved to trash'); await loadEntries(); playSound('delete'); } } + } catch (e) { if (!noToast) toast('Error', 'error'); } +} + +// ==================== UTILS ==================== +function searchEntries() { + const input = document.getElementById('searchInput'); + searchQuery = input.value.trim(); + const btn = document.getElementById('clearSearchBtn'); + if (btn) btn.style.display = searchQuery ? 'block' : 'none'; + loadEntries(searchQuery); +} +function showExportModal() { + const overlay = document.createElement('div'); + overlay.className = 'custom-modal-overlay show'; + overlay.innerHTML = `

📤 Export Passwords

Re-enter master password to export plaintext passwords

`; + document.body.appendChild(overlay); + document.getElementById('cancelExport').onclick = () => overlay.remove(); + document.getElementById('confirmExport').onclick = async () => { + const pwd = document.getElementById('exportPassword').value; + if (!pwd) { toast('Enter your master password', 'error'); return; } + try { + const r = await fetch(API + '/reauth', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken }, + body: JSON.stringify({ masterPassword: pwd }) + }); + if (r.ok) { + overlay.remove(); + const b = new Blob([JSON.stringify(entries, null, 2)], { type: 'application/json' }); + const a = document.createElement('a'); + a.href = URL.createObjectURL(b); + a.download = 'vault-' + new Date().toISOString().slice(0, 10) + '.json'; + a.click(); + URL.revokeObjectURL(a.href); + toast('Exported!'); + playSound('success'); + } else { + toast('❌ Invalid password', 'error'); + } + } catch (e) { toast('⚠️ Error', 'error'); } + }; + overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); + playSound('open'); +} +function esc(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; } +function escRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } +function highlightText(text, query) { + if (!query || !query.trim()) return esc(text); + const re = new RegExp('(' + escRegex(query.trim()) + ')', 'gi'); + return esc(text).replace(re, '$1'); +} +function showShortcutsHelp() { + const overlay = document.createElement('div'); + overlay.className = 'custom-modal-overlay show'; + overlay.innerHTML = `

⌨️ Keyboard Shortcuts

Alt+NNew entryCtrl+ASelect allCtrl+FSearchAlt+TToggle trashCtrl+LLock vaultCtrl+SSave entryDelDelete selectedEscClose modal / deselect◀ ▶Detail view nav?Show this help

💡 Click any entry to select, Shift+click for range, Ctrl+click to toggle

`; + document.body.appendChild(overlay); + overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); }); +} +function clearSearch() { + const input = document.getElementById('searchInput'); + input.value = ''; + searchQuery = ''; + const btn = document.getElementById('clearSearchBtn'); + if (btn) btn.style.display = 'none'; + loadEntries(); + input.focus(); +} +// ==================== STARTUP – session persistence ==================== +init(); +applyTheme(); + +if (token && curUser) { + (async () => { + if (await restoreCryptoKey()) { + await loadFolders(); + showVault(); + loadEntries(); + } else { + sessionStorage.clear(); + token = null; + curUser = null; + document.getElementById('loginUsername').value = localStorage.getItem('savedLoginUser') || ''; + } + })(); +} + +['click', 'keypress', 'scroll', 'mousemove'].forEach(e => document.addEventListener(e, () => { if (token) resetIdle(); })); +// Trash button as drop target (set up once, outside setupDrag to avoid duplicates) +(function() { + const trashBtn = document.getElementById('trashBtn'); + if (trashBtn) { + trashBtn.addEventListener('dragover', e => { if (!showTrash) { e.preventDefault(); trashBtn.classList.add('drag-over'); } }); + trashBtn.addEventListener('dragleave', () => trashBtn.classList.remove('drag-over')); + trashBtn.addEventListener('drop', async function(e) { + e.preventDefault(); + this.classList.remove('drag-over'); + const id = parseInt(e.dataTransfer.getData('text/plain')); + if (!id) return; + const ids = selectedIds.has(id) && selectedIds.size > 1 ? [...selectedIds] : [id]; + for (const sid of ids) await delEntry(sid, true); + clearSelection(); + await loadEntries(); + toast('📦 Moved ' + ids.length + ' entries to trash', 'success', { + label: '↩ Undo', + cb: async () => { for (const sid of ids) await restoreEntry(sid, true); await loadEntries(); toast('↩ Restored ' + ids.length + ' entries'); playSound('success'); } + }); + playSound('delete'); + }); + } +})(); + +// Prevent native drag on non-card elements (table headers, text, etc.) +document.getElementById('entriesContainer').addEventListener('dragstart', function(e) { + if (!e.target?.closest?.('[draggable="true"]')) e.preventDefault(); +}); + +// Document mousedown: rect selection on vault background, clear outside vault +document.addEventListener('mousedown', function(e) { + if (e.button !== 0 || rectState.active) return; + if (e.target?.closest?.('.entry-card,.entry-row,.entry-compact,.table-row-drag,.detail-card,.detail-nav,#batchBar,.custom-modal-overlay.show,.edit-modal.show,.modal-overlay.show,#genModal,#settingsMenu,.batch-confirm-overlay')) return; + if (e.target?.closest?.('button,input,select,.folders-bar,.toolbar,#trashActions,.settings-dropdown,.fab,.auth-section')) { + if (selectedIds.size > 0) clearSelection(); + return; + } + if (e.target?.closest?.('.vault')) { + rectState.active = true; + rectState.startX = e.clientX; + rectState.startY = e.clientY; + rectState.started = false; + rectState.el = null; + selectedIds.clear(); + lastSelectedId = null; + hideBatchBar(); + document.getElementById('entriesContainer')?.querySelectorAll('.selected').forEach(el => el.classList.remove('selected')); + } else if (selectedIds.size > 0) { + clearSelection(); + } +}); +document.addEventListener('mousemove', function(e) { + if (!rectState.active) return; + const dx = e.clientX - rectState.startX; + const dy = e.clientY - rectState.startY; + if (!rectState.started && (dx > 5 || dx < -5 || dy > 5 || dy < -5)) { + rectState.started = true; + rectState.el = document.createElement('div'); + rectState.el.id = 'rectSelect'; + document.body.appendChild(rectState.el); + } + if (rectState.el) { + const x = Math.min(rectState.startX, e.clientX); + const y = Math.min(rectState.startY, e.clientY); + const w = Math.abs(dx); + const h = Math.abs(dy); + rectState.el.style.cssText = `left:${x}px;top:${y}px;width:${w}px;height:${h}px;display:block;position:fixed;pointer-events:none;z-index:999;border:1px solid var(--accent);background:rgba(59,130,246,0.1);`; + } +}); +document.addEventListener('mouseup', function(e) { + if (!rectState.active) return; + rectState.active = false; + if (rectState.el) { + rectState.el.remove(); + rectState.el = null; + if (rectState.started) { + const r = { + left: Math.min(rectState.startX, e.clientX), + top: Math.min(rectState.startY, e.clientY), + right: Math.max(rectState.startX, e.clientX), + bottom: Math.max(rectState.startY, e.clientY) + }; + const container = document.getElementById('entriesContainer'); + if (container) { + container.querySelectorAll('.entry-card,.entry-row,.entry-compact,.table-row-drag').forEach(el => { + const er = el.getBoundingClientRect(); + if (er.left < r.right && er.right > r.left && er.top < r.bottom && er.bottom > r.top) { + const id = parseInt(el.dataset.id); + if (id) selectedIds.add(id); + } + }); + } + if (selectedIds.size > 0) { updateBatchBar(); render(); } + } + } +}); +document.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.ctrlKey && !e.altKey && !e.metaKey) { const a = document.activeElement; if (!a || a.tagName === 'BUTTON') return; e.preventDefault(); if (document.getElementById('editModal').classList.contains('show') && a.closest('.edit-box')) saveEdit(); else if (document.getElementById('addModal').classList.contains('show') && a.closest('.modal-box')) addEntry(); else if (!document.getElementById('authSection').classList.contains('hidden')) { if (a.id === 'loginUsername' || a.id === 'loginPassword') login(); else if (a.id === 'regPassword') register(); } } }); +document.getElementById('genModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGen(); }); +document.getElementById('editModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeEdit(); }); +// ==================== KEYBOARD SHORTCUTS ==================== +document.addEventListener('keydown', function(e) { + const tag = document.activeElement?.tagName; + const isInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT'; + + // Escape – close any open modal or settings + if (e.key === 'Escape') { + if (rectState.active || rectState.el) { rectState.active = false; if (rectState.el) { rectState.el.remove(); rectState.el = null; } clearSelection(); return; } + if (selectedIds.size > 0) { clearSelection(); return; } + if (!document.getElementById('settingsMenu').classList.contains('hidden')) { + document.getElementById('settingsMenu').classList.add('hidden'); + return; + } + if (document.getElementById('addModal').classList.contains('show')) { closeAdd(); return; } + if (document.getElementById('editModal').classList.contains('show')) { closeEdit(); return; } + if (document.getElementById('genModal').style.display === 'flex') { closeGen(); return; } + const confirm = document.querySelector('.custom-confirm.show'); + if (confirm) confirm.remove(); + return; + } + + // Arrow keys for detail view navigation + if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && !isInput && view === 'detail' && document.getElementById('authSection').classList.contains('hidden')) { + e.preventDefault(); + goDetail(e.key === 'ArrowLeft' ? -1 : 1); + return; + } + + // Arrow keys — navigate entries in vault + if ((e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') && !isInput && document.getElementById('authSection').classList.contains('hidden') && view !== 'detail' && !document.getElementById('addModal').classList.contains('show') && !document.getElementById('editModal').classList.contains('show')) { + e.preventDefault(); + const filtered = getFilteredEntries(); + if (!filtered.length) return; + const isNext = e.key === 'ArrowDown' || e.key === 'ArrowRight'; + let idx = arrowFocus >= 0 ? arrowFocus : -1; + if (idx < 0 && selectedIds.size > 0) { + const firstId = [...selectedIds][0]; + idx = filtered.findIndex(e => e.id == firstId); + } + if (view === 'grid' && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) { + if (idx < 0) idx = 0; + else { const cols = getGridCols(); if (e.key === 'ArrowDown') { const next = idx + cols; idx = next < filtered.length ? next : idx; } else { const prev = idx - cols; idx = prev >= 0 ? prev : idx; } } + } else { + if (isNext) idx = idx < filtered.length - 1 ? idx + 1 : 0; + else idx = idx > 0 ? idx - 1 : filtered.length - 1; + } + if (e.shiftKey) { + if (arrowAnchor < 0) arrowAnchor = idx; + arrowFocus = idx; + const start = Math.min(arrowAnchor, arrowFocus), end = Math.max(arrowAnchor, arrowFocus); + selectedIds.clear(); + for (let i = start; i <= end; i++) selectedIds.add(filtered[i].id); + } else { + selectedIds.clear(); + selectedIds.add(filtered[idx].id); + arrowAnchor = idx; + arrowFocus = idx; + } + updateBatchBar(); render(); playSound('click'); + return; + } + + // Enter — open edit for single selected entry + if (e.key === 'Enter' && !isInput && selectedIds.size === 1 && document.getElementById('authSection').classList.contains('hidden') && !document.getElementById('editModal').classList.contains('show') && !document.getElementById('addModal').classList.contains('show')) { + e.preventDefault(); + openEdit([...selectedIds][0]); + return; + } + + // ? or / to show shortcuts help (only in vault) + if ((e.key === '?' || e.key === '/') && !isInput) { + if (!document.getElementById('authSection').classList.contains('hidden')) return; + e.preventDefault(); + showShortcutsHelp(); + return; + } + + // Delete key — move to trash or permanently delete selected entries + if (e.key === 'Delete' && !isInput && selectedIds.size > 0 && document.getElementById('authSection').classList.contains('hidden')) { + e.preventDefault(); + if (showTrash) batchPermanentDelete(); + else batchDelete(); + return; + } + + // Alt+N — New entry (Ctrl+N intercepted by browser) + if (e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey && (e.key === 'n' || e.key === 'N') && !isInput && !document.getElementById('addModal').classList.contains('show') && document.getElementById('authSection').classList.contains('hidden')) { + e.preventDefault(); + openAdd(); + return; + } + + // Alt+T — Toggle trash (Ctrl+T intercepted by browser) + if (e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey && (e.key === 't' || e.key === 'T') && !isInput && document.getElementById('authSection').classList.contains('hidden')) { + e.preventDefault(); + toggleTrash(); + return; + } + + // Only handle Ctrl+[key], no Shift/Alt/Meta + if (!e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return; + + // Prevent browser defaults for ALL our shortcuts BEFORE dispatching + const code = e.code; + if (code === 'KeyF' || code === 'KeyL' || code === 'KeyS') { + e.preventDefault(); + } + + if (e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey && (e.key === 'a' || e.key === 'A') && !isInput && document.getElementById('authSection').classList.contains('hidden')) { + e.preventDefault(); + getFilteredEntries(view === 'grouped' || view === 'detail').forEach(e => selectedIds.add(e.id)); + updateBatchBar(); + render(); + playSound('click'); + return; + } + + if (code === 'KeyF') { + const el = document.getElementById('searchInput'); + if (el) { el.focus(); el.select(); } + } else if (code === 'KeyL') { + if (!isInput) doLogout(); + } else if (code === 'KeyS') { + if (document.getElementById('addModal').classList.contains('show')) addEntry(); + else if (document.getElementById('editModal').classList.contains('show')) saveEdit(); + } +}); +document.getElementById('loginUsername').focus(); +document.querySelectorAll('.fab').forEach(b => b.classList.add('hidden')); \ No newline at end of file diff --git a/js/app.js b/js/app.js index a77f394..fba7b17 100644 --- a/js/app.js +++ b/js/app.js @@ -13,6 +13,7 @@ const API = (location.pathname.indexOf('/password-manager/') === 0) // ---- Delphi native bridge ---------------------------------- // Active only when running inside the Delphi-hosted WebView2 (API === ''). // Falls back to navigator.clipboard for the standalone PHP frontend. +const prefResolvers = {}; const Bridge = (() => { const active = (API === ''); @@ -35,10 +36,17 @@ const Bridge = (() => { return true; }, - // Called by Delphi (ExecuteJavaScript) on WTS_SESSION_LOCK. - // Exposed as window.Bridge.onSystemLock so the Delphi side can call it, - // but the actual lock is triggered directly via lockVault() in Delphi. + // Called by Delphi (ExecuteJavaScript) on WTS_SESSION_LOCK and + // PBT_APMSUSPEND. When quick-unlock is enabled on this device the + // DPAPI blob already gates access via the Windows user account, so + // re-locking is redundant — we just stay unlocked and the user is + // back where they left off when they return. onSystemLock() { + if (state.quickUnlockEnabled) { + if (typeof toast === 'function') + toast('System lock — vault kept unlocked (quick unlock active)'); + return; + } if (typeof lockVault === 'function') lockVault(); }, @@ -55,6 +63,131 @@ const Bridge = (() => { if (typeof toast === 'function') toast('Welcome back'); } }, + + // ---- Autofill (Ctrl+Shift+L / Ctrl+Shift+P global hotkeys) ---------- + + // Called by Delphi when a hotkey fires. windowTitle = foreground + // window title at hotkey time. kind = "full" (user+Tab+pwd) or + // "password" (password only — for step-2 forms, unlock screens). + onAutofillRequest(windowTitle, kind) { + autofillHandleRequest(windowTitle, kind || 'full'); + }, + + // Called by Delphi on Ctrl+Shift+A. Opens the new-entry modal with + // the foreground window title pre-filled (browser suffix stripped). + onNewEntryFromTitle(windowTitle) { + if (!state.cryptoKey || state.locked || !state.token) { + toast('Unlock the vault first', 'warning'); + return; + } + const cleaned = autofillStripBrowserSuffix(windowTitle); + openEntryModal(); + setTimeout(() => { + const titleField = $('#entryTitle'); + if (titleField) { + titleField.value = cleaned; + $('#entrySite').focus(); + } + }, 0); + }, + + // Tell Delphi to simulate keystrokes. Empty username = password only + // (no Tab is sent). + executeAutofill(username, password) { + if (!active) return; + cmd('cmd://autofill/execute?username=' + encodeURIComponent(username) + + '&password=' + encodeURIComponent(password)); + }, + + // Ask Delphi to bring the main window to front (used when the + // multi-match picker opens, so the user definitely sees it even + // if the app was minimised to tray or hidden behind other apps). + focusApp() { + if (!active) return; + cmd('cmd://app/focus'); + }, + + // Tell Delphi the page is rendered and waiting for input — Delphi + // calls WebBrowser.SetFocus (the FMX control needs OS-level focus + // before any input.focus() inside the DOM can work) then injects + // a focus script targeting the visible auth field. + appReady() { + if (!active) return; + cmd('cmd://app/ready'); + }, + + // Sync the Windows title bar with the app theme (dark vs light). + // Calls DwmSetWindowAttribute DWMWA_USE_IMMERSIVE_DARK_MODE on the + // form's HWND. No-op on Windows < 10 build 19044. + syncTitleBarTheme(mode) { + if (!active) return; + cmd('cmd://app/theme?mode=' + (mode === 'light' ? 'light' : 'dark')); + }, + + // Tell Delphi we couldn't find a match / user cancelled. + cancelAutofill() { + if (!active) return; + cmd('cmd://autofill/cancel'); + }, + + // Sync the hotkey registration state with this device's preference. + configureAutofill(enabled) { + if (!active) return; + cmd('cmd://autofill/configure?enabled=' + (enabled ? '1' : '0')); + }, + + // Send the full hotkey configuration to Delphi (enabled + combos). + // combos = { full: {ctrl,shift,alt,win,key}, password: {…} }. + setAutofillHotkeys(enabled, combos) { + if (!active) return; + const f = autofillComboToWin32(combos.full); + const p = autofillComboToWin32(combos.password); + cmd('cmd://autofill/hotkeys?enabled=' + (enabled ? '1' : '0') + + '&full_mods=' + f.mods + '&full_vk=' + f.vk + + '&pwd_mods=' + p.mods + '&pwd_vk=' + p.vk); + }, + + // Called by Delphi after a setAutofillHotkeys request, with true if + // BOTH combos registered successfully, false otherwise (clash with + // another app holding a global hotkey). Surfaces a toast. + onAutofillHotkeysResult(allOk) { + if (allOk) { + toast('Autofill hotkeys updated'); + } else { + toast('Autofill: one or both hotkeys are already used by another app', 'warning'); + } + }, + + // ---- Device-bound prefs (DPAPI-backed) ---------------------------- + // localStorage is keyed by origin, and our HTTP port is random on + // every launch — so anything we put there is wiped at reboot. For + // prefs that need to survive a reboot (remembered username, etc.), + // round-trip through Delphi which persists via DPAPI. + getPref(key) { + if (!active) return Promise.resolve(''); + return new Promise(resolve => { + prefResolvers[key] = resolve; + cmd('cmd://prefs/get?key=' + encodeURIComponent(key)); + setTimeout(() => { + if (prefResolvers[key] === resolve) { + delete prefResolvers[key]; + resolve(''); + } + }, 2000); + }); + }, + setPref(key, value) { + if (!active) return; + cmd('cmd://prefs/set?key=' + encodeURIComponent(key) + + '&value=' + encodeURIComponent(value || '')); + }, + onPrefResult(key, value) { + const r = prefResolvers[key]; + if (r) { + delete prefResolvers[key]; + r(value || ''); + } + }, }; })(); @@ -74,6 +207,7 @@ const state = { cryptoKey: null, entries: [], trashed: [], + trashedCount: 0, // server-side count, updated separately from state.trashed folders: ['All'], view: 'all', // 'all' | 'favorites' | 'folder:' | 'tag:' | 'trash' search: '', @@ -83,14 +217,39 @@ const state = { autoLock: parseInt(localStorage.getItem('autoLockMin') || '5'), askBeforeDelete: localStorage.getItem('askBeforeDelete') !== '0', // default true maskUsernames: localStorage.getItem('maskUsernames') === '1', // default false + // Show the raw site/URL under the display name on cards. Default OFF + // because the display name is meant to be the user-friendly label; + // most users don't want the hostname cluttering the card layout. + showSiteOnCards: localStorage.getItem('showSiteOnCards') === '1', // default false compactActions: localStorage.getItem('compactActions') === '1', // default false - viewMode: localStorage.getItem('viewMode') || 'cards', // 'cards' | 'list' + viewMode: localStorage.getItem('viewMode') || 'cards', // 'cards' | 'list' | 'table' + // Sort criterion + direction. Defaults: alphabetical by display name — + // the most common pattern for a password manager (predictable lookup). + // Other values: 'updated' (last modified), 'created' (creation order), + // 'site' (raw site/URL, distinct from name when user set a title). + sortBy: localStorage.getItem('sortBy') || 'name', + sortDir: localStorage.getItem('sortDir') || 'asc', + pageSize: parseInt(localStorage.getItem('pageSize') || '25') || 25, + currentPage: 1, checked: new Set(), // entry IDs checked for batch operations hibpEnabled: localStorage.getItem('hibpEnabled') === '1', // default OFF // entry.id → count from HIBP (0 = clean, >0 = pwned, undefined = unchecked) hibpResults: new Map(), quickUnlockEnabled: localStorage.getItem('quickUnlockEnabled') === '1', recoveryConfigured: false, // refreshed by refreshRecoveryStatus on Settings open + autofillEnabled: localStorage.getItem('autofillEnabled') !== '0', // default ON + // Hotkey combos. Each combo = { ctrl, shift, alt, win, key }. + // key is the uppercase character or VK label ('A'..'Z', '0'..'9', + // 'F1'..'F12'). Default: Ctrl+Shift+L / Ctrl+Shift+P. Combos are + // local-by-default but ALSO synced via settings_json so they travel + // with the user (still Windows-only at runtime — non-Windows clients + // ignore the value). + autofillHotkeyFull: JSON.parse(localStorage.getItem('autofillHotkeyFull') || + '{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"L"}'), + autofillHotkeyPwd: JSON.parse(localStorage.getItem('autofillHotkeyPwd') || + '{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"P"}'), + sidebarCollapsed: JSON.parse(localStorage.getItem('sidebarCollapsed') || + '{"folders":false,"tags":false,"tools":false}'), }; // ============================================================ @@ -317,6 +476,105 @@ async function decryptTotpSecret(encB64, ivB64) { return await decryptPwd(encB64, ivB64); } +// Generate a cryptographically random RFC 4648 base32 secret. 20 bytes = +// 160 bits → 32 base32 chars, RFC 6238 §5.1 recommended TOTP key size. +function randomBase32Secret(numBytes) { + numBytes = numBytes || 20; + const ALPH = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + const bytes = crypto.getRandomValues(new Uint8Array(numBytes)); + let bits = 0, buffer = 0, out = ''; + for (let i = 0; i < bytes.length; i++) { + buffer = (buffer << 8) | bytes[i]; + bits += 8; + while (bits >= 5) { + bits -= 5; + out += ALPH[(buffer >> bits) & 0x1F]; + } + } + if (bits > 0) out += ALPH[(buffer << (5 - bits)) & 0x1F]; + return out; +} + +// ---- Standalone TOTP generator modal (paste secret → live code) ----- +let totpToolTimer = null; +function openTotpTool() { + const modal = document.getElementById('totpToolModal'); + const input = document.getElementById('totpToolSecret'); + const codeEl = document.getElementById('totpToolCode'); + const barEl = document.getElementById('totpToolBar'); + const errEl = document.getElementById('totpToolError'); + modal.classList.remove('is-hidden'); + input.value = ''; + codeEl.textContent = '— — — — — —'; + barEl.style.width = '100%'; + errEl.style.display = 'none'; + setTimeout(() => input.focus(), 0); + + async function tick() { + let secret = input.value.trim(); + if (!secret) { + codeEl.textContent = '— — — — — —'; + barEl.style.width = '100%'; + errEl.style.display = 'none'; + return; + } + if (secret.toLowerCase().startsWith('otpauth://')) { + const fromUri = parseOtpAuthUri(secret); + if (fromUri) { secret = fromUri; input.value = fromUri; } + } + try { + const t = await generateTOTP(secret); + codeEl.textContent = t.code.replace(/(\d{3})(\d{3})/, '$1 $2'); + const newPct = (t.secondsLeft / 30) * 100; + if (newPct > (tick._lastPct || 0) + 5) { + barEl.style.transition = 'none'; + barEl.style.width = newPct.toFixed(1) + '%'; + void barEl.offsetWidth; + barEl.style.transition = ''; + } else { + barEl.style.width = newPct.toFixed(1) + '%'; + } + tick._lastPct = newPct; + errEl.style.display = 'none'; + } catch (e) { + codeEl.textContent = '— — — — — —'; + barEl.style.width = '0%'; + errEl.textContent = 'Invalid base32 secret'; + errEl.style.display = ''; + } + } + input.addEventListener('input', tick); + if (totpToolTimer) clearInterval(totpToolTimer); + totpToolTimer = setInterval(tick, 1000); + + document.getElementById('totpToolCopy').onclick = async () => { + const code = codeEl.textContent.replace(/\s/g, ''); + if (!/^\d{6}$/.test(code)) return; + if (Bridge.active) Bridge.copySecure(code, 30000); + else { try { await navigator.clipboard.writeText(code); } catch (e) {} } + toast('TOTP code copied'); + }; + document.getElementById('totpToolGen').onclick = () => { + input.value = randomBase32Secret(20); + tick(); + toast('Random secret generated'); + }; + document.getElementById('totpToolCopySecret').onclick = async () => { + const s = input.value.trim(); + if (!s) return; + if (Bridge.active) Bridge.copySecure(s, 30000); + else { try { await navigator.clipboard.writeText(s); } catch (e) {} } + toast('Secret copied'); + }; + modal.querySelectorAll('[data-close]').forEach(b => { + b.onclick = () => closeTotpTool(); + }); +} +function closeTotpTool() { + document.getElementById('totpToolModal').classList.add('is-hidden'); + if (totpToolTimer) { clearInterval(totpToolTimer); totpToolTimer = null; } +} + // ============================================================ // HIBP — Have I Been Pwned breach check (k-anonymity) // ============================================================ @@ -633,8 +891,20 @@ async function doLogin(e) { sessionStorage.setItem('salt', state.salt); sessionStorage.setItem('username', state.username); sessionStorage.setItem('kdfIterations', String(state.kdfIterations)); + // Persist via DPAPI when running inside the Delphi host (localStorage + // is wiped on each restart because the HTTP port — and therefore the + // origin — changes every launch). Fall back to localStorage for the + // web/PHP frontend where the origin is stable. + const remember = $('#loginRememberUser').checked; + if (Bridge.active) { + Bridge.setPref('rememberedUsername', remember ? u : ''); + } else { + if (remember) localStorage.setItem('rememberedUsername', u); + else localStorage.removeItem('rememberedUsername'); + } // cryptoKey is already derived — no second PBKDF2 pass. state.cryptoKey = derived.cryptoKey; + state.justRecovered = false; await persistCryptoKey(); toast('Welcome back, ' + u); await enterApp(); @@ -739,6 +1009,13 @@ function lockVault() { state.entries = []; state.trashed = []; state.locked = true; + state.justRecovered = false; + if (typeof authTickTimer !== 'undefined' && authTickTimer) { + clearInterval(authTickTimer); authTickTimer = null; + } + if (typeof totpToolTimer !== 'undefined' && totpToolTimer) { + clearInterval(totpToolTimer); totpToolTimer = null; + } showAuth(); // Two UI variants for the auth screen: @@ -838,9 +1115,17 @@ async function loadTrash() { try { const r = await api('/entries?deleted=1', { headers: authHeaders() }); state.trashed = Array.isArray(r) ? r : []; + state.trashedCount = state.trashed.length; } catch (e) { state.trashed = []; } } +async function loadEntryCounts() { + try { + const r = await api('/entries/count', { headers: authHeaders() }); + if (r && typeof r.trashed === 'number') state.trashedCount = r.trashed; + } catch (e) { /* silent — sidebar count just stays 0 */ } +} + // ============================================================ // FILTERS / DERIVED // ============================================================ @@ -855,7 +1140,10 @@ function filteredEntries() { if (state.view === 'favorites') list = list.filter(e => e.favorite); else if (state.view.startsWith('folder:')) { const f = state.view.slice(7); - if (f !== 'All') list = list.filter(e => e.folder === f); + // 'folder:All' is now the "(no folder)" pseudo-entry → filter + // to entries that are uncategorized (folder is 'All', empty, + // or absent). All other folder names are exact-matched. + list = list.filter(e => (e.folder || 'All') === f); } else if (state.view.startsWith('tag:')) { const t = state.view.slice(4); list = list.filter(e => parseTags(e.tags).includes(t)); @@ -865,10 +1153,16 @@ function filteredEntries() { const q = state.search.toLowerCase(); list = list.filter(e => (e.site || '').toLowerCase().includes(q) || + (e.title || '').toLowerCase().includes(q) || (e.username || '').toLowerCase().includes(q) || (e.tags || '').toLowerCase().includes(q) ); } + // Trash view keeps the deletion order (newest first) — re-sorting feels + // wrong for a recoverable archive. All other views honour user choice. + if (state.view !== 'trash') { + list = sortEntries(list, state.sortBy, state.sortDir); + } return list; } @@ -887,6 +1181,7 @@ function viewTitle() { if (state.view === 'all') return 'All items'; if (state.view === 'favorites') return 'Favorites'; if (state.view === 'trash') return 'Trash'; + if (state.view === 'authenticator') return 'Authenticator'; if (state.view.startsWith('folder:')) return state.view.slice(7); if (state.view.startsWith('tag:')) return '# ' + state.view.slice(4); return 'Items'; @@ -905,17 +1200,32 @@ function renderSidebar() { // counts $('#countAll').textContent = state.entries.length; $('#countFav').textContent = state.entries.filter(e => e.favorite).length; - $('#countTrash').textContent = state.trashed.length || ''; + // Prefer the server-side count (always up-to-date even if user never + // navigated to Trash this session) ; fall back to local array length. + const trashN = state.trashedCount || state.trashed.length || 0; + $('#countTrash').textContent = trashN || ''; + + // Section totals — shown next to the section header so the user still + // sees the count when the section is collapsed. + const folderCount = state.folders.filter(n => n !== 'All').length; + const tagCount = allTags().length; + const fc = document.getElementById('countFolders'); + const tc = document.getElementById('countTags'); + if (fc) fc.textContent = String(folderCount); + if (tc) tc.textContent = String(tagCount); // active state for top-level items $$('#appShell .nav-item[data-view]').forEach(n => { n.classList.toggle('is-active', n.dataset.view === state.view); }); - // folders + // folders — hide the special "All" container (it doubles up with the + // "All items" view in the top nav and confuses users with two "All" + // entries that count different sets). Entries with folder='All' are + // still reachable via "All items". const fList = $('#foldersList'); fList.innerHTML = ''; - state.folders.forEach(name => { + state.folders.filter(n => n !== 'All').forEach(name => { const count = state.entries.filter(e => e.folder === name).length; const key = 'folder:' + name; const item = el('button', { @@ -937,9 +1247,54 @@ function renderSidebar() { if (id) await moveEntryToFolder(parseInt(id), name); }); + const delBtn = el('button', { + class: 'folder-delete', + type: 'button', + title: 'Delete folder', + on: { click: ev => { ev.stopPropagation(); deleteFolder(name, count); } }, + }); + delBtn.appendChild(icon('i-x')); + item.appendChild(delBtn); + fList.appendChild(item); }); + // "(no folder)" pseudo-entry: filters to entries with no real folder + // (folder is empty or the default "All"). Skipped when there are zero + // such entries so the sidebar stays clean for organised users. + // No folder icon — visually communicates "this is the absence of a + // folder, not a folder". Drag target so users can quickly uncategorise. + const uncatCount = state.entries.filter(e => !e.folder || e.folder === 'All').length; + if (uncatCount > 0) { + const key = 'folder:All'; + const item = el('button', { + class: 'nav-item is-uncategorized' + (state.view === key ? ' is-active' : ''), + 'data-folder': 'All', + on: { click: () => setView(key) }, + }); + // Spacer instead of the folder icon — keeps alignment with real + // folders without implying "this is a folder". MUST NOT be a + // because .nav-item > span:first-of-type { flex: 1 } would target + // the spacer instead of the label and push the count off to the + // right. Other nav-items have as their first child, so the + // label span naturally wins :first-of-type; we mimic that by + // making the spacer a non-span element. + item.appendChild(el('i', { class: 'nav-icon-spacer' })); + item.appendChild(el('span', null, '(no folder)')); + item.appendChild(el('span', { class: 'nav-count' }, String(uncatCount))); + + item.addEventListener('dragover', e => { e.preventDefault(); item.classList.add('drag-over'); }); + item.addEventListener('dragleave', () => item.classList.remove('drag-over')); + item.addEventListener('drop', async e => { + e.preventDefault(); + item.classList.remove('drag-over'); + const id = e.dataTransfer.getData('text/plain'); + if (id) await moveEntryToFolder(parseInt(id), 'All'); + }); + + fList.appendChild(item); + } + // tags const tList = $('#tagsList'); tList.innerHTML = ''; @@ -987,7 +1342,7 @@ async function addTagToEntry(id, tag) { method: 'PUT', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ - site: e.site, username: e.username, + site: e.site, title: e.title || '', username: e.username, encrypted_password: e.encrypted_password, iv: e.iv, folder: e.folder, tags: tags.join(','), }), @@ -1000,6 +1355,34 @@ async function addTagToEntry(id, tag) { function renderGrid() { $('#contentTitle').textContent = viewTitle(); + + // Authenticator view: bypass the standard pipeline — render a dedicated + // grid of TOTP cards (only entries that have a TOTP secret configured). + if (state.view === 'authenticator') { + if (authTickTimer) { clearInterval(authTickTimer); authTickTimer = null; } + const oldBtn = $('#emptyTrashBtn'); if (oldBtn) oldBtn.remove(); + renderBatchBar(); + const totpEntries = state.entries.filter(e => e.totp_secret && e.totp_iv); + $('#contentMeta').textContent = totpEntries.length + + (totpEntries.length === 1 ? ' code' : ' codes'); + const grid = $('#entryGrid'); + grid.className = 'entry-grid is-auth'; + grid.innerHTML = ''; + if (totpEntries.length === 0) { + $('#emptyState').classList.remove('is-hidden'); + const illu = $('#emptyIllustration use'); + illu.setAttribute('href', '#i-empty-vault'); + $('#emptyTitle').textContent = 'No TOTP codes yet'; + $('#emptyMessage').innerHTML = 'Add a TOTP secret to any entry to see its live code here.'; + return; + } + $('#emptyState').classList.add('is-hidden'); + renderAuthenticatorGrid(grid, totpEntries); + return; + } else if (authTickTimer) { + clearInterval(authTickTimer); authTickTimer = null; + } + const list = filteredEntries(); $('#contentMeta').textContent = list.length + (list.length === 1 ? ' item' : ' items'); @@ -1019,7 +1402,9 @@ function renderGrid() { renderBatchBar(); const grid = $('#entryGrid'); - grid.className = 'entry-grid' + (state.viewMode === 'list' ? ' is-list' : ''); + grid.className = 'entry-grid' + + (state.viewMode === 'list' ? ' is-list' : '') + + (state.viewMode === 'table' ? ' is-table' : ''); grid.innerHTML = ''; if (list.length === 0) { showEmptyState(); @@ -1027,7 +1412,108 @@ function renderGrid() { } $('#emptyState').classList.add('is-hidden'); - list.forEach(e => grid.appendChild(renderCard(e))); + const total = list.length; + const totalPages = Math.max(1, Math.ceil(total / state.pageSize)); + if (state.currentPage > totalPages) state.currentPage = totalPages; + if (state.currentPage < 1) state.currentPage = 1; + const start = (state.currentPage - 1) * state.pageSize; + const pageList = list.slice(start, start + state.pageSize); + + if (total > 10) { + grid.appendChild(renderPagination(total, totalPages)); + } + + if (state.viewMode === 'table') { + grid.appendChild(renderTable(pageList)); + } else { + pageList.forEach(e => grid.appendChild(renderCard(e))); + } +} + +// Authenticator view tick timer — recomputes every code once per second. +let authTickTimer = null; + +function renderAuthenticatorGrid(grid, entries) { + // Decrypt all secrets once up-front (slow); render cards immediately + // with a placeholder, then patch in the codes as they decrypt. + const cards = entries.map(e => { + const wrap = el('div', { class: 'auth-card', 'data-id': String(e.id) }); + const head = el('div', { class: 'auth-card-head' }); + head.appendChild(el('div', { class: 'auth-card-title' }, + e.title || e.site || '(no name)')); + if (e.username) head.appendChild(el('div', { class: 'auth-card-sub' }, e.username)); + wrap.appendChild(head); + + const codeRow = el('div', { class: 'auth-card-code-row' }); + const codeEl = el('div', { class: 'auth-card-code' }, '— — — — — —'); + const copyBtn = el('button', { class: 'icon-btn', title: 'Copy code', type: 'button' }); + copyBtn.appendChild(icon('i-copy')); + copyBtn.addEventListener('click', async ev => { + ev.stopPropagation(); + const code = codeEl.textContent.replace(/\s/g, ''); + if (!/^\d{6}$/.test(code)) return; + if (Bridge.active) Bridge.copySecure(code, 30000); + else { try { await navigator.clipboard.writeText(code); } catch (_) {} } + toast('Code copied'); + }); + codeRow.appendChild(codeEl); + codeRow.appendChild(copyBtn); + wrap.appendChild(codeRow); + + const barWrap = el('div', { class: 'auth-card-bar-wrap' }); + const bar = el('div', { class: 'auth-card-bar' }); + barWrap.appendChild(bar); + wrap.appendChild(barWrap); + + // Click anywhere on card (outside copy) opens the entry detail. + wrap.addEventListener('click', () => openSlideover(e.id)); + + return { entry: e, wrap, codeEl, bar, secret: null }; + }); + + cards.forEach(c => grid.appendChild(c.wrap)); + + // Decrypt then start ticking + (async () => { + for (const c of cards) { + try { + c.secret = await decryptTotpSecret(c.entry.totp_secret, c.entry.totp_iv); + } catch (e) { + c.secret = null; + } + } + async function tick() { + for (const c of cards) { + if (!c.secret) { c.codeEl.textContent = 'error'; continue; } + try { + const t = await generateTOTP(c.secret); + c.codeEl.textContent = t.code.replace(/(\d{3})(\d{3})/, '$1 $2'); + const newPct = (t.secondsLeft / 30) * 100; + // Detect period reset (countdown wrapped from ~0 back to 30s): + // snap the bar instantly to 100% instead of letting the CSS + // transition animate the jump backwards, which looks like a + // freeze / reverse glide. + if (newPct > (c.lastPct || 0) + 5) { + c.bar.style.transition = 'none'; + c.bar.style.width = newPct.toFixed(1) + '%'; + // Force reflow then restore the transition for the smooth + // forward countdown. + void c.bar.offsetWidth; + c.bar.style.transition = ''; + } else { + c.bar.style.width = newPct.toFixed(1) + '%'; + } + c.lastPct = newPct; + c.bar.classList.toggle('is-warning', t.secondsLeft <= 5); + } catch (e) { + c.codeEl.textContent = 'error'; + } + } + } + await tick(); + if (authTickTimer) clearInterval(authTickTimer); + authTickTimer = setInterval(tick, 1000); + })(); } function showEmptyState() { @@ -1087,6 +1573,47 @@ function initials(s) { return (s || '?').replace(/[^a-zA-Z0-9]/g, '').slice(0, 2).toUpperCase() || '?'; } +// User-facing name for an entry. Falls back to `site` when `title` is empty +// (default for legacy entries and any entry the user hasn't customised). +// IMPORTANT: do NOT use this for autofill domain matching or search-by-host +// — those need the raw site/URL/hostname. +function entryDisplayName(e) { + if (!e) return ''; + const t = (e.title || '').trim(); + return t || e.site || ''; +} + +// Display label for a folder value. "All" is the default "uncategorized" +// bucket; we relabel it so users don't see two "All" entries in folder +// pickers (the top nav "All items" also says "All"). +function folderLabel(f) { + if (!f || f === 'All') return '(no folder)'; + return f; +} + +// Comparator for sorting entries. by ∈ {name, site, updated, created, folder}. +// dir ∈ {asc, desc}. Stable sort: ties keep their relative order (Array.sort +// is stable per the modern spec). +function sortEntries(list, by, dir) { + const mul = dir === 'desc' ? -1 : 1; + const get = e => { + switch (by) { + case 'site': return (e.site || '').toLowerCase(); + case 'updated': return e.updated_at || ''; + case 'created': return e.created_at || ''; + case 'folder': return (e.folder || '').toLowerCase(); + case 'name': + default: return entryDisplayName(e).toLowerCase(); + } + }; + return list.slice().sort((a, b) => { + const va = get(a), vb = get(b); + if (va < vb) return -1 * mul; + if (va > vb) return 1 * mul; + return 0; + }); +} + // Compact-action kebab menu shown on each card when state.compactActions is on. function buildKebabMenu(entry) { const wrap = el('div', { class: 'entry-kebab-wrap' }); @@ -1111,6 +1638,7 @@ function buildKebabMenu(entry) { { lbl: 'Copy password', ic: 'i-copy', fn: () => copyPassword(entry) }, { lbl: 'Copy username', ic: 'i-user', fn: () => copyUsername(entry) }, { lbl: 'Edit', ic: 'i-edit', fn: () => openSlideOver(entry.id) }, + { lbl: 'Duplicate', ic: 'i-copy', fn: () => duplicateEntry(entry) }, { lbl: 'Move to trash', ic: 'i-trash', fn: () => deleteEntry(entry.id), danger: true }, ]; items.forEach(it => { @@ -1149,16 +1677,34 @@ function renderCard(e) { }); } - // head: avatar acts as a multi-select checkbox (click on avatar -> toggle) + // head: avatar shows identity (initials). A checkbox overlay LIVES + // INSIDE the avatar (absolute inset:0) — it covers the avatar exactly + // when visible, no separate footprint that could collide with the + // avatar's position. Visible on hover or whenever the entry is + // checked. Card click anywhere not on the checkbox opens slideover. const head = el('div', { class: 'entry-head' }); + const displayName = entryDisplayName(e); const avatar = el('div', { - class: 'entry-avatar is-checkable', - title: 'Click to select', + class: 'entry-avatar', + }, initials(displayName)); + const checkbox = el('button', { + class: 'entry-check' + (checked ? ' is-checked' : ''), + type: 'button', + title: checked ? 'Deselect' : 'Select', on: { click: ev => { ev.stopPropagation(); toggleChecked(e.id); } }, - }, checked ? '✓' : initials(e.site)); + }); + if (checked) checkbox.appendChild(el('span', null, '✓')); + avatar.appendChild(checkbox); // NESTED inside avatar — no overlap head.appendChild(avatar); const title = el('div', { class: 'entry-title' }); - title.appendChild(el('b', null, e.site)); + title.appendChild(el('b', null, displayName)); + // If user set a custom title AND it differs from site, optionally show + // site as a small subtitle. Hidden by default to keep cards clean — + // toggled via Settings > Appearance. + if (state.showSiteOnCards && + e.title && e.title.trim() && e.title.trim() !== e.site) { + title.appendChild(el('span', { class: 'entry-subtitle' }, e.site)); + } // Username row with inline copy button (visible on card hover) const userRow = el('small', { class: 'entry-user-row' }); userRow.appendChild(el('span', null, displayUsername(e.username))); @@ -1189,7 +1735,7 @@ function renderCard(e) { head.appendChild(restore); head.appendChild(purge); } else if (state.compactActions) { - // Compact mode: single kebab menu replaces fav + del + // Compact mode: single kebab menu replaces fav + dup + del head.appendChild(buildKebabMenu(e)); } else { const fav = el('button', { @@ -1200,8 +1746,14 @@ function renderCard(e) { fav.appendChild(icon('i-star')); head.appendChild(fav); - // Quick-delete: small X visible on card hover. Always available - // without opening the slide-over. + const dup = el('button', { + class: 'entry-dup', + title: 'Duplicate', + on: { click: ev => { ev.stopPropagation(); duplicateEntry(e); } }, + }); + dup.appendChild(icon('i-copy')); + head.appendChild(dup); + const del = el('button', { class: 'entry-del', title: 'Move to trash', @@ -1224,9 +1776,10 @@ function renderCard(e) { pwRow.appendChild(copyBtn); card.appendChild(pwRow); - // meta chips: folder + first 2 tags + // meta chips: folder + first 2 tags. "All" is the default "uncategorized" + // bucket and shouldn't be shown as a chip (visually duplicates "All items"). const meta = el('div', { class: 'entry-meta' }); - if (e.folder) { + if (e.folder && e.folder !== 'All') { const chip = el('span', { class: 'entry-chip is-folder' }); chip.appendChild(icon('i-folder')); chip.appendChild(el('span', null, e.folder)); @@ -1269,84 +1822,267 @@ function renderCard(e) { } // ============================================================ -// MARQUEE (rubber-band) SELECTION +// TABLE VIEW // ============================================================ -// Click-drag on empty space in the entry grid draws a rectangle. -// Cards whose bounding box intersects the rectangle become selected. -// Shift/Ctrl held = add to existing selection (otherwise replace). +// Dense, spreadsheet-like layout for users who manage many entries. +// Sortable headers — clicking a column header sets state.sortBy/sortDir +// (with toggle on the active column) and re-renders. Same source of +// truth as the Settings dropdown — both stay in sync. -let marqueeEl = null; -let marqueeStart = null; -let marqueeAdditive = false; -let marqueeInitialSet = null; - -function startMarquee(ev) { - // Only fire on left mouse button, and only when starting on grid background - if (ev.button !== 0) return; - if (ev.target.closest('.entry-card')) return; // ignore drags from cards - if (ev.target.closest('.batch-bar')) return; - if (!ev.target.closest('#entryGrid')) return; - - marqueeAdditive = ev.shiftKey || ev.ctrlKey || ev.metaKey; - marqueeInitialSet = new Set(state.checked); - if (!marqueeAdditive) state.checked.clear(); - - marqueeStart = { x: ev.clientX, y: ev.clientY }; - marqueeEl = el('div', { class: 'marquee' }); - Object.assign(marqueeEl.style, { - left: marqueeStart.x + 'px', - top: marqueeStart.y + 'px', - width: '0px', height: '0px', - }); - document.body.appendChild(marqueeEl); - ev.preventDefault(); - - document.addEventListener('mousemove', updateMarquee); - document.addEventListener('mouseup', endMarquee); +// Columns built dynamically — the Site column tracks the user's +// "Show site under display name" preference so card view and table view +// stay consistent (default: site hidden, can be re-enabled from Settings). +function getTableColumns() { + const cols = [ + { key: 'check', label: '', sortKey: null }, + { key: 'name', label: 'Name', sortKey: 'name' }, + ]; + if (state.showSiteOnCards) { + cols.push({ key: 'site', label: 'Site', sortKey: 'site' }); + } + cols.push( + { key: 'user', label: 'Username', sortKey: null }, // no sort: derived/varies + { key: 'folder', label: 'Folder', sortKey: 'folder' }, + { key: 'updated', label: 'Updated', sortKey: 'updated' }, + { key: 'actions', label: '', sortKey: null }, + ); + return cols; } -function updateMarquee(ev) { - if (!marqueeEl) return; - const x1 = Math.min(marqueeStart.x, ev.clientX); - const y1 = Math.min(marqueeStart.y, ev.clientY); - const x2 = Math.max(marqueeStart.x, ev.clientX); - const y2 = Math.max(marqueeStart.y, ev.clientY); - Object.assign(marqueeEl.style, { - left: x1 + 'px', top: y1 + 'px', - width: (x2 - x1) + 'px', height: (y2 - y1) + 'px', +function renderPagination(total, totalPages) { + const wrap = el('div', { class: 'pagination' }); + + const start = (state.currentPage - 1) * state.pageSize + 1; + const end = Math.min(start + state.pageSize - 1, total); + wrap.appendChild(el('span', { class: 'pagination-info' }, + start + '–' + end + ' of ' + total)); + + function pageBtn(label, page, opts) { + opts = opts || {}; + const b = el('button', { + class: 'pagination-btn' + + (opts.active ? ' is-active' : '') + + (opts.disabled ? ' is-disabled' : ''), + type: 'button', + on: { click: () => { + if (opts.disabled || opts.active) return; + state.currentPage = page; + render(); + } }, + }, String(label)); + return b; + } + + wrap.appendChild(pageBtn('‹ Prev', state.currentPage - 1, + { disabled: state.currentPage <= 1 })); + + const pages = computePageList(state.currentPage, totalPages); + pages.forEach(p => { + if (p === '…') wrap.appendChild(el('span', { class: 'pagination-ellipsis' }, '…')); + else wrap.appendChild(pageBtn(p, p, { active: p === state.currentPage })); }); - // Re-check intersections - const marqueeRect = { left: x1, top: y1, right: x2, bottom: y2 }; - state.checked = new Set(marqueeAdditive ? marqueeInitialSet : []); - $$('#entryGrid .entry-card').forEach(card => { - const r = card.getBoundingClientRect(); - const intersects = !(r.right < marqueeRect.left || r.left > marqueeRect.right || - r.bottom < marqueeRect.top || r.top > marqueeRect.bottom); - if (intersects) { - const id = parseInt(card.dataset.id); - state.checked.add(id); - card.classList.add('is-checked'); - } else if (!marqueeInitialSet.has(parseInt(card.dataset.id))) { - card.classList.remove('is-checked'); + wrap.appendChild(pageBtn('Next ›', state.currentPage + 1, + { disabled: state.currentPage >= totalPages })); + + const sizeSel = el('select', { + class: 'pagination-size', + on: { change: ev => { + state.pageSize = parseInt(ev.target.value); + state.currentPage = 1; + localStorage.setItem('pageSize', String(state.pageSize)); + saveServerSettings(); + render(); + } }, + }); + [10, 25, 50, 100].forEach(n => { + const opt = el('option', { value: String(n) }, String(n) + ' / page'); + if (n === state.pageSize) opt.selected = true; + sizeSel.appendChild(opt); + }); + wrap.appendChild(sizeSel); + + return wrap; +} + +function computePageList(current, total) { + if (total <= 7) { + const arr = []; + for (let i = 1; i <= total; i++) arr.push(i); + return arr; + } + const pages = [1]; + if (current > 3) pages.push('…'); + const from = Math.max(2, current - 1); + const to = Math.min(total - 1, current + 1); + for (let i = from; i <= to; i++) pages.push(i); + if (current < total - 2) pages.push('…'); + pages.push(total); + return pages; +} + +function renderTable(list) { + const columns = getTableColumns(); + const table = el('table', { class: 'entry-table' }); + + // Header row with click-to-sort + const thead = el('thead'); + const headerRow = el('tr'); + columns.forEach(col => { + const th = el('th', { 'data-col': col.key }); + if (col.sortKey) { + th.classList.add('is-sortable'); + const isActive = state.sortBy === col.sortKey; + if (isActive) th.classList.add('is-active'); + th.appendChild(el('span', null, col.label)); + // Arrow indicator only on the active column. + if (isActive) { + th.appendChild(el('span', { class: 'sort-arrow' }, + state.sortDir === 'asc' ? '↑' : '↓')); + } + th.addEventListener('click', () => { + if (state.sortBy === col.sortKey) { + // Same column → flip direction + state.sortDir = state.sortDir === 'asc' ? 'desc' : 'asc'; + } else { + state.sortBy = col.sortKey; + // Sensible default direction per field + state.sortDir = (col.sortKey === 'updated' || + col.sortKey === 'created') ? 'desc' : 'asc'; + } + localStorage.setItem('sortBy', state.sortBy); + localStorage.setItem('sortDir', state.sortDir); + // Keep the Settings dropdown in sync if it's currently displayed. + const sel = $('#settingSort'); + if (sel) sel.value = state.sortBy + ':' + state.sortDir; + render(); + saveServerSettings(); + }); + } else { + th.appendChild(el('span', null, col.label)); } + headerRow.appendChild(th); }); + thead.appendChild(headerRow); + table.appendChild(thead); + + // Body rows + const tbody = el('tbody'); + list.forEach(e => tbody.appendChild(renderTableRow(e))); + table.appendChild(tbody); + + return table; } -function endMarquee() { - document.removeEventListener('mousemove', updateMarquee); - document.removeEventListener('mouseup', endMarquee); - if (marqueeEl) marqueeEl.remove(); - marqueeEl = null; - marqueeStart = null; - marqueeInitialSet = null; - // Re-render so the batch bar appears with the new count + avatar states - renderGrid(); +function renderTableRow(e) { + const checked = state.checked.has(e.id); + const inTrash = state.view === 'trash'; + const tr = el('tr', { + class: 'entry-row' + + (state.selectedId === e.id ? ' is-selected' : '') + + (checked ? ' is-checked' : ''), + 'data-id': String(e.id), + on: { click: ev => handleCardClick(ev, e, inTrash) }, + }); + + // Cells are emitted in EXACTLY the same order as getTableColumns() + // returns headers, otherwise THs and TDs drift apart and clicks land + // on the wrong column. Switch by col.key so add/remove of a column + // affects header + body in one place. + getTableColumns().forEach(col => { + let td; + switch (col.key) { + case 'check': { + td = el('td', { class: 'col-check' }); + // Standalone variant — not nested inside an avatar, so it + // needs its own explicit dimensions. .entry-check-static + // overrides the absolute/inset:0 positioning used in the + // card-view (where the box gets its size from its avatar + // parent). + const checkbox = el('button', { + class: 'entry-check entry-check-static' + (checked ? ' is-checked' : ''), + type: 'button', + title: checked ? 'Deselect' : 'Select', + on: { click: ev => { ev.stopPropagation(); toggleChecked(e.id); } }, + }); + if (checked) checkbox.appendChild(el('span', null, '✓')); + td.appendChild(checkbox); + break; + } + case 'name': { + td = el('td', { class: 'col-name' }); + const avatar = el('span', { class: 'entry-avatar entry-avatar-sm' }, + initials(entryDisplayName(e))); + td.appendChild(avatar); + const nameWrap = el('span', { class: 'cell-name-wrap' }); + nameWrap.appendChild(el('b', null, entryDisplayName(e))); + if (e.favorite) nameWrap.appendChild(el('span', { class: 'fav-dot', title: 'Favorite' }, '★')); + td.appendChild(nameWrap); + break; + } + case 'site': + td = el('td', { class: 'col-site' }, e.site || ''); + break; + case 'user': { + td = el('td', { class: 'col-user' }); + td.appendChild(el('span', null, displayUsername(e.username))); + if (e.username) { + const btn = el('button', { + class: 'icon-btn icon-btn-sm', + title: 'Copy username', + on: { click: ev => { ev.stopPropagation(); copyUsername(e); } }, + }); + btn.appendChild(icon('i-copy')); + td.appendChild(btn); + } + break; + } + case 'folder': + td = el('td', { class: 'col-folder' }, + (e.folder && e.folder !== 'All') ? e.folder : ''); + break; + case 'updated': + td = el('td', { class: 'col-updated' }, + formatDateShort(e.updated_at)); + break; + case 'actions': { + td = el('td', { class: 'col-actions' }); + const pwBtn = el('button', { + class: 'icon-btn icon-btn-sm', + title: 'Copy password', + on: { click: ev => { ev.stopPropagation(); copyPassword(e); } }, + }); + pwBtn.appendChild(icon('i-copy')); + td.appendChild(pwBtn); + td.appendChild(buildKebabMenu(e)); + break; + } + } + tr.appendChild(td); + }); + + return tr; +} + +// Compact date for the table column. ISO string in → "2026-05-23" out. +// Avoids per-locale parsing surprises (server uses ISO already). +function formatDateShort(iso) { + if (!iso) return ''; + return iso.slice(0, 10); } // ============================================================ // MULTI-SELECTION + BATCH ACTIONS // ============================================================ +// +// Selection patterns (no rubber-band marquee — removed 2026-05-24, value +// for a password manager was too low vs the accidental-deselect cost): +// - Click the checkbox overlay on a card → toggle just that entry +// - Click a card body (when ≥1 is already checked) → toggle (sticky) +// - Ctrl+click row/card → toggle (always, no need for sticky mode) +// - Shift+click row/card → range select from last anchor +// - Ctrl+A (when not typing) → select every visible entry +// - Escape (when nothing else to dismiss) → clear selection let selectionAnchor = null; // last single-clicked card, used for shift+click range @@ -1403,7 +2139,7 @@ async function batchMoveToFolder(folder) { method: 'PUT', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ - site: e.site, username: e.username, + site: e.site, title: e.title || '', username: e.username, encrypted_password: e.encrypted_password, iv: e.iv, folder, tags: e.tags || '', }), @@ -1431,7 +2167,7 @@ async function batchAddTag(tag) { method: 'PUT', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ - site: e.site, username: e.username, + site: e.site, title: e.title || '', username: e.username, encrypted_password: e.encrypted_password, iv: e.iv, folder: e.folder, tags: tags.join(','), }), @@ -1522,7 +2258,7 @@ function renderBatchBar() { // Normal view: Move to folder | Add tag | Delete (soft) const moveSel = el('select'); moveSel.appendChild(el('option', { value: '' }, 'Move to folder…')); - state.folders.forEach(f => moveSel.appendChild(el('option', { value: f }, f))); + state.folders.forEach(f => moveSel.appendChild(el('option', { value: f }, folderLabel(f)))); moveSel.addEventListener('change', () => { if (moveSel.value) batchMoveToFolder(moveSel.value); }); @@ -1570,7 +2306,7 @@ async function openSlideOver(id) { if (!e) return; state.selectedId = id; - $('#slideoverTitle').textContent = e.site; + $('#slideoverTitle').textContent = entryDisplayName(e); const body = $('#slideoverBody'); body.innerHTML = ''; @@ -1590,7 +2326,8 @@ async function openSlideOver(id) { soState = { id: e.id, original: { - site: e.site, username: e.username || '', password: plain, + site: e.site, title: e.title || '', + username: e.username || '', password: plain, folder: e.folder || 'All', tags: parseTags(e.tags).join(','), totp: plainTotp, }, @@ -1601,6 +2338,7 @@ async function openSlideOver(id) { originalTotpIV: e.totp_iv, }; + body.appendChild(soEditableField('Display name', 'soTitle', e.title || '')); body.appendChild(soEditableField('Site', 'soSite', e.site)); body.appendChild(soEditableField('Username', 'soUsername', e.username || '')); body.appendChild(soPasswordField(plain)); @@ -1621,7 +2359,7 @@ async function openSlideOver(id) { body.appendChild(actions); // Wire change detection - ['#soSite', '#soUsername', '#soPassword', '#soFolder'].forEach(sel => { + ['#soTitle', '#soSite', '#soUsername', '#soPassword', '#soFolder'].forEach(sel => { const el = $(sel); if (el) el.addEventListener('input', soDirtyCheck); if (el) el.addEventListener('change', soDirtyCheck); }); @@ -1634,10 +2372,17 @@ function soEditableField(label, id, value) { const wrap = el('div', { class: 'slideover-field' }); wrap.appendChild(el('div', { class: 'slideover-field-label' }, label)); const input = el('input', { type: 'text', id, value, class: 'so-input' }); + input.addEventListener('keydown', soOnEnterSave); wrap.appendChild(input); return wrap; } +function soOnEnterSave(ev) { + if (ev.key !== 'Enter') return; + ev.preventDefault(); + soSave(); +} + function soPasswordField(plain) { const wrap = el('div', { class: 'slideover-field' }); wrap.appendChild(el('div', { class: 'slideover-field-label' }, 'Password')); @@ -1646,6 +2391,7 @@ function soPasswordField(plain) { type: 'password', id: 'soPassword', value: plain, class: 'so-input', style: 'flex:1;font-family:JetBrains Mono,monospace', }); + input.addEventListener('keydown', soOnEnterSave); const toggle = el('button', { class: 'icon-btn icon-btn-sm', type: 'button', title: 'Show/hide' }); toggle.appendChild(icon('i-eye')); toggle.addEventListener('click', () => { @@ -1684,7 +2430,10 @@ function startTotpTick() { // Refresh once per second so the countdown bar moves smoothly and the // code auto-rolls when the 30s window expires. totpTickTimer = setInterval(updateTotpDisplay, 1000); - updateTotpDisplay(); + // Defer the first immediate update: callers append the wrap to the DOM + // AFTER soTotpField() returns, so a sync $() lookup here would see null + // elements and stopTotpTick() would kill the interval we just created. + setTimeout(updateTotpDisplay, 0); } function stopTotpTick() { @@ -1731,6 +2480,7 @@ function soTotpField(plainSecret) { value: plainSecret || '', class: 'so-input', placeholder: 'Paste base32 secret or otpauth:// URI', + on: { keydown: soOnEnterSave }, style: 'flex:1;font-family:JetBrains Mono,monospace', autocomplete: 'off', spellcheck: 'false', }); @@ -1802,7 +2552,7 @@ function soFolderField(current) { wrap.appendChild(el('div', { class: 'slideover-field-label' }, 'Folder')); const sel = el('select', { id: 'soFolder', class: 'so-input' }); state.folders.forEach(f => { - const opt = el('option', { value: f }, f); + const opt = el('option', { value: f }, folderLabel(f)); if (f === current) opt.selected = true; sel.appendChild(opt); }); @@ -1818,10 +2568,26 @@ function soTagsField() { type: 'text', id: 'soTagsField', placeholder: 'add a tag…', autocomplete: 'off', }); + // Render existing chips inline (renderSoChips() would no-op here because + // the container isn't attached to the DOM yet, $ would return null). + soState.tags.forEach((t, i) => { + const chip = el('span', { class: 'chip' }); + chip.appendChild(el('span', null, t)); + const x = el('button', { + type: 'button', + on: { click: ev => { + ev.stopPropagation(); + soState.tags.splice(i, 1); + renderSoChips(); + soDirtyCheck(); + } }, + }); + x.appendChild(icon('i-x')); + chip.appendChild(x); + cont.appendChild(chip); + }); cont.appendChild(input); wrap.appendChild(cont); - // Render existing chips - renderSoChips(); input.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); @@ -1851,7 +2617,12 @@ function renderSoChips() { chip.appendChild(el('span', null, t)); const x = el('button', { type: 'button', - on: { click: () => { soState.tags.splice(i, 1); renderSoChips(); soDirtyCheck(); } }, + on: { click: ev => { + ev.stopPropagation(); + soState.tags.splice(i, 1); + renderSoChips(); + soDirtyCheck(); + } }, }); x.appendChild(icon('i-x')); chip.appendChild(x); @@ -1862,6 +2633,7 @@ function renderSoChips() { function soDirtyCheck() { if (!soState) return; const cur = { + title: ($('#soTitle') || {}).value || '', site: ($('#soSite') || {}).value || '', username: ($('#soUsername') || {}).value || '', password: ($('#soPassword') || {}).value || '', @@ -1870,6 +2642,7 @@ function soDirtyCheck() { tags: soState.tags.join(','), }; const dirty = + cur.title !== soState.original.title || cur.site !== soState.original.site || cur.username !== soState.original.username || cur.password !== soState.original.password || @@ -1882,6 +2655,14 @@ function soDirtyCheck() { async function soSave() { if (!soState) return; + // Flush any uncommitted tag text — user may have typed in the chip + // input without pressing Enter / comma before clicking Save. + const pendingTag = (($('#soTagsField') || {}).value || '').trim(); + if (pendingTag && !soState.tags.includes(pendingTag)) { + soState.tags.push(pendingTag); + $('#soTagsField').value = ''; + } + const title = ($('#soTitle') || {}).value || ''; const site = $('#soSite').value.trim(); const user = $('#soUsername').value.trim(); const pwd = $('#soPassword').value; @@ -1922,7 +2703,7 @@ async function soSave() { method: 'PUT', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ - site, username: user, + site, title: title.trim(), username: user, encrypted_password: enc.encrypted, iv: enc.iv, totp_secret: totpEnc, totp_iv: totpIv, folder: fold, tags: soState.tags.join(','), @@ -2012,7 +2793,12 @@ function renderChips() { chip.appendChild(el('span', null, t)); const x = el('button', { type: 'button', - on: { click: () => { editingTags.splice(i, 1); renderChips(); syncTagsHidden(); } }, + on: { click: ev => { + ev.stopPropagation(); + editingTags.splice(i, 1); + renderChips(); + syncTagsHidden(); + } }, }); x.appendChild(icon('i-x')); chip.appendChild(x); @@ -2102,6 +2888,7 @@ async function openEntryModal(entry) { if (entry) { $('#entryModalTitle').textContent = 'Edit entry'; $('#entryId').value = entry.id; + $('#entryTitle').value = entry.title || ''; $('#entrySite').value = entry.site; $('#entryUsername').value = entry.username || ''; $('#entryFolder').value = entry.folder || 'All'; @@ -2129,7 +2916,7 @@ function closeEntryModal() { function populateFolderSelect() { const sel = $('#entryFolder'); sel.innerHTML = ''; - state.folders.forEach(f => sel.appendChild(el('option', { value: f }, f))); + state.folders.forEach(f => sel.appendChild(el('option', { value: f }, folderLabel(f)))); } async function saveEntry(e) { @@ -2137,20 +2924,22 @@ async function saveEntry(e) { // Flush any pending text in the chip input as a final tag const pending = $('#entryTagsField').value.trim(); if (pending) { addTag(pending); $('#entryTagsField').value = ''; } - const id = $('#entryId').value; - const site = $('#entrySite').value.trim(); - const user = $('#entryUsername').value.trim(); - const pwd = $('#entryPassword').value; - const fold = $('#entryFolder').value; - const tags = editingTags.join(','); + const id = $('#entryId').value; + const title = $('#entryTitle').value.trim(); + const site = $('#entrySite').value.trim(); + const user = $('#entryUsername').value.trim(); + const pwd = $('#entryPassword').value; + const fold = $('#entryFolder').value; + const tags = editingTags.join(','); if (!site || !pwd) return toast('Site and password required', 'error'); const enc = await encryptPwd(pwd); const body = JSON.stringify({ - site, username: user, encrypted_password: enc.encrypted, iv: enc.iv, + site, title, username: user, encrypted_password: enc.encrypted, iv: enc.iv, folder: fold, tags, }); try { + let savedId = id ? parseInt(id) : null; if (id) { await api('/entries/' + id, { method: 'PUT', @@ -2159,16 +2948,18 @@ async function saveEntry(e) { }); toast('Updated'); } else { - await api('/entries', { + const r = await api('/entries', { method: 'POST', headers: authHeaders({ 'Content-Type': 'application/json' }), body, }); + if (r && typeof r.id === 'number') savedId = r.id; toast('Saved'); } closeEntryModal(); await loadEntries(); render(); + if (savedId) flashEntry(savedId); } catch (err) { toast(err.message, 'error'); } @@ -2222,6 +3013,47 @@ function openTrashActions(id) { // But user can click outside the buttons to no-op. Could open a read-only view later. } +async function duplicateEntry(entry) { + if (!entry) return; + try { + const r = await api('/entries', { + method: 'POST', + headers: authHeaders({ 'Content-Type': 'application/json' }), + body: JSON.stringify({ + site: entry.site, + title: entryDisplayName(entry) + ' (copy)', + username: entry.username || '', + encrypted_password: entry.encrypted_password, + iv: entry.iv, + folder: entry.folder || 'All', + tags: entry.tags || '', + totp_secret: entry.totp_secret || '', + totp_iv: entry.totp_iv || '', + }), + }); + await loadEntries(); + render(); + toast('Duplicated: ' + entryDisplayName(entry)); + if (r && typeof r.id === 'number') flashEntry(r.id); + } catch (err) { + toast(err.message || 'Duplicate failed', 'error'); + } +} + +function flashEntry(id) { + if (!id) return; + setTimeout(() => { + const el = document.querySelector( + '.entry-card[data-id="' + id + '"], ' + + '.entry-row[data-id="' + id + '"]' + ); + if (!el) return; + el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + el.classList.add('is-flash'); + setTimeout(() => el.classList.remove('is-flash'), 2600); + }, 50); +} + async function deleteEntry(id) { const e = state.entries.find(x => x.id === id); if (state.askBeforeDelete) { @@ -2238,6 +3070,7 @@ async function deleteEntry(id) { toast('Moved to trash'); closeSlideOver(); await loadEntries(); + state.trashedCount = (state.trashedCount || 0) + 1; render(); } catch (err) { toast(err.message, 'error'); } } @@ -2259,7 +3092,7 @@ async function moveEntryToFolder(id, folder) { method: 'PUT', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ - site: e.site, username: e.username, + site: e.site, title: e.title || '', username: e.username, encrypted_password: e.encrypted_password, iv: e.iv, folder, tags: e.tags || '', }), @@ -2304,6 +3137,30 @@ function displayUsername(u) { // FOLDERS CRUD // ============================================================ +async function deleteFolder(name, entryCount) { + const message = entryCount > 0 + ? '' + name + ' contains ' + entryCount + ' entries. They will be moved to (no folder). Continue?' + : 'Delete folder ' + name + '?'; + const ok = await confirmDialog({ + title: 'Delete folder', + message, + okText: 'Delete', + danger: true, + }); + if (!ok) return; + try { + await api('/folders/' + encodeURIComponent(name), { + method: 'DELETE', + headers: authHeaders(), + }); + if (state.view === 'folder:' + name) state.view = 'all'; + await loadFolders(); + await loadEntries(); + render(); + toast('Folder deleted'); + } catch (e) { toast(e.message || 'Delete failed', 'error'); } +} + async function addFolder() { const name = await promptDialog({ title: 'New folder', @@ -2312,11 +3169,19 @@ async function addFolder() { okText: 'Create', }); if (!name || !name.trim()) return; + const clean = name.trim(); + // "All" is reserved as the internal default for "uncategorized" entries + // (and would visually duplicate the "All items" top nav). Reject here + // rather than letting the user create a confusing duplicate. + if (clean.toLowerCase() === 'all') { + toast('"All" is reserved — pick another name', 'warning'); + return; + } try { await api('/folders', { method: 'POST', headers: authHeaders({ 'Content-Type': 'application/json' }), - body: JSON.stringify({ name: name.trim() }), + body: JSON.stringify({ name: clean }), }); await loadFolders(); render(); @@ -2412,7 +3277,12 @@ function paletteCommands() { function renderPaletteResults(q) { const cmds = paletteCommands(); const entries = state.entries.map(e => ({ - id: 'entry-' + e.id, label: e.site, sub: e.username || '', + id: 'entry-' + e.id, label: entryDisplayName(e), + // Sub: site (when different from displayName) + username, joined. + sub: [ + (e.title && e.title.trim() && e.title.trim() !== e.site) ? e.site : '', + e.username || '', + ].filter(Boolean).join(' · '), icon: 'i-globe', run: () => { closePalette(); openSlideOver(e.id); }, })); const all = cmds.concat(entries); @@ -2420,7 +3290,7 @@ function renderPaletteResults(q) { const filtered = q ? all.filter(c => c.label.toLowerCase().includes(q) || (c.sub||'').toLowerCase().includes(q)) : all; const out = $('#cmdResults'); out.innerHTML = ''; - filtered.slice(0, 12).forEach((c, i) => { + filtered.slice(0, 50).forEach((c, i) => { const it = el('div', { class: 'cmd-item' + (i === 0 ? ' is-active' : ''), on: { click: c.run }, @@ -2548,6 +3418,29 @@ function bridgeRequestQuickUnlock() { }); } +let clipboardReadResolver = null; + +function bridgeReadClipboard() { + if (!Bridge.active) return Promise.resolve(''); + return new Promise(resolve => { + clipboardReadResolver = resolve; + setTimeout(() => { + if (clipboardReadResolver === resolve) { + clipboardReadResolver = null; + resolve(''); + } + }, 1500); + window.location.href = 'cmd://clipboard/read'; + }); +} + +Bridge.onClipboardRead = function(text) { + if (clipboardReadResolver) { + clipboardReadResolver(text || ''); + clipboardReadResolver = null; + } +}; + function bridgeQuickUnlockStatus() { if (!Bridge.active) return Promise.resolve(false); return new Promise(resolve => { @@ -2597,16 +3490,16 @@ async function enableQuickUnlock() { return toast('Wrong master password', 'error'); } - // Export the raw key + bundle session pieces needed for a cold-start - // restore (no master pw available). Send as base64-encoded UTF-8 JSON. + // Export the raw key + identity. We don't store the session token — + // tryQuickUnlock re-logs in with a verifier derived from the key, + // which always yields a fresh server session (the stored token would + // expire after 24 h and break cold-start restore on a moved exe). const raw = await crypto.subtle.exportKey('raw', state.cryptoKey); const blob = JSON.stringify({ - v: 1, + v: 2, username: state.username, salt: state.salt, kdfIterations: state.kdfIterations, - token: state.token, - csrf: state.csrf, key: bytesToBase64(raw), }); const b64 = bytesToBase64(new TextEncoder().encode(blob)); @@ -2645,11 +3538,13 @@ function updateQuickUnlockUI() { // false otherwise (caller falls back to master-pw login). async function tryQuickUnlock() { if (!Bridge.active) return false; - if (localStorage.getItem('quickUnlockEnabled') !== '1') return false; const b64 = await bridgeRequestQuickUnlock(); if (!b64) return false; + localStorage.setItem('quickUnlockEnabled', '1'); + state.quickUnlockEnabled = true; + let parsed; try { const jsonStr = new TextDecoder().decode(base64ToBytes(b64)); @@ -2659,27 +3554,49 @@ async function tryQuickUnlock() { } if (!parsed || !parsed.key || !parsed.salt || !parsed.username) return false; - // Restore session state from the blob. + // Restore identity + crypto key from the blob. state.username = parsed.username; state.salt = parsed.salt; state.kdfIterations = parsed.kdfIterations || 600000; - state.token = parsed.token || sessionStorage.getItem('authToken') || ''; - state.csrf = parsed.csrf || sessionStorage.getItem('csrfToken') || ''; - sessionStorage.setItem('username', state.username); - sessionStorage.setItem('salt', state.salt); - sessionStorage.setItem('kdfIterations', String(state.kdfIterations)); - if (state.token) sessionStorage.setItem('authToken', state.token); - if (state.csrf) sessionStorage.setItem('csrfToken', state.csrf); + const rawKey = base64ToBytes(parsed.key); try { state.cryptoKey = await crypto.subtle.importKey( - 'raw', base64ToBytes(parsed.key), + 'raw', rawKey, { name: 'AES-GCM' }, true, ['encrypt', 'decrypt']); - await persistCryptoKey(); } catch (e) { return false; } + // Always request a fresh session token via /login using the key-derived + // verifier. The stored token (if any) may have expired or been cleaned + // up by the server's session GC, which used to drop the user back to + // the login screen on cold start. + try { + const verifier = bytesToHex(rawKey); + const r = await api('/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: state.username, verifier }), + }); + state.token = r.token; + state.csrf = r.csrfToken; + if (r.salt) state.salt = r.salt; + if (r.kdfIterations) state.kdfIterations = r.kdfIterations; + } catch (e) { + // Login failed — vault credentials may have changed (master pw + // rotation) since Quick Unlock was set up. Force a fresh master-pw + // login; the user will need to re-enable Quick Unlock afterwards. + return false; + } + + sessionStorage.setItem('username', state.username); + sessionStorage.setItem('salt', state.salt); + sessionStorage.setItem('kdfIterations', String(state.kdfIterations)); + sessionStorage.setItem('authToken', state.token); + sessionStorage.setItem('csrfToken', state.csrf); + await persistCryptoKey(); + state.locked = false; return true; } @@ -2806,18 +3723,50 @@ async function doGenerateRecoveryKey() { // Show the code ONCE. Use the confirm modal so the user has to // explicitly click "I saved it" before the value vanishes. + // + // Wire the inline Copy button BEFORE awaiting the dialog: confirmDialog + // injects the HTML synchronously, so a 0-ms task fires after the DOM + // is in place but before the user can interact. CSP forbids inline + // onclick handlers, hence the addEventListener route. + setTimeout(() => { + const btn = document.getElementById('copyRecoveryCodeBtn'); + if (!btn) return; + btn.addEventListener('click', () => { + // Same path as password copy: secure-clipboard via Delphi + // (excluded from Win+V history, auto-cleared after 30s) when + // running embedded, navigator.clipboard with manual scrub + // otherwise. + if (Bridge.copySecure(code, 30000)) { + toast('Recovery code copied · clears in 30s'); + } else { + navigator.clipboard.writeText(code).then(() => { + toast('Recovery code copied · clears in 30s'); + setTimeout(() => navigator.clipboard.writeText('').catch(()=>{}), 30000); + }); + } + }); + }, 0); + await confirmDialog({ title: 'Your recovery code', message: '

Save this code somewhere safe (password manager, ' + 'safe deposit box, printed copy). It will not be ' + 'shown again.

' + - '

' + - code + '

' + + '
' + + '' + + code + '' + + '' + + '
' + '

' + - 'Using it later will let you recover access if you forget ' + - 'your master password. The code is single-use.

', + 'Using it lets you recover access if you forget your master ' + + 'password. The code can be used up to 5 times, and ' + + 'is permanently erased as soon as you successfully change ' + + 'your master password — so set a new one right after ' + + 'recovering.

', okText: 'I saved it', }); toast('Recovery code generated'); @@ -2849,7 +3798,11 @@ function updateRecoveryStatusLabel() { const removeBtn = $('#recoveryRemoveBtn'); if (!lbl) return; if (state.recoveryConfigured) { - lbl.textContent = 'Recovery key is configured.'; + const left = state.recoveryRemainingUses; + const usesNote = (typeof left === 'number' && left < 5) + ? ' (' + left + ' use' + (left === 1 ? '' : 's') + ' left)' + : ''; + lbl.textContent = 'Recovery key is configured.' + usesNote; if (setupBtn) setupBtn.textContent = 'Regenerate code'; if (removeBtn) removeBtn.style.display = ''; } else { @@ -2863,6 +3816,7 @@ async function refreshRecoveryStatus() { try { const r = await api('/recovery-key/status', { headers: authHeaders() }); state.recoveryConfigured = !!r.configured; + state.recoveryRemainingUses = (typeof r.remaining_uses === 'number') ? r.remaining_uses : 5; updateRecoveryStatusLabel(); } catch (e) { /* ignore */ } } @@ -2880,8 +3834,9 @@ async function doRecoveryRedeem() { if (!u) return; const code = await promptDialog({ title: 'Enter recovery code', - message: 'Recovery codes look like XXXX-XXXX-XXXX-XXXX. ' + - 'They\'re single-use — using one will remove it from your account.', + message: 'Recovery codes look like XXXX-XXXX-XXXX-XXXX. They allow ' + + 'up to 5 uses, and are erased when you set a new master ' + + 'password — remember to generate a fresh code afterwards.', placeholder: 'XXXX-XXXX-XXXX-XXXX', okText: 'Recover', password: true, @@ -2929,14 +3884,15 @@ async function doRecoveryRedeem() { 'raw', rawKey, { name: 'AES-GCM' }, true, ['encrypt', 'decrypt']); await persistCryptoKey(); - toast('Access recovered — please set a new master password'); + const remaining = (typeof r.remainingUses === 'number') ? r.remainingUses : 0; + if (remaining <= 0) { + toast('Last recovery use — set a new master password now or the code is gone forever', 'warning'); + } else { + toast('Recovery code used. ' + remaining + ' use(s) left before it expires. Change your master password now.', 'warning'); + } + state.justRecovered = true; await enterApp(); - // Force a master pw change immediately. The recovery code is consumed - // (server deleted the row); the account is currently orphaned from - // a "we know who you are" perspective. Setting a new master pw both - // restores normal login AND lets the user generate a fresh recovery - // code afterwards. setTimeout(openChangeMasterModal, 300); } @@ -2968,8 +3924,25 @@ function openChangeMasterModal() { $('#cmConfirmPwd').value = ''; const errEl = $('#cmError'); if (errEl) { errEl.textContent = ''; errEl.style.display = 'none'; } + + const curInput = $('#cmCurrentPwd'); + const curWrap = curInput.closest('.field') || curInput.parentElement; + const titleEl = $('#changeMasterTitle'); + if (state.justRecovered) { + if (curWrap) curWrap.classList.add('is-hidden'); + if (curInput) curInput.required = false; + if (titleEl) titleEl.textContent = 'Set new master password'; + } else { + if (curWrap) curWrap.classList.remove('is-hidden'); + if (curInput) curInput.required = true; + if (titleEl) titleEl.textContent = 'Change master password'; + } + $('#changeMasterModal').classList.remove('is-hidden'); - setTimeout(() => $('#cmCurrentPwd').focus(), 50); + setTimeout(() => { + if (state.justRecovered) $('#cmNewPwd').focus(); + else $('#cmCurrentPwd').focus(); + }, 50); } function closeChangeMasterModal() { @@ -2984,16 +3957,16 @@ function showCmError(msg) { } async function doChangeMasterPassword() { + const recoveryMode = !!state.justRecovered; const curPwd = $('#cmCurrentPwd').value; const newPwd = $('#cmNewPwd').value; const confPwd = $('#cmConfirmPwd').value; - // Local validation. Server enforces these too, but failing fast saves - // a round trip + leaves the modal open so the user can fix and retry. - if (!curPwd || !newPwd || !confPwd) return showCmError('All fields are required'); + if (!recoveryMode && !curPwd) return showCmError('All fields are required'); + if (!newPwd || !confPwd) return showCmError('All fields are required'); if (newPwd.length < 8) return showCmError('New password must be at least 8 characters'); if (newPwd !== confPwd) return showCmError('New password and confirmation do not match'); - if (newPwd === curPwd) return showCmError('New password must differ from the current one'); + if (!recoveryMode && newPwd === curPwd) return showCmError('New password must differ from the current one'); // Disable the confirm button so a double-click doesn't fire two // re-encryption passes in parallel. @@ -3006,8 +3979,14 @@ async function doChangeMasterPassword() { const newSalt = randomHexSalt(); const newDerived = await deriveKeyAndVerifier(newPwd, newSalt, 600000); const newKey = newDerived.cryptoKey; - const currentVerifier = await computeVerifier( - curPwd, state.salt, state.kdfIterations || 100000); + let currentVerifier; + if (recoveryMode) { + const rawCurrentKey = new Uint8Array(await crypto.subtle.exportKey('raw', state.cryptoKey)); + currentVerifier = bytesToHex(rawCurrentKey); + } else { + currentVerifier = await computeVerifier( + curPwd, state.salt, state.kdfIterations || 100000); + } // Step 2: re-encrypt every entry's password AND every entry's TOTP // secret (if present) under the new key. The current state.cryptoKey @@ -3085,8 +4064,9 @@ async function doChangeMasterPassword() { state.quickUnlockEnabled = false; } + state.justRecovered = false; closeChangeMasterModal(); - toast('Master password changed · other sessions signed out'); + toast(recoveryMode ? 'New master password set' : 'Master password changed · other sessions signed out'); } catch (err) { if (err.status === 401) { showCmError('Current password is incorrect'); @@ -3356,6 +4336,7 @@ async function encryptImportEntry(plain) { } return { site: plain.site, + title: plain.title || '', username: plain.username || '', encrypted_password: pw.encrypted, iv: pw.iv, @@ -3535,6 +4516,7 @@ async function doExport() { } payload.entries.push({ site: e.site, + title: e.title || '', username: e.username, password: plain, folder: e.folder, @@ -3562,12 +4544,280 @@ async function doExport() { toast(payload.entries.length + ' entries exported (encrypted)'); } +// ============================================================ +// AUTOFILL (Ctrl+Shift+L global hotkey) +// ============================================================ + +// Win32 modifier flags for RegisterHotKey. +const WIN32_MOD = { alt: 0x0001, ctrl: 0x0002, shift: 0x0004, win: 0x0008 }; + +// Format a combo for human display: "Ctrl+Shift+L". +function autofillComboLabel(c) { + if (!c || !c.key) return '— not set —'; + const parts = []; + if (c.ctrl) parts.push('Ctrl'); + if (c.alt) parts.push('Alt'); + if (c.shift) parts.push('Shift'); + if (c.win) parts.push('Win'); + parts.push(c.key); + return parts.join('+'); +} + +// Convert a combo to the Win32 (mods bitmask, virtual-key code) pair that +// Delphi's RegisterHotKey takes. key='A'..'Z'/'0'..'9' → ASCII code; +// 'F1'..'F12' → 0x70..0x7B. +function autofillComboToWin32(c) { + let mods = 0; + if (c.ctrl) mods |= WIN32_MOD.ctrl; + if (c.alt) mods |= WIN32_MOD.alt; + if (c.shift) mods |= WIN32_MOD.shift; + if (c.win) mods |= WIN32_MOD.win; + let vk = 0; + const k = (c.key || '').toUpperCase(); + if (/^F([1-9]|1[0-2])$/.test(k)) vk = 0x70 + parseInt(k.slice(1)) - 1; + else if (k.length === 1 && k >= 'A' && k <= 'Z') vk = k.charCodeAt(0); + else if (k.length === 1 && k >= '0' && k <= '9') vk = k.charCodeAt(0); + return { mods, vk }; +} + +// Validate a captured combo. Requires at least one modifier (otherwise a +// single key would steal that letter globally) and a valid main key. +function autofillComboValid(c) { + if (!c) return false; + if (!(c.ctrl || c.alt || c.win)) return false; // shift-only is unreliable + const w = autofillComboToWin32(c); + return w.vk !== 0; +} + +// Capture a key combo from a single keydown event. Returns null if the +// event is "incomplete" (only modifiers pressed so far) or Escape. +function autofillCaptureFromEvent(e) { + const k = e.key; + if (k === 'Escape') return 'cancel'; + // Ignore pure-modifier keydowns (user is still building the combo). + if (k === 'Control' || k === 'Shift' || k === 'Alt' || + k === 'Meta' || k === 'OS') return null; + // Accept letter, digit, F1-F12. + let key = null; + if (k.length === 1 && /[a-z0-9]/i.test(k)) { + key = k.toUpperCase(); + } else if (/^F([1-9]|1[0-2])$/i.test(k)) { + key = k.toUpperCase(); + } else { + return 'invalid'; + } + return { + ctrl: !!e.ctrlKey, + shift: !!e.shiftKey, + alt: !!e.altKey, + win: !!e.metaKey, + key, + }; +} + +// Push current state to Delphi (toggle + both combos) and persist. +// Called after any change so Delphi's RegisterHotKey reflects state. +function autofillPushHotkeys() { + localStorage.setItem('autofillHotkeyFull', JSON.stringify(state.autofillHotkeyFull)); + localStorage.setItem('autofillHotkeyPwd', JSON.stringify(state.autofillHotkeyPwd)); + if (Bridge.active) { + Bridge.setAutofillHotkeys(state.autofillEnabled, { + full: state.autofillHotkeyFull, + password: state.autofillHotkeyPwd, + }); + } +} + +// Extract a bare hostname from a site string for fuzzy matching. +// "https://www.github.com/login" → "github.com" +// Strip the browser brand suffix that lives at the end of every tab title +// ("Some Page - Google Chrome", "Page — Mozilla Firefox", etc.). Without +// this, entries whose site is "google" / "mozilla" / "edge" would match +// every single page that has Chrome / Firefox / Edge as the browser brand. +const BROWSER_SUFFIX_RE = + /\s*[-—–|]\s*(google chrome|chromium|mozilla firefox|firefox|microsoft edge|edge|brave|opera|vivaldi|safari|tor browser|tor|arc)\s*$/i; + +function autofillStripBrowserSuffix(title) { + return (title || '').replace(BROWSER_SUFFIX_RE, '').trim(); +} + +function autofillExtractHost(site) { + return site.toLowerCase() + .replace(/^https?:\/\//i, '') + .replace(/^www\./i, '') + .split('/')[0] + .split(':')[0]; +} + +// Get the second-level domain (brand part) from a hostname. +// "github.com" → "github" ; "mail.google.com" → "google" ; "x.com" → "x" +function autofillSLD(host) { + const parts = host.split('.').filter(p => p.length > 0); + if (parts.length <= 1) return host; + return parts[parts.length - 2]; +} + +// Escape a string for safe insertion into a RegExp. +function autofillEscapeRegex(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +// Score how well a vault entry matches the foreground window title. +// Returns 0 (no match) or a positive integer (higher = better). +// +// Strategy (browser titles rarely contain the full hostname — usually +// just the brand name, e.g. "Sign in to GitHub" or "X. C'est… - Google Chrome"): +// 1. Full hostname substring → strongest (score 1000 + len) +// 2. SLD ≥3 chars as substring → medium (score 500 + len) +// 3. SLD <3 chars as word → weak (score 100), requires word +// boundaries to avoid matching "x" inside arbitrary words. +function autofillScore(entry, titleLower) { + // 1. Display name (entry.title) lowercased substring — strongest brand + // match. Skips when title is empty or same as site (already tested + // via the site path below). + const displayTitle = (entry.title || '').trim().toLowerCase(); + if (displayTitle.length >= 2 && titleLower.includes(displayTitle)) + return 1500 + displayTitle.length; + + if (!entry.site) return 0; + const host = autofillExtractHost(entry.site); + if (host.length < 2) return 0; + + // 2. Full hostname (rare in tab titles, but strongest URL signal) + if (titleLower.includes(host)) return 1000 + host.length; + + // 3/4. Second-level domain + const sld = autofillSLD(host); + if (sld.length === 0) return 0; + + if (sld.length >= 3) { + if (titleLower.includes(sld)) return 500 + sld.length; + return 0; + } + + // Short SLD ("x", "qq", "vk"…) — require word boundaries so we don't + // match the letter inside random words. + const re = new RegExp('(^|[^a-z0-9])' + autofillEscapeRegex(sld) + + '([^a-z0-9]|$)', 'i'); + if (re.test(titleLower)) return 100; + + return 0; +} + +// Called by Bridge.onAutofillRequest when a hotkey fires. +// kind: 'full' = Ctrl+Shift+L (user + Tab + pwd) ; 'password' = Ctrl+Shift+P. +async function autofillHandleRequest(windowTitle, kind) { + if (!state.autofillEnabled) return; + + if (!state.cryptoKey || state.locked || !state.token) { + // Vault is locked — bring the app to the front so the user can + // unlock immediately, rather than silently no-op'ing the hotkey. + Bridge.cancelAutofill(); + Bridge.focusApp(); + setTimeout(() => { + const pwd = document.getElementById('loginPassword'); + const user = document.getElementById('loginUsername'); + if (pwd && !document.getElementById('authScreen').classList.contains('is-hidden')) { + if (user && !user.value) user.focus(); + else pwd.focus(); + } + }, 80); + toast('Vault is locked — unlock to autofill', 'warning'); + return; + } + + const titleLower = autofillStripBrowserSuffix(windowTitle).toLowerCase(); + const scored = state.entries + .map(e => ({ entry: e, score: autofillScore(e, titleLower) })) + .filter(x => x.score > 0) + .sort((a, b) => b.score - a.score); + + if (scored.length === 0) { + toast('Autofill: no match for "' + windowTitle.slice(0, 40) + '"', 'warning'); + Bridge.cancelAutofill(); + return; + } + + if (scored.length === 1) { + await autofillFillEntry(scored[0].entry, kind); + return; + } + + // Multiple candidates — show picker. kind is captured so clicking a + // candidate honours password-only mode. + openAutofillPicker(scored.map(x => x.entry), windowTitle, kind); +} + +// Decrypt and type an entry. kind = 'full' or 'password'. +async function autofillFillEntry(entry, kind) { + const password = await decryptPwd(entry.encrypted_password, entry.iv); + if (password === '[ERROR]') { + toast('Autofill: decryption error', 'error'); + Bridge.cancelAutofill(); + return; + } + // password-only kind → empty username → Delphi skips Tab. + // full kind with empty entry.username → also no Tab (Delphi handles it). + const user = (kind === 'password') ? '' : (entry.username || ''); + Bridge.executeAutofill(user, password); + toast((kind === 'password' ? 'Password filled: ' : 'Autofilled: ') + entry.site); + // Audit (best-effort, ignore failures) + fetch('' + '/audit', { + method: 'POST', + headers: authHeaders({ 'Content-Type': 'application/json' }), + body: JSON.stringify({ + action: kind === 'password' ? 'autofill_pwd' : 'autofill', + site: entry.site, + }), + }).catch(() => {}); +} + +// Picker modal for multi-match case. kind is forwarded to autofillFillEntry +// so the user's hotkey intent (full vs password-only) is preserved through +// the manual choice. +function openAutofillPicker(entries, windowTitle, kind) { + // Bring the app to front so the picker is unambiguously visible — + // otherwise the modal opens behind / next to the user's original + // window (e.g. Notepad) and easy to miss. ExecuteAutofill restores + // the original target HWND via ForceForegroundWindow on selection. + Bridge.focusApp(); + + const list = $('#autofillPickerList'); + list.innerHTML = ''; + entries.forEach(e => { + const btn = el('button', { + class: 'autofill-pick-btn', + on: { + click: async () => { + closeAutofillPicker(false); + await autofillFillEntry(e, kind); + }, + }, + }); + btn.appendChild(el('span', { class: 'autofill-pick-site' }, entryDisplayName(e))); + if (e.username) { + btn.appendChild(el('span', { class: 'autofill-pick-user' }, e.username)); + } + list.appendChild(btn); + }); + const head = (kind === 'password' ? 'Pick entry (password only) — ' : 'Pick entry — ') + + entries.length + ' match "' + windowTitle.slice(0, 30) + '…"'; + $('#autofillPickerTitle').textContent = head; + $('#autofillPickerModal').classList.remove('is-hidden'); +} + +function closeAutofillPicker(notifyCancel = true) { + $('#autofillPickerModal').classList.add('is-hidden'); + if (notifyCancel) Bridge.cancelAutofill(); +} + // ============================================================ // VIEWS / NAV // ============================================================ async function setView(v) { state.view = v; + state.currentPage = 1; if (v === 'trash') { await loadTrash(); } @@ -3583,15 +4833,24 @@ function setTheme(t) { localStorage.setItem('theme', t); const sel = $('#settingTheme'); if (sel) sel.value = t; + Bridge.syncTitleBarTheme(t); } function openSettings() { $('#settingTheme').value = state.theme; + $('#settingSort').value = state.sortBy + ':' + state.sortDir; $('#settingAutoLock').value = String(state.autoLock); $('#settingAskDelete').checked = state.askBeforeDelete; $('#settingCompact').checked = state.compactActions; $('#settingMaskUser').checked = state.maskUsernames; $('#settingHIBP').checked = state.hibpEnabled; + $('#settingShowSite').checked = state.showSiteOnCards; + $('#settingAutofill').checked = state.autofillEnabled; + $('#settingAutofillRow').style.display = Bridge.active ? '' : 'none'; + // Hotkey capture buttons — labels reflect current combos. + $('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull); + $('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd); + $('#settingAutofillHotkeysRow').style.display = Bridge.active ? '' : 'none'; $('#settingUser').textContent = state.username; // Async: query server for recovery key state and update the label refreshRecoveryStatus(); @@ -3668,34 +4927,249 @@ function resetAutoLock() { }, { passive: true }) ); -function showAuth() { +async function showAuth() { $('#authScreen').classList.remove('is-hidden'); $('#appShell').classList.add('is-hidden'); if (autoLockTimer) { clearTimeout(autoLockTimer); autoLockTimer = null; } + + let remembered = ''; + if (Bridge.active) { + remembered = await Bridge.getPref('rememberedUsername'); + } else { + remembered = localStorage.getItem('rememberedUsername') || ''; + } + const userInput = $('#loginUsername'); + const remCb = $('#loginRememberUser'); + if (remCb) remCb.checked = !!remembered; + if (userInput && !userInput.value && remembered) userInput.value = remembered; + // Delphi-hosted: ask Delphi to SetFocus the WebBrowser control first + // (DOM input.focus() is a no-op while the WebView2 lacks OS-level + // focus). Web fallback: direct DOM focus. + if (Bridge.active) { + Bridge.appReady(); + } else { + setTimeout(() => { + const u = $('#loginUsername'); + const p = $('#loginPassword'); + if (u && u.value) p && p.focus(); + else u && u.focus(); + }, 0); + } } async function enterApp() { $('#authScreen').classList.add('is-hidden'); $('#appShell').classList.remove('is-hidden'); $('#userName').textContent = state.username; + // Server-side prefs override localStorage cache; runs before render so + // theme / view mode / mask flags are applied to the first paint. + await loadServerSettings(); // Show skeleton cards immediately while the initial fetch runs showSkeletons(6); await loadFolders(); await loadEntries(); + await loadEntryCounts(); render(); resetAutoLock(); // Fire-and-forget HIBP scan if the user opted in. Runs in background, // re-renders when done to show badges. if (state.hibpEnabled) hibpCheckAllEntries(); + // Push the user-configured hotkeys (combos + enabled state) to Delphi. + // Replaces the historical "always Ctrl+Shift+L on startup" path. + autofillPushHotkeys(); +} + +// ============================================================ +// SERVER-SIDE SETTINGS SYNC +// ============================================================ +// +// Synced keys (user preferences, portable across devices). Device-specific +// toggles (quickUnlockEnabled, autofillEnabled) stay in localStorage. +const SYNCED_SETTING_KEYS = [ + 'theme', 'autoLock', 'askBeforeDelete', 'maskUsernames', + 'compactActions', 'viewMode', 'hibpEnabled', 'showSiteOnCards', + 'sortBy', 'sortDir', 'pageSize', + // Hotkey combos are user preferences — values are portable. The + // registration itself is Windows-only, so non-Windows clients just + // ignore them. + 'autofillHotkeyFull', 'autofillHotkeyPwd', + // Sidebar section collapsed state. Object of { folders, tags, tools } + // booleans. Synced so the user gets the same fold state across devices. + 'sidebarCollapsed', +]; + +function applySidebarCollapsed() { + const s = state.sidebarCollapsed || {}; + document.querySelectorAll('.sidebar-section[data-section]').forEach(sec => { + const k = sec.getAttribute('data-section'); + sec.classList.toggle('is-collapsed', !!s[k]); + }); +} + +async function loadServerSettings() { + try { + const r = await fetch(API + '/settings', { headers: authHeaders() }); + if (!r.ok) return; + const remote = await r.json(); + // Merge remote into state (remote wins). localStorage is updated + // too so first-paint on next reload uses the synced value. + SYNCED_SETTING_KEYS.forEach(k => { + if (!(k in remote)) return; + const v = remote[k]; + state[k] = v; + switch (k) { + case 'theme': localStorage.setItem('theme', v); break; + case 'autoLock': localStorage.setItem('autoLockMin', String(v)); break; + case 'askBeforeDelete': localStorage.setItem('askBeforeDelete', v ? '1' : '0'); break; + case 'maskUsernames': localStorage.setItem('maskUsernames', v ? '1' : '0'); break; + case 'compactActions': localStorage.setItem('compactActions', v ? '1' : '0'); break; + case 'viewMode': localStorage.setItem('viewMode', v); break; + case 'hibpEnabled': localStorage.setItem('hibpEnabled', v ? '1' : '0'); break; + case 'showSiteOnCards': localStorage.setItem('showSiteOnCards', v ? '1' : '0'); break; + case 'sortBy': localStorage.setItem('sortBy', String(v)); break; + case 'sortDir': localStorage.setItem('sortDir', String(v)); break; + case 'pageSize': localStorage.setItem('pageSize', String(v)); break; + case 'autofillHotkeyFull': + case 'autofillHotkeyPwd': + case 'sidebarCollapsed': + // Object; persist as JSON so the next cold start picks it up. + localStorage.setItem(k, JSON.stringify(v)); + break; + } + }); + // Apply visual settings immediately. + if (remote.theme) setTheme(remote.theme); + if ('sidebarCollapsed' in remote) applySidebarCollapsed(); + } catch (e) { + // Network/server hiccup is harmless — localStorage cache still works. + } +} + +let _settingsSaveTimer = null; +function saveServerSettings() { + // Debounce: collapse rapid toggles (e.g. user playing with the theme + // dropdown) into a single PUT. + if (_settingsSaveTimer) clearTimeout(_settingsSaveTimer); + _settingsSaveTimer = setTimeout(async () => { + _settingsSaveTimer = null; + const payload = {}; + SYNCED_SETTING_KEYS.forEach(k => { payload[k] = state[k]; }); + try { + await fetch(API + '/settings', { + method: 'PUT', + headers: authHeaders({ 'Content-Type': 'application/json' }), + body: JSON.stringify(payload), + }); + } catch (e) { + // Silent — next change will retry. + } + }, 400); } // ============================================================ // INIT // ============================================================ +function installCustomContextMenu() { + const menu = el('div', { class: 'custom-ctxmenu is-hidden' }); + document.body.appendChild(menu); + + function isEditable(node) { + if (!node) return false; + const tag = node.tagName; + if (tag === 'INPUT') return !['button','submit','checkbox','radio','range','color','file'].includes(node.type); + if (tag === 'TEXTAREA') return true; + if (node.isContentEditable) return true; + return false; + } + + function hide() { menu.classList.add('is-hidden'); } + + function buildItems(target) { + menu.innerHTML = ''; + const isInput = isEditable(target); + const hasSelection = isInput && target.selectionStart !== target.selectionEnd; + const items = [ + { lbl: 'Cut', on: hasSelection, fn: () => doCut(target) }, + { lbl: 'Copy', on: hasSelection, fn: () => doCopy(target) }, + { lbl: 'Paste', on: isInput && !target.readOnly, fn: () => doPaste(target) }, + { lbl: 'Select all', on: isInput, fn: () => target.select() }, + ]; + items.forEach(it => { + const mi = el('button', { + class: 'custom-ctxmenu-item' + (it.on ? '' : ' is-disabled'), + type: 'button', + on: { click: ev => { + ev.preventDefault(); + ev.stopPropagation(); + if (!it.on) return; + hide(); + it.fn(); + } }, + }, it.lbl); + menu.appendChild(mi); + }); + } + + async function doCopy(target) { + const sel = target.value.slice(target.selectionStart, target.selectionEnd); + try { await navigator.clipboard.writeText(sel); } catch (e) {} + } + async function doCut(target) { + await doCopy(target); + const s = target.selectionStart, e = target.selectionEnd; + target.value = target.value.slice(0, s) + target.value.slice(e); + target.selectionStart = target.selectionEnd = s; + target.dispatchEvent(new Event('input', { bubbles: true })); + } + async function doPaste(target) { + const txt = Bridge.active ? await bridgeReadClipboard() + : await navigator.clipboard.readText().catch(() => ''); + if (!txt) return; + const s = target.selectionStart, e = target.selectionEnd; + target.value = target.value.slice(0, s) + txt + target.value.slice(e); + target.selectionStart = target.selectionEnd = s + txt.length; + target.dispatchEvent(new Event('input', { bubbles: true })); + } + + document.addEventListener('contextmenu', ev => { + ev.preventDefault(); + if (!isEditable(ev.target)) { hide(); return; } + buildItems(ev.target); + menu.classList.remove('is-hidden'); + const vw = window.innerWidth, vh = window.innerHeight; + const mw = menu.offsetWidth || 160, mh = menu.offsetHeight || 140; + const x = Math.min(ev.clientX, vw - mw - 4); + const y = Math.min(ev.clientY, vh - mh - 4); + menu.style.left = x + 'px'; + menu.style.top = y + 'px'; + }); + document.addEventListener('mousedown', ev => { + if (!ev.target.closest('.custom-ctxmenu')) hide(); + }); + document.addEventListener('keydown', ev => { + if (ev.key === 'Escape') hide(); + }); + window.addEventListener('blur', hide); +} + async function init() { document.documentElement.setAttribute('data-theme', state.theme); + if (location.search.indexOf('pmt=') !== -1) { + history.replaceState(null, '', location.pathname + location.hash); + } + + installCustomContextMenu(); + document.addEventListener('keydown', e => { + const mod = e.ctrlKey || e.metaKey; + if (e.key === 'F12' || e.key === 'F5') return e.preventDefault(); + if (mod && e.shiftKey && /^[ijIJ]$/.test(e.key)) return e.preventDefault(); // DevTools + if (mod && /^[uUjJhHsSpPtTnNrR]$/.test(e.key)) return e.preventDefault(); // View source / Downloads / History / Save / Print / New tab+win / Reload + if (mod && e.shiftKey && /^[nNwW]$/.test(e.key)) return e.preventDefault(); // New incognito / Close window + if (mod && e.shiftKey && e.key === 'Delete') return e.preventDefault(); // Clear browsing data + }); + // Auth tabs $$('.auth-tab').forEach(t => { t.addEventListener('click', () => { @@ -3721,14 +5195,23 @@ async function init() { applyViewMode(); $$('.view-btn').forEach(b => b.addEventListener('click', () => { state.viewMode = b.dataset.view; + state.currentPage = 1; localStorage.setItem('viewMode', state.viewMode); applyViewMode(); + saveServerSettings(); })); - $('#themeBtn').addEventListener('click', toggleTheme); + $('#themeBtn').addEventListener('click', () => { + toggleTheme(); + saveServerSettings(); + }); $('#newEntryBtn').addEventListener('click', () => openEntryModal()); $('#userChip').addEventListener('click', () => $('#userDropdown').classList.toggle('is-hidden')); $('#lockBtn').addEventListener('click', lockVault); + $('#dropdownSettingsBtn').addEventListener('click', () => { + $('#userDropdown').classList.add('is-hidden'); + openSettings(); + }); $('#logoutBtn').addEventListener('click', doLogout); document.addEventListener('click', e => { if (!e.target.closest('.user-menu')) $('#userDropdown').classList.add('is-hidden'); @@ -3767,12 +5250,10 @@ async function init() { // Search $('#searchInput').addEventListener('input', e => { state.search = e.target.value; + state.currentPage = 1; renderGrid(); }); - // Marquee rubber-band selection on the entry grid - $('#entryGrid').addEventListener('mousedown', startMarquee); - // Slide-over $('#slideoverClose').addEventListener('click', closeSlideOver); // Click outside the slide-over closes it. Clicks on cards re-open it for @@ -3788,6 +5269,19 @@ async function init() { closeSlideOver(); }); + // Click outside the settings panel closes it. Each setting change has + // already pushed to server + localStorage, so "close = autosave" is + // implicit. Ignore clicks on the triggers and on any open modal (so the + // reauth / confirm flows fired from inside settings don't dismiss it). + document.addEventListener('click', e => { + if (!$('#settingsPanel').classList.contains('is-open')) return; + if (e.target.closest('#settingsPanel')) return; + if (e.target.closest('#settingsBtn')) return; + if (e.target.closest('#dropdownSettingsBtn')) return; + if (e.target.closest('.modal')) return; + closeSettings(); + }); + // Entry modal $('#entryForm').addEventListener('submit', saveEntry); $('#entrySaveBtn').addEventListener('click', saveEntry); @@ -3796,6 +5290,10 @@ async function init() { const input = $('#entryPassword'); input.type = input.type === 'password' ? 'text' : 'password'; }); + $('#loginPwToggle').addEventListener('click', () => { + const input = $('#loginPassword'); + input.type = input.type === 'password' ? 'text' : 'password'; + }); $('#entryPwGen').addEventListener('click', openGen); // Chip input (tags) @@ -3851,6 +5349,26 @@ async function init() { $('#sidebarGenBtn').addEventListener('click', () => openGen('standalone')); $('#sidebarExportBtn').addEventListener('click', doExport); $('#sidebarImportBtn').addEventListener('click', doImport); + $('#sidebarAuthenticatorBtn').addEventListener('click', () => { + state.view = 'authenticator'; + state.currentPage = 1; + $$('.nav-item').forEach(b => b.classList.remove('is-active')); + render(); + }); + $('#sidebarTotpToolBtn').addEventListener('click', openTotpTool); + + // Sidebar section collapse toggles + document.querySelectorAll('[data-section-toggle]').forEach(btn => { + btn.addEventListener('click', () => { + const key = btn.getAttribute('data-section-toggle'); + state.sidebarCollapsed = state.sidebarCollapsed || {}; + state.sidebarCollapsed[key] = !state.sidebarCollapsed[key]; + applySidebarCollapsed(); + localStorage.setItem('sidebarCollapsed', JSON.stringify(state.sidebarCollapsed)); + saveServerSettings(); + }); + }); + applySidebarCollapsed(); // Idle warning "Stay unlocked" $('#idleStayBtn').addEventListener('click', resetAutoLock); @@ -3858,31 +5376,57 @@ async function init() { // Settings slide-over $('#settingsBtn').addEventListener('click', openSettings); $('#settingsClose').addEventListener('click', closeSettings); - $('#settingTheme').addEventListener('change', e => setTheme(e.target.value)); + // Theme: setTheme already writes localStorage. Capture before/after so + // sync only fires if it actually changed. + $('#settingTheme').addEventListener('change', e => { + setTheme(e.target.value); + state.theme = e.target.value; + saveServerSettings(); + }); + $('#settingSort').addEventListener('change', e => { + const [by, dir] = e.target.value.split(':'); + state.sortBy = by; state.sortDir = dir; + state.currentPage = 1; + localStorage.setItem('sortBy', by); + localStorage.setItem('sortDir', dir); + render(); + saveServerSettings(); + }); $('#settingAutoLock').addEventListener('change', e => { state.autoLock = parseInt(e.target.value); localStorage.setItem('autoLockMin', String(state.autoLock)); resetAutoLock(); + saveServerSettings(); toast(state.autoLock ? ('Auto-lock: ' + state.autoLock + ' min') : 'Auto-lock disabled'); }); $('#settingAskDelete').addEventListener('change', e => { state.askBeforeDelete = e.target.checked; localStorage.setItem('askBeforeDelete', state.askBeforeDelete ? '1' : '0'); + saveServerSettings(); toast(state.askBeforeDelete ? 'Will ask before deleting' : 'Will delete without asking'); }); $('#settingCompact').addEventListener('change', e => { state.compactActions = e.target.checked; localStorage.setItem('compactActions', state.compactActions ? '1' : '0'); render(); + saveServerSettings(); }); $('#settingMaskUser').addEventListener('change', e => { state.maskUsernames = e.target.checked; localStorage.setItem('maskUsernames', state.maskUsernames ? '1' : '0'); render(); + saveServerSettings(); + }); + $('#settingShowSite').addEventListener('change', e => { + state.showSiteOnCards = e.target.checked; + localStorage.setItem('showSiteOnCards', state.showSiteOnCards ? '1' : '0'); + render(); + saveServerSettings(); }); $('#settingHIBP').addEventListener('change', e => { state.hibpEnabled = e.target.checked; localStorage.setItem('hibpEnabled', state.hibpEnabled ? '1' : '0'); + saveServerSettings(); if (state.hibpEnabled) { toast('Checking passwords against breach database…'); hibpCheckAllEntries(); @@ -3892,6 +5436,89 @@ async function init() { toast('Breach check disabled'); } }); + $('#settingAutofill').addEventListener('change', e => { + state.autofillEnabled = e.target.checked; + localStorage.setItem('autofillEnabled', state.autofillEnabled ? '1' : '0'); + // Push the FULL state (toggle + combos) so Delphi register/unregister + // uses the user's current combos, not the defaults. + autofillPushHotkeys(); + const lbl = autofillComboLabel(state.autofillHotkeyFull); + toast(state.autofillEnabled ? ('Autofill enabled (' + lbl + ')') : 'Autofill disabled'); + }); + + // ---- Hotkey capture buttons ---- + // Click → button label becomes "Press combo…" → next keydown captures. + // While capturing, all other keys are swallowed so the user can press + // any modifier+letter combo without triggering app shortcuts. + function bindHotkeyCapture(btnId, kind) { + const btn = $(btnId); + if (!btn) return; + btn.addEventListener('click', () => { + if (btn.dataset.capturing === '1') return; + btn.dataset.capturing = '1'; + btn.classList.add('is-capturing'); + const original = btn.textContent; + btn.textContent = 'Press combo… (Esc to cancel)'; + + function finish(restore) { + btn.dataset.capturing = ''; + btn.classList.remove('is-capturing'); + document.removeEventListener('keydown', onKey, true); + if (restore) btn.textContent = original; + } + + function onKey(e) { + // Swallow EVERYTHING while capturing so the user's combo + // doesn't trigger the cmd-palette etc. + e.preventDefault(); + e.stopPropagation(); + const captured = autofillCaptureFromEvent(e); + if (captured === null) return; // still building + if (captured === 'cancel') { finish(true); return; } + if (captured === 'invalid'){ + toast('Unsupported key — use a letter, digit or F-key', 'warning'); + finish(true); + return; + } + if (!autofillComboValid(captured)) { + toast('Combo needs at least Ctrl, Alt or Win as a modifier', 'warning'); + finish(true); + return; + } + // Reject if it collides with the other slot. + const other = (kind === 'full') ? state.autofillHotkeyPwd : state.autofillHotkeyFull; + if (JSON.stringify(other) === JSON.stringify(captured)) { + toast('That combo is already used by the other hotkey', 'warning'); + finish(true); + return; + } + // Commit. + if (kind === 'full') state.autofillHotkeyFull = captured; + else state.autofillHotkeyPwd = captured; + btn.textContent = autofillComboLabel(captured); + finish(false); + autofillPushHotkeys(); // re-register in Delphi + saveServerSettings(); // sync to server (debounced) + } + document.addEventListener('keydown', onKey, true); + }); + } + bindHotkeyCapture('#settingAutofillFullCombo', 'full'); + bindHotkeyCapture('#settingAutofillPwdCombo', 'password'); + + $('#settingAutofillResetHotkeys').addEventListener('click', () => { + state.autofillHotkeyFull = { ctrl: true, shift: true, alt: false, win: false, key: 'L' }; + state.autofillHotkeyPwd = { ctrl: true, shift: true, alt: false, win: false, key: 'P' }; + $('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull); + $('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd); + autofillPushHotkeys(); + saveServerSettings(); + toast('Hotkeys reset to defaults'); + }); + + // Autofill picker modal close button + $$('#autofillPickerModal [data-close]').forEach(b => + b.addEventListener('click', closeAutofillPicker)); $('#openClipboardSettings').addEventListener('click', () => { toast('Open Windows Settings → System → Clipboard → turn off "Clipboard history"', 'warning'); }); @@ -3931,11 +5558,35 @@ async function init() { b.addEventListener('click', () => closeConfirm(false)) ); - // Command palette + // Helper: is the user currently typing into an input/textarea/select + // or contenteditable surface? Shortcuts like Ctrl+A must NOT hijack + // input focus (browser default = select all text in the field). + function isTypingTarget(t) { + if (!t) return false; + const tag = t.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true; + if (t.isContentEditable) return true; + return false; + } + + // Command palette + bulk shortcuts document.addEventListener('keydown', e => { if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); openPalette(); + } else if ((e.ctrlKey || e.metaKey) && (e.key === 'a' || e.key === 'A')) { + // Ctrl+A = select every visible entry. Replaces the role of + // a marquee "drag across all cards" that we used to have. + // Only fire when the user isn't typing in a field — otherwise + // we'd steal the universal Select All in inputs. + if (isTypingTarget(e.target)) return; + // Don't fire if the app isn't actually showing the entry grid + // (auth screen, locked, etc.) + if ($('#appShell').classList.contains('is-hidden')) return; + e.preventDefault(); + const visible = filteredEntries(); + visible.forEach(en => state.checked.add(en.id)); + renderGrid(); } else if (e.key === 'Escape') { // Close in priority order: confirm first (most modal-y) then others if (!$('#confirmModal').classList.contains('is-hidden')) { @@ -3950,11 +5601,28 @@ async function init() { closeSlideOver(); closeEntryModal(); closeGen(); + // If nothing else needed dismissing and there's an active + // selection, clear it. Replaces the "click empty space to + // deselect" path that the marquee provided. + if (state.checked.size > 0) { + state.checked.clear(); + renderGrid(); + } } }); $('#cmdInput').addEventListener('input', e => renderPaletteResults(e.target.value)); $$('#cmdPalette [data-close]').forEach(b => b.addEventListener('click', closePalette)); + // Re-sync quickUnlockEnabled from the DPAPI source of truth. localStorage + // is wiped at each launch (random port → new origin), so the cached value + // can lie about the actual server-side state. + if (Bridge.active) { + const has = await bridgeQuickUnlockStatus(); + state.quickUnlockEnabled = has; + if (has) localStorage.setItem('quickUnlockEnabled', '1'); + else localStorage.removeItem('quickUnlockEnabled'); + } + // Restore session if any if (state.token && state.salt) { const ok = await restoreCryptoKey(); @@ -3982,5 +5650,4 @@ async function init() { } } } - document.addEventListener('DOMContentLoaded', init); diff --git a/test_pbkdf2.php b/test_pbkdf2.php new file mode 100644 index 0000000..fd3a73e --- /dev/null +++ b/test_pbkdf2.php @@ -0,0 +1,51 @@ +busyTimeout(5000); + +$st = $db->prepare('SELECT id, username, password_hash, salt, hash_algo FROM users WHERE username=:u'); +$st->bindValue(':u', $u, SQLITE3_TEXT); +$row = $st->execute()->fetchArray(SQLITE3_ASSOC); + +if (!$row) { echo "User '$u' not found.\n"; exit; } + +echo "=== Stored in vault.db ===\n"; +echo "id : " . $row['id'] . "\n"; +echo "username : " . $row['username'] . "\n"; +echo "hash_algo : " . ($row['hash_algo'] ?? '(null)') . "\n"; +echo "salt : " . $row['salt'] . "\n"; +echo "salt len : " . strlen($row['salt']) . "\n"; +echo "stored hash : " . $row['password_hash'] . "\n"; +echo "stored len : " . strlen($row['password_hash']) . "\n\n"; + +$algo = $row['hash_algo'] ?? 'pbkdf2'; + +if ($algo === 'pbkdf2') { + $computed = hash_pbkdf2('sha256', $p, $row['salt'], 100000); + echo "=== PHP hash_pbkdf2('sha256', '$p', salt, 100000) ===\n"; + echo "computed : $computed\n"; + echo "computed len : " . strlen($computed) . "\n\n"; + + $match = hash_equals($row['password_hash'], $computed); + echo "Match : " . ($match ? 'YES ✓ (PHP would let this user in)' : 'NO ✗ (Delphi & PHP disagree on PBKDF2)') . "\n"; + + if (!$match) { + echo "\nFirst chars side by side:\n"; + echo "Stored : " . substr($row['password_hash'], 0, 32) . "\n"; + echo "Computed : " . substr($computed, 0, 32) . "\n"; + } +} else { + echo "Account uses BCRYPT, not PBKDF2. Cannot diagnose PBKDF2 mismatch here.\n"; +} + +$db->close();