Skip to content

Add Windows CI and WPF management client - #140

Open
netoyou wants to merge 16 commits into
multiOTP:masterfrom
netoyou:codex/windows-ci-wpf-manager
Open

Add Windows CI and WPF management client#140
netoyou wants to merge 16 commits into
multiOTP:masterfrom
netoyou:codex/windows-ci-wpf-manager

Conversation

@netoyou

@netoyou netoyou commented Aug 26, 2026

Copy link
Copy Markdown

Summary

  • Add a GitHub Actions Windows build matrix for x64 and x86 credential-provider builds and MSI packages
  • Add a dependency-free WPF management client targeting .NET Framework 4.5.2
  • Support authentication testing, user list/details, and credential-provider registry settings
  • Keep CLI invocation asynchronous with cancellation, timeout, UTF-8 decoding, and Windows-safe argument escaping

Testing

netoyou and others added 16 commits August 26, 2026 20:33
Add Tokens, Logs, AD/LDAP and Maintenance tabs and extend the Users tab:
- Users: create (-fastcreate), activate, deactivate, lock, unlock,
  resync (dialog for two consecutive OTP codes), delete
- Tokens: list, assign, remove, delete
- Logs: show/clear log, error codes, version info
- AD/LDAP: connection check, user list, user sync (min 30s timeout)
- Maintenance: encrypted backup/restore, purge lock folder,
  purge LDAP cache

Success now recognizes multiOTP INFO exit codes 11-19, and the exit
code table covers the documented 0-59 range. Destructive operations
require confirmation; backup/restore passwords are never logged and
are cleared after use.
The Users tab gains a QR Code button for the selected user:
- -urllink retrieves the otpauth:// provisioning URL (validated before
  use; unsupported token types are reported instead of showing a QR)
- -qrcode renders the QR PNG into a temp file, shown in a dialog with
  the copyable URL; the temp file embeds the token secret and is
  deleted as soon as the image is loaded in memory
- If image generation fails (e.g. missing GD support), the dialog
  falls back to the provisioning URL alone
The Create button used -fastcreate, which generates a random 4-digit
prefix PIN and enables the prefix-PIN requirement. The PIN is never
shown in the GUI, so users who typed only the 6-digit OTP from their
authenticator app got exit code 98 (wrong token length) on verify.

Switch the default Create to -fastcreatenopin so verification works
with just the OTP, matching the Google Authenticator workflow.

Also extend the friendly exit-code table to cover codes 60-99
(SMS/email, server, AD/LDAP, push, and authentication errors),
so future failures display meaningful text instead of a bare code.
multiOTP Credential Provider only triggers the OTP challenge after a
Windows account has logged in, so creating a multiOTP entry for a
username that does not exist locally (or in the joined domain) leaves
the user with no 2FA coverage. Before -fastcreatenopin runs, the GUI
now probes local SAM via System.DirectoryServices.AccountManagement and,
if the machine is joined to a domain, also the domain. When the
Windows account is missing, a confirmation dialog (default No) explains
the situation; if the user overrides, the new entry is created and the
status line records the warning.
Users created with -fastcreate carry request_prefix_pin=1, so they
still expect the PIN+OTP concatenation even after the default Create
button moved to -fastcreatenopin. The Disable PIN button runs
'-set <user> request_prefix_pin=0' on the selected user so admins can
switch any account to OTP-only verification without touching the CLI.
The user-info dump (Activated, AD/LDAP synchronized, attributes, etc.)
was getting cramped on typical Win7 screens with the default 720px
window. Three small layout tweaks:

- Window default height 720 -> 820, MinHeight 580 -> 640
- UserDetailBox MinHeight=240 so it always shows a useful chunk
  even when the field grid above is large
- Inner Panel padding 14 -> 10 across all tabs to reclaim a few
  pixels in every panel
- Add per-key Resources.resx (English) and Resources.zh-Hans.resx
  (Simplified Chinese), with hand-generated Resources.Designer.cs so
  XAML can reference entries via {x:Static p:Resources.Key} and
  code-behind via Resources.Key.
- Move every hardcoded user-visible string out of MainWindow.xaml,
  MainWindow.xaml.cs, QrCodeDialog and PromptDialog into resources,
  including 70+ multiOTP exit-code descriptions and all
  confirmation/status messages.
- Add Core/AppSettings.cs to persist user preferences as JSON in
  %LocalAppData%\MultiOtpManager\settings.json (hand-rolled parser
  to avoid the System.Text.Json reference assembly dependency on
  .NET Framework 4.5.2).
- App.OnStartup reads the saved language and sets the UI culture
  before MainWindow is constructed; on first run it guesses the
  best built-in language from the system UI culture and persists
  the choice.
- Add a Language ComboBox to the top bar with three entries
  (System default, English, 简体中文). Changing it saves the
  preference and offers to restart the application so the new
  UI culture takes effect.
Replacing the ItemsSource in LoadUsersAsync was wiping the user's
selection whenever the Users tab auto-refreshed on activation, which
made Deactivate/Disable PIN/Delete etc. silently fall through the
'SelectUserFirst' branch.

- Drop the refreshingUsers flag; it was both racing with the user
  click and stomping on the SelectedItem reset.
- Snapshot the selected user's name before the CLI call and restore
  it by name on the freshly built list. Missing users fall through
  to ClearUserDetail as expected.
- Detach SelectedItem right before swapping ItemsSource so WPF does
  not briefly raise a SelectionChanged against a stale item.
Auto-refreshing the Users list on every tab activation was producing a
runaway loop in practice: the SelectionChanged raised during the
ItemsSource swap fired LoadUserDetailsAsync again, which could trigger
additional tab/layout invalidations and queue more refreshes, all
forking multiotp CLI child processes.

This change:

- Adds a per-refresh CancellationTokenSource and a monotonically
  increasing request id. New refreshes cancel the previous one and the
  stale tasks bail out before mutating ItemsSource.
- Suppresses UsersListBox.SelectionChanged while the refresh is
  re-binding ItemsSource so the temporary null selection does not
  trigger another detail load.
- Only reacts to MainTabs.SelectionChanged when the Users tab is
  actually being added to the selection, instead of on every
  selection-change inside the TabControl.
- Reloads the detail panel for the (restored) selection once the
  refresh completes, guarded by the request id so a newer selection
  is never overwritten by stale data.
Places the most-used Users tab first; Authentication becomes the
second tab. Tab x:Names and the MainTabs_SelectionChanged handler
match by reference, so reordering is transparent to the code-behind.
Hover-over explanations for the controls that need operator
interpretation rather than restating their label:

- Authentication: TIMEOUT (SECONDS) and the -verify checkbox.
- Users: Disable PIN, Resync, QR code, and a note that the QR
  image is deleted from disk right after the dialog loads it.
- Tokens: refresher, list, Assign/Remove/Delete semantics.
- Credential Provider: LOGON/UNLOCK scope, the multiOTP-only
  exclusivity choice, and the two-step / single-step OTP field
  layout.
- Maintenance: backup/restore scope, lock-folder and LDAP-cache
  purges.
- AD/LDAP: explicit reminder that Sync is a write operation.
- Logs: warning on ClearLog, plus the dictionary/version buttons.

Strings live in Resources.resx / Resources.zh-Hans.resx so the
existing language switcher picks them up automatically.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant