- add: TFA think
This commit is contained in:
1
.idea/.idea.Nebula/.idea/avalonia.xml
generated
1
.idea/.idea.Nebula/.idea/avalonia.xml
generated
@@ -25,6 +25,7 @@
|
|||||||
<entry key="Nebula.Launcher/Views/Popup/LoadingContextView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Popup/LoadingContextView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Popup/LogPopupView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Popup/LogPopupView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Popup/MessagePopupView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Popup/MessagePopupView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
|
<entry key="Nebula.Launcher/Views/Popup/TfaView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/ServerContainer.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/ServerContainer.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/ServerEntryView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/ServerEntryView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/ServerList.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/ServerList.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
|
|||||||
20
Nebula.Launcher/LauncherConVar.cs
Normal file
20
Nebula.Launcher/LauncherConVar.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher;
|
||||||
|
|
||||||
|
public static class LauncherConVar
|
||||||
|
{
|
||||||
|
public static readonly ConVar<ProfileAuthCredentials[]> AuthProfiles =
|
||||||
|
ConVarBuilder.Build<ProfileAuthCredentials[]>("auth.profiles.v2", []);
|
||||||
|
|
||||||
|
public static readonly ConVar<CurrentAuthInfo?> AuthCurrent =
|
||||||
|
ConVarBuilder.Build<CurrentAuthInfo?>("auth.current.v2");
|
||||||
|
|
||||||
|
public static readonly ConVar<string[]> Favorites =
|
||||||
|
ConVarBuilder.Build<string[]>("server.favorites", []);
|
||||||
|
|
||||||
|
public static readonly ConVar<string[]> AuthServers = ConVarBuilder.Build<string[]>("launcher.authServers", [
|
||||||
|
"https://auth.spacestation14.com/"
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
@@ -38,7 +39,7 @@ public partial class AccountInfoViewModel : ViewModelBase, IViewModelPage
|
|||||||
[GenerateProperty] private AuthService AuthService { get; } = default!;
|
[GenerateProperty] private AuthService AuthService { get; } = default!;
|
||||||
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; } = default!;
|
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; } = default!;
|
||||||
|
|
||||||
public ObservableCollection<AuthLoginPasswordModel> Accounts { get; } = new();
|
public ObservableCollection<ProfileAuthCredentials> Accounts { get; } = new();
|
||||||
public ObservableCollection<string> AuthUrls { get; } = new();
|
public ObservableCollection<string> AuthUrls { get; } = new();
|
||||||
|
|
||||||
private AuthLoginPassword CurrentAlp
|
private AuthLoginPassword CurrentAlp
|
||||||
@@ -52,6 +53,9 @@ public partial class AccountInfoViewModel : ViewModelBase, IViewModelPage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private CurrentAuthInfo? _currAuthTemp;
|
||||||
|
|
||||||
public string AuthItemSelect
|
public string AuthItemSelect
|
||||||
{
|
{
|
||||||
set => CurrentAuthServer = value;
|
set => CurrentAuthServer = value;
|
||||||
@@ -71,37 +75,61 @@ public partial class AccountInfoViewModel : ViewModelBase, IViewModelPage
|
|||||||
ReadAuthConfig();
|
ReadAuthConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AuthByAlp(AuthLoginPassword authLoginPassword)
|
public void AuthByProfile(ProfileAuthCredentials credentials)
|
||||||
{
|
{
|
||||||
CurrentAlp = authLoginPassword;
|
CurrentAlp = new AuthLoginPassword(credentials.Login, credentials.Password, credentials.AuthServer);
|
||||||
DoAuth();
|
DoAuth();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DoAuth()
|
public void DoAuth(string? code = null)
|
||||||
{
|
{
|
||||||
var message = ViewHelperService.GetViewModel<InfoPopupViewModel>();
|
var message = ViewHelperService.GetViewModel<InfoPopupViewModel>();
|
||||||
message.InfoText = "Auth think, please wait...";
|
message.InfoText = "Auth think, please wait...";
|
||||||
message.IsInfoClosable = false;
|
message.IsInfoClosable = false;
|
||||||
Console.WriteLine("AUTH SHIT");
|
|
||||||
PopupMessageService.Popup(message);
|
PopupMessageService.Popup(message);
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
if (await AuthService.Auth(CurrentAlp))
|
try
|
||||||
{
|
{
|
||||||
|
await AuthService.Auth(CurrentAlp, code);
|
||||||
message.Dispose();
|
message.Dispose();
|
||||||
IsLogged = true;
|
IsLogged = true;
|
||||||
ConfigurationService.SetConfigValue(CurrentConVar.AuthCurrent, CurrentAlp);
|
ConfigurationService.SetConfigValue(LauncherConVar.AuthCurrent, AuthService.SelectedAuth);
|
||||||
}
|
}
|
||||||
else
|
catch (AuthException e)
|
||||||
|
{
|
||||||
|
message.Dispose();
|
||||||
|
|
||||||
|
switch (e.Error.Code)
|
||||||
|
{
|
||||||
|
case AuthenticateDenyCode.TfaRequired:
|
||||||
|
case AuthenticateDenyCode.TfaInvalid:
|
||||||
|
var p = ViewHelperService.GetViewModel<TfaViewModel>();
|
||||||
|
p.OnTfaEntered += OnTfaEntered;
|
||||||
|
PopupMessageService.Popup(p);
|
||||||
|
break;
|
||||||
|
case AuthenticateDenyCode.InvalidCredentials:
|
||||||
|
PopupMessageService.Popup("Invalid Credentials!");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
message.Dispose();
|
message.Dispose();
|
||||||
Logout();
|
Logout();
|
||||||
PopupMessageService.Popup("Well, shit is happened: " + AuthService.Reason);
|
PopupMessageService.Popup(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnTfaEntered(string code)
|
||||||
|
{
|
||||||
|
DoAuth(code);
|
||||||
|
}
|
||||||
|
|
||||||
public void Logout()
|
public void Logout()
|
||||||
{
|
{
|
||||||
IsLogged = false;
|
IsLogged = false;
|
||||||
@@ -118,10 +146,10 @@ public partial class AccountInfoViewModel : ViewModelBase, IViewModelPage
|
|||||||
|
|
||||||
private void AddAccount(AuthLoginPassword authLoginPassword)
|
private void AddAccount(AuthLoginPassword authLoginPassword)
|
||||||
{
|
{
|
||||||
var onDelete = new DelegateCommand<AuthLoginPasswordModel>(OnDeleteProfile);
|
var onDelete = new DelegateCommand<ProfileAuthCredentials>(OnDeleteProfile);
|
||||||
var onSelect = new DelegateCommand<AuthLoginPasswordModel>(AuthByAlp);
|
var onSelect = new DelegateCommand<ProfileAuthCredentials>(AuthByProfile);
|
||||||
|
|
||||||
var alpm = new AuthLoginPasswordModel(
|
var alpm = new ProfileAuthCredentials(
|
||||||
authLoginPassword.Login,
|
authLoginPassword.Login,
|
||||||
authLoginPassword.Password,
|
authLoginPassword.Password,
|
||||||
authLoginPassword.AuthServer,
|
authLoginPassword.AuthServer,
|
||||||
@@ -134,25 +162,39 @@ public partial class AccountInfoViewModel : ViewModelBase, IViewModelPage
|
|||||||
Accounts.Add(alpm);
|
Accounts.Add(alpm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReadAuthConfig()
|
private async void ReadAuthConfig()
|
||||||
{
|
{
|
||||||
|
var message = ViewHelperService.GetViewModel<InfoPopupViewModel>();
|
||||||
|
message.InfoText = "Read configuration file, please wait...";
|
||||||
|
message.IsInfoClosable = false;
|
||||||
|
PopupMessageService.Popup(message);
|
||||||
foreach (var profile in
|
foreach (var profile in
|
||||||
ConfigurationService.GetConfigValue(CurrentConVar.AuthProfiles)!)
|
ConfigurationService.GetConfigValue(LauncherConVar.AuthProfiles)!)
|
||||||
AddAccount(profile);
|
AddAccount(new AuthLoginPassword(profile.Login, profile.Password, profile.AuthServer));
|
||||||
|
|
||||||
if (Accounts.Count == 0) UpdateAuthMenu();
|
if (Accounts.Count == 0) UpdateAuthMenu();
|
||||||
|
|
||||||
var currProfile = ConfigurationService.GetConfigValue(CurrentConVar.AuthCurrent);
|
AuthUrls.Clear();
|
||||||
|
var authUrls = ConfigurationService.GetConfigValue(LauncherConVar.AuthServers)!;
|
||||||
|
foreach (var url in authUrls) AuthUrls.Add(url);
|
||||||
|
|
||||||
|
var currProfile = ConfigurationService.GetConfigValue(LauncherConVar.AuthCurrent);
|
||||||
|
|
||||||
if (currProfile != null)
|
if (currProfile != null)
|
||||||
{
|
{
|
||||||
CurrentAlp = currProfile;
|
try
|
||||||
DoAuth();
|
{
|
||||||
|
CurrentAlp = new AuthLoginPassword(currProfile.Login, string.Empty, currProfile.AuthServer);
|
||||||
|
IsLogged = await AuthService.SetAuth(currProfile);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
message.Dispose();
|
||||||
|
PopupMessageService.Popup(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthUrls.Clear();
|
message.Dispose();
|
||||||
var authUrls = ConfigurationService.GetConfigValue(CurrentConVar.AuthServers)!;
|
|
||||||
foreach (var url in authUrls) AuthUrls.Add(url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -164,7 +206,7 @@ public partial class AccountInfoViewModel : ViewModelBase, IViewModelPage
|
|||||||
DirtyProfile();
|
DirtyProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDeleteProfile(AuthLoginPasswordModel account)
|
private void OnDeleteProfile(ProfileAuthCredentials account)
|
||||||
{
|
{
|
||||||
Accounts.Remove(account);
|
Accounts.Remove(account);
|
||||||
_isProfilesEmpty = Accounts.Count == 0;
|
_isProfilesEmpty = Accounts.Count == 0;
|
||||||
@@ -187,20 +229,18 @@ public partial class AccountInfoViewModel : ViewModelBase, IViewModelPage
|
|||||||
|
|
||||||
private void DirtyProfile()
|
private void DirtyProfile()
|
||||||
{
|
{
|
||||||
ConfigurationService.SetConfigValue(CurrentConVar.AuthProfiles,
|
ConfigurationService.SetConfigValue(LauncherConVar.AuthProfiles,
|
||||||
Accounts.Select(a => (AuthLoginPassword)a).ToArray());
|
Accounts.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnPageOpen(object? args)
|
public void OnPageOpen(object? args)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public sealed record ProfileAuthCredentials(
|
||||||
public record AuthLoginPasswordModel(
|
|
||||||
string Login,
|
string Login,
|
||||||
string Password,
|
string Password,
|
||||||
string AuthServer,
|
string AuthServer,
|
||||||
ICommand OnSelect = default!,
|
[property: JsonIgnore] ICommand OnSelect = default!,
|
||||||
ICommand OnDelete = default!)
|
[property: JsonIgnore] ICommand OnDelete = default!
|
||||||
: AuthLoginPassword(Login, Password, AuthServer);
|
);
|
||||||
@@ -22,7 +22,7 @@ public partial class ServerListViewModel
|
|||||||
{
|
{
|
||||||
FavoriteServers.Clear();
|
FavoriteServers.Clear();
|
||||||
|
|
||||||
var servers = ConfigurationService.GetConfigValue(CurrentConVar.Favorites);
|
var servers = ConfigurationService.GetConfigValue(LauncherConVar.Favorites);
|
||||||
if (servers is null || servers.Length == 0)
|
if (servers is null || servers.Length == 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -44,17 +44,17 @@ public partial class ServerListViewModel
|
|||||||
|
|
||||||
public void AddFavorite(RobustUrl robustUrl)
|
public void AddFavorite(RobustUrl robustUrl)
|
||||||
{
|
{
|
||||||
var servers = (ConfigurationService.GetConfigValue(CurrentConVar.Favorites) ?? []).ToList();
|
var servers = (ConfigurationService.GetConfigValue(LauncherConVar.Favorites) ?? []).ToList();
|
||||||
servers.Add(robustUrl.ToString());
|
servers.Add(robustUrl.ToString());
|
||||||
ConfigurationService.SetConfigValue(CurrentConVar.Favorites, servers.ToArray());
|
ConfigurationService.SetConfigValue(LauncherConVar.Favorites, servers.ToArray());
|
||||||
UpdateFavoriteEntries();
|
UpdateFavoriteEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveFavorite(ServerEntryModelView entryModelView)
|
public void RemoveFavorite(ServerEntryModelView entryModelView)
|
||||||
{
|
{
|
||||||
var servers = (ConfigurationService.GetConfigValue(CurrentConVar.Favorites) ?? []).ToList();
|
var servers = (ConfigurationService.GetConfigValue(LauncherConVar.Favorites) ?? []).ToList();
|
||||||
servers.Remove(entryModelView.Address.ToString());
|
servers.Remove(entryModelView.Address.ToString());
|
||||||
ConfigurationService.SetConfigValue(CurrentConVar.Favorites, servers.ToArray());
|
ConfigurationService.SetConfigValue(LauncherConVar.Favorites, servers.ToArray());
|
||||||
entryModelView.IsFavorite = false;
|
entryModelView.IsFavorite = false;
|
||||||
UpdateFavoriteEntries();
|
UpdateFavoriteEntries();
|
||||||
}
|
}
|
||||||
|
|||||||
29
Nebula.Launcher/ViewModels/Popup/TfaViewModel.cs
Normal file
29
Nebula.Launcher/ViewModels/Popup/TfaViewModel.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using Nebula.Launcher.Views.Popup;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ViewModels.Popup;
|
||||||
|
|
||||||
|
[ConstructGenerator, ViewModelRegister(typeof(TfaView))]
|
||||||
|
public partial class TfaViewModel : PopupViewModelBase
|
||||||
|
{
|
||||||
|
public Action<string>? OnTfaEntered;
|
||||||
|
|
||||||
|
protected override void InitialiseInDesignMode()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Initialise()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnTfaEnter(string code)
|
||||||
|
{
|
||||||
|
OnTfaEntered?.Invoke(code);
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[GenerateProperty] public override PopupMessageService PopupMessageService { get; }
|
||||||
|
public override string Title => "2fa";
|
||||||
|
public override bool IsClosable => true;
|
||||||
|
}
|
||||||
@@ -185,10 +185,10 @@ public partial class ServerEntryModelView : ViewModelBase
|
|||||||
{
|
{
|
||||||
{ "ROBUST_AUTH_USERID", authProv?.UserId.ToString() },
|
{ "ROBUST_AUTH_USERID", authProv?.UserId.ToString() },
|
||||||
{ "ROBUST_AUTH_TOKEN", authProv?.Token.Token },
|
{ "ROBUST_AUTH_TOKEN", authProv?.Token.Token },
|
||||||
{ "ROBUST_AUTH_SERVER", authProv?.AuthLoginPassword.AuthServer },
|
{ "ROBUST_AUTH_SERVER", authProv?.AuthServer },
|
||||||
{ "ROBUST_AUTH_PUBKEY", buildInfo.BuildInfo.Auth.PublicKey },
|
{ "ROBUST_AUTH_PUBKEY", buildInfo.BuildInfo.Auth.PublicKey },
|
||||||
{ "GAME_URL", Address.ToString() },
|
{ "GAME_URL", Address.ToString() },
|
||||||
{ "AUTH_LOGIN", authProv?.AuthLoginPassword.Login }
|
{ "AUTH_LOGIN", authProv?.Login }
|
||||||
},
|
},
|
||||||
CreateNoWindow = true,
|
CreateNoWindow = true,
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
@@ -253,8 +253,7 @@ public partial class ServerEntryModelView : ViewModelBase
|
|||||||
CurrLog.Append(e.Data);
|
CurrLog.Append(e.Data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void ReadLog()
|
public void ReadLog()
|
||||||
{
|
{
|
||||||
PopupMessageService.Popup(CurrLog);
|
PopupMessageService.Popup(CurrLog);
|
||||||
@@ -286,7 +285,6 @@ public partial class ServerEntryModelView : ViewModelBase
|
|||||||
{
|
{
|
||||||
Links.Add(link);
|
Links.Add(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FindDotnetPath()
|
private static string FindDotnetPath()
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
ItemsSource="{Binding Accounts}"
|
ItemsSource="{Binding Accounts}"
|
||||||
Padding="0">
|
Padding="0">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate DataType="{x:Type pages:AuthLoginPasswordModel}">
|
<DataTemplate DataType="{x:Type pages:ProfileAuthCredentials}">
|
||||||
<Border
|
<Border
|
||||||
Background="{StaticResource DefaultBackground}"
|
Background="{StaticResource DefaultBackground}"
|
||||||
BoxShadow="0 1 15 -2 #121212"
|
BoxShadow="0 1 15 -2 #121212"
|
||||||
|
|||||||
39
Nebula.Launcher/Views/Popup/TfaView.axaml
Normal file
39
Nebula.Launcher/Views/Popup/TfaView.axaml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:popup="clr-namespace:Nebula.Launcher.ViewModels.Popup"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Nebula.Launcher.Views.Popup.TfaView">
|
||||||
|
<Design.DataContext>
|
||||||
|
<popup:TfaViewModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
<StackPanel HorizontalAlignment="Stretch" Spacing="25" VerticalAlignment="Center">
|
||||||
|
<Label HorizontalAlignment="Center">You have two-factor authentication enabled. Please enter the code.</Label>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="10" x:Name="TContainer">
|
||||||
|
<Border BoxShadow="{StaticResource DefaultShadow}">
|
||||||
|
<TextBox MaxLength="1"/>
|
||||||
|
</Border>
|
||||||
|
<Border BoxShadow="{StaticResource DefaultShadow}">
|
||||||
|
<TextBox MaxLength="1"/>
|
||||||
|
</Border>
|
||||||
|
<Border BoxShadow="{StaticResource DefaultShadow}">
|
||||||
|
<TextBox MaxLength="1"/>
|
||||||
|
</Border>
|
||||||
|
<Border BoxShadow="{StaticResource DefaultShadow}">
|
||||||
|
<TextBox MaxLength="1"/>
|
||||||
|
</Border>
|
||||||
|
<Border BoxShadow="{StaticResource DefaultShadow}">
|
||||||
|
<TextBox MaxLength="1"/>
|
||||||
|
</Border>
|
||||||
|
<Border BoxShadow="{StaticResource DefaultShadow}">
|
||||||
|
<TextBox MaxLength="1"/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
<Border BoxShadow="{StaticResource DefaultShadow}" Background="{StaticResource DefaultSelected}" HorizontalAlignment="Center">
|
||||||
|
<Button Click="Button_OnClick">
|
||||||
|
<Label HorizontalAlignment="Center" Margin="15,5,15,5">OK</Label>
|
||||||
|
</Button>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
100
Nebula.Launcher/Views/Popup/TfaView.axaml.cs
Normal file
100
Nebula.Launcher/Views/Popup/TfaView.axaml.cs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Nebula.Launcher.ViewModels.Popup;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Views.Popup;
|
||||||
|
|
||||||
|
public partial class TfaView : UserControl
|
||||||
|
{
|
||||||
|
public List<TextBox> Boxes = new();
|
||||||
|
|
||||||
|
public TfaView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
foreach (var textBox in TContainer.Children.Select(UnzipBox))
|
||||||
|
{
|
||||||
|
var currIndex = Boxes.Count;
|
||||||
|
Boxes.Add(textBox);
|
||||||
|
textBox.TextChanged += (_,_) => OnTextChanged(currIndex);
|
||||||
|
textBox.PastingFromClipboard += OnPasteFromClipboard;
|
||||||
|
textBox.KeyUp += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (args.Key == Key.Back && string.IsNullOrEmpty(textBox.Text)) OnTextChanged(currIndex);
|
||||||
|
};
|
||||||
|
textBox.KeyDown += (sender, args) =>
|
||||||
|
{
|
||||||
|
textBox.Text = args.KeySymbol;
|
||||||
|
textBox.SelectionStart = 1;
|
||||||
|
//OnTextChanged(currIndex);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPasteFromClipboard(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
// TODO: CLIPBOARD THINK
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTextChanged(int index)
|
||||||
|
{
|
||||||
|
var box = Boxes[index];
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(box.Text))
|
||||||
|
{
|
||||||
|
if(index == 0) return;
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(!int.TryParse(box.Text, out var _))
|
||||||
|
{
|
||||||
|
box.Text = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index == 5)
|
||||||
|
{
|
||||||
|
CheckupCode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Boxes[index].Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckupCode()
|
||||||
|
{
|
||||||
|
var str = "";
|
||||||
|
foreach (var vtTextBox in Boxes)
|
||||||
|
{
|
||||||
|
if(string.IsNullOrEmpty(vtTextBox.Text)) return;
|
||||||
|
str += vtTextBox.Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
((TfaViewModel)DataContext!).OnTfaEnter(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextBox UnzipBox(Control control)
|
||||||
|
{
|
||||||
|
var box = (Border)control;
|
||||||
|
return (TextBox)box.Child!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TfaView(TfaViewModel tfaViewModel) : this()
|
||||||
|
{
|
||||||
|
DataContext = tfaViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Button_OnClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
CheckupCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
<StackPanel
|
<StackPanel
|
||||||
Grid.Column="2"
|
Grid.Column="3"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
IsVisible="{Binding ExpandInfo}"
|
IsVisible="{Binding ExpandInfo}"
|
||||||
Margin="5,5,0,0"
|
Margin="5,5,0,0"
|
||||||
|
|||||||
@@ -21,19 +21,6 @@ public static class CurrentConVar
|
|||||||
"https://hub.spacestation14.com/api/servers"
|
"https://hub.spacestation14.com/api/servers"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
public static readonly ConVar<string[]> AuthServers = ConVarBuilder.Build<string[]>("launcher.authServers", [
|
|
||||||
"https://auth.spacestation14.com/"
|
|
||||||
]);
|
|
||||||
|
|
||||||
public static readonly ConVar<AuthLoginPassword[]> AuthProfiles =
|
|
||||||
ConVarBuilder.Build<AuthLoginPassword[]>("auth.profiles", []);
|
|
||||||
|
|
||||||
public static readonly ConVar<AuthLoginPassword> AuthCurrent =
|
|
||||||
ConVarBuilder.Build<AuthLoginPassword>("auth.current");
|
|
||||||
|
|
||||||
public static readonly ConVar<string[]> Favorites =
|
|
||||||
ConVarBuilder.Build<string[]>("server.favorites", []);
|
|
||||||
|
|
||||||
public static readonly ConVar<Dictionary<string, EngineVersionInfo>> EngineManifestBackup =
|
public static readonly ConVar<Dictionary<string, EngineVersionInfo>> EngineManifestBackup =
|
||||||
ConVarBuilder.Build<Dictionary<string, EngineVersionInfo>>("engine.manifest.backup");
|
ConVarBuilder.Build<Dictionary<string, EngineVersionInfo>>("engine.manifest.backup");
|
||||||
public static readonly ConVar<ModulesInfo> ModuleManifestBackup =
|
public static readonly ConVar<ModulesInfo> ModuleManifestBackup =
|
||||||
|
|||||||
@@ -1,13 +1,3 @@
|
|||||||
namespace Nebula.Shared.Models.Auth;
|
namespace Nebula.Shared.Models.Auth;
|
||||||
|
|
||||||
public readonly struct LoginToken
|
public sealed record LoginToken(string Token, DateTimeOffset ExpireTime);
|
||||||
{
|
|
||||||
public readonly string Token;
|
|
||||||
public readonly DateTimeOffset ExpireTime;
|
|
||||||
|
|
||||||
public LoginToken(string token, DateTimeOffset expireTime)
|
|
||||||
{
|
|
||||||
Token = token;
|
|
||||||
ExpireTime = expireTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
using System.Net.Http.Headers;
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Nebula.Shared.Models.Auth;
|
using Nebula.Shared.Models.Auth;
|
||||||
|
using Nebula.Shared.Utils;
|
||||||
|
|
||||||
namespace Nebula.Shared.Services;
|
namespace Nebula.Shared.Services;
|
||||||
|
|
||||||
@@ -10,11 +14,9 @@ public class AuthService(
|
|||||||
CancellationService cancellationService)
|
CancellationService cancellationService)
|
||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient = new();
|
private readonly HttpClient _httpClient = new();
|
||||||
|
public CurrentAuthInfo? SelectedAuth { get; private set; }
|
||||||
|
|
||||||
public string Reason = "";
|
public async Task Auth(AuthLoginPassword authLoginPassword, string? code = null)
|
||||||
public CurrentAuthInfo? SelectedAuth { get; internal set; }
|
|
||||||
|
|
||||||
public async Task<bool> Auth(AuthLoginPassword authLoginPassword)
|
|
||||||
{
|
{
|
||||||
var authServer = authLoginPassword.AuthServer;
|
var authServer = authLoginPassword.AuthServer;
|
||||||
var login = authLoginPassword.Login;
|
var login = authLoginPassword.Login;
|
||||||
@@ -24,20 +26,24 @@ public class AuthService(
|
|||||||
|
|
||||||
var authUrl = new Uri($"{authServer}api/auth/authenticate");
|
var authUrl = new Uri($"{authServer}api/auth/authenticate");
|
||||||
|
|
||||||
var result =
|
try
|
||||||
await restService.PostAsync<AuthenticateResponse, AuthenticateRequest>(
|
|
||||||
new AuthenticateRequest(login, password), authUrl, cancellationService.Token);
|
|
||||||
|
|
||||||
if (result.Value is null)
|
|
||||||
{
|
{
|
||||||
Reason = result.Message;
|
var result =
|
||||||
return false;
|
await restService.PostAsync<AuthenticateResponse, AuthenticateRequest>(
|
||||||
|
new AuthenticateRequest(login, null, password, code), authUrl, cancellationService.Token);
|
||||||
|
|
||||||
|
SelectedAuth = new CurrentAuthInfo(result.UserId,
|
||||||
|
new LoginToken(result.Token, result.ExpireTime), authLoginPassword.Login, authLoginPassword.AuthServer);
|
||||||
|
}
|
||||||
|
catch (RestRequestException e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e.Content);
|
||||||
|
if (e.StatusCode != HttpStatusCode.Unauthorized) throw;
|
||||||
|
var err = await e.Content.AsJson<AuthDenyError>();
|
||||||
|
|
||||||
|
if (err is null) throw;
|
||||||
|
throw new AuthException(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
SelectedAuth = new CurrentAuthInfo(result.Value.UserId,
|
|
||||||
new LoginToken(result.Value.Token, result.Value.ExpireTime), authLoginPassword);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearAuth()
|
public void ClearAuth()
|
||||||
@@ -45,17 +51,17 @@ public class AuthService(
|
|||||||
SelectedAuth = null;
|
SelectedAuth = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetAuth(Guid guid, string token, string login, string authServer)
|
public async Task<bool> SetAuth(CurrentAuthInfo info)
|
||||||
{
|
{
|
||||||
SelectedAuth = new CurrentAuthInfo(guid, new LoginToken(token, DateTimeOffset.Now),
|
SelectedAuth = info;
|
||||||
new AuthLoginPassword(login, "", authServer));
|
return await EnsureToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> EnsureToken()
|
public async Task<bool> EnsureToken()
|
||||||
{
|
{
|
||||||
if (SelectedAuth is null) return false;
|
if (SelectedAuth is null) return false;
|
||||||
|
|
||||||
var authUrl = new Uri($"{SelectedAuth.AuthLoginPassword.AuthServer}api/auth/ping");
|
var authUrl = new Uri($"{SelectedAuth.AuthServer}api/auth/ping");
|
||||||
|
|
||||||
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, authUrl);
|
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, authUrl);
|
||||||
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("SS14Auth", SelectedAuth.Token.Token);
|
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("SS14Auth", SelectedAuth.Token.Token);
|
||||||
@@ -67,6 +73,24 @@ public class AuthService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record CurrentAuthInfo(Guid UserId, LoginToken Token, AuthLoginPassword AuthLoginPassword);
|
public sealed record CurrentAuthInfo(Guid UserId, LoginToken Token, string Login, string AuthServer);
|
||||||
|
|
||||||
public record AuthLoginPassword(string Login, string Password, string AuthServer);
|
public record AuthLoginPassword(string Login, string Password, string AuthServer);
|
||||||
|
|
||||||
|
public sealed record AuthDenyError(string[] Errors, AuthenticateDenyCode Code);
|
||||||
|
|
||||||
|
public sealed class AuthException(AuthDenyError error) : Exception
|
||||||
|
{
|
||||||
|
public AuthDenyError Error { get; } = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public enum AuthenticateDenyCode
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
InvalidCredentials = 1,
|
||||||
|
AccountUnconfirmed = 2,
|
||||||
|
TfaRequired = 3,
|
||||||
|
TfaInvalid = 4,
|
||||||
|
AccountLocked = 5,
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,13 +17,12 @@ public partial class ContentService(
|
|||||||
var info = new RobustBuildInfo();
|
var info = new RobustBuildInfo();
|
||||||
info.Url = url;
|
info.Url = url;
|
||||||
var bi = await restService.GetAsync<ServerInfo>(url.InfoUri, cancellationToken);
|
var bi = await restService.GetAsync<ServerInfo>(url.InfoUri, cancellationToken);
|
||||||
if (bi.Value is null) throw new NoNullAllowedException();
|
info.BuildInfo = bi;
|
||||||
info.BuildInfo = bi.Value;
|
|
||||||
info.RobustManifestInfo = info.BuildInfo.Build.Acz
|
info.RobustManifestInfo = info.BuildInfo.Build.Acz
|
||||||
? new RobustManifestInfo(new RobustPath(info.Url, "manifest.txt"), new RobustPath(info.Url, "download"),
|
? new RobustManifestInfo(new RobustPath(info.Url, "manifest.txt"), new RobustPath(info.Url, "download"),
|
||||||
bi.Value.Build.ManifestHash)
|
bi.Build.ManifestHash)
|
||||||
: new RobustManifestInfo(new Uri(info.BuildInfo.Build.ManifestUrl),
|
: new RobustManifestInfo(new Uri(info.BuildInfo.Build.ManifestUrl),
|
||||||
new Uri(info.BuildInfo.Build.ManifestDownloadUrl), bi.Value.Build.ManifestHash);
|
new Uri(info.BuildInfo.Build.ManifestDownloadUrl), bi.Build.ManifestHash);
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,12 +40,10 @@ public sealed class EngineService
|
|||||||
_debugService.Log("Fetching engine manifest from: " + CurrentConVar.EngineManifestUrl);
|
_debugService.Log("Fetching engine manifest from: " + CurrentConVar.EngineManifestUrl);
|
||||||
var info = await _restService.GetAsync<Dictionary<string, EngineVersionInfo>>(
|
var info = await _restService.GetAsync<Dictionary<string, EngineVersionInfo>>(
|
||||||
new Uri(_varService.GetConfigValue(CurrentConVar.EngineManifestUrl)!), cancellationToken);
|
new Uri(_varService.GetConfigValue(CurrentConVar.EngineManifestUrl)!), cancellationToken);
|
||||||
if (info.Value is null)
|
|
||||||
throw new Exception("Engine version info is null");
|
|
||||||
|
|
||||||
VersionInfos = info.Value;
|
|
||||||
|
|
||||||
_varService.SetConfigValue(CurrentConVar.EngineManifestBackup, info.Value);
|
VersionInfos = info;
|
||||||
|
|
||||||
|
_varService.SetConfigValue(CurrentConVar.EngineManifestBackup, info);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -64,11 +62,11 @@ public sealed class EngineService
|
|||||||
var moduleInfo = await _restService.GetAsync<ModulesInfo>(
|
var moduleInfo = await _restService.GetAsync<ModulesInfo>(
|
||||||
new Uri(_varService.GetConfigValue(CurrentConVar.EngineModuleManifestUrl)!), cancellationToken);
|
new Uri(_varService.GetConfigValue(CurrentConVar.EngineModuleManifestUrl)!), cancellationToken);
|
||||||
|
|
||||||
if (moduleInfo.Value is null)
|
if (moduleInfo is null)
|
||||||
throw new Exception("Module version info is null");
|
throw new Exception("Module version info is null");
|
||||||
|
|
||||||
ModuleInfos = moduleInfo.Value.Modules;
|
ModuleInfos = moduleInfo.Modules;
|
||||||
_varService.SetConfigValue(CurrentConVar.ModuleManifestBackup, moduleInfo.Value);
|
_varService.SetConfigValue(CurrentConVar.ModuleManifestBackup, moduleInfo);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -177,7 +175,7 @@ public sealed class EngineService
|
|||||||
|
|
||||||
var engineVersionObj = Version.Parse(engineVersion);
|
var engineVersionObj = Version.Parse(engineVersion);
|
||||||
var module = ModuleInfos[moduleName];
|
var module = ModuleInfos[moduleName];
|
||||||
var selectedVersion = module.Versions.Select(kv => new { Version = Version.Parse(kv.Key), kv.Key, kv.Value })
|
var selectedVersion = module.Versions.Select(kv => new { Version = Version.Parse(kv.Key), kv.Key, kv })
|
||||||
.Where(kv => engineVersionObj >= kv.Version)
|
.Where(kv => engineVersionObj >= kv.Version)
|
||||||
.MaxBy(kv => kv.Version);
|
.MaxBy(kv => kv.Version);
|
||||||
|
|
||||||
|
|||||||
@@ -23,19 +23,26 @@ public class RestService
|
|||||||
_debug = debug;
|
_debug = debug;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<RestResult<T>> GetAsync<T>(Uri uri, CancellationToken cancellationToken) where T : notnull
|
public async Task<T> GetAsync<T>(Uri uri, CancellationToken cancellationToken) where T : notnull
|
||||||
{
|
{
|
||||||
var response = await _client.GetAsync(uri, cancellationToken);
|
var response = await _client.GetAsync(uri, cancellationToken);
|
||||||
return await ReadResult<T>(response, cancellationToken);
|
return await ReadResult<T>(response, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T> GetAsyncDefault<T>(Uri uri, T defaultValue, CancellationToken cancellationToken)
|
public async Task<T> GetAsyncDefault<T>(Uri uri, T defaultValue, CancellationToken cancellationToken) where T : notnull
|
||||||
{
|
{
|
||||||
var result = await GetAsync<T>(uri, cancellationToken);
|
try
|
||||||
return result.Value ?? defaultValue;
|
{
|
||||||
|
return await GetAsync<T>(uri, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_debug.Error(e);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<RestResult<K>> PostAsync<K, T>(T information, Uri uri, CancellationToken cancellationToken) where K : notnull
|
public async Task<K> PostAsync<K, T>(T information, Uri uri, CancellationToken cancellationToken) where K : notnull
|
||||||
{
|
{
|
||||||
var json = JsonSerializer.Serialize(information, _serializerOptions);
|
var json = JsonSerializer.Serialize(information, _serializerOptions);
|
||||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
@@ -43,7 +50,7 @@ public class RestService
|
|||||||
return await ReadResult<K>(response, cancellationToken);
|
return await ReadResult<K>(response, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<RestResult<T>> PostAsync<T>(Stream stream, Uri uri, CancellationToken cancellationToken) where T : notnull
|
public async Task<T> PostAsync<T>(Stream stream, Uri uri, CancellationToken cancellationToken) where T : notnull
|
||||||
{
|
{
|
||||||
using var multipartFormContent =
|
using var multipartFormContent =
|
||||||
new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture));
|
new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture));
|
||||||
@@ -52,61 +59,32 @@ public class RestService
|
|||||||
return await ReadResult<T>(response, cancellationToken);
|
return await ReadResult<T>(response, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<RestResult<T>> DeleteAsync<T>(Uri uri, CancellationToken cancellationToken) where T : notnull
|
public async Task<T> DeleteAsync<T>(Uri uri, CancellationToken cancellationToken) where T : notnull
|
||||||
{
|
{
|
||||||
var response = await _client.DeleteAsync(uri, cancellationToken);
|
var response = await _client.DeleteAsync(uri, cancellationToken);
|
||||||
return await ReadResult<T>(response, cancellationToken);
|
return await ReadResult<T>(response, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<RestResult<T>> ReadResult<T>(HttpResponseMessage response, CancellationToken cancellationToken) where T : notnull
|
private async Task<T> ReadResult<T>(HttpResponseMessage response, CancellationToken cancellationToken) where T : notnull
|
||||||
{
|
{
|
||||||
var content = await response.Content.ReadAsStringAsync(cancellationToken);
|
var content = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
|
||||||
if (typeof(T) == typeof(RawResult))
|
if (typeof(T) == typeof(string) && content is T t)
|
||||||
return (new RestResult<RawResult>(new RawResult(content), null, response.StatusCode) as RestResult<T>)!;
|
return t;
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
_debug.Debug($"SUCCESSFUL GET CONTENT {typeof(T)}");
|
_debug.Debug($"SUCCESSFUL GET CONTENT {typeof(T)}");
|
||||||
|
|
||||||
return new RestResult<T>(await response.Content.AsJson<T>(), null,
|
return await response.Content.AsJson<T>();
|
||||||
response.StatusCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new HttpRequestException();
|
throw new RestRequestException(response.Content, response.StatusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RestResult<T>
|
public sealed class RestRequestException(HttpContent content, HttpStatusCode statusCode) : Exception
|
||||||
{
|
{
|
||||||
public string Message = "Ok";
|
public HttpStatusCode StatusCode { get; } = statusCode;
|
||||||
public HttpStatusCode StatusCode;
|
public HttpContent Content { get; } = content;
|
||||||
public T Value;
|
|
||||||
|
|
||||||
public RestResult(T value, string? message, HttpStatusCode statusCode)
|
|
||||||
{
|
|
||||||
Value = value;
|
|
||||||
if (message != null) Message = message;
|
|
||||||
StatusCode = statusCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator T(RestResult<T> result)
|
|
||||||
{
|
|
||||||
return result.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RawResult
|
|
||||||
{
|
|
||||||
public string Result;
|
|
||||||
|
|
||||||
public RawResult(string result)
|
|
||||||
{
|
|
||||||
Result = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator string(RawResult result)
|
|
||||||
{
|
|
||||||
return result.Result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user