Compare commits

..

143 Commits

Author SHA1 Message Date
Joe Julian 198732239a Limit group view to direct entries 2026-04-03 08:51:19 -07:00
Joe Julian da53696435 Keep parent breadcrumb on phone 2026-04-03 08:35:09 -07:00
Joe Julian 48dfe604cf Snapshot phone entry list during redraw 2026-04-03 08:28:58 -07:00
Joe Julian e31baddea1 Fix phone header spacer 2026-04-03 07:25:25 -07:00
Joe Julian e65f5db14e Autosave Android settings and refine header 2026-04-03 07:23:31 -07:00
Joe Julian 7da0fda88f Right-align phone action row 2026-04-03 07:19:26 -07:00
Joe Julian 69de8e8ab3 Align phone header actions right 2026-04-03 07:18:10 -07:00
Joe Julian a260046d5a Tighten phone group and detail layout 2026-04-03 07:10:25 -07:00
Joe Julian ea7de89c7d Simplify phone vault screen and settings navigation 2026-04-02 21:50:36 -07:00
Joe Julian 05f56d648e Make phone entry pane fully scrollable 2026-04-02 21:38:51 -07:00
Joe Julian e8ae0f6109 Trim verbose group navigation copy 2026-04-02 21:32:07 -07:00
Joe Julian 689309f704 Collapse phone subgroup browser after navigation 2026-04-02 09:41:33 -07:00
Joe Julian bbde2516a3 Fix Android reopen crash and simplify group navigation 2026-04-02 07:28:55 -07:00
Joe Julian f70b56c0b6 Fix Android local open flow 2026-04-01 21:42:19 -07:00
Joe Julian 984ff58e31 Move remote preference help out of open flow 2026-04-01 19:22:56 -07:00
Joe Julian 096e5bb48d Integrate autofill privacy settings controls 2026-04-01 18:46:10 -07:00
Joe Julian 0b1d560ef9 Add accessibility settings preferences 2026-04-01 18:46:10 -07:00
Joe Julian 021cf82758 Move sync defaults into vault settings 2026-04-01 18:46:10 -07:00
Joe Julian 5e1ca9981d Stabilize settings persistence tests 2026-04-01 18:46:10 -07:00
Joe Julian 5af8b8174d Add autofill privacy settings controls 2026-04-01 18:46:10 -07:00
Joe Julian f3ee8794c6 Add accessibility settings preferences 2026-04-01 18:46:10 -07:00
Joe Julian 6b224cfde3 Add notification feedback settings 2026-04-01 18:46:10 -07:00
Joe Julian 6dab14b3d9 Add persisted UI settings preferences 2026-04-01 18:46:10 -07:00
Joe Julian 98be2ddf77 Move remote preference help out of open flow 2026-04-01 17:37:06 -07:00
Joe Julian d8db9f14b3 Align audit UI tests with empty-state model 2026-04-01 17:21:16 -07:00
Joe Julian d1485a66c1 Refine audit UI and vault settings placement 2026-04-01 17:21:16 -07:00
Joe Julian cb1447e554 Improve local vault open usability 2026-04-01 17:17:37 -07:00
Joe Julian 315988134b Refine entry detail and edit panels 2026-04-01 17:15:58 -07:00
Joe Julian 6293eca072 Tighten mobile locked vault hierarchy 2026-04-01 17:15:20 -07:00
Joe Julian 3510bcc3b6 Improve autofill status feedback 2026-04-01 17:13:43 -07:00
Joe Julian 5c56abf31b Refine API token management UI 2026-04-01 17:12:29 -07:00
Joe Julian 139ea12317 Improve entries navigation readability 2026-04-01 17:12:17 -07:00
Joe Julian fddf864c42 Refine remote open lifecycle flow 2026-04-01 17:10:26 -07:00
Joe Julian 6eee43c1df Clarify group navigation and management 2026-04-01 17:10:04 -07:00
Joe Julian 18d57a2f8e Refine recycle bin and sync UX 2026-04-01 17:09:51 -07:00
Joe Julian b4bcceab31 Clarify lifecycle loading and target selection 2026-04-01 17:00:24 -07:00
Joe Julian 85f07ded39 Avoid blocking UI during vault open and unlock 2026-04-01 16:54:16 -07:00
Joe Julian 2fded8a403 Improve recent targets and workflow context 2026-04-01 16:43:15 -07:00
Joe Julian 23853c479a Refine group, editor, and admin workflows 2026-04-01 16:34:24 -07:00
Joe Julian 54ec195e61 Clarify start, locked, and recycle flows 2026-04-01 16:29:53 -07:00
Joe Julian 1675811aa3 Tighten navigation and admin UI 2026-04-01 16:24:11 -07:00
Joe Julian 14f4b31cf6 Refine settings and empty-state affordances 2026-04-01 15:00:46 -07:00
Joe Julian 1148eb819f Simplify lifecycle and section UI 2026-04-01 14:58:56 -07:00
Joe Julian 1ec373821d Add UI review follow-ups to TODO 2026-04-01 14:22:53 -07:00
Joe Julian 3e03d4669a Honor alternate autofill targets from entry fields 2026-04-01 10:36:08 -07:00
Joe Julian b9ecd0ae70 Fix entry and group navigation workflows 2026-04-01 10:24:57 -07:00
Joe Julian 69aa72ee3b Support Android app targets in autofill service 2026-04-01 05:56:52 -07:00
Joe Julian fbd364f01d Disambiguate same-host Android fill matches 2026-04-01 05:21:15 -07:00
Joe Julian 8be0aad14d Add Android accessibility-based Chrome form fill 2026-04-01 01:24:28 -07:00
Joe Julian eef209ae43 Add Android autofill service packaging 2026-03-31 23:13:10 -07:00
Joe Julian 322dc84917 Tighten Android lifecycle screen layout 2026-03-31 22:53:47 -07:00
Joe Julian 3d53ff8e95 Add KeePassGO branding assets 2026-03-31 22:34:36 -07:00
Joe Julian 0a6b7fe5c0 Fix Android rendering by pinning Gio v0.8 2026-03-31 22:23:45 -07:00
Joe Julian 82bfd694b6 Align Android build guidance with Gio 2026-03-31 21:37:19 -07:00
Joe Julian d9c2d48ff5 Harden API token policy button state 2026-03-31 20:23:59 -07:00
Joe Julian 8495beb8dc Test Android packaging configuration 2026-03-31 20:15:48 -07:00
Joe Julian 3e471366ce Isolate test state and paint full frame 2026-03-31 20:12:54 -07:00
Joe Julian f9b58367da Fix API token detail panel panic 2026-03-30 14:46:07 -07:00
Joe Julian 20f84ac58e Restore entries tab state and preload recent vault 2026-03-30 14:31:06 -07:00
Joe Julian 2db8a71704 Fix hidden root section restore 2026-03-30 09:24:41 -07:00
Joe Julian b7a4742ee6 Add configurable vault security settings 2026-03-30 07:58:27 -07:00
Joe Julian 9afddd7a93 Host the gRPC API and add token admin views 2026-03-30 07:50:34 -07:00
Joe Julian 84c188129e Fix Android startup defaults and window sizing 2026-03-30 07:30:06 -07:00
Joe Julian 980d30f6c2 Record audit events for API token authorization 2026-03-29 23:19:54 -07:00
Joe Julian e6b33134c9 Add token management helpers for API credentials 2026-03-29 23:17:34 -07:00
Joe Julian a269217c60 Expose API approval prompts to app state and UI 2026-03-29 23:14:39 -07:00
Joe Julian 722d2eefa0 Add API approval broker for gRPC authorization prompts 2026-03-29 23:09:36 -07:00
Joe Julian dba0bf1f2c Authorize gRPC requests with vault API tokens 2026-03-29 22:56:18 -07:00
Joe Julian 47942014d7 Add API token domain model and policy evaluation 2026-03-29 22:46:44 -07:00
Joe Julian ed2118223f Add API token authorization TODO segments 2026-03-29 22:38:09 -07:00
Joe Julian 9e30f654b2 Preserve single-root KDBX group trees 2026-03-29 22:17:40 -07:00
Joe Julian 1ecad1fae9 Set up Android SDK prerequisites for APK builds 2026-03-29 21:59:52 -07:00
Joe Julian fc34508689 Add advanced synchronize dialog and APK tooling 2026-03-29 21:38:46 -07:00
Joe Julian 9dea7a4a34 Preserve remote conflicts in entry history 2026-03-29 21:19:42 -07:00
Joe Julian e5904dd224 Tighten banner and navigation behavior 2026-03-29 21:16:38 -07:00
Joe Julian da769a9728 Keep recent remote passwords masked 2026-03-29 21:07:57 -07:00
Joe Julian f47682f3a1 Scroll lifecycle screen and restore remote groups 2026-03-29 17:46:48 -07:00
Joe Julian 9dcc5f1d6f Collapse empty phone detail pane 2026-03-29 17:41:51 -07:00
Joe Julian ee9b00bd85 Rebalance desktop open-vault layout 2026-03-29 17:36:41 -07:00
Joe Julian 8ce924e08f Reset password peek across view changes 2026-03-29 17:27:19 -07:00
Joe Julian bda28eef4b Remember recent remote vault connections 2026-03-29 17:05:42 -07:00
Joe Julian bf56e38bc5 Tighten desktop vault navigation header 2026-03-29 16:58:21 -07:00
Joe Julian d137018d3a Honor env state dir in default UI paths 2026-03-29 16:54:20 -07:00
Joe Julian fcc0291dd9 Simplify locked vault screen 2026-03-29 16:47:29 -07:00
Joe Julian cc127a013e Add collapsible group tools section 2026-03-29 16:39:27 -07:00
Joe Julian b2f1d9a66d Use structured custom fields in entry editor 2026-03-29 16:37:20 -07:00
Joe Julian b391cea295 Restore per-vault groups and scroll edit forms 2026-03-29 16:32:54 -07:00
Joe Julian fba135ff09 Require isolated state dir for test runs 2026-03-29 16:26:21 -07:00
Joe Julian 71b383114d Improve group deletion and status banner UX 2026-03-29 16:17:22 -07:00
Joe Julian 75dc6453f7 Support KeePassGO state dir via flag and env 2026-03-29 15:41:44 -07:00
Joe Julian 72b413a11a Clear KeePassGO master password after use 2026-03-29 15:38:30 -07:00
Joe Julian 75002a4c47 Fix KeePassGO open screen field and banner UX 2026-03-29 15:35:32 -07:00
Joe Julian e98256709d Default Enter to opening the selected vault 2026-03-29 15:33:41 -07:00
Joe Julian 5e15ad3265 Persist recent KeePassGO vault list 2026-03-29 15:28:20 -07:00
Joe Julian d492743eb1 Improve KeePassGO vault navigation UX 2026-03-29 15:18:14 -07:00
Joe Julian 4a629c16bd Simplify KeePassGO desktop vault workflow 2026-03-29 14:50:33 -07:00
Joe Julian 62fc343ecf Refine lifecycle setup screen 2026-03-29 14:00:20 -07:00
Joe Julian 1e51eff76e Document remaining AGENTS gaps 2026-03-29 13:53:27 -07:00
Joe Julian 4509634a15 Update lifecycle tests for controller state 2026-03-29 13:46:30 -07:00
Joe Julian 47bbbdb05e Restore search path context helper 2026-03-29 13:46:30 -07:00
Joe Julian 4fbaee3970 Align keyboard and tests with controller state 2026-03-29 13:46:30 -07:00
Joe Julian d2b49ca0ad Remove duplicate app state group deletion 2026-03-29 13:46:30 -07:00
Joe Julian fedeab2fc1 Merge branch 'seg01-appstate' into merge-main-01-seg01-appstate 2026-03-29 13:46:30 -07:00
Joe Julian b56401b5c6 Centralize app state ownership in controller 2026-03-29 13:46:30 -07:00
Joe Julian 01559a3a2b Fix rebased main path tests 2026-03-29 13:44:05 -07:00
Joe Julian 16dc1de3c8 Add password generation UI profile workflow 2026-03-29 13:44:05 -07:00
Joe Julian bd9674a1f5 Add local vault lifecycle UI coverage 2026-03-29 13:43:13 -07:00
Joe Julian e2e38a97a0 Drop obsolete UI path fallback after rebase 2026-03-29 13:41:44 -07:00
Joe Julian 34a6e0150e Sync UI path fallbacks during main landing 2026-03-29 13:40:57 -07:00
Joe Julian ec063ac81f Align moved-entry test with controller navigation 2026-03-29 13:40:36 -07:00
Joe Julian ac5b6894cf Add attachment replace workflow UI 2026-03-29 13:40:36 -07:00
Joe Julian d6bc213474 test: align rebased path coverage with controller state 2026-03-29 13:37:57 -07:00
Joe Julian 706a72e135 test: align keyboard navigation with controller paths 2026-03-29 13:37:00 -07:00
Joe Julian f8111f626f test: align template UI coverage with controller navigation 2026-03-29 13:37:00 -07:00
Joe Julian 0bfa30de91 test: cover template CRUD UI flows 2026-03-29 13:37:00 -07:00
Joe Julian 24dcf9c8ad Make group navigation controller-driven 2026-03-29 13:37:00 -07:00
Joe Julian ea5c8b3c31 Merge commit '7651b3b' into merge-main-13-seg13-copy-reveal 2026-03-29 13:36:26 -07:00
Joe Julian c355eebed5 Merge commit 'cffe05a' into merge-main-13-seg13-copy-reveal
# Conflicts:
#	main.go
2026-03-29 13:35:59 -07:00
Joe Julian 7651b3b237 Resolve remote lifecycle landing conflicts 2026-03-29 13:35:18 -07:00
Joe Julian da942b41d7 Add WebDAV lifecycle UI feedback 2026-03-29 13:35:18 -07:00
Joe Julian 1c0cd20bcc Merge commit '4fe912b' into merge-main-13-seg13-copy-reveal 2026-03-29 13:35:12 -07:00
Joe Julian cffe05af82 Add entry history browsing to UI 2026-03-29 13:35:12 -07:00
Joe Julian 8deccced9e Add keyboard-first accessibility behavior 2026-03-29 13:35:12 -07:00
Joe Julian 4fe912b41f Merge branch 'main' into merge-main-20-seg20-regression 2026-03-29 13:34:26 -07:00
Joe Julian 55ca6352b4 Add secret-safe copy and reveal UX coverage 2026-03-29 13:34:25 -07:00
Joe Julian 0cfccb58d6 Remove dead deleted-object helper 2026-03-29 13:34:12 -07:00
Joe Julian b043ecdc83 Add regression coverage for KDBX reopen cycles 2026-03-29 13:34:12 -07:00
Joe Julian d3be07f252 Add Segment 6 entry editor coverage 2026-03-29 13:34:12 -07:00
Joe Julian 0f30c5b1c5 Add gRPC vault lifecycle backend flow 2026-03-29 13:34:12 -07:00
Joe Julian 985150c36f Add gRPC group deletion and custom entry fields 2026-03-29 13:33:52 -07:00
Joe Julian f39bdcd3be Complete search section behavior 2026-03-29 13:33:52 -07:00
Joe Julian 48ffa78fa2 Add keyboard-first accessibility behavior 2026-03-29 13:33:52 -07:00
Joe Julian 1349fe6e38 Remove dead deleted-object helper 2026-03-29 13:33:30 -07:00
Joe Julian 19ccab1d8d Add regression coverage for KDBX reopen cycles 2026-03-29 13:33:30 -07:00
Joe Julian ae1921506d Add Segment 6 entry editor coverage 2026-03-29 13:33:30 -07:00
Joe Julian 1f51904567 Add gRPC vault lifecycle backend flow 2026-03-29 13:33:30 -07:00
Joe Julian ef867adfa1 Merge branch 'seg09-search' into merge-main-09-seg09-search 2026-03-29 13:33:07 -07:00
Joe Julian 5eb2068d3e Add keyboard-first accessibility behavior 2026-03-29 13:33:07 -07:00
Joe Julian 77407e0510 Complete search section behavior 2026-03-29 13:33:07 -07:00
Joe Julian dd0b0b6f6e Complete UI state and error surfaces 2026-03-29 13:32:28 -07:00
Joe Julian 566256ab79 Add UI master key setup and change flows 2026-03-29 11:23:54 -07:00
Joe Julian a4b3d5c1e1 Break TODO into parallel work segments 2026-03-29 11:10:12 -07:00
Joe Julian ce01c47256 Reconstruct KeePassGO repository 2026-03-29 11:04:38 -07:00
34 changed files with 930 additions and 2626 deletions
-116
View File
@@ -1,116 +0,0 @@
---
name: keepassgo-apk-test
description: Build and run the KeePassGO Android APK for emulator testing. Use when work requires `make apk`, APK install/launch, emulator validation, or checking known KeePassGO Android regressions such as black screens, clipboard, open flow, or local sync behavior.
---
# KeePassGO APK Test
Use this skill together with the installed `android-emulator-debug` skill. That skill covers generic emulator and `adb` mechanics. This skill adds the KeePassGO-specific build, install, and validation requirements that have already been established in this repo.
## Use This Skill When
- Building `build/keepassgo.apk`.
- Installing or launching KeePassGO in the Android emulator.
- Verifying Android regressions such as black screen, clipboard, open flow, file picker, or Advanced Sync behavior.
- Checking whether a Gio or Android toolchain change broke runtime behavior.
## Known Working Environment
- Preferred emulator AVD: `KeepassGoAPI35`
- Package: `org.julianfamily.keepassgo`
- Activity: `org.gioui.GioActivity`
- Local APK build defaults:
`ANDROID_SDK_ROOT=/opt/android-sdk`
`ANDROID_NDK_ROOT=/opt/android-ndk`
`JAVA_HOME=/usr/lib/jvm/java-25-openjdk`
- CI APK build uses:
`ANDROID_SDK_ROOT=/opt/android-sdk`
`ANDROID_NDK_ROOT=/opt/android-sdk/ndk`
`JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64`
## Known Regression
- `gioui.org v0.9.0` caused a black screen in the `KeepassGoAPI35` emulator.
- `gioui.org v0.8.0` rendered correctly in the same environment.
- If Android rendering regresses after a Gio change, treat Gio as a primary suspect and verify against this known-bad versus known-good history before guessing about app logic.
## Safety Rules
- Do not clobber the users real KeePassGO state while testing.
- For host-side validation, use isolated state such as:
`KEEPASSGO_STATE_DIR="$(mktemp -d)" go test ./...`
- Prefer sanitized demo or test vaults when seeding emulator tests.
- If a real vault is absolutely required to reproduce a problem, do not modify it and do not overwrite the users existing recent-vault state.
## Build Workflow
1. Verify the JDK/SDK paths match the known working environment.
2. Build with `make apk`.
3. If `make apk` fails, inspect the effective `JAVA_HOME`, `ANDROID_SDK_ROOT`, and `ANDROID_NDK_ROOT` before changing code.
4. If the problem is Android-only, avoid desktop-only conclusions from `go test ./...`.
Typical local build:
```sh
JAVA_HOME=/usr/lib/jvm/java-25-openjdk make apk
```
## Emulator Workflow
1. Reuse an existing emulator session if one is already running.
2. Prefer the `KeepassGoAPI35` AVD.
3. Install with `adb install -r build/keepassgo.apk`.
4. Launch with:
```sh
adb shell monkey -p org.julianfamily.keepassgo -c android.intent.category.LAUNCHER 1
```
5. Confirm focus before drawing conclusions:
```sh
adb shell dumpsys window | rg 'mCurrentFocus|mFocusedApp'
```
6. Capture evidence before editing code:
screenshot
focused window
relevant `logcat`
## Validation Checklist
- APK builds successfully with `make apk`.
- App launches to `org.julianfamily.keepassgo/org.gioui.GioActivity`.
- Screenshot shows the expected screen, not just a black frame.
- `logcat` shows no app crash or Android runtime fatal error.
- If the change is about user interaction, perform the tap-through in the emulator instead of stopping at install success.
## Issue-Specific Checks
### Black Screen
- Confirm the app is focused.
- Capture a screenshot and `logcat` before changing anything.
- Compare the current Gio version against the known `v0.9.0` regression history.
- If needed, test whether a minimal Gio example renders in the same emulator before blaming KeePassGO layout code.
### Clipboard
- Verify behavior in the emulator, not just unit tests.
- Android clipboard writes must use the Gio-native command path, not desktop clipboard backends.
### File Picker Or Local Sync
- Verify the picker flow on Android itself.
- Prefer document-stream or content-URI based behavior over raw filesystem paths when Android permissions are involved.
## Report Back
When closing out work, state:
- the exact build command used
- whether the emulator session was reused or newly started
- whether install succeeded
- whether the app actually rendered
- what was verified manually in the emulator
- what remains unverified
-179
View File
@@ -1,179 +0,0 @@
name: ci
on:
push:
branches:
- main
tags:
- "v*"
- "release-*"
- "[0-9]+.[0-9]+.[0-9]+*"
permissions:
contents: write
env:
GO_VERSION: "1.26.1"
ANDROID_SDK_ROOT: /opt/android-sdk
ANDROID_NDK_ROOT: /opt/android-sdk/ndk
JAVA_HOME: /usr/lib/jvm/java-21-openjdk-amd64
DIST_DIR: dist
jobs:
lint-test:
runs-on: keepassgo-android
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Install native build dependencies
shell: bash
run: |
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends \
zsh \
pkg-config \
libx11-dev \
libx11-xcb-dev \
libxkbcommon-dev \
libxkbcommon-x11-dev \
libwayland-dev \
libvulkan-dev \
libegl1-mesa-dev \
libxcursor-dev \
libxfixes-dev
- name: Lint
shell: bash
run: |
set -euo pipefail
state_dir="$(mktemp -d)"
trap 'rm -rf -- "$state_dir"' EXIT
KEEPASSGO_STATE_DIR="$state_dir" go tool golangci-lint run --build-tags nox11,nowayland,novulkan ./...
- name: Test
shell: bash
run: |
set -euo pipefail
state_dir="$(mktemp -d)"
trap 'rm -rf -- "$state_dir"' EXIT
KEEPASSGO_STATE_DIR="$state_dir" go test -tags nox11,nowayland,novulkan ./...
build:
needs: lint-test
runs-on: keepassgo-android
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Install native build dependencies
shell: bash
run: |
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends \
zsh \
pkg-config \
libx11-dev \
libx11-xcb-dev \
libxkbcommon-dev \
libxkbcommon-x11-dev \
libwayland-dev \
libvulkan-dev \
libegl1-mesa-dev \
libxcursor-dev \
libxfixes-dev
- name: Prepare dist directory
shell: bash
run: |
set -euo pipefail
mkdir -p "${DIST_DIR}"
- name: Build desktop binaries
shell: bash
run: |
set -euo pipefail
app_version="$(git describe --tags --always --dirty)"
# Gio needs a Linux ARM64 cgo cross-toolchain for desktop builds.
# Keep the CI matrix to targets this runner can build reproducibly.
for target in \
"linux amd64" \
"windows amd64" \
"windows arm64"
do
set -- ${target}
goos="$1"
goarch="$2"
ext=""
cgo_enabled=0
if [[ "${goos}" == "linux" ]]; then
cgo_enabled=1
fi
if [[ "${goos}" == "windows" ]]; then
ext=".exe"
fi
out="${DIST_DIR}/keepassgo-${goos}-${goarch}${ext}"
GOOS="${goos}" GOARCH="${goarch}" CGO_ENABLED="${cgo_enabled}" \
go build -ldflags "-X main.appVersion=${app_version}" -o "${out}" .
done
- name: Build APK
shell: bash
run: |
set -euo pipefail
signkey_path="$(mktemp)"
trap 'rm -f -- "$signkey_path"' EXIT
printf '%s' '${{ secrets.APK_SIGNKEY_B64 }}' | base64 -d > "$signkey_path"
export APP_VERSION="$(git describe --tags --always --dirty)"
make apk SIGNKEY="$signkey_path" SIGNPASS='${{ secrets.APK_SIGNPASS }}'
cp build/keepassgo.apk "${DIST_DIR}/keepassgo.apk"
- name: Upload CI artifacts
uses: christopherhx/gitea-upload-artifact@v4
env:
NODE_OPTIONS: --use-system-ca
SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt
with:
name: keepassgo-${{ gitea.sha }}
path: |
dist/keepassgo-linux-amd64
dist/keepassgo-windows-amd64.exe
dist/keepassgo-windows-arm64.exe
dist/keepassgo.apk
retention-days: 30
- name: Publish release artifacts
if: startsWith(gitea.ref, 'refs/tags/')
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_SERVER_URL: ${{ gitea.server_url }}
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_REF_NAME: ${{ gitea.ref_name }}
SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt
REQUESTS_CA_BUNDLE: /etc/ssl/certs/ca-certificates.crt
shell: bash
run: |
set -euo pipefail
python3 scripts/gitea_release.py \
--server "${GITEA_SERVER_URL}" \
--repo "${GITEA_REPOSITORY}" \
--token "${GITEA_TOKEN}" \
--tag "${GITEA_REF_NAME}" \
"${DIST_DIR}/keepassgo-linux-amd64" \
"${DIST_DIR}/keepassgo-windows-amd64.exe" \
"${DIST_DIR}/keepassgo-windows-arm64.exe" \
"${DIST_DIR}/keepassgo.apk"
-16
View File
@@ -14,8 +14,6 @@ These instructions apply to all future work in this repository.
## Skills ## Skills
- Use the installed Go skills whenever they materially apply to the current slice of work. - Use the installed Go skills whenever they materially apply to the current slice of work.
- Use the installed `android-emulator-debug` skill for Android emulator lifecycle, `adb`, screenshots, and log capture work.
- Use the repo-local `keepassgo-apk-test` skill for KeePassGO-specific APK build and emulator test runs.
- The available Go skills for this repository are: - The available Go skills for this repository are:
`go-code-review`, `go-code-review`,
`go-concurrency`, `go-concurrency`,
@@ -128,20 +126,6 @@ These features are product requirements, not “nice to have” ideas.
- Keep `golangci-lint` passing. - Keep `golangci-lint` passing.
- Keep `go test ./...` passing. - Keep `go test ./...` passing.
- Track `gogio` as a Go tool and keep a reproducible `make apk` path for Android packaging. - Track `gogio` as a Go tool and keep a reproducible `make apk` path for Android packaging.
- Keep the Android build requirements aligned with the known working setup:
`ANDROID_SDK_ROOT=/opt/android-sdk`,
local `ANDROID_NDK_ROOT=/opt/android-ndk`,
CI `ANDROID_NDK_ROOT=/opt/android-sdk/ndk`,
local `JAVA_HOME=/usr/lib/jvm/java-25-openjdk`,
CI `JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64`.
- Remember the known Android runtime regression:
`gioui.org v0.9.0` produced a black screen on the `KeepassGoAPI35` emulator, while `gioui.org v0.8.0` rendered correctly. Treat Gio upgrades on Android as regression-sensitive and verify them on-device or in the emulator.
- When validating an APK in the emulator, prefer the known KeePassGO setup:
AVD `KeepassGoAPI35`,
package `org.julianfamily.keepassgo`,
activity `org.gioui.GioActivity`.
- Do not run emulator/manual APK tests against the users real persisted app state.
Use an isolated `KEEPASSGO_STATE_DIR` for host-side validation, and when emulator testing requires seeded vault data, use sanitized test/demo vaults rather than the users real vault files whenever possible.
- When running tests or other automated validation that may touch persisted UI state, set `KEEPASSGO_STATE_DIR` to an isolated temporary directory so recent-vault history and other local state do not pollute the users real config. - When running tests or other automated validation that may touch persisted UI state, set `KEEPASSGO_STATE_DIR` to an isolated temporary directory so recent-vault history and other local state do not pollute the users real config.
- Prefer commands shaped like `KEEPASSGO_STATE_DIR=\"$(mktemp -d)\" go test ./...` for ad hoc local validation unless a test already manages its own isolated state directory. - Prefer commands shaped like `KEEPASSGO_STATE_DIR=\"$(mktemp -d)\" go test ./...` for ad hoc local validation unless a test already manages its own isolated state directory.
- Do not assume the agent can decrypt SOPS-encrypted secrets in this repository. - Do not assume the agent can decrypt SOPS-encrypted secrets in this repository.
-1
View File
@@ -12,7 +12,6 @@ Environment:
- `ANDROID_NDK_ROOT` defaults to `/opt/android-ndk`. - `ANDROID_NDK_ROOT` defaults to `/opt/android-ndk`.
- `JAVA_HOME` defaults to `/usr/lib/jvm/java-25-openjdk`. - `JAVA_HOME` defaults to `/usr/lib/jvm/java-25-openjdk`.
- `APP_ID` overrides the Android application id. - `APP_ID` overrides the Android application id.
- `APP_VERSION` overrides the version shown inside KeePassGO itself.
- `APK_OUT` overrides the output path. - `APK_OUT` overrides the output path.
- `APK_VERSION` overrides the packaged app version. - `APK_VERSION` overrides the packaged app version.
- `ANDROID_MIN_SDK` overrides the minimum supported Android SDK. - `ANDROID_MIN_SDK` overrides the minimum supported Android SDK.
-14
View File
@@ -5,20 +5,8 @@ PATH := $(JAVA_HOME)/bin:$(ANDROID_SDK_ROOT)/cmdline-tools/latest/bin:$(ANDROID_
APP_ID ?= org.julianfamily.keepassgo APP_ID ?= org.julianfamily.keepassgo
APK_OUT ?= build/keepassgo.apk APK_OUT ?= build/keepassgo.apk
APK_VERSION ?= 0.1.0.1 APK_VERSION ?= 0.1.0.1
APP_VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
GO_LDFLAGS ?= -X main.appVersion=$(APP_VERSION)
ANDROID_MIN_SDK ?= 28 ANDROID_MIN_SDK ?= 28
ANDROID_TARGET_SDK ?= 35 ANDROID_TARGET_SDK ?= 35
SIGNKEY ?=
SIGNPASS ?=
GOGIO_SIGN_FLAGS :=
ifneq ($(strip $(SIGNKEY)),)
GOGIO_SIGN_FLAGS += -signkey $(SIGNKEY)
endif
ifneq ($(strip $(SIGNPASS)),)
GOGIO_SIGN_FLAGS += -signpass $(SIGNPASS)
endif
.PHONY: apk .PHONY: apk
apk: android/keepassgo-android.jar apk: android/keepassgo-android.jar
@@ -36,8 +24,6 @@ apk: android/keepassgo-android.jar
go tool gogio -target android \ go tool gogio -target android \
-buildmode exe \ -buildmode exe \
-appid $(APP_ID) \ -appid $(APP_ID) \
-ldflags "$(GO_LDFLAGS)" \
$(GOGIO_SIGN_FLAGS) \
-o $(APK_OUT) \ -o $(APK_OUT) \
-version $(APK_VERSION) \ -version $(APK_VERSION) \
-minsdk $(ANDROID_MIN_SDK) \ -minsdk $(ANDROID_MIN_SDK) \
-25
View File
@@ -41,31 +41,6 @@ Desktop build:
go build ./... go build ./...
``` ```
By default, build outputs stamp the app version from `git describe --tags --always --dirty`.
You can override the version shown in KeePassGO with:
```bash
go build -ldflags "-X main.appVersion=v0.0.1" ./...
```
## Arch Linux Package
An AUR-style package definition for the Linux desktop client lives under:
- `packaging/archlinux/keepassgo-git/`
From that directory you can build and install it with:
```bash
makepkg -si
```
The package installs:
- `/usr/bin/keepassgo`
- a desktop entry at `/usr/share/applications/keepassgo.desktop`
- application icons under the hicolor theme
## Android Packaging ## Android Packaging
KeePassGO uses Gio, so Android packaging is done with `gogio`. KeePassGO uses Gio, so Android packaging is done with `gogio`.
+1 -1
View File
@@ -20,7 +20,7 @@ func TestStartHostServesVaultLifecycleAndSyncsSessionState(t *testing.T) {
if err := lifecycle.Create(vault.Model{ if err := lifecycle.Create(vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
testAPITokenEntry(t), testAPITokenEntry(t),
{ID: "entry-1", Title: "Vault Console", Path: []string{"Root", "Internet"}}, {ID: "entry-1", Title: "Git Server", Path: []string{"Root", "Internet"}},
}, },
}, vault.MasterKey{Password: "correct horse battery staple"}); err != nil { }, vault.MasterKey{Password: "correct horse battery staple"}); err != nil {
t.Fatalf("Create() error = %v", err) t.Fatalf("Create() error = %v", err)
+69 -69
View File
@@ -50,11 +50,11 @@ func TestVaultServiceRejectsExpiredAPITokens(t *testing.T) {
client, _, cleanup := newTestClientForModel(t, vault.Model{ client, _, cleanup := newTestClientForModel(t, vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "vault-console", ID: "git-server",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
token.Entry([]string{"Root", "API Tokens"}), token.Entry([]string{"Root", "API Tokens"}),
@@ -78,22 +78,22 @@ func TestVaultServiceRejectsUnauthorizedEntryAccess(t *testing.T) {
client, _, cleanup := newTestClientForModel(t, vault.Model{ client, _, cleanup := newTestClientForModel(t, vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "vault-console", ID: "git-server",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
testAPITokenEntry(t, testAPITokenEntry(t,
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationListEntries, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root", "Internet"}}}, apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationListEntries, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root", "Internet"}}},
apitokens.PolicyRule{Effect: apitokens.EffectDeny, Operation: apitokens.OperationCopyPassword, Resource: apitokens.Resource{Kind: apitokens.ResourceEntry, EntryID: "vault-console", Path: []string{"Root", "Internet"}}}, apitokens.PolicyRule{Effect: apitokens.EffectDeny, Operation: apitokens.OperationCopyPassword, Resource: apitokens.Resource{Kind: apitokens.ResourceEntry, EntryID: "git-server", Path: []string{"Root", "Internet"}}},
), ),
}, },
}) })
defer cleanup() defer cleanup()
_, err := client.CopyEntryField(tokenContext(defaultTestTokenSecret), &keepassgov1.CopyEntryFieldRequest{Id: "vault-console", Target: "password"}) _, err := client.CopyEntryField(tokenContext(defaultTestTokenSecret), &keepassgov1.CopyEntryFieldRequest{Id: "git-server", Target: "password"})
if status.Code(err) != codes.PermissionDenied { if status.Code(err) != codes.PermissionDenied {
t.Fatalf("CopyEntryField() code = %v, want %v", status.Code(err), codes.PermissionDenied) t.Fatalf("CopyEntryField() code = %v, want %v", status.Code(err), codes.PermissionDenied)
} }
@@ -105,11 +105,11 @@ func TestVaultServicePromptsAndResumesWhenApproved(t *testing.T) {
model := vault.Model{ model := vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "vault-console", ID: "git-server",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
testAPITokenEntry(t), testAPITokenEntry(t),
@@ -139,8 +139,8 @@ func TestVaultServicePromptsAndResumesWhenApproved(t *testing.T) {
if err := <-errCh; err != nil { if err := <-errCh; err != nil {
t.Fatalf("ListEntries() error = %v", err) t.Fatalf("ListEntries() error = %v", err)
} }
if len(resp.Entries) != 1 || resp.Entries[0].Id != "vault-console" { if len(resp.Entries) != 1 || resp.Entries[0].Id != "git-server" {
t.Fatalf("ListEntries().Entries = %#v, want vault-console", resp.Entries) t.Fatalf("ListEntries().Entries = %#v, want git-server", resp.Entries)
} }
} }
@@ -150,11 +150,11 @@ func TestVaultServicePersistsPermanentDenyApproval(t *testing.T) {
model := vault.Model{ model := vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "vault-console", ID: "git-server",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
testAPITokenEntry(t), testAPITokenEntry(t),
@@ -196,7 +196,7 @@ func TestVaultServiceReturnsCanceledForCanceledApproval(t *testing.T) {
model := vault.Model{ model := vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ID: "vault-console", Title: "Vault Console", Path: []string{"Root", "Internet"}}, {ID: "git-server", Title: "Git Server", Path: []string{"Root", "Internet"}},
testAPITokenEntry(t), testAPITokenEntry(t),
}, },
} }
@@ -225,7 +225,7 @@ func TestVaultServiceTimesOutPendingApproval(t *testing.T) {
model := vault.Model{ model := vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ID: "vault-console", Title: "Vault Console", Path: []string{"Root", "Internet"}}, {ID: "git-server", Title: "Git Server", Path: []string{"Root", "Internet"}},
testAPITokenEntry(t), testAPITokenEntry(t),
}, },
} }
@@ -244,7 +244,7 @@ func TestVaultServiceRecordsApprovalAuditEvents(t *testing.T) {
model := vault.Model{ model := vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ID: "vault-console", Title: "Vault Console", Path: []string{"Root", "Internet"}}, {ID: "git-server", Title: "Git Server", Path: []string{"Root", "Internet"}},
testAPITokenEntry(t), testAPITokenEntry(t),
}, },
} }
@@ -282,11 +282,11 @@ func TestVaultServiceReportsSessionStatusAndSupportsLockUnlock(t *testing.T) {
model: vault.Model{ model: vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "vault-console", ID: "git-server",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
}, },
@@ -454,7 +454,7 @@ func TestVaultServiceOpensAndSavesVaultThroughLifecycleBackend(t *testing.T) {
if _, err := client.OpenRemoteVault(ctx, &keepassgov1.OpenRemoteVaultRequest{ if _, err := client.OpenRemoteVault(ctx, &keepassgov1.OpenRemoteVaultRequest{
BaseUrl: "https://dav.example.com", BaseUrl: "https://dav.example.com",
Path: "vaults/main.kdbx", Path: "vaults/main.kdbx",
Username: "rustyryan", Username: "jjulian",
Password: "dav-token", Password: "dav-token",
MasterPassword: "correct horse battery staple", MasterPassword: "correct horse battery staple",
}); err != nil { }); err != nil {
@@ -618,8 +618,8 @@ func TestVaultServiceListsEntriesForAuthorizedClients(t *testing.T) {
t.Fatalf("len(ListEntries().Entries) = %d, want 1", len(resp.Entries)) t.Fatalf("len(ListEntries().Entries) = %d, want 1", len(resp.Entries))
} }
if resp.Entries[0].Title != "Vault Console" { if resp.Entries[0].Title != "Git Server" {
t.Fatalf("ListEntries().Entries[0].Title = %q, want %q", resp.Entries[0].Title, "Vault Console") t.Fatalf("ListEntries().Entries[0].Title = %q, want %q", resp.Entries[0].Title, "Git Server")
} }
if got := resp.Entries[0].Fields["X-Role"]; got != "automation" { if got := resp.Entries[0].Fields["X-Role"]; got != "automation" {
t.Fatalf("ListEntries().Entries[0].Fields[X-Role] = %q, want %q", got, "automation") t.Fatalf("ListEntries().Entries[0].Fields[X-Role] = %q, want %q", got, "automation")
@@ -747,7 +747,7 @@ func TestVaultServiceCopiesEntryFieldsForAuthorizedClients(t *testing.T) {
ctx := tokenContext(defaultTestTokenSecret) ctx := tokenContext(defaultTestTokenSecret)
if _, err := client.CopyEntryField(ctx, &keepassgov1.CopyEntryFieldRequest{ if _, err := client.CopyEntryField(ctx, &keepassgov1.CopyEntryFieldRequest{
Id: "vault-console", Id: "git-server",
Target: "password", Target: "password",
}); err != nil { }); err != nil {
t.Fatalf("CopyEntryField() error = %v", err) t.Fatalf("CopyEntryField() error = %v", err)
@@ -767,11 +767,11 @@ func TestVaultServiceUpsertsEntriesForAuthorizedClients(t *testing.T) {
ctx := tokenContext(defaultTestTokenSecret) ctx := tokenContext(defaultTestTokenSecret)
upserted, err := client.UpsertEntry(ctx, &keepassgov1.UpsertEntryRequest{ upserted, err := client.UpsertEntry(ctx, &keepassgov1.UpsertEntryRequest{
Entry: &keepassgov1.Entry{ Entry: &keepassgov1.Entry{
Id: "surveillance-console", Id: "ha-codex",
Title: "Surveillance Console", Title: "Home Assistant (Codex)",
Username: "codex", Username: "codex",
Password: "token-2", Password: "token-2",
Url: "https://surveillance.crew.example.invalid", Url: "https://lights.julianfamily.org",
Fields: map[string]string{ Fields: map[string]string{
"X-Role": "lights-admin", "X-Role": "lights-admin",
}, },
@@ -782,8 +782,8 @@ func TestVaultServiceUpsertsEntriesForAuthorizedClients(t *testing.T) {
t.Fatalf("UpsertEntry() error = %v", err) t.Fatalf("UpsertEntry() error = %v", err)
} }
if upserted.Entry.Title != "Surveillance Console" { if upserted.Entry.Title != "Home Assistant (Codex)" {
t.Fatalf("UpsertEntry().Entry.Title = %q, want %q", upserted.Entry.Title, "Surveillance Console") t.Fatalf("UpsertEntry().Entry.Title = %q, want %q", upserted.Entry.Title, "Home Assistant (Codex)")
} }
if got := upserted.Entry.Fields["X-Role"]; got != "lights-admin" { if got := upserted.Entry.Fields["X-Role"]; got != "lights-admin" {
t.Fatalf("UpsertEntry().Entry.Fields[X-Role] = %q, want %q", got, "lights-admin") t.Fatalf("UpsertEntry().Entry.Fields[X-Role] = %q, want %q", got, "lights-admin")
@@ -809,7 +809,7 @@ func TestVaultServiceDeletesAndRestoresEntriesForAuthorizedClients(t *testing.T)
defer cleanup() defer cleanup()
ctx := tokenContext(defaultTestTokenSecret) ctx := tokenContext(defaultTestTokenSecret)
if _, err := client.DeleteEntry(ctx, &keepassgov1.DeleteEntryRequest{Id: "vault-console"}); err != nil { if _, err := client.DeleteEntry(ctx, &keepassgov1.DeleteEntryRequest{Id: "git-server"}); err != nil {
t.Fatalf("DeleteEntry() error = %v", err) t.Fatalf("DeleteEntry() error = %v", err)
} }
@@ -822,13 +822,13 @@ func TestVaultServiceDeletesAndRestoresEntriesForAuthorizedClients(t *testing.T)
t.Fatalf("len(ListEntries().Entries) = %d, want 0 after delete", len(listed.Entries)) t.Fatalf("len(ListEntries().Entries) = %d, want 0 after delete", len(listed.Entries))
} }
restored, err := client.RestoreEntry(ctx, &keepassgov1.RestoreEntryRequest{Id: "vault-console"}) restored, err := client.RestoreEntry(ctx, &keepassgov1.RestoreEntryRequest{Id: "git-server"})
if err != nil { if err != nil {
t.Fatalf("RestoreEntry() error = %v", err) t.Fatalf("RestoreEntry() error = %v", err)
} }
if restored.Entry.Title != "Vault Console" { if restored.Entry.Title != "Git Server" {
t.Fatalf("RestoreEntry().Entry.Title = %q, want %q", restored.Entry.Title, "Vault Console") t.Fatalf("RestoreEntry().Entry.Title = %q, want %q", restored.Entry.Title, "Git Server")
} }
listed, err = client.ListEntries(ctx, &keepassgov1.ListEntriesRequest{Path: []string{"Root", "Internet"}}) listed, err = client.ListEntries(ctx, &keepassgov1.ListEntriesRequest{Path: []string{"Root", "Internet"}})
@@ -836,8 +836,8 @@ func TestVaultServiceDeletesAndRestoresEntriesForAuthorizedClients(t *testing.T)
t.Fatalf("ListEntries() error = %v", err) t.Fatalf("ListEntries() error = %v", err)
} }
if len(listed.Entries) != 1 || listed.Entries[0].Title != "Vault Console" { if len(listed.Entries) != 1 || listed.Entries[0].Title != "Git Server" {
t.Fatalf("ListEntries().Entries = %#v, want restored Vault Console entry", listed.Entries) t.Fatalf("ListEntries().Entries = %#v, want restored Git Server entry", listed.Entries)
} }
} }
@@ -863,11 +863,11 @@ func TestVaultServiceListsAndInstantiatesTemplatesForAuthorizedClients(t *testin
instantiated, err := client.InstantiateTemplate(ctx, &keepassgov1.InstantiateTemplateRequest{ instantiated, err := client.InstantiateTemplate(ctx, &keepassgov1.InstantiateTemplateRequest{
TemplateId: "website-login", TemplateId: "website-login",
Overrides: &keepassgov1.Entry{ Overrides: &keepassgov1.Entry{
Id: "bellagio", Id: "dynadot",
Title: "Bellagio", Title: "Dynadot",
Username: "rustyryan", Username: "jjulian",
Password: "hunter2", Password: "hunter2",
Url: "https://bellagio.example.invalid", Url: "https://www.dynadot.com",
Fields: map[string]string{ Fields: map[string]string{
"Environment": "staging", "Environment": "staging",
}, },
@@ -879,8 +879,8 @@ func TestVaultServiceListsAndInstantiatesTemplatesForAuthorizedClients(t *testin
t.Fatalf("InstantiateTemplate() error = %v", err) t.Fatalf("InstantiateTemplate() error = %v", err)
} }
if instantiated.Entry.Title != "Bellagio" || instantiated.Entry.Notes != "Reusable template for website accounts." { if instantiated.Entry.Title != "Dynadot" || instantiated.Entry.Notes != "Reusable template for website accounts." {
t.Fatalf("InstantiateTemplate().Entry = %#v, want Bellagio entry with template notes", instantiated.Entry) t.Fatalf("InstantiateTemplate().Entry = %#v, want Dynadot entry with template notes", instantiated.Entry)
} }
if got := instantiated.Entry.Fields["Environment"]; got != "staging" { if got := instantiated.Entry.Fields["Environment"]; got != "staging" {
t.Fatalf("InstantiateTemplate().Entry.Fields[Environment] = %q, want %q", got, "staging") t.Fatalf("InstantiateTemplate().Entry.Fields[Environment] = %q, want %q", got, "staging")
@@ -956,7 +956,7 @@ func TestVaultServiceListsAndRestoresEntryHistoryForAuthorizedClients(t *testing
defer cleanup() defer cleanup()
ctx := tokenContext(defaultTestTokenSecret) ctx := tokenContext(defaultTestTokenSecret)
history, err := client.ListEntryHistory(ctx, &keepassgov1.ListEntryHistoryRequest{Id: "vault-console"}) history, err := client.ListEntryHistory(ctx, &keepassgov1.ListEntryHistoryRequest{Id: "git-server"})
if err != nil { if err != nil {
t.Fatalf("ListEntryHistory() error = %v", err) t.Fatalf("ListEntryHistory() error = %v", err)
} }
@@ -965,7 +965,7 @@ func TestVaultServiceListsAndRestoresEntryHistoryForAuthorizedClients(t *testing
} }
restored, err := client.RestoreEntryHistory(ctx, &keepassgov1.RestoreEntryHistoryRequest{ restored, err := client.RestoreEntryHistory(ctx, &keepassgov1.RestoreEntryHistoryRequest{
Id: "vault-console", Id: "git-server",
HistoryIndex: 0, HistoryIndex: 0,
}) })
if err != nil { if err != nil {
@@ -986,14 +986,14 @@ func TestVaultServiceListsUploadsDownloadsAndDeletesAttachments(t *testing.T) {
uploaded := []byte("attachment-content") uploaded := []byte("attachment-content")
if _, err := client.UploadAttachment(ctx, &keepassgov1.UploadAttachmentRequest{ if _, err := client.UploadAttachment(ctx, &keepassgov1.UploadAttachmentRequest{
EntryId: "vault-console", EntryId: "git-server",
Name: "token.txt", Name: "token.txt",
Content: uploaded, Content: uploaded,
}); err != nil { }); err != nil {
t.Fatalf("UploadAttachment() error = %v", err) t.Fatalf("UploadAttachment() error = %v", err)
} }
listed, err := client.ListAttachments(ctx, &keepassgov1.ListAttachmentsRequest{EntryId: "vault-console"}) listed, err := client.ListAttachments(ctx, &keepassgov1.ListAttachmentsRequest{EntryId: "git-server"})
if err != nil { if err != nil {
t.Fatalf("ListAttachments() error = %v", err) t.Fatalf("ListAttachments() error = %v", err)
} }
@@ -1002,7 +1002,7 @@ func TestVaultServiceListsUploadsDownloadsAndDeletesAttachments(t *testing.T) {
} }
downloaded, err := client.DownloadAttachment(ctx, &keepassgov1.DownloadAttachmentRequest{ downloaded, err := client.DownloadAttachment(ctx, &keepassgov1.DownloadAttachmentRequest{
EntryId: "vault-console", EntryId: "git-server",
Name: "token.txt", Name: "token.txt",
}) })
if err != nil { if err != nil {
@@ -1013,13 +1013,13 @@ func TestVaultServiceListsUploadsDownloadsAndDeletesAttachments(t *testing.T) {
} }
if _, err := client.DeleteAttachment(ctx, &keepassgov1.DeleteAttachmentRequest{ if _, err := client.DeleteAttachment(ctx, &keepassgov1.DeleteAttachmentRequest{
EntryId: "vault-console", EntryId: "git-server",
Name: "token.txt", Name: "token.txt",
}); err != nil { }); err != nil {
t.Fatalf("DeleteAttachment() error = %v", err) t.Fatalf("DeleteAttachment() error = %v", err)
} }
listed, err = client.ListAttachments(ctx, &keepassgov1.ListAttachmentsRequest{EntryId: "vault-console"}) listed, err = client.ListAttachments(ctx, &keepassgov1.ListAttachmentsRequest{EntryId: "git-server"})
if err != nil { if err != nil {
t.Fatalf("ListAttachments() error = %v", err) t.Fatalf("ListAttachments() error = %v", err)
} }
@@ -1055,32 +1055,32 @@ func newTestClient(t *testing.T) (keepassgov1.VaultServiceClient, *memoryClipboa
model := vault.Model{ model := vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "vault-console", ID: "git-server",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Fields: map[string]string{ Fields: map[string]string{
"X-Role": "automation", "X-Role": "automation",
}, },
History: []vault.Entry{ History: []vault.Entry{
{ {
ID: "vault-console-h1", ID: "git-server-h1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-0", Password: "token-0",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
}, },
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
{ {
ID: "surveillance-console", ID: "ha-codex",
Title: "Surveillance Console", Title: "Home Assistant (Codex)",
Username: "codex", Username: "codex",
Password: "token-2", Password: "token-2",
URL: "https://surveillance.crew.example.invalid", URL: "https://lights.julianfamily.org",
Path: []string{"Root", "Home Assistant"}, Path: []string{"Root", "Home Assistant"},
}, },
testAPITokenEntry(t, testAPITokenEntry(t,
@@ -1090,9 +1090,9 @@ func newTestClient(t *testing.T) (keepassgov1.VaultServiceClient, *memoryClipboa
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationMutateGroup, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}}, apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationMutateGroup, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}},
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationMutateEntry, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}}, apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationMutateEntry, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}},
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationReadEntry, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}}, apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationReadEntry, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}},
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationCopyPassword, Resource: apitokens.Resource{Kind: apitokens.ResourceEntry, EntryID: "vault-console", Path: []string{"Root", "Internet"}}}, apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationCopyPassword, Resource: apitokens.Resource{Kind: apitokens.ResourceEntry, EntryID: "git-server", Path: []string{"Root", "Internet"}}},
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationCopyUsername, Resource: apitokens.Resource{Kind: apitokens.ResourceEntry, EntryID: "vault-console", Path: []string{"Root", "Internet"}}}, apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationCopyUsername, Resource: apitokens.Resource{Kind: apitokens.ResourceEntry, EntryID: "git-server", Path: []string{"Root", "Internet"}}},
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationCopyURL, Resource: apitokens.Resource{Kind: apitokens.ResourceEntry, EntryID: "vault-console", Path: []string{"Root", "Internet"}}}, apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationCopyURL, Resource: apitokens.Resource{Kind: apitokens.ResourceEntry, EntryID: "git-server", Path: []string{"Root", "Internet"}}},
), ),
}, },
Templates: []vault.Entry{ Templates: []vault.Entry{
+1 -15
View File
@@ -26,7 +26,6 @@ const (
SectionRecycleBin Section = "recycle-bin" SectionRecycleBin Section = "recycle-bin"
SectionAPITokens Section = "api-tokens" SectionAPITokens Section = "api-tokens"
SectionAPIAudit Section = "api-audit" SectionAPIAudit Section = "api-audit"
SectionAbout Section = "about"
) )
type CurrentSession interface { type CurrentSession interface {
@@ -62,7 +61,6 @@ type SynchronizableSession interface {
type AdvancedSynchronizableSession interface { type AdvancedSynchronizableSession interface {
CurrentSession CurrentSession
SynchronizeFromLocal(string) error SynchronizeFromLocal(string) error
SynchronizeFromLocalBytes(string, []byte) error
SynchronizeToLocal(string) error SynchronizeToLocal(string) error
SynchronizeFromRemote(webdav.Client, string) error SynchronizeFromRemote(webdav.Client, string) error
SynchronizeToRemote(webdav.Client, string) error SynchronizeToRemote(webdav.Client, string) error
@@ -377,7 +375,7 @@ func (s *State) entriesForSection(model vault.Model) []vault.Entry {
return slices.Clone(model.Templates) return slices.Clone(model.Templates)
case SectionRecycleBin: case SectionRecycleBin:
return slices.Clone(model.RecycleBin) return slices.Clone(model.RecycleBin)
case SectionAPITokens, SectionAPIAudit, SectionAbout: case SectionAPITokens, SectionAPIAudit:
return nil return nil
default: default:
return slices.Clone(model.Entries) return slices.Clone(model.Entries)
@@ -724,18 +722,6 @@ func (s *State) SynchronizeFromLocal(path string) error {
return nil return nil
} }
func (s *State) SynchronizeFromLocalBytes(name string, content []byte) error {
session, ok := s.Session.(AdvancedSynchronizableSession)
if !ok {
return fmt.Errorf("session is not advanced-synchronizable")
}
if err := session.SynchronizeFromLocalBytes(name, content); err != nil {
return err
}
s.Dirty = false
return nil
}
func (s *State) SynchronizeToLocal(path string) error { func (s *State) SynchronizeToLocal(path string) error {
session, ok := s.Session.(AdvancedSynchronizableSession) session, ok := s.Session.(AdvancedSynchronizableSession)
if !ok { if !ok {
+100 -120
View File
@@ -20,13 +20,13 @@ func TestVisibleEntriesFollowsCurrentPathWithoutSearch(t *testing.T) {
Session: stubSession{ Session: stubSession{
model: vault.Model{ model: vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ID: "bellagio", Title: "Bellagio", Path: []string{"Crew", "Internet"}}, {ID: "dynadot", Title: "Dynadot", Path: []string{"Joe", "Internet"}},
{ID: "vault-console", Title: "Vault Console", Path: []string{"Crew", "Internet"}}, {ID: "git-server", Title: "Git Server", Path: []string{"Joe", "Internet"}},
{ID: "surveillance-console", Title: "Surveillance Console", Path: []string{"Crew", "Home Assistant"}}, {ID: "ha-codex", Title: "Home Assistant (Codex)", Path: []string{"Joe", "Home Assistant"}},
}, },
}, },
}, },
CurrentPath: []string{"Crew", "Internet"}, CurrentPath: []string{"Joe", "Internet"},
} }
got, err := state.VisibleEntries() got, err := state.VisibleEntries()
@@ -39,8 +39,8 @@ func TestVisibleEntriesFollowsCurrentPathWithoutSearch(t *testing.T) {
titles = append(titles, entry.Title) titles = append(titles, entry.Title)
} }
if !slices.Equal(titles, []string{"Bellagio", "Vault Console"}) { if !slices.Equal(titles, []string{"Dynadot", "Git Server"}) {
t.Fatalf("visible titles = %v, want [Bellagio Vault Console]", titles) t.Fatalf("visible titles = %v, want [Dynadot Git Server]", titles)
} }
} }
@@ -51,14 +51,14 @@ func TestVisibleEntriesAtParentGroupOnlyShowsDirectEntries(t *testing.T) {
Session: stubSession{ Session: stubSession{
model: vault.Model{ model: vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ID: "joe-note", Title: "Crew Note", Path: []string{"Crew"}}, {ID: "joe-note", Title: "Joe Note", Path: []string{"Joe"}},
{ID: "bellagio", Title: "Bellagio", Path: []string{"Crew", "Internet"}}, {ID: "dynadot", Title: "Dynadot", Path: []string{"Joe", "Internet"}},
{ID: "vault-console", Title: "Vault Console", Path: []string{"Crew", "Internet"}}, {ID: "git-server", Title: "Git Server", Path: []string{"Joe", "Internet"}},
{ID: "surveillance-console", Title: "Surveillance Console", Path: []string{"Crew", "Home Assistant"}}, {ID: "ha-codex", Title: "Home Assistant (Codex)", Path: []string{"Joe", "Home Assistant"}},
}, },
}, },
}, },
CurrentPath: []string{"Crew"}, CurrentPath: []string{"Joe"},
} }
got, err := state.VisibleEntries() got, err := state.VisibleEntries()
@@ -70,8 +70,8 @@ func TestVisibleEntriesAtParentGroupOnlyShowsDirectEntries(t *testing.T) {
for _, entry := range got { for _, entry := range got {
titles = append(titles, entry.Title) titles = append(titles, entry.Title)
} }
if !slices.Equal(titles, []string{"Crew Note"}) { if !slices.Equal(titles, []string{"Joe Note"}) {
t.Fatalf("visible titles = %v, want only direct entries from Crew", titles) t.Fatalf("visible titles = %v, want only direct entries from Joe", titles)
} }
} }
@@ -171,14 +171,14 @@ func TestVisibleEntriesUsesGlobalSearchWhenQueryPresent(t *testing.T) {
Session: stubSession{ Session: stubSession{
model: vault.Model{ model: vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ID: "bellagio", Title: "Bellagio", Path: []string{"Crew", "Internet"}}, {ID: "dynadot", Title: "Dynadot", Path: []string{"Joe", "Internet"}},
{ID: "vault-console", Title: "Vault Console", URL: "https://vault.crew.example.invalid", Path: []string{"Crew", "Internet"}}, {ID: "git-server", Title: "Git Server", URL: "https://git.julianfamily.org", Path: []string{"Joe", "Internet"}},
{ID: "surveillance-console", Title: "Surveillance Console", URL: "https://surveillance.crew.example.invalid", Path: []string{"Crew", "Home Assistant"}}, {ID: "ha-codex", Title: "Home Assistant (Codex)", URL: "https://lights.julianfamily.org", Path: []string{"Joe", "Home Assistant"}},
}, },
}, },
}, },
CurrentPath: []string{"Crew", "Internet"}, CurrentPath: []string{"Joe", "Internet"},
SearchQuery: "surveillance", SearchQuery: "lights",
} }
got, err := state.VisibleEntries() got, err := state.VisibleEntries()
@@ -186,7 +186,7 @@ func TestVisibleEntriesUsesGlobalSearchWhenQueryPresent(t *testing.T) {
t.Fatalf("VisibleEntries() error = %v", err) t.Fatalf("VisibleEntries() error = %v", err)
} }
if len(got) != 1 || got[0].Title != "Surveillance Console" { if len(got) != 1 || got[0].Title != "Home Assistant (Codex)" {
t.Fatalf("VisibleEntries() = %#v, want Home Assistant search match", got) t.Fatalf("VisibleEntries() = %#v, want Home Assistant search match", got)
} }
} }
@@ -198,13 +198,13 @@ func TestVisibleEntriesReturnsDescendantsAfterClearingSearch(t *testing.T) {
Session: stubSession{ Session: stubSession{
model: vault.Model{ model: vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ID: "bellagio", Title: "Bellagio", Path: []string{"Crew", "Internet"}}, {ID: "dynadot", Title: "Dynadot", Path: []string{"Joe", "Internet"}},
{ID: "vault-console", Title: "Vault Console", URL: "https://vault.crew.example.invalid", Path: []string{"Crew", "Internet"}}, {ID: "git-server", Title: "Git Server", URL: "https://git.julianfamily.org", Path: []string{"Joe", "Internet"}},
{ID: "surveillance-console", Title: "Surveillance Console", URL: "https://surveillance.crew.example.invalid", Path: []string{"Crew", "Home Assistant"}}, {ID: "ha-codex", Title: "Home Assistant (Codex)", URL: "https://lights.julianfamily.org", Path: []string{"Joe", "Home Assistant"}},
}, },
}, },
}, },
CurrentPath: []string{"Crew"}, CurrentPath: []string{"Joe"},
SearchQuery: "missing", SearchQuery: "missing",
} }
@@ -222,7 +222,7 @@ func TestVisibleEntriesReturnsDescendantsAfterClearingSearch(t *testing.T) {
t.Fatalf("VisibleEntries() after clearing search error = %v", err) t.Fatalf("VisibleEntries() after clearing search error = %v", err)
} }
if len(got) != 0 { if len(got) != 0 {
t.Fatalf("len(VisibleEntries()) after clearing search = %d, want 0 direct entries at Crew", len(got)) t.Fatalf("len(VisibleEntries()) after clearing search = %d, want 0 direct entries at Joe", len(got))
} }
} }
@@ -349,7 +349,7 @@ func TestVisibleEntriesUsesGlobalSearchWithinRecycleBin(t *testing.T) {
Session: stubSession{ Session: stubSession{
model: vault.Model{ model: vault.Model{
RecycleBin: []vault.Entry{ RecycleBin: []vault.Entry{
{ID: "deleted-1", Title: "Deleted Bellagio", Path: []string{"Root", "Internet"}}, {ID: "deleted-1", Title: "Deleted Dynadot", Path: []string{"Root", "Internet"}},
{ID: "deleted-2", Title: "Deleted HVAC", URL: "https://climate.example.com", Path: []string{"Root", "Home"}}, {ID: "deleted-2", Title: "Deleted HVAC", URL: "https://climate.example.com", Path: []string{"Root", "Home"}},
}, },
}, },
@@ -418,13 +418,13 @@ func TestChildGroupsUsesCurrentModelAndCurrentPath(t *testing.T) {
Session: stubSession{ Session: stubSession{
model: vault.Model{ model: vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ID: "bellagio", Title: "Bellagio", Path: []string{"Crew", "Internet"}}, {ID: "dynadot", Title: "Dynadot", Path: []string{"Joe", "Internet"}},
{ID: "surveillance-console", Title: "Surveillance Console", Path: []string{"Crew", "Home Assistant"}}, {ID: "ha-codex", Title: "Home Assistant (Codex)", Path: []string{"Joe", "Home Assistant"}},
{ID: "alma", Title: "Alma (WA Prep)", Path: []string{"Tricia", "School"}}, {ID: "alma", Title: "Alma (WA Prep)", Path: []string{"Tricia", "School"}},
}, },
}, },
}, },
CurrentPath: []string{"Crew"}, CurrentPath: []string{"Joe"},
} }
got, err := state.ChildGroups() got, err := state.ChildGroups()
@@ -470,19 +470,19 @@ func TestSelectVisibleEntryAndToggleSelection(t *testing.T) {
Session: stubSession{ Session: stubSession{
model: vault.Model{ model: vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ID: "bellagio", Title: "Bellagio", Path: []string{"Crew", "Internet"}}, {ID: "dynadot", Title: "Dynadot", Path: []string{"Joe", "Internet"}},
{ID: "vault-console", Title: "Vault Console", Path: []string{"Crew", "Internet"}}, {ID: "git-server", Title: "Git Server", Path: []string{"Joe", "Internet"}},
}, },
}, },
}, },
CurrentPath: []string{"Crew", "Internet"}, CurrentPath: []string{"Joe", "Internet"},
} }
if err := state.SelectVisibleIndex(1); err != nil { if err := state.SelectVisibleIndex(1); err != nil {
t.Fatalf("SelectVisibleIndex() error = %v", err) t.Fatalf("SelectVisibleIndex() error = %v", err)
} }
if got := state.SelectedEntryID; got != "vault-console" { if got := state.SelectedEntryID; got != "git-server" {
t.Fatalf("SelectedEntryID = %q, want %q", got, "vault-console") t.Fatalf("SelectedEntryID = %q, want %q", got, "git-server")
} }
if err := state.ToggleVisibleIndex(1); err != nil { if err := state.ToggleVisibleIndex(1); err != nil {
@@ -524,14 +524,14 @@ func TestDeleteSelectedEntryUpdatesSessionAndClearsSelection(t *testing.T) {
sess := &mutableStubSession{ sess := &mutableStubSession{
model: vault.Model{ model: vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ID: "vault-console", Title: "Vault Console", Path: []string{"Root", "Internet"}}, {ID: "git-server", Title: "Git Server", Path: []string{"Root", "Internet"}},
}, },
}, },
} }
state := State{ state := State{
Session: sess, Session: sess,
CurrentPath: []string{"Root", "Internet"}, CurrentPath: []string{"Root", "Internet"},
SelectedEntryID: "vault-console", SelectedEntryID: "git-server",
} }
if err := state.DeleteSelectedEntry(); err != nil { if err := state.DeleteSelectedEntry(); err != nil {
@@ -546,8 +546,8 @@ func TestDeleteSelectedEntryUpdatesSessionAndClearsSelection(t *testing.T) {
t.Fatalf("len(Entries) = %d, want 0", len(sess.model.Entries)) t.Fatalf("len(Entries) = %d, want 0", len(sess.model.Entries))
} }
if len(sess.model.RecycleBin) != 1 || sess.model.RecycleBin[0].ID != "vault-console" { if len(sess.model.RecycleBin) != 1 || sess.model.RecycleBin[0].ID != "git-server" {
t.Fatalf("RecycleBin = %#v, want vault-console entry", sess.model.RecycleBin) t.Fatalf("RecycleBin = %#v, want git-server entry", sess.model.RecycleBin)
} }
if !state.Dirty { if !state.Dirty {
@@ -561,7 +561,7 @@ func TestRestoreEntryMovesEntryBackIntoVisibleEntries(t *testing.T) {
sess := &mutableStubSession{ sess := &mutableStubSession{
model: vault.Model{ model: vault.Model{
RecycleBin: []vault.Entry{ RecycleBin: []vault.Entry{
{ID: "vault-console", Title: "Vault Console", Path: []string{"Root", "Internet"}}, {ID: "git-server", Title: "Git Server", Path: []string{"Root", "Internet"}},
}, },
}, },
} }
@@ -570,7 +570,7 @@ func TestRestoreEntryMovesEntryBackIntoVisibleEntries(t *testing.T) {
CurrentPath: []string{"Root", "Internet"}, CurrentPath: []string{"Root", "Internet"},
} }
if err := state.RestoreEntry("vault-console"); err != nil { if err := state.RestoreEntry("git-server"); err != nil {
t.Fatalf("RestoreEntry() error = %v", err) t.Fatalf("RestoreEntry() error = %v", err)
} }
@@ -579,8 +579,8 @@ func TestRestoreEntryMovesEntryBackIntoVisibleEntries(t *testing.T) {
t.Fatalf("VisibleEntries() error = %v", err) t.Fatalf("VisibleEntries() error = %v", err)
} }
if len(got) != 1 || got[0].ID != "vault-console" { if len(got) != 1 || got[0].ID != "git-server" {
t.Fatalf("VisibleEntries() = %#v, want restored vault-console entry", got) t.Fatalf("VisibleEntries() = %#v, want restored git-server entry", got)
} }
if !state.Dirty { if !state.Dirty {
@@ -600,19 +600,19 @@ func TestUpsertEntryPersistsEntryAndSelectsIt(t *testing.T) {
} }
entry := vault.Entry{ entry := vault.Entry{
ID: "vault-console", ID: "git-server",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
} }
if err := state.UpsertEntry(entry); err != nil { if err := state.UpsertEntry(entry); err != nil {
t.Fatalf("UpsertEntry() error = %v", err) t.Fatalf("UpsertEntry() error = %v", err)
} }
if got := state.SelectedEntryID; got != "vault-console" { if got := state.SelectedEntryID; got != "git-server" {
t.Fatalf("SelectedEntryID = %q, want %q", got, "vault-console") t.Fatalf("SelectedEntryID = %q, want %q", got, "git-server")
} }
got, err := state.VisibleEntries() got, err := state.VisibleEntries()
@@ -620,7 +620,7 @@ func TestUpsertEntryPersistsEntryAndSelectsIt(t *testing.T) {
t.Fatalf("VisibleEntries() error = %v", err) t.Fatalf("VisibleEntries() error = %v", err)
} }
if len(got) != 1 || got[0].Password != "token-1" { if len(got) != 1 || got[0].Password != "token-1" {
t.Fatalf("VisibleEntries() = %#v, want persisted vault-console entry", got) t.Fatalf("VisibleEntries() = %#v, want persisted git-server entry", got)
} }
if !state.Dirty { if !state.Dirty {
@@ -652,11 +652,11 @@ func TestInstantiateTemplateCreatesEntryAndSelectsIt(t *testing.T) {
} }
entry, err := state.InstantiateTemplate("website-login", vault.Entry{ entry, err := state.InstantiateTemplate("website-login", vault.Entry{
ID: "bellagio", ID: "dynadot",
Title: "Bellagio", Title: "Dynadot",
Username: "rustyryan", Username: "jjulian",
Password: "hunter2", Password: "hunter2",
URL: "https://bellagio.example.invalid", URL: "https://www.dynadot.com",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}) })
if err != nil { if err != nil {
@@ -667,16 +667,16 @@ func TestInstantiateTemplateCreatesEntryAndSelectsIt(t *testing.T) {
t.Fatalf("entry.Notes = %q, want template notes", entry.Notes) t.Fatalf("entry.Notes = %q, want template notes", entry.Notes)
} }
if got := state.SelectedEntryID; got != "bellagio" { if got := state.SelectedEntryID; got != "dynadot" {
t.Fatalf("SelectedEntryID = %q, want %q", got, "bellagio") t.Fatalf("SelectedEntryID = %q, want %q", got, "dynadot")
} }
got, err := state.VisibleEntries() got, err := state.VisibleEntries()
if err != nil { if err != nil {
t.Fatalf("VisibleEntries() error = %v", err) t.Fatalf("VisibleEntries() error = %v", err)
} }
if len(got) != 1 || got[0].ID != "bellagio" { if len(got) != 1 || got[0].ID != "dynadot" {
t.Fatalf("VisibleEntries() = %#v, want instantiated bellagio entry", got) t.Fatalf("VisibleEntries() = %#v, want instantiated dynadot entry", got)
} }
if !state.Dirty { if !state.Dirty {
@@ -743,24 +743,24 @@ func TestDuplicateSelectedEntryCreatesCopyAndSelectsIt(t *testing.T) {
sess := &mutableStubSession{model: vault.Model{ sess := &mutableStubSession{model: vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ID: "vault-console", Title: "Vault Console", Path: []string{"Root", "Internet"}}, {ID: "git-server", Title: "Git Server", Path: []string{"Root", "Internet"}},
}, },
}} }}
state := State{ state := State{
Session: sess, Session: sess,
SelectedEntryID: "vault-console", SelectedEntryID: "git-server",
} }
duplicate, err := state.DuplicateSelectedEntry("vault-console-copy") duplicate, err := state.DuplicateSelectedEntry("git-server-copy")
if err != nil { if err != nil {
t.Fatalf("DuplicateSelectedEntry() error = %v", err) t.Fatalf("DuplicateSelectedEntry() error = %v", err)
} }
if duplicate.ID != "vault-console-copy" { if duplicate.ID != "git-server-copy" {
t.Fatalf("duplicate.ID = %q, want %q", duplicate.ID, "vault-console-copy") t.Fatalf("duplicate.ID = %q, want %q", duplicate.ID, "git-server-copy")
} }
if state.SelectedEntryID != "vault-console-copy" { if state.SelectedEntryID != "git-server-copy" {
t.Fatalf("SelectedEntryID = %q, want vault-console-copy", state.SelectedEntryID) t.Fatalf("SelectedEntryID = %q, want git-server-copy", state.SelectedEntryID)
} }
if len(sess.model.Entries) != 2 { if len(sess.model.Entries) != 2 {
t.Fatalf("len(Entries) = %d, want 2 after duplicate", len(sess.model.Entries)) t.Fatalf("len(Entries) = %d, want 2 after duplicate", len(sess.model.Entries))
@@ -772,13 +772,13 @@ func TestMoveSelectedEntryMovesEntryToNewPathAndMarksDirty(t *testing.T) {
sess := &mutableStubSession{model: vault.Model{ sess := &mutableStubSession{model: vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ID: "vault-console", Title: "Vault Console", Path: []string{"Root", "Internet"}}, {ID: "git-server", Title: "Git Server", Path: []string{"Root", "Internet"}},
}, },
}} }}
state := State{ state := State{
Session: sess, Session: sess,
CurrentPath: []string{"Root", "Internet"}, CurrentPath: []string{"Root", "Internet"},
SelectedEntryID: "vault-console", SelectedEntryID: "git-server",
} }
if err := state.MoveSelectedEntry([]string{"Root", "Infrastructure"}); err != nil { if err := state.MoveSelectedEntry([]string{"Root", "Infrastructure"}); err != nil {
@@ -791,8 +791,8 @@ func TestMoveSelectedEntryMovesEntryToNewPathAndMarksDirty(t *testing.T) {
} }
newPath := sess.model.EntriesInPath([]string{"Root", "Infrastructure"}) newPath := sess.model.EntriesInPath([]string{"Root", "Infrastructure"})
if len(newPath) != 1 || newPath[0].ID != "vault-console" { if len(newPath) != 1 || newPath[0].ID != "git-server" {
t.Fatalf("EntriesInPath(Root/Infrastructure) = %#v, want moved vault-console entry", newPath) t.Fatalf("EntriesInPath(Root/Infrastructure) = %#v, want moved git-server entry", newPath)
} }
if !state.Dirty { if !state.Dirty {
@@ -806,19 +806,19 @@ func TestRestoreSelectedEntryVersionReplacesCurrentVersion(t *testing.T) {
sess := &mutableStubSession{model: vault.Model{ sess := &mutableStubSession{model: vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "vault-console", ID: "git-server",
Title: "Vault Console", Title: "Git Server",
Password: "new-token", Password: "new-token",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
History: []vault.Entry{ History: []vault.Entry{
{ID: "vault-console-h1", Title: "Vault Console", Password: "old-token", Path: []string{"Root", "Internet"}}, {ID: "git-server-h1", Title: "Git Server", Password: "old-token", Path: []string{"Root", "Internet"}},
}, },
}, },
}, },
}} }}
state := State{ state := State{
Session: sess, Session: sess,
SelectedEntryID: "vault-console", SelectedEntryID: "git-server",
} }
if err := state.RestoreSelectedEntryVersion(0); err != nil { if err := state.RestoreSelectedEntryVersion(0); err != nil {
@@ -862,7 +862,7 @@ func TestCreateVaultResetsSelectionPathAndDirtyState(t *testing.T) {
state := State{ state := State{
Session: sess, Session: sess,
CurrentPath: []string{"Root", "Internet"}, CurrentPath: []string{"Root", "Internet"},
SelectedEntryID: "vault-console", SelectedEntryID: "git-server",
Dirty: true, Dirty: true,
} }
@@ -891,7 +891,7 @@ func TestOpenVaultResetsSelectionPathAndDirtyState(t *testing.T) {
state := State{ state := State{
Session: sess, Session: sess,
CurrentPath: []string{"Root", "Internet"}, CurrentPath: []string{"Root", "Internet"},
SelectedEntryID: "vault-console", SelectedEntryID: "git-server",
Dirty: true, Dirty: true,
} }
@@ -942,7 +942,7 @@ func TestOpenRemoteVaultResetsSelectionPathAndDirtyState(t *testing.T) {
state := State{ state := State{
Session: sess, Session: sess,
CurrentPath: []string{"Root", "Internet"}, CurrentPath: []string{"Root", "Internet"},
SelectedEntryID: "vault-console", SelectedEntryID: "git-server",
Dirty: true, Dirty: true,
} }
@@ -970,14 +970,14 @@ func TestLockClearsSelectionAndMakesVaultUnavailable(t *testing.T) {
sess := &lockableStubSession{ sess := &lockableStubSession{
model: vault.Model{ model: vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ID: "vault-console", Title: "Vault Console", Path: []string{"Root", "Internet"}}, {ID: "git-server", Title: "Git Server", Path: []string{"Root", "Internet"}},
}, },
}, },
} }
state := State{ state := State{
Session: sess, Session: sess,
CurrentPath: []string{"Root", "Internet"}, CurrentPath: []string{"Root", "Internet"},
SelectedEntryID: "vault-console", SelectedEntryID: "git-server",
} }
if err := state.Lock(); err != nil { if err := state.Lock(); err != nil {
@@ -1000,7 +1000,7 @@ func TestUnlockRestoresVaultVisibility(t *testing.T) {
sess := &lockableStubSession{ sess := &lockableStubSession{
model: vault.Model{ model: vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ID: "vault-console", Title: "Vault Console", Path: []string{"Root", "Internet"}}, {ID: "git-server", Title: "Git Server", Path: []string{"Root", "Internet"}},
}, },
}, },
locked: true, locked: true,
@@ -1018,8 +1018,8 @@ func TestUnlockRestoresVaultVisibility(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("VisibleEntries() error = %v", err) t.Fatalf("VisibleEntries() error = %v", err)
} }
if len(got) != 1 || got[0].ID != "vault-console" { if len(got) != 1 || got[0].ID != "git-server" {
t.Fatalf("VisibleEntries() = %#v, want vault-console entry after unlock", got) t.Fatalf("VisibleEntries() = %#v, want git-server entry after unlock", got)
} }
} }
@@ -1048,7 +1048,7 @@ func TestShowSectionResetsPathAndSelection(t *testing.T) {
state := State{ state := State{
Section: SectionEntries, Section: SectionEntries,
CurrentPath: []string{"Root", "Internet"}, CurrentPath: []string{"Root", "Internet"},
SelectedEntryID: "vault-console", SelectedEntryID: "git-server",
SearchQuery: "git", SearchQuery: "git",
} }
@@ -1084,7 +1084,7 @@ func TestBeginNewEntryClearsSelectionAndStatus(t *testing.T) {
t.Parallel() t.Parallel()
state := State{ state := State{
SelectedEntryID: "vault-console", SelectedEntryID: "git-server",
ErrorMessage: "previous error", ErrorMessage: "previous error",
} }
@@ -1128,7 +1128,7 @@ func TestEnterGroupAppendsPathAndClearsSelection(t *testing.T) {
state := State{ state := State{
CurrentPath: []string{"Root"}, CurrentPath: []string{"Root"},
SelectedEntryID: "vault-console", SelectedEntryID: "git-server",
} }
state.EnterGroup("Internet") state.EnterGroup("Internet")
@@ -1146,7 +1146,7 @@ func TestNavigateToPathReplacesPathAndClearsSelection(t *testing.T) {
state := State{ state := State{
CurrentPath: []string{"Root", "Internet"}, CurrentPath: []string{"Root", "Internet"},
SelectedEntryID: "vault-console", SelectedEntryID: "git-server",
} }
state.NavigateToPath([]string{"Root", "Home Assistant"}) state.NavigateToPath([]string{"Root", "Home Assistant"})
@@ -1297,7 +1297,7 @@ func TestMoveSelectedEntryPersistsPathChangeAndMarksDirty(t *testing.T) {
state := State{ state := State{
Session: sess, Session: sess,
CurrentPath: []string{"Root", "Internet"}, CurrentPath: []string{"Root", "Internet"},
SelectedEntryID: "bellagio", SelectedEntryID: "dynadot",
} }
if err := state.MoveSelectedEntry([]string{"Root", "Home Assistant"}); err != nil { if err := state.MoveSelectedEntry([]string{"Root", "Home Assistant"}); err != nil {
@@ -1313,8 +1313,8 @@ func TestMoveSelectedEntryPersistsPathChangeAndMarksDirty(t *testing.T) {
if len(got) != 2 { if len(got) != 2 {
t.Fatalf("len(VisibleEntries()) = %d, want 2", len(got)) t.Fatalf("len(VisibleEntries()) = %d, want 2", len(got))
} }
if got[0].ID != "bellagio" && got[1].ID != "bellagio" { if got[0].ID != "dynadot" && got[1].ID != "dynadot" {
t.Fatalf("VisibleEntries() = %#v, want moved bellagio entry in destination group", got) t.Fatalf("VisibleEntries() = %#v, want moved dynadot entry in destination group", got)
} }
if !state.Dirty { if !state.Dirty {
t.Fatal("Dirty = false, want true after MoveSelectedEntry") t.Fatal("Dirty = false, want true after MoveSelectedEntry")
@@ -1326,7 +1326,7 @@ func TestMoveCurrentGroupMovesHierarchyAndMarksDirty(t *testing.T) {
model := vault.Model{ model := vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ID: "vault-console", Title: "Vault Console", Path: []string{"Root", "Internet"}}, {ID: "git-server", Title: "Git Server", Path: []string{"Root", "Internet"}},
}, },
} }
model.CreateGroup([]string{"Root", "Internet"}, "Infrastructure") model.CreateGroup([]string{"Root", "Internet"}, "Infrastructure")
@@ -1337,15 +1337,15 @@ func TestMoveCurrentGroupMovesHierarchyAndMarksDirty(t *testing.T) {
CurrentPath: []string{"Root", "Internet"}, CurrentPath: []string{"Root", "Internet"},
} }
if err := state.MoveCurrentGroup([]string{"Root", "Crew"}); err != nil { if err := state.MoveCurrentGroup([]string{"Root", "Joe"}); err != nil {
t.Fatalf("MoveCurrentGroup() error = %v", err) t.Fatalf("MoveCurrentGroup() error = %v", err)
} }
if !slices.Equal(state.CurrentPath, []string{"Root", "Crew", "Internet"}) { if !slices.Equal(state.CurrentPath, []string{"Root", "Joe", "Internet"}) {
t.Fatalf("CurrentPath = %v, want [Root Crew Internet]", state.CurrentPath) t.Fatalf("CurrentPath = %v, want [Root Joe Internet]", state.CurrentPath)
} }
if got := session.model.EntriesInPath([]string{"Root", "Crew", "Internet"}); len(got) != 1 || got[0].ID != "vault-console" { if got := session.model.EntriesInPath([]string{"Root", "Joe", "Internet"}); len(got) != 1 || got[0].ID != "git-server" {
t.Fatalf("EntriesInPath(Root/Crew/Internet) = %#v, want moved entry", got) t.Fatalf("EntriesInPath(Root/Joe/Internet) = %#v, want moved entry", got)
} }
if !state.Dirty { if !state.Dirty {
t.Fatal("Dirty = false, want true after MoveCurrentGroup") t.Fatal("Dirty = false, want true after MoveCurrentGroup")
@@ -1359,7 +1359,7 @@ func TestAddAttachmentToSelectedEntryPersistsAndMarksDirty(t *testing.T) {
state := State{ state := State{
Session: sess, Session: sess,
CurrentPath: []string{"Root", "Internet"}, CurrentPath: []string{"Root", "Internet"},
SelectedEntryID: "bellagio", SelectedEntryID: "dynadot",
} }
if err := state.AddAttachmentToSelectedEntry("token.txt", []byte("secret")); err != nil { if err := state.AddAttachmentToSelectedEntry("token.txt", []byte("secret")); err != nil {
@@ -1387,7 +1387,7 @@ func TestAddAttachmentToSelectedEntryRejectsDuplicateNames(t *testing.T) {
state := State{ state := State{
Session: sess, Session: sess,
CurrentPath: []string{"Root", "Internet"}, CurrentPath: []string{"Root", "Internet"},
SelectedEntryID: "bellagio", SelectedEntryID: "dynadot",
} }
err := state.AddAttachmentToSelectedEntry("token.txt", []byte("replacement")) err := state.AddAttachmentToSelectedEntry("token.txt", []byte("replacement"))
@@ -1413,7 +1413,7 @@ func TestReplaceAttachmentOnSelectedEntryPersistsAndMarksDirty(t *testing.T) {
state := State{ state := State{
Session: sess, Session: sess,
CurrentPath: []string{"Root", "Internet"}, CurrentPath: []string{"Root", "Internet"},
SelectedEntryID: "bellagio", SelectedEntryID: "dynadot",
} }
if err := state.ReplaceAttachmentOnSelectedEntry("token.txt", []byte("replacement")); err != nil { if err := state.ReplaceAttachmentOnSelectedEntry("token.txt", []byte("replacement")); err != nil {
@@ -1439,7 +1439,7 @@ func TestReplaceAttachmentOnSelectedEntryRequiresExistingAttachment(t *testing.T
state := State{ state := State{
Session: sess, Session: sess,
CurrentPath: []string{"Root", "Internet"}, CurrentPath: []string{"Root", "Internet"},
SelectedEntryID: "bellagio", SelectedEntryID: "dynadot",
} }
err := state.ReplaceAttachmentOnSelectedEntry("token.txt", []byte("replacement")) err := state.ReplaceAttachmentOnSelectedEntry("token.txt", []byte("replacement"))
@@ -1457,7 +1457,7 @@ func TestDeleteAttachmentFromSelectedEntryPersistsAndMarksDirty(t *testing.T) {
state := State{ state := State{
Session: sess, Session: sess,
CurrentPath: []string{"Root", "Internet"}, CurrentPath: []string{"Root", "Internet"},
SelectedEntryID: "bellagio", SelectedEntryID: "dynadot",
} }
if err := state.DeleteAttachmentFromSelectedEntry("token.txt"); err != nil { if err := state.DeleteAttachmentFromSelectedEntry("token.txt"); err != nil {
@@ -1483,7 +1483,7 @@ func TestDeleteAttachmentFromSelectedEntryRequiresExistingAttachment(t *testing.
state := State{ state := State{
Session: sess, Session: sess,
CurrentPath: []string{"Root", "Internet"}, CurrentPath: []string{"Root", "Internet"},
SelectedEntryID: "bellagio", SelectedEntryID: "dynadot",
} }
err := state.DeleteAttachmentFromSelectedEntry("token.txt") err := state.DeleteAttachmentFromSelectedEntry("token.txt")
@@ -1511,8 +1511,8 @@ func (s *mutableStubSession) Replace(model vault.Model) {
func testVaultModel() vault.Model { func testVaultModel() vault.Model {
return vault.Model{ return vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ID: "bellagio", Title: "Bellagio", Path: []string{"Root", "Internet"}}, {ID: "dynadot", Title: "Dynadot", Path: []string{"Root", "Internet"}},
{ID: "surveillance-console", Title: "Surveillance Console", Path: []string{"Root", "Home Assistant"}}, {ID: "ha-codex", Title: "Home Assistant (Codex)", Path: []string{"Root", "Home Assistant"}},
}, },
} }
} }
@@ -1584,26 +1584,6 @@ func (s *lifecycleStubSession) OpenRemote(_ webdav.Client, path string, _ vault.
return nil return nil
} }
func (s *lifecycleStubSession) SynchronizeFromLocal(string) error {
return nil
}
func (s *lifecycleStubSession) SynchronizeFromLocalBytes(string, []byte) error {
return nil
}
func (s *lifecycleStubSession) SynchronizeToLocal(string) error {
return nil
}
func (s *lifecycleStubSession) SynchronizeFromRemote(webdav.Client, string) error {
return nil
}
func (s *lifecycleStubSession) SynchronizeToRemote(webdav.Client, string) error {
return nil
}
func (s *lifecycleStubSession) ChangeMasterKey(key vault.MasterKey) error { func (s *lifecycleStubSession) ChangeMasterKey(key vault.MasterKey) error {
s.changedKey = key s.changedKey = key
return nil return nil
+10 -10
View File
@@ -22,7 +22,7 @@ func TestBuildFiltersAndNormalizesEntries(t *testing.T) {
Username: "joe", Username: "joe",
Password: "secret", Password: "secret",
URL: "https://10.0.2.2:8443/login", URL: "https://10.0.2.2:8443/login",
Path: []string{"Crew", "Internet"}, Path: []string{"Joe", "Internet"},
}, },
{ {
ID: "two", ID: "two",
@@ -35,10 +35,10 @@ func TestBuildFiltersAndNormalizesEntries(t *testing.T) {
Title: "Bare Host", Title: "Bare Host",
Username: "user", Username: "user",
Password: "pass", Password: "pass",
URL: "surveillance.crew.example.invalid", URL: "lights.julianfamily.org",
Fields: map[string]string{ Fields: map[string]string{
"AndroidApp1": "androidapp://com.lights.mobile", "AndroidApp1": "androidapp://com.lights.mobile",
"KP2A_URL_1": "https://surveillance.crew.example.invalid/account", "KP2A_URL_1": "https://lights.julianfamily.org/account",
}, },
}, },
}, },
@@ -50,8 +50,8 @@ func TestBuildFiltersAndNormalizesEntries(t *testing.T) {
if got.Entries[0].Host != "10.0.2.2" { if got.Entries[0].Host != "10.0.2.2" {
t.Fatalf("first host = %q, want 10.0.2.2", got.Entries[0].Host) t.Fatalf("first host = %q, want 10.0.2.2", got.Entries[0].Host)
} }
if got.Entries[1].Host != "surveillance.crew.example.invalid" { if got.Entries[1].Host != "lights.julianfamily.org" {
t.Fatalf("second host = %q, want surveillance.crew.example.invalid", got.Entries[1].Host) t.Fatalf("second host = %q, want lights.julianfamily.org", got.Entries[1].Host)
} }
if len(got.Entries[1].Targets) != 3 { if len(got.Entries[1].Targets) != 3 {
t.Fatalf("len(second targets) = %d, want 3", len(got.Entries[1].Targets)) t.Fatalf("len(second targets) = %d, want 3", len(got.Entries[1].Targets))
@@ -137,21 +137,21 @@ func TestMatchRejectsAmbiguousSharedHost(t *testing.T) {
Title: "Host A", Title: "Host A",
Username: "first", Username: "first",
Password: "secret1", Password: "secret1",
URL: "https://surveillance.crew.example.invalid/", URL: "https://lights.julianfamily.org/",
Host: "surveillance.crew.example.invalid", Host: "lights.julianfamily.org",
}, },
{ {
ID: "two", ID: "two",
Title: "Host B", Title: "Host B",
Username: "second", Username: "second",
Password: "secret2", Password: "secret2",
URL: "https://surveillance.crew.example.invalid/", URL: "https://lights.julianfamily.org/",
Host: "surveillance.crew.example.invalid", Host: "lights.julianfamily.org",
}, },
}, },
} }
if _, ok := Match(cache, "https://surveillance.crew.example.invalid/"); ok { if _, ok := Match(cache, "https://lights.julianfamily.org/"); ok {
t.Fatalf("Match() unexpectedly resolved ambiguous shared host") t.Fatalf("Match() unexpectedly resolved ambiguous shared host")
} }
} }
-4
View File
@@ -13,7 +13,6 @@ const (
DefaultAppID = "org.julianfamily.keepassgo" DefaultAppID = "org.julianfamily.keepassgo"
DefaultAPKOut = "build/keepassgo.apk" DefaultAPKOut = "build/keepassgo.apk"
DefaultVersion = "0.1.0.1" DefaultVersion = "0.1.0.1"
DefaultLdflags = "-X main.appVersion=dev"
DefaultMinSDK = "28" DefaultMinSDK = "28"
DefaultTargetSDK = "35" DefaultTargetSDK = "35"
DefaultIconPath = "assets/keepassgo-icon.png" DefaultIconPath = "assets/keepassgo-icon.png"
@@ -26,7 +25,6 @@ type Config struct {
AppID string AppID string
APKOut string APKOut string
Version string Version string
Ldflags string
MinSDK string MinSDK string
TargetSDK string TargetSDK string
IconPath string IconPath string
@@ -40,7 +38,6 @@ func DefaultConfig() Config {
AppID: DefaultAppID, AppID: DefaultAppID,
APKOut: DefaultAPKOut, APKOut: DefaultAPKOut,
Version: DefaultVersion, Version: DefaultVersion,
Ldflags: DefaultLdflags,
MinSDK: DefaultMinSDK, MinSDK: DefaultMinSDK,
TargetSDK: DefaultTargetSDK, TargetSDK: DefaultTargetSDK,
IconPath: DefaultIconPath, IconPath: DefaultIconPath,
@@ -52,7 +49,6 @@ func (c Config) GogioArgs() []string {
"-target", "android", "-target", "android",
"-buildmode", "exe", "-buildmode", "exe",
"-appid", c.AppID, "-appid", c.AppID,
"-ldflags", c.Ldflags,
"-o", c.APKOut, "-o", c.APKOut,
"-version", c.Version, "-version", c.Version,
"-minsdk", c.MinSDK, "-minsdk", c.MinSDK,
-3
View File
@@ -15,7 +15,6 @@ func TestDefaultConfigGogioArgs(t *testing.T) {
"-target", "android", "-target", "android",
"-buildmode", "exe", "-buildmode", "exe",
"-appid", DefaultAppID, "-appid", DefaultAppID,
"-ldflags", DefaultLdflags,
"-o", DefaultAPKOut, "-o", DefaultAPKOut,
"-version", DefaultVersion, "-version", DefaultVersion,
"-minsdk", DefaultMinSDK, "-minsdk", DefaultMinSDK,
@@ -53,7 +52,6 @@ func TestValidateAcceptsCompleteAndroidToolchainLayout(t *testing.T) {
AppID: DefaultAppID, AppID: DefaultAppID,
APKOut: DefaultAPKOut, APKOut: DefaultAPKOut,
Version: DefaultVersion, Version: DefaultVersion,
Ldflags: DefaultLdflags,
MinSDK: DefaultMinSDK, MinSDK: DefaultMinSDK,
TargetSDK: DefaultTargetSDK, TargetSDK: DefaultTargetSDK,
IconPath: filepath.Join(root, "icon.png"), IconPath: filepath.Join(root, "icon.png"),
@@ -79,7 +77,6 @@ func TestValidateRejectsMissingPrerequisites(t *testing.T) {
AppID: DefaultAppID, AppID: DefaultAppID,
APKOut: DefaultAPKOut, APKOut: DefaultAPKOut,
Version: DefaultVersion, Version: DefaultVersion,
Ldflags: DefaultLdflags,
MinSDK: DefaultMinSDK, MinSDK: DefaultMinSDK,
TargetSDK: DefaultTargetSDK, TargetSDK: DefaultTargetSDK,
} }
+11 -11
View File
@@ -13,11 +13,11 @@ func TestServiceCopiesUsernamePasswordAndURL(t *testing.T) {
model := vault.Model{ model := vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "vault-console", ID: "git-server",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
}, },
}, },
} }
@@ -27,9 +27,9 @@ func TestServiceCopiesUsernamePasswordAndURL(t *testing.T) {
target Target target Target
want string want string
}{ }{
{name: "username", target: TargetUsername, want: "dannyocean"}, {name: "username", target: TargetUsername, want: "joejulian"},
{name: "password", target: TargetPassword, want: "token-1"}, {name: "password", target: TargetPassword, want: "token-1"},
{name: "url", target: TargetURL, want: "https://vault.crew.example.invalid"}, {name: "url", target: TargetURL, want: "https://git.julianfamily.org"},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -37,7 +37,7 @@ func TestServiceCopiesUsernamePasswordAndURL(t *testing.T) {
var writer memoryWriter var writer memoryWriter
service := Service{Writer: &writer} service := Service{Writer: &writer}
if err := service.Copy(model, "vault-console", tt.target); err != nil { if err := service.Copy(model, "git-server", tt.target); err != nil {
t.Fatalf("Copy() error = %v", err) t.Fatalf("Copy() error = %v", err)
} }
@@ -60,9 +60,9 @@ func TestServiceRejectsUnknownEntryAndUnsupportedTarget(t *testing.T) {
} }
model := vault.Model{ model := vault.Model{
Entries: []vault.Entry{{ID: "vault-console", Username: "dannyocean"}}, Entries: []vault.Entry{{ID: "git-server", Username: "joejulian"}},
} }
err = service.Copy(model, "vault-console", Target("unsupported")) err = service.Copy(model, "git-server", Target("unsupported"))
if !errors.Is(err, ErrUnsupportedTarget) { if !errors.Is(err, ErrUnsupportedTarget) {
t.Fatalf("Copy() unsupported target error = %v, want ErrUnsupportedTarget", err) t.Fatalf("Copy() unsupported target error = %v, want ErrUnsupportedTarget", err)
} }
@@ -74,11 +74,11 @@ func TestServiceSanitizesClipboardWriteErrors(t *testing.T) {
service := Service{Writer: failingWriter{err: errors.New("backend refused token-1")}} service := Service{Writer: failingWriter{err: errors.New("backend refused token-1")}}
model := vault.Model{ model := vault.Model{
Entries: []vault.Entry{ Entries: []vault.Entry{
{ID: "vault-console", Password: "token-1"}, {ID: "git-server", Password: "token-1"},
}, },
} }
err := service.Copy(model, "vault-console", TargetPassword) err := service.Copy(model, "git-server", TargetPassword)
if !errors.Is(err, ErrWriteFailed) { if !errors.Is(err, ErrWriteFailed) {
t.Fatalf("Copy() write error = %v, want ErrWriteFailed", err) t.Fatalf("Copy() write error = %v, want ErrWriteFailed", err)
} }
-63
View File
@@ -1,63 +0,0 @@
package main
import (
"io"
"strings"
"sync"
gioclipboard "gioui.org/io/clipboard"
"gioui.org/layout"
appclipboard "git.julianfamily.org/keepassgo/clipboard"
)
type clipboardCommandWriter struct {
mu sync.Mutex
pending []string
invalidate func()
}
func newPlatformClipboardWriter(goos string, invalidate func()) appclipboard.Writer {
if strings.EqualFold(goos, "android") {
return &clipboardCommandWriter{invalidate: invalidate}
}
return nil
}
func processClipboardWrites(gtx layout.Context, writer appclipboard.Writer) {
commandWriter, ok := writer.(*clipboardCommandWriter)
if !ok {
return
}
commandWriter.Process(gtx)
}
func (w *clipboardCommandWriter) WriteText(text string) error {
w.mu.Lock()
w.pending = append(w.pending, text)
w.mu.Unlock()
if w.invalidate != nil {
w.invalidate()
}
return nil
}
func (w *clipboardCommandWriter) Process(gtx layout.Context) {
for _, text := range w.drain() {
gtx.Execute(gioclipboard.WriteCmd{
Type: "application/text",
Data: io.NopCloser(strings.NewReader(text)),
})
}
}
func (w *clipboardCommandWriter) drain() []string {
w.mu.Lock()
defer w.mu.Unlock()
if len(w.pending) == 0 {
return nil
}
pending := append([]string(nil), w.pending...)
w.pending = nil
return pending
}
-42
View File
@@ -1,42 +0,0 @@
package main
import (
"slices"
"testing"
)
func TestNewPlatformClipboardWriterUsesCommandWriterOnAndroid(t *testing.T) {
t.Parallel()
writer := newPlatformClipboardWriter("android", nil)
if _, ok := writer.(*clipboardCommandWriter); !ok {
t.Fatalf("newPlatformClipboardWriter(android) = %T, want *clipboardCommandWriter", writer)
}
}
func TestNewPlatformClipboardWriterUsesSystemClipboardOffAndroid(t *testing.T) {
t.Parallel()
if writer := newPlatformClipboardWriter("linux", nil); writer != nil {
t.Fatalf("newPlatformClipboardWriter(linux) = %T, want nil", writer)
}
}
func TestClipboardCommandWriterDrainsQueuedWrites(t *testing.T) {
t.Parallel()
writer := &clipboardCommandWriter{}
if err := writer.WriteText("username"); err != nil {
t.Fatalf("WriteText(username) error = %v", err)
}
if err := writer.WriteText("password"); err != nil {
t.Fatalf("WriteText(password) error = %v", err)
}
if got := writer.drain(); !slices.Equal(got, []string{"username", "password"}) {
t.Fatalf("drain() = %v, want [username password]", got)
}
if got := writer.drain(); got != nil {
t.Fatalf("drain() after flush = %v, want nil", got)
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ KeePassGO currently targets keyboard-first desktop use on Linux and Windows.
- list navigation - list navigation
- search focus - search focus
- new-entry focus transitions - new-entry focus transitions
- Controls that participate in keyboard navigation have intent-revealing accessibility labels through `accessibilityLabel` in [`ui_accessibility.go`](/workspace/keepassgo/ui_accessibility.go). - Controls that participate in keyboard navigation have intent-revealing accessibility labels through `accessibilityLabel` in [`ui_accessibility.go`](/home/jjulian/dev/go/src/git.julianfamily.org/keepassgo/ui_accessibility.go).
## Current screen-reader boundary ## Current screen-reader boundary
+1 -4
View File
@@ -6,7 +6,6 @@ replace gioui.org/cmd => ./third_party/gioui-cmd
require ( require (
gioui.org v0.8.0 gioui.org v0.8.0
gioui.org/x v0.8.0
github.com/atotto/clipboard v0.1.4 github.com/atotto/clipboard v0.1.4
github.com/tobischo/gokeepasslib/v3 v3.6.2 github.com/tobischo/gokeepasslib/v3 v3.6.2
golang.org/x/exp/shiny v0.0.0-20260312153236-7ab1446f8b90 golang.org/x/exp/shiny v0.0.0-20260312153236-7ab1446f8b90
@@ -19,7 +18,6 @@ require (
4d63.com/gochecknoglobals v0.2.2 // indirect 4d63.com/gochecknoglobals v0.2.2 // indirect
gioui.org/cmd v0.8.0 // indirect gioui.org/cmd v0.8.0 // indirect
gioui.org/shader v1.0.8 // indirect gioui.org/shader v1.0.8 // indirect
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 // indirect
github.com/4meepo/tagalign v1.4.2 // indirect github.com/4meepo/tagalign v1.4.2 // indirect
github.com/Abirdcfly/dupword v0.1.3 // indirect github.com/Abirdcfly/dupword v0.1.3 // indirect
github.com/Antonboom/errname v1.0.0 // indirect github.com/Antonboom/errname v1.0.0 // indirect
@@ -76,7 +74,6 @@ require (
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/glob v0.2.3 // indirect
github.com/godbus/dbus/v5 v5.0.6 // indirect
github.com/gofrs/flock v0.12.1 // indirect github.com/gofrs/flock v0.12.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
@@ -195,7 +192,7 @@ require (
go.uber.org/multierr v1.6.0 // indirect go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.24.0 // indirect go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/image v0.37.0 // indirect golang.org/x/image v0.37.0 // indirect
golang.org/x/mod v0.33.0 // indirect golang.org/x/mod v0.33.0 // indirect
+2 -8
View File
@@ -42,10 +42,6 @@ gioui.org v0.8.0/go.mod h1:vEMmpxMOd/iwJhXvGVIzWEbxMWhnMQ9aByOGQdlQ8rc=
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA= gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
gioui.org/x v0.8.0 h1:RhIlQNOFKKn8D8FeaKKaXCo7vB3x+fq4VcD10HW/YpA=
gioui.org/x v0.8.0/go.mod h1:aXtQb+kyqoUOjDl5/uMqAopjzVzMkeHBbMQOGT5KnSE=
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 h1:bGG/g4ypjrCJoSvFrP5hafr9PPB5aw8SjcOWWila7ZI=
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0/go.mod h1:+axXBRUTIDlCeE73IKeD/os7LoEnTKdkp8/gQOFjqyo=
github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E= github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E=
github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI= github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI=
github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE= github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE=
@@ -228,8 +224,6 @@ github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -675,8 +669,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
golang.org/x/exp/shiny v0.0.0-20260312153236-7ab1446f8b90 h1:kyPrwnEYXdME284bE7xgS9BPxhG7MCa5hw1/TpaTJVs= golang.org/x/exp/shiny v0.0.0-20260312153236-7ab1446f8b90 h1:kyPrwnEYXdME284bE7xgS9BPxhG7MCa5hw1/TpaTJVs=
golang.org/x/exp/shiny v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:jqkJFnLVkS8zgKKY4+MOPCZtuZGw3hONUjhapUSwZ8c= golang.org/x/exp/shiny v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:jqkJFnLVkS8zgKKY4+MOPCZtuZGw3hONUjhapUSwZ8c=
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
+86 -314
View File
@@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
"io"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -27,7 +26,6 @@ import (
"gioui.org/unit" "gioui.org/unit"
"gioui.org/widget" "gioui.org/widget"
"gioui.org/widget/material" "gioui.org/widget/material"
"gioui.org/x/explorer"
"git.julianfamily.org/keepassgo/api" "git.julianfamily.org/keepassgo/api"
"git.julianfamily.org/keepassgo/apiapproval" "git.julianfamily.org/keepassgo/apiapproval"
"git.julianfamily.org/keepassgo/apiaudit" "git.julianfamily.org/keepassgo/apiaudit"
@@ -43,15 +41,6 @@ import (
"golang.org/x/exp/shiny/materialdesign/icons" "golang.org/x/exp/shiny/materialdesign/icons"
) )
var appVersion = "dev"
func currentAppVersion() string {
if strings.TrimSpace(appVersion) == "" {
return "dev"
}
return strings.TrimSpace(appVersion)
}
type entry = vault.Entry type entry = vault.Entry
const ( const (
@@ -211,7 +200,6 @@ const (
type ui struct { type ui struct {
mode string mode string
theme *material.Theme theme *material.Theme
fileExplorer *explorer.Explorer
logoHorizontal paint.ImageOp logoHorizontal paint.ImageOp
splashSquare paint.ImageOp splashSquare paint.ImageOp
search widget.Editor search widget.Editor
@@ -343,7 +331,6 @@ type ui struct {
showRecycle widget.Clickable showRecycle widget.Clickable
showAPITokens widget.Clickable showAPITokens widget.Clickable
showAPIAudit widget.Clickable showAPIAudit widget.Clickable
showAbout widget.Clickable
showLocalLifecycle widget.Clickable showLocalLifecycle widget.Clickable
showRemoteLifecycle widget.Clickable showRemoteLifecycle widget.Clickable
showSyncLocal widget.Clickable showSyncLocal widget.Clickable
@@ -397,9 +384,6 @@ type ui struct {
disableAPIToken widget.Clickable disableAPIToken widget.Clickable
revokeAPIToken widget.Clickable revokeAPIToken widget.Clickable
deleteAPIToken widget.Clickable deleteAPIToken widget.Clickable
useCurrentGroupForPolicy widget.Clickable
useSelectedEntryForPolicy widget.Clickable
clearAPIPolicyTarget widget.Clickable
addAPIPolicyRule widget.Clickable addAPIPolicyRule widget.Clickable
phoneSplit widget.Float phoneSplit widget.Float
splitDrag gesture.Drag splitDrag gesture.Drag
@@ -412,7 +396,6 @@ type ui struct {
copyIcon *widget.Icon copyIcon *widget.Icon
expandMoreIcon *widget.Icon expandMoreIcon *widget.Icon
expandLessIcon *widget.Icon expandLessIcon *widget.Icon
chevronRightIcon *widget.Icon
chevronDownIcon *widget.Icon chevronDownIcon *widget.Icon
settingsIcon *widget.Icon settingsIcon *widget.Icon
menuIcon *widget.Icon menuIcon *widget.Icon
@@ -422,8 +405,6 @@ type ui struct {
lifecycleMode string lifecycleMode string
syncSourceMode syncSourceMode syncSourceMode syncSourceMode
syncDirection syncDirection syncDirection syncDirection
syncLocalImportName string
syncLocalImportContent []byte
syncLocalPath widget.Editor syncLocalPath widget.Editor
syncRemoteBaseURL widget.Editor syncRemoteBaseURL widget.Editor
syncRemotePath widget.Editor syncRemotePath widget.Editor
@@ -432,7 +413,6 @@ type ui struct {
syncDialogOpen bool syncDialogOpen bool
syncMenuOpen bool syncMenuOpen bool
mainMenuOpen bool mainMenuOpen bool
selectedRemoteConnection bool
securityDialogOpen bool securityDialogOpen bool
remotePrefsDialogOpen bool remotePrefsDialogOpen bool
showSyncPassword bool showSyncPassword bool
@@ -635,7 +615,6 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
u.copyIcon, _ = widget.NewIcon(icons.ContentContentCopy) u.copyIcon, _ = widget.NewIcon(icons.ContentContentCopy)
u.expandMoreIcon, _ = widget.NewIcon(icons.NavigationExpandMore) u.expandMoreIcon, _ = widget.NewIcon(icons.NavigationExpandMore)
u.expandLessIcon, _ = widget.NewIcon(icons.NavigationExpandLess) u.expandLessIcon, _ = widget.NewIcon(icons.NavigationExpandLess)
u.chevronRightIcon, _ = widget.NewIcon(icons.NavigationChevronRight)
u.chevronDownIcon, _ = widget.NewIcon(icons.NavigationArrowDropDown) u.chevronDownIcon, _ = widget.NewIcon(icons.NavigationArrowDropDown)
u.settingsIcon, _ = widget.NewIcon(icons.ActionSettings) u.settingsIcon, _ = widget.NewIcon(icons.ActionSettings)
u.menuIcon, _ = widget.NewIcon(icons.NavigationMenu) u.menuIcon, _ = widget.NewIcon(icons.NavigationMenu)
@@ -666,21 +645,13 @@ func (u *ui) filter() {
return return
} }
u.visible = visible u.visible = visible
u.ensureEntryClicks() if len(u.entryClicks) < len(u.visible) {
u.entryClicks = make([]widget.Clickable, len(u.visible))
} }
func (u *ui) ensureEntryClicks() {
if len(u.entryClicks) >= len(u.visible) {
return
}
next := make([]widget.Clickable, len(u.visible))
copy(next, u.entryClicks)
u.entryClicks = next
} }
func (u *ui) visibleEntrySnapshot() ([]entry, []*widget.Clickable) { func (u *ui) visibleEntrySnapshot() ([]entry, []*widget.Clickable) {
visible := append([]entry(nil), u.visible...) visible := append([]entry(nil), u.visible...)
u.ensureEntryClicks()
clicks := make([]*widget.Clickable, len(visible)) clicks := make([]*widget.Clickable, len(visible))
for i := range visible { for i := range visible {
clicks[i] = &u.entryClicks[i] clicks[i] = &u.entryClicks[i]
@@ -688,21 +659,6 @@ func (u *ui) visibleEntrySnapshot() ([]entry, []*widget.Clickable) {
return visible, clicks return visible, clicks
} }
func (u *ui) searchPlaceholder() string {
switch u.state.Section {
case appstate.SectionAPITokens:
return "Search API tokens"
case appstate.SectionAPIAudit:
return "Search audit log"
case appstate.SectionRecycleBin:
return "Search recycle bin"
case appstate.SectionAbout:
return "Search disabled on About"
default:
return "Search vault"
}
}
func defaultStatePaths(stateDir string) statePaths { func defaultStatePaths(stateDir string) statePaths {
baseDir := strings.TrimSpace(stateDir) baseDir := strings.TrimSpace(stateDir)
if baseDir == "" { if baseDir == "" {
@@ -827,14 +783,6 @@ func (u *ui) showAPIAuditSection() {
u.filter() u.filter()
} }
func (u *ui) showAboutSection() {
u.resetPasswordPeek()
u.rememberEntriesSectionState()
u.state.ShowSection(appstate.SectionAbout)
u.mainMenuOpen = false
u.filter()
}
func (u *ui) returnToMainEntries() { func (u *ui) returnToMainEntries() {
u.clearDeleteGroupConfirmation() u.clearDeleteGroupConfirmation()
u.showEntriesSection() u.showEntriesSection()
@@ -1242,19 +1190,6 @@ func (u *ui) openAdvancedSyncDialog() {
} }
} }
func (u *ui) clearSyncLocalImport() {
u.syncLocalImportName = ""
u.syncLocalImportContent = nil
}
func (u *ui) selectedSyncLocalImport() (string, []byte, bool) {
name := strings.TrimSpace(u.syncLocalImportName)
if name == "" || name != strings.TrimSpace(u.syncLocalPath.Text()) || len(u.syncLocalImportContent) == 0 {
return "", nil, false
}
return name, append([]byte(nil), u.syncLocalImportContent...), true
}
func sanitizeSyncSourceMode(mode syncSourceMode) syncSourceMode { func sanitizeSyncSourceMode(mode syncSourceMode) syncSourceMode {
switch mode { switch mode {
case syncSourceRemote: case syncSourceRemote:
@@ -1294,12 +1229,6 @@ func (u *ui) advancedSyncFromAction() error {
return err return err
} }
default: default:
if name, content, ok := u.selectedSyncLocalImport(); ok {
if err := u.state.SynchronizeFromLocalBytes(name, content); err != nil {
return err
}
break
}
path := strings.TrimSpace(u.syncLocalPath.Text()) path := strings.TrimSpace(u.syncLocalPath.Text())
if path == "" { if path == "" {
return errors.New(errVaultPathRequired) return errors.New(errVaultPathRequired)
@@ -1314,37 +1243,6 @@ func (u *ui) advancedSyncFromAction() error {
return nil return nil
} }
func (u *ui) startChooseSyncLocalSourceAction() {
if runtime.GOOS != "android" || u.fileExplorer == nil {
u.runAction("choose sync path", func() error {
u.clearSyncLocalImport()
return u.chooseExistingFileAction(&u.syncLocalPath)
})
return
}
u.runBackgroundAction("choose sync file", func() (func() error, error) {
file, err := u.fileExplorer.ChooseFile(".kdbx")
if err != nil {
if errors.Is(err, explorer.ErrUserDecline) {
return func() error { return nil }, nil
}
return nil, err
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
return nil, err
}
label := "Selected Android vault"
return func() error {
u.syncLocalImportName = label
u.syncLocalImportContent = append([]byte(nil), content...)
u.syncLocalPath.SetText(label)
return nil
}, nil
})
}
func (u *ui) advancedSyncToAction() error { func (u *ui) advancedSyncToAction() error {
switch u.syncSourceMode { switch u.syncSourceMode {
case syncSourceRemote: case syncSourceRemote:
@@ -1773,16 +1671,12 @@ func (u *ui) restoreStartupLifecycleTarget() {
func (u *ui) hasSelectedLifecycleTarget() bool { func (u *ui) hasSelectedLifecycleTarget() bool {
switch strings.TrimSpace(u.lifecycleMode) { switch strings.TrimSpace(u.lifecycleMode) {
case "remote": case "remote":
return u.hasSelectedRemoteTarget() return strings.TrimSpace(u.remoteBaseURL.Text()) != "" && strings.TrimSpace(u.remotePath.Text()) != ""
default: default:
return strings.TrimSpace(u.vaultPath.Text()) != "" return strings.TrimSpace(u.vaultPath.Text()) != ""
} }
} }
func (u *ui) hasSelectedRemoteTarget() bool {
return u.selectedRemoteConnection
}
func (u *ui) latestRecentVault() (string, time.Time) { func (u *ui) latestRecentVault() (string, time.Time) {
for _, path := range u.recentVaults { for _, path := range u.recentVaults {
if strings.TrimSpace(path) == "" { if strings.TrimSpace(path) == "" {
@@ -1793,58 +1687,6 @@ func (u *ui) latestRecentVault() (string, time.Time) {
return "", time.Time{} return "", time.Time{}
} }
func (u *ui) hasSelectedVaultPath() bool {
return strings.TrimSpace(u.vaultPath.Text()) != ""
}
func (u *ui) showLocalVaultChooser() bool {
return u.lifecycleMode != "local" || !u.hasSelectedVaultPath()
}
func (u *ui) showRemoteConnectionChooser() bool {
return u.lifecycleMode != "remote" || !u.hasSelectedRemoteTarget()
}
func (u *ui) switchToLifecycleSelection(mode string) {
u.state.Session = &session.Manager{}
u.state.CurrentPath = nil
u.state.SelectedEntryID = ""
u.state.Section = appstate.SectionEntries
u.state.Dirty = false
u.state.ErrorMessage = ""
u.state.StatusMessage = ""
u.loadingMessage = ""
u.loadingActionLabel = ""
u.lastLifecycleAction = ""
u.lifecycleMode = mode
u.editingEntry = false
u.currentPath = nil
u.syncedPath = nil
u.clearMasterPassword()
u.keyFilePath.SetText("")
u.search.SetText("")
switch mode {
case "remote":
u.vaultPath.SetText("")
u.remoteBaseURL.SetText("")
u.remotePath.SetText("")
u.remoteUsername.SetText("")
u.remotePassword.SetText("")
u.rememberRemoteAuth.Value = false
u.selectedRemoteConnection = false
default:
u.vaultPath.SetText("")
u.remoteBaseURL.SetText("")
u.remotePath.SetText("")
u.remoteUsername.SetText("")
u.remotePassword.SetText("")
u.rememberRemoteAuth.Value = false
u.selectedRemoteConnection = false
}
u.requestMasterPassFocus = u.hasSelectedLifecycleTarget()
u.filter()
}
func (u *ui) latestRecentRemote() (recentRemoteRecord, bool, time.Time) { func (u *ui) latestRecentRemote() (recentRemoteRecord, bool, time.Time) {
for _, record := range u.recentRemotes { for _, record := range u.recentRemotes {
if strings.TrimSpace(record.BaseURL) == "" || strings.TrimSpace(record.Path) == "" { if strings.TrimSpace(record.BaseURL) == "" || strings.TrimSpace(record.Path) == "" {
@@ -1888,7 +1730,6 @@ func (u *ui) applyRecentRemoteRecord(record recentRemoteRecord) {
u.remotePassword.SetText(record.Password) u.remotePassword.SetText(record.Password)
u.remotePassword.Mask = '•' u.remotePassword.Mask = '•'
u.rememberRemoteAuth.Value = strings.TrimSpace(record.Username) != "" || record.Password != "" u.rememberRemoteAuth.Value = strings.TrimSpace(record.Username) != "" || record.Password != ""
u.selectedRemoteConnection = true
} }
func (u *ui) remotePreferencesCurrentSummary() string { func (u *ui) remotePreferencesCurrentSummary() string {
@@ -2741,7 +2582,7 @@ func (u *ui) listEmptyState() emptyState {
case appstate.SectionAPITokens: case appstate.SectionAPITokens:
return emptyState{ return emptyState{
Title: "No matching API tokens", Title: "No matching API tokens",
Body: fmt.Sprintf("No API tokens match %q. Clear or refine Search API tokens to find a token by name, client, or expiration.", query), Body: fmt.Sprintf("No API tokens match %q. Clear or refine Search vault to find a token by name, client, or expiration.", query),
} }
case appstate.SectionAPIAudit: case appstate.SectionAPIAudit:
return emptyState{ return emptyState{
@@ -2776,11 +2617,6 @@ func (u *ui) listEmptyState() emptyState {
Title: "No API audit events yet", Title: "No API audit events yet",
Body: "Connect a trusted client, respond to approval prompts, or issue a token to start recording activity.", Body: "Connect a trusted client, respond to approval prompts, or issue a token to start recording activity.",
} }
case appstate.SectionAbout:
return emptyState{
Title: "About KeePassGO",
Body: "Product details, compatibility notes, and platform targets appear in the detail pane.",
}
case appstate.SectionTemplates: case appstate.SectionTemplates:
return emptyState{ return emptyState{
Title: "Templates unavailable", Title: "Templates unavailable",
@@ -2818,9 +2654,7 @@ func (u *ui) detailPlaceholderMessage() string {
case appstate.SectionAPITokens: case appstate.SectionAPITokens:
return "Select an API token, issue a new one, or search to narrow the list." return "Select an API token, issue a new one, or search to narrow the list."
case appstate.SectionAPIAudit: case appstate.SectionAPIAudit:
return "Select an audit event to inspect it, or use Search audit log or the quick filters above." return "Select an audit event to inspect it, or use Search vault or the quick filters above."
case appstate.SectionAbout:
return "Review the product overview, platform support, and compatibility goals."
case appstate.SectionTemplates: case appstate.SectionTemplates:
return "Select a template or start a reusable entry." return "Select a template or start a reusable entry."
case appstate.SectionRecycleBin: case appstate.SectionRecycleBin:
@@ -3038,10 +2872,6 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
u.clearDeleteGroupConfirmation() u.clearDeleteGroupConfirmation()
u.showAPIAuditSection() u.showAPIAuditSection()
} }
for u.showAbout.Clicked(gtx) {
u.clearDeleteGroupConfirmation()
u.showAboutSection()
}
for u.showLocalLifecycle.Clicked(gtx) { for u.showLocalLifecycle.Clicked(gtx) {
if u.lifecycleBusy() { if u.lifecycleBusy() {
continue continue
@@ -3054,7 +2884,6 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
continue continue
} }
u.lifecycleMode = "remote" u.lifecycleMode = "remote"
u.selectedRemoteConnection = false
u.requestMasterPassFocus = true u.requestMasterPassFocus = true
} }
for u.toggleLifecycleAdvanced.Clicked(gtx) { for u.toggleLifecycleAdvanced.Clicked(gtx) {
@@ -3157,15 +2986,6 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
for u.addAPIPolicyRule.Clicked(gtx) { for u.addAPIPolicyRule.Clicked(gtx) {
u.runAction("add API policy rule", u.addAPIPolicyRuleAction) u.runAction("add API policy rule", u.addAPIPolicyRuleAction)
} }
for u.useCurrentGroupForPolicy.Clicked(gtx) {
u.runAction("use current group for API policy", u.useCurrentGroupForPolicyAction)
}
for u.useSelectedEntryForPolicy.Clicked(gtx) {
u.runAction("use selected entry for API policy", u.useSelectedEntryForPolicyAction)
}
for u.clearAPIPolicyTarget.Clicked(gtx) {
u.runAction("clear API policy target", u.clearAPIPolicyTargetAction)
}
for i := range u.apiPolicyRemoves { for i := range u.apiPolicyRemoves {
for u.apiPolicyRemoves[i].Clicked(gtx) { for u.apiPolicyRemoves[i].Clicked(gtx) {
index := i index := i
@@ -3205,7 +3025,7 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
u.runAction("choose key file", func() error { return u.chooseExistingFileAction(&u.keyFilePath) }) u.runAction("choose key file", func() error { return u.chooseExistingFileAction(&u.keyFilePath) })
} }
for u.pickSyncLocalPath.Clicked(gtx) { for u.pickSyncLocalPath.Clicked(gtx) {
u.startChooseSyncLocalSourceAction() u.runAction("choose sync path", func() error { return u.chooseExistingFileAction(&u.syncLocalPath) })
} }
for i := range u.recentVaultClicks { for i := range u.recentVaultClicks {
for u.recentVaultClicks[i].Clicked(gtx) { for u.recentVaultClicks[i].Clicked(gtx) {
@@ -3235,10 +3055,6 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
if u.lifecycleBusy() { if u.lifecycleBusy() {
continue continue
} }
if u.shouldUseLockedSinglePane() {
u.switchToLifecycleSelection("local")
continue
}
u.vaultPath.SetText("") u.vaultPath.SetText("")
u.state.ErrorMessage = "" u.state.ErrorMessage = ""
u.state.StatusMessage = "" u.state.StatusMessage = ""
@@ -3248,11 +3064,6 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
if u.lifecycleBusy() { if u.lifecycleBusy() {
continue continue
} }
if u.shouldUseLockedSinglePane() {
u.switchToLifecycleSelection("remote")
continue
}
u.selectedRemoteConnection = false
u.remoteBaseURL.SetText("") u.remoteBaseURL.SetText("")
u.remotePath.SetText("") u.remotePath.SetText("")
u.remoteUsername.SetText("") u.remoteUsername.SetText("")
@@ -4228,10 +4039,6 @@ func (u *ui) mainMenu(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.showAPIAudit, "API Audit") return tonedButton(gtx, u.theme, &u.showAPIAudit, "API Audit")
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.showAbout, "About")
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Settings") return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Settings")
}), }),
@@ -4239,70 +4046,6 @@ func (u *ui) mainMenu(gtx layout.Context) layout.Dimensions {
}) })
} }
func (u *ui) aboutDetailPanel(gtx layout.Context) layout.Dimensions {
rows := []layout.Widget{
func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(22), "KeePassGO")
lbl.Color = accentColor
return lbl.Layout(gtx)
},
func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(15), "A KeePass-compatible password manager built in Go.")
lbl.Color = mutedColor
return lbl.Layout(gtx)
},
layout.Spacer{Height: unit.Dp(14)}.Layout,
aboutFact(u.theme, "Compatibility", "KeePass and KDBX interoperability", "Designed to coexist with desktop KeePass and KeePass2Android workflows."),
layout.Spacer{Height: unit.Dp(10)}.Layout,
aboutFact(u.theme, "Platforms", "Windows and Linux first, Android supported", "Desktop remains the primary product surface while Android stays compatible."),
layout.Spacer{Height: unit.Dp(10)}.Layout,
aboutFact(u.theme, "Sync", "Local files and direct WebDAV", "Remote-file workflows are first-class and avoid browser-stack dependencies."),
layout.Spacer{Height: unit.Dp(10)}.Layout,
aboutFact(u.theme, "Programmatic Access", "Secure local gRPC API", "Built for trusted clients such as browser extensions and automation."),
layout.Spacer{Height: unit.Dp(14)}.Layout,
func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "Version")
lbl.Color = mutedColor
return lbl.Layout(gtx)
},
func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(14), currentAppVersion())
lbl.Color = u.theme.Palette.Fg
return lbl.Layout(gtx)
},
}
return material.List(u.theme, &u.detailList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions {
return rows[i](gtx)
})
}
func aboutFact(theme *material.Theme, title, primary, secondary string) layout.Widget {
return func(gtx layout.Context) layout.Dimensions {
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(theme, unit.Sp(12), strings.ToUpper(title))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(theme, unit.Sp(16), primary)
lbl.Color = theme.Palette.Fg
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(theme, unit.Sp(13), secondary)
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
)
})
})
}
}
func (u *ui) syncButtonGroup(gtx layout.Context) layout.Dimensions { func (u *ui) syncButtonGroup(gtx layout.Context) layout.Dimensions {
label := "Sync" label := "Sync"
spacing := unit.Dp(4) spacing := unit.Dp(4)
@@ -4413,11 +4156,10 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
return panel(gtx, func(gtx layout.Context) layout.Dimensions { return panel(gtx, func(gtx layout.Context) layout.Dimensions {
visibleEntries, entryClicks := u.visibleEntrySnapshot() visibleEntries, entryClicks := u.visibleEntrySnapshot()
rows := make([]layout.Widget, 0, 16+len(visibleEntries)) rows := make([]layout.Widget, 0, 16+len(visibleEntries))
if u.state.Section != appstate.SectionAbout {
rows = append(rows, func(gtx layout.Context) layout.Dimensions { rows = append(rows, func(gtx layout.Context) layout.Dimensions {
gtx.Constraints.Min.X = gtx.Constraints.Max.X gtx.Constraints.Min.X = gtx.Constraints.Max.X
return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions { return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions {
editor := material.Editor(u.theme, &u.search, u.searchPlaceholder()) editor := material.Editor(u.theme, &u.search, "Search vault")
editor.Color = u.theme.Palette.Fg editor.Color = u.theme.Palette.Fg
editor.HintColor = mutedColor editor.HintColor = mutedColor
return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout) return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout)
@@ -4426,15 +4168,20 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
rows = append(rows, func(gtx layout.Context) layout.Dimensions { rows = append(rows, func(gtx layout.Context) layout.Dimensions {
return layout.Spacer{Height: spacing}.Layout(gtx) return layout.Spacer{Height: spacing}.Layout(gtx)
}) })
}
if !u.isVaultLocked() { if !u.isVaultLocked() {
rows = append(rows, u.navigationHeader) rows = append(rows, u.navigationHeader)
if u.state.Section == appstate.SectionEntries || u.state.Section == appstate.SectionAbout { if u.state.Section == appstate.SectionEntries {
rows = append(rows, func(gtx layout.Context) layout.Dimensions { rows = append(rows, func(gtx layout.Context) layout.Dimensions {
return layout.Spacer{Height: spacing}.Layout(gtx) return layout.Spacer{Height: spacing}.Layout(gtx)
}) })
} }
} }
if !u.isVaultLocked() && u.state.Section == appstate.SectionRecycleBin {
rows = append(rows, u.recycleBinSectionNotice)
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
return layout.Spacer{Height: spacing}.Layout(gtx)
})
}
if !u.isVaultLocked() && (u.state.Section == appstate.SectionEntries || u.state.Section == appstate.SectionRecycleBin) { if !u.isVaultLocked() && (u.state.Section == appstate.SectionEntries || u.state.Section == appstate.SectionRecycleBin) {
rows = append(rows, u.pathBar) rows = append(rows, u.pathBar)
rows = append(rows, func(gtx layout.Context) layout.Dimensions { rows = append(rows, func(gtx layout.Context) layout.Dimensions {
@@ -4459,8 +4206,6 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
return btn.Layout(gtx) return btn.Layout(gtx)
case appstate.SectionAPITokens: case appstate.SectionAPITokens:
return tonedButton(gtx, u.theme, &u.issueAPIToken, "Issue API Token") return tonedButton(gtx, u.theme, &u.issueAPIToken, "Issue API Token")
case appstate.SectionAbout:
return emptyStatePanel(gtx, u.theme, u.listEmptyState())
default: default:
return layout.Dimensions{} return layout.Dimensions{}
} }
@@ -4474,7 +4219,6 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
rows = append(rows, u.apiTokenListPanel) rows = append(rows, u.apiTokenListPanel)
case u.state.Section == appstate.SectionAPIAudit: case u.state.Section == appstate.SectionAPIAudit:
rows = append(rows, u.apiAuditListPanel) rows = append(rows, u.apiAuditListPanel)
case u.state.Section == appstate.SectionAbout:
case len(visibleEntries) == 0: case len(visibleEntries) == 0:
rows = append(rows, func(gtx layout.Context) layout.Dimensions { rows = append(rows, func(gtx layout.Context) layout.Dimensions {
return emptyStatePanel(gtx, u.theme, u.listEmptyState()) return emptyStatePanel(gtx, u.theme, u.listEmptyState())
@@ -4502,17 +4246,17 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
}), }),
layout.Rigid(layout.Spacer{Height: spacing}.Layout), layout.Rigid(layout.Spacer{Height: spacing}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.state.Section == appstate.SectionAbout { if u.isVaultLocked() || u.state.Section != appstate.SectionRecycleBin {
return emptyStatePanel(gtx, u.theme, u.listEmptyState())
}
return layout.Dimensions{}
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.state.Section == appstate.SectionAbout {
return layout.Dimensions{} return layout.Dimensions{}
} }
return layout.Spacer{Height: spacing}.Layout(gtx) return u.recycleBinSectionNotice(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: func() unit.Dp {
if u.isVaultLocked() || u.state.Section != appstate.SectionRecycleBin {
return 0
}
return spacing
}()}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.isVaultLocked() || (u.state.Section != appstate.SectionEntries && u.state.Section != appstate.SectionRecycleBin) { if u.isVaultLocked() || (u.state.Section != appstate.SectionEntries && u.state.Section != appstate.SectionRecycleBin) {
return layout.Dimensions{} return layout.Dimensions{}
@@ -4535,14 +4279,11 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
}), }),
layout.Rigid(layout.Spacer{Height: spacing}.Layout), layout.Rigid(layout.Spacer{Height: spacing}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.state.Section == appstate.SectionAbout {
return layout.Dimensions{}
}
if u.mode == "phone" { if u.mode == "phone" {
gtx.Constraints.Min.X = gtx.Constraints.Max.X gtx.Constraints.Min.X = gtx.Constraints.Max.X
} }
return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions { return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions {
editor := material.Editor(u.theme, &u.search, u.searchPlaceholder()) editor := material.Editor(u.theme, &u.search, "Search vault")
editor.Color = u.theme.Palette.Fg editor.Color = u.theme.Palette.Fg
editor.HintColor = mutedColor editor.HintColor = mutedColor
return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout) return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout)
@@ -4550,9 +4291,6 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
}), }),
layout.Rigid(layout.Spacer{Height: spacing}.Layout), layout.Rigid(layout.Spacer{Height: spacing}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.state.Section == appstate.SectionAbout {
return layout.Dimensions{}
}
if u.isVaultLocked() { if u.isVaultLocked() {
return layout.Dimensions{} return layout.Dimensions{}
} }
@@ -4578,9 +4316,6 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
if u.state.Section == appstate.SectionAPIAudit { if u.state.Section == appstate.SectionAPIAudit {
return u.apiAuditListPanel(gtx) return u.apiAuditListPanel(gtx)
} }
if u.state.Section == appstate.SectionAbout {
return layout.Dimensions{}
}
if len(u.visible) == 0 { if len(u.visible) == 0 {
return emptyStatePanel(gtx, u.theme, u.listEmptyState()) return emptyStatePanel(gtx, u.theme, u.listEmptyState())
} }
@@ -4596,23 +4331,13 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
func (u *ui) navigationHeader(gtx layout.Context) layout.Dimensions { func (u *ui) navigationHeader(gtx layout.Context) layout.Dimensions {
if u.mode == "phone" { if u.mode == "phone" {
if u.state.Section != appstate.SectionEntries && u.state.Section != appstate.SectionAbout { if u.state.Section != appstate.SectionEntries {
return layout.Dimensions{} return layout.Dimensions{}
} }
if u.state.Section == appstate.SectionAbout {
lbl := material.Label(u.theme, unit.Sp(18), "About")
lbl.Color = accentColor
return lbl.Layout(gtx)
}
return u.groupControlsDisclosure(gtx) return u.groupControlsDisclosure(gtx)
} }
return layout.Flex{Alignment: layout.Middle}.Layout(gtx, return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
if u.state.Section == appstate.SectionAbout {
lbl := material.Label(u.theme, unit.Sp(18), "About")
lbl.Color = accentColor
return lbl.Layout(gtx)
}
return u.sectionBar(gtx) return u.sectionBar(gtx)
}), }),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
@@ -4887,6 +4612,7 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions {
_ = panel _ = panel
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild {
if u.isVaultLocked() { if u.isVaultLocked() {
summary := u.currentVaultSummary()
return []layout.FlexChild{ return []layout.FlexChild{
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(18), "Unlock Vault") lbl := material.Label(u.theme, unit.Sp(18), "Unlock Vault")
@@ -4899,6 +4625,47 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions {
lbl.Color = mutedColor lbl.Color = mutedColor
return lbl.Layout(gtx) return lbl.Layout(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if strings.TrimSpace(summary.Title) == "" {
return layout.Dimensions{}
}
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(11), "UNLOCK TARGET")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(15), summary.Title)
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if strings.TrimSpace(summary.Detail) == "" || summary.Detail == summary.Title {
return layout.Dimensions{}
}
return layout.Inset{Top: unit.Dp(2)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), summary.Detail)
lbl.Color = mutedColor
return lbl.Layout(gtx)
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if strings.TrimSpace(summary.Context) == "" {
return layout.Dimensions{}
}
return layout.Inset{Top: unit.Dp(2)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), summary.Context)
lbl.Color = mutedColor
return lbl.Layout(gtx)
})
}),
)
})
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
layout.Rigid(u.unlockPanel), layout.Rigid(u.unlockPanel),
} }
@@ -4913,11 +4680,6 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions {
layout.Flexed(1, u.apiAuditDetailPanel), layout.Flexed(1, u.apiAuditDetailPanel),
} }
} }
if u.state.Section == appstate.SectionAbout {
return []layout.FlexChild{
layout.Flexed(1, u.aboutDetailPanel),
}
}
item, ok := u.selectedEntry() item, ok := u.selectedEntry()
if !ok && !u.editingEntry { if !ok && !u.editingEntry {
return []layout.FlexChild{ return []layout.FlexChild{
@@ -5630,11 +5392,21 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
if atRoot { if atRoot {
u.phoneGroupBrowserExpanded = true u.phoneGroupBrowserExpanded = true
} }
children := make([]layout.FlexChild, 0, len(groups)) return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
for i := range groups { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if len(groups) == 0 {
return layout.Dimensions{}
}
maxY := gtx.Dp(unit.Dp(168))
if gtx.Constraints.Max.Y > maxY {
gtx.Constraints.Max.Y = maxY
}
if gtx.Constraints.Min.Y > gtx.Constraints.Max.Y {
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
}
return material.List(u.theme, &u.groupList).Layout(gtx, len(groups), func(gtx layout.Context, i int) layout.Dimensions {
idx := i idx := i
name := groups[i] name := groups[i]
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
for u.groupClicks[idx].Clicked(gtx) { for u.groupClicks[idx].Clicked(gtx) {
u.state.EnterGroup(name) u.state.EnterGroup(name)
@@ -5643,9 +5415,9 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
} }
return tonedButton(gtx, u.theme, &u.groupClicks[idx], name) return tonedButton(gtx, u.theme, &u.groupClicks[idx], name)
}) })
})) })
} }),
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...) )
} }
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
@@ -5969,6 +5741,10 @@ func recyclePathCard(gtx layout.Context, th *material.Theme, title, body string)
}) })
} }
func (u *ui) recycleBinSectionNotice(gtx layout.Context) layout.Dimensions {
return recyclePathCard(gtx, u.theme, "Recycle Bin", "Deleted entries are separated from normal browsing so you can review or restore them safely.")
}
func recycleDetailTitle(gtx layout.Context, th *material.Theme, title string) layout.Dimensions { func recycleDetailTitle(gtx layout.Context, th *material.Theme, title string) layout.Dimensions {
return layout.Background{}.Layout(gtx, fill(color.NRGBA{R: 247, G: 239, B: 231, A: 255}), func(gtx layout.Context) layout.Dimensions { return layout.Background{}.Layout(gtx, fill(color.NRGBA{R: 247, G: 239, B: 231, A: 255}), func(gtx layout.Context) layout.Dimensions {
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
@@ -6074,9 +5850,7 @@ func run(w *app.Window, mode string, paths statePaths, grpcAddr string) error {
var ops op.Ops var ops op.Ops
manager := &session.Manager{} manager := &session.Manager{}
ui := newUIWithSession(mode, manager, paths) ui := newUIWithSession(mode, manager, paths)
ui.fileExplorer = explorer.NewExplorer(w)
ui.invalidate = w.Invalidate ui.invalidate = w.Invalidate
ui.clipboardWriter = newPlatformClipboardWriter(runtime.GOOS, w.Invalidate)
host, err := api.StartHost(grpcAddr, manager, passwords.DefaultProfiles(), ui.clipboardWriter, func() bool { return ui.state.Dirty }) host, err := api.StartHost(grpcAddr, manager, passwords.DefaultProfiles(), ui.clipboardWriter, func() bool { return ui.state.Dirty })
if err != nil { if err != nil {
ui.state.ErrorMessage = fmt.Sprintf("start gRPC API: %v", err) ui.state.ErrorMessage = fmt.Sprintf("start gRPC API: %v", err)
@@ -6089,7 +5863,6 @@ func run(w *app.Window, mode string, paths statePaths, grpcAddr string) error {
} }
for { for {
e := w.Event() e := w.Event()
ui.fileExplorer.ListenEvents(e)
switch e := e.(type) { switch e := e.(type) {
case app.DestroyEvent: case app.DestroyEvent:
return e.Err return e.Err
@@ -6097,7 +5870,6 @@ func run(w *app.Window, mode string, paths statePaths, grpcAddr string) error {
gtx := app.NewContext(&ops, e) gtx := app.NewContext(&ops, e)
ui.processBackgroundActions() ui.processBackgroundActions()
ui.layout(gtx) ui.layout(gtx)
processClipboardWrites(gtx, ui.clipboardWriter)
e.Frame(gtx.Ops) e.Frame(gtx.Ops)
} }
} }
+279 -722
View File
File diff suppressed because it is too large Load Diff
@@ -1,26 +0,0 @@
pkgbase = keepassgo-git
pkgdesc = KeePass-compatible password manager written in Go
pkgver = r160.5fa79bd
pkgrel = 1
url = https://git.julianfamily.org/joejulian/keepassgo
arch = x86_64
arch = aarch64
license = custom
makedepends = git
makedepends = go
makedepends = pkgconf
depends = glibc
depends = hicolor-icon-theme
depends = libx11
depends = libxcursor
depends = libxfixes
depends = libxkbcommon
depends = libxkbcommon-x11
depends = mesa
depends = wayland
provides = keepassgo
conflicts = keepassgo
source = git+https://git.julianfamily.org/joejulian/keepassgo.git
sha256sums = SKIP
pkgname = keepassgo-git
@@ -1,65 +0,0 @@
pkgname=keepassgo-git
pkgver=r0.0000000
pkgrel=1
pkgdesc='KeePass-compatible password manager written in Go'
arch=('x86_64' 'aarch64')
url='https://git.julianfamily.org/joejulian/keepassgo'
license=('custom')
depends=(
'glibc'
'hicolor-icon-theme'
'libx11'
'libxcursor'
'libxfixes'
'libxkbcommon'
'libxkbcommon-x11'
'mesa'
'wayland'
)
makedepends=(
'git'
'go'
'pkgconf'
)
provides=('keepassgo')
conflicts=('keepassgo')
source=('git+https://git.julianfamily.org/joejulian/keepassgo.git')
sha256sums=('SKIP')
_repo_dir() {
if [[ -d "${srcdir}/keepassgo/.git" ]]; then
printf '%s\n' "${srcdir}/keepassgo"
return
fi
cd "${startdir}/../../.." || exit 1
pwd
}
pkgver() {
cd "$(_repo_dir)"
printf 'r%s.%s' "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
build() {
cd "$(_repo_dir)"
export CGO_ENABLED=1
export GOFLAGS="-trimpath"
local app_version
app_version="$(git describe --tags --always --dirty)"
go build -ldflags "-X main.appVersion=${app_version}" -o keepassgo .
}
package() {
cd "$(_repo_dir)"
install -Dm755 keepassgo "${pkgdir}/usr/bin/keepassgo"
install -Dm644 assets/keepassgo-icon.png \
"${pkgdir}/usr/share/icons/hicolor/512x512/apps/keepassgo.png"
install -Dm644 assets/keepassgo-icon.svg \
"${pkgdir}/usr/share/icons/hicolor/scalable/apps/keepassgo.svg"
install -Dm644 packaging/archlinux/keepassgo-git/keepassgo.desktop \
"${pkgdir}/usr/share/applications/keepassgo.desktop"
install -Dm644 README.md \
"${pkgdir}/usr/share/licenses/${pkgname}/README.md"
}
@@ -1,10 +0,0 @@
[Desktop Entry]
Type=Application
Name=KeePassGO
Comment=KeePass-compatible password manager
Exec=keepassgo
Icon=keepassgo
Terminal=false
Categories=Utility;Security;
Keywords=keepass;password;vault;kdbx;
StartupNotify=true
-95
View File
@@ -1,95 +0,0 @@
#!/usr/bin/env python3
import argparse
import json
import mimetypes
import pathlib
import sys
import urllib.error
import urllib.parse
import urllib.request
def request_json(method: str, url: str, token: str, payload: dict | None = None) -> dict:
data = None
headers = {"Authorization": f"token {token}", "Accept": "application/json"}
if payload is not None:
data = json.dumps(payload).encode()
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read().decode())
def request_no_content(method: str, url: str, token: str) -> None:
req = urllib.request.Request(
url,
headers={"Authorization": f"token {token}", "Accept": "application/json"},
method=method,
)
with urllib.request.urlopen(req):
return
def get_or_create_release(server: str, repo: str, token: str, tag: str) -> dict:
base = f"{server.rstrip('/')}/api/v1/repos/{repo}"
tag_url = f"{base}/releases/tags/{urllib.parse.quote(tag, safe='')}"
try:
return request_json("GET", tag_url, token)
except urllib.error.HTTPError as err:
if err.code != 404:
raise
payload = {
"tag_name": tag,
"name": tag,
"draft": False,
"prerelease": False,
}
return request_json("POST", f"{base}/releases", token, payload)
def upload_asset(server: str, repo: str, token: str, release: dict, path: pathlib.Path) -> None:
base = f"{server.rstrip('/')}/api/v1/repos/{repo}"
assets = release.get("assets", [])
for asset in assets:
if asset.get("name") == path.name:
request_no_content("DELETE", f"{base}/releases/{release['id']}/assets/{asset['id']}", token)
query = urllib.parse.urlencode({"name": path.name})
url = f"{base}/releases/{release['id']}/assets?{query}"
content_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
req = urllib.request.Request(
url,
data=path.read_bytes(),
headers={
"Authorization": f"token {token}",
"Accept": "application/json",
"Content-Type": content_type,
},
method="POST",
)
with urllib.request.urlopen(req):
return
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--server", required=True)
parser.add_argument("--repo", required=True)
parser.add_argument("--token", required=True)
parser.add_argument("--tag", required=True)
parser.add_argument("artifacts", nargs="+")
args = parser.parse_args()
paths = [pathlib.Path(p) for p in args.artifacts]
missing = [str(p) for p in paths if not p.is_file()]
if missing:
print(f"missing artifacts: {', '.join(missing)}", file=sys.stderr)
return 1
release = get_or_create_release(args.server, args.repo, args.token, args.tag)
for path in paths:
upload_asset(args.server, args.repo, args.token, release, path)
return 0
if __name__ == "__main__":
raise SystemExit(main())
-24
View File
@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@@ -178,18 +177,6 @@ func (m *Manager) SynchronizeFromLocal(path string) error {
return m.persistMergedToCurrentSource(merged) return m.persistMergedToCurrentSource(merged)
} }
func (m *Manager) SynchronizeFromLocalBytes(name string, content []byte) error {
other, _, err := loadLocalSourceBytes(name, content, m.key)
if err != nil {
return err
}
merged, err := m.mergedWithPeer(other)
if err != nil {
return err
}
return m.persistMergedToCurrentSource(merged)
}
func (m *Manager) SynchronizeToLocal(path string) error { func (m *Manager) SynchronizeToLocal(path string) error {
other, config, err := loadLocalSourceOrEmpty(path, m.key) other, config, err := loadLocalSourceOrEmpty(path, m.key)
if err != nil { if err != nil {
@@ -921,17 +908,6 @@ func loadLocalSource(path string, key vault.MasterKey) (vault.Model, *vault.KDBX
return model, config, nil return model, config, nil
} }
func loadLocalSourceBytes(name string, content []byte, key vault.MasterKey) (vault.Model, *vault.KDBXConfig, error) {
if len(content) == 0 {
return vault.Model{}, nil, fmt.Errorf("open %s for synchronize: %w", name, io.EOF)
}
model, config, err := vault.LoadKDBXWithConfig(bytes.NewReader(content), key)
if err != nil {
return vault.Model{}, nil, fmt.Errorf("decode %s for synchronize: %w", name, err)
}
return model, config, nil
}
func loadLocalSourceOrEmpty(path string, key vault.MasterKey) (vault.Model, *vault.KDBXConfig, error) { func loadLocalSourceOrEmpty(path string, key vault.MasterKey) (vault.Model, *vault.KDBXConfig, error) {
model, config, err := loadLocalSource(path, key) model, config, err := loadLocalSource(path, key)
if err == nil { if err == nil {
+83 -140
View File
@@ -24,10 +24,10 @@ func TestCreateSaveAsLockAndUnlockRoundTripsVault(t *testing.T) {
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "entry-1", ID: "entry-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
}, },
@@ -65,8 +65,8 @@ func TestCreateSaveAsLockAndUnlockRoundTripsVault(t *testing.T) {
} }
got := current.EntriesInPath([]string{"Root", "Internet"}) got := current.EntriesInPath([]string{"Root", "Internet"})
if len(got) != 1 || got[0].Title != "Vault Console" || got[0].Password != "token-1" { if len(got) != 1 || got[0].Title != "Git Server" || got[0].Password != "token-1" {
t.Fatalf("Current() entries = %#v, want persisted Vault Console entry", got) t.Fatalf("Current() entries = %#v, want persisted Git Server entry", got)
} }
} }
@@ -78,10 +78,10 @@ func TestOpenLoadsExistingKDBXFromDisk(t *testing.T) {
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "entry-1", ID: "entry-1",
Title: "Surveillance Console", Title: "Home Assistant (Codex)",
Username: "codex", Username: "codex",
Password: "token-2", Password: "token-2",
URL: "https://surveillance.crew.example.invalid", URL: "https://lights.julianfamily.org",
Path: []string{"Root", "Home Assistant"}, Path: []string{"Root", "Home Assistant"},
}, },
}, },
@@ -124,10 +124,10 @@ func TestSavePersistsEditsBackToCurrentPath(t *testing.T) {
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "entry-1", ID: "entry-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
}, },
@@ -146,10 +146,10 @@ func TestSavePersistsEditsBackToCurrentPath(t *testing.T) {
updated := model updated := model
updated.UpsertEntry(vault.Entry{ updated.UpsertEntry(vault.Entry{
ID: "entry-1", ID: "entry-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-2", Password: "token-2",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}) })
sess.Replace(updated) sess.Replace(updated)
@@ -186,19 +186,19 @@ func TestSaveReparentsMixedPathsUnderSingleVaultRoot(t *testing.T) {
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "entry-1", ID: "entry-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"keepass", "Crew", "Internet"}, Path: []string{"keepass", "Joe", "Internet"},
}, },
{ {
ID: "entry-2", ID: "entry-2",
Title: "Mail", Title: "Mail",
Username: "dannyocean", Username: "joejulian",
Password: "token-2", Password: "token-2",
URL: "https://dispatch.crew.example.invalid", URL: "https://mail.julianfamily.org",
Path: []string{"keepass", "Crew", "eMail"}, Path: []string{"keepass", "Joe", "eMail"},
}, },
}, },
}, key.Password); err != nil { }, key.Password); err != nil {
@@ -217,8 +217,8 @@ func TestSaveReparentsMixedPathsUnderSingleVaultRoot(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Current() error = %v", err) t.Fatalf("Current() error = %v", err)
} }
current.Entries[0].Path = []string{"Crew", "Internet"} current.Entries[0].Path = []string{"Joe", "Internet"}
current.Groups = append(current.Groups, []string{"Crew"}, []string{"Crew", "Internet"}, []string{"Crew", "eMail"}) current.Groups = append(current.Groups, []string{"Joe"}, []string{"Joe", "Internet"}, []string{"Joe", "eMail"})
sess.Replace(current) sess.Replace(current)
if err := sess.Save(); err != nil { if err := sess.Save(); err != nil {
@@ -244,8 +244,8 @@ func TestSaveReparentsMixedPathsUnderSingleVaultRoot(t *testing.T) {
t.Fatalf("top-level groups = %#v, want single keepass root", db.Content.Root.Groups) t.Fatalf("top-level groups = %#v, want single keepass root", db.Content.Root.Groups)
} }
rootGroups := db.Content.Root.Groups[0].Groups rootGroups := db.Content.Root.Groups[0].Groups
if len(rootGroups) != 1 || rootGroups[0].Name != "Crew" { if len(rootGroups) != 1 || rootGroups[0].Name != "Joe" {
t.Fatalf("keepass child groups = %#v, want single Crew group", rootGroups) t.Fatalf("keepass child groups = %#v, want single Joe group", rootGroups)
} }
} }
@@ -271,10 +271,10 @@ func TestOpenRemoteLoadsExistingKDBXFromWebDAV(t *testing.T) {
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "entry-1", ID: "entry-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
}, },
@@ -309,7 +309,7 @@ func TestOpenRemoteLoadsExistingKDBXFromWebDAV(t *testing.T) {
got := current.EntriesInPath([]string{"Root", "Internet"}) got := current.EntriesInPath([]string{"Root", "Internet"})
if len(got) != 1 || got[0].Password != "token-1" { if len(got) != 1 || got[0].Password != "token-1" {
t.Fatalf("Current() entries = %#v, want Vault Console entry from remote vault", got) t.Fatalf("Current() entries = %#v, want Git Server entry from remote vault", got)
} }
} }
@@ -321,10 +321,10 @@ func TestSaveRemotePersistsEditsBackToWebDAV(t *testing.T) {
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "entry-1", ID: "entry-1",
Title: "Surveillance Console", Title: "Home Assistant (Codex)",
Username: "codex", Username: "codex",
Password: "token-1", Password: "token-1",
URL: "https://surveillance.crew.example.invalid", URL: "https://lights.julianfamily.org",
Path: []string{"Root", "Home Assistant"}, Path: []string{"Root", "Home Assistant"},
}, },
}, },
@@ -371,10 +371,10 @@ func TestSaveRemotePersistsEditsBackToWebDAV(t *testing.T) {
} }
current.UpsertEntry(vault.Entry{ current.UpsertEntry(vault.Entry{
ID: "entry-1", ID: "entry-1",
Title: "Surveillance Console", Title: "Home Assistant (Codex)",
Username: "codex", Username: "codex",
Password: "token-2", Password: "token-2",
URL: "https://surveillance.crew.example.invalid", URL: "https://lights.julianfamily.org",
Path: []string{"Root", "Home Assistant"}, Path: []string{"Root", "Home Assistant"},
}) })
sess.Replace(current) sess.Replace(current)
@@ -406,10 +406,10 @@ func TestSaveUsesRemoteTargetWhenVaultWasOpenedFromWebDAV(t *testing.T) {
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "entry-1", ID: "entry-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
}, },
@@ -448,10 +448,10 @@ func TestSaveUsesRemoteTargetWhenVaultWasOpenedFromWebDAV(t *testing.T) {
} }
current.UpsertEntry(vault.Entry{ current.UpsertEntry(vault.Entry{
ID: "entry-1", ID: "entry-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-2", Password: "token-2",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}) })
sess.Replace(current) sess.Replace(current)
@@ -477,10 +477,10 @@ func TestChangeMasterKeyReencryptsSavedAndLockedVault(t *testing.T) {
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "entry-1", ID: "entry-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
}, },
@@ -514,8 +514,8 @@ func TestChangeMasterKeyReencryptsSavedAndLockedVault(t *testing.T) {
t.Fatalf("Current() error = %v", err) t.Fatalf("Current() error = %v", err)
} }
got := current.EntriesInPath([]string{"Root", "Internet"}) got := current.EntriesInPath([]string{"Root", "Internet"})
if len(got) != 1 || got[0].Title != "Vault Console" { if len(got) != 1 || got[0].Title != "Git Server" {
t.Fatalf("Current() entries = %#v, want Vault Console entry after ChangeMasterKey", got) t.Fatalf("Current() entries = %#v, want Git Server entry after ChangeMasterKey", got)
} }
var reopened Manager var reopened Manager
@@ -540,10 +540,10 @@ func TestSavePreservesOpenedKDBXSecuritySettings(t *testing.T) {
{ {
UUID: gokeepasslib.NewUUID(), UUID: gokeepasslib.NewUUID(),
Values: []gokeepasslib.ValueData{ Values: []gokeepasslib.ValueData{
{Key: "Title", Value: gokeepasslib.V{Content: "Vault Console"}}, {Key: "Title", Value: gokeepasslib.V{Content: "Git Server"}},
{Key: "UserName", Value: gokeepasslib.V{Content: "dannyocean"}}, {Key: "UserName", Value: gokeepasslib.V{Content: "joejulian"}},
{Key: "Password", Value: gokeepasslib.V{Content: "token-1", Protected: w.NewBoolWrapper(true)}}, {Key: "Password", Value: gokeepasslib.V{Content: "token-1", Protected: w.NewBoolWrapper(true)}},
{Key: "URL", Value: gokeepasslib.V{Content: "https://vault.crew.example.invalid"}}, {Key: "URL", Value: gokeepasslib.V{Content: "https://git.julianfamily.org"}},
}, },
}, },
}, },
@@ -577,10 +577,10 @@ func TestSavePreservesOpenedKDBXSecuritySettings(t *testing.T) {
} }
current.UpsertEntry(vault.Entry{ current.UpsertEntry(vault.Entry{
ID: current.Entries[0].ID, ID: current.Entries[0].ID,
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-2", Password: "token-2",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: current.Entries[0].Path, Path: current.Entries[0].Path,
}) })
sess.Replace(current) sess.Replace(current)
@@ -620,10 +620,10 @@ func TestRemoteSaveAndReopenPreservesCrossFeatureState(t *testing.T) {
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "entry-1", ID: "entry-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-2", Password: "token-2",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
Attachments: map[string][]byte{ Attachments: map[string][]byte{
"token.txt": []byte("secret attachment contents"), "token.txt": []byte("secret attachment contents"),
@@ -631,10 +631,10 @@ func TestRemoteSaveAndReopenPreservesCrossFeatureState(t *testing.T) {
History: []vault.Entry{ History: []vault.Entry{
{ {
ID: "entry-1-history-1", ID: "entry-1-history-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
}, },
@@ -778,10 +778,10 @@ func TestSynchronizeRemotePreservesOverwrittenRemoteVariantInHistory(t *testing.
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "entry-1", ID: "entry-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
}, },
@@ -841,10 +841,10 @@ func TestSynchronizeRemotePreservesOverwrittenRemoteVariantInHistory(t *testing.
} }
firstCurrent.UpsertEntry(vault.Entry{ firstCurrent.UpsertEntry(vault.Entry{
ID: "entry-1", ID: "entry-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "remote-token-2", Password: "remote-token-2",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Notes: "updated remotely first", Notes: "updated remotely first",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}) })
@@ -859,10 +859,10 @@ func TestSynchronizeRemotePreservesOverwrittenRemoteVariantInHistory(t *testing.
} }
secondCurrent.UpsertEntry(vault.Entry{ secondCurrent.UpsertEntry(vault.Entry{
ID: "entry-1", ID: "entry-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "local-token-2", Password: "local-token-2",
URL: "https://vault.crew.example.invalid/security/badges", URL: "https://git.julianfamily.org/settings/tokens",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}) })
second.Replace(secondCurrent) second.Replace(secondCurrent)
@@ -883,7 +883,7 @@ func TestSynchronizeRemotePreservesOverwrittenRemoteVariantInHistory(t *testing.
if len(got) != 1 { if len(got) != 1 {
t.Fatalf("len(EntriesInPath(Root/Internet)) = %d, want 1", len(got)) t.Fatalf("len(EntriesInPath(Root/Internet)) = %d, want 1", len(got))
} }
if got[0].Password != "local-token-2" || got[0].URL != "https://vault.crew.example.invalid/security/badges" { if got[0].Password != "local-token-2" || got[0].URL != "https://git.julianfamily.org/settings/tokens" {
t.Fatalf("entry after synchronize = %#v, want local winning version", got[0]) t.Fatalf("entry after synchronize = %#v, want local winning version", got[0])
} }
if len(got[0].History) == 0 { if len(got[0].History) == 0 {
@@ -905,10 +905,10 @@ func TestSynchronizeFromLocalMergesOtherVaultIntoCurrentSource(t *testing.T) {
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "entry-current", ID: "entry-current",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-current", Password: "token-current",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
}, },
@@ -917,10 +917,10 @@ func TestSynchronizeFromLocalMergesOtherVaultIntoCurrentSource(t *testing.T) {
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "entry-other", ID: "entry-other",
Title: "Bellagio", Title: "Dynadot",
Username: "rustyryan", Username: "jjulian",
Password: "token-other", Password: "token-other",
URL: "https://bellagio.example.invalid", URL: "https://www.dynadot.com",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
}, },
@@ -953,63 +953,6 @@ func TestSynchronizeFromLocalMergesOtherVaultIntoCurrentSource(t *testing.T) {
} }
} }
func TestSynchronizeFromLocalBytesMergesOtherVaultIntoCurrentSource(t *testing.T) {
t.Parallel()
key := vault.MasterKey{Password: "correct horse battery staple"}
currentPath := filepath.Join(t.TempDir(), "current.kdbx")
currentModel := vault.Model{
Entries: []vault.Entry{{
ID: "entry-current",
Title: "Vault Console",
Username: "dannyocean",
Password: "token-current",
URL: "https://vault.crew.example.invalid",
Path: []string{"Root", "Internet"},
}},
}
otherModel := vault.Model{
Entries: []vault.Entry{{
ID: "entry-other",
Title: "Bellagio",
Username: "rustyryan",
Password: "token-other",
URL: "https://bellagio.example.invalid",
Path: []string{"Root", "Internet"},
}},
}
writeKDBXTestFile(t, currentPath, currentModel, key)
var other bytes.Buffer
if err := vault.SaveKDBX(&other, otherModel, key.Password); err != nil {
t.Fatalf("SaveKDBX(other) error = %v", err)
}
var sess Manager
if err := sess.Open(currentPath, key); err != nil {
t.Fatalf("Open(current) error = %v", err)
}
if err := sess.SynchronizeFromLocalBytes("picked-other.kdbx", other.Bytes()); err != nil {
t.Fatalf("SynchronizeFromLocalBytes() error = %v", err)
}
var reopened Manager
if err := reopened.Open(currentPath, key); err != nil {
t.Fatalf("reopen Open(current) error = %v", err)
}
current, err := reopened.Current()
if err != nil {
t.Fatalf("reopened Current() error = %v", err)
}
got := current.EntriesInPath([]string{"Root", "Internet"})
if len(got) != 2 {
t.Fatalf("len(EntriesInPath(Root/Internet)) = %d, want 2", len(got))
}
}
func TestSynchronizeToLocalWritesMergedVaultToTarget(t *testing.T) { func TestSynchronizeToLocalWritesMergedVaultToTarget(t *testing.T) {
t.Parallel() t.Parallel()
@@ -1021,10 +964,10 @@ func TestSynchronizeToLocalWritesMergedVaultToTarget(t *testing.T) {
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "entry-current", ID: "entry-current",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-current", Password: "token-current",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
}, },
@@ -1033,10 +976,10 @@ func TestSynchronizeToLocalWritesMergedVaultToTarget(t *testing.T) {
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "entry-other", ID: "entry-other",
Title: "Bellagio", Title: "Dynadot",
Username: "rustyryan", Username: "jjulian",
Password: "token-other", Password: "token-other",
URL: "https://bellagio.example.invalid", URL: "https://www.dynadot.com",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
}, },
@@ -1078,10 +1021,10 @@ func TestSynchronizeToRemoteWritesMergedVaultToTarget(t *testing.T) {
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "entry-current", ID: "entry-current",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-current", Password: "token-current",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
}, },
@@ -1090,10 +1033,10 @@ func TestSynchronizeToRemoteWritesMergedVaultToTarget(t *testing.T) {
Entries: []vault.Entry{ Entries: []vault.Entry{
{ {
ID: "entry-remote", ID: "entry-remote",
Title: "Bellagio", Title: "Dynadot",
Username: "rustyryan", Username: "jjulian",
Password: "token-remote", Password: "token-remote",
URL: "https://bellagio.example.invalid", URL: "https://www.dynadot.com",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
}, },
+1 -1
View File
@@ -471,7 +471,7 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe
<uses-sdk android:minSdkVersion="{{.MinSDK}}" android:targetSdkVersion="{{.TargetSDK}}" /> <uses-sdk android:minSdkVersion="{{.MinSDK}}" android:targetSdkVersion="{{.TargetSDK}}" />
{{range .Permissions}} <uses-permission android:name="{{.}}"/> {{range .Permissions}} <uses-permission android:name="{{.}}"/>
{{end}}{{range .Features}} <uses-feature android:{{.}} android:required="false"/> {{end}}{{range .Features}} <uses-feature android:{{.}} android:required="false"/>
{{end}}{{.ManifestSnip}} <application {{.IconSnip}} android:label="{{.AppName}}" android:enableOnBackInvokedCallback="false"> {{end}}{{.ManifestSnip}} <application {{.IconSnip}} android:label="{{.AppName}}">
{{.AppSnip}} {{.AppSnip}}
<activity android:name="org.gioui.GioActivity" <activity android:name="org.gioui.GioActivity"
android:label="{{.AppName}}" android:label="{{.AppName}}"
+3 -89
View File
@@ -369,57 +369,6 @@ func (u *ui) addAPIPolicyRuleAction() error {
return nil return nil
} }
func (u *ui) apiPolicyGroupPathSummary() string {
path := parsePath(u.apiPolicyPath.Text())
if len(path) == 0 {
return "No group selected"
}
return strings.Join(path, " / ")
}
func (u *ui) apiPolicyEntrySummary() string {
id := strings.TrimSpace(u.apiPolicyEntryID.Text())
if id == "" {
return "No entry selected"
}
if item, ok := u.selectedEntry(); ok && item.ID == id {
if strings.TrimSpace(item.Title) != "" {
return item.Title + " (" + id + ")"
}
}
return id
}
func (u *ui) useCurrentGroupForPolicyAction() error {
u.syncCurrentPath()
path := u.displayPath()
if len(path) == 0 {
return fmt.Errorf("navigate to a group first")
}
u.apiPolicyGroupScope = true
u.apiPolicyGroupScopeW.Value = true
u.apiPolicyPath.SetText(strings.Join(path, " / "))
u.apiPolicyEntryID.SetText("")
return nil
}
func (u *ui) useSelectedEntryForPolicyAction() error {
item, ok := u.selectedEntry()
if !ok || strings.TrimSpace(item.ID) == "" {
return fmt.Errorf("select an entry first")
}
u.apiPolicyGroupScope = false
u.apiPolicyGroupScopeW.Value = false
u.apiPolicyEntryID.SetText(item.ID)
return nil
}
func (u *ui) clearAPIPolicyTargetAction() error {
u.apiPolicyPath.SetText("")
u.apiPolicyEntryID.SetText("")
return nil
}
func (u *ui) removeAPIPolicyRuleAction(index int) error { func (u *ui) removeAPIPolicyRuleAction(index int) error {
token, ok := u.selectedAPIToken() token, ok := u.selectedAPIToken()
if !ok { if !ok {
@@ -1053,44 +1002,9 @@ func (u *ui) apiTokenDetailPanel(gtx layout.Context) layout.Dimensions {
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(labeledEditorHelp(u.theme, "Operation", "Valid operations: "+strings.Join(stringOps(apiOperations()), ", "), &u.apiPolicyOperation, false)), layout.Rigid(labeledEditorHelp(u.theme, "Operation", "Valid operations: "+strings.Join(stringOps(apiOperations()), ", "), &u.apiPolicyOperation, false)),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(labeledEditorHelp(u.theme, "Group Path", "Used when group scope is enabled.", &u.apiPolicyPath, false)),
if u.apiPolicyGroupScopeW.Value { layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { layout.Rigid(labeledEditorHelp(u.theme, "Entry ID", "Used when group scope is disabled.", &u.apiPolicyEntryID, false)),
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(detailLine(u.theme, "Group Path", u.apiPolicyGroupPathSummary())),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.useCurrentGroupForPolicy, "Use Current Group")
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.clearAPIPolicyTarget, "Clear")
}),
)
}),
)
})
}
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(detailLine(u.theme, "Entry", u.apiPolicyEntrySummary())),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.useSelectedEntryForPolicy, "Use Selected Entry")
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.clearAPIPolicyTarget, "Clear")
}),
)
}),
)
})
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.addAPIPolicyRule, "Add Rule") return tonedButton(gtx, u.theme, &u.addAPIPolicyRule, "Add Rule")
+108 -264
View File
@@ -20,9 +20,6 @@ import (
func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
busy := u.lifecycleBusy() busy := u.lifecycleBusy()
showLocalChooser := u.showLocalVaultChooser()
showRemoteChooser := u.showRemoteConnectionChooser()
selectedLocalPath := strings.TrimSpace(u.vaultPath.Text())
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "OPEN A VAULT") lbl := material.Label(u.theme, unit.Sp(12), "OPEN A VAULT")
@@ -62,173 +59,174 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
if u.lifecycleMode == "remote" { if u.lifecycleMode == "remote" {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(12), "LOCATION") lbl := material.Label(u.theme, unit.Sp(12), "LOCATION")
lbl.Color = mutedColor lbl.Color = mutedColor
return lbl.Layout(gtx) return lbl.Layout(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(labeledEditorHelp(u.theme, "Remote Base URL", "Base WebDAV endpoint, for example https://server/remote.php/webdav.", &u.remoteBaseURL, false)),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(labeledEditorHelp(u.theme, "Remote Path", "Path to the remote .kdbx file under the WebDAV base URL.", &u.remotePath, false)),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser { if strings.TrimSpace(u.remoteBaseURL.Text()) == "" || strings.TrimSpace(u.remotePath.Text()) == "" {
return layout.Dimensions{} return layout.Dimensions{}
} }
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx) record := u.currentRemoteRecord()
lastGroup := u.recentRemoteGroup(record.BaseURL, record.Path)
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "SELECTED CONNECTION")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser { lbl := material.Label(u.theme, unit.Sp(14), friendlyRecentRemoteLabel(record))
return layout.Dimensions{} lbl.Color = accentColor
} return lbl.Layout(gtx)
return labeledEditorHelp(u.theme, "Remote Base URL", "Base WebDAV endpoint, for example https://server/remote.php/webdav.", &u.remoteBaseURL, false)(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser { lbl := material.Label(u.theme, unit.Sp(11), "Path: "+strings.TrimSpace(record.Path))
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return labeledEditorHelp(u.theme, "Remote Path", "Path to the remote .kdbx file under the WebDAV base URL.", &u.remotePath, false)(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if showRemoteChooser || !u.hasSelectedRemoteTarget() {
return layout.Dimensions{}
}
return layout.Dimensions{}
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if showRemoteChooser && !busy {
return u.recentRemoteList(gtx)
}
return layout.Dimensions{}
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(10)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(12), "AUTHENTICATION")
lbl.Color = mutedColor lbl.Color = mutedColor
return lbl.Layout(gtx) return lbl.Layout(gtx)
}), }),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser { lbl := material.Label(u.theme, unit.Sp(11), "Server: "+strings.TrimSpace(record.BaseURL))
return layout.Dimensions{} lbl.Color = mutedColor
} return lbl.Layout(gtx)
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
}), }),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser { lbl := material.Label(u.theme, unit.Sp(11), "Auth: "+recentRemoteStoredAuthSummary(recentRemoteRecord{
return layout.Dimensions{} Username: strings.TrimSpace(u.remoteUsername.Text()),
} Password: u.remotePassword.Text(),
return labeledEditorHelp(u.theme, "Remote Username", "Username used to authenticate to the WebDAV server.", &u.remoteUsername, false)(gtx) }))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}), }),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser { if len(lastGroup) == 0 {
return layout.Dimensions{} return layout.Dimensions{}
} }
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx) lbl := material.Label(u.theme, unit.Sp(11), "Last group: "+strings.Join(u.displayEntryPath(lastGroup), " / "))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser { if busy {
return layout.Dimensions{} return layout.Dimensions{}
} }
return tonedButton(gtx, u.theme, &u.clearRemoteSelection, "Change...")
}),
)
})
})
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy {
return layout.Dimensions{}
}
return u.recentRemoteList(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "AUTHENTICATION")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(labeledEditorHelp(u.theme, "Remote Username", "Username used to authenticate to the WebDAV server.", &u.remoteUsername, false)),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return labeledEditorHelp(u.theme, "Remote Password", "Password or app token used to authenticate to the WebDAV server.", &u.remotePassword, true)(gtx) return labeledEditorHelp(u.theme, "Remote Password", "Password or app token used to authenticate to the WebDAV server.", &u.remotePassword, true)(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
box := material.CheckBox(u.theme, &u.rememberRemoteAuth, "Remember sign-in on this device") box := material.CheckBox(u.theme, &u.rememberRemoteAuth, "Remember sign-in on this device")
box.Color = accentColor box.Color = accentColor
return box.Layout(gtx) return box.Layout(gtx)
}), }),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showRemoteChooser {
return layout.Dimensions{}
}
return layout.Inset{Top: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Inset{Top: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.openRemotePrefsHelp, "Settings & Help") return tonedButton(gtx, u.theme, &u.openRemotePrefsHelp, "Settings & Help")
}) })
}), }),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
if !showRemoteChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
}),
) )
} }
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showLocalChooser {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(12), "RECENT VAULTS") lbl := material.Label(u.theme, unit.Sp(12), "RECENT VAULTS")
lbl.Color = mutedColor lbl.Color = mutedColor
return lbl.Layout(gtx) return lbl.Layout(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showLocalChooser { if busy {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showLocalChooser || busy {
return layout.Dimensions{} return layout.Dimensions{}
} }
return u.recentVaultList(gtx) return u.recentVaultList(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showLocalChooser {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showLocalChooser {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(12), "VAULT FILE") lbl := material.Label(u.theme, unit.Sp(12), "VAULT FILE")
lbl.Color = mutedColor lbl.Color = mutedColor
return lbl.Layout(gtx) return lbl.Layout(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !showLocalChooser { selectedPath := strings.TrimSpace(u.vaultPath.Text())
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
switch { switch {
case busy: case busy:
return labeledEditorHelp(u.theme, "Vault Path", localVaultPathHelp(), &u.vaultPath, false)(gtx) return labeledEditorHelp(u.theme, "Vault Path", localVaultPathHelp(), &u.vaultPath, false)(gtx)
case selectedLocalPath == "": case selectedPath == "":
return localPathSelector(u.theme, &u.vaultPath, &u.pickVaultPath)(gtx) return localPathSelector(u.theme, &u.vaultPath, &u.pickVaultPath)(gtx)
default: default:
lastGroup := u.recentVaultGroup(selectedPath)
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "SELECTED VAULT")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(16), friendlyRecentVaultLabel(selectedPath))
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
dir := compactPathDirectorySummary(selectedPath)
if dir == "" {
return layout.Dimensions{} return layout.Dimensions{}
} }
lbl := material.Label(u.theme, unit.Sp(11), dir)
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if len(lastGroup) == 0 {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(11), "Last group: "+strings.Join(u.displayEntryPath(lastGroup), " / "))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.clearVaultSelection, "Change...")
}),
)
})
})
}
}), }),
) )
}), }),
@@ -298,26 +296,10 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.lifecycleMode == "remote" { if u.lifecycleMode == "remote" {
label := u.remoteOpenButtonLabel() label := u.remoteOpenButtonLabel()
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy { if busy {
return passiveTonedButton(gtx, u.theme, label) return passiveTonedButton(gtx, u.theme, label)
} }
return tonedButton(gtx, u.theme, &u.openRemote, label) return tonedButton(gtx, u.theme, &u.openRemote, label)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy || !u.hasSelectedRemoteTarget() {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy || !u.hasSelectedRemoteTarget() {
return layout.Dimensions{}
}
return u.selectedRemoteConnectionCard(gtx)
}),
)
} }
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
@@ -343,118 +325,11 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
} }
return sectionTabButton(gtx, u.theme, &u.createVault, "Create New Vault", false) return sectionTabButton(gtx, u.theme, &u.createVault, "Create New Vault", false)
}), }),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy || selectedLocalPath == "" {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy || selectedLocalPath == "" {
return layout.Dimensions{}
}
return u.selectedLocalVaultCard(gtx, selectedLocalPath)
}),
) )
}), }),
) )
} }
func (u *ui) selectedRemoteConnectionCard(gtx layout.Context) layout.Dimensions {
record := u.currentRemoteRecord()
lastGroup := u.recentRemoteGroup(record.BaseURL, record.Path)
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "SELECTED CONNECTION")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(14), friendlyRecentRemoteLabel(record))
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(11), "Path: "+strings.TrimSpace(record.Path))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(11), "Server: "+strings.TrimSpace(record.BaseURL))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(11), "Auth: "+recentRemoteStoredAuthSummary(recentRemoteRecord{
Username: strings.TrimSpace(u.remoteUsername.Text()),
Password: u.remotePassword.Text(),
}))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if len(lastGroup) == 0 {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(11), "Last group: "+strings.Join(u.displayEntryPath(lastGroup), " / "))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.clearRemoteSelection, "Open Different Connection")
}),
)
})
})
}
func (u *ui) selectedLocalVaultCard(gtx layout.Context, path string) layout.Dimensions {
lastGroup := u.recentVaultGroup(path)
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "SELECTED VAULT")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(16), friendlyRecentVaultLabel(path))
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
dir := compactPathDirectorySummary(path)
if dir == "" {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(11), dir)
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if len(lastGroup) == 0 {
return layout.Dimensions{}
}
lbl := material.Label(u.theme, unit.Sp(11), "Last group: "+strings.Join(u.displayEntryPath(lastGroup), " / "))
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.clearVaultSelection, "Open Different Vault")
}),
)
})
})
}
func (u *ui) lifecycleSecuritySettingsSummary() string { func (u *ui) lifecycleSecuritySettingsSummary() string {
return "Cipher and KDF now live in Vault Settings so opening and creating a vault stays focused on the file, key material, and sync choices." return "Cipher and KDF now live in Vault Settings so opening and creating a vault stays focused on the file, key material, and sync choices."
} }
@@ -1034,7 +909,7 @@ func (u *ui) groupControlsDisclosure(gtx layout.Context) layout.Dimensions {
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
icon := u.expandLessIcon icon := u.expandLessIcon
if u.groupControlsHidden { if u.groupControlsHidden {
icon = u.chevronRightIcon icon = u.expandMoreIcon
} }
if icon == nil { if icon == nil {
lbl := material.Label(u.theme, unit.Sp(16), ">") lbl := material.Label(u.theme, unit.Sp(16), ">")
@@ -1337,14 +1212,12 @@ func selectorEditorHelp(th *material.Theme, label, help string, editor *widget.E
func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions { func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
targetLabel := "Locked vault" targetLabel := "Locked vault"
targetValue := "Unlock the active vault to continue." targetValue := "Unlock the active vault to continue."
changeLabel := "Open Different Vault"
if u.state.Session != nil { if u.state.Session != nil {
if strings.TrimSpace(u.remoteBaseURL.Text()) != "" || strings.TrimSpace(u.remotePath.Text()) != "" { if strings.TrimSpace(u.remoteBaseURL.Text()) != "" || strings.TrimSpace(u.remotePath.Text()) != "" {
baseURL := strings.TrimSpace(u.remoteBaseURL.Text()) baseURL := strings.TrimSpace(u.remoteBaseURL.Text())
path := strings.TrimSpace(u.remotePath.Text()) path := strings.TrimSpace(u.remotePath.Text())
targetLabel = "Remote vault" targetLabel = "Remote vault"
targetValue = friendlyRecentRemoteLabel(recentRemoteRecord{BaseURL: baseURL, Path: path}) targetValue = friendlyRecentRemoteLabel(recentRemoteRecord{BaseURL: baseURL, Path: path})
changeLabel = "Open Different Connection"
if strings.TrimSpace(targetValue) == "" { if strings.TrimSpace(targetValue) == "" {
targetValue = "Remote WebDAV vault" targetValue = "Remote WebDAV vault"
} }
@@ -1360,7 +1233,8 @@ func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
} }
} }
} }
targetCard := func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
@@ -1375,27 +1249,9 @@ func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
lbl.Color = accentColor lbl.Color = accentColor
return lbl.Layout(gtx) return lbl.Layout(gtx)
}), }),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !u.shouldUseLockedSinglePane() {
return layout.Dimensions{}
}
return layout.Inset{Top: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
if targetLabel == "Remote vault" {
return tonedButton(gtx, u.theme, &u.clearRemoteSelection, changeLabel)
}
return tonedButton(gtx, u.theme, &u.clearVaultSelection, changeLabel)
})
}),
) )
}) })
}) })
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.mode == "desktop" {
return layout.Dimensions{}
}
return targetCard(gtx)
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
@@ -1407,18 +1263,6 @@ func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.unlockVault, "Unlock") return tonedButton(gtx, u.theme, &u.unlockVault, "Unlock")
}), }),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.mode != "desktop" {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.mode != "desktop" {
return layout.Dimensions{}
}
return targetCard(gtx)
}),
) )
} }
+28 -28
View File
@@ -8,11 +8,11 @@ func TestUpsertEntryPreservesPreviousVersionInHistory(t *testing.T) {
model := Model{ model := Model{
Entries: []Entry{ Entries: []Entry{
{ {
ID: "vault-console", ID: "git-server",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "old-token", Password: "old-token",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Notes: "Original note", Notes: "Original note",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
@@ -20,11 +20,11 @@ func TestUpsertEntryPreservesPreviousVersionInHistory(t *testing.T) {
} }
model.UpsertEntry(Entry{ model.UpsertEntry(Entry{
ID: "vault-console", ID: "git-server",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "new-token", Password: "new-token",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Notes: "Updated note", Notes: "Updated note",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}) })
@@ -53,17 +53,17 @@ func TestDeleteEntryMovesItToRecycleBin(t *testing.T) {
model := Model{ model := Model{
Entries: []Entry{ Entries: []Entry{
{ {
ID: "surveillance-console", ID: "ha-codex",
Title: "Surveillance Console", Title: "Home Assistant (Codex)",
Username: "codex", Username: "codex",
Password: "token-2", Password: "token-2",
URL: "https://surveillance.crew.example.invalid", URL: "https://lights.julianfamily.org",
Path: []string{"Root", "Home Assistant"}, Path: []string{"Root", "Home Assistant"},
}, },
}, },
} }
if err := model.DeleteEntry("surveillance-console"); err != nil { if err := model.DeleteEntry("ha-codex"); err != nil {
t.Fatalf("DeleteEntry() error = %v", err) t.Fatalf("DeleteEntry() error = %v", err)
} }
@@ -75,8 +75,8 @@ func TestDeleteEntryMovesItToRecycleBin(t *testing.T) {
t.Fatalf("len(RecycleBin) = %d, want 1", len(model.RecycleBin)) t.Fatalf("len(RecycleBin) = %d, want 1", len(model.RecycleBin))
} }
if model.RecycleBin[0].Title != "Surveillance Console" { if model.RecycleBin[0].Title != "Home Assistant (Codex)" {
t.Fatalf("RecycleBin[0].Title = %q, want %q", model.RecycleBin[0].Title, "Surveillance Console") t.Fatalf("RecycleBin[0].Title = %q, want %q", model.RecycleBin[0].Title, "Home Assistant (Codex)")
} }
} }
@@ -86,17 +86,17 @@ func TestRestoreEntryMovesItBackFromRecycleBin(t *testing.T) {
model := Model{ model := Model{
RecycleBin: []Entry{ RecycleBin: []Entry{
{ {
ID: "bellagio", ID: "dynadot",
Title: "Bellagio", Title: "Dynadot",
Username: "rustyryan", Username: "jjulian",
Password: "token-3", Password: "token-3",
URL: "https://bellagio.example.invalid", URL: "https://www.dynadot.com",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
}, },
} }
if err := model.RestoreEntry("bellagio"); err != nil { if err := model.RestoreEntry("dynadot"); err != nil {
t.Fatalf("RestoreEntry() error = %v", err) t.Fatalf("RestoreEntry() error = %v", err)
} }
@@ -105,8 +105,8 @@ func TestRestoreEntryMovesItBackFromRecycleBin(t *testing.T) {
t.Fatalf("len(EntriesInPath()) = %d, want 1", len(got)) t.Fatalf("len(EntriesInPath()) = %d, want 1", len(got))
} }
if got[0].Title != "Bellagio" { if got[0].Title != "Dynadot" {
t.Fatalf("EntriesInPath()[0].Title = %q, want %q", got[0].Title, "Bellagio") t.Fatalf("EntriesInPath()[0].Title = %q, want %q", got[0].Title, "Dynadot")
} }
if len(model.RecycleBin) != 0 { if len(model.RecycleBin) != 0 {
@@ -120,17 +120,17 @@ func TestRestoreEntryVersionPromotesHistoricalVersionAndRetainsCurrentInHistory(
model := Model{ model := Model{
Entries: []Entry{ Entries: []Entry{
{ {
ID: "vault-console", ID: "git-server",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "new-token", Password: "new-token",
Notes: "Current note", Notes: "Current note",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
History: []Entry{ History: []Entry{
{ {
ID: "vault-console-history-1", ID: "git-server-history-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "old-token", Password: "old-token",
Notes: "Previous note", Notes: "Previous note",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
@@ -140,7 +140,7 @@ func TestRestoreEntryVersionPromotesHistoricalVersionAndRetainsCurrentInHistory(
}, },
} }
if err := model.RestoreEntryVersion("vault-console", 0); err != nil { if err := model.RestoreEntryVersion("git-server", 0); err != nil {
t.Fatalf("RestoreEntryVersion() error = %v", err) t.Fatalf("RestoreEntryVersion() error = %v", err)
} }
+46 -46
View File
@@ -22,11 +22,11 @@ func TestLoadKDBXBuildsModelFromNestedGroups(t *testing.T) {
Groups: []gokeepasslib.Group{ Groups: []gokeepasslib.Group{
mustGroup("Root", mustGroup("Root",
mustGroup("Internet", mustGroup("Internet",
mustEntry("Bellagio", "rustyryan", "https://bellagio.example.invalid", "hunter2"), mustEntry("Dynadot", "jjulian", "https://www.dynadot.com", "hunter2"),
mustEntry("Vault Console", "dannyocean", "https://vault.crew.example.invalid", "token-1"), mustEntry("Git Server", "joejulian", "https://git.julianfamily.org", "token-1"),
), ),
mustGroup("Home Assistant", mustGroup("Home Assistant",
mustEntry("Surveillance Console", "codex", "https://surveillance.crew.example.invalid", "token-2"), mustEntry("Home Assistant (Codex)", "codex", "https://lights.julianfamily.org", "token-2"),
), ),
), ),
}, },
@@ -53,11 +53,11 @@ func TestLoadKDBXBuildsModelFromNestedGroups(t *testing.T) {
t.Fatalf("len(EntriesInPath()) = %d, want 2", len(got)) t.Fatalf("len(EntriesInPath()) = %d, want 2", len(got))
} }
if got[0].Title != "Bellagio" || got[0].Username != "rustyryan" || got[0].URL != "https://bellagio.example.invalid" { if got[0].Title != "Dynadot" || got[0].Username != "jjulian" || got[0].URL != "https://www.dynadot.com" {
t.Fatalf("unexpected first entry: %#v", got[0]) t.Fatalf("unexpected first entry: %#v", got[0])
} }
if got[1].Title != "Vault Console" || got[1].Username != "dannyocean" || got[1].URL != "https://vault.crew.example.invalid" { if got[1].Title != "Git Server" || got[1].Username != "joejulian" || got[1].URL != "https://git.julianfamily.org" {
t.Fatalf("unexpected second entry: %#v", got[1]) t.Fatalf("unexpected second entry: %#v", got[1])
} }
@@ -70,7 +70,7 @@ func TestLoadKDBXBuildsModelFromNestedGroups(t *testing.T) {
func TestLoadKDBXPreservesEntryDetails(t *testing.T) { func TestLoadKDBXPreservesEntryDetails(t *testing.T) {
t.Parallel() t.Parallel()
entry := mustEntry("Surveillance Console", "codex", "https://surveillance.crew.example.invalid", "token-2") entry := mustEntry("Home Assistant (Codex)", "codex", "https://lights.julianfamily.org", "token-2")
entry.Tags = "automation; home" entry.Tags = "automation; home"
entry.Values = append(entry.Values, entry.Values = append(entry.Values,
mkValue("Notes", "Long-lived token used by Codex for home automation tasks."), mkValue("Notes", "Long-lived token used by Codex for home automation tasks."),
@@ -133,10 +133,10 @@ func TestSaveKDBXRoundTripsModel(t *testing.T) {
Entries: []Entry{ Entries: []Entry{
{ {
ID: "entry-1", ID: "entry-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Notes: "Personal git server token entry used for automation and CLI auth.", Notes: "Personal git server token entry used for automation and CLI auth.",
Tags: []string{"git", "infra"}, Tags: []string{"git", "infra"},
Fields: map[string]string{ Fields: map[string]string{
@@ -146,10 +146,10 @@ func TestSaveKDBXRoundTripsModel(t *testing.T) {
}, },
{ {
ID: "entry-2", ID: "entry-2",
Title: "Surveillance Console", Title: "Home Assistant (Codex)",
Username: "codex", Username: "codex",
Password: "token-2", Password: "token-2",
URL: "https://surveillance.crew.example.invalid", URL: "https://lights.julianfamily.org",
Notes: "Long-lived token used by Codex for home automation tasks.", Notes: "Long-lived token used by Codex for home automation tasks.",
Tags: []string{"automation", "home"}, Tags: []string{"automation", "home"},
Path: []string{"Root", "Home Assistant"}, Path: []string{"Root", "Home Assistant"},
@@ -167,7 +167,7 @@ func TestSaveKDBXRoundTripsModel(t *testing.T) {
t.Fatalf("LoadKDBX() error = %v", err) t.Fatalf("LoadKDBX() error = %v", err)
} }
got := loaded.Search("vault") got := loaded.Search("git")
if len(got) != 1 { if len(got) != 1 {
t.Fatalf("len(Search(\"git\")) = %d, want 1", len(got)) t.Fatalf("len(Search(\"git\")) = %d, want 1", len(got))
} }
@@ -245,18 +245,18 @@ func TestSaveKDBXRoundTripsEntryHistory(t *testing.T) {
Entries: []Entry{ Entries: []Entry{
{ {
ID: "entry-1", ID: "entry-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "new-token", Password: "new-token",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
History: []Entry{ History: []Entry{
{ {
ID: "entry-1-old", ID: "entry-1-old",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "old-token", Password: "old-token",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
Notes: "Original version", Notes: "Original version",
}, },
@@ -296,10 +296,10 @@ func TestSaveKDBXRoundTripsRecycleBinEntries(t *testing.T) {
RecycleBin: []Entry{ RecycleBin: []Entry{
{ {
ID: "entry-1", ID: "entry-1",
Title: "Surveillance Console", Title: "Home Assistant (Codex)",
Username: "codex", Username: "codex",
Password: "token-2", Password: "token-2",
URL: "https://surveillance.crew.example.invalid", URL: "https://lights.julianfamily.org",
Path: []string{"Root", "Home Assistant"}, Path: []string{"Root", "Home Assistant"},
}, },
}, },
@@ -319,8 +319,8 @@ func TestSaveKDBXRoundTripsRecycleBinEntries(t *testing.T) {
t.Fatalf("len(RecycleBin) = %d, want 1", len(loaded.RecycleBin)) t.Fatalf("len(RecycleBin) = %d, want 1", len(loaded.RecycleBin))
} }
if loaded.RecycleBin[0].Title != "Surveillance Console" { if loaded.RecycleBin[0].Title != "Home Assistant (Codex)" {
t.Fatalf("RecycleBin[0].Title = %q, want %q", loaded.RecycleBin[0].Title, "Surveillance Console") t.Fatalf("RecycleBin[0].Title = %q, want %q", loaded.RecycleBin[0].Title, "Home Assistant (Codex)")
} }
if len(loaded.RecycleBin[0].Path) != 2 || loaded.RecycleBin[0].Path[0] != "Root" || loaded.RecycleBin[0].Path[1] != "Home Assistant" { if len(loaded.RecycleBin[0].Path) != 2 || loaded.RecycleBin[0].Path[0] != "Root" || loaded.RecycleBin[0].Path[1] != "Home Assistant" {
@@ -358,7 +358,7 @@ func TestLoadKDBXWithKeyFileCredentials(t *testing.T) {
Meta: gokeepasslib.NewMetaData(), Meta: gokeepasslib.NewMetaData(),
Root: &gokeepasslib.RootData{ Root: &gokeepasslib.RootData{
Groups: []gokeepasslib.Group{ Groups: []gokeepasslib.Group{
mustGroup("Root", mustGroup("Internet", mustEntry("Vault Console", "dannyocean", "https://vault.crew.example.invalid", "token-1"))), mustGroup("Root", mustGroup("Internet", mustEntry("Git Server", "joejulian", "https://git.julianfamily.org", "token-1"))),
}, },
}, },
}, },
@@ -378,9 +378,9 @@ func TestLoadKDBXWithKeyFileCredentials(t *testing.T) {
t.Fatalf("LoadKDBXWithKey() error = %v", err) t.Fatalf("LoadKDBXWithKey() error = %v", err)
} }
got := model.Search("vault") got := model.Search("git")
if len(got) != 1 || got[0].Entry.Password != "token-1" { if len(got) != 1 || got[0].Entry.Password != "token-1" {
t.Fatalf("LoadKDBXWithKey() = %#v, want password-preserving vault entry", got) t.Fatalf("LoadKDBXWithKey() = %#v, want password-preserving git entry", got)
} }
} }
@@ -413,7 +413,7 @@ func TestLoadKDBXWithCompositeCredentials(t *testing.T) {
Meta: gokeepasslib.NewMetaData(), Meta: gokeepasslib.NewMetaData(),
Root: &gokeepasslib.RootData{ Root: &gokeepasslib.RootData{
Groups: []gokeepasslib.Group{ Groups: []gokeepasslib.Group{
mustGroup("Root", mustGroup("Home Assistant", mustEntry("Surveillance Console", "codex", "https://surveillance.crew.example.invalid", "token-2"))), mustGroup("Root", mustGroup("Home Assistant", mustEntry("Home Assistant (Codex)", "codex", "https://lights.julianfamily.org", "token-2"))),
}, },
}, },
}, },
@@ -452,7 +452,7 @@ func TestLoadKDBXReturnsInvalidCredentialsError(t *testing.T) {
Meta: gokeepasslib.NewMetaData(), Meta: gokeepasslib.NewMetaData(),
Root: &gokeepasslib.RootData{ Root: &gokeepasslib.RootData{
Groups: []gokeepasslib.Group{ Groups: []gokeepasslib.Group{
mustGroup("Root", mustGroup("Internet", mustEntry("Vault Console", "dannyocean", "https://vault.crew.example.invalid", "token-1"))), mustGroup("Root", mustGroup("Internet", mustEntry("Git Server", "joejulian", "https://git.julianfamily.org", "token-1"))),
}, },
}, },
}, },
@@ -490,11 +490,11 @@ func TestSaveKDBXWithKeyRoundTripsModel(t *testing.T) {
model := Model{ model := Model{
Entries: []Entry{ Entries: []Entry{
{ {
ID: "vault-console", ID: "git-server",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
}, },
@@ -510,9 +510,9 @@ func TestSaveKDBXWithKeyRoundTripsModel(t *testing.T) {
t.Fatalf("LoadKDBXWithKey() error = %v", err) t.Fatalf("LoadKDBXWithKey() error = %v", err)
} }
got := loaded.Search("vault") got := loaded.Search("git")
if len(got) != 1 || got[0].Entry.Password != "token-1" { if len(got) != 1 || got[0].Entry.Password != "token-1" {
t.Fatalf("round-trip with key file = %#v, want vault entry with password", got) t.Fatalf("round-trip with key file = %#v, want git entry with password", got)
} }
} }
@@ -533,11 +533,11 @@ func TestSaveKDBXWithCompositeKeyRoundTripsModel(t *testing.T) {
model := Model{ model := Model{
Entries: []Entry{ Entries: []Entry{
{ {
ID: "surveillance-console", ID: "ha-codex",
Title: "Surveillance Console", Title: "Home Assistant (Codex)",
Username: "codex", Username: "codex",
Password: "token-2", Password: "token-2",
URL: "https://surveillance.crew.example.invalid", URL: "https://lights.julianfamily.org",
Path: []string{"Root", "Home Assistant"}, Path: []string{"Root", "Home Assistant"},
}, },
}, },
@@ -570,11 +570,11 @@ func TestKDBXRoundTripsEntryAttachments(t *testing.T) {
model := Model{ model := Model{
Entries: []Entry{ Entries: []Entry{
{ {
ID: "vault-console", ID: "git-server",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
Attachments: map[string][]byte{ Attachments: map[string][]byte{
"token.txt": []byte("secret attachment contents"), "token.txt": []byte("secret attachment contents"),
@@ -610,10 +610,10 @@ func TestKDBXReopenCyclesPreserveStableIDsAndCrossFeatureState(t *testing.T) {
Entries: []Entry{ Entries: []Entry{
{ {
ID: "entry-1", ID: "entry-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-2", Password: "token-2",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Notes: "Current credential", Notes: "Current credential",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
Attachments: map[string][]byte{ Attachments: map[string][]byte{
@@ -622,10 +622,10 @@ func TestKDBXReopenCyclesPreserveStableIDsAndCrossFeatureState(t *testing.T) {
History: []Entry{ History: []Entry{
{ {
ID: "entry-1-history-1", ID: "entry-1-history-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
URL: "https://vault.crew.example.invalid", URL: "https://git.julianfamily.org",
Notes: "Original credential", Notes: "Original credential",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
+49 -49
View File
@@ -9,9 +9,9 @@ import (
func testModel() Model { func testModel() Model {
return Model{ return Model{
Entries: []Entry{ Entries: []Entry{
{ID: "1", Title: "Bellagio", Username: "rustyryan", URL: "https://bellagio.example.invalid", Path: []string{"Crew", "Internet"}}, {ID: "1", Title: "Dynadot", Username: "jjulian", URL: "https://www.dynadot.com", Path: []string{"Joe", "Internet"}},
{ID: "2", Title: "Vault Console", Username: "dannyocean", URL: "https://vault.crew.example.invalid", Path: []string{"Crew", "Internet"}}, {ID: "2", Title: "Git Server", Username: "joejulian", URL: "https://git.julianfamily.org", Path: []string{"Joe", "Internet"}},
{ID: "3", Title: "Surveillance Console", Username: "codex", URL: "https://surveillance.crew.example.invalid", Path: []string{"Crew", "Home Assistant"}}, {ID: "3", Title: "Home Assistant (Codex)", Username: "codex", URL: "https://lights.julianfamily.org", Path: []string{"Joe", "Home Assistant"}},
{ID: "4", Title: "Alma (WA Prep)", Username: "christina.julian", URL: "https://waprep.getalma.com", Path: []string{"Tricia", "School"}}, {ID: "4", Title: "Alma (WA Prep)", Username: "christina.julian", URL: "https://waprep.getalma.com", Path: []string{"Tricia", "School"}},
}, },
} }
@@ -20,7 +20,7 @@ func testModel() Model {
func TestChildGroupsReturnsImmediateGroupsOnly(t *testing.T) { func TestChildGroupsReturnsImmediateGroupsOnly(t *testing.T) {
model := testModel() model := testModel()
got := model.ChildGroups([]string{"Crew"}) got := model.ChildGroups([]string{"Joe"})
want := []string{"Home Assistant", "Internet"} want := []string{"Home Assistant", "Internet"}
if !slices.Equal(got, want) { if !slices.Equal(got, want) {
@@ -31,12 +31,12 @@ func TestChildGroupsReturnsImmediateGroupsOnly(t *testing.T) {
func TestEntriesInPathReturnsOnlyDirectEntries(t *testing.T) { func TestEntriesInPathReturnsOnlyDirectEntries(t *testing.T) {
model := testModel() model := testModel()
got := model.EntriesInPath([]string{"Crew", "Internet"}) got := model.EntriesInPath([]string{"Joe", "Internet"})
if len(got) != 2 { if len(got) != 2 {
t.Fatalf("len(EntriesInPath()) = %d, want 2", len(got)) t.Fatalf("len(EntriesInPath()) = %d, want 2", len(got))
} }
if got[0].Title != "Bellagio" || got[1].Title != "Vault Console" { if got[0].Title != "Dynadot" || got[1].Title != "Git Server" {
t.Fatalf("EntriesInPath() titles = %q, %q", got[0].Title, got[1].Title) t.Fatalf("EntriesInPath() titles = %q, %q", got[0].Title, got[1].Title)
} }
} }
@@ -45,29 +45,29 @@ func TestEntriesUnderPathReturnsDescendantEntries(t *testing.T) {
t.Parallel() t.Parallel()
model := testModel() model := testModel()
got := model.EntriesUnderPath([]string{"Crew"}) got := model.EntriesUnderPath([]string{"Joe"})
if len(got) != 3 { if len(got) != 3 {
t.Fatalf("len(EntriesUnderPath(Crew)) = %d, want 3", len(got)) t.Fatalf("len(EntriesUnderPath(Joe)) = %d, want 3", len(got))
} }
if got[0].Title != "Bellagio" || got[1].Title != "Surveillance Console" || got[2].Title != "Vault Console" { if got[0].Title != "Dynadot" || got[1].Title != "Git Server" || got[2].Title != "Home Assistant (Codex)" {
t.Fatalf("EntriesUnderPath(Crew) titles = %q, %q, %q", got[0].Title, got[1].Title, got[2].Title) t.Fatalf("EntriesUnderPath(Joe) titles = %q, %q, %q", got[0].Title, got[1].Title, got[2].Title)
} }
} }
func TestSearchReturnsMatchesWithFullPathContext(t *testing.T) { func TestSearchReturnsMatchesWithFullPathContext(t *testing.T) {
model := testModel() model := testModel()
got := model.Search("vault") got := model.Search("git")
if len(got) != 1 { if len(got) != 1 {
t.Fatalf("len(Search()) = %d, want 1", len(got)) t.Fatalf("len(Search()) = %d, want 1", len(got))
} }
if got[0].Entry.Title != "Vault Console" { if got[0].Entry.Title != "Git Server" {
t.Fatalf("Search() title = %q, want %q", got[0].Entry.Title, "Vault Console") t.Fatalf("Search() title = %q, want %q", got[0].Entry.Title, "Git Server")
} }
if got[0].Path != "Crew / Internet" { if got[0].Path != "Joe / Internet" {
t.Fatalf("Search() path = %q, want %q", got[0].Path, "Crew / Internet") t.Fatalf("Search() path = %q, want %q", got[0].Path, "Joe / Internet")
} }
} }
@@ -119,11 +119,11 @@ func TestInstantiateTemplateCreatesNormalEntryWithOverrides(t *testing.T) {
entry, err := model.InstantiateTemplate("tpl-1", Entry{ entry, err := model.InstantiateTemplate("tpl-1", Entry{
ID: "entry-1", ID: "entry-1",
Title: "Bellagio", Title: "Dynadot",
Username: "rustyryan", Username: "jjulian",
Password: "hunter2", Password: "hunter2",
URL: "https://bellagio.example.invalid", URL: "https://www.dynadot.com",
Path: []string{"Crew", "Internet"}, Path: []string{"Joe", "Internet"},
Tags: []string{"dns"}, Tags: []string{"dns"},
}) })
if err != nil { if err != nil {
@@ -134,11 +134,11 @@ func TestInstantiateTemplateCreatesNormalEntryWithOverrides(t *testing.T) {
t.Fatalf("entry.ID = %q, want %q", entry.ID, "entry-1") t.Fatalf("entry.ID = %q, want %q", entry.ID, "entry-1")
} }
if entry.Title != "Bellagio" { if entry.Title != "Dynadot" {
t.Fatalf("entry.Title = %q, want %q", entry.Title, "Bellagio") t.Fatalf("entry.Title = %q, want %q", entry.Title, "Dynadot")
} }
if entry.Username != "rustyryan" || entry.Password != "hunter2" || entry.URL != "https://bellagio.example.invalid" { if entry.Username != "jjulian" || entry.Password != "hunter2" || entry.URL != "https://www.dynadot.com" {
t.Fatalf("entry credentials = %#v, want override values", entry) t.Fatalf("entry credentials = %#v, want override values", entry)
} }
@@ -154,9 +154,9 @@ func TestInstantiateTemplateCreatesNormalEntryWithOverrides(t *testing.T) {
t.Fatalf("entry.Fields[Environment] = %q, want %q", entry.Fields["Environment"], "prod") t.Fatalf("entry.Fields[Environment] = %q, want %q", entry.Fields["Environment"], "prod")
} }
got := model.EntriesInPath([]string{"Crew", "Internet"}) got := model.EntriesInPath([]string{"Joe", "Internet"})
if len(got) != 1 || got[0].Title != "Bellagio" { if len(got) != 1 || got[0].Title != "Dynadot" {
t.Fatalf("EntriesInPath() = %#v, want instantiated Bellagio entry", got) t.Fatalf("EntriesInPath() = %#v, want instantiated Dynadot entry", got)
} }
} }
@@ -178,7 +178,7 @@ func TestDeleteTemplateRemovesTemplateWithoutTouchingEntries(t *testing.T) {
model := Model{ model := Model{
Entries: []Entry{ Entries: []Entry{
{ID: "entry-1", Title: "Vault Console", Path: []string{"Root", "Internet"}}, {ID: "entry-1", Title: "Git Server", Path: []string{"Root", "Internet"}},
}, },
Templates: []Entry{ Templates: []Entry{
{ID: "tpl-1", Title: "Website Login", Path: []string{"Templates"}}, {ID: "tpl-1", Title: "Website Login", Path: []string{"Templates"}},
@@ -222,8 +222,8 @@ func TestDuplicateEntryCopiesEntryWithNewIDAndTitle(t *testing.T) {
Entries: []Entry{ Entries: []Entry{
{ {
ID: "entry-1", ID: "entry-1",
Title: "Vault Console", Title: "Git Server",
Username: "dannyocean", Username: "joejulian",
Password: "token-1", Password: "token-1",
Path: []string{"Root", "Internet"}, Path: []string{"Root", "Internet"},
}, },
@@ -238,8 +238,8 @@ func TestDuplicateEntryCopiesEntryWithNewIDAndTitle(t *testing.T) {
if duplicate.ID != "entry-2" { if duplicate.ID != "entry-2" {
t.Fatalf("duplicate.ID = %q, want %q", duplicate.ID, "entry-2") t.Fatalf("duplicate.ID = %q, want %q", duplicate.ID, "entry-2")
} }
if duplicate.Title != "Vault Console (Copy)" { if duplicate.Title != "Git Server (Copy)" {
t.Fatalf("duplicate.Title = %q, want %q", duplicate.Title, "Vault Console (Copy)") t.Fatalf("duplicate.Title = %q, want %q", duplicate.Title, "Git Server (Copy)")
} }
got := model.EntriesInPath([]string{"Root", "Internet"}) got := model.EntriesInPath([]string{"Root", "Internet"})
if len(got) != 2 { if len(got) != 2 {
@@ -250,9 +250,9 @@ func TestDuplicateEntryCopiesEntryWithNewIDAndTitle(t *testing.T) {
func TestCreateGroupMakesItVisibleAsChildGroup(t *testing.T) { func TestCreateGroupMakesItVisibleAsChildGroup(t *testing.T) {
model := testModel() model := testModel()
model.CreateGroup([]string{"Crew"}, "Finance") model.CreateGroup([]string{"Joe"}, "Finance")
got := model.ChildGroups([]string{"Crew"}) got := model.ChildGroups([]string{"Joe"})
want := []string{"Finance", "Home Assistant", "Internet"} want := []string{"Finance", "Home Assistant", "Internet"}
if !slices.Equal(got, want) { if !slices.Equal(got, want) {
t.Fatalf("ChildGroups() = %v, want %v", got, want) t.Fatalf("ChildGroups() = %v, want %v", got, want)
@@ -263,32 +263,32 @@ func TestCreateGroupSupportsNestedRelativePath(t *testing.T) {
t.Parallel() t.Parallel()
model := testModel() model := testModel()
model.CreateGroup([]string{"Crew"}, "Infrastructure / Prod") model.CreateGroup([]string{"Joe"}, "Infrastructure / Prod")
got := model.ChildGroups([]string{"Crew"}) got := model.ChildGroups([]string{"Joe"})
if !slices.Equal(got, []string{"Home Assistant", "Infrastructure", "Internet"}) { if !slices.Equal(got, []string{"Home Assistant", "Infrastructure", "Internet"}) {
t.Fatalf("ChildGroups(Crew) = %v, want [Home Assistant Infrastructure Internet]", got) t.Fatalf("ChildGroups(Joe) = %v, want [Home Assistant Infrastructure Internet]", got)
} }
got = model.ChildGroups([]string{"Crew", "Infrastructure"}) got = model.ChildGroups([]string{"Joe", "Infrastructure"})
if !slices.Equal(got, []string{"Prod"}) { if !slices.Equal(got, []string{"Prod"}) {
t.Fatalf("ChildGroups(Crew/Infrastructure) = %v, want [Prod]", got) t.Fatalf("ChildGroups(Joe/Infrastructure) = %v, want [Prod]", got)
} }
} }
func TestRenameGroupMovesEntriesAndKeepsHierarchy(t *testing.T) { func TestRenameGroupMovesEntriesAndKeepsHierarchy(t *testing.T) {
model := testModel() model := testModel()
if err := model.RenameGroup([]string{"Crew", "Internet"}, "Infra"); err != nil { if err := model.RenameGroup([]string{"Joe", "Internet"}, "Infra"); err != nil {
t.Fatalf("RenameGroup() error = %v", err) t.Fatalf("RenameGroup() error = %v", err)
} }
got := model.EntriesInPath([]string{"Crew", "Infra"}) got := model.EntriesInPath([]string{"Joe", "Infra"})
if len(got) != 2 { if len(got) != 2 {
t.Fatalf("len(EntriesInPath(Crew/Infra)) = %d, want 2", len(got)) t.Fatalf("len(EntriesInPath(Joe/Infra)) = %d, want 2", len(got))
} }
if len(model.EntriesInPath([]string{"Crew", "Internet"})) != 0 { if len(model.EntriesInPath([]string{"Joe", "Internet"})) != 0 {
t.Fatal("EntriesInPath(Crew/Internet) should be empty after rename") t.Fatal("EntriesInPath(Joe/Internet) should be empty after rename")
} }
} }
@@ -309,8 +309,8 @@ func TestMoveGroupMovesEntriesAndNestedGroups(t *testing.T) {
t.Parallel() t.Parallel()
model := testModel() model := testModel()
model.CreateGroup([]string{"Crew", "Internet"}, "Infrastructure") model.CreateGroup([]string{"Joe", "Internet"}, "Infrastructure")
if err := model.MoveGroup([]string{"Crew", "Internet"}, []string{"Tricia"}); err != nil { if err := model.MoveGroup([]string{"Joe", "Internet"}, []string{"Tricia"}); err != nil {
t.Fatalf("MoveGroup() error = %v", err) t.Fatalf("MoveGroup() error = %v", err)
} }
@@ -318,8 +318,8 @@ func TestMoveGroupMovesEntriesAndNestedGroups(t *testing.T) {
if len(got) != 2 { if len(got) != 2 {
t.Fatalf("len(EntriesInPath(Tricia/Internet)) = %d, want 2", len(got)) t.Fatalf("len(EntriesInPath(Tricia/Internet)) = %d, want 2", len(got))
} }
if len(model.EntriesInPath([]string{"Crew", "Internet"})) != 0 { if len(model.EntriesInPath([]string{"Joe", "Internet"})) != 0 {
t.Fatal("EntriesInPath(Crew/Internet) should be empty after move") t.Fatal("EntriesInPath(Joe/Internet) should be empty after move")
} }
gotGroups := model.ChildGroups([]string{"Tricia", "Internet"}) gotGroups := model.ChildGroups([]string{"Tricia", "Internet"})
if !slices.Equal(gotGroups, []string{"Infrastructure"}) { if !slices.Equal(gotGroups, []string{"Infrastructure"}) {
@@ -330,12 +330,12 @@ func TestMoveGroupMovesEntriesAndNestedGroups(t *testing.T) {
func TestDeleteEmptyGroupRemovesItFromNavigation(t *testing.T) { func TestDeleteEmptyGroupRemovesItFromNavigation(t *testing.T) {
model := testModel() model := testModel()
model.CreateGroup([]string{"Crew"}, "Finance") model.CreateGroup([]string{"Joe"}, "Finance")
if err := model.DeleteGroup([]string{"Crew", "Finance"}); err != nil { if err := model.DeleteGroup([]string{"Joe", "Finance"}); err != nil {
t.Fatalf("DeleteGroup() error = %v", err) t.Fatalf("DeleteGroup() error = %v", err)
} }
got := model.ChildGroups([]string{"Crew"}) got := model.ChildGroups([]string{"Joe"})
want := []string{"Home Assistant", "Internet"} want := []string{"Home Assistant", "Internet"}
if !slices.Equal(got, want) { if !slices.Equal(got, want) {
t.Fatalf("ChildGroups() = %v, want %v", got, want) t.Fatalf("ChildGroups() = %v, want %v", got, want)
+5 -5
View File
@@ -15,8 +15,8 @@ func TestClientOpenDownloadsRemoteVault(t *testing.T) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Fatalf("method = %s, want GET", r.Method) t.Fatalf("method = %s, want GET", r.Method)
} }
if user, pass, ok := r.BasicAuth(); !ok || user != "rustyryan" || pass != "secret" { if user, pass, ok := r.BasicAuth(); !ok || user != "jjulian" || pass != "secret" {
t.Fatalf("basic auth = %q/%q ok=%v, want rustyryan/secret true", user, pass, ok) t.Fatalf("basic auth = %q/%q ok=%v, want jjulian/secret true", user, pass, ok)
} }
w.Header().Set("ETag", `"etag-1"`) w.Header().Set("ETag", `"etag-1"`)
_, _ = io.WriteString(w, "vault-bytes") _, _ = io.WriteString(w, "vault-bytes")
@@ -26,7 +26,7 @@ func TestClientOpenDownloadsRemoteVault(t *testing.T) {
client := Client{ client := Client{
HTTPClient: server.Client(), HTTPClient: server.Client(),
BaseURL: server.URL, BaseURL: server.URL,
Username: "rustyryan", Username: "jjulian",
Password: "secret", Password: "secret",
} }
@@ -69,7 +69,7 @@ func TestClientSaveUploadsVaultWithIfMatch(t *testing.T) {
client := Client{ client := Client{
HTTPClient: server.Client(), HTTPClient: server.Client(),
BaseURL: server.URL, BaseURL: server.URL,
Username: "rustyryan", Username: "jjulian",
Password: "secret", Password: "secret",
} }
@@ -94,7 +94,7 @@ func TestClientSaveReturnsConflictOnVersionMismatch(t *testing.T) {
client := Client{ client := Client{
HTTPClient: server.Client(), HTTPClient: server.Client(),
BaseURL: server.URL, BaseURL: server.URL,
Username: "rustyryan", Username: "jjulian",
Password: "secret", Password: "secret",
} }