feat: remove Bitbucket OAuth, add RawGit username/password auth#1
feat: remove Bitbucket OAuth, add RawGit username/password auth#1cloud-hai-vo wants to merge 1 commit into
Conversation
- Remove BitbucketIdentity, OAuthProvider.Bitbucket, all Bitbucket methods - Remove BitbucketClientId/Secret from OAuthClientConfig - Remove KeychainKeys.Bitbucket - Add RawGitCredentialService (IRawGitCredentialService) with keychain storage - Update GitSyncService: GitHub token -> raw credential fallback - Update SettingsViewModel: remove Bitbucket props, add RawGit props/commands - Update SettingsView.axaml: Custom Git Remote section - Update OAuthConnectDialog: remove Bitbucket tab - Remove Bitbucket tests, add RawGitCredentialTests - Update README + Blueprint
There was a problem hiding this comment.
Pull request overview
This PR removes all Bitbucket OAuth support and introduces a “Raw Git” username/password credential path intended to allow syncing to arbitrary HTTPS Git remotes, while keeping GitHub OAuth (Device Flow + PAT) intact.
Changes:
- Removed Bitbucket identity/provider, UI panels, keychain key, and related tests.
- Added
RawGitCredentialService(+ tests) for validating and storing per-remote username/password credentials in the keychain. - Updated
GitSyncServiceto resolve credentials via PAT → GitHub token → raw credentials fallback; updated Settings UI/VM to manage custom remote credentials.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/SpotDesk.UI.Tests/M5_IntegrationTests.cs | Removes Bitbucket integration test coverage. |
| tests/SpotDesk.Core.Tests/RawGitCredentialTests.cs | Adds unit tests for raw credential storage/normalization/validation. |
| tests/SpotDesk.Core.Tests/M1_OAuthTests.cs | Removes Bitbucket OAuth tests; updates OAuthClientConfig.Resolve call sites in tests. |
| src/SpotDesk.UI/Views/SettingsView.axaml | Replaces Bitbucket Sync UI with Custom Git Remote UI. |
| src/SpotDesk.UI/Views/MainWindow.axaml.cs | Removes Bitbucket identity handling post-auth. |
| src/SpotDesk.UI/ViewModels/SettingsViewModel.cs | Adds raw credential connect/disconnect commands and state. |
| src/SpotDesk.UI/Dialogs/OAuthConnectDialog.axaml(.cs) | Removes Bitbucket panel and wiring; adjusts dialog title. |
| src/SpotDesk.Core/Vault/VaultService.cs | Updates docs to remove Bitbucket references. |
| src/SpotDesk.Core/Sync/GitSyncService.cs | Adds credential resolution logic and raw-credential fallback. |
| src/SpotDesk.Core/Auth/RawGitCredentialService.cs | New service for validating/storing raw HTTPS credentials in keychain. |
| src/SpotDesk.Core/Auth/OAuthService.cs | Removes Bitbucket OAuth/App Password flows and models. |
| src/SpotDesk.Core/Auth/OAuthClientConfig.cs | Removes Bitbucket config fields; simplifies Resolve. |
| src/SpotDesk.Core/Auth/MasterPasswordFallback.cs | Updates docs to remove Bitbucket mention. |
| src/SpotDesk.Core/Auth/KeychainService.cs | Removes Bitbucket key constant. |
| SpotDesk_Blueprint.md | Updates architecture/roadmap docs to remove Bitbucket and add raw git credentials. |
| README.md | Updates marketing/docs to describe GitHub + custom remote sync. |
Comments suppressed due to low confidence (1)
README.md:33
- The Vault Modes table still says Git Sync “First run” is “Sign in via Device Flow”, but the README now also advertises syncing via “any Git remote with username/password”. Update this row to reflect that Git sync can be configured either via GitHub Device Flow/PAT or via custom remote credentials, otherwise the documentation is misleading.
| | Local Mode | Git Sync |
|---|---|---|
| Account required | None | GitHub or any Git remote |
| Encrypted at rest | Yes (AES-256-GCM) | Yes (AES-256-GCM) |
| Sync across devices | No | Yes (private Git repo) |
| Works offline | Yes | Yes (cached locally) |
| First run | Set a master password | Sign in via Device Flow |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Environment.GetEnvironmentVariable("SPOTDESK_BITBUCKET_CLIENT_SECRET"), | ||
| savedBitbucketClientSecret), | ||
| }; | ||
|
|
There was a problem hiding this comment.
OAuthClientConfig.Resolve signature was reduced to a single parameter, but there is still at least one call site using the old 3-argument overload (e.g. src/SpotDesk.App/Program.cs:37). This will fail to compile; update DI bootstrap to call Resolve(null) (and adjust any other remaining call sites).
| /// <summary> | |
| /// Backward-compatible overload for older call sites that passed additional, now-unused | |
| /// OAuth client IDs. Extra parameters are ignored and only the GitHub client ID is used. | |
| /// </summary> | |
| public static OAuthClientConfig Resolve( | |
| string? savedGitHubClientId, | |
| string? savedAzureClientId, | |
| string? savedGoogleClientId) => | |
| Resolve(savedGitHubClientId); |
| private async Task ConnectRawGitAsync() | ||
| { | ||
| BitbucketStatus = "Validating…"; | ||
| if (_rawGitCredential is null) return; |
There was a problem hiding this comment.
ConnectRawGitAsync silently no-ops when _rawGitCredential is null. In the current DI setup there is no registration for IRawGitCredentialService (and SettingsViewModel is constructed without it), so the Custom Git Remote UI will never work. Consider making _rawGitCredential non-nullable and registering it in DI (and/or set RawGitStatus to an explicit error when it’s missing).
| if (_rawGitCredential is null) return; | |
| if (_rawGitCredential is null) | |
| { | |
| RawGitStatus = "Error: Raw Git credential service is not available."; | |
| return; | |
| } |
| // Try GitHub token-based auth first | ||
| var token = _keychain.Retrieve(KeychainKeys.VaultRepoPat) | ||
| ?? _keychain.Retrieve(KeychainKeys.GitHub); | ||
|
|
||
| if (!string.IsNullOrEmpty(token)) | ||
| return new UsernamePasswordCredentials { Username = "x-token", Password = token }; | ||
|
|
||
| // Fall back to raw git credentials for this remote URL |
There was a problem hiding this comment.
ResolveCredentials always prefers GitHub/PAT tokens even when syncing against a non-GitHub remote. If a user is signed into GitHub (needed for vault unlock) but configures a custom GitLab/Gitea remote, Git operations will attempt to authenticate with the GitHub token and fail, never reaching the raw-credential fallback. Consider selecting the auth mechanism based on the remote URL host/scheme (e.g., only use GitHub token for GitHub remotes, otherwise prefer raw credentials for that URL).
| if (string.IsNullOrWhiteSpace(remoteUrl)) | ||
| throw new ArgumentException("Remote URL must not be empty.", nameof(remoteUrl)); | ||
| if (string.IsNullOrWhiteSpace(username)) | ||
| throw new ArgumentException("Username must not be empty.", nameof(username)); | ||
| if (string.IsNullOrWhiteSpace(password)) | ||
| throw new ArgumentException("Password must not be empty.", nameof(password)); | ||
|
|
||
| await Task.Run(() => | ||
| { | ||
| try | ||
| { | ||
| Repository.ListRemoteReferences(remoteUrl.Trim(), (_, _, _) => | ||
| new UsernamePasswordCredentials | ||
| { | ||
| Username = username.Trim(), | ||
| Password = password.Trim() | ||
| }); |
There was a problem hiding this comment.
ValidateAsync accepts any remoteUrl string and then uses UsernamePasswordCredentials, which only applies to HTTP(S) remotes. To avoid confusing failures and credential leaks, validate that the URL is a well-formed HTTPS URL and reject URLs containing embedded credentials (userinfo like https://user:pass@host/...) before attempting authentication.
| public void Store(string remoteUrl, string username, string password) | ||
| { | ||
| var key = NormalizeKey(remoteUrl); | ||
| _keychain.Store(key, $"{username}\n{password}"); | ||
| } | ||
|
|
||
| public (string Username, string Password)? Retrieve(string remoteUrl) | ||
| { | ||
| var key = NormalizeKey(remoteUrl); | ||
| var stored = _keychain.Retrieve(key); | ||
| if (stored is null) return null; | ||
|
|
||
| var newlineIdx = stored.IndexOf('\n'); | ||
| if (newlineIdx < 0) return null; | ||
|
|
||
| return (stored[..newlineIdx], stored[(newlineIdx + 1)..]); | ||
| } |
There was a problem hiding this comment.
Raw credentials are stored as a single string using a newline delimiter ($"{username}\n{password}"). Because username is not constrained, a newline in the username would make retrieval ambiguous/corrupted. Add validation to forbid \r/\n in the username (and consider a less fragile encoding/serialization format).
| RawGitRemoteUrl = string.Empty; | ||
| RawGitUsername = string.Empty; | ||
| RawGitPassword = string.Empty; | ||
| RawGitStatus = string.Empty; |
There was a problem hiding this comment.
DisconnectRawGit relies on _rawGitCredential.Delete(...), but the current MacOsKeychainService.Delete implementation is a no-op (src/SpotDesk.Core/Auth/KeychainService.cs:125). On macOS this means “Disconnect” won’t actually remove stored raw credentials (and likewise GitHub revoke). Consider implementing keychain deletion on macOS or changing the UI/wording to avoid implying credentials were removed when they weren’t.
| RawGitStatus = string.Empty; | |
| RawGitStatus = "Disconnected from raw Git. OS-level Git credentials (e.g. in Keychain or the system credential manager) may need to be removed separately."; |
| /// Returns the best available credentials for git operations. | ||
| /// Priority: vault-repo fine-grained PAT → GitHub identity token → raw git credentials for the remote URL. | ||
| /// </summary> | ||
| private string ResolveGitToken() => | ||
| _keychain.Retrieve(KeychainKeys.VaultRepoPat) | ||
| ?? _keychain.Retrieve(KeychainKeys.GitHub) | ||
| ?? string.Empty; | ||
| private Credentials ResolveCredentials(string url) | ||
| { | ||
| // Try GitHub token-based auth first | ||
| var token = _keychain.Retrieve(KeychainKeys.VaultRepoPat) | ||
| ?? _keychain.Retrieve(KeychainKeys.GitHub); | ||
|
|
||
| if (!string.IsNullOrEmpty(token)) | ||
| return new UsernamePasswordCredentials { Username = "x-token", Password = token }; | ||
|
|
||
| // Fall back to raw git credentials for this remote URL | ||
| if (_rawGit is not null) | ||
| { | ||
| var rawCred = _rawGit.Retrieve(url); | ||
| if (rawCred is not null) | ||
| return new UsernamePasswordCredentials { Username = rawCred.Value.Username, Password = rawCred.Value.Password }; | ||
| } |
There was a problem hiding this comment.
ResolveCredentials introduces new credential-selection behavior (PAT vs GitHub token vs raw credentials), but there are no unit tests asserting which credential path is chosen under each condition. Since tests/SpotDesk.Core.Tests/M2_GitSyncTests.cs already covers GitSyncService, consider adding tests that verify (1) PAT takes precedence over GitHub token, (2) raw credentials are used when tokens are absent, and (3) behavior for non-GitHub remotes when a GitHub token exists.
Changes
RawGitCredentialService— sync vault to ANY Git remote with username + passwordGitSyncService: GitHub token → raw credential fallbackSettingsViewModel+SettingsView: Custom Git Remote sectionOAuthConnectDialogRawGitCredentialTestsReview checklist
/cc @eggspot_devclaw_bot please review 🙏