Spotify Auth working, strongly typed web config

This commit is contained in:
andy 2021-10-27 17:31:29 +01:00
parent d1563c3365
commit 222c738854
6 changed files with 191 additions and 7 deletions

View File

@ -30,7 +30,6 @@ namespace Selector.CLI
// CONFIG
services.Configure<RootOptions>(options =>
{
OptionsHelper.ConfigureOptions(options, context.Configuration);
});
var config = OptionsHelper.ConfigureOptions(context.Configuration);

View File

@ -38,12 +38,14 @@ namespace Selector.Web.Areas.Identity.Pages.Account.Manage
public string Username { get; set; }
}
private async Task LoadAsync(ApplicationUser user)
private Task LoadAsync(ApplicationUser user)
{
Input = new InputModel
{
Username = user.LastFmUsername,
};
return Task.CompletedTask;
}
public async Task<IActionResult> OnGetAsync()
@ -74,8 +76,8 @@ namespace Selector.Web.Areas.Identity.Pages.Account.Manage
if (Input.Username != user.LastFmUsername)
{
user.LastFmUsername = Input.Username;
_userManager.UpdateAsync(user);
user.LastFmUsername = Input.Username.Trim();
await _userManager.UpdateAsync(user);
StatusMessage = "Username changed";
return RedirectToPage();

View File

@ -12,17 +12,27 @@ using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using Selector.Model;
using SpotifyAPI.Web;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Logging;
namespace Selector.Web.Areas.Identity.Pages.Account.Manage
{
public partial class SpotifyModel : PageModel
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly ILogger<SpotifyModel> Logger;
private readonly RootOptions Config;
public SpotifyModel(
UserManager<ApplicationUser> userManager)
UserManager<ApplicationUser> userManager,
ILogger<SpotifyModel> logger,
IOptions<RootOptions> config
)
{
_userManager = userManager;
Logger = logger;
Config = config.Value;
}
[BindProperty]
@ -46,12 +56,58 @@ namespace Selector.Web.Areas.Identity.Pages.Account.Manage
public async Task<IActionResult> OnPostLinkAsync()
{
StatusMessage = "Spotify Linked";
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if(Config.ClientId is null)
{
Logger.LogError($"Cannot link user, no Spotify client ID");
StatusMessage = "Could not link Spotify, no app credentials";
return RedirectToPage();
}
if (Config.ClientSecret is null)
{
Logger.LogError($"Cannot link user, no Spotify client secret");
StatusMessage = "Could not link Spotify, no app credentials";
return RedirectToPage();
}
var loginRequest = new LoginRequest(
new Uri(Config.SpotifyCallback),
Config.ClientId,
LoginRequest.ResponseType.Code
) {
Scope = new[] {
Scopes.UserReadPlaybackState
}
};
return Redirect(loginRequest.ToUri().ToString());
}
public async Task<IActionResult> OnPostUnlinkAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
// TODO: stop users Spotify-linked resources (watchers)
user.SpotifyIsLinked = false;
user.SpotifyAccessToken = null;
user.SpotifyRefreshToken = null;
user.SpotifyTokenExpiry = 0;
user.SpotifyLastRefresh = default;
await _userManager.UpdateAsync(user);
StatusMessage = "Spotify Unlinked";
return RedirectToPage();
}

View File

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Logging;
using Selector.Model;
using SpotifyAPI.Web;
namespace Selector.Web.Controller
{
[ApiController]
[Route("api/[controller]/callback")]
public class SpotifyController : BaseAuthController
{
private readonly RootOptions Config;
private const string ManageSpotifyPath = "/Identity/Account/Manage/Spotify";
public SpotifyController(
ApplicationDbContext context,
IAuthorizationService auth,
UserManager<ApplicationUser> userManager,
ILogger<UsersController> logger,
IOptions<RootOptions> config
) : base(context, auth, userManager, logger)
{
Config = config.Value;
}
[HttpGet]
public async Task<RedirectResult> Callback(string code)
{
if (Config.ClientId is null)
{
Logger.LogError($"Cannot link user, no Spotify client ID");
TempData["StatusMessage"] = "Could not link Spotify, no app credentials";
return Redirect(ManageSpotifyPath);
}
if (Config.ClientSecret is null)
{
Logger.LogError($"Cannot link user, no Spotify client secret");
TempData["StatusMessage"] = "Could not link Spotify, no app credentials";
return Redirect(ManageSpotifyPath);
}
var user = await UserManager.GetUserAsync(User);
if (user == null)
{
throw new ArgumentNullException("No user returned");
}
// TODO: Authorise
var response = await new OAuthClient()
.RequestToken(
new AuthorizationCodeTokenRequest(
Config.ClientId,
Config.ClientSecret,
code,
new Uri(Config.SpotifyCallback)
)
);
user.SpotifyIsLinked = true;
user.SpotifyAccessToken = response.AccessToken;
user.SpotifyRefreshToken = response.RefreshToken;
user.SpotifyLastRefresh = response.CreatedAt;
user.SpotifyTokenExpiry = response.ExpiresIn;
await UserManager.UpdateAsync(user);
TempData["StatusMessage"] = "Spotify Linked";
return Redirect(ManageSpotifyPath);
}
}
}

39
Selector.Web/Options.cs Normal file
View File

@ -0,0 +1,39 @@
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
namespace Selector.Web
{
public static class OptionsHelper {
public static void ConfigureOptions(RootOptions options, IConfiguration config)
{
config.GetSection(RootOptions.Key).Bind(options);
}
public static RootOptions ConfigureOptions(IConfiguration config)
{
var options = config.GetSection(RootOptions.Key).Get<RootOptions>();
ConfigureOptions(options, config);
return options;
}
public static string FormatKeys(string[] args) => string.Join(":", args);
}
public class RootOptions
{
public const string Key = "Selector";
/// <summary>
/// Spotify client ID
/// </summary>
public string ClientId { get; set; }
/// <summary>
/// Spotify app secret
/// </summary>
public string ClientSecret { get; set; }
/// <summary>
/// Spotify callback for authentication
/// </summary>
public string SpotifyCallback { get; set; }
}
}

View File

@ -30,6 +30,11 @@ namespace Selector.Web
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<RootOptions>(options =>
{
OptionsHelper.ConfigureOptions(options, Configuration);
});
services.AddRazorPages().AddRazorRuntimeCompilation();
services.AddControllers();