diff --git a/Moonlight/App/Http/Controllers/Api/Moonlight/OAuth2Controller.cs b/Moonlight/App/Http/Controllers/Api/Moonlight/OAuth2Controller.cs index 5fc50874..9cff03d0 100644 --- a/Moonlight/App/Http/Controllers/Api/Moonlight/OAuth2Controller.cs +++ b/Moonlight/App/Http/Controllers/Api/Moonlight/OAuth2Controller.cs @@ -1,6 +1,7 @@ using Logging.Net; using Microsoft.AspNetCore.Mvc; using Moonlight.App.Services; +using Moonlight.App.Services.Sessions; namespace Moonlight.App.Http.Controllers.Api.Moonlight; @@ -11,12 +12,41 @@ public class OAuth2Controller : Controller private readonly UserService UserService; private readonly OAuth2Service OAuth2Service; private readonly DateTimeService DateTimeService; + private readonly IdentityService IdentityService; - public OAuth2Controller(UserService userService, OAuth2Service oAuth2Service, DateTimeService dateTimeService) + public OAuth2Controller( + UserService userService, + OAuth2Service oAuth2Service, + DateTimeService dateTimeService, + IdentityService identityService) { UserService = userService; OAuth2Service = oAuth2Service; DateTimeService = dateTimeService; + IdentityService = identityService; + } + + [HttpGet("{id}/start")] + public async Task Start([FromRoute] string id) + { + try + { + if (OAuth2Service.Providers.ContainsKey(id)) + { + return Redirect(await OAuth2Service.GetUrl(id)); + } + + Logger.Warn($"Someone tried to start an oauth2 flow using the id '{id}' which is not registered"); + + return Redirect("/"); + } + catch (Exception e) + { + Logger.Warn($"Error starting oauth2 flow for id: {id}"); + Logger.Warn(e); + + return Redirect("/"); + } } [HttpGet("{id}")] @@ -24,6 +54,18 @@ public class OAuth2Controller : Controller { try { + var currentUser = await IdentityService.Get(); + + if (currentUser != null) + { + if (await OAuth2Service.CanBeLinked(id)) + { + await OAuth2Service.LinkToUser(id, currentUser, code); + + return Redirect("/profile"); + } + } + var user = await OAuth2Service.HandleCode(id, code); Response.Cookies.Append("token", await UserService.GenerateToken(user), new() diff --git a/Moonlight/App/OAuth2/OAuth2Provider.cs b/Moonlight/App/OAuth2/OAuth2Provider.cs index af582c13..bad02d7e 100644 --- a/Moonlight/App/OAuth2/OAuth2Provider.cs +++ b/Moonlight/App/OAuth2/OAuth2Provider.cs @@ -9,7 +9,9 @@ public abstract class OAuth2Provider public string Url { get; set; } public IServiceScopeFactory ServiceScopeFactory { get; set; } public string DisplayName { get; set; } + public bool CanBeLinked { get; set; } = false; public abstract Task GetUrl(); public abstract Task HandleCode(string code); + public abstract Task LinkToUser(User user, string code); } \ No newline at end of file diff --git a/Moonlight/App/OAuth2/Providers/DiscordOAuth2Provider.cs b/Moonlight/App/OAuth2/Providers/DiscordOAuth2Provider.cs index 5ed2efea..ffe58b29 100644 --- a/Moonlight/App/OAuth2/Providers/DiscordOAuth2Provider.cs +++ b/Moonlight/App/OAuth2/Providers/DiscordOAuth2Provider.cs @@ -12,6 +12,11 @@ namespace Moonlight.App.OAuth2.Providers; public class DiscordOAuth2Provider : OAuth2Provider { + public DiscordOAuth2Provider() + { + CanBeLinked = true; + } + public override Task GetUrl() { string url = $"https://discord.com/api/oauth2/authorize?client_id={Config.ClientId}" + @@ -119,4 +124,74 @@ public class DiscordOAuth2Provider : OAuth2Provider return user; } } + + public override async Task LinkToUser(User user, string code) + { + // Endpoints + + var endpoint = Url + "/api/moonlight/oauth2/discord"; + var discordUserDataEndpoint = "https://discordapp.com/api/users/@me"; + var discordEndpoint = "https://discordapp.com/api/oauth2/token"; + + // Generate access token + + using var client = new RestClient(); + var request = new RestRequest(discordEndpoint); + + request.AddParameter("client_id", Config.ClientId); + request.AddParameter("client_secret", Config.ClientSecret); + request.AddParameter("grant_type", "authorization_code"); + request.AddParameter("code", code); + request.AddParameter("redirect_uri", endpoint); + + var response = await client.ExecutePostAsync(request); + + if (!response.IsSuccessful) + { + Logger.Warn("Error verifying oauth2 code"); + Logger.Warn(response.ErrorMessage); + throw new DisplayException("An error occured while verifying oauth2 code"); + } + + // parse response + + var data = new ConfigurationBuilder().AddJsonStream( + new MemoryStream(Encoding.ASCII.GetBytes(response.Content!)) + ).Build(); + + var accessToken = data.GetValue("access_token"); + + // Now, we will call the discord api with our access token to get the data we need + + var getRequest = new RestRequest(discordUserDataEndpoint); + getRequest.AddHeader("Authorization", $"Bearer {accessToken}"); + + var getResponse = await client.ExecuteGetAsync(getRequest); + + if (!getResponse.IsSuccessful) + { + Logger.Warn("An unexpected error occured while fetching user data from remote api"); + Logger.Warn(getResponse.ErrorMessage); + + throw new DisplayException("An unexpected error occured while fetching user data from remote api"); + } + + // Parse response + + var getData = new ConfigurationBuilder().AddJsonStream( + new MemoryStream(Encoding.ASCII.GetBytes(getResponse.Content!)) + ).Build(); + + var id = getData.GetValue("id"); + + // Handle data + + using var scope = ServiceScopeFactory.CreateScope(); + + var userRepo = scope.ServiceProvider.GetRequiredService>(); + + user.DiscordId = id; + + userRepo.Update(user); + } } \ No newline at end of file diff --git a/Moonlight/App/OAuth2/Providers/GoogleOAuth2Provider.cs b/Moonlight/App/OAuth2/Providers/GoogleOAuth2Provider.cs index 72758542..c0e56d55 100644 --- a/Moonlight/App/OAuth2/Providers/GoogleOAuth2Provider.cs +++ b/Moonlight/App/OAuth2/Providers/GoogleOAuth2Provider.cs @@ -4,7 +4,6 @@ using Moonlight.App.ApiClients.Google.Requests; using Moonlight.App.Database.Entities; using Moonlight.App.Exceptions; using Moonlight.App.Helpers; -using Moonlight.App.Models.Misc; using Moonlight.App.Repositories; using Moonlight.App.Services; using RestSharp; @@ -13,6 +12,11 @@ namespace Moonlight.App.OAuth2.Providers; public class GoogleOAuth2Provider : OAuth2Provider { + public GoogleOAuth2Provider() + { + CanBeLinked = false; + } + public override Task GetUrl() { var endpoint = Url + "/api/moonlight/oauth2/google"; @@ -127,4 +131,9 @@ public class GoogleOAuth2Provider : OAuth2Provider return user; } } + + public override Task LinkToUser(User user, string code) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/Moonlight/App/Services/OAuth2Service.cs b/Moonlight/App/Services/OAuth2Service.cs index c96bd497..31f17ff4 100644 --- a/Moonlight/App/Services/OAuth2Service.cs +++ b/Moonlight/App/Services/OAuth2Service.cs @@ -80,6 +80,26 @@ public class OAuth2Service return await provider.HandleCode(code); } + public Task CanBeLinked(string id) + { + if (Providers.All(x => x.Key != id)) + throw new DisplayException("Invalid oauth2 id"); + + var provider = Providers[id]; + + return Task.FromResult(provider.CanBeLinked); + } + + public async Task LinkToUser(string id, User user, string code) + { + if (Providers.All(x => x.Key != id)) + throw new DisplayException("Invalid oauth2 id"); + + var provider = Providers[id]; + + await provider.LinkToUser(user, code); + } + private string GetAppUrl() { if (EnableOverrideUrl) diff --git a/Moonlight/Shared/Components/Navigations/ProfileNavigation.razor b/Moonlight/Shared/Components/Navigations/ProfileNavigation.razor index ab04ef6e..dbf3bea5 100644 --- a/Moonlight/Shared/Components/Navigations/ProfileNavigation.razor +++ b/Moonlight/Shared/Components/Navigations/ProfileNavigation.razor @@ -9,7 +9,7 @@
@(User.FirstName) @(User.LastName) - + @if (User.Status == UserStatus.Verified) { @@ -31,12 +31,17 @@ + diff --git a/Moonlight/Shared/Views/Profile/Discord.razor b/Moonlight/Shared/Views/Profile/Discord.razor new file mode 100644 index 00000000..c6236d00 --- /dev/null +++ b/Moonlight/Shared/Views/Profile/Discord.razor @@ -0,0 +1,77 @@ +@page "/profile/discord" + +@using Moonlight.Shared.Components.Navigations +@using Moonlight.App.Database.Entities +@using Moonlight.App.Repositories +@using Moonlight.App.Services + +@inject Repository UserRepository +@inject SmartTranslateService SmartTranslateService + + + +@if (User.DiscordId == 0) +{ +
+
+
+ + +
+

+ Your account is currently not linked to discord +

+ To use features like the discord bot, link your moonlight account with your discord account
+
+
+
+ + +
+} +else +{ +
+
+
+ + +
+

+ Your account is linked to a discord account +

+ You are able to use features like the discord bot of moonlight +
+
+
+
+
+ + +
+
+
+} + +@code +{ + [CascadingParameter] + public User User { get; set; } + + private async Task RemoveLink() + { + User.DiscordId = 0; + UserRepository.Update(User); + + await InvokeAsync(StateHasChanged); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Profile/Security.razor b/Moonlight/Shared/Views/Profile/Security.razor index 7cd7bff4..5aad28bc 100644 --- a/Moonlight/Shared/Views/Profile/Security.razor +++ b/Moonlight/Shared/Views/Profile/Security.razor @@ -19,7 +19,7 @@ @inject AlertService AlertService @inject ToastService ToastService - +
diff --git a/Moonlight/Shared/Views/Profile/Subscriptions.razor b/Moonlight/Shared/Views/Profile/Subscriptions.razor index c8382775..2cfc7d1a 100644 --- a/Moonlight/Shared/Views/Profile/Subscriptions.razor +++ b/Moonlight/Shared/Views/Profile/Subscriptions.razor @@ -11,7 +11,7 @@ @inject SubscriptionService SubscriptionService @inject SmartTranslateService SmartTranslateService - +