Implemented new discord linking system

This commit is contained in:
Marcel Baumgartner
2023-05-26 17:14:06 +02:00
parent 93328b8b88
commit ac3bdba3e8
9 changed files with 237 additions and 7 deletions

View File

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

View File

@@ -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<string> GetUrl();
public abstract Task<User> HandleCode(string code);
public abstract Task LinkToUser(User user, string code);
}

View File

@@ -12,6 +12,11 @@ namespace Moonlight.App.OAuth2.Providers;
public class DiscordOAuth2Provider : OAuth2Provider
{
public DiscordOAuth2Provider()
{
CanBeLinked = true;
}
public override Task<string> 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<string>("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<ulong>("id");
// Handle data
using var scope = ServiceScopeFactory.CreateScope();
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
user.DiscordId = id;
userRepo.Update(user);
}
}

View File

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

View File

@@ -80,6 +80,26 @@ public class OAuth2Service
return await provider.HandleCode(code);
}
public Task<bool> 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)

View File

@@ -31,12 +31,17 @@
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/profile/security">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/profile/discord">
<TL>Discord</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 2 ? "active" : "")" href="/profile/security">
<TL>Security</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 2 ? "active" : "")" href="/profile/subscriptions">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 3 ? "active" : "")" href="/profile/subscriptions">
<TL>Subscriptions</TL>
</a>
</li>

View File

@@ -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<User> UserRepository
@inject SmartTranslateService SmartTranslateService
<ProfileNavigation Index="1"/>
@if (User.DiscordId == 0)
{
<div class="row">
<div class="col">
<div class="alert bg-warning d-flex flex-column flex-sm-row p-5 mb-10">
<i class="fs-2hx text-light me-4 mb-5 mb-sm-0 bx bx-error bx-lg"></i>
<div class="d-flex flex-column text-light pe-0 pe-sm-10">
<h4 class="mb-2 light">
<TL>Your account is currently not linked to discord</TL>
</h4>
<TL>To use features like the discord bot, link your moonlight account with your discord account</TL><br/>
</div>
</div>
</div>
<div class="col">
<div class="card card-body">
<a class="btn btn-primary" href="/api/moonlight/oauth2/discord/start">
<TL>Link account</TL>
</a>
</div>
</div>
</div>
}
else
{
<div class="row">
<div class="col">
<div class="alert bg-success d-flex flex-column flex-sm-row p-5 mb-10">
<i class="fs-2hx text-light me-4 mb-5 mb-sm-0 bx bx-check bx-lg"></i>
<div class="d-flex flex-column text-light pe-0 pe-sm-10">
<h4 class="mb-2 light">
<TL>Your account is linked to a discord account</TL>
</h4>
<TL>You are able to use features like the discord bot of moonlight</TL>
</div>
</div>
</div>
<div class="col">
<div class="card card-body">
<WButton CssClasses="btn-danger"
Text="@(SmartTranslateService.Translate("Remove link"))"
WorkingText="@(SmartTranslateService.Translate("Working"))"
OnClick="RemoveLink">
</WButton>
</div>
</div>
</div>
}
@code
{
[CascadingParameter]
public User User { get; set; }
private async Task RemoveLink()
{
User.DiscordId = 0;
UserRepository.Update(User);
await InvokeAsync(StateHasChanged);
}
}

View File

@@ -19,7 +19,7 @@
@inject AlertService AlertService
@inject ToastService ToastService
<ProfileNavigation Index="1"/>
<ProfileNavigation Index="2"/>
<div class="card mb-5 mb-xl-10">
<LazyLoader Load="Load">

View File

@@ -11,7 +11,7 @@
@inject SubscriptionService SubscriptionService
@inject SmartTranslateService SmartTranslateService
<ProfileNavigation Index="2"/>
<ProfileNavigation Index="3"/>
<div class="card mb-3">
<div class="row g-0">