Merge pull request #132 from Moonlight-Panel/AddDiscordLinkSettings
Implemented new discord linking system
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
using Logging.Net;
|
using Logging.Net;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Moonlight.App.Services;
|
using Moonlight.App.Services;
|
||||||
|
using Moonlight.App.Services.Sessions;
|
||||||
|
|
||||||
namespace Moonlight.App.Http.Controllers.Api.Moonlight;
|
namespace Moonlight.App.Http.Controllers.Api.Moonlight;
|
||||||
|
|
||||||
@@ -11,12 +12,41 @@ public class OAuth2Controller : Controller
|
|||||||
private readonly UserService UserService;
|
private readonly UserService UserService;
|
||||||
private readonly OAuth2Service OAuth2Service;
|
private readonly OAuth2Service OAuth2Service;
|
||||||
private readonly DateTimeService DateTimeService;
|
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;
|
UserService = userService;
|
||||||
OAuth2Service = oAuth2Service;
|
OAuth2Service = oAuth2Service;
|
||||||
DateTimeService = dateTimeService;
|
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}")]
|
[HttpGet("{id}")]
|
||||||
@@ -24,6 +54,18 @@ public class OAuth2Controller : Controller
|
|||||||
{
|
{
|
||||||
try
|
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);
|
var user = await OAuth2Service.HandleCode(id, code);
|
||||||
|
|
||||||
Response.Cookies.Append("token", await UserService.GenerateToken(user), new()
|
Response.Cookies.Append("token", await UserService.GenerateToken(user), new()
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ public abstract class OAuth2Provider
|
|||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
public IServiceScopeFactory ServiceScopeFactory { get; set; }
|
public IServiceScopeFactory ServiceScopeFactory { get; set; }
|
||||||
public string DisplayName { get; set; }
|
public string DisplayName { get; set; }
|
||||||
|
public bool CanBeLinked { get; set; } = false;
|
||||||
|
|
||||||
public abstract Task<string> GetUrl();
|
public abstract Task<string> GetUrl();
|
||||||
public abstract Task<User> HandleCode(string code);
|
public abstract Task<User> HandleCode(string code);
|
||||||
|
public abstract Task LinkToUser(User user, string code);
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,11 @@ namespace Moonlight.App.OAuth2.Providers;
|
|||||||
|
|
||||||
public class DiscordOAuth2Provider : OAuth2Provider
|
public class DiscordOAuth2Provider : OAuth2Provider
|
||||||
{
|
{
|
||||||
|
public DiscordOAuth2Provider()
|
||||||
|
{
|
||||||
|
CanBeLinked = true;
|
||||||
|
}
|
||||||
|
|
||||||
public override Task<string> GetUrl()
|
public override Task<string> GetUrl()
|
||||||
{
|
{
|
||||||
string url = $"https://discord.com/api/oauth2/authorize?client_id={Config.ClientId}" +
|
string url = $"https://discord.com/api/oauth2/authorize?client_id={Config.ClientId}" +
|
||||||
@@ -119,4 +124,74 @@ public class DiscordOAuth2Provider : OAuth2Provider
|
|||||||
return user;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,6 @@ using Moonlight.App.ApiClients.Google.Requests;
|
|||||||
using Moonlight.App.Database.Entities;
|
using Moonlight.App.Database.Entities;
|
||||||
using Moonlight.App.Exceptions;
|
using Moonlight.App.Exceptions;
|
||||||
using Moonlight.App.Helpers;
|
using Moonlight.App.Helpers;
|
||||||
using Moonlight.App.Models.Misc;
|
|
||||||
using Moonlight.App.Repositories;
|
using Moonlight.App.Repositories;
|
||||||
using Moonlight.App.Services;
|
using Moonlight.App.Services;
|
||||||
using RestSharp;
|
using RestSharp;
|
||||||
@@ -13,6 +12,11 @@ namespace Moonlight.App.OAuth2.Providers;
|
|||||||
|
|
||||||
public class GoogleOAuth2Provider : OAuth2Provider
|
public class GoogleOAuth2Provider : OAuth2Provider
|
||||||
{
|
{
|
||||||
|
public GoogleOAuth2Provider()
|
||||||
|
{
|
||||||
|
CanBeLinked = false;
|
||||||
|
}
|
||||||
|
|
||||||
public override Task<string> GetUrl()
|
public override Task<string> GetUrl()
|
||||||
{
|
{
|
||||||
var endpoint = Url + "/api/moonlight/oauth2/google";
|
var endpoint = Url + "/api/moonlight/oauth2/google";
|
||||||
@@ -127,4 +131,9 @@ public class GoogleOAuth2Provider : OAuth2Provider
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override Task LinkToUser(User user, string code)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -80,6 +80,26 @@ public class OAuth2Service
|
|||||||
return await provider.HandleCode(code);
|
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()
|
private string GetAppUrl()
|
||||||
{
|
{
|
||||||
if (EnableOverrideUrl)
|
if (EnableOverrideUrl)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<div class="d-flex align-items-center mb-2">
|
<div class="d-flex align-items-center mb-2">
|
||||||
<a class="text-gray-900 fs-2 fw-bold me-1">@(User.FirstName) @(User.LastName)</a>
|
<a class="text-gray-900 fs-2 fw-bold me-1">@(User.FirstName) @(User.LastName)</a>
|
||||||
|
|
||||||
@if (User.Status == UserStatus.Verified)
|
@if (User.Status == UserStatus.Verified)
|
||||||
{
|
{
|
||||||
<i class="text-success bx bx-md bxs-badge-check"></i>
|
<i class="text-success bx bx-md bxs-badge-check"></i>
|
||||||
@@ -31,12 +31,17 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item mt-2">
|
<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>
|
<TL>Security</TL>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item mt-2">
|
<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>
|
<TL>Subscriptions</TL>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
77
Moonlight/Shared/Views/Profile/Discord.razor
Normal file
77
Moonlight/Shared/Views/Profile/Discord.razor
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
@inject AlertService AlertService
|
@inject AlertService AlertService
|
||||||
@inject ToastService ToastService
|
@inject ToastService ToastService
|
||||||
|
|
||||||
<ProfileNavigation Index="1"/>
|
<ProfileNavigation Index="2"/>
|
||||||
|
|
||||||
<div class="card mb-5 mb-xl-10">
|
<div class="card mb-5 mb-xl-10">
|
||||||
<LazyLoader Load="Load">
|
<LazyLoader Load="Load">
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
@inject SubscriptionService SubscriptionService
|
@inject SubscriptionService SubscriptionService
|
||||||
@inject SmartTranslateService SmartTranslateService
|
@inject SmartTranslateService SmartTranslateService
|
||||||
|
|
||||||
<ProfileNavigation Index="2"/>
|
<ProfileNavigation Index="3"/>
|
||||||
|
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
|
|||||||
Reference in New Issue
Block a user