diff --git a/src/Yavsc.Api/Controllers/accounting/AccountController.cs b/src/Yavsc.Api/Controllers/accounting/AccountController.cs index aff71013..d8ab9d33 100644 --- a/src/Yavsc.Api/Controllers/accounting/AccountController.cs +++ b/src/Yavsc.Api/Controllers/accounting/AccountController.cs @@ -10,7 +10,7 @@ using System.Diagnostics; namespace Yavsc.WebApi.Controllers { - [Route("~/api/account")] + [Route( Constants.APIPrefix + "/account")] [Authorize("ApiScope")] public class ApiAccountController : Controller { @@ -61,12 +61,12 @@ namespace Yavsc.WebApi.Controllers return Ok(new { host = Request.ForwardedFor() }); } - + /// /// Updates the avatar /// /// - [HttpPost("~/api/set-avatar")] + [HttpPost("set-avatar")] public async Task SetAvatar() { var user = await GetUserData(User.GetUserId()); diff --git a/src/Yavsc.Api/Program.cs b/src/Yavsc.Api/Program.cs index 8db29e53..5269eef1 100644 --- a/src/Yavsc.Api/Program.cs +++ b/src/Yavsc.Api/Program.cs @@ -20,6 +20,7 @@ using Yavsc.Helpers; using Yavsc.Interface; using Yavsc.Interfaces; using Yavsc.Models; +using Yavsc; using Yavsc.Server.Helpers; using Yavsc.Services; @@ -32,6 +33,7 @@ internal class Program var builder = WebApplication.CreateBuilder(args); builder.AddConfiguration("api"); + Config.SiteSetup = builder.Configuration.GetSection("Site").Get() ?? new SiteSettings(); var services = builder.Services; diff --git a/src/Yavsc.Api/appsettings-api.json b/src/Yavsc.Api/appsettings-api.json index ac626c0d..d8b295de 100644 --- a/src/Yavsc.Api/appsettings-api.json +++ b/src/Yavsc.Api/appsettings-api.json @@ -2,7 +2,8 @@ "Site": { "Authority": "https://localhost:5001", "CorsAllowedOrigins": [ - "https://localhost:5003" + "https://localhost:5003", + "https://yavsc.pschneider.fr" ] }, "Logging": { diff --git a/src/Yavsc.Org/Controllers/Accounting/AccountController.cs b/src/Yavsc.Org/Controllers/Accounting/AccountController.cs index 05cb55b5..77e0e32a 100644 --- a/src/Yavsc.Org/Controllers/Accounting/AccountController.cs +++ b/src/Yavsc.Org/Controllers/Accounting/AccountController.cs @@ -94,107 +94,6 @@ IHtmlLocalizerFactory htmlLocalizerFactory, } - public async Task SignIn(SignInModel model, [FromForm] string button) - { - if (Request.Method == "POST") // "hGbkk9B94NAae#aG" - - { - if (model.Provider == null || model.Provider == "LOCAL") - { - if (ModelState.IsValid) - { - var user = await _userManager.FindByNameAsync(model.UserName); - var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); - if (user != null) - { - - - var signin = await _signInManager.CheckPasswordSignInAsync(user, model.Password, true); - - // validate username/password against in-memory store - if (signin.Succeeded) - { - await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName, clientId: context?.Client.ClientId)); - - // only set explicit expiration here if user chooses "remember me". - // otherwise we rely upon expiration configured in cookie middleware. - await HttpContext.SignInAsync(user, _roleManager, model.RememberMe, _dbContext); - var authResult = await HttpContext.AuthenticateAsync(); - if (!authResult.Succeeded) - { - return this.Unauthorized(); - } - String bearer = await HttpContext.GetTokenAsync("Bearer", "Bearer"); - HttpContext.Response.Cookies.Append("Bearer", bearer); - - if (context != null) - { - if (context.IsNativeClient()) - { - // The client is native, so this change in how to - // return the response is for better UX for the end user. - return this.LoadingPage("Redirect", model.ReturnUrl); - } - - // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null - return Redirect(model.ReturnUrl); - } - - // request for a local page - if (Url.IsLocalUrl(model.ReturnUrl)) - { - return Redirect(model.ReturnUrl); - } - else if (string.IsNullOrEmpty(model.ReturnUrl)) - { - return Redirect("~/"); - } - else - { - // user might have clicked on a malicious link - should be logged - throw new Exception("invalid return URL"); - } - } - } - - await _events.RaiseAsync(new UserLoginFailureEvent(model.UserName, "invalid credentials", clientId: context?.Client.ClientId)); - ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage); - } - } - else - { - - // Note: the "provider" parameter corresponds to the external - // authentication provider choosen by the user agent. - if (string.IsNullOrEmpty(model.Provider)) - { - _logger.LogWarning("Provider not specified"); - return BadRequest(); - } - - // Instruct the middleware corresponding to the requested external identity - // provider to redirect the user agent to its own authorization endpoint. - // Note: the authenticationScheme parameter must match the value configured in Startup.cs - - // Note: the "returnUrl" parameter corresponds to the endpoint the user agent - // will be redirected to after a successful authentication and not - // the redirect_uri of the requesting client application. - if (string.IsNullOrEmpty(model.ReturnUrl)) - { - _logger.LogWarning("ReturnUrl not specified"); - return BadRequest(); - } - // Note: this still is not the redirect uri given to the third party provider, at building the challenge. - var redirectUrl = Url.Action("ExternalLoginCallback", "Account", new { model.ReturnUrl }, protocol: "https", host: Config.Authority); - var properties = _signInManager.ConfigureExternalAuthenticationProperties(model.Provider, redirectUrl); - // var properties = new AuthenticationProperties{RedirectUri=ReturnUrl}; - return new ChallengeResult(model.Provider, properties); - - } - } - return View(model); - } - /// /// Entry point into the login workflow /// diff --git a/src/Yavsc.Org/Controllers/Accounting/ManageController.cs b/src/Yavsc.Org/Controllers/Accounting/ManageController.cs index 6326726d..e09bbb5a 100644 --- a/src/Yavsc.Org/Controllers/Accounting/ManageController.cs +++ b/src/Yavsc.Org/Controllers/Accounting/ManageController.cs @@ -16,6 +16,7 @@ using Yavsc.Services; using Yavsc.ViewModels.Manage; using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.AspNetCore.Authorization; +using IdentityServer8; using Yavsc.Server.Helpers; namespace Yavsc.Controllers @@ -524,8 +525,45 @@ namespace Yavsc.Controllers } [HttpGet] - public IActionResult SetAvatar() + public async Task SetAvatar( + [FromServices] IdentityServerTools identityServerTools) { + var currentUser = await GetCurrentUserAsync(); + if (currentUser == null) + { + return Challenge(); + } + + var claims = new List + { + new("sub", currentUser.Id), + new("name", currentUser.UserName ?? currentUser.Email ?? currentUser.Id), + new("scope", "api"), + new("aud", "api") + }; + + // Short-lived token limited to avatar upload from this page. + string avatarAccessToken = string.Empty; + try + { + avatarAccessToken = await identityServerTools.IssueClientJwtAsync( + "postit", + 300, + new[] { "api" }, + new[] { "api" }, + claims); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "IssueClientJwtAsync failed for SetAvatar, trying direct IssueJwtAsync fallback."); + } + + if (string.IsNullOrWhiteSpace(avatarAccessToken)) + { + avatarAccessToken = await identityServerTools.IssueJwtAsync(300, claims); + } + + ViewData["AvatarAccessToken"] = avatarAccessToken; return View(); } diff --git a/src/Yavsc.Org/Views/Manage/SetAvatar.cshtml b/src/Yavsc.Org/Views/Manage/SetAvatar.cshtml index b9292434..a4f842a3 100644 --- a/src/Yavsc.Org/Views/Manage/SetAvatar.cshtml +++ b/src/Yavsc.Org/Views/Manage/SetAvatar.cshtml @@ -1,9 +1,16 @@ @model PerformerProfile @{ ViewBag.Title = "Edit your avatar"; } @{ + var apiBaseUrl = string.IsNullOrWhiteSpace(SiteSettings.Value.ApiUrl) + ? string.Empty + : SiteSettings.Value.ApiUrl.TrimEnd('/'); + var setAvatarUrl = string.IsNullOrEmpty(apiBaseUrl) + ? "/api/v1/account/set-avatar" + : $"{apiBaseUrl}/api/v1/account/set-avatar"; var previewAvatarSrc = string.IsNullOrWhiteSpace(User?.Identity?.Name) ? Yavsc.Constants.DefaultAvatar : $"{Yavsc.Constants.AvatarsPath}/{User.Identity.Name}.png"; + var accessToken = ViewData["AvatarAccessToken"] as string ?? string.Empty; } @section header{ @@ -11,7 +18,12 @@ } @section scripts{ } -
+
diff --git a/src/Yavsc.Org/appsettings-org.json b/src/Yavsc.Org/appsettings-org.json index 931b6f17..84a7ffa0 100644 --- a/src/Yavsc.Org/appsettings-org.json +++ b/src/Yavsc.Org/appsettings-org.json @@ -18,6 +18,7 @@ "Authority": "https://[Your domaine name]", "Audience": ["blogs"], "ExternalUrl": "https://[Your domaine name]", + "ApiUrl": "https://[Your API domaine name]", "CorsAllowedOrigins": [ "*" ], diff --git a/src/Yavsc.Server/Helpers/FileSystemHelpers.cs b/src/Yavsc.Server/Helpers/FileSystemHelpers.cs index 91202727..7d9d86ed 100644 --- a/src/Yavsc.Server/Helpers/FileSystemHelpers.cs +++ b/src/Yavsc.Server/Helpers/FileSystemHelpers.cs @@ -179,7 +179,7 @@ namespace Yavsc.Server.Helpers /// /// /// - /// + /// /// /// /// @@ -244,8 +244,24 @@ namespace Yavsc.Server.Helpers public static FileReceivedInfo ReceiveAvatar(this ApplicationUser user, IFormFile formFile) { + if (user == null) throw new ArgumentNullException(nameof(user)); + if (formFile == null) throw new ArgumentNullException(nameof(formFile)); + + var avatarsRequestPath = Config.AvatarsOptions?.RequestPath.ToUriComponent(); + if (string.IsNullOrWhiteSpace(avatarsRequestPath)) + { + avatarsRequestPath = Constants.AvatarsPath; + } + + var avatarsDirectory = Config.SiteSetup?.Avatars; + if (string.IsNullOrWhiteSpace(avatarsDirectory)) + { + avatarsDirectory = "avatars"; + } + Directory.CreateDirectory(avatarsDirectory); + var item = new FileReceivedInfo - (Config.AvatarsOptions.RequestPath.ToUriComponent(), + (avatarsRequestPath, user.UserName + ".png"); using (var org = formFile.OpenReadStream()) @@ -256,15 +272,15 @@ namespace Yavsc.Server.Helpers using var image = new MagickImage(org); image.Resize(size); - image.Write(Path.Combine(Config.SiteSetup.Avatars, item.FileName)); + image.Write(Path.Combine(avatarsDirectory, item.FileName)); size.X = 64; size.Y = 64; image.Resize(size); - image.Write(Path.Combine(Config.SiteSetup.Avatars, user.UserName + ".s.png")); + image.Write(Path.Combine(avatarsDirectory, user.UserName + ".s.png")); size.X = 32; size.Y = 32; image.Resize(size); - image.Write(Path.Combine(Config.SiteSetup.Avatars, user.UserName + ".xs.png")); + image.Write(Path.Combine(avatarsDirectory, user.UserName + ".xs.png")); } diff --git a/src/Yavsc.Server/Helpers/ServiceExtensions.cs b/src/Yavsc.Server/Helpers/ServiceExtensions.cs index 2c421025..06e877be 100644 --- a/src/Yavsc.Server/Helpers/ServiceExtensions.cs +++ b/src/Yavsc.Server/Helpers/ServiceExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; @@ -133,6 +134,23 @@ public static class ServiceExtensions (_, _, _, _) => true }; } + + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + // Fallback for clients that cannot reliably attach Authorization header + // on multipart uploads. Restrict query-token support to this endpoint only. + if (string.IsNullOrEmpty(context.Token) + && context.Request.Path.Value?.Contains("/api/v1/account/set-avatar", StringComparison.OrdinalIgnoreCase) == true + && context.Request.Query.TryGetValue("access_token", out var tokenValues)) + { + context.Token = tokenValues.ToString(); + } + + return Task.CompletedTask; + } + }; }); return result; diff --git a/src/Yavsc.Server/Settings/SiteSettings.cs b/src/Yavsc.Server/Settings/SiteSettings.cs index 9267370e..ef74781d 100644 --- a/src/Yavsc.Server/Settings/SiteSettings.cs +++ b/src/Yavsc.Server/Settings/SiteSettings.cs @@ -5,7 +5,7 @@ namespace Yavsc public class SiteSettings { public string Title { get; set; } = "Yavsc"; - + public string Slogan { get; set; } = ""; public string Banner { get; set; } = ""; @@ -27,6 +27,10 @@ namespace Yavsc /// public string ExternalUrl { get; set; } = "http://lua.pschneider.fr"; /// + /// Base URL of the API fronting this site. + /// + public string ApiUrl { get; set; } = ""; + /// /// Must be a fqdn. /// ///