diff --git a/Selector.CLI/Program.cs b/Selector.CLI/Program.cs index 2529392..6ec9540 100644 --- a/Selector.CLI/Program.cs +++ b/Selector.CLI/Program.cs @@ -30,7 +30,6 @@ namespace Selector.CLI // CONFIG services.Configure(options => { - OptionsHelper.ConfigureOptions(options, context.Configuration); }); var config = OptionsHelper.ConfigureOptions(context.Configuration); diff --git a/Selector.Web/Areas/Identity/Pages/Account/Manage/Lastfm.cshtml.cs b/Selector.Web/Areas/Identity/Pages/Account/Manage/Lastfm.cshtml.cs index e9d84f5..6f7fe0d 100644 --- a/Selector.Web/Areas/Identity/Pages/Account/Manage/Lastfm.cshtml.cs +++ b/Selector.Web/Areas/Identity/Pages/Account/Manage/Lastfm.cshtml.cs @@ -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 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(); diff --git a/Selector.Web/Areas/Identity/Pages/Account/Manage/Spotify.cshtml.cs b/Selector.Web/Areas/Identity/Pages/Account/Manage/Spotify.cshtml.cs index 0495eac..320fb1e 100644 --- a/Selector.Web/Areas/Identity/Pages/Account/Manage/Spotify.cshtml.cs +++ b/Selector.Web/Areas/Identity/Pages/Account/Manage/Spotify.cshtml.cs @@ -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 _userManager; + private readonly ILogger Logger; + private readonly RootOptions Config; public SpotifyModel( - UserManager userManager) + UserManager userManager, + ILogger logger, + IOptions config + ) { _userManager = userManager; + Logger = logger; + Config = config.Value; } [BindProperty] @@ -46,12 +56,58 @@ namespace Selector.Web.Areas.Identity.Pages.Account.Manage public async Task OnPostLinkAsync() { - StatusMessage = "Spotify Linked"; - return RedirectToPage(); + 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 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(); } diff --git a/Selector.Web/Controller/SpotifyController.cs b/Selector.Web/Controller/SpotifyController.cs new file mode 100644 index 0000000..40e4f73 --- /dev/null +++ b/Selector.Web/Controller/SpotifyController.cs @@ -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 userManager, + ILogger logger, + IOptions config + ) : base(context, auth, userManager, logger) + { + Config = config.Value; + } + + [HttpGet] + public async Task 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); + } + } +} \ No newline at end of file diff --git a/Selector.Web/Options.cs b/Selector.Web/Options.cs new file mode 100644 index 0000000..c794f68 --- /dev/null +++ b/Selector.Web/Options.cs @@ -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(); + ConfigureOptions(options, config); + return options; + } + + public static string FormatKeys(string[] args) => string.Join(":", args); + } + + public class RootOptions + { + public const string Key = "Selector"; + + /// + /// Spotify client ID + /// + public string ClientId { get; set; } + /// + /// Spotify app secret + /// + public string ClientSecret { get; set; } + /// + /// Spotify callback for authentication + /// + public string SpotifyCallback { get; set; } + } +} diff --git a/Selector.Web/Startup.cs b/Selector.Web/Startup.cs index 2249e6e..13e8ea9 100644 --- a/Selector.Web/Startup.cs +++ b/Selector.Web/Startup.cs @@ -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(options => + { + OptionsHelper.ConfigureOptions(options, Configuration); + }); + services.AddRazorPages().AddRazorRuntimeCompilation(); services.AddControllers();