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