- add: TFA think

This commit is contained in:
2025-02-01 18:19:18 +03:00
parent 77956d35f3
commit 32fd63c5f3
16 changed files with 351 additions and 148 deletions

View File

@@ -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" />

View 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/"
]);
}

View File

@@ -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); );

View File

@@ -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();
} }

View 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;
}

View File

@@ -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()

View File

@@ -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"

View 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>

View 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();
}
}

View File

@@ -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"

View File

@@ -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 =

View File

@@ -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;
}
}

View File

@@ -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,
}

View File

@@ -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;
} }

View File

@@ -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);

View File

@@ -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;
}
} }