diff --git a/src/main/Program.cs b/src/main/Program.cs index ab4aa34..32c519d 100644 --- a/src/main/Program.cs +++ b/src/main/Program.cs @@ -1,4 +1,6 @@ -using Lentia.Utils; +using Lentia.Core.Auth.OAuth2; +using Lentia.Core.Constants; +using Lentia.Utils; using Photino.NET; using System.Text.Json; using LentRules = Lentia.Core.Auth.Yggdrasil; @@ -9,7 +11,6 @@ class Program { private static LentRules.AuthenticateResponse? _authenticatedPlayer; private static readonly string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); private static readonly LentRules.Authenticator _ygg = new(); - private static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; [STAThread] static void Main(string[] args) { @@ -64,13 +65,15 @@ class Program { case "auth::lentia": string user = jsonPayload.GetProperty("username").GetString()!; string pass = jsonPayload.GetProperty("password").GetString()!; - var result = await _ygg.Login(user, pass); + var lentiaAuthResult = await _ygg.Login(user, pass); - if (result.Success) { - _authenticatedPlayer = result.Player; + if (lentiaAuthResult.Success) { + _authenticatedPlayer = lentiaAuthResult.Player; WindowHelper.MakeStandardWindow(pw, 1356, 720, "wwwroot/logged.html"); + payload = new { success = true }; + } else { + payload = new { success = false, error = lentiaAuthResult.Error }; } - payload = result; break; case "auth::logout": @@ -83,7 +86,7 @@ class Program { case "dialog::error": string title = jsonPayload.TryGetProperty("title", out var t) ? t.GetString()! : "Lentia"; string msg = jsonPayload.GetProperty("message").GetString()!; - pw.ShowMessage(title, msg); + pw.ShowMessage(title, msg, PhotinoDialogButtons.Ok, PhotinoDialogIcon.Error); payload = new { success = true }; break; @@ -188,10 +191,25 @@ class Program { payload = new { success = false, error = ex.Message }; } break; + + case "oauth2::discord": + BashUtils.OpenUrl("https://yggdrasil.azures.fr/auth/provider/discord/login"); + string? code = await Discord.ListenForCode(); + var discordLoginResult = await Discord.LoginWithDiscordOAuth2(code!); + + if (discordLoginResult.Success) { + _authenticatedPlayer = discordLoginResult.Player; + WindowHelper.MakeStandardWindow(pw, 1356, 720, "wwwroot/logged.html"); + payload = new { success = true }; + } else { + Console.WriteLine(discordLoginResult); + payload = new { success = false, error = discordLoginResult.Error }; + } + break; } var response = new { requestId, payload }; - window.SendWebMessage(JsonSerializer.Serialize(response, _jsonOptions)); + window.SendWebMessage(JsonSerializer.Serialize(response, LauncherConstants._jsonOptions)); } catch (Exception ex) { Console.WriteLine($"Bridge error: {ex.Message}"); } diff --git a/src/main/core/Constants.cs b/src/main/core/Constants.cs index 5ea64d7..6a5bc3f 100644 --- a/src/main/core/Constants.cs +++ b/src/main/core/Constants.cs @@ -1,11 +1,12 @@ using System.Net.Http; +using System.Text.Json; namespace Lentia.Core.Constants { public static class LauncherConstants { public static readonly HttpClient Http = new HttpClient(); - + public static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; public static class Urls { public const string MojangManifest = "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"; diff --git a/src/main/core/auth/oauth2/Discord.cs b/src/main/core/auth/oauth2/Discord.cs index e69de29..9b6f572 100644 --- a/src/main/core/auth/oauth2/Discord.cs +++ b/src/main/core/auth/oauth2/Discord.cs @@ -0,0 +1,57 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using Lentia.Core.Auth.Yggdrasil; +using Lentia.Core.Constants; + +namespace Lentia.Core.Auth.OAuth2; + +public class Discord { + + private static HttpListener? _listener; + private static readonly string _url = "http://localhost:8877/auth/provider/discord/login/callback/"; + + public static async Task ListenForCode() { + _listener = new HttpListener(); + _listener.Prefixes.Add(_url); + _listener.Start(); + + HttpListenerContext context = await _listener.GetContextAsync(); + HttpListenerRequest request = context.Request; + + string? code = request.QueryString["code"]; + string redirectUrl = "https://yggdrasil.azures.fr/static/success.html"; + + HttpListenerResponse response = context.Response; + + response.StatusCode = (int)HttpStatusCode.Redirect; + response.RedirectLocation = redirectUrl; + response.OutputStream.Close(); + + _listener.Stop(); + + return code; + } + + public static async Task LoginWithDiscordOAuth2(string authCode) { + var url = $"https://yggdrasil.azures.fr/auth/provider/discord/login/callback?code={authCode}&requestUser=true"; + try { + var response = await LauncherConstants.Http.GetAsync(url); + if (response.IsSuccessStatusCode) { + var data = await response.Content.ReadFromJsonAsync(); + return data != null ? AuthResult.Ok(data) : AuthResult.Fail("Erreur de lecture des données."); + } else { + var errorData = await response.Content.ReadFromJsonAsync(); + Console.WriteLine(await response.Content.ReadAsStringAsync()); + return AuthResult.Fail( + errorData?.errorMessage ?? "Identifiants invalides.", + errorData?.error, + errorData?.cause + ); + } + } catch (Exception ex) { + throw new Exception($"Error when establishing connection to the API : {ex.Message}"); + } + } + +} \ No newline at end of file diff --git a/src/main/utils/Bash.cs b/src/main/utils/Bash.cs index bf83d7c..612b923 100644 --- a/src/main/utils/Bash.cs +++ b/src/main/utils/Bash.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Runtime.InteropServices; namespace Lentia.Utils; @@ -16,4 +17,22 @@ public static class BashUtils { proc.Start(); return proc.StandardOutput.ReadToEnd(); } + + public static void OpenUrl(string url) { + try { + Process.Start(url); + } + catch { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + url = url.Replace("&", "^&"); + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { + Process.Start("xdg-open", url); + } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { + Process.Start("open", url); + } else { + throw; + } + } + } } \ No newline at end of file diff --git a/src/main/utils/MojangAPI.cs b/src/main/utils/MojangAPI.cs index 3a26d37..19b1e6f 100644 --- a/src/main/utils/MojangAPI.cs +++ b/src/main/utils/MojangAPI.cs @@ -1,15 +1,14 @@ using System.Net.Http.Headers; using System.Text; using System.Text.Json; +using Lentia.Core.Constants; namespace Lentia.Utils; public static class MojangAPI { - private static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; public static async Task UploadSkinAsync(string filePath, string variant, string token) { - using var client = new HttpClient(); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + LauncherConstants.Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using var content = new MultipartFormDataContent(); @@ -21,7 +20,7 @@ public static class MojangAPI { content.Add(fileContent, "file", Path.GetFileName(filePath)); - var response = await client.PostAsync("https://yggdrasil.azures.fr/minecraftservices/minecraft/profile/skins", content); + var response = await LauncherConstants.Http.PostAsync("https://yggdrasil.azures.fr/minecraftservices/minecraft/profile/skins", content); if (response.IsSuccessStatusCode) { return await response.Content.ReadAsStringAsync(); @@ -31,66 +30,61 @@ public static class MojangAPI { } - public static async Task GetPlayerProfileAsync(string accessToken) { - using var client = new HttpClient(); - - client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); - client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); + public static async Task GetPlayerProfileAsync(string accessToken) { + LauncherConstants.Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + LauncherConstants.Http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var response = await client.GetAsync("https://yggdrasil.azures.fr/minecraftservices/minecraft/profile"); + var response = await LauncherConstants.Http.GetAsync("https://yggdrasil.azures.fr/minecraftservices/minecraft/profile"); if (response.IsSuccessStatusCode) { string jsonResponse = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize(jsonResponse, _jsonOptions)!; + return JsonSerializer.Deserialize(jsonResponse, LauncherConstants._jsonOptions)!; } else { throw new Exception($"API Error: {response.StatusCode}"); } } public static async Task HideCapeAsync(string accessToken) { - using var client = new HttpClient(); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + LauncherConstants.Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - var response = await client.DeleteAsync("https://yggdrasil.azures.fr/minecraftservices/minecraft/profile/capes/active"); + var response = await LauncherConstants.Http.DeleteAsync("https://yggdrasil.azures.fr/minecraftservices/minecraft/profile/capes/active"); if (response.IsSuccessStatusCode) { string jsonResponse = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize(jsonResponse, _jsonOptions)!; + return JsonSerializer.Deserialize(jsonResponse, LauncherConstants._jsonOptions)!; } else { throw new Exception($"Échec du retrait de la cape : {response.StatusCode}"); } } public static async Task ShowCapeAsync(string accessToken, string capeId) { - using var client = new HttpClient(); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + LauncherConstants.Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var body = new { capeId = capeId }; string jsonBody = JsonSerializer.Serialize(body); var content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); - var response = await client.PutAsync("https://yggdrasil.azures.fr/minecraftservices/minecraft/profile/capes/active", content); + var response = await LauncherConstants.Http.PutAsync("https://yggdrasil.azures.fr/minecraftservices/minecraft/profile/capes/active", content); if (response.IsSuccessStatusCode) { string jsonResponse = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize(jsonResponse, _jsonOptions)!; + return JsonSerializer.Deserialize(jsonResponse, LauncherConstants._jsonOptions)!; } else { throw new Exception($"Erreur lors de l'équipement de la cape : {response.StatusCode}"); } } public static async Task ChangeUsernameAsync(string newName, string accessToken) { - using var client = new HttpClient(); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + LauncherConstants.Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var url = $"https://yggdrasil.azures.fr/minecraftservices/minecraft/profile/name/{newName}"; var request = new HttpRequestMessage(HttpMethod.Put, url); - var response = await client.SendAsync(request); + var response = await LauncherConstants.Http.SendAsync(request); if (response.IsSuccessStatusCode) { string jsonResponse = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize(jsonResponse, _jsonOptions)!; + return JsonSerializer.Deserialize(jsonResponse, LauncherConstants._jsonOptions)!; } else { throw new Exception($"Erreur lors du changement de pseudo (Code: {response.StatusCode})"); diff --git a/wwwroot/assets/js/logged.js b/wwwroot/assets/js/logged.js index 8f78c5b..70755d7 100644 --- a/wwwroot/assets/js/logged.js +++ b/wwwroot/assets/js/logged.js @@ -1,6 +1,7 @@ const buttons = document.querySelectorAll("button[frame]") const dynmapFrame = document.querySelector("article.frame.dynmap > iframe") const capesSelector = document.querySelector("article.capes > div.capes") +const logsContainer = document.querySelector("div.container.logs") let viewerInstance = new skinview3d.SkinViewer({ canvas: document.getElementById("skin"), @@ -99,7 +100,7 @@ window.initSkin = async function initSkin() { canvas: document.getElementById("skin"), width: container.clientWidth, height: container.clientHeight, - skin: `https://yggdrasil.azures.fr/textures/${profile.skins.find(skin => skin.state == "ACTIVE").url}`.replace(/\/+$/, "") + skin: `https://yggdrasil.azures.fr/textures/${profile.skins.find(skin => skin.state == "ACTIVE").url.replace(/^\//, "")}` }) const activeCape = window.profile.capes.find(s => s.state === "ACTIVE") || null @@ -228,6 +229,18 @@ window.changeUsername = async function changeUsername(newName) { } } +window.gamelog = {} + +window.gamelog.put = async function put(log) { + const logElement = document.createElement("p") + logElement.innerText = log + logsContainer.appendChild(log) +} + +window.gamelog.clear = async function clear() { + logsContainer.innerHTML = "" +} + await initSkin() await initSettings() await initCapesSelector() diff --git a/wwwroot/assets/js/login.js b/wwwroot/assets/js/login.js index 2e4728c..e101bc9 100644 --- a/wwwroot/assets/js/login.js +++ b/wwwroot/assets/js/login.js @@ -17,22 +17,32 @@ async function login() { const username = document.querySelector("#username").value const password = document.querySelector("#password").value const result = await system.call("auth::lentia", { username, password }) - if (result.success == false) { - await system.call("dialog::error", { - title: result.error.error, - message: result.error.errorMessage - }) - } + await processLoginResult(result) } function clearPassword() { password.value = "" } -function requestLoginWithOAuth2() { +async function requestLoginWithOAuth2() { showFrame("oauth2") hideInformation() showLoadingBar() + const result = await system.call("oauth2::discord") + await processLoginResult(result, true) +} + +async function processLoginResult(payload, showDefaultPage = false) { + if (payload.success == false) { + await system.call("dialog::error", { + title: payload.error.error, + message: payload.error.errorMessage + }) + if (showDefaultPage) { + hideLoadingBar() + showFrame("provider") + } + } } hideLoadingBar()