Initial project snapshot
This commit is contained in:
+26
@@ -0,0 +1,26 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace RemoteAccessPlatform.Windows.Application.Common;
|
||||
|
||||
public abstract class ObservableObject : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Diagnostics;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace RemoteAccessPlatform.Windows.Application.Common;
|
||||
|
||||
public sealed class RelayCommand(Action execute, Func<bool>? canExecute = null) : ICommand
|
||||
{
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public bool CanExecute(object? parameter) => canExecute?.Invoke() ?? true;
|
||||
|
||||
public void Execute(object? parameter) => execute();
|
||||
|
||||
public void NotifyCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public sealed class RelayCommand<T>(Action<T?> execute, Func<T?, bool>? canExecute = null) : ICommand
|
||||
{
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public bool CanExecute(object? parameter) => canExecute?.Invoke((T?)parameter) ?? true;
|
||||
|
||||
public void Execute(object? parameter) => execute((T?)parameter);
|
||||
|
||||
public void NotifyCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public sealed class AsyncRelayCommand(Func<Task> executeAsync, Func<bool>? canExecute = null) : ICommand
|
||||
{
|
||||
private bool _isExecuting;
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public bool CanExecute(object? parameter) => !_isExecuting && (canExecute?.Invoke() ?? true);
|
||||
|
||||
public async void Execute(object? parameter)
|
||||
{
|
||||
if (!CanExecute(parameter))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isExecuting = true;
|
||||
NotifyCanExecuteChanged();
|
||||
try
|
||||
{
|
||||
await executeAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"[{DateTimeOffset.Now:O}] AsyncRelayCommand ignored unhandled command exception: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isExecuting = false;
|
||||
NotifyCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void NotifyCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public sealed class AsyncRelayCommand<T>(Func<T?, Task> executeAsync, Func<T?, bool>? canExecute = null) : ICommand
|
||||
{
|
||||
private bool _isExecuting;
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public bool CanExecute(object? parameter) => !_isExecuting && (canExecute?.Invoke((T?)parameter) ?? true);
|
||||
|
||||
public async void Execute(object? parameter)
|
||||
{
|
||||
if (!CanExecute(parameter))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isExecuting = true;
|
||||
NotifyCanExecuteChanged();
|
||||
try
|
||||
{
|
||||
await executeAsync((T?)parameter);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"[{DateTimeOffset.Now:O}] AsyncRelayCommand<T> ignored unhandled command exception: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isExecuting = false;
|
||||
NotifyCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void NotifyCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System.Globalization;
|
||||
using System.Resources;
|
||||
|
||||
namespace RemoteAccessPlatform.Windows.Application.Localization;
|
||||
|
||||
public static class Strings
|
||||
{
|
||||
private static readonly ResourceManager ResourceManager = new("RemoteAccessPlatform.Windows.Application.Resources.Strings", typeof(Strings).Assembly);
|
||||
|
||||
public static CultureInfo? Culture
|
||||
{
|
||||
get => CultureInfo.CurrentUICulture;
|
||||
set
|
||||
{
|
||||
if (value is not null)
|
||||
{
|
||||
CultureInfo.CurrentUICulture = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static string MainWindowTitle => Get("ui.main.title");
|
||||
public static string MainHeaderTitle => Get("ui.main.title");
|
||||
public static string MainLogout => Get("ui.main.logout");
|
||||
public static string MainStartSession => Get("ui.main.start_session");
|
||||
public static string MainAttach => Get("ui.main.attach");
|
||||
public static string MainTakeOver => Get("ui.main.take_over");
|
||||
public static string MainTerminate => Get("ui.main.terminate");
|
||||
|
||||
public static string LoginHeading => Get("ui.login.heading");
|
||||
public static string LoginSubtitle => Get("ui.login.subtitle");
|
||||
public static string LoginEmail => Get("ui.login.email");
|
||||
public static string LoginPassword => Get("ui.login.password");
|
||||
public static string LoginTrustDevice => Get("ui.login.trust_device");
|
||||
public static string LoginSignIn => Get("ui.login.sign_in");
|
||||
|
||||
public static string OrganizationHeading => Get("ui.organization.heading");
|
||||
public static string OrganizationSubtitle => Get("ui.organization.subtitle");
|
||||
public static string OrganizationSetActive => Get("ui.organization.set_active");
|
||||
|
||||
public static string ResourceHeading => Get("ui.resources.heading");
|
||||
public static string RefreshResources => Get("ui.resources.refresh");
|
||||
public static string ResourceColumnName => Get("ui.resources.column.name");
|
||||
public static string ResourceColumnType => Get("ui.resources.column.type");
|
||||
public static string ResourceColumnHost => Get("ui.resources.column.host");
|
||||
public static string ResourceColumnCert => Get("ui.resources.column.cert");
|
||||
|
||||
public static string SessionsHeading => Get("ui.sessions.heading");
|
||||
public static string RefreshSessions => Get("ui.sessions.refresh");
|
||||
public static string SessionColumnSession => Get("ui.sessions.column.session");
|
||||
public static string SessionColumnState => Get("ui.sessions.column.state");
|
||||
public static string SessionColumnProtocol => Get("ui.sessions.column.protocol");
|
||||
public static string SessionColumnWorker => Get("ui.sessions.column.worker");
|
||||
public static string SessionColumnTakeover => Get("ui.sessions.column.takeover");
|
||||
|
||||
public static string SessionWindowReconnect => Get("ui.session_window.reconnect");
|
||||
public static string SessionWindowDetach => Get("ui.session_window.detach");
|
||||
public static string SessionWindowTakeOver => Get("ui.session_window.take_over");
|
||||
public static string SessionWindowTerminate => Get("ui.session_window.terminate");
|
||||
public static string SessionWindowIntro => Get("ui.session_window.intro");
|
||||
public static string SessionWindowEventLog => Get("ui.session_window.event_log");
|
||||
public static string SessionWindowRenderHeading => Get("ui.session_window.render.heading");
|
||||
public static string SessionWindowClipboardSend => Get("ui.session_window.clipboard.send");
|
||||
public static string SessionWindowClipboardReceive => Get("ui.session_window.clipboard.receive");
|
||||
public static string SessionWindowFileUpload => Get("ui.session_window.file_upload.send");
|
||||
public static string SessionWindowFileUploadCancel => Get("ui.session_window.file_upload.cancel");
|
||||
public static string SessionWindowFileDownload => Get("ui.session_window.file_download.receive");
|
||||
public static string SessionWindowFileDownloadCancel => Get("ui.session_window.file_download.cancel");
|
||||
public static string SessionWindowSurfaceTitle => Get("ui.session_window.surface_title");
|
||||
public static string SessionWindowSurfaceSubtitle => Get("ui.session_window.surface_subtitle");
|
||||
|
||||
public static string Get(string key)
|
||||
{
|
||||
return ResourceManager.GetString(key, Culture) ?? key;
|
||||
}
|
||||
|
||||
public static string Format(string key, params object[] args)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentUICulture, Get(key), args);
|
||||
}
|
||||
|
||||
public static string ResolveBackendMessage(string? messageKey, string? fallbackMessage, string? code = null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(messageKey))
|
||||
{
|
||||
string? localized = ResourceManager.GetString(messageKey!, Culture);
|
||||
if (!string.IsNullOrWhiteSpace(localized))
|
||||
{
|
||||
return localized;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(fallbackMessage))
|
||||
{
|
||||
return fallbackMessage!;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(code))
|
||||
{
|
||||
string? localized = ResourceManager.GetString("errors." + code, Culture);
|
||||
if (!string.IsNullOrWhiteSpace(localized))
|
||||
{
|
||||
return localized;
|
||||
}
|
||||
}
|
||||
|
||||
return Get("errors.common.unexpected");
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RemoteAccessPlatform.Windows.Contracts\RemoteAccessPlatform.Windows.Contracts.csproj" />
|
||||
<ProjectReference Include="..\RemoteAccessPlatform.Windows.Models\RemoteAccessPlatform.Windows.Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,189 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<resheader name="resmimetype"><value>text/microsoft-resx</value></resheader>
|
||||
<resheader name="version"><value>2.0</value></resheader>
|
||||
<resheader name="reader"><value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></resheader>
|
||||
<resheader name="writer"><value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></resheader>
|
||||
<data name="ui.main.title"><value>Remote Access Platform</value></data>
|
||||
<data name="ui.main.logout"><value>Logout</value></data>
|
||||
<data name="ui.main.start_session"><value>Start Session</value></data>
|
||||
<data name="ui.main.attach"><value>Attach</value></data>
|
||||
<data name="ui.main.take_over"><value>Take Over</value></data>
|
||||
<data name="ui.main.terminate"><value>Terminate</value></data>
|
||||
<data name="ui.login.heading"><value>Secure Access</value></data>
|
||||
<data name="ui.login.subtitle"><value>Sign in to load organizations, resources, and active sessions.</value></data>
|
||||
<data name="ui.login.email"><value>Login</value></data>
|
||||
<data name="ui.login.password"><value>Password</value></data>
|
||||
<data name="ui.login.trust_device"><value>Trust this device for refresh sessions</value></data>
|
||||
<data name="ui.login.sign_in"><value>Sign In</value></data>
|
||||
<data name="ui.organization.heading"><value>Organization Context</value></data>
|
||||
<data name="ui.organization.subtitle"><value>The selected organization scopes resources and admin actions.</value></data>
|
||||
<data name="ui.organization.set_active"><value>Set Active Org</value></data>
|
||||
<data name="ui.resources.heading"><value>Accessible Resources</value></data>
|
||||
<data name="ui.resources.refresh"><value>Refresh</value></data>
|
||||
<data name="ui.resources.column.name"><value>Name</value></data>
|
||||
<data name="ui.resources.column.type"><value>Type</value></data>
|
||||
<data name="ui.resources.column.host"><value>Host</value></data>
|
||||
<data name="ui.resources.column.cert"><value>Cert</value></data>
|
||||
<data name="ui.sessions.heading"><value>Active Sessions</value></data>
|
||||
<data name="ui.sessions.refresh"><value>Refresh</value></data>
|
||||
<data name="ui.sessions.column.session"><value>Session</value></data>
|
||||
<data name="ui.sessions.column.state"><value>State</value></data>
|
||||
<data name="ui.sessions.column.protocol"><value>Protocol</value></data>
|
||||
<data name="ui.sessions.column.worker"><value>Worker</value></data>
|
||||
<data name="ui.sessions.column.takeover"><value>Takeover</value></data>
|
||||
<data name="ui.session_window.reconnect"><value>Reconnect</value></data>
|
||||
<data name="ui.session_window.detach"><value>Detach</value></data>
|
||||
<data name="ui.session_window.take_over"><value>Take Over</value></data>
|
||||
<data name="ui.session_window.terminate"><value>Terminate</value></data>
|
||||
<data name="ui.session_window.intro"><value>This MVP window proves attach, detach, reconnect, takeover, and terminate flows over the existing session gateway. Rendering is intentionally deferred to future viewer work.</value></data>
|
||||
<data name="ui.session_window.event_log"><value>Session Event Log</value></data>
|
||||
<data name="ui.session_window.render.heading"><value>Rendering</value></data>
|
||||
<data name="ui.session_window.render_profile_prefix"><value>Render profile: {0}</value></data>
|
||||
<data name="ui.session_window.render_state_prefix"><value>Render state: {0}</value></data>
|
||||
<data name="ui.session_window.render_size_prefix"><value>Surface size: {0}</value></data>
|
||||
<data name="ui.session_window.render_size.unknown"><value>unknown</value></data>
|
||||
<data name="ui.session_window.render_cursor_prefix"><value>Cursor: {0}</value></data>
|
||||
<data name="ui.session_window.render_cursor_value"><value>{0} @ {1}, {2}</value></data>
|
||||
<data name="ui.session_window.render_cursor.visible"><value>visible</value></data>
|
||||
<data name="ui.session_window.render_cursor.hidden"><value>hidden</value></data>
|
||||
<data name="ui.session_window.render_telemetry_prefix"><value>Dirty updates: {0}; last render: {1}</value></data>
|
||||
<data name="ui.session_window.render_last_update.unknown"><value>unknown</value></data>
|
||||
<data name="ui.session_window.clipboard.send"><value>Send Clipboard Text</value></data>
|
||||
<data name="ui.session_window.clipboard.receive"><value>Receive Clipboard Text</value></data>
|
||||
<data name="ui.session_window.file_upload.send"><value>Upload File</value></data>
|
||||
<data name="ui.session_window.file_upload.cancel"><value>Cancel Upload</value></data>
|
||||
<data name="ui.session_window.file_upload.ready"><value>File upload is available only for active sessions allowed by resource policy.</value></data>
|
||||
<data name="ui.session_window.file_download.receive"><value>Download File</value></data>
|
||||
<data name="ui.session_window.file_download.cancel"><value>Cancel Download</value></data>
|
||||
<data name="ui.session_window.file_download.ready"><value>Copy a file into RAP_Transfers\ToClient inside the remote session to make it available for download.</value></data>
|
||||
<data name="ui.session_window.file_download.none"><value>No remote files are available for download.</value></data>
|
||||
<data name="ui.session_window.file_download.available"><value>Available: {0} ({1} bytes)</value></data>
|
||||
<data name="ui.session_window.clipboard.connecting"><value>Clipboard is waiting for an active live session.</value></data>
|
||||
<data name="ui.session_window.clipboard.ready"><value>Text clipboard is available when allowed by resource policy.</value></data>
|
||||
<data name="ui.session_window.clipboard.reconnecting"><value>Clipboard is paused while the session is reconnecting or updating.</value></data>
|
||||
<data name="ui.session_window.clipboard.detached"><value>Clipboard is disabled while the session is detached.</value></data>
|
||||
<data name="ui.session_window.clipboard.unavailable"><value>Clipboard is unavailable because the session is no longer active.</value></data>
|
||||
<data name="ui.session_window.clipboard.taken_over"><value>Clipboard is disabled because another controller took over the session.</value></data>
|
||||
<data name="ui.session_window.render.profile.low_bandwidth"><value>Low bandwidth</value></data>
|
||||
<data name="ui.session_window.render.profile.balanced"><value>Balanced</value></data>
|
||||
<data name="ui.session_window.render.profile.high_quality"><value>High quality</value></data>
|
||||
<data name="ui.session_window.render.profile.text_priority"><value>Text priority</value></data>
|
||||
<data name="ui.session_window.render.state.connecting"><value>Connecting</value></data>
|
||||
<data name="ui.session_window.render.state.ready"><value>Ready</value></data>
|
||||
<data name="ui.session_window.render.state.dirty"><value>Dirty updates</value></data>
|
||||
<data name="ui.session_window.render.state.resized"><value>Resized</value></data>
|
||||
<data name="ui.session_window.render.state.cursor"><value>Cursor updated</value></data>
|
||||
<data name="ui.session_window.render.state.detached"><value>Detached</value></data>
|
||||
<data name="ui.session_window.render.state.reconnecting"><value>Reconnecting</value></data>
|
||||
<data name="ui.session_window.render.state.taken_over"><value>Taken over</value></data>
|
||||
<data name="ui.session_window.render.state.failed"><value>Failed</value></data>
|
||||
<data name="ui.session_window.render.state.terminated"><value>Terminated</value></data>
|
||||
<data name="ui.session_window.render.state.unknown"><value>Unknown</value></data>
|
||||
<data name="ui.session_window.state_prefix"><value>Session state: {0}</value></data>
|
||||
<data name="ui.session_window.gateway_prefix"><value>Gateway: {0}</value></data>
|
||||
<data name="ui.session_window.shell_state_prefix"><value>Shell state: {0}</value></data>
|
||||
<data name="ui.session_window.summary.connecting"><value>Preparing the remote session shell and waiting for the live gateway connection.</value></data>
|
||||
<data name="ui.session_window.summary.active"><value>The remote RDP session is active and attached to this window.</value></data>
|
||||
<data name="ui.session_window.summary.detached"><value>The remote session is still running on the worker. You can reconnect without recreating it.</value></data>
|
||||
<data name="ui.session_window.summary.failed"><value>The remote session failed. Review the event log and start a new session if needed.</value></data>
|
||||
<data name="ui.session_window.summary.terminated"><value>The remote session has been terminated and will not accept reconnects.</value></data>
|
||||
<data name="ui.session_window.summary.taken_over"><value>This window is no longer the active controller for the remote session.</value></data>
|
||||
<data name="ui.session_window.actions.connecting"><value>Available actions will become active once the session finishes connecting.</value></data>
|
||||
<data name="ui.session_window.actions.active"><value>You can detach, terminate, or hand over control from this window.</value></data>
|
||||
<data name="ui.session_window.actions.detached"><value>Reconnect is available while the session keeps running on the worker.</value></data>
|
||||
<data name="ui.session_window.actions.failed"><value>This session is no longer usable. Close the window or return to the session list.</value></data>
|
||||
<data name="ui.session_window.actions.terminated"><value>This session is complete. Only close the window or start a new session from the main shell.</value></data>
|
||||
<data name="ui.session_window.actions.taken_over"><value>Control moved to another controller. This window remains read-only for status and history.</value></data>
|
||||
<data name="ui.session_window.actions.updating"><value>Updating remote session state. Commands are temporarily disabled.</value></data>
|
||||
<data name="ui.session_window.surface_title"><value>Session Input Surface</value></data>
|
||||
<data name="ui.session_window.surface_subtitle"><value>Focus this surface to send keyboard and mouse input to the active remote RDP session.</value></data>
|
||||
<data name="ui.session_window.input.connecting"><value>Input is waiting for an active live session.</value></data>
|
||||
<data name="ui.session_window.input.ready"><value>Click inside the surface to focus it before typing or using the mouse.</value></data>
|
||||
<data name="ui.session_window.input.focused"><value>Input focus is active in this window.</value></data>
|
||||
<data name="ui.session_window.input.reconnecting"><value>Input is paused while the session is reconnecting or updating.</value></data>
|
||||
<data name="ui.session_window.input.detached"><value>Input is disabled while the session is detached.</value></data>
|
||||
<data name="ui.session_window.input.unavailable"><value>Input is unavailable because the session is no longer active.</value></data>
|
||||
<data name="ui.session_window.input.taken_over"><value>Input is disabled because another controller took over the session.</value></data>
|
||||
<data name="status.ready"><value>Ready</value></data>
|
||||
<data name="status.auth.logging_in"><value>Logging in...</value></data>
|
||||
<data name="status.auth.logged_in"><value>Login successful</value></data>
|
||||
<data name="status.auth.logging_out"><value>Logging out...</value></data>
|
||||
<data name="status.auth.logged_out"><value>Logged out</value></data>
|
||||
<data name="status.auth.session_expired"><value>Session expired. Please sign in again.</value></data>
|
||||
<data name="status.organization.switching"><value>Switching organization...</value></data>
|
||||
<data name="status.organization.active"><value>Active organization: {0}</value></data>
|
||||
<data name="status.resources.refreshing"><value>Refreshing resources...</value></data>
|
||||
<data name="status.resources.loaded"><value>Loaded {0} resources</value></data>
|
||||
<data name="status.sessions.refreshing"><value>Refreshing sessions...</value></data>
|
||||
<data name="status.sessions.loaded"><value>Loaded {0} sessions</value></data>
|
||||
<data name="status.workspace.ready"><value>Workspace ready</value></data>
|
||||
<data name="status.session.starting"><value>Starting session for {0}...</value></data>
|
||||
<data name="status.session.attaching"><value>Attaching to {0}...</value></data>
|
||||
<data name="status.session.taking_over"><value>Taking over {0}...</value></data>
|
||||
<data name="status.session.terminating"><value>Terminating {0}...</value></data>
|
||||
<data name="status.session.terminated"><value>Terminated session {0}</value></data>
|
||||
<data name="status.session.window.start"><value>Start: {0}</value></data>
|
||||
<data name="status.session.window.attach"><value>Attach: {0}</value></data>
|
||||
<data name="status.session.window.takeover"><value>Takeover: {0}</value></data>
|
||||
<data name="status.session.connection.pending"><value>Pending</value></data>
|
||||
<data name="status.session.connection.connecting"><value>Connecting</value></data>
|
||||
<data name="status.session.connection.reconnecting"><value>Reconnecting</value></data>
|
||||
<data name="status.session.connection.connected"><value>Connected</value></data>
|
||||
<data name="status.session.connection.taken_over"><value>Taken over by another controller</value></data>
|
||||
<data name="status.session.connection.transport_closed"><value>Transport closed</value></data>
|
||||
<data name="status.session.connection.disconnected"><value>Disconnected</value></data>
|
||||
<data name="status.session.connection.detached"><value>Detached</value></data>
|
||||
<data name="status.session.connection.failed"><value>Session failed</value></data>
|
||||
<data name="status.session.connection.terminated"><value>Session terminated</value></data>
|
||||
<data name="status.session.shell.connecting"><value>Connecting</value></data>
|
||||
<data name="status.session.shell.active"><value>Active</value></data>
|
||||
<data name="status.session.shell.detached"><value>Detached</value></data>
|
||||
<data name="status.session.shell.reconnecting"><value>Reconnecting</value></data>
|
||||
<data name="status.session.shell.failed"><value>Failed</value></data>
|
||||
<data name="status.session.shell.terminated"><value>Terminated</value></data>
|
||||
<data name="status.session.shell.taken_over"><value>Taken over</value></data>
|
||||
<data name="status.session.shell.updating"><value>Updating</value></data>
|
||||
<data name="events.session.window_created"><value>Session window created for {0}</value></data>
|
||||
<data name="events.session.gateway_connected"><value>Connected to session gateway</value></data>
|
||||
<data name="events.session.detached"><value>Detached from session</value></data>
|
||||
<data name="events.session.takeover_requested"><value>Takeover requested for this device</value></data>
|
||||
<data name="events.session.terminated"><value>Session terminated</value></data>
|
||||
<data name="events.session.taken_over"><value>This session was taken over from another device.</value></data>
|
||||
<data name="events.session.frame"><value>The session desktop frame updated.</value></data>
|
||||
<data name="events.session.frame_received"><value>Desktop frame received.</value></data>
|
||||
<data name="events.session.failed_after_transport_closed"><value>Session failed after transport closed</value></data>
|
||||
<data name="events.session.terminated_after_transport_closed"><value>Session terminated after transport closed</value></data>
|
||||
<data name="events.session.transport"><value>Envelope: {0}</value></data>
|
||||
<data name="events.input.surface_focus_acquired"><value>Session input surface focused</value></data>
|
||||
<data name="events.input.surface_focus_lost"><value>Session input surface focus lost</value></data>
|
||||
<data name="events.input.keyboard_forwarded"><value>Keyboard input forwarding active</value></data>
|
||||
<data name="events.input.mouse_forwarded"><value>Mouse input forwarding active</value></data>
|
||||
<data name="events.clipboard.client_to_server_sent"><value>Clipboard text sent to the remote session.</value></data>
|
||||
<data name="events.clipboard.server_to_client_received"><value>Clipboard text received from the remote session.</value></data>
|
||||
<data name="events.clipboard.text_received"><value>Clipboard text received from the remote session.</value></data>
|
||||
<data name="events.clipboard.blocked"><value>Clipboard transfer is blocked by session state or resource policy.</value></data>
|
||||
<data name="events.file_upload.started"><value>Started uploading {0}.</value></data>
|
||||
<data name="events.file_upload.transferring"><value>Uploading file...</value></data>
|
||||
<data name="events.file_upload.completed"><value>Finished uploading {0}.</value></data>
|
||||
<data name="events.file_upload.gateway_started"><value>Gateway accepted file upload.</value></data>
|
||||
<data name="events.file_upload.gateway_completed"><value>Gateway routed all upload chunks to the worker.</value></data>
|
||||
<data name="events.file_upload.cancelled"><value>File upload cancelled.</value></data>
|
||||
<data name="events.file_upload.failed"><value>File upload failed.</value></data>
|
||||
<data name="events.file_upload.blocked"><value>File upload is blocked by session state or resource policy.</value></data>
|
||||
<data name="events.file_download.available"><value>Remote file is available for download: {0}.</value></data>
|
||||
<data name="events.file_download.started"><value>Started downloading {0}.</value></data>
|
||||
<data name="events.file_download.transferring"><value>Downloading file...</value></data>
|
||||
<data name="events.file_download.completed"><value>File download completed.</value></data>
|
||||
<data name="events.file_download.gateway_started"><value>Gateway accepted file download.</value></data>
|
||||
<data name="events.file_download.cancelled"><value>File download cancelled.</value></data>
|
||||
<data name="events.file_download.failed"><value>File download failed.</value></data>
|
||||
<data name="events.file_download.blocked"><value>File download is blocked by session state or resource policy.</value></data>
|
||||
<data name="errors.common.unexpected"><value>Something went wrong. Please try again.</value></data>
|
||||
<data name="errors.common.access_denied"><value>Access denied.</value></data>
|
||||
<data name="errors.common.internal_server_error"><value>An internal server error occurred.</value></data>
|
||||
<data name="errors.bad_request.user_id_is_required"><value>user_id is required</value></data>
|
||||
<data name="errors.auth.invalid_credentials"><value>Invalid credentials.</value></data>
|
||||
<data name="errors.auth.session_expired"><value>Session expired. Please sign in again.</value></data>
|
||||
<data name="events.session.transport_closed"><value>The session transport closed.</value></data>
|
||||
</root>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<resheader name="resmimetype"><value>text/microsoft-resx</value></resheader>
|
||||
<resheader name="version"><value>2.0</value></resheader>
|
||||
<resheader name="reader"><value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></resheader>
|
||||
<resheader name="writer"><value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></resheader>
|
||||
</root>
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
using RemoteAccessPlatform.Windows.Contracts;
|
||||
using RemoteAccessPlatform.Windows.Models;
|
||||
|
||||
namespace RemoteAccessPlatform.Windows.Application.Services;
|
||||
|
||||
public sealed class AuthenticationService
|
||||
{
|
||||
private readonly IBackendAuthClient _authClient;
|
||||
private readonly ITokenStore _tokenStore;
|
||||
private readonly IDeviceIdentityProvider _deviceIdentityProvider;
|
||||
private StoredAuthState? _current;
|
||||
|
||||
public AuthenticationService(IBackendAuthClient authClient, ITokenStore tokenStore, IDeviceIdentityProvider deviceIdentityProvider)
|
||||
{
|
||||
_authClient = authClient;
|
||||
_tokenStore = tokenStore;
|
||||
_deviceIdentityProvider = deviceIdentityProvider;
|
||||
}
|
||||
|
||||
public event Action? StateChanged;
|
||||
public event Action? SessionExpired;
|
||||
|
||||
public StoredAuthState? Current => _current;
|
||||
public bool IsAuthenticated => _current is not null;
|
||||
public string? CurrentUserId => _current?.User.ID;
|
||||
public string? CurrentDeviceId => _current?.Device.ID;
|
||||
public string? CurrentAccessToken => _current?.Tokens.AccessToken;
|
||||
|
||||
public async Task InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_current = await _tokenStore.LoadAsync(cancellationToken);
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
public async Task LoginAsync(string email, string password, bool trustDevice, CancellationToken cancellationToken)
|
||||
{
|
||||
string fingerprint = await _deviceIdentityProvider.GetOrCreateFingerprintAsync(cancellationToken);
|
||||
string label = await _deviceIdentityProvider.GetDeviceLabelAsync(cancellationToken);
|
||||
|
||||
AuthResultDto result = await _authClient.LoginAsync(new LoginRequest
|
||||
{
|
||||
Email = email,
|
||||
Password = password,
|
||||
TrustDevice = trustDevice,
|
||||
DeviceFingerprint = fingerprint,
|
||||
DeviceLabel = label
|
||||
}, cancellationToken);
|
||||
|
||||
_current = new StoredAuthState
|
||||
{
|
||||
User = result.User,
|
||||
Device = result.Device,
|
||||
AuthSession = result.AuthSession,
|
||||
Tokens = result.Tokens
|
||||
};
|
||||
await _tokenStore.SaveAsync(_current, cancellationToken);
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
public async Task<bool> TryRefreshAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_current is null || string.IsNullOrWhiteSpace(_current.Tokens.RefreshToken))
|
||||
{
|
||||
await ClearInternalAsync(cancellationToken, true);
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
AuthResultDto result = await _authClient.RefreshAsync(new RefreshRequest
|
||||
{
|
||||
RefreshToken = _current.Tokens.RefreshToken
|
||||
}, cancellationToken);
|
||||
|
||||
_current = new StoredAuthState
|
||||
{
|
||||
User = result.User,
|
||||
Device = result.Device,
|
||||
AuthSession = result.AuthSession,
|
||||
Tokens = result.Tokens
|
||||
};
|
||||
|
||||
await _tokenStore.SaveAsync(_current, cancellationToken);
|
||||
StateChanged?.Invoke();
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await ClearInternalAsync(cancellationToken, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LogoutAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_current is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _authClient.RevokeAuthSessionAsync(_current.User.ID, _current.AuthSession.ID, "client_logout", cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
await ClearInternalAsync(cancellationToken, false);
|
||||
}
|
||||
|
||||
public Task<string?> GetAccessTokenAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(_current?.Tokens.AccessToken);
|
||||
}
|
||||
|
||||
private async Task ClearInternalAsync(CancellationToken cancellationToken, bool expired)
|
||||
{
|
||||
_current = null;
|
||||
await _tokenStore.ClearAsync(cancellationToken);
|
||||
StateChanged?.Invoke();
|
||||
if (expired)
|
||||
{
|
||||
SessionExpired?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using RemoteAccessPlatform.Windows.Contracts;
|
||||
using RemoteAccessPlatform.Windows.Models;
|
||||
|
||||
namespace RemoteAccessPlatform.Windows.Application.Services;
|
||||
|
||||
public sealed class OrganizationContextService(IBackendOrganizationClient organizationClient, ILocalSettingsStore settingsStore)
|
||||
{
|
||||
public async Task<IReadOnlyList<OrganizationDto>> LoadOrganizationsAsync(string userId, CancellationToken cancellationToken)
|
||||
{
|
||||
return await organizationClient.GetOrganizationsAsync(userId, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<string?> GetSavedOrganizationIdAsync(string userId, CancellationToken cancellationToken)
|
||||
{
|
||||
LocalClientSettings settings = await settingsStore.LoadAsync(cancellationToken);
|
||||
return settings.LastOrganizationByUserId.TryGetValue(userId, out string? organizationId) ? organizationId : null;
|
||||
}
|
||||
|
||||
public async Task SaveOrganizationIdAsync(string userId, string organizationId, CancellationToken cancellationToken)
|
||||
{
|
||||
LocalClientSettings settings = await settingsStore.LoadAsync(cancellationToken);
|
||||
settings.LastOrganizationByUserId[userId] = organizationId;
|
||||
await settingsStore.SaveAsync(settings, cancellationToken);
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
using RemoteAccessPlatform.Windows.Contracts;
|
||||
using RemoteAccessPlatform.Windows.Models;
|
||||
|
||||
namespace RemoteAccessPlatform.Windows.Application.Services;
|
||||
|
||||
public sealed class ResourceCatalogService(IBackendResourceClient resourceClient)
|
||||
{
|
||||
public Task<IReadOnlyList<ResourceDto>> LoadResourcesAsync(string userId, string organizationId, CancellationToken cancellationToken)
|
||||
{
|
||||
return resourceClient.GetResourcesAsync(userId, organizationId, cancellationToken);
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
using RemoteAccessPlatform.Windows.Contracts;
|
||||
using RemoteAccessPlatform.Windows.Models;
|
||||
|
||||
namespace RemoteAccessPlatform.Windows.Application.Services;
|
||||
|
||||
public sealed class SessionService(IBackendSessionClient sessionClient)
|
||||
{
|
||||
public Task<IReadOnlyList<RemoteSessionDto>> LoadSessionsAsync(string userId, CancellationToken cancellationToken) =>
|
||||
sessionClient.GetSessionsAsync(userId, cancellationToken);
|
||||
|
||||
public Task<SessionControlResultDto> StartSessionAsync(string resourceId, string userId, string deviceId, CancellationToken cancellationToken) =>
|
||||
sessionClient.StartSessionAsync(new StartSessionRequest
|
||||
{
|
||||
ResourceId = resourceId,
|
||||
UserId = userId,
|
||||
DeviceId = deviceId
|
||||
}, cancellationToken);
|
||||
|
||||
public Task<SessionControlResultDto> AttachSessionAsync(string sessionId, string userId, string deviceId, CancellationToken cancellationToken) =>
|
||||
sessionClient.AttachSessionAsync(sessionId, new AttachSessionRequest
|
||||
{
|
||||
UserId = userId,
|
||||
DeviceId = deviceId
|
||||
}, cancellationToken);
|
||||
|
||||
public Task<SessionControlResultDto> DetachSessionAsync(string sessionId, string attachmentId, string userId, string reason, CancellationToken cancellationToken) =>
|
||||
sessionClient.DetachSessionAsync(sessionId, new DetachSessionRequest
|
||||
{
|
||||
AttachmentId = attachmentId,
|
||||
UserId = userId,
|
||||
Reason = reason
|
||||
}, cancellationToken);
|
||||
|
||||
public Task<SessionControlResultDto> TakeoverSessionAsync(string sessionId, string userId, string deviceId, string reason, CancellationToken cancellationToken) =>
|
||||
sessionClient.TakeoverSessionAsync(sessionId, new TakeoverSessionRequest
|
||||
{
|
||||
UserId = userId,
|
||||
DeviceId = deviceId,
|
||||
Reason = reason
|
||||
}, cancellationToken);
|
||||
|
||||
public Task TerminateSessionAsync(string sessionId, string userId, string reason, CancellationToken cancellationToken) =>
|
||||
sessionClient.TerminateSessionAsync(sessionId, new TerminateSessionRequest
|
||||
{
|
||||
UserId = userId,
|
||||
Reason = reason
|
||||
}, cancellationToken);
|
||||
}
|
||||
+406
@@ -0,0 +1,406 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using RemoteAccessPlatform.Windows.Application.Common;
|
||||
using RemoteAccessPlatform.Windows.Application.Localization;
|
||||
using RemoteAccessPlatform.Windows.Application.Services;
|
||||
using RemoteAccessPlatform.Windows.Contracts;
|
||||
using RemoteAccessPlatform.Windows.Models;
|
||||
|
||||
namespace RemoteAccessPlatform.Windows.Application.ViewModels;
|
||||
|
||||
public sealed class MainViewModel : ObservableObject
|
||||
{
|
||||
private readonly AuthenticationService _authenticationService;
|
||||
private readonly OrganizationContextService _organizationContextService;
|
||||
private readonly ResourceCatalogService _resourceCatalogService;
|
||||
private readonly SessionService _sessionService;
|
||||
private readonly Contracts.ISessionGatewayClient _sessionGatewayClient;
|
||||
private string _email = string.Empty;
|
||||
private string _password = string.Empty;
|
||||
private bool _trustDevice = true;
|
||||
private string _statusMessage = Strings.Get("status.ready");
|
||||
private bool _isBusy;
|
||||
private OrganizationDto? _selectedOrganization;
|
||||
private ResourceDto? _selectedResource;
|
||||
private RemoteSessionDto? _selectedSession;
|
||||
|
||||
public MainViewModel(
|
||||
AuthenticationService authenticationService,
|
||||
OrganizationContextService organizationContextService,
|
||||
ResourceCatalogService resourceCatalogService,
|
||||
SessionService sessionService,
|
||||
Contracts.ISessionGatewayClient sessionGatewayClient)
|
||||
{
|
||||
_authenticationService = authenticationService;
|
||||
_organizationContextService = organizationContextService;
|
||||
_resourceCatalogService = resourceCatalogService;
|
||||
_sessionService = sessionService;
|
||||
_sessionGatewayClient = sessionGatewayClient;
|
||||
|
||||
LoginCommand = new AsyncRelayCommand(LoginAsync, () => !IsAuthenticated && !IsBusy);
|
||||
LogoutCommand = new AsyncRelayCommand(LogoutAsync, () => IsAuthenticated && !IsBusy);
|
||||
SwitchOrganizationCommand = new AsyncRelayCommand(SwitchOrganizationAsync, () => IsAuthenticated && SelectedOrganization is not null && !IsBusy);
|
||||
RefreshResourcesCommand = new AsyncRelayCommand(RefreshResourcesAsync, () => IsAuthenticated && SelectedOrganization is not null && !IsBusy);
|
||||
RefreshSessionsCommand = new AsyncRelayCommand(RefreshSessionsAsync, () => IsAuthenticated && !IsBusy);
|
||||
StartSessionCommand = new AsyncRelayCommand<ResourceDto>(StartSessionAsync, resource => IsAuthenticated && resource is not null && !IsBusy);
|
||||
AttachSessionCommand = new AsyncRelayCommand<RemoteSessionDto>(AttachSessionAsync, session => IsAuthenticated && session is not null && !IsBusy);
|
||||
TakeOverSessionCommand = new AsyncRelayCommand<RemoteSessionDto>(TakeOverSessionAsync, session => IsAuthenticated && session is not null && !IsBusy);
|
||||
TerminateSessionCommand = new AsyncRelayCommand<RemoteSessionDto>(TerminateSessionAsync, session => IsAuthenticated && session is not null && !IsBusy);
|
||||
|
||||
_authenticationService.StateChanged += HandleAuthenticationStateChanged;
|
||||
_authenticationService.SessionExpired += HandleSessionExpired;
|
||||
}
|
||||
|
||||
public event Action<SessionWindowViewModel>? SessionWindowRequested;
|
||||
|
||||
public ObservableCollection<OrganizationDto> Organizations { get; } = [];
|
||||
public ObservableCollection<ResourceDto> Resources { get; } = [];
|
||||
public ObservableCollection<RemoteSessionDto> Sessions { get; } = [];
|
||||
|
||||
public string Email
|
||||
{
|
||||
get => _email;
|
||||
set => SetProperty(ref _email, value);
|
||||
}
|
||||
|
||||
public string Password
|
||||
{
|
||||
get => _password;
|
||||
set => SetProperty(ref _password, value);
|
||||
}
|
||||
|
||||
public bool TrustDevice
|
||||
{
|
||||
get => _trustDevice;
|
||||
set => SetProperty(ref _trustDevice, value);
|
||||
}
|
||||
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
private set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
|
||||
public bool IsBusy
|
||||
{
|
||||
get => _isBusy;
|
||||
private set
|
||||
{
|
||||
if (SetProperty(ref _isBusy, value))
|
||||
{
|
||||
LoginCommand.NotifyCanExecuteChanged();
|
||||
LogoutCommand.NotifyCanExecuteChanged();
|
||||
SwitchOrganizationCommand.NotifyCanExecuteChanged();
|
||||
RefreshResourcesCommand.NotifyCanExecuteChanged();
|
||||
RefreshSessionsCommand.NotifyCanExecuteChanged();
|
||||
StartSessionCommand.NotifyCanExecuteChanged();
|
||||
AttachSessionCommand.NotifyCanExecuteChanged();
|
||||
TakeOverSessionCommand.NotifyCanExecuteChanged();
|
||||
TerminateSessionCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAuthenticated => _authenticationService.IsAuthenticated;
|
||||
public string CurrentUserEmail => _authenticationService.Current?.User.Email ?? string.Empty;
|
||||
|
||||
public OrganizationDto? SelectedOrganization
|
||||
{
|
||||
get => _selectedOrganization;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedOrganization, value))
|
||||
{
|
||||
SwitchOrganizationCommand.NotifyCanExecuteChanged();
|
||||
RefreshResourcesCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ResourceDto? SelectedResource
|
||||
{
|
||||
get => _selectedResource;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedResource, value))
|
||||
{
|
||||
StartSessionCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public RemoteSessionDto? SelectedSession
|
||||
{
|
||||
get => _selectedSession;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedSession, value))
|
||||
{
|
||||
AttachSessionCommand.NotifyCanExecuteChanged();
|
||||
TakeOverSessionCommand.NotifyCanExecuteChanged();
|
||||
TerminateSessionCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public AsyncRelayCommand LoginCommand { get; }
|
||||
public AsyncRelayCommand LogoutCommand { get; }
|
||||
public AsyncRelayCommand SwitchOrganizationCommand { get; }
|
||||
public AsyncRelayCommand RefreshResourcesCommand { get; }
|
||||
public AsyncRelayCommand RefreshSessionsCommand { get; }
|
||||
public AsyncRelayCommand<ResourceDto> StartSessionCommand { get; }
|
||||
public AsyncRelayCommand<RemoteSessionDto> AttachSessionCommand { get; }
|
||||
public AsyncRelayCommand<RemoteSessionDto> TakeOverSessionCommand { get; }
|
||||
public AsyncRelayCommand<RemoteSessionDto> TerminateSessionCommand { get; }
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _authenticationService.InitializeAsync(CancellationToken.None);
|
||||
if (IsAuthenticated)
|
||||
{
|
||||
await LoadWorkspaceAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoginAsync()
|
||||
{
|
||||
await ExecuteBusyAsync(Strings.Get("status.auth.logging_in"), async cancellationToken =>
|
||||
{
|
||||
await _authenticationService.LoginAsync(Email, Password, TrustDevice, cancellationToken);
|
||||
Password = string.Empty;
|
||||
await LoadWorkspaceAsync(cancellationToken);
|
||||
StatusMessage = Strings.Get("status.auth.logged_in");
|
||||
});
|
||||
}
|
||||
|
||||
private async Task LogoutAsync()
|
||||
{
|
||||
await ExecuteBusyAsync(Strings.Get("status.auth.logging_out"), async cancellationToken =>
|
||||
{
|
||||
await _authenticationService.LogoutAsync(cancellationToken);
|
||||
ClearWorkspace();
|
||||
StatusMessage = Strings.Get("status.auth.logged_out");
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SwitchOrganizationAsync()
|
||||
{
|
||||
if (SelectedOrganization is null || _authenticationService.CurrentUserId is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ExecuteBusyAsync(Strings.Get("status.organization.switching"), async cancellationToken =>
|
||||
{
|
||||
await _organizationContextService.SaveOrganizationIdAsync(_authenticationService.CurrentUserId, SelectedOrganization.Id, cancellationToken);
|
||||
await RefreshResourcesAsync();
|
||||
await RefreshSessionsAsync();
|
||||
StatusMessage = Strings.Format("status.organization.active", SelectedOrganization.Name);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task RefreshResourcesAsync()
|
||||
{
|
||||
if (SelectedOrganization is null || _authenticationService.CurrentUserId is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ExecuteBusyAsync(Strings.Get("status.resources.refreshing"), async cancellationToken =>
|
||||
{
|
||||
IReadOnlyList<ResourceDto> resources = await _resourceCatalogService.LoadResourcesAsync(
|
||||
_authenticationService.CurrentUserId,
|
||||
SelectedOrganization.Id,
|
||||
cancellationToken);
|
||||
|
||||
ReplaceCollection(Resources, resources);
|
||||
StatusMessage = Strings.Format("status.resources.loaded", Resources.Count);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task RefreshSessionsAsync()
|
||||
{
|
||||
if (_authenticationService.CurrentUserId is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ExecuteBusyAsync(Strings.Get("status.sessions.refreshing"), async cancellationToken =>
|
||||
{
|
||||
IReadOnlyList<RemoteSessionDto> sessions = await _sessionService.LoadSessionsAsync(_authenticationService.CurrentUserId, cancellationToken);
|
||||
ReplaceCollection(Sessions, sessions);
|
||||
StatusMessage = Strings.Format("status.sessions.loaded", Sessions.Count);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task StartSessionAsync(ResourceDto? resource)
|
||||
{
|
||||
if (resource is null || _authenticationService.CurrentUserId is null || _authenticationService.CurrentDeviceId is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ExecuteBusyAsync(Strings.Format("status.session.starting", resource.Name), async cancellationToken =>
|
||||
{
|
||||
SessionControlResultDto result = await _sessionService.StartSessionAsync(
|
||||
resource.Id,
|
||||
_authenticationService.CurrentUserId,
|
||||
_authenticationService.CurrentDeviceId,
|
||||
cancellationToken);
|
||||
await OpenSessionWindowAsync(result, "start");
|
||||
await RefreshSessionsAsync();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task AttachSessionAsync(RemoteSessionDto? session)
|
||||
{
|
||||
if (session is null || _authenticationService.CurrentUserId is null || _authenticationService.CurrentDeviceId is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ExecuteBusyAsync(Strings.Format("status.session.attaching", session.ID), async cancellationToken =>
|
||||
{
|
||||
SessionControlResultDto result = await _sessionService.AttachSessionAsync(
|
||||
session.ID,
|
||||
_authenticationService.CurrentUserId,
|
||||
_authenticationService.CurrentDeviceId,
|
||||
cancellationToken);
|
||||
await OpenSessionWindowAsync(result, "attach");
|
||||
await RefreshSessionsAsync();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task TakeOverSessionAsync(RemoteSessionDto? session)
|
||||
{
|
||||
if (session is null || _authenticationService.CurrentUserId is null || _authenticationService.CurrentDeviceId is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ExecuteBusyAsync(Strings.Format("status.session.taking_over", session.ID), async cancellationToken =>
|
||||
{
|
||||
SessionControlResultDto result = await _sessionService.TakeoverSessionAsync(
|
||||
session.ID,
|
||||
_authenticationService.CurrentUserId,
|
||||
_authenticationService.CurrentDeviceId,
|
||||
"windows_client_takeover",
|
||||
cancellationToken);
|
||||
await OpenSessionWindowAsync(result, "takeover");
|
||||
await RefreshSessionsAsync();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task TerminateSessionAsync(RemoteSessionDto? session)
|
||||
{
|
||||
if (session is null || _authenticationService.CurrentUserId is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ExecuteBusyAsync(Strings.Format("status.session.terminating", session.ID), async cancellationToken =>
|
||||
{
|
||||
await _sessionService.TerminateSessionAsync(session.ID, _authenticationService.CurrentUserId, "windows_client_terminate", cancellationToken);
|
||||
await RefreshSessionsAsync();
|
||||
StatusMessage = Strings.Format("status.session.terminated", session.ID);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task LoadWorkspaceAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_authenticationService.CurrentUserId is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IReadOnlyList<OrganizationDto> organizations = await _organizationContextService.LoadOrganizationsAsync(_authenticationService.CurrentUserId, cancellationToken);
|
||||
ReplaceCollection(Organizations, organizations);
|
||||
|
||||
string? savedOrganizationId = await _organizationContextService.GetSavedOrganizationIdAsync(_authenticationService.CurrentUserId, cancellationToken);
|
||||
SelectedOrganization = Organizations.FirstOrDefault(x => x.Id == savedOrganizationId) ?? Organizations.FirstOrDefault();
|
||||
|
||||
if (SelectedOrganization is not null)
|
||||
{
|
||||
await _organizationContextService.SaveOrganizationIdAsync(_authenticationService.CurrentUserId, SelectedOrganization.Id, cancellationToken);
|
||||
IReadOnlyList<ResourceDto> resources = await _resourceCatalogService.LoadResourcesAsync(_authenticationService.CurrentUserId, SelectedOrganization.Id, cancellationToken);
|
||||
ReplaceCollection(Resources, resources);
|
||||
}
|
||||
else
|
||||
{
|
||||
Resources.Clear();
|
||||
}
|
||||
|
||||
IReadOnlyList<RemoteSessionDto> sessions = await _sessionService.LoadSessionsAsync(_authenticationService.CurrentUserId, cancellationToken);
|
||||
ReplaceCollection(Sessions, sessions);
|
||||
|
||||
RaisePropertyChanged(nameof(CurrentUserEmail));
|
||||
RaisePropertyChanged(nameof(IsAuthenticated));
|
||||
StatusMessage = Strings.Get("status.workspace.ready");
|
||||
}
|
||||
|
||||
private async Task OpenSessionWindowAsync(SessionControlResultDto result, string launchReason)
|
||||
{
|
||||
Trace.WriteLine($"[{DateTimeOffset.Now:O}] [T{Environment.CurrentManagedThreadId}] MainViewModel.OpenSessionWindowAsync start: reason={launchReason}, session={result.Session.ID}");
|
||||
var vm = new SessionWindowViewModel(_sessionService, _sessionGatewayClient, _authenticationService, result, launchReason);
|
||||
SessionWindowRequested?.Invoke(vm);
|
||||
Trace.WriteLine($"[{DateTimeOffset.Now:O}] [T{Environment.CurrentManagedThreadId}] MainViewModel.OpenSessionWindowAsync event invoked: reason={launchReason}, session={result.Session.ID}");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void HandleAuthenticationStateChanged()
|
||||
{
|
||||
RaisePropertyChanged(nameof(IsAuthenticated));
|
||||
RaisePropertyChanged(nameof(CurrentUserEmail));
|
||||
LoginCommand.NotifyCanExecuteChanged();
|
||||
LogoutCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
private void HandleSessionExpired()
|
||||
{
|
||||
ClearWorkspace();
|
||||
StatusMessage = Strings.Get("status.auth.session_expired");
|
||||
}
|
||||
|
||||
private void ClearWorkspace()
|
||||
{
|
||||
Organizations.Clear();
|
||||
Resources.Clear();
|
||||
Sessions.Clear();
|
||||
SelectedOrganization = null;
|
||||
SelectedResource = null;
|
||||
SelectedSession = null;
|
||||
RaisePropertyChanged(nameof(IsAuthenticated));
|
||||
RaisePropertyChanged(nameof(CurrentUserEmail));
|
||||
}
|
||||
|
||||
private async Task ExecuteBusyAsync(string status, Func<CancellationToken, Task> action)
|
||||
{
|
||||
IsBusy = true;
|
||||
StatusMessage = status;
|
||||
try
|
||||
{
|
||||
await action(CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex) when (ex is IBackendMessageException backendMessage)
|
||||
{
|
||||
StatusMessage = Strings.ResolveBackendMessage(backendMessage.MessageKey, backendMessage.FallbackMessage, backendMessage.Code);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
StatusMessage = Strings.Get("errors.common.unexpected");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReplaceCollection<T>(ObservableCollection<T> target, IEnumerable<T> source)
|
||||
{
|
||||
target.Clear();
|
||||
foreach (T item in source)
|
||||
{
|
||||
target.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2066
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user