Discord authentification (#819)

* Auth setup

* fixes

* messages

* adapt to new auth

* cvar fix

* generating link setup

* new version adapt

* fix

* Update DiscordAuthManager.cs
This commit is contained in:
Ed
2025-02-04 22:44:22 +03:00
committed by GitHub
parent dd6ddf840b
commit b2234b7c4a
19 changed files with 394 additions and 2 deletions

View File

@@ -1,3 +1,4 @@
using Content.Client._CP14.Discord;
using Content.Client.Administration.Managers;
using Content.Client.Changelog;
using Content.Client.Chat.Managers;
@@ -43,6 +44,9 @@ namespace Content.Client.Entry
{
public sealed class EntryPoint : GameClient
{
//CP14
[Dependency] private readonly DiscordAuthManager _discordAuth = default!;
//CP14 end
[Dependency] private readonly IBaseClient _baseClient = default!;
[Dependency] private readonly IGameController _gameController = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
@@ -160,7 +164,10 @@ namespace Content.Client.Entry
_parallaxManager.LoadDefaultParallax();
_overlayManager.AddOverlay(new CP14BasePostProcessOverlay()); // CP14-PostProcess
//CP14
_overlayManager.AddOverlay(new CP14BasePostProcessOverlay());
_discordAuth.Initialize();
//CP14 end
_overlayManager.AddOverlay(new SingularityOverlay());
_overlayManager.AddOverlay(new RadiationPulseOverlay());
_chatManager.Initialize();

View File

@@ -1,3 +1,4 @@
using Content.Client._CP14.Discord;
using Content.Client.Administration.Managers;
using Content.Client.Changelog;
using Content.Client.Chat.Managers;
@@ -33,6 +34,9 @@ namespace Content.Client.IoC
{
var collection = IoCManager.Instance!;
//CP14
collection.Register<DiscordAuthManager>();
//CP14 end
collection.Register<IParallaxManager, ParallaxManager>();
collection.Register<IChatManager, ChatManager>();
collection.Register<ISharedChatManager, ChatManager>();

View File

@@ -0,0 +1,27 @@
using Content.Shared._CP14.Discord;
using Robust.Client.State;
using Robust.Shared.Network;
namespace Content.Client._CP14.Discord;
public sealed class DiscordAuthManager
{
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
public string AuthUrl { get; private set; } = "";
public void Initialize()
{
_netManager.RegisterNetMessage<MsgDiscordAuthCheck>();
_netManager.RegisterNetMessage<MsgDiscordAuthRequired>(OnDiscordAuthRequired);
}
private void OnDiscordAuthRequired(MsgDiscordAuthRequired msg)
{
if (_stateManager.CurrentState is DiscordAuthState)
return;
AuthUrl = msg.AuthUrl;
_stateManager.RequestStateChange<DiscordAuthState>();
}
}

View File

@@ -0,0 +1,36 @@
using System.Threading;
using Content.Shared._CP14.Discord;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Shared.Network;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Client._CP14.Discord;
public sealed class DiscordAuthState : State
{
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IClientNetManager _netManager = default!;
private DiscordAuthGui? _gui;
private readonly CancellationTokenSource _checkTimerCancel = new();
protected override void Startup()
{
_gui = new DiscordAuthGui();
_userInterfaceManager.StateRoot.AddChild(_gui);
Timer.SpawnRepeating(TimeSpan.FromSeconds(5),
() =>
{
_netManager.ClientSendMessage(new MsgDiscordAuthCheck());
},
_checkTimerCancel.Token);
}
protected override void Shutdown()
{
_checkTimerCancel.Cancel();
_gui!.Dispose();
}
}

View File

@@ -0,0 +1,29 @@
<Control xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:parallax="clr-namespace:Content.Client.Parallax"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
<parallax:ParallaxControl />
<Control HorizontalAlignment="Center" VerticalAlignment="Center">
<PanelContainer StyleClasses="AngleRect" />
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Margin="8 0 0 0" Text="{Loc 'cp14-discord-auth-title'}"
StyleClasses="LabelHeading" VAlign="Center" />
<Button Name="QuitButton" Text="{Loc 'cp14-discord-auth-quit-btn'}"
HorizontalAlignment="Right" HorizontalExpand="True" />
</BoxContainer>
<controls:HighDivider />
<BoxContainer Orientation="Vertical" Margin="50 20 50 20">
<RichTextLabel Name="InfoLabel" />
</BoxContainer>
<BoxContainer HorizontalExpand="True">
<LineEdit Editable="False" Name="AuthLinkEdit" HorizontalExpand="True" />
<LineEdit Editable="False" Name="DLinkEdit" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Button Name="AuthorizeButton" Text="{Loc 'cp14-discord-auth-text'}" HorizontalExpand="True" StyleClasses="OpenRight" />
<Button Name="DiscordButton" Text="{Loc 'cp14-discord-auth-browser-btn'}" HorizontalExpand="True" StyleClasses="OpenRight" />
</BoxContainer>
</BoxContainer>
</Control>
</Control>

View File

@@ -0,0 +1,46 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Console;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._CP14.Discord;
[GenerateTypedNameReferences]
public sealed partial class DiscordAuthGui : Control
{
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly DiscordAuthManager _discordAuthManager = default!;
private const string DiscordLink = "https://discord.com/invite/Sud2DMfhCC"; //TODO: Unhardcode
public DiscordAuthGui()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
LayoutContainer.SetAnchorPreset(this, LayoutContainer.LayoutPreset.Wide);
var link = _discordAuthManager.AuthUrl;
AuthLinkEdit.SetText(link);
DLinkEdit.SetText(DiscordLink);
InfoLabel.SetMessage(Loc.GetString("cp14-discord-info"));
var uriOpener = IoCManager.Resolve<IUriOpener>();
QuitButton.OnPressed += _ =>
{
_consoleHost.ExecuteCommand("quit");
};
AuthorizeButton.OnPressed += _ =>
{
uriOpener.OpenUri(link);
};
DiscordButton.OnPressed += _ =>
{
uriOpener.OpenUri(DiscordLink);
};
}
}

View File

@@ -1,3 +1,4 @@
using Content.Server._CP14.Discord;
using Content.Server.Acz;
using Content.Server.Administration;
using Content.Server.Administration.Logs;
@@ -101,6 +102,10 @@ namespace Content.Server.Entry
logManager.GetSawmill("Storage").Level = LogLevel.Info;
logManager.GetSawmill("db.ef").Level = LogLevel.Info;
//CP14
IoCManager.Resolve<DiscordAuthManager>().Initialize();
//CP14 end
IoCManager.Resolve<IAdminLogManager>().Initialize();
IoCManager.Resolve<IConnectionManager>().Initialize();
_dbManager.Init();

View File

@@ -57,7 +57,7 @@ namespace Content.Server.GameTicking
// Make the player actually join the game.
// timer time must be > tick length
Timer.Spawn(0, () => _playerManager.JoinGame(args.Session));
//Timer.Spawn(0, () => _playerManager.JoinGame(args.Session)); //CP14 Discord AuthManager
var record = await _db.GetPlayerRecordByUserId(args.Session.UserId);
var firstConnection = record != null &&

View File

@@ -1,3 +1,4 @@
using Content.Server._CP14.Discord;
using Content.Server.Administration;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
@@ -36,6 +37,9 @@ namespace Content.Server.IoC
{
public static void Register()
{
//CP14
IoCManager.Register<DiscordAuthManager>();
//CP14 end
IoCManager.Register<IChatManager, ChatManager>();
IoCManager.Register<ISharedChatManager, ChatManager>();
IoCManager.Register<IChatSanitizationManager, ChatSanitizationManager>();

View File

@@ -0,0 +1,138 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Content.Shared._CP14.Discord;
using Content.Shared.CCVar;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server._CP14.Discord;
public sealed class DiscordAuthManager
{
[Dependency] private readonly IServerNetManager _netMgr = default!;
[Dependency] private readonly IPlayerManager _playerMgr = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private ISawmill _sawmill = default!;
private readonly HttpClient _httpClient = new();
private bool _enabled = false;
private string _apiUrl = string.Empty;
private string _apiKey = string.Empty;
public event EventHandler<ICommonSession>? PlayerVerified;
public void Initialize()
{
_sawmill = Logger.GetSawmill("discordAuth");
_cfg.OnValueChanged(CCVars.DiscordAuthEnabled, v => _enabled = v, true);
_cfg.OnValueChanged(CCVars.DiscordAuthUrl, v => _apiUrl = v, true);
_cfg.OnValueChanged(CCVars.DiscordAuthToken, v => _apiKey = v, true);
_netMgr.RegisterNetMessage<MsgDiscordAuthRequired>();
_netMgr.RegisterNetMessage<MsgDiscordAuthCheck>(OnAuthCheck);
_playerMgr.PlayerStatusChanged += OnPlayerStatusChanged;
PlayerVerified += OnPlayerVerified;
}
private void OnPlayerVerified(object? obj, ICommonSession session)
{
Timer.Spawn(0, () => _playerMgr.JoinGame(session));
}
private async void OnAuthCheck(MsgDiscordAuthCheck msg)
{
var verified = await IsVerified(msg.MsgChannel.UserId);
if (!verified)
return;
var session = _playerMgr.GetSessionById(msg.MsgChannel.UserId);
PlayerVerified?.Invoke(this, session);
}
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs args)
{
if (args.NewStatus != SessionStatus.Connected)
return;
if (!_enabled)
{
PlayerVerified?.Invoke(this, args.Session);
return;
}
if (args.NewStatus == SessionStatus.Connected)
{
var verified = await IsVerified(args.Session.UserId);
if (verified)
{
PlayerVerified?.Invoke(this, args.Session);
return;
}
var message = new MsgDiscordAuthRequired();
message.AuthUrl = await GenerateLink(args.Session.UserId) ?? string.Empty;
args.Session.Channel.SendMessage(message);
}
}
public async Task<bool> IsVerified(NetUserId userId, CancellationToken cancel = default)
{
_sawmill.Debug($"Player {userId} check Discord verification");
var requestUrl = $"{_apiUrl}/api/uuid?method=uid&id={userId}";
_sawmill.Debug($"Auth request url:{requestUrl}");
var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
var response = await _httpClient.SendAsync(request, cancel);
_sawmill.Debug($"{await response.Content.ReadAsStringAsync(cancel)}");
_sawmill.Debug($"{(int) response.StatusCode}");
return response.StatusCode == HttpStatusCode.OK;
}
public async Task<string?> GenerateLink(NetUserId userId, CancellationToken cancel = default)
{
_sawmill.Debug($"Generating link for {userId}");
var requestUrl = $"{_apiUrl}/api/link?uid={userId}";
// try catch block to catch HttpRequestExceptions due to remote service unavailability
try
{
var response = await _httpClient.GetAsync(requestUrl, cancel);
if (!response.IsSuccessStatusCode)
return null;
var link = await response.Content.ReadFromJsonAsync<DiscordLinkResponse>(cancel);
return link!.Link;
}
catch (HttpRequestException)
{
_sawmill.Error("Remote auth service is unreachable. Check if its online!");
return null;
}
catch (Exception e)
{
_sawmill.Error($"Unexpected error verifying user via auth service. Error: {e.Message}. Stack: \n{e.StackTrace}");
return null;
}
}
sealed class DiscordLinkResponse
{
[JsonPropertyName("link")]
public string Link { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,15 @@
using Robust.Shared.Configuration;
namespace Content.Shared.CCVar;
public sealed partial class CCVars
{
public static readonly CVarDef<bool> DiscordAuthEnabled =
CVarDef.Create("cp14.discord_auth_enabled", false, CVar.SERVERONLY);
public static readonly CVarDef<string> DiscordAuthUrl =
CVarDef.Create("cp14.discord_auth_url", "http://localhost:8000/sponsors", CVar.SERVERONLY | CVar.CONFIDENTIAL);
public static readonly CVarDef<string> DiscordAuthToken =
CVarDef.Create("cp14.discord_auth_token", "token", CVar.SERVERONLY | CVar.CONFIDENTIAL);
}

View File

@@ -0,0 +1,9 @@
using Robust.Shared.Configuration;
namespace Content.Shared.CCVar;
public sealed partial class CCVars
{
public static readonly CVarDef<bool> QueueEnabled =
CVarDef.Create("cp14.join_queue_enabled", true, CVar.SERVERONLY);
}

View File

@@ -0,0 +1,12 @@
using Robust.Shared.Configuration;
namespace Content.Shared.CCVar;
public sealed partial class CCVars
{
public static readonly CVarDef<string> SponsorsApiUrl =
CVarDef.Create("cp14.sponsor_api_url", "http://localhost:8000/sponsors", CVar.SERVERONLY | CVar.CONFIDENTIAL);
public static readonly CVarDef<string> SponsorsApiKey =
CVarDef.Create("cp14.sponsor_api_key", "token", CVar.SERVERONLY | CVar.CONFIDENTIAL);
}

View File

@@ -0,0 +1,18 @@
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
namespace Content.Shared._CP14.Discord;
public sealed class MsgDiscordAuthCheck : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Command;
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
}
}

View File

@@ -0,0 +1,21 @@
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
namespace Content.Shared._CP14.Discord;
public sealed class MsgDiscordAuthRequired : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Command;
public string AuthUrl { get; set; } = string.Empty;
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
AuthUrl = buffer.ReadString();
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
buffer.Write(AuthUrl);
}
}

View File

@@ -0,0 +1,5 @@
stalker-discord-info = To play on the server you need to go to our server in discord to authorise your account.
stalker-discord-auth-quit-btn = Exit
stalker-discord-auth-title = Authorisation
stalker-discord-auth-link = https://discord.com/invite/Sud2DMfhCC
stalker-discord-auth-browser-btn = Discord Server

View File

@@ -0,0 +1,5 @@
queue-title = Queue
queue-quit = Disconnect
queue-position = Position
queue-total = Total
queue-priority-join = Priority join

View File

@@ -0,0 +1,6 @@
cp14-discord-info = Для игры на сервере вам необходимо пройти на наш сервер в дискорд для авторизации вашего аккаунта.
cp14-discord-auth-quit-btn = Выход
cp14-discord-auth-title = Авторизация
cp14-discord-auth-link = https://discord.com/invite/Sud2DMfhCC
cp14-discord-auth-browser-btn = Сервер Discord
cp14-discord-auth-text = Авторизоваться

View File

@@ -0,0 +1,5 @@
queue-title = Очередь
queue-quit = Отключиться
queue-position = Позиция
queue-total = Всего
queue-priority-join = Приоритетный вход