diff --git a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs index 7cb52c8..43a2955 100644 --- a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs @@ -6,6 +6,7 @@ using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NodaTime; +using FediverseAuthService = Foxnouns.Backend.Services.Auth.FediverseAuthService; namespace Foxnouns.Backend.Controllers.Authentication; @@ -22,15 +23,9 @@ public class FediverseAuthController( [HttpGet] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task GetFediverseUrlAsync( - [FromQuery] string instance, - [FromQuery] bool forceRefresh = false - ) + public async Task GetFediverseUrlAsync([FromQuery] string instance) { - if (instance.Any(c => c is '@' or ':' or '/') || !instance.Contains('.')) - throw new ApiError.BadRequest("Not a valid domain.", "instance", instance); - - var url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh); + var url = await fediverseAuthService.GenerateAuthUrlAsync(instance); return Ok(new FediverseUrlResponse(url)); } @@ -39,11 +34,7 @@ public class FediverseAuthController( public async Task FediverseCallbackAsync([FromBody] CallbackRequest req) { var app = await fediverseAuthService.GetApplicationAsync(req.Instance); - var remoteUser = await fediverseAuthService.GetRemoteFediverseUserAsync( - app, - req.Code, - req.State - ); + var remoteUser = await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code); var user = await authService.AuthenticateUserAsync( AuthType.Fediverse, @@ -79,16 +70,12 @@ public class FediverseAuthController( ) { var ticketData = await keyCacheService.GetKeyAsync( - $"fediverse:{req.Ticket}", - delete: true + $"fediverse:{req.Ticket}" ); if (ticketData == null) throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); var app = await db.FediverseApplications.FindAsync(ticketData.ApplicationId); - if (app == null) - throw new FoxnounsError("Null application found for ticket"); - if ( await db.AuthMethods.AnyAsync(a => a.AuthType == AuthType.Fediverse @@ -118,7 +105,7 @@ public class FediverseAuthController( return Ok(await authService.GenerateUserTokenAsync(user)); } - public record CallbackRequest(string Instance, string Code, string State); + public record CallbackRequest(string Instance, string Code); private record FediverseUrlResponse(string Url); diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index ba9cf28..22d23d4 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -94,7 +94,6 @@ public class MembersController( Names = req.Names ?? [], Pronouns = req.Pronouns ?? [], Unlisted = req.Unlisted ?? false, - Sid = null!, }; db.Add(member); diff --git a/Foxnouns.Backend/Controllers/SidController.cs b/Foxnouns.Backend/Controllers/SidController.cs deleted file mode 100644 index b8f5948..0000000 --- a/Foxnouns.Backend/Controllers/SidController.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Foxnouns.Backend.Database; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace Foxnouns.Backend.Controllers; - -[Route("/sid")] -[SuppressMessage( - "Performance", - "CA1862:Use the \'StringComparison\' method overloads to perform case-insensitive string comparisons", - Justification = "Not usable with EFCore" -)] -public class SidController(Config config, DatabaseContext db) : ApiControllerBase -{ - [HttpGet("{**id}")] - public async Task ResolveSidAsync(string id, CancellationToken ct = default) => - id.Length switch - { - 5 => await ResolveUserSidAsync(id, ct), - 6 => await ResolveMemberSidAsync(id, ct), - _ => Redirect(config.BaseUrl), - }; - - private async Task ResolveUserSidAsync(string id, CancellationToken ct = default) - { - var username = await db - .Users.Where(u => u.Sid == id.ToLowerInvariant() && !u.Deleted) - .Select(u => u.Username) - .FirstOrDefaultAsync(ct); - if (username == null) - return Redirect(config.BaseUrl); - - return Redirect($"{config.BaseUrl}/@{username}"); - } - - private async Task ResolveMemberSidAsync( - string id, - CancellationToken ct = default - ) - { - var member = await db - .Members.Include(m => m.User) - .Where(m => m.Sid == id.ToLowerInvariant() && !m.User.Deleted) - .Select(m => new { m.Name, m.User.Username }) - .FirstOrDefaultAsync(ct); - if (member == null) - return Redirect(config.BaseUrl); - - return Redirect($"{config.BaseUrl}/@{member.Username}/{member.Name}"); - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20241123210306_RemoveFediverseApplicationTokens.cs b/Foxnouns.Backend/Database/Migrations/20241123210306_RemoveFediverseApplicationTokens.cs deleted file mode 100644 index fbc8d3d..0000000 --- a/Foxnouns.Backend/Database/Migrations/20241123210306_RemoveFediverseApplicationTokens.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20241123210306_RemoveFediverseApplicationTokens")] - public partial class RemoveFediverseApplicationTokens : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn(name: "access_token", table: "fediverse_applications"); - - migrationBuilder.DropColumn(name: "token_valid_until", table: "fediverse_applications"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "access_token", - table: "fediverse_applications", - type: "text", - nullable: true - ); - - migrationBuilder.AddColumn( - name: "token_valid_until", - table: "fediverse_applications", - type: "timestamp with time zone", - nullable: true - ); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index e1e05c2..97316ac 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -107,6 +107,10 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("bigint") .HasColumnName("id"); + b.Property("AccessToken") + .HasColumnType("text") + .HasColumnName("access_token"); + b.Property("ClientId") .IsRequired() .HasColumnType("text") @@ -126,6 +130,10 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("integer") .HasColumnName("instance_type"); + b.Property("TokenValidUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("token_valid_until"); + b.HasKey("Id") .HasName("pk_fediverse_applications"); diff --git a/Foxnouns.Backend/Database/Models/FediverseApplication.cs b/Foxnouns.Backend/Database/Models/FediverseApplication.cs index 882a377..fa7b6a6 100644 --- a/Foxnouns.Backend/Database/Models/FediverseApplication.cs +++ b/Foxnouns.Backend/Database/Models/FediverseApplication.cs @@ -8,6 +8,10 @@ public class FediverseApplication : BaseModel public required string ClientId { get; set; } public required string ClientSecret { get; set; } public required FediverseInstanceType InstanceType { get; set; } + + // These are for ensuring the application is still valid. + public string? AccessToken { get; set; } + public Instant? TokenValidUntil { get; set; } } public enum FediverseInstanceType diff --git a/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs b/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs similarity index 97% rename from Foxnouns.Backend/Extensions/ImageObjectExtensions.cs rename to Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs index 2126610..efa2d60 100644 --- a/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs +++ b/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs @@ -10,7 +10,7 @@ using SixLabors.ImageSharp.Processing.Processors.Transforms; namespace Foxnouns.Backend.Extensions; -public static class ImageObjectExtensions +public static class AvatarObjectExtensions { private static readonly string[] ValidContentTypes = ["image/png", "image/webp", "image/jpeg"]; @@ -39,7 +39,7 @@ public static class ImageObjectExtensions ) => await objectStorageService.RemoveObjectAsync(CreateFlagInvocable.Path(hash), ct); public static async Task<(string Hash, Stream Image)> ConvertBase64UriToImage( - string uri, + this string uri, int size, bool crop ) diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index dbc46d3..a9e7b74 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -20,7 +20,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - @@ -36,7 +35,6 @@ - diff --git a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs index e7ce0e3..cfe1ca0 100644 --- a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs +++ b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs @@ -26,8 +26,7 @@ public class CreateFlagInvocable( try { - var (hash, image) = await ImageObjectExtensions.ConvertBase64UriToImage( - Payload.ImageData, + var (hash, image) = await Payload.ImageData.ConvertBase64UriToImage( size: 256, crop: false ); diff --git a/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs index d97e1a7..91640cb 100644 --- a/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs +++ b/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs @@ -39,11 +39,7 @@ public class MemberAvatarUpdateInvocable( try { - var (hash, image) = await ImageObjectExtensions.ConvertBase64UriToImage( - newAvatar, - size: 512, - crop: true - ); + var (hash, image) = await newAvatar.ConvertBase64UriToImage(size: 512, crop: true); var prevHash = member.Avatar; await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp"); diff --git a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs index 8147424..31433f9 100644 --- a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs +++ b/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs @@ -39,11 +39,7 @@ public class UserAvatarUpdateInvocable( try { - var (hash, image) = await ImageObjectExtensions.ConvertBase64UriToImage( - newAvatar, - size: 512, - crop: true - ); + var (hash, image) = await newAvatar.ConvertBase64UriToImage(size: 512, crop: true); image.Seek(0, SeekOrigin.Begin); var prevHash = user.Avatar; diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index 17a56d9..f0c3e19 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -18,7 +18,7 @@ builder.AddSerilog(); builder .WebHost.UseSentry(opts => { - opts.Dsn = config.Logging.SentryUrl ?? ""; + opts.Dsn = config.Logging.SentryUrl; opts.TracesSampleRate = config.Logging.SentryTracesSampleRate; opts.MaxRequestBodySize = RequestSize.Small; }) diff --git a/Foxnouns.Backend/Services/Auth/AuthService.cs b/Foxnouns.Backend/Services/Auth/AuthService.cs index adbf5b1..9675f22 100644 --- a/Foxnouns.Backend/Services/Auth/AuthService.cs +++ b/Foxnouns.Backend/Services/Auth/AuthService.cs @@ -45,7 +45,6 @@ public class AuthService( }, }, LastActive = clock.GetCurrentInstant(), - Sid = null!, }; db.Add(user); @@ -89,7 +88,6 @@ public class AuthService( }, }, LastActive = clock.GetCurrentInstant(), - Sid = null!, }; db.Add(user); diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs index 97e411a..139830b 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs @@ -1,9 +1,8 @@ -using System.Diagnostics.CodeAnalysis; using System.Net; using System.Web; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Extensions; +using Duration = NodaTime.Duration; using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; namespace Foxnouns.Backend.Services.Auth; @@ -18,13 +17,16 @@ public partial class FediverseAuthService Snowflake? existingAppId = null ) { - var resp = await _client.PostAsJsonAsync( + var resp = await _client.PostAsync( $"https://{instance}/api/v1/apps", - new CreateMastodonApplicationRequest( - ClientName: $"pronouns.cc (+{_config.BaseUrl})", - RedirectUris: MastodonRedirectUri(instance), - Scopes: "read read:accounts", - Website: _config.BaseUrl + new FormUrlEncodedContent( + new Dictionary + { + { "client_name", $"pronouns.cc (+{_config.BaseUrl})" }, + { "redirect_uris", MastodonRedirectUri(instance) }, + { "scope", "read:accounts" }, + { "website", _config.BaseUrl }, + } ) ); resp.EnsureSuccessStatusCode(); @@ -35,6 +37,12 @@ public partial class FediverseAuthService $"Application created on Mastodon-compatible instance {instance} was null" ); + var token = await GetMastodonAppTokenAsync( + instance, + mastodonApp.ClientId, + mastodonApp.ClientSecret + ); + FediverseApplication app; if (existingAppId == null) @@ -46,6 +54,8 @@ public partial class FediverseAuthService ClientSecret = mastodonApp.ClientSecret, Domain = instance, InstanceType = FediverseInstanceType.MastodonApi, + AccessToken = token, + TokenValidUntil = _clock.GetCurrentInstant() + Duration.FromDays(60), }; _db.Add(app); @@ -59,6 +69,8 @@ public partial class FediverseAuthService app.ClientId = mastodonApp.ClientId; app.ClientSecret = mastodonApp.ClientSecret; app.InstanceType = FediverseInstanceType.MastodonApi; + app.AccessToken = null; + app.TokenValidUntil = null; } await _db.SaveChangesAsync(); @@ -66,14 +78,8 @@ public partial class FediverseAuthService return app; } - private async Task GetMastodonUserAsync( - FediverseApplication app, - string code, - string state - ) + private async Task GetMastodonUserAsync(FediverseApplication app, string code) { - await _keyCacheService.ValidateAuthStateAsync(state); - var tokenResp = await _client.PostAsync( MastodonTokenUri(app.Domain), new FormUrlEncodedContent( @@ -118,27 +124,109 @@ public partial class FediverseAuthService private record MastodonTokenResponse([property: J("access_token")] string AccessToken); - private async Task GenerateMastodonAuthUrlAsync( - FediverseApplication app, - bool forceRefresh - ) + // TODO: Mastodon's OAuth documentation doesn't specify a "state" parameter. that feels... wrong + // https://docs.joinmastodon.org/methods/oauth/ + private async Task GenerateMastodonAuthUrlAsync(FediverseApplication app) { - if (forceRefresh) + try { - _logger.Information( - "An app credentials refresh was requested for {ApplicationId}, creating a new application", - app.Id + await ValidateMastodonAppAsync(app); + } + catch (FoxnounsError.RemoteAuthError e) + { + _logger.Error( + e, + "Error validating app token for {AppId} on {Instance}", + app.Id, + app.Domain ); + app = await CreateMastodonApplicationAsync(app.Domain, existingAppId: app.Id); } - var state = HttpUtility.UrlEncode(await _keyCacheService.GenerateAuthStateAsync()); - return $"https://{app.Domain}/oauth/authorize?response_type=code" + $"&client_id={app.ClientId}" + $"&scope={HttpUtility.UrlEncode("read:accounts")}" - + $"&redirect_uri={HttpUtility.UrlEncode(MastodonRedirectUri(app.Domain))}" - + $"&state={state}"; + + $"&redirect_uri={HttpUtility.UrlEncode(MastodonRedirectUri(app.Domain))}"; + } + + private async Task ValidateMastodonAppAsync(FediverseApplication app) + { + // If we don't have an access token stored, or it's too old, get one + // When doing this we don't need to fetch the application info + if (app.AccessToken == null || app.TokenValidUntil < _clock.GetCurrentInstant()) + { + _logger.Debug( + "Application {AppId} on instance {Instance} has no valid token, fetching it", + app.Id, + app.Domain + ); + + app.AccessToken = await GetMastodonAppTokenAsync( + app.Domain, + app.ClientId, + app.ClientSecret + ); + app.TokenValidUntil = _clock.GetCurrentInstant() + Duration.FromDays(60); + + _db.Update(app); + await _db.SaveChangesAsync(); + return; + } + + _logger.Debug( + "Checking whether application {AppId} on instance {Instance} is still valid", + app.Id, + app.Domain + ); + + var req = new HttpRequestMessage(HttpMethod.Get, MastodonCurrentAppUri(app.Domain)); + req.Headers.Add("Authorization", $"Bearer {app.AccessToken}"); + + var resp = await _client.SendAsync(req); + if (!resp.IsSuccessStatusCode) + { + var error = await resp.Content.ReadAsStringAsync(); + throw new FoxnounsError.RemoteAuthError( + "Verifying app credentials returned an error", + error + ); + } + } + + private async Task GetMastodonAppTokenAsync( + string instance, + string clientId, + string clientSecret + ) + { + var resp = await _client.PostAsync( + MastodonTokenUri(instance), + new FormUrlEncodedContent( + new Dictionary + { + { "grant_type", "client_credentials" }, + { "client_id", clientId }, + { "client_secret", clientSecret }, + } + ) + ); + if (!resp.IsSuccessStatusCode) + { + var error = await resp.Content.ReadAsStringAsync(); + throw new FoxnounsError.RemoteAuthError( + "Requesting app token returned an error", + error + ); + } + + var token = (await resp.Content.ReadFromJsonAsync())?.AccessToken; + if (token == null) + { + throw new FoxnounsError($"Token response from instance {instance} was invalid"); + } + + return token; } private static string MastodonTokenUri(string instance) => $"https://{instance}/oauth/token"; @@ -146,17 +234,12 @@ public partial class FediverseAuthService private static string MastodonCurrentUserUri(string instance) => $"https://{instance}/api/v1/accounts/verify_credentials"; - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] + private static string MastodonCurrentAppUri(string instance) => + $"https://{instance}/api/v1/apps/verify_credentials"; + private record PartialMastodonApplication( [property: J("name")] string Name, [property: J("client_id")] string ClientId, [property: J("client_secret")] string ClientSecret ); - - private record CreateMastodonApplicationRequest( - [property: J("client_name")] string ClientName, - [property: J("redirect_uris")] string RedirectUris, - [property: J("scopes")] string Scopes, - [property: J("website")] string Website - ); } diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs index f78fbde..fc54017 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs @@ -1,6 +1,7 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Microsoft.EntityFrameworkCore; +using NodaTime; using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; namespace Foxnouns.Backend.Services.Auth; @@ -9,27 +10,26 @@ public partial class FediverseAuthService { private const string NodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/2.0"; - private readonly HttpClient _client; private readonly ILogger _logger; - private readonly Config _config; + private readonly HttpClient _client; private readonly DatabaseContext _db; - private readonly KeyCacheService _keyCacheService; + private readonly Config _config; private readonly ISnowflakeGenerator _snowflakeGenerator; + private readonly IClock _clock; public FediverseAuthService( ILogger logger, Config config, DatabaseContext db, - KeyCacheService keyCacheService, - ISnowflakeGenerator snowflakeGenerator + ISnowflakeGenerator snowflakeGenerator, + IClock clock ) { - _logger = logger.ForContext(); _config = config; _db = db; - _keyCacheService = keyCacheService; _snowflakeGenerator = snowflakeGenerator; - + _clock = clock; + _logger = logger.ForContext(); _client = new HttpClient(); _client.DefaultRequestHeaders.Remove("User-Agent"); _client.DefaultRequestHeaders.Remove("Accept"); @@ -37,10 +37,10 @@ public partial class FediverseAuthService _client.DefaultRequestHeaders.Add("Accept", "application/json"); } - public async Task GenerateAuthUrlAsync(string instance, bool forceRefresh) + public async Task GenerateAuthUrlAsync(string instance) { var app = await GetApplicationAsync(instance); - return await GenerateAuthUrlAsync(app, forceRefresh); + return await GenerateAuthUrlAsync(app); } // thank you, gargron and syuilo, for agreeing on a name for *once* in your lives, @@ -96,25 +96,21 @@ public partial class FediverseAuthService ); } - private async Task GenerateAuthUrlAsync(FediverseApplication app, bool forceRefresh) => + private async Task GenerateAuthUrlAsync(FediverseApplication app) => app.InstanceType switch { - FediverseInstanceType.MastodonApi => await GenerateMastodonAuthUrlAsync( - app, - forceRefresh - ), + FediverseInstanceType.MastodonApi => await GenerateMastodonAuthUrlAsync(app), FediverseInstanceType.MisskeyApi => throw new NotImplementedException(), _ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null), }; public async Task GetRemoteFediverseUserAsync( FediverseApplication app, - string code, - string state + string code ) => app.InstanceType switch { - FediverseInstanceType.MastodonApi => await GetMastodonUserAsync(app, code, state), + FediverseInstanceType.MastodonApi => await GetMastodonUserAsync(app, code), FediverseInstanceType.MisskeyApi => throw new NotImplementedException(), _ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null), }; diff --git a/Foxnouns.Backend/Services/MemberRendererService.cs b/Foxnouns.Backend/Services/MemberRendererService.cs index 7d7cac0..f8588c2 100644 --- a/Foxnouns.Backend/Services/MemberRendererService.cs +++ b/Foxnouns.Backend/Services/MemberRendererService.cs @@ -32,7 +32,7 @@ public class MemberRendererService(DatabaseContext db, Config config) member.Id, member.Sid, member.Name, - member.DisplayName ?? member.Name, + member.DisplayName, member.Bio, AvatarUrlFor(member), member.Links, @@ -60,7 +60,7 @@ public class MemberRendererService(DatabaseContext db, Config config) member.Id, member.Sid, member.Name, - member.DisplayName ?? member.Name, + member.DisplayName, member.Bio, AvatarUrlFor(member), member.Names, @@ -87,7 +87,7 @@ public class MemberRendererService(DatabaseContext db, Config config) Snowflake Id, string Sid, string Name, - string DisplayName, + string? DisplayName, string? Bio, string? AvatarUrl, IEnumerable Names, @@ -99,7 +99,7 @@ public class MemberRendererService(DatabaseContext db, Config config) Snowflake Id, string Sid, string Name, - string DisplayName, + string? DisplayName, string? Bio, string? AvatarUrl, string[] Links, diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index bb225ff..0969a47 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -162,7 +162,6 @@ public static partial class ValidationUtils } public const int MaxBioLength = 1024; - public const int MaxAvatarLength = 1_500_000; public static ValidationError? ValidateBio(string? bio) { @@ -184,10 +183,7 @@ public static partial class ValidationUtils return avatar?.Length switch { 0 => ValidationError.GenericValidationError("Avatar cannot be empty", null), - > MaxAvatarLength => ValidationError.GenericValidationError( - "Avatar is too large", - null - ), + > 1_500_000 => ValidationError.GenericValidationError("Avatar is too large", null), _ => null, }; } diff --git a/Foxnouns.Backend/packages.lock.json b/Foxnouns.Backend/packages.lock.json index 02ca7ca..90ba53c 100644 --- a/Foxnouns.Backend/packages.lock.json +++ b/Foxnouns.Backend/packages.lock.json @@ -96,19 +96,6 @@ "Mono.TextTemplating": "2.2.1" } }, - "Microsoft.Extensions.Caching.Memory": { - "type": "Direct", - "requested": "[9.0.0, )", - "resolved": "9.0.0", - "contentHash": "zbnPX/JQ0pETRSUG9fNPBvpIq42Aufvs15gGYyNIMhCun9yhmWihz0WgsI7bSDPjxWTKBf8oX/zv6v2uZ3W9OQ==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "9.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", - "Microsoft.Extensions.Logging.Abstractions": "9.0.0", - "Microsoft.Extensions.Options": "9.0.0", - "Microsoft.Extensions.Primitives": "9.0.0" - } - }, "Minio": { "type": "Direct", "requested": "[6.0.3, )", @@ -259,16 +246,6 @@ "Swashbuckle.AspNetCore.SwaggerUI": "6.6.2" } }, - "System.Text.Json": { - "type": "Direct", - "requested": "[9.0.0, )", - "resolved": "9.0.0", - "contentHash": "js7+qAu/9mQvnhA4EfGMZNEzXtJCDxgkgj8ohuxq/Qxv+R56G+ljefhiJHOxTNiw54q8vmABCWUwkMulNdlZ4A==", - "dependencies": { - "System.IO.Pipelines": "9.0.0", - "System.Text.Encodings.Web": "9.0.0" - } - }, "System.Text.RegularExpressions": { "type": "Direct", "requested": "[4.3.1, )", @@ -435,10 +412,22 @@ }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "FPWZAa9c0H4dvOj351iR1jkUIs4u9ykL4Bm592yhjDyO5lCoWd+TMAHx2EMbarzUvCvgjWjJIoC6//Q9kH6YhA==", + "resolved": "8.0.0", + "contentHash": "3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==", "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.0" + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7pqivmrZDzo1ADPkRwjy+8jtRKWRCPag9qPI+p7sgu7Q4QreWhcvbiWXsbhP+yY8XSiDvZpu2/LWdBv7PnmOpQ==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration": { @@ -476,8 +465,8 @@ }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg==" + "resolved": "8.0.1", + "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", @@ -553,11 +542,10 @@ }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==", + "resolved": "8.0.0", + "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", - "System.Diagnostics.DiagnosticSource": "9.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Logging.Configuration": { @@ -582,11 +570,11 @@ }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==", + "resolved": "8.0.0", + "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", - "Microsoft.Extensions.Primitives": "9.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { @@ -603,8 +591,8 @@ }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg==" + "resolved": "8.0.0", + "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" }, "Microsoft.NETCore.Platforms": { "type": "Transitive", @@ -995,8 +983,8 @@ }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "ddppcFpnbohLWdYKr/ZeLZHmmI+DXFgZ3Snq+/E7SwcdW4UnvxmaugkwGywvGVWkHPGCSZjCP+MLzu23AL5SDw==" + "resolved": "8.0.0", + "contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ==" }, "System.Diagnostics.Tracing": { "type": "Transitive", @@ -1084,8 +1072,8 @@ }, "System.IO.Pipelines": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "eA3cinogwaNB4jdjQHOP3Z3EuyiDII7MT35jgtnsA4vkn0LUrrSHsU0nzHTzFzmaFYeKV7MYyMxOocFzsBHpTw==" + "resolved": "6.0.3", + "contentHash": "ryTgF+iFkpGZY1vRQhfCzX0xTdlV3pyaTTqRu2ETbEv+HlV7O6y7hyQURnghNIXvctl5DuZ//Dpks6HdL/Txgw==" }, "System.Linq": { "type": "Transitive", @@ -1496,8 +1484,16 @@ }, "System.Text.Encodings.Web": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "e2hMgAErLbKyUUwt18qSBf9T5Y+SFAL3ZedM8fLupkVj8Rj2PZ9oxQ37XX2LF8fTO1wNIxvKpihD7Of7D/NxZw==" + "resolved": "8.0.0", + "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "8.0.4", + "contentHash": "bAkhgDJ88XTsqczoxEMliSrpijKZHhbJQldhAmObj/RbrN3sU5dcokuXmWJWsdQAhiMJ9bTayWsL1C9fbbCRhw==", + "dependencies": { + "System.Text.Encodings.Web": "8.0.0" + } }, "System.Threading": { "type": "Transitive",