Files
ClaudeDo/src/ClaudeDo.Ui/ViewModels/Modals/Settings/OnlineInboxSettingsViewModel.cs
mika kuns 23c3065f20 feat(online-inbox): gate access on Zitadel "user" project role
The Online API now requires the "user" project role (claim
urn:zitadel:iam:org:project:roles) instead of an ALLOWED_USER_IDS allowlist.

- IOnlineAuthProvider: add GetAccessTokenAsync(forceRefresh) overload
- ZitadelAuthProvider: forceRefresh drops the cached token and re-runs the
  refresh-token grant to mint a fresh, role-bearing token
- OnlineInboxApiClient: on 401, force-refresh and retry once; if still 401,
  throw a clear "missing 'user' role" error
- OnlineSyncService: surface the 401 at Error level (no longer silent)
- UI: ZitadelTokenInspector decodes the access token after login and warns
  early when the "user" role is absent (fail-open); shown in settings
- docs: online-inbox-api-contract reflects role-based access (no allowlist)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:46:17 +02:00

124 lines
3.7 KiB
C#

using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
public sealed partial class OnlineInboxSettingsViewModel : ViewModelBase
{
private readonly IWorkerClient _worker;
private readonly IOnlineLoginService _loginService;
[ObservableProperty] private bool _enabled;
[ObservableProperty] private string _apiBaseUrl = "";
[ObservableProperty] private string _authority = "";
[ObservableProperty] private string _clientId = "";
[ObservableProperty] private string _scopes = "openid offline_access";
[ObservableProperty] private string _redirectUri = "http://localhost:8765/callback";
[ObservableProperty] private int _pollIntervalSeconds = 60;
[ObservableProperty] private bool _signedIn;
[ObservableProperty] private bool _isBusy;
[ObservableProperty] private string _statusMessage = "";
public OnlineInboxSettingsViewModel(IWorkerClient worker, IOnlineLoginService loginService)
{
_worker = worker;
_loginService = loginService;
}
public async Task LoadAsync()
{
IsBusy = true;
StatusMessage = "";
try
{
var dto = await _worker.GetOnlineInboxStateAsync();
if (dto is null)
{
StatusMessage = Loc.T("vm.onlineInbox.workerOffline");
return;
}
Enabled = dto.Enabled;
ApiBaseUrl = dto.ApiBaseUrl;
Authority = dto.Authority;
ClientId = dto.ClientId;
Scopes = dto.Scopes;
RedirectUri = dto.RedirectUri;
SignedIn = dto.SignedIn;
PollIntervalSeconds = dto.PollIntervalSeconds;
}
finally { IsBusy = false; }
}
[RelayCommand]
private async Task Save()
{
IsBusy = true;
StatusMessage = "";
try
{
await _worker.SetOnlineInboxConfigAsync(new OnlineInboxConfigInputDto(
Enabled,
ApiBaseUrl,
PollIntervalSeconds,
Authority,
ClientId,
Scopes,
RedirectUri));
StatusMessage = Loc.T("vm.onlineInbox.saved");
}
catch (Exception ex)
{
StatusMessage = Loc.T("vm.onlineInbox.saveFailed", ex.Message);
}
finally { IsBusy = false; }
}
[RelayCommand]
private async Task SignIn()
{
IsBusy = true;
StatusMessage = "";
try
{
var result = await _loginService.LoginAsync(Authority, ClientId, Scopes, RedirectUri);
if (!result.Success)
{
StatusMessage = Loc.T("vm.onlineInbox.signInFailed", result.Error ?? "Unknown error");
return;
}
await _worker.SetOnlineInboxAuthAsync(result.RefreshToken!);
SignedIn = true;
StatusMessage = result.Warning == "missing-user-role"
? Loc.T("vm.onlineInbox.signedInNoRole")
: Loc.T("vm.onlineInbox.signedIn");
}
catch (Exception ex)
{
StatusMessage = Loc.T("vm.onlineInbox.signInFailed", ex.Message);
}
finally { IsBusy = false; }
}
[RelayCommand]
private async Task SignOut()
{
IsBusy = true;
StatusMessage = "";
try
{
await _worker.ClearOnlineInboxAuthAsync();
SignedIn = false;
StatusMessage = Loc.T("vm.onlineInbox.signedOut");
}
catch (Exception ex)
{
StatusMessage = Loc.T("vm.onlineInbox.signOutFailed", ex.Message);
}
finally { IsBusy = false; }
}
}