diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index 1587f87..a3854a6 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -36,7 +36,6 @@ public class EmailAuthController( DatabaseContext db, AuthService authService, MailService mailService, - EmailRateLimiter rateLimiter, KeyCacheService keyCacheService, UserRendererService userRenderer, IClock clock, @@ -69,9 +68,6 @@ public class EmailAuthController( return NoContent(); } - if (IsRateLimited()) - return NoContent(); - mailService.QueueAccountCreationEmail(req.Email, state); return NoContent(); } @@ -225,9 +221,6 @@ public class EmailAuthController( return NoContent(); } - if (IsRateLimited()) - return NoContent(); - mailService.QueueAddEmailAddressEmail(req.Email, state, CurrentUser.Username); return NoContent(); } @@ -281,34 +274,4 @@ public class EmailAuthController( if (!config.EmailAuth.Enabled) throw new ApiError.BadRequest("Email authentication is not enabled on this instance."); } - - /// - /// Checks whether the context's IP address is rate limited from dispatching emails. - /// - private bool IsRateLimited() - { - if (HttpContext.Connection.RemoteIpAddress == null) - { - _logger.Information( - "No remote IP address in HTTP context for email-related request, ignoring as we can't rate limit it" - ); - return true; - } - - if ( - !rateLimiter.IsLimited( - HttpContext.Connection.RemoteIpAddress.ToString(), - out Duration retryAfter - ) - ) - { - return false; - } - - _logger.Information( - "IP address cannot send email until {RetryAfter}, ignoring", - retryAfter - ); - return true; - } } diff --git a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs index 3dcc817..d95c622 100644 --- a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs @@ -161,13 +161,20 @@ public class FediverseAuthController( [FromBody] FediverseCallbackRequest req ) { + await remoteAuthService.ValidateAddAccountStateAsync( + req.State, + CurrentUser!.Id, + AuthType.Fediverse, + req.Instance + ); + FediverseApplication app = await fediverseAuthService.GetApplicationAsync(req.Instance); FediverseAuthService.FediverseUser remoteUser = await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code); try { AuthMethod authMethod = await authService.AddAuthMethodAsync( - CurrentUser!.Id, + CurrentUser.Id, AuthType.Fediverse, remoteUser.Id, remoteUser.Username, diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs index c68fb96..0c35afd 100644 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -34,8 +34,7 @@ public class FlagsController( ) : ApiControllerBase { [HttpGet] - [Limit(UsableBySuspendedUsers = true)] - [Authorize("user.read_flags")] + [Authorize("identify")] [ProducesResponseType>(statusCode: StatusCodes.Status200OK)] public async Task GetFlagsAsync(CancellationToken ct = default) { @@ -51,7 +50,7 @@ public class FlagsController( public const int MaxFlagCount = 500; [HttpPost] - [Authorize("user.update_flags")] + [Authorize("user.update")] [ProducesResponseType(statusCode: StatusCodes.Status202Accepted)] public async Task CreateFlagAsync([FromBody] CreateFlagRequest req) { @@ -80,7 +79,7 @@ public class FlagsController( } [HttpPatch("{id}")] - [Authorize("user.create_flags")] + [Authorize("user.update")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task UpdateFlagAsync(Snowflake id, [FromBody] UpdateFlagRequest req) { @@ -105,7 +104,7 @@ public class FlagsController( } [HttpDelete("{id}")] - [Authorize("user.update_flags")] + [Authorize("user.update")] public async Task DeleteFlagAsync(Snowflake id) { PrideFlag? flag = await db.PrideFlags.FirstOrDefaultAsync(f => diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 9b94b30..534c51f 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -44,7 +44,6 @@ public class MembersController( [HttpGet] [ProducesResponseType>(StatusCodes.Status200OK)] - [Limit(UsableBySuspendedUsers = true)] public async Task GetMembersAsync(string userRef, CancellationToken ct = default) { User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); @@ -53,7 +52,6 @@ public class MembersController( [HttpGet("{memberRef}")] [ProducesResponseType(StatusCodes.Status200OK)] - [Limit(UsableBySuspendedUsers = true)] public async Task GetMemberAsync( string userRef, string memberRef, diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index d567bdb..4a3be72 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -42,7 +42,6 @@ public class UsersController( [HttpGet("{userRef}")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - [Limit(UsableBySuspendedUsers = true)] public async Task GetUserAsync(string userRef, CancellationToken ct = default) { User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index ddf7853..a5dede8 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -84,9 +84,6 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options) modelBuilder.Entity().HasIndex(m => new { m.UserId, m.Name }).IsUnique(); modelBuilder.Entity().HasIndex(m => m.Sid).IsUnique(); modelBuilder.Entity().HasIndex(k => k.Key).IsUnique(); - modelBuilder.Entity().HasIndex(d => d.Filename).IsUnique(); - - // Two indexes on auth_methods, one for fediverse auth and one for all other types. modelBuilder .Entity() .HasIndex(m => new @@ -97,6 +94,7 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options) }) .HasFilter("fediverse_application_id IS NOT NULL") .IsUnique(); + modelBuilder.Entity().HasIndex(d => d.Filename).IsUnique(); modelBuilder .Entity() diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index d804dfe..790b9df 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -31,7 +31,6 @@ public static class DatabaseQueryExtensions { if (userRef == "@me") { - // Not filtering deleted users, as a suspended user should still be able to look at their own profile. return token != null ? await context.Users.FirstAsync(u => u.Id == token.UserId, ct) : throw new ApiError.Unauthorized( @@ -44,14 +43,14 @@ public static class DatabaseQueryExtensions if (Snowflake.TryParse(userRef, out Snowflake? snowflake)) { user = await context - .Users.Where(u => !u.Deleted || (token != null && token.UserId == u.Id)) + .Users.Where(u => !u.Deleted) .FirstOrDefaultAsync(u => u.Id == snowflake, ct); if (user != null) return user; } user = await context - .Users.Where(u => !u.Deleted || (token != null && token.UserId == u.Id)) + .Users.Where(u => !u.Deleted) .FirstOrDefaultAsync(u => u.Username == userRef, ct); if (user != null) return user; @@ -99,14 +98,13 @@ public static class DatabaseQueryExtensions ) { User user = await context.ResolveUserAsync(userRef, token, ct); - return await context.ResolveMemberAsync(user.Id, memberRef, token, ct); + return await context.ResolveMemberAsync(user.Id, memberRef, ct); } public static async Task ResolveMemberAsync( this DatabaseContext context, Snowflake userId, string memberRef, - Token? token = null, CancellationToken ct = default ) { @@ -116,8 +114,7 @@ public static class DatabaseQueryExtensions member = await context .Members.Include(m => m.User) .Include(m => m.ProfileFlags) - // Return members if their user isn't deleted or the user querying it is the member's owner - .Where(m => !m.User.Deleted || (token != null && token.UserId == m.UserId)) + .Where(m => !m.User.Deleted) .FirstOrDefaultAsync(m => m.Id == snowflake && m.UserId == userId, ct); if (member != null) return member; @@ -126,8 +123,7 @@ public static class DatabaseQueryExtensions member = await context .Members.Include(m => m.User) .Include(m => m.ProfileFlags) - // Return members if their user isn't deleted or the user querying it is the member's owner - .Where(m => !m.User.Deleted || (token != null && token.UserId == m.UserId)) + .Where(m => !m.User.Deleted) .FirstOrDefaultAsync(m => m.Name == memberRef && m.UserId == userId, ct); if (member != null) return member; diff --git a/Foxnouns.Backend/Database/Migrations/20241211193653_AddSentEmailCache.cs b/Foxnouns.Backend/Database/Migrations/20241211193653_AddSentEmailCache.cs deleted file mode 100644 index e0fe00d..0000000 --- a/Foxnouns.Backend/Database/Migrations/20241211193653_AddSentEmailCache.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20241211193653_AddSentEmailCache")] - public partial class AddSentEmailCache : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "sent_emails", - columns: table => new - { - id = table - .Column(type: "integer", nullable: false) - .Annotation( - "Npgsql:ValueGenerationStrategy", - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn - ), - email = table.Column(type: "text", nullable: false), - sent_at = table.Column( - type: "timestamp with time zone", - nullable: false - ), - }, - constraints: table => - { - table.PrimaryKey("pk_sent_emails", x => x.id); - } - ); - - migrationBuilder.CreateIndex( - name: "ix_sent_emails_email_sent_at", - table: "sent_emails", - columns: new[] { "email", "sent_at" } - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "sent_emails"); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index cfe2513..4bd1ede 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -1,4 +1,5 @@ // +using System; using System.Collections.Generic; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; @@ -19,7 +20,7 @@ namespace Foxnouns.Backend.Database.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("ProductVersion", "8.0.7") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -45,12 +46,12 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("name"); - b.PrimitiveCollection("RedirectUris") + b.Property("RedirectUris") .IsRequired() .HasColumnType("text[]") .HasColumnName("redirect_uris"); - b.PrimitiveCollection("Scopes") + b.Property("Scopes") .IsRequired() .HasColumnType("text[]") .HasColumnName("scopes"); @@ -192,7 +193,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("jsonb") .HasColumnName("fields"); - b.PrimitiveCollection("Links") + b.Property("Links") .IsRequired() .HasColumnType("text[]") .HasColumnName("links"); @@ -358,7 +359,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("boolean") .HasColumnName("manually_expired"); - b.PrimitiveCollection("Scopes") + b.Property("Scopes") .IsRequired() .HasColumnType("text[]") .HasColumnName("scopes"); @@ -427,7 +428,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("timestamp with time zone") .HasColumnName("last_sid_reroll"); - b.PrimitiveCollection("Links") + b.Property("Links") .IsRequired() .HasColumnType("text[]") .HasColumnName("links"); diff --git a/Foxnouns.Backend/Dto/Auth.cs b/Foxnouns.Backend/Dto/Auth.cs index ea9e67d..05308a5 100644 --- a/Foxnouns.Backend/Dto/Auth.cs +++ b/Foxnouns.Backend/Dto/Auth.cs @@ -59,4 +59,4 @@ public record EmailCallbackRequest(string State); public record EmailChangePasswordRequest(string Current, string New); -public record FediverseCallbackRequest(string Instance, string Code, string? State = null); +public record FediverseCallbackRequest(string Instance, string Code, string State); diff --git a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs index d7e8784..1d99830 100644 --- a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs +++ b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs @@ -91,34 +91,6 @@ public static class KeyCacheExtensions string state, CancellationToken ct = default ) => await keyCacheService.GetKeyAsync($"add_account:{state}", true, ct); - - public static async Task GenerateForgotPasswordStateAsync( - this KeyCacheService keyCacheService, - string email, - Snowflake userId, - CancellationToken ct = default - ) - { - string state = AuthUtils.RandomToken(); - await keyCacheService.SetKeyAsync( - $"forgot_password:{state}", - new ForgotPasswordState(email, userId), - Duration.FromHours(1), - ct - ); - return state; - } - - public static async Task GetForgotPasswordStateAsync( - this KeyCacheService keyCacheService, - string state, - CancellationToken ct = default - ) => - await keyCacheService.GetKeyAsync( - $"forgot_password:{state}", - true, - ct - ); } public record RegisterEmailState( @@ -126,6 +98,4 @@ public record RegisterEmailState( [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Snowflake? ExistingUserId ); -public record ForgotPasswordState(string Email, Snowflake UserId); - public record AddExtraAccountState(AuthType AuthType, Snowflake UserId, string? Instance = null); diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 41c9712..30d97d8 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -110,7 +110,6 @@ public static class WebApplicationExtensions .AddSingleton(SystemClock.Instance) .AddSnowflakeGenerator() .AddSingleton() - .AddSingleton() .AddScoped() .AddScoped() .AddScoped() diff --git a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs index 908598a..1132dc1 100644 --- a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs @@ -14,7 +14,6 @@ // along with this program. If not, see . using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Utils; -using Microsoft.AspNetCore.Mvc.ViewFeatures; namespace Foxnouns.Backend.Middleware; @@ -23,11 +22,9 @@ public class AuthorizationMiddleware : IMiddleware public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { Endpoint? endpoint = ctx.GetEndpoint(); - AuthorizeAttribute? authorizeAttribute = - endpoint?.Metadata.GetMetadata(); - LimitAttribute? limitAttribute = endpoint?.Metadata.GetMetadata(); + AuthorizeAttribute? attribute = endpoint?.Metadata.GetMetadata(); - if (authorizeAttribute == null || authorizeAttribute.Scopes.Length == 0) + if (attribute == null) { await next(ctx); return; @@ -42,35 +39,24 @@ public class AuthorizationMiddleware : IMiddleware ); } - // Users who got suspended by a moderator can still access *some* endpoints. if ( - token.User.Deleted - && (limitAttribute?.UsableBySuspendedUsers != true || token.User.DeletedBy == null) - ) - { - throw new ApiError.Forbidden("Deleted users cannot access this endpoint."); - } - - if ( - authorizeAttribute.Scopes.Length > 0 - && authorizeAttribute.Scopes.Except(token.Scopes.ExpandScopes()).Any() + attribute.Scopes.Length > 0 + && attribute.Scopes.Except(token.Scopes.ExpandScopes()).Any() ) { throw new ApiError.Forbidden( "This endpoint requires ungranted scopes.", - authorizeAttribute.Scopes.Except(token.Scopes.ExpandScopes()), + attribute.Scopes.Except(token.Scopes.ExpandScopes()), ErrorCode.MissingScopes ); } - if (limitAttribute?.RequireAdmin == true && token.User.Role != UserRole.Admin) - { + if (attribute.RequireAdmin && token.User.Role != UserRole.Admin) throw new ApiError.Forbidden("This endpoint can only be used by admins."); - } - if ( - limitAttribute?.RequireModerator == true - && token.User.Role is not (UserRole.Admin or UserRole.Moderator) + attribute.RequireModerator + && token.User.Role != UserRole.Admin + && token.User.Role != UserRole.Moderator ) { throw new ApiError.Forbidden("This endpoint can only be used by moderators."); @@ -83,13 +69,8 @@ public class AuthorizationMiddleware : IMiddleware [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class AuthorizeAttribute(params string[] scopes) : Attribute { - public readonly string[] Scopes = scopes.Except([":admin", ":moderator", ":deleted"]).ToArray(); -} + public readonly bool RequireAdmin = scopes.Contains(":admin"); + public readonly bool RequireModerator = scopes.Contains(":moderator"); -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class LimitAttribute : Attribute -{ - public bool UsableBySuspendedUsers { get; init; } - public bool RequireAdmin { get; init; } - public bool RequireModerator { get; init; } + public readonly string[] Scopes = scopes.Except([":admin", ":moderator"]).ToArray(); } diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index 66e57a6..3597ae7 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -66,11 +66,9 @@ builder }) .ConfigureApiBehaviorOptions(options => { - // the type isn't needed but without it, rider keeps complaining for no reason (it compiles just fine) - options.InvalidModelStateResponseFactory = (ActionContext actionContext) => - new BadRequestObjectResult( - new ApiError.AspBadRequest("Bad request", actionContext.ModelState).ToJson() - ); + options.InvalidModelStateResponseFactory = actionContext => new BadRequestObjectResult( + new ApiError.AspBadRequest("Bad request", actionContext.ModelState).ToJson() + ); }); builder.Services.AddOpenApi( diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs deleted file mode 100644 index beff74a..0000000 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Net; -using System.Text.Json.Serialization; -using System.Web; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Extensions; - -namespace Foxnouns.Backend.Services.Auth; - -public partial class FediverseAuthService -{ - private static string MisskeyAppUri(string instance) => $"https://{instance}/api/app/create"; - - private static string MisskeyTokenUri(string instance) => - $"https://{instance}/api/auth/session/userkey"; - - private static string MisskeyGenerateSessionUri(string instance) => - $"https://{instance}/api/auth/session/generate"; - - private async Task CreateMisskeyApplicationAsync( - string instance, - Snowflake? existingAppId = null - ) - { - HttpResponseMessage resp = await _client.PostAsJsonAsync( - MisskeyAppUri(instance), - new CreateMisskeyApplicationRequest( - $"pronouns.cc (+{_config.BaseUrl})", - $"pronouns.cc on {_config.BaseUrl}", - ["read:account"], - MastodonRedirectUri(instance) - ) - ); - resp.EnsureSuccessStatusCode(); - - PartialMisskeyApplication? misskeyApp = - await resp.Content.ReadFromJsonAsync(); - if (misskeyApp == null) - { - throw new FoxnounsError( - $"Application created on Misskey-compatible instance {instance} was null" - ); - } - - FediverseApplication app; - - if (existingAppId == null) - { - app = new FediverseApplication - { - Id = existingAppId ?? _snowflakeGenerator.GenerateSnowflake(), - ClientId = misskeyApp.Id, - ClientSecret = misskeyApp.Secret, - Domain = instance, - InstanceType = FediverseInstanceType.MisskeyApi, - }; - - _db.Add(app); - } - else - { - app = - await _db.FediverseApplications.FindAsync(existingAppId) - ?? throw new FoxnounsError($"Existing app with ID {existingAppId} was null"); - - app.ClientId = misskeyApp.Id; - app.ClientSecret = misskeyApp.Secret; - app.InstanceType = FediverseInstanceType.MisskeyApi; - } - - await _db.SaveChangesAsync(); - - return app; - } - - private record GetMisskeySessionUserKeyRequest( - [property: JsonPropertyName("appSecret")] string Secret, - [property: JsonPropertyName("token")] string Token - ); - - private record GetMisskeySessionUserKeyResponse( - [property: JsonPropertyName("user")] FediverseUser User - ); - - private async Task GetMisskeyUserAsync(FediverseApplication app, string code) - { - HttpResponseMessage resp = await _client.PostAsJsonAsync( - MisskeyTokenUri(app.Domain), - new GetMisskeySessionUserKeyRequest(app.ClientSecret, code) - ); - if (resp.StatusCode == HttpStatusCode.Unauthorized) - { - throw new FoxnounsError($"Application for instance {app.Domain} was invalid"); - } - - resp.EnsureSuccessStatusCode(); - GetMisskeySessionUserKeyResponse? userResp = - await resp.Content.ReadFromJsonAsync(); - if (userResp == null) - { - throw new FoxnounsError($"User response from instance {app.Domain} was invalid"); - } - - return userResp.User; - } - - private async Task GenerateMisskeyAuthUrlAsync( - FediverseApplication app, - bool forceRefresh, - string? state = null - ) - { - if (forceRefresh) - { - _logger.Information( - "An app credentials refresh was requested for {ApplicationId}, creating a new application", - app.Id - ); - app = await CreateMisskeyApplicationAsync(app.Domain, app.Id); - } - - HttpResponseMessage resp = await _client.PostAsJsonAsync( - MisskeyGenerateSessionUri(app.Domain), - new CreateMisskeySessionUriRequest(app.ClientSecret) - ); - resp.EnsureSuccessStatusCode(); - - CreateMisskeySessionUriResponse? misskeyResp = - await resp.Content.ReadFromJsonAsync(); - if (misskeyResp == null) - throw new FoxnounsError($"Session create response for app {app.Id} was null"); - - return misskeyResp.Url; - } - - private record CreateMisskeySessionUriRequest( - [property: JsonPropertyName("appSecret")] string Secret - ); - - private record CreateMisskeySessionUriResponse( - [property: JsonPropertyName("token")] string Token, - [property: JsonPropertyName("url")] string Url - ); - - private record CreateMisskeyApplicationRequest( - [property: JsonPropertyName("name")] string Name, - [property: JsonPropertyName("description")] string Description, - [property: JsonPropertyName("permission")] string[] Permissions, - [property: JsonPropertyName("callbackUrl")] string CallbackUrl - ); - - private record PartialMisskeyApplication( - [property: JsonPropertyName("id")] string Id, - [property: JsonPropertyName("secret")] string Secret - ); -} diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs index 250455a..7e67fa7 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs @@ -81,11 +81,11 @@ public partial class FediverseAuthService string softwareName = await GetSoftwareNameAsync(instance); if (IsMastodonCompatible(softwareName)) + { return await CreateMastodonApplicationAsync(instance); - if (IsMisskeyCompatible(softwareName)) - return await CreateMisskeyApplicationAsync(instance); + } - throw new ApiError.BadRequest($"{softwareName} is not a supported instance type, sorry."); + throw new NotImplementedException(); } private async Task GetSoftwareNameAsync(string instance) @@ -129,11 +129,7 @@ public partial class FediverseAuthService forceRefresh, state ), - FediverseInstanceType.MisskeyApi => await GenerateMisskeyAuthUrlAsync( - app, - forceRefresh, - state - ), + FediverseInstanceType.MisskeyApi => throw new NotImplementedException(), _ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null), }; @@ -145,7 +141,7 @@ public partial class FediverseAuthService app.InstanceType switch { FediverseInstanceType.MastodonApi => await GetMastodonUserAsync(app, code, state), - FediverseInstanceType.MisskeyApi => await GetMisskeyUserAsync(app, code), + FediverseInstanceType.MisskeyApi => throw new NotImplementedException(), _ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null), }; diff --git a/Foxnouns.Backend/Services/DataCleanupService.cs b/Foxnouns.Backend/Services/DataCleanupService.cs index 3d60462..2b42f86 100644 --- a/Foxnouns.Backend/Services/DataCleanupService.cs +++ b/Foxnouns.Backend/Services/DataCleanupService.cs @@ -33,10 +33,10 @@ public class DataCleanupService( public async Task InvokeAsync(CancellationToken ct = default) { - _logger.Debug("Cleaning up expired users"); + _logger.Information("Cleaning up expired users"); await CleanUsersAsync(ct); - _logger.Debug("Cleaning up expired data exports"); + _logger.Information("Cleaning up expired data exports"); await CleanExportsAsync(ct); } diff --git a/Foxnouns.Backend/Services/EmailRateLimiter.cs b/Foxnouns.Backend/Services/EmailRateLimiter.cs deleted file mode 100644 index 9e73792..0000000 --- a/Foxnouns.Backend/Services/EmailRateLimiter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Concurrent; -using System.Threading.RateLimiting; -using NodaTime; -using NodaTime.Extensions; - -namespace Foxnouns.Backend.Services; - -public class EmailRateLimiter -{ - private readonly ConcurrentDictionary _limiters = new(); - - private readonly FixedWindowRateLimiterOptions _limiterOptions = - new() { Window = TimeSpan.FromHours(2), PermitLimit = 3 }; - - private RateLimiter GetLimiter(string bucket) => - _limiters.GetOrAdd(bucket, _ => new FixedWindowRateLimiter(_limiterOptions)); - - public bool IsLimited(string bucket, out Duration retryAfter) - { - RateLimiter limiter = GetLimiter(bucket); - RateLimitLease lease = limiter.AttemptAcquire(); - - if (!lease.IsAcquired) - { - retryAfter = lease.TryGetMetadata(MetadataName.RetryAfter, out TimeSpan timeSpan) - ? timeSpan.ToDuration() - : default; - } - else - { - retryAfter = Duration.Zero; - } - - return !lease.IsAcquired; - } -} diff --git a/Foxnouns.Backend/Services/MailService.cs b/Foxnouns.Backend/Services/MailService.cs index a1444d9..9ae61bd 100644 --- a/Foxnouns.Backend/Services/MailService.cs +++ b/Foxnouns.Backend/Services/MailService.cs @@ -12,7 +12,6 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -using Coravel.Mailer.Mail; using Coravel.Mailer.Mail.Interfaces; using Coravel.Queuing.Interfaces; using Foxnouns.Backend.Mailables; @@ -27,18 +26,25 @@ public class MailService(ILogger logger, IMailer mailer, IQueue queue, Config co { queue.QueueAsyncTask(async () => { - await SendEmailAsync( - to, - new AccountCreationMailable( - config, - new AccountCreationMailableView - { - BaseUrl = config.BaseUrl, - To = to, - Code = code, - } - ) - ); + _logger.Debug("Sending account creation email to {ToEmail}", to); + try + { + await mailer.SendAsync( + new AccountCreationMailable( + config, + new AccountCreationMailableView + { + BaseUrl = config.BaseUrl, + To = to, + Code = code, + } + ) + ); + } + catch (Exception exc) + { + _logger.Error(exc, "Sending account creation email"); + } }); } @@ -47,31 +53,25 @@ public class MailService(ILogger logger, IMailer mailer, IQueue queue, Config co _logger.Debug("Sending add email address email to {ToEmail}", to); queue.QueueAsyncTask(async () => { - await SendEmailAsync( - to, - new AddEmailMailable( - config, - new AddEmailMailableView - { - BaseUrl = config.BaseUrl, - To = to, - Code = code, - Username = username, - } - ) - ); + try + { + await mailer.SendAsync( + new AddEmailMailable( + config, + new AddEmailMailableView + { + BaseUrl = config.BaseUrl, + To = to, + Code = code, + Username = username, + } + ) + ); + } + catch (Exception exc) + { + _logger.Error(exc, "Sending add email address email"); + } }); } - - private async Task SendEmailAsync(string to, Mailable mailable) - { - try - { - await mailer.SendAsync(mailable); - } - catch (Exception exc) - { - _logger.Error(exc, "Sending email"); - } - } } diff --git a/Foxnouns.Backend/Utils/AuthUtils.cs b/Foxnouns.Backend/Utils/AuthUtils.cs index 491694a..bcc2700 100644 --- a/Foxnouns.Backend/Utils/AuthUtils.cs +++ b/Foxnouns.Backend/Utils/AuthUtils.cs @@ -35,9 +35,6 @@ public static class AuthUtils "user.read_hidden", "user.read_privileged", "user.update", - "user.read_flags", - "user.create_flags", - "user.update_flags", ]; public static readonly string[] MemberScopes = diff --git a/Foxnouns.Frontend/src/app.html b/Foxnouns.Frontend/src/app.html index 1391f88..77a5ff5 100644 --- a/Foxnouns.Frontend/src/app.html +++ b/Foxnouns.Frontend/src/app.html @@ -2,7 +2,7 @@ - + %sveltekit.head% diff --git a/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.server.ts index b092b1e..fff5322 100644 --- a/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.server.ts +++ b/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.server.ts @@ -5,10 +5,9 @@ import createRegisterAction from "$lib/actions/register"; export const load = createCallbackLoader("fediverse", async ({ params, url }) => { const code = url.searchParams.get("code") as string | null; const state = url.searchParams.get("state") as string | null; - const token = url.searchParams.get("token") as string | null; - if ((!code || !state) && !token) throw new ApiError(undefined, ErrorCode.BadRequest).obj; + if (!code || !state) throw new ApiError(undefined, ErrorCode.BadRequest).obj; - return { code: code || token, state, instance: params.instance! }; + return { code, state, instance: params.instance! }; }); export const actions = { diff --git a/Foxnouns.Frontend/static/favicon.png b/Foxnouns.Frontend/static/favicon.png new file mode 100644 index 0000000..825b9e6 Binary files /dev/null and b/Foxnouns.Frontend/static/favicon.png differ