feat(ui): Online Inbox settings tab + auth-code/PKCE login
New Settings tab: enable toggle, config fields, sign-in/out + status. OnlineLoginService runs the PKCE loopback flow (Duende.IdentityModel.OidcClient 7.1.0), opens the system browser, captures the callback, hands the refresh token to the Worker. en/de localized. Fixes: loopback callback URL built from host:port base (avoids doubled redirect path); PollIntervalSeconds threaded through the state DTO so it loads instead of resetting to 60. Visual layout + the live sign-in round-trip need manual verification. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -123,6 +123,7 @@ sealed class Program
|
||||
sc.AddTransient<Func<WorktreesOverviewModalViewModel>>(sp => () => sp.GetRequiredService<WorktreesOverviewModalViewModel>());
|
||||
sc.AddSingleton<IPrimeScheduleApi, WorkerPrimeScheduleApi>();
|
||||
sc.AddSingleton<INotesApi, WorkerNotesApi>();
|
||||
sc.AddSingleton<IOnlineLoginService, OnlineLoginService>();
|
||||
sc.AddTransient<PrimeClaudeTabViewModel>();
|
||||
sc.AddTransient<SettingsModalViewModel>();
|
||||
sc.AddTransient<MergeModalViewModel>();
|
||||
|
||||
@@ -63,6 +63,26 @@
|
||||
"daySa": "Sa",
|
||||
"daySu": "So"
|
||||
},
|
||||
"onlineInbox": {
|
||||
"tabHeader": "Online-Posteingang",
|
||||
"enabledLabel": "Online-Posteingang-Sync aktivieren",
|
||||
"restartHint": "Aktivieren oder Deaktivieren wird erst nach einem Worker-Neustart wirksam.",
|
||||
"apiBaseUrlLabel": "API-Basis-URL",
|
||||
"apiBaseUrlPlaceholder": "https://inbox.claudedo.kuns.dev",
|
||||
"authorityLabel": "Zitadel-Authority (Issuer-URL)",
|
||||
"authorityPlaceholder": "https://auth.example.com",
|
||||
"clientIdLabel": "Client-ID",
|
||||
"scopesLabel": "Scopes",
|
||||
"redirectUriLabel": "Redirect-URI",
|
||||
"pollIntervalLabel": "Abfrageintervall (Sekunden)",
|
||||
"statusSection": "AUTH-STATUS",
|
||||
"signedInStatus": "Angemeldet",
|
||||
"signedOutStatus": "Nicht angemeldet",
|
||||
"signInButton": "Im Browser anmelden",
|
||||
"signOutButton": "Abmelden",
|
||||
"configSection": "KONFIGURATION",
|
||||
"saveButton": "Konfiguration speichern"
|
||||
},
|
||||
"inherit": {
|
||||
"inheritedFromList": "geerbt · Liste",
|
||||
"inheritedFromGlobal": "geerbt · Global",
|
||||
@@ -427,6 +447,7 @@
|
||||
"merge": { "commitMessage": "Merge-Aufgabe: {0}", "workerOfflineBranches": "Worker offline — Branches können nicht aufgelistet werden.", "loadBranchesFailed": "Branches konnten nicht geladen werden: {0}", "merged": "Zusammengeführt.", "conflict": "Merge-Konflikt — Ziel-Branch wiederhergestellt. Manuell oder über Fortsetzen lösen, dann erneut versuchen.", "blocked": "Blockiert: {0}", "unknownStatus": "Unbekannter Status: {0}", "mergeFailed": "Merge fehlgeschlagen: {0}" },
|
||||
"conflictResolution": { "vsCodeError": "VS Code konnte nicht gestartet werden: {0}. Die Pfade sind oben aufgeführt — kopiere sie manuell.", "subtaskPrefix": "Konflikte in Teilaufgabe: {0}", "targetPrefix": "Zusammenführen in: {0}" },
|
||||
"settingsModal": { "workerOffline": "Worker offline — Einstellungen schreibgeschützt.", "saveFailed": "Speichern fehlgeschlagen: {0}" },
|
||||
"onlineInbox": { "workerOffline": "Worker offline — Konfiguration kann nicht geladen werden.", "saved": "Konfiguration gespeichert.", "saveFailed": "Speichern fehlgeschlagen: {0}", "signedIn": "Erfolgreich angemeldet.", "signInFailed": "Anmeldung fehlgeschlagen: {0}", "signedOut": "Abgemeldet.", "signOutFailed": "Abmeldung fehlgeschlagen: {0}" },
|
||||
"weeklyReport": { "invalidRange": "Ungültiger Datumsbereich.", "generating": "Bericht wird erstellt…", "error": "Fehler: {0}" },
|
||||
"filesTab": { "workerOffline": "Worker offline.", "noneBundled": "Keine Standard-Agenten mitgeliefert.", "allPresent": "Alle Standard-Agenten bereits vorhanden.", "restored": "{0} Standard-Agent(en) wiederhergestellt.", "restoreFailed": "Wiederherstellung fehlgeschlagen: {0}", "openFailed": "Öffnen fehlgeschlagen: {0}" },
|
||||
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "{0} Worktree(s) entfernt.", "blocked": "Zwangsentfernung nicht möglich: {0} Aufgabe(n) laufen noch. Brich sie zuerst ab.", "removedFrom": "{0} Worktree(s) von {1} Aufgabe(n) entfernt." },
|
||||
|
||||
@@ -63,6 +63,26 @@
|
||||
"daySa": "Sa",
|
||||
"daySu": "Su"
|
||||
},
|
||||
"onlineInbox": {
|
||||
"tabHeader": "Online Inbox",
|
||||
"enabledLabel": "Enable online inbox sync",
|
||||
"restartHint": "Enabling or disabling takes effect after a Worker restart.",
|
||||
"apiBaseUrlLabel": "API base URL",
|
||||
"apiBaseUrlPlaceholder": "https://inbox.claudedo.kuns.dev",
|
||||
"authorityLabel": "Zitadel authority (issuer URL)",
|
||||
"authorityPlaceholder": "https://auth.example.com",
|
||||
"clientIdLabel": "Client ID",
|
||||
"scopesLabel": "Scopes",
|
||||
"redirectUriLabel": "Redirect URI",
|
||||
"pollIntervalLabel": "Poll interval (seconds)",
|
||||
"statusSection": "AUTH STATUS",
|
||||
"signedInStatus": "Signed in",
|
||||
"signedOutStatus": "Not signed in",
|
||||
"signInButton": "Sign in via browser",
|
||||
"signOutButton": "Sign out",
|
||||
"configSection": "CONFIGURATION",
|
||||
"saveButton": "Save config"
|
||||
},
|
||||
"inherit": {
|
||||
"inheritedFromList": "inherited · List",
|
||||
"inheritedFromGlobal": "inherited · Global",
|
||||
@@ -427,6 +447,7 @@
|
||||
"merge": { "commitMessage": "Merge task: {0}", "workerOfflineBranches": "Worker offline — cannot list branches.", "loadBranchesFailed": "Failed to load branches: {0}", "merged": "Merged.", "conflict": "Merge conflict — target branch restored. Resolve manually or via Continue, then retry.", "blocked": "Blocked: {0}", "unknownStatus": "Unknown status: {0}", "mergeFailed": "Merge failed: {0}" },
|
||||
"conflictResolution": { "vsCodeError": "Could not launch VS Code: {0}. Paths are listed above — copy them manually.", "subtaskPrefix": "Conflicts in subtask: {0}", "targetPrefix": "Merging into: {0}" },
|
||||
"settingsModal": { "workerOffline": "Worker offline — settings read-only.", "saveFailed": "Save failed: {0}" },
|
||||
"onlineInbox": { "workerOffline": "Worker offline — cannot load config.", "saved": "Config saved.", "saveFailed": "Save failed: {0}", "signedIn": "Signed in successfully.", "signInFailed": "Sign-in failed: {0}", "signedOut": "Signed out.", "signOutFailed": "Sign-out failed: {0}" },
|
||||
"weeklyReport": { "invalidRange": "Invalid date range.", "generating": "Generating report…", "error": "Error: {0}" },
|
||||
"filesTab": { "workerOffline": "Worker offline.", "noneBundled": "No default agents bundled.", "allPresent": "All default agents already present.", "restored": "Restored {0} default agent(s).", "restoreFailed": "Restore failed: {0}", "openFailed": "Open failed: {0}" },
|
||||
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "Removed {0} worktree(s).", "blocked": "Cannot force-remove: {0} task(s) still running. Cancel them first.", "removedFrom": "Removed {0} worktree(s) from {1} task(s)." },
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
<PackageReference Include="Duende.IdentityModel.OidcClient" Version="7.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
10
src/ClaudeDo.Ui/Services/Interfaces/IOnlineLoginService.cs
Normal file
10
src/ClaudeDo.Ui/Services/Interfaces/IOnlineLoginService.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace ClaudeDo.Ui.Services;
|
||||
|
||||
public sealed record OnlineLoginResult(bool Success, string? RefreshToken, string? Error);
|
||||
|
||||
public interface IOnlineLoginService
|
||||
{
|
||||
Task<OnlineLoginResult> LoginAsync(
|
||||
string authority, string clientId, string scope, string redirectUri,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
137
src/ClaudeDo.Ui/Services/OnlineLoginService.cs
Normal file
137
src/ClaudeDo.Ui/Services/OnlineLoginService.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Duende.IdentityModel.OidcClient;
|
||||
using Duende.IdentityModel.OidcClient.Browser;
|
||||
|
||||
namespace ClaudeDo.Ui.Services;
|
||||
|
||||
public sealed class OnlineLoginService : IOnlineLoginService
|
||||
{
|
||||
public async Task<OnlineLoginResult> LoginAsync(
|
||||
string authority, string clientId, string scope, string redirectUri,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var browser = new LoopbackBrowser(redirectUri);
|
||||
var options = new OidcClientOptions
|
||||
{
|
||||
Authority = authority,
|
||||
ClientId = clientId,
|
||||
Scope = scope,
|
||||
RedirectUri = redirectUri,
|
||||
Browser = browser,
|
||||
};
|
||||
|
||||
var client = new OidcClient(options);
|
||||
var result = await client.LoginAsync(new LoginRequest(), ct);
|
||||
|
||||
if (result.IsError)
|
||||
return new OnlineLoginResult(false, null, result.Error);
|
||||
|
||||
if (string.IsNullOrEmpty(result.RefreshToken))
|
||||
return new OnlineLoginResult(false, null,
|
||||
"No refresh token returned. Ensure 'offline_access' is in scope and the client allows it.");
|
||||
|
||||
return new OnlineLoginResult(true, result.RefreshToken, null);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new OnlineLoginResult(false, null, "Login cancelled.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new OnlineLoginResult(false, null, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IBrowser implementation: opens the system browser and captures the authorization
|
||||
/// response via a loopback HttpListener on the redirect URI's host/port.
|
||||
/// </summary>
|
||||
sealed class LoopbackBrowser : IBrowser
|
||||
{
|
||||
private static readonly TimeSpan Timeout = TimeSpan.FromMinutes(3);
|
||||
private readonly string _redirectUri;
|
||||
|
||||
public LoopbackBrowser(string redirectUri) => _redirectUri = redirectUri;
|
||||
|
||||
public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken ct = default)
|
||||
{
|
||||
// Derive the listener prefix from the redirect URI
|
||||
var uri = new Uri(_redirectUri);
|
||||
var prefix = $"{uri.Scheme}://{uri.Host}:{uri.Port}/";
|
||||
|
||||
using var listener = new HttpListener();
|
||||
listener.Prefixes.Add(prefix);
|
||||
|
||||
try
|
||||
{
|
||||
listener.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new BrowserResult
|
||||
{
|
||||
ResultType = BrowserResultType.UnknownError,
|
||||
Error = $"Could not start loopback listener on {prefix}: {ex.Message}"
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(options.StartUrl) { UseShellExecute = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new BrowserResult
|
||||
{
|
||||
ResultType = BrowserResultType.UnknownError,
|
||||
Error = $"Could not open browser: {ex.Message}"
|
||||
};
|
||||
}
|
||||
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
cts.CancelAfter(Timeout);
|
||||
|
||||
try
|
||||
{
|
||||
var context = await listener.GetContextAsync().WaitAsync(cts.Token);
|
||||
|
||||
var responseBody = Encoding.UTF8.GetBytes(
|
||||
"<html><body style=\"font-family:sans-serif;background:#0D1311;color:#E4EBE4;padding:40px\">" +
|
||||
"<h2>Login successful</h2><p>You may close this tab.</p></body></html>");
|
||||
|
||||
context.Response.ContentLength64 = responseBody.Length;
|
||||
context.Response.ContentType = "text/html; charset=utf-8";
|
||||
await context.Response.OutputStream.WriteAsync(responseBody, cts.Token);
|
||||
context.Response.OutputStream.Close();
|
||||
|
||||
// rawUrl already includes the redirect path (e.g. "/callback?code=..."),
|
||||
// so build the full URL from the scheme://host:port base — NOT the full
|
||||
// redirect URI, or the path would be doubled (".../callback/callback").
|
||||
var rawUrl = context.Request.RawUrl ?? "";
|
||||
var fullUri = prefix.TrimEnd('/') + rawUrl;
|
||||
|
||||
return new BrowserResult
|
||||
{
|
||||
ResultType = BrowserResultType.Success,
|
||||
Response = fullUri
|
||||
};
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new BrowserResult
|
||||
{
|
||||
ResultType = BrowserResultType.Timeout,
|
||||
Error = "Login timed out waiting for browser callback."
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
listener.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -588,7 +588,8 @@ public sealed record OnlineInboxStateDto(
|
||||
string ClientId,
|
||||
string Scopes,
|
||||
string RedirectUri,
|
||||
bool SignedIn);
|
||||
bool SignedIn,
|
||||
int PollIntervalSeconds);
|
||||
|
||||
public sealed record OnlineInboxConfigInputDto(
|
||||
bool Enabled,
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
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 = 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; }
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
||||
public WorktreesSettingsTabViewModel Worktrees { get; }
|
||||
public FilesSettingsTabViewModel Files { get; }
|
||||
public PrimeClaudeTabViewModel Prime { get; }
|
||||
public OnlineInboxSettingsViewModel OnlineInbox { get; }
|
||||
|
||||
[ObservableProperty] private string _validationError = "";
|
||||
[ObservableProperty] private bool _isBusy;
|
||||
@@ -25,6 +26,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
||||
public Action? CloseAction { get; set; }
|
||||
|
||||
public SettingsModalViewModel(IWorkerClient worker, PrimeClaudeTabViewModel prime,
|
||||
IOnlineLoginService onlineLoginService,
|
||||
ILocalizer localizer, AppSettings appSettings)
|
||||
{
|
||||
_worker = worker;
|
||||
@@ -36,6 +38,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
||||
Worktrees = new WorktreesSettingsTabViewModel(worker);
|
||||
Files = new FilesSettingsTabViewModel(worker);
|
||||
Prime = prime;
|
||||
OnlineInbox = new OnlineInboxSettingsViewModel(worker, onlineLoginService);
|
||||
}
|
||||
|
||||
public async Task LoadAsync()
|
||||
@@ -65,6 +68,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
||||
else StatusMessage = Loc.T("vm.settingsModal.workerOffline");
|
||||
|
||||
await Prime.LoadAsync();
|
||||
await OnlineInbox.LoadAsync();
|
||||
}
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
|
||||
@@ -261,6 +261,99 @@
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="{loc:Tr settings.onlineInbox.tabHeader}">
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="14" Margin="0,8,0,0">
|
||||
|
||||
<!-- Enable toggle + restart hint -->
|
||||
<StackPanel Spacing="4">
|
||||
<CheckBox IsChecked="{Binding OnlineInbox.Enabled, Mode=TwoWay}"
|
||||
Content="{loc:Tr settings.onlineInbox.enabledLabel}"/>
|
||||
<TextBlock Classes="meta" Text="{loc:Tr settings.onlineInbox.restartHint}"
|
||||
TextWrapping="Wrap" Opacity="0.6"/>
|
||||
</StackPanel>
|
||||
|
||||
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0" Margin="0,2,0,0"/>
|
||||
|
||||
<!-- Auth status section -->
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="section-label" Text="{loc:Tr settings.onlineInbox.statusSection}"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" VerticalAlignment="Center"
|
||||
IsVisible="{Binding OnlineInbox.SignedIn}">
|
||||
<Border Width="8" Height="8" CornerRadius="4"
|
||||
Background="{DynamicResource StatusRunningBrush}"/>
|
||||
<TextBlock Classes="body" VerticalAlignment="Center"
|
||||
Text="{loc:Tr settings.onlineInbox.signedInStatus}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" VerticalAlignment="Center"
|
||||
IsVisible="{Binding !OnlineInbox.SignedIn}">
|
||||
<Border Width="8" Height="8" CornerRadius="4"
|
||||
Background="{DynamicResource StatusIdleBrush}"/>
|
||||
<TextBlock Classes="body" VerticalAlignment="Center"
|
||||
Text="{loc:Tr settings.onlineInbox.signedOutStatus}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="btn"
|
||||
Content="{loc:Tr settings.onlineInbox.signInButton}"
|
||||
Command="{Binding OnlineInbox.SignInCommand}"
|
||||
IsEnabled="{Binding !OnlineInbox.IsBusy}"
|
||||
IsVisible="{Binding !OnlineInbox.SignedIn}"/>
|
||||
<Button Classes="btn danger"
|
||||
Content="{loc:Tr settings.onlineInbox.signOutButton}"
|
||||
Command="{Binding OnlineInbox.SignOutCommand}"
|
||||
IsEnabled="{Binding !OnlineInbox.IsBusy}"
|
||||
IsVisible="{Binding OnlineInbox.SignedIn}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0" Margin="0,2,0,0"/>
|
||||
|
||||
<!-- Config fields -->
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Classes="section-label" Text="{loc:Tr settings.onlineInbox.configSection}"/>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.onlineInbox.apiBaseUrlLabel}"/>
|
||||
<TextBox Text="{Binding OnlineInbox.ApiBaseUrl, Mode=TwoWay}"
|
||||
PlaceholderText="{loc:Tr settings.onlineInbox.apiBaseUrlPlaceholder}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.onlineInbox.authorityLabel}"/>
|
||||
<TextBox Text="{Binding OnlineInbox.Authority, Mode=TwoWay}"
|
||||
PlaceholderText="{loc:Tr settings.onlineInbox.authorityPlaceholder}"/>
|
||||
</StackPanel>
|
||||
<Grid ColumnDefinitions="*,12,*">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.onlineInbox.clientIdLabel}"/>
|
||||
<TextBox Text="{Binding OnlineInbox.ClientId, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Spacing="4">
|
||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.onlineInbox.scopesLabel}"/>
|
||||
<TextBox Text="{Binding OnlineInbox.Scopes, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.onlineInbox.redirectUriLabel}"/>
|
||||
<TextBox Text="{Binding OnlineInbox.RedirectUri, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.onlineInbox.pollIntervalLabel}"/>
|
||||
<NumericUpDown Value="{Binding OnlineInbox.PollIntervalSeconds, Mode=TwoWay}"
|
||||
Minimum="10" Maximum="3600" Increment="10" FormatString="0"
|
||||
HorizontalAlignment="Left" Width="140"/>
|
||||
</StackPanel>
|
||||
<Button Classes="btn"
|
||||
Content="{loc:Tr settings.onlineInbox.saveButton}"
|
||||
Command="{Binding OnlineInbox.SaveCommand}"
|
||||
IsEnabled="{Binding !OnlineInbox.IsBusy}"
|
||||
HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Classes="meta" Text="{Binding OnlineInbox.StatusMessage}"
|
||||
IsVisible="{Binding OnlineInbox.StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
</TabControl>
|
||||
</DockPanel>
|
||||
|
||||
|
||||
@@ -74,7 +74,8 @@ public record OnlineInboxStateDto(
|
||||
string ClientId,
|
||||
string Scopes,
|
||||
string RedirectUri,
|
||||
bool SignedIn);
|
||||
bool SignedIn,
|
||||
int PollIntervalSeconds);
|
||||
|
||||
public record OnlineInboxConfigInput(
|
||||
bool Enabled,
|
||||
@@ -725,7 +726,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
_onlineInboxConfig.Zitadel.ClientId,
|
||||
_onlineInboxConfig.Zitadel.Scopes,
|
||||
_onlineInboxConfig.RedirectUri,
|
||||
signedIn);
|
||||
signedIn,
|
||||
_onlineInboxConfig.PollIntervalSeconds);
|
||||
}
|
||||
|
||||
public void SetOnlineInboxConfig(OnlineInboxConfigInput input)
|
||||
|
||||
Reference in New Issue
Block a user