diff --git a/.editorconfig b/.editorconfig index e6b41f9..1ecf322 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,52 +1,8 @@ -[*] +[*.cs] # We use PostgresSQL which doesn't recommend more specific string types resharper_entity_framework_model_validation_unlimited_string_length_highlighting = none # This is raised for every single property of records returned by endpoints resharper_not_accessed_positional_property_local_highlighting = none - -# Microsoft .NET properties -csharp_new_line_before_members_in_object_initializers = false -csharp_preferred_modifier_order = public, internal, protected, private, file, new, required, abstract, virtual, sealed, static, override, extern, unsafe, volatile, async, readonly:suggestion - -# ReSharper properties -resharper_align_multiline_binary_expressions_chain = false -resharper_arguments_skip_single = true -resharper_blank_lines_after_start_comment = 0 -resharper_blank_lines_around_single_line_invocable = 0 -resharper_blank_lines_before_block_statements = 0 -resharper_braces_for_foreach = required_for_multiline -resharper_braces_for_ifelse = required_for_multiline -resharper_braces_redundant = false -resharper_csharp_blank_lines_around_field = 0 -resharper_csharp_empty_block_style = together_same_line -resharper_csharp_max_line_length = 166 -resharper_csharp_wrap_after_declaration_lpar = true -resharper_csharp_wrap_before_binary_opsign = true -resharper_csharp_wrap_before_declaration_rpar = true -resharper_csharp_wrap_parameters_style = chop_if_long -resharper_indent_preprocessor_other = do_not_change -resharper_instance_members_qualify_declared_in = -resharper_keep_existing_attribute_arrangement = true -resharper_max_attribute_length_for_same_line = 70 -resharper_place_accessorholder_attribute_on_same_line = false -resharper_place_expr_method_on_single_line = if_owner_is_single_line -resharper_place_method_attribute_on_same_line = if_owner_is_single_line -resharper_place_record_field_attribute_on_same_line = true -resharper_place_simple_embedded_statement_on_same_line = false -resharper_place_simple_initializer_on_single_line = false -resharper_place_simple_list_pattern_on_single_line = false -resharper_space_within_empty_braces = false -resharper_trailing_comma_in_multiline_lists = true -resharper_wrap_after_invocation_lpar = false -resharper_wrap_before_invocation_rpar = false -resharper_wrap_before_primary_constructor_declaration_rpar = true -resharper_wrap_chained_binary_patterns = chop_if_long -resharper_wrap_list_pattern = chop_always -resharper_wrap_object_and_collection_initializer_style = chop_always - -# Roslynator properties -dotnet_diagnostic.RCS1194.severity = none - [*generated.cs] -generated_code = true \ No newline at end of file +generated_code = true diff --git a/Foxnouns.Backend/BuildInfo.cs b/Foxnouns.Backend/BuildInfo.cs index 2d58277..debd2ba 100644 --- a/Foxnouns.Backend/BuildInfo.cs +++ b/Foxnouns.Backend/BuildInfo.cs @@ -7,21 +7,19 @@ public static class BuildInfo public static async Task ReadBuildInfo() { - await using Stream? stream = typeof(BuildInfo).Assembly.GetManifestResourceStream( - "version" - ); + await using var stream = typeof(BuildInfo).Assembly.GetManifestResourceStream("version"); if (stream == null) return; using var reader = new StreamReader(stream); - string[] data = (await reader.ReadToEndAsync()).Trim().Split("\n"); + var data = (await reader.ReadToEndAsync()).Trim().Split("\n"); if (data.Length < 3) return; Hash = data[0]; - bool dirty = data[2] == "dirty"; + var dirty = data[2] == "dirty"; - string[] versionData = data[1].Split("-"); + var versionData = data[1].Split("-"); if (versionData.Length < 3) return; Version = versionData[0]; diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index ced8c3d..bc34c9f 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -33,16 +33,14 @@ public class AuthController( config.GoogleAuth.Enabled, config.TumblrAuth.Enabled ); - string state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync(ct)); + var state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync(ct)); string? discord = null; if (config.DiscordAuth is { ClientId: not null, ClientSecret: not null }) - { discord = - "https://discord.com/oauth2/authorize?response_type=code" + $"https://discord.com/oauth2/authorize?response_type=code" + $"&client_id={config.DiscordAuth.ClientId}&scope=identify" + $"&prompt=none&state={state}" + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/discord")}"; - } return Ok(new UrlsResponse(config.EmailAuth.Enabled, discord, null, null)); } @@ -88,7 +86,7 @@ public class AuthController( )] public async Task GetAuthMethodAsync(Snowflake id) { - AuthMethod? authMethod = await db + var authMethod = await db .AuthMethods.Include(a => a.FediverseApplication) .FirstOrDefaultAsync(a => a.UserId == CurrentUser!.Id && a.Id == id); if (authMethod == null) @@ -101,19 +99,17 @@ public class AuthController( [Authorize("*")] public async Task DeleteAuthMethodAsync(Snowflake id) { - List authMethods = await db + var authMethods = await db .AuthMethods.Where(a => a.UserId == CurrentUser!.Id) .ToListAsync(); if (authMethods.Count < 2) - { throw new ApiError( "You cannot remove your last authentication method.", HttpStatusCode.BadRequest, ErrorCode.LastAuthMethod ); - } - AuthMethod? authMethod = authMethods.FirstOrDefault(a => a.Id == id); + var authMethod = authMethods.FirstOrDefault(a => a.Id == id); if (authMethod == null) throw new ApiError.NotFound("No authentication method with that ID found."); @@ -123,20 +119,6 @@ public class AuthController( CurrentUser!.Id ); - // If this is the user's last email, we should also clear the user's password. - if ( - authMethod.AuthType == AuthType.Email - && authMethods.Count(a => a.AuthType == AuthType.Email) == 1 - ) - { - _logger.Debug( - "Deleted last email address for user {UserId}, resetting their password", - CurrentUser.Id - ); - CurrentUser.Password = null; - db.Update(CurrentUser); - } - db.Remove(authMethod); await db.SaveChangesAsync(); diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index f7f9c3d..118caa8 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -34,10 +34,8 @@ public class DiscordAuthController( CheckRequirements(); await keyCacheService.ValidateAuthStateAsync(req.State); - RemoteAuthService.RemoteUser remoteUser = await remoteAuthService.RequestDiscordTokenAsync( - req.Code - ); - User? user = await authService.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id); + var remoteUser = await remoteAuthService.RequestDiscordTokenAsync(req.Code); + var user = await authService.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id); if (user != null) return Ok(await authService.GenerateUserTokenAsync(user)); @@ -47,14 +45,23 @@ public class DiscordAuthController( remoteUser.Id ); - string ticket = AuthUtils.RandomToken(); + var ticket = AuthUtils.RandomToken(); await keyCacheService.SetKeyAsync( $"discord:{ticket}", remoteUser, Duration.FromMinutes(20) ); - return Ok(new CallbackResponse(false, ticket, remoteUser.Username, null, null, null)); + return Ok( + new CallbackResponse( + HasAccount: false, + Ticket: ticket, + RemoteUsername: remoteUser.Username, + User: null, + Token: null, + ExpiresAt: null + ) + ); } [HttpPost("register")] @@ -63,10 +70,9 @@ public class DiscordAuthController( [FromBody] AuthController.OauthRegisterRequest req ) { - RemoteAuthService.RemoteUser? remoteUser = - await keyCacheService.GetKeyAsync( - $"discord:{req.Ticket}" - ); + var remoteUser = await keyCacheService.GetKeyAsync( + $"discord:{req.Ticket}" + ); if (remoteUser == null) throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); if ( @@ -82,7 +88,7 @@ public class DiscordAuthController( throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); } - User user = await authService.CreateUserWithRemoteAuthAsync( + var user = await authService.CreateUserWithRemoteAuthAsync( req.Username, AuthType.Discord, remoteUser.Id, @@ -98,13 +104,13 @@ public class DiscordAuthController( { CheckRequirements(); - string state = await remoteAuthService.ValidateAddAccountRequestAsync( + var state = await remoteAuthService.ValidateAddAccountRequestAsync( CurrentUser!.Id, AuthType.Discord ); - string url = - "https://discord.com/oauth2/authorize?response_type=code" + var url = + $"https://discord.com/oauth2/authorize?response_type=code" + $"&client_id={config.DiscordAuth.ClientId}&scope=identify" + $"&prompt=none&state={state}" + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/discord")}"; @@ -126,12 +132,10 @@ public class DiscordAuthController( AuthType.Discord ); - RemoteAuthService.RemoteUser remoteUser = await remoteAuthService.RequestDiscordTokenAsync( - req.Code - ); + var remoteUser = await remoteAuthService.RequestDiscordTokenAsync(req.Code); try { - AuthMethod authMethod = await authService.AddAuthMethodAsync( + var authMethod = await authService.AddAuthMethodAsync( CurrentUser.Id, AuthType.Discord, remoteUser.Id, @@ -165,10 +169,8 @@ public class DiscordAuthController( private void CheckRequirements() { if (!config.DiscordAuth.Enabled) - { throw new ApiError.BadRequest( "Discord authentication is not enabled on this instance." ); - } } } diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index 4162f4c..6aadf65 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -1,5 +1,3 @@ -using System.Net; -using EntityFramework.Exceptions.Common; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; @@ -28,8 +26,8 @@ public class EmailAuthController( { private readonly ILogger _logger = logger.ForContext(); - [HttpPost("register/init")] - public async Task RegisterInitAsync( + [HttpPost("register")] + public async Task RegisterAsync( [FromBody] RegisterRequest req, CancellationToken ct = default ) @@ -39,7 +37,11 @@ public class EmailAuthController( if (!req.Email.Contains('@')) throw new ApiError.BadRequest("Email is invalid", "email", req.Email); - string state = await keyCacheService.GenerateRegisterEmailStateAsync(req.Email, null, ct); + var state = await keyCacheService.GenerateRegisterEmailStateAsync( + req.Email, + userId: null, + ct + ); // If there's already a user with that email address, pretend we sent an email but actually ignore it if ( @@ -48,9 +50,7 @@ public class EmailAuthController( ct ) ) - { return NoContent(); - } mailService.QueueAccountCreationEmail(req.Email, state); return NoContent(); @@ -61,35 +61,62 @@ public class EmailAuthController( { CheckRequirements(); - RegisterEmailState? state = await keyCacheService.GetRegisterEmailStateAsync(req.State); - if (state is not { ExistingUserId: null }) + var state = await keyCacheService.GetRegisterEmailStateAsync(req.State); + if (state == null) throw new ApiError.BadRequest("Invalid state", "state", req.State); - string ticket = AuthUtils.RandomToken(); + // If this callback is for an existing user, add the email address to their auth methods + if (state.ExistingUserId != null) + { + var authMethod = await authService.AddAuthMethodAsync( + state.ExistingUserId.Value, + AuthType.Email, + state.Email + ); + _logger.Debug( + "Added email auth {AuthId} for user {UserId}", + authMethod.Id, + state.ExistingUserId + ); + return NoContent(); + } + + var ticket = AuthUtils.RandomToken(); await keyCacheService.SetKeyAsync($"email:{ticket}", state.Email, Duration.FromMinutes(20)); - return Ok(new CallbackResponse(false, ticket, state.Email, null, null, null)); + return Ok( + new CallbackResponse( + HasAccount: false, + Ticket: ticket, + RemoteUsername: state.Email, + User: null, + Token: null, + ExpiresAt: null + ) + ); } - [HttpPost("register")] + [HttpPost("complete-registration")] public async Task CompleteRegistrationAsync( [FromBody] CompleteRegistrationRequest req ) { CheckRequirements(); - string? email = await keyCacheService.GetKeyAsync($"email:{req.Ticket}"); + var email = await keyCacheService.GetKeyAsync($"email:{req.Ticket}"); if (email == null) throw new ApiError.BadRequest("Unknown ticket", "ticket", req.Ticket); - User user = await authService.CreateUserWithPasswordAsync( - req.Username, - email, - req.Password - ); - Application frontendApp = await db.GetFrontendApplicationAsync(); + // Check if username is valid at all + ValidationUtils.Validate([("username", ValidationUtils.ValidateUsername(req.Username))]); + // Check if username is already taken + if (await db.Users.AnyAsync(u => u.Username == req.Username)) + throw new ApiError.BadRequest("Username is already taken", "username", req.Username); - (string? tokenStr, Token? token) = authService.GenerateToken( + var user = await authService.CreateUserWithPasswordAsync(req.Username, email, req.Password); + var frontendApp = await db.GetFrontendApplicationAsync(); + + var (tokenStr, token) = authService.GenerateToken( user, frontendApp, ["*"], @@ -103,7 +130,7 @@ public class EmailAuthController( return Ok( new AuthController.AuthResponse( - await userRenderer.RenderUserAsync(user, user, renderMembers: false), + await userRenderer.RenderUserAsync(user, selfUser: user, renderMembers: false), tokenStr, token.ExpiresAt ) @@ -119,16 +146,19 @@ public class EmailAuthController( { CheckRequirements(); - (User? user, AuthService.EmailAuthenticationResult authenticationResult) = - await authService.AuthenticateUserAsync(req.Email, req.Password, ct); + var (user, authenticationResult) = await authService.AuthenticateUserAsync( + req.Email, + req.Password, + ct + ); if (authenticationResult == AuthService.EmailAuthenticationResult.MfaRequired) throw new NotImplementedException("MFA is not implemented yet"); - Application frontendApp = await db.GetFrontendApplicationAsync(ct); + var frontendApp = await db.GetFrontendApplicationAsync(ct); _logger.Debug("Logging user {Id} in with email and password", user.Id); - (string? tokenStr, Token? token) = authService.GenerateToken( + var (tokenStr, token) = authService.GenerateToken( user, frontendApp, ["*"], @@ -142,34 +172,25 @@ public class EmailAuthController( return Ok( new AuthController.AuthResponse( - await userRenderer.RenderUserAsync(user, user, renderMembers: false, ct: ct), + await userRenderer.RenderUserAsync( + user, + selfUser: user, + renderMembers: false, + ct: ct + ), tokenStr, token.ExpiresAt ) ); } - [HttpPost("change-password")] - [Authorize("*")] - public async Task UpdatePasswordAsync([FromBody] ChangePasswordRequest req) - { - if (!await authService.ValidatePasswordAsync(CurrentUser!, req.Current)) - throw new ApiError.Forbidden("Invalid password"); - - ValidationUtils.Validate([("new", ValidationUtils.ValidatePassword(req.New))]); - - await authService.SetUserPasswordAsync(CurrentUser!, req.New); - await db.SaveChangesAsync(); - return NoContent(); - } - - [HttpPost("add-email")] + [HttpPost("add")] [Authorize("*")] public async Task AddEmailAddressAsync([FromBody] AddEmailAddressRequest req) { CheckRequirements(); - List emails = await db + var emails = await db .AuthMethods.Where(m => m.UserId == CurrentUser!.Id && m.AuthType == AuthType.Email) .ToListAsync(); if (emails.Count > AuthUtils.MaxAuthMethodsPerType) @@ -183,8 +204,11 @@ public class EmailAuthController( if (emails.Count != 0) { - if (!await authService.ValidatePasswordAsync(CurrentUser!, req.Password)) + var validPassword = await authService.ValidatePasswordAsync(CurrentUser!, req.Password); + if (!validPassword) + { throw new ApiError.Forbidden("Invalid password"); + } } else { @@ -192,12 +216,12 @@ public class EmailAuthController( await db.SaveChangesAsync(); } - string state = await keyCacheService.GenerateRegisterEmailStateAsync( + var state = await keyCacheService.GenerateRegisterEmailStateAsync( req.Email, - CurrentUser!.Id + userId: CurrentUser!.Id ); - bool emailExists = await db + var emailExists = await db .AuthMethods.Where(m => m.AuthType == AuthType.Email && m.RemoteId == req.Email) .AnyAsync(); if (emailExists) @@ -209,48 +233,6 @@ public class EmailAuthController( return NoContent(); } - [HttpPost("add-email/callback")] - [Authorize("*")] - public async Task AddEmailCallbackAsync([FromBody] CallbackRequest req) - { - CheckRequirements(); - - RegisterEmailState? state = await keyCacheService.GetRegisterEmailStateAsync(req.State); - if (state?.ExistingUserId != CurrentUser!.Id) - throw new ApiError.BadRequest("Invalid state", "state", req.State); - - try - { - AuthMethod authMethod = await authService.AddAuthMethodAsync( - CurrentUser.Id, - AuthType.Email, - state.Email - ); - _logger.Debug( - "Added email auth {AuthId} for user {UserId}", - authMethod.Id, - CurrentUser.Id - ); - - return Ok( - new AuthController.AddOauthAccountResponse( - authMethod.Id, - AuthType.Email, - authMethod.RemoteId, - null - ) - ); - } - catch (UniqueConstraintException) - { - throw new ApiError( - "That email address is already linked.", - HttpStatusCode.BadRequest, - ErrorCode.AccountAlreadyLinked - ); - } - } - public record AddEmailAddressRequest(string Email, string Password); private void CheckRequirements() @@ -266,6 +248,4 @@ public class EmailAuthController( public record CompleteRegistrationRequest(string Ticket, string Username, string Password); public record CallbackRequest(string State); - - public record ChangePasswordRequest(string Current, string New); } diff --git a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs index f47eb43..103061b 100644 --- a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs @@ -34,7 +34,7 @@ public class FediverseAuthController( if (instance.Any(c => c is '@' or ':' or '/') || !instance.Contains('.')) throw new ApiError.BadRequest("Not a valid domain.", "instance", instance); - string url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh); + var url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh); return Ok(new AuthController.SingleUrlResponse(url)); } @@ -42,19 +42,22 @@ public class FediverseAuthController( [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task FediverseCallbackAsync([FromBody] CallbackRequest req) { - FediverseApplication app = await fediverseAuthService.GetApplicationAsync(req.Instance); - FediverseAuthService.FediverseUser remoteUser = - await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code, req.State); + var app = await fediverseAuthService.GetApplicationAsync(req.Instance); + var remoteUser = await fediverseAuthService.GetRemoteFediverseUserAsync( + app, + req.Code, + req.State + ); - User? user = await authService.AuthenticateUserAsync( + var user = await authService.AuthenticateUserAsync( AuthType.Fediverse, remoteUser.Id, - app + instance: app ); if (user != null) return Ok(await authService.GenerateUserTokenAsync(user)); - string ticket = AuthUtils.RandomToken(); + var ticket = AuthUtils.RandomToken(); await keyCacheService.SetKeyAsync( $"fediverse:{ticket}", new FediverseTicketData(app.Id, remoteUser), @@ -63,12 +66,12 @@ public class FediverseAuthController( return Ok( new CallbackResponse( - false, - ticket, - $"@{remoteUser.Username}@{app.Domain}", - null, - null, - null + HasAccount: false, + Ticket: ticket, + RemoteUsername: $"@{remoteUser.Username}@{app.Domain}", + User: null, + Token: null, + ExpiresAt: null ) ); } @@ -79,16 +82,14 @@ public class FediverseAuthController( [FromBody] AuthController.OauthRegisterRequest req ) { - FediverseTicketData? ticketData = await keyCacheService.GetKeyAsync( + var ticketData = await keyCacheService.GetKeyAsync( $"fediverse:{req.Ticket}", - true + delete: true ); if (ticketData == null) throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); - FediverseApplication? app = await db.FediverseApplications.FindAsync( - ticketData.ApplicationId - ); + var app = await db.FediverseApplications.FindAsync(ticketData.ApplicationId); if (app == null) throw new FoxnounsError("Null application found for ticket"); @@ -110,12 +111,12 @@ public class FediverseAuthController( throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); } - User user = await authService.CreateUserWithRemoteAuthAsync( + var user = await authService.CreateUserWithRemoteAuthAsync( req.Username, AuthType.Fediverse, ticketData.User.Id, ticketData.User.Username, - app + instance: app ); return Ok(await authService.GenerateUserTokenAsync(user)); @@ -131,13 +132,13 @@ public class FediverseAuthController( if (instance.Any(c => c is '@' or ':' or '/') || !instance.Contains('.')) throw new ApiError.BadRequest("Not a valid domain.", "instance", instance); - string state = await remoteAuthService.ValidateAddAccountRequestAsync( + var state = await remoteAuthService.ValidateAddAccountRequestAsync( CurrentUser!.Id, AuthType.Fediverse, instance ); - string url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh, state); + var url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh, state); return Ok(new AuthController.SingleUrlResponse(url)); } @@ -152,12 +153,11 @@ public class FediverseAuthController( req.Instance ); - FediverseApplication app = await fediverseAuthService.GetApplicationAsync(req.Instance); - FediverseAuthService.FediverseUser remoteUser = - await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code); + var app = await fediverseAuthService.GetApplicationAsync(req.Instance); + var remoteUser = await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code); try { - AuthMethod authMethod = await authService.AddAuthMethodAsync( + var authMethod = await authService.AddAuthMethodAsync( CurrentUser.Id, AuthType.Fediverse, remoteUser.Id, diff --git a/Foxnouns.Backend/Controllers/ExportsController.cs b/Foxnouns.Backend/Controllers/ExportsController.cs index feb8d91..30ecaaa 100644 --- a/Foxnouns.Backend/Controllers/ExportsController.cs +++ b/Foxnouns.Backend/Controllers/ExportsController.cs @@ -25,7 +25,7 @@ public class ExportsController( [HttpGet] public async Task GetDataExportsAsync() { - DataExport? export = await db + var export = await db .DataExports.Where(d => d.UserId == CurrentUser!.Id) .OrderByDescending(d => d.Id) .FirstOrDefaultAsync(); diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs index 9bb9f91..e7a17e9 100644 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -1,6 +1,5 @@ using Coravel.Queuing.Interfaces; using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; @@ -8,7 +7,6 @@ using Foxnouns.Backend.Services; using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; namespace Foxnouns.Backend.Controllers; @@ -31,9 +29,7 @@ public class FlagsController( )] public async Task GetFlagsAsync(CancellationToken ct = default) { - List flags = await db - .PrideFlags.Where(f => f.UserId == CurrentUser!.Id) - .ToListAsync(ct); + var flags = await db.PrideFlags.Where(f => f.UserId == CurrentUser!.Id).ToListAsync(ct); return Ok(flags.Select(userRenderer.RenderPrideFlag)); } @@ -47,7 +43,7 @@ public class FlagsController( { ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, req.Image)); - Snowflake id = snowflakeGenerator.GenerateSnowflake(); + var id = snowflakeGenerator.GenerateSnowflake(); queue.QueueInvocableWithPayload( new CreateFlagPayload(id, CurrentUser!.Id, req.Name, req.Image, req.Description) @@ -66,7 +62,7 @@ public class FlagsController( { ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, null)); - PrideFlag? flag = await db.PrideFlags.FirstOrDefaultAsync(f => + var flag = await db.PrideFlags.FirstOrDefaultAsync(f => f.Id == id && f.UserId == CurrentUser!.Id ); if (flag == null) @@ -94,20 +90,20 @@ public class FlagsController( [Authorize("user.update")] public async Task DeleteFlagAsync(Snowflake id) { - await using IDbContextTransaction tx = await db.Database.BeginTransactionAsync(); + await using var tx = await db.Database.BeginTransactionAsync(); - PrideFlag? flag = await db.PrideFlags.FirstOrDefaultAsync(f => + var flag = await db.PrideFlags.FirstOrDefaultAsync(f => f.Id == id && f.UserId == CurrentUser!.Id ); if (flag == null) throw new ApiError.NotFound("Unknown flag ID, or it's not your flag."); - string hash = flag.Hash; + var hash = flag.Hash; db.PrideFlags.Remove(flag); await db.SaveChangesAsync(); - int flagCount = await db.PrideFlags.CountAsync(f => f.Hash == flag.Hash); + var flagCount = await db.PrideFlags.CountAsync(f => f.Hash == flag.Hash); if (flagCount == 0) { try @@ -124,9 +120,7 @@ public class FlagsController( } } else - { _logger.Debug("Flag file {Hash} is used by other flags, not deleting", hash); - } await tx.CommitAsync(); diff --git a/Foxnouns.Backend/Controllers/InternalController.cs b/Foxnouns.Backend/Controllers/InternalController.cs index 9e94022..c19d456 100644 --- a/Foxnouns.Backend/Controllers/InternalController.cs +++ b/Foxnouns.Backend/Controllers/InternalController.cs @@ -44,22 +44,21 @@ public partial class InternalController(DatabaseContext db) : ControllerBase [HttpPost("request-data")] public async Task GetRequestDataAsync([FromBody] RequestDataRequest req) { - RouteEndpoint? endpoint = GetEndpoint(HttpContext, req.Path, req.Method); + var endpoint = GetEndpoint(HttpContext, req.Path, req.Method); if (endpoint == null) throw new ApiError.BadRequest("Path/method combination is invalid"); - ControllerActionDescriptor? actionDescriptor = - endpoint.Metadata.GetMetadata(); - string? template = actionDescriptor?.AttributeRouteInfo?.Template; + var actionDescriptor = endpoint.Metadata.GetMetadata(); + var template = actionDescriptor?.AttributeRouteInfo?.Template; if (template == null) throw new FoxnounsError("Template value was null on valid endpoint"); template = GetCleanedTemplate(template); // If no token was supplied, or it isn't valid base 64, return a null user ID (limiting by IP) - if (!AuthUtils.TryParseToken(req.Token, out byte[]? rawToken)) + if (!AuthUtils.TryParseToken(req.Token, out var rawToken)) return Ok(new RequestDataResponse(null, template)); - Snowflake? userId = await db.GetTokenUserId(rawToken); + var userId = await db.GetTokenUserId(rawToken); return Ok(new RequestDataResponse(userId, template)); } @@ -73,13 +72,12 @@ public partial class InternalController(DatabaseContext db) : ControllerBase string requestMethod ) { - EndpointDataSource? endpointDataSource = - httpContext.RequestServices.GetService(); + var endpointDataSource = httpContext.RequestServices.GetService(); if (endpointDataSource == null) return null; - IEnumerable endpoints = endpointDataSource.Endpoints.OfType(); + var endpoints = endpointDataSource.Endpoints.OfType(); - foreach (RouteEndpoint? endpoint in endpoints) + foreach (var endpoint in endpoints) { if (endpoint.RoutePattern.RawText == null) continue; @@ -88,19 +86,16 @@ public partial class InternalController(DatabaseContext db) : ControllerBase TemplateParser.Parse(endpoint.RoutePattern.RawText), new RouteValueDictionary() ); - if (!templateMatcher.TryMatch(url, new RouteValueDictionary())) + if (!templateMatcher.TryMatch(url, new())) continue; - HttpMethodAttribute? httpMethodAttribute = - endpoint.Metadata.GetMetadata(); + var httpMethodAttribute = endpoint.Metadata.GetMetadata(); if ( - httpMethodAttribute?.HttpMethods.Any(x => + httpMethodAttribute != null + && !httpMethodAttribute.HttpMethods.Any(x => x.Equals(requestMethod, StringComparison.OrdinalIgnoreCase) - ) == false + ) ) - { continue; - } - return endpoint; } diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index a9021b9..42b8ee5 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -9,7 +9,6 @@ using Foxnouns.Backend.Services; using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; using NodaTime; namespace Foxnouns.Backend.Controllers; @@ -33,7 +32,7 @@ public class MembersController( )] public async Task GetMembersAsync(string userRef, CancellationToken ct = default) { - User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); + var user = await db.ResolveUserAsync(userRef, CurrentToken, ct); return Ok(await memberRenderer.RenderUserMembersAsync(user, CurrentToken)); } @@ -45,7 +44,7 @@ public class MembersController( CancellationToken ct = default ) { - Member member = await db.ResolveMemberAsync(userRef, memberRef, CurrentToken, ct); + var member = await db.ResolveMemberAsync(userRef, memberRef, CurrentToken, ct); return Ok(memberRenderer.RenderMember(member, CurrentToken)); } @@ -79,7 +78,7 @@ public class MembersController( ] ); - int memberCount = await db.Members.CountAsync(m => m.UserId == CurrentUser.Id, ct); + var memberCount = await db.Members.CountAsync(m => m.UserId == CurrentUser.Id, ct); if (memberCount >= MaxMemberCount) throw new ApiError.BadRequest("Maximum number of members reached"); @@ -121,11 +120,9 @@ public class MembersController( } if (req.Avatar != null) - { queue.QueueInvocableWithPayload( new AvatarUpdatePayload(member.Id, req.Avatar) ); - } return Ok(memberRenderer.RenderMember(member, CurrentToken)); } @@ -137,8 +134,8 @@ public class MembersController( [FromBody] UpdateMemberRequest req ) { - await using IDbContextTransaction tx = await db.Database.BeginTransactionAsync(); - Member member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); + await using var tx = await db.Database.BeginTransactionAsync(); + var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); var errors = new List<(string, ValidationError?)>(); // We might add extra validations for names later down the line. @@ -200,11 +197,7 @@ public class MembersController( if (req.Flags != null) { - ValidationError? flagError = await db.SetMemberFlagsAsync( - CurrentUser!.Id, - member.Id, - req.Flags - ); + var flagError = await db.SetMemberFlagsAsync(CurrentUser!.Id, member.Id, req.Flags); if (flagError != null) errors.Add(("flags", flagError)); } @@ -217,12 +210,9 @@ public class MembersController( // (atomic operations are hard when combined with background jobs) // so it's in a separate block to the validation above. if (req.HasProperty(nameof(req.Avatar))) - { queue.QueueInvocableWithPayload( new AvatarUpdatePayload(member.Id, req.Avatar) ); - } - try { await db.SaveChangesAsync(); @@ -238,7 +228,7 @@ public class MembersController( throw new ApiError.BadRequest( "A member with that name already exists", "name", - req.Name + req.Name! ); } @@ -264,8 +254,8 @@ public class MembersController( [Authorize("member.update")] public async Task DeleteMemberAsync(string memberRef) { - Member member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); - int deleteCount = await db + var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); + var deleteCount = await db .Members.Where(m => m.UserId == CurrentUser!.Id && m.Id == member.Id) .ExecuteDeleteAsync(); if (deleteCount == 0) @@ -299,9 +289,9 @@ public class MembersController( [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task RerollSidAsync(string memberRef) { - Member member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); + var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); - Instant minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1); + var minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1); if (CurrentUser!.LastSidReroll > minTimeAgo) throw new ApiError.BadRequest("Cannot reroll short ID yet"); @@ -318,10 +308,7 @@ public class MembersController( ); // Fetch the new sid then pass that to RenderMember - string newSid = await db - .Members.Where(m => m.Id == member.Id) - .Select(m => m.Sid) - .FirstAsync(); + var newSid = await db.Members.Where(m => m.Id == member.Id).Select(m => m.Sid).FirstAsync(); return Ok(memberRenderer.RenderMember(member, CurrentToken, newSid)); } } diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs index 2a9466e..76132ee 100644 --- a/Foxnouns.Backend/Controllers/MetaController.cs +++ b/Foxnouns.Backend/Controllers/MetaController.cs @@ -10,8 +10,9 @@ public class MetaController : ApiControllerBase [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] - public IActionResult GetMeta() => - Ok( + public IActionResult GetMeta() + { + return Ok( new MetaResponse( Repository, BuildInfo.Version, @@ -24,13 +25,14 @@ public class MetaController : ApiControllerBase (int)FoxnounsMetrics.UsersActiveDayCount.Value ), new Limits( - MembersController.MaxMemberCount, - ValidationUtils.MaxBioLength, - ValidationUtils.MaxCustomPreferences, - AuthUtils.MaxAuthMethodsPerType + MemberCount: MembersController.MaxMemberCount, + BioLength: ValidationUtils.MaxBioLength, + CustomPreferences: ValidationUtils.MaxCustomPreferences, + MaxAuthMethods: AuthUtils.MaxAuthMethodsPerType ) ) ); + } [HttpGet("/api/v2/coffee")] public IActionResult BrewCoffee() => diff --git a/Foxnouns.Backend/Controllers/SidController.cs b/Foxnouns.Backend/Controllers/SidController.cs index c89b120..b8f5948 100644 --- a/Foxnouns.Backend/Controllers/SidController.cs +++ b/Foxnouns.Backend/Controllers/SidController.cs @@ -24,7 +24,7 @@ public class SidController(Config config, DatabaseContext db) : ApiControllerBas private async Task ResolveUserSidAsync(string id, CancellationToken ct = default) { - string? username = await db + var username = await db .Users.Where(u => u.Sid == id.ToLowerInvariant() && !u.Deleted) .Select(u => u.Username) .FirstOrDefaultAsync(ct); diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 98d5645..b133954 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -9,7 +9,6 @@ using Foxnouns.Backend.Services; using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; using NodaTime; namespace Foxnouns.Backend.Controllers; @@ -30,9 +29,16 @@ public class UsersController( [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task GetUserAsync(string userRef, CancellationToken ct = default) { - User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); + var user = await db.ResolveUserAsync(userRef, CurrentToken, ct); return Ok( - await userRenderer.RenderUserAsync(user, CurrentUser, CurrentToken, true, true, ct: ct) + await userRenderer.RenderUserAsync( + user, + selfUser: CurrentUser, + token: CurrentToken, + renderMembers: true, + renderAuthMethods: true, + ct: ct + ) ); } @@ -44,8 +50,8 @@ public class UsersController( CancellationToken ct = default ) { - await using IDbContextTransaction tx = await db.Database.BeginTransactionAsync(ct); - User user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct); + await using var tx = await db.Database.BeginTransactionAsync(ct); + var user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct); var errors = new List<(string, ValidationError?)>(); if (req.Username != null && req.Username != user.Username) @@ -102,7 +108,7 @@ public class UsersController( if (req.Flags != null) { - ValidationError? flagError = await db.SetUserFlagsAsync(CurrentUser!.Id, req.Flags); + var flagError = await db.SetUserFlagsAsync(CurrentUser!.Id, req.Flags); if (flagError != null) errors.Add(("flags", flagError)); } @@ -135,18 +141,14 @@ public class UsersController( else { if (TimeZoneInfo.TryFindSystemTimeZoneById(req.Timezone, out _)) - { user.Timezone = req.Timezone; - } else - { errors.Add( ( "timezone", ValidationError.GenericValidationError("Invalid timezone", req.Timezone) ) ); - } } } @@ -155,11 +157,9 @@ public class UsersController( // (atomic operations are hard when combined with background jobs) // so it's in a separate block to the validation above. if (req.HasProperty(nameof(req.Avatar))) - { queue.QueueInvocableWithPayload( new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar) ); - } try { @@ -176,7 +176,7 @@ public class UsersController( throw new ApiError.BadRequest( "That username is already taken.", "username", - req.Username + req.Username! ); } @@ -202,12 +202,12 @@ public class UsersController( { ValidationUtils.Validate(ValidationUtils.ValidateCustomPreferences(req)); - User user = await db.ResolveUserAsync(CurrentUser!.Id, ct); + var user = await db.ResolveUserAsync(CurrentUser!.Id, ct); var preferences = user .CustomPreferences.Where(x => req.Any(r => r.Id == x.Key)) .ToDictionary(); - foreach (CustomPreferenceUpdate? r in req) + foreach (var r in req) { if (r.Id != null && preferences.ContainsKey(r.Id.Value)) { @@ -271,7 +271,7 @@ public class UsersController( [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task GetUserSettingsAsync(CancellationToken ct = default) { - User user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct); + var user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct); return Ok(user.Settings); } @@ -283,7 +283,7 @@ public class UsersController( CancellationToken ct = default ) { - User user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct); + var user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct); if (req.HasProperty(nameof(req.DarkMode))) user.Settings.DarkMode = req.DarkMode; @@ -304,7 +304,7 @@ public class UsersController( [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task RerollSidAsync() { - Instant minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1); + var minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1); if (CurrentUser!.LastSidReroll > minTimeAgo) throw new ApiError.BadRequest("Cannot reroll short ID yet"); @@ -318,18 +318,18 @@ public class UsersController( ); // Get the user's new sid - string newSid = await db + var newSid = await db .Users.Where(u => u.Id == CurrentUser.Id) .Select(u => u.Sid) .FirstAsync(); - User user = await db.ResolveUserAsync(CurrentUser.Id); + var user = await db.ResolveUserAsync(CurrentUser.Id); return Ok( await userRenderer.RenderUserAsync( - user, + CurrentUser, CurrentUser, CurrentToken, - false, + renderMembers: false, overrideSid: newSid ) ); diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index f79f4fe..b3d4d76 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -11,8 +11,9 @@ namespace Foxnouns.Backend.Database; public class DatabaseContext(DbContextOptions options) : DbContext(options) { - private static string GenerateConnectionString(Config.DatabaseConfig config) => - new NpgsqlConnectionStringBuilder(config.Url) + private static string GenerateConnectionString(Config.DatabaseConfig config) + { + return new NpgsqlConnectionStringBuilder(config.Url) { Pooling = config.EnablePooling ?? true, Timeout = config.Timeout ?? 5, @@ -21,6 +22,7 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options) ConnectionPruningInterval = 10, ConnectionIdleLifetime = 10, }.ConnectionString; + } public static NpgsqlDataSource BuildDataSource(Config config) { @@ -44,18 +46,18 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options) .UseSnakeCaseNamingConvention() .UseExceptionProcessor(); - public DbSet Users { get; init; } = null!; - public DbSet Members { get; init; } = null!; - public DbSet AuthMethods { get; init; } = null!; - public DbSet FediverseApplications { get; init; } = null!; - public DbSet Tokens { get; init; } = null!; - public DbSet Applications { get; init; } = null!; - public DbSet TemporaryKeys { get; init; } = null!; - public DbSet DataExports { get; init; } = null!; + public DbSet Users { get; init; } + public DbSet Members { get; init; } + public DbSet AuthMethods { get; init; } + public DbSet FediverseApplications { get; init; } + public DbSet Tokens { get; init; } + public DbSet Applications { get; init; } + public DbSet TemporaryKeys { get; init; } + public DbSet DataExports { get; init; } - public DbSet PrideFlags { get; init; } = null!; - public DbSet UserFlags { get; init; } = null!; - public DbSet MemberFlags { get; init; } = null!; + public DbSet PrideFlags { get; init; } + public DbSet UserFlags { get; init; } + public DbSet MemberFlags { get; init; } protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { @@ -136,16 +138,16 @@ public class DesignTimeDatabaseContextFactory : IDesignTimeDbContextFactory() ?? new Config(); + .Get() ?? new(); - NpgsqlDataSource dataSource = DatabaseContext.BuildDataSource(config); + var dataSource = DatabaseContext.BuildDataSource(config); - DbContextOptions options = DatabaseContext + var options = DatabaseContext .BuildOptions(new DbContextOptionsBuilder(), dataSource, null) .Options; diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index dc25b55..e8d2a98 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -26,7 +26,7 @@ public static class DatabaseQueryExtensions } User? user; - if (Snowflake.TryParse(userRef, out Snowflake? snowflake)) + if (Snowflake.TryParse(userRef, out var snowflake)) { user = await context .Users.Where(u => !u.Deleted) @@ -42,7 +42,7 @@ public static class DatabaseQueryExtensions return user; throw new ApiError.NotFound( "No user with that ID or username found.", - ErrorCode.UserNotFound + code: ErrorCode.UserNotFound ); } @@ -52,12 +52,12 @@ public static class DatabaseQueryExtensions CancellationToken ct = default ) { - User? user = await context + var user = await context .Users.Where(u => !u.Deleted) .FirstOrDefaultAsync(u => u.Id == id, ct); if (user != null) return user; - throw new ApiError.NotFound("No user with that ID found.", ErrorCode.UserNotFound); + throw new ApiError.NotFound("No user with that ID found.", code: ErrorCode.UserNotFound); } public static async Task ResolveMemberAsync( @@ -66,13 +66,16 @@ public static class DatabaseQueryExtensions CancellationToken ct = default ) { - Member? member = await context + var member = await context .Members.Include(m => m.User) .Where(m => !m.User.Deleted) .FirstOrDefaultAsync(m => m.Id == id, ct); if (member != null) return member; - throw new ApiError.NotFound("No member with that ID found.", ErrorCode.MemberNotFound); + throw new ApiError.NotFound( + "No member with that ID found.", + code: ErrorCode.MemberNotFound + ); } public static async Task ResolveMemberAsync( @@ -83,7 +86,7 @@ public static class DatabaseQueryExtensions CancellationToken ct = default ) { - User user = await context.ResolveUserAsync(userRef, token, ct); + var user = await context.ResolveUserAsync(userRef, token, ct); return await context.ResolveMemberAsync(user.Id, memberRef, ct); } @@ -95,7 +98,7 @@ public static class DatabaseQueryExtensions ) { Member? member; - if (Snowflake.TryParse(memberRef, out Snowflake? snowflake)) + if (Snowflake.TryParse(memberRef, out var snowflake)) { member = await context .Members.Include(m => m.User) @@ -115,7 +118,7 @@ public static class DatabaseQueryExtensions return member; throw new ApiError.NotFound( "No member with that ID or name found.", - ErrorCode.MemberNotFound + code: ErrorCode.MemberNotFound ); } @@ -124,10 +127,7 @@ public static class DatabaseQueryExtensions CancellationToken ct = default ) { - Application? app = await context.Applications.FirstOrDefaultAsync( - a => a.Id == new Snowflake(0), - ct - ); + var app = await context.Applications.FirstOrDefaultAsync(a => a.Id == new Snowflake(0), ct); if (app != null) return app; @@ -152,9 +152,9 @@ public static class DatabaseQueryExtensions CancellationToken ct = default ) { - byte[] hash = SHA512.HashData(rawToken); + var hash = SHA512.HashData(rawToken); - Token? oauthToken = await context + var oauthToken = await context .Tokens.Include(t => t.Application) .Include(t => t.User) .FirstOrDefaultAsync( @@ -174,7 +174,7 @@ public static class DatabaseQueryExtensions CancellationToken ct = default ) { - byte[] hash = SHA512.HashData(rawToken); + var hash = SHA512.HashData(rawToken); return await context .Tokens.Where(t => t.Hash == hash diff --git a/Foxnouns.Backend/Database/DatabaseServiceExtensions.cs b/Foxnouns.Backend/Database/DatabaseServiceExtensions.cs index 878927b..4d966bf 100644 --- a/Foxnouns.Backend/Database/DatabaseServiceExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseServiceExtensions.cs @@ -1,4 +1,3 @@ -using Npgsql; using Serilog; namespace Foxnouns.Backend.Database; @@ -10,8 +9,8 @@ public static class DatabaseServiceExtensions Config config ) { - NpgsqlDataSource dataSource = DatabaseContext.BuildDataSource(config); - ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(dispose: false); + var dataSource = DatabaseContext.BuildDataSource(config); + var loggerFactory = new LoggerFactory().AddSerilog(dispose: false); serviceCollection.AddDbContext(options => DatabaseContext.BuildOptions(options, dataSource, loggerFactory) diff --git a/Foxnouns.Backend/Database/FlagQueryExtensions.cs b/Foxnouns.Backend/Database/FlagQueryExtensions.cs index a6354d8..cce6eb4 100644 --- a/Foxnouns.Backend/Database/FlagQueryExtensions.cs +++ b/Foxnouns.Backend/Database/FlagQueryExtensions.cs @@ -20,10 +20,8 @@ public static class FlagQueryExtensions Snowflake[] flagIds ) { - List currentFlags = await db - .UserFlags.Where(f => f.UserId == userId) - .ToListAsync(); - foreach (UserFlag flag in currentFlags) + var currentFlags = await db.UserFlags.Where(f => f.UserId == userId).ToListAsync(); + foreach (var flag in currentFlags) db.UserFlags.Remove(flag); // If there's no new flags to set, we're done @@ -32,16 +30,12 @@ public static class FlagQueryExtensions if (flagIds.Length > 100) return ValidationError.LengthError("Too many profile flags", 0, 100, flagIds.Length); - List flags = await db.GetFlagsAsync(userId); - Snowflake[] unknownFlagIds = flagIds.Where(id => flags.All(f => f.Id != id)).ToArray(); + var flags = await db.GetFlagsAsync(userId); + var unknownFlagIds = flagIds.Where(id => flags.All(f => f.Id != id)).ToArray(); if (unknownFlagIds.Length != 0) return ValidationError.GenericValidationError("Unknown flag IDs", unknownFlagIds); - IEnumerable userFlags = flagIds.Select(id => new UserFlag - { - PrideFlagId = id, - UserId = userId, - }); + var userFlags = flagIds.Select(id => new UserFlag { PrideFlagId = id, UserId = userId }); db.UserFlags.AddRange(userFlags); return null; @@ -54,10 +48,8 @@ public static class FlagQueryExtensions Snowflake[] flagIds ) { - List currentFlags = await db - .MemberFlags.Where(f => f.MemberId == memberId) - .ToListAsync(); - foreach (MemberFlag flag in currentFlags) + var currentFlags = await db.MemberFlags.Where(f => f.MemberId == memberId).ToListAsync(); + foreach (var flag in currentFlags) db.MemberFlags.Remove(flag); if (flagIds.Length == 0) @@ -65,12 +57,12 @@ public static class FlagQueryExtensions if (flagIds.Length > 100) return ValidationError.LengthError("Too many profile flags", 0, 100, flagIds.Length); - List flags = await db.GetFlagsAsync(userId); - Snowflake[] unknownFlagIds = flagIds.Where(id => flags.All(f => f.Id != id)).ToArray(); + var flags = await db.GetFlagsAsync(userId); + var unknownFlagIds = flagIds.Where(id => flags.All(f => f.Id != id)).ToArray(); if (unknownFlagIds.Length != 0) return ValidationError.GenericValidationError("Unknown flag IDs", unknownFlagIds); - IEnumerable memberFlags = flagIds.Select(id => new MemberFlag + var memberFlags = flagIds.Select(id => new MemberFlag { PrideFlagId = id, MemberId = memberId, diff --git a/Foxnouns.Backend/Database/Migrations/20240527132444_Init.cs b/Foxnouns.Backend/Database/Migrations/20240527132444_Init.cs index 7d2f05c..7a29aa0 100644 --- a/Foxnouns.Backend/Database/Migrations/20240527132444_Init.cs +++ b/Foxnouns.Backend/Database/Migrations/20240527132444_Init.cs @@ -24,7 +24,10 @@ namespace Foxnouns.Backend.Database.Migrations client_secret = table.Column(type: "text", nullable: false), instance_type = table.Column(type: "integer", nullable: false), }, - constraints: table => table.PrimaryKey("pk_fediverse_applications", x => x.id) + constraints: table => + { + table.PrimaryKey("pk_fediverse_applications", x => x.id); + } ); migrationBuilder.CreateTable( @@ -43,7 +46,10 @@ namespace Foxnouns.Backend.Database.Migrations names = table.Column(type: "jsonb", nullable: false), pronouns = table.Column(type: "jsonb", nullable: false), }, - constraints: table => table.PrimaryKey("pk_users", x => x.id) + constraints: table => + { + table.PrimaryKey("pk_users", x => x.id); + } ); migrationBuilder.CreateTable( diff --git a/Foxnouns.Backend/Database/Migrations/20240528125310_AddApplications.cs b/Foxnouns.Backend/Database/Migrations/20240528125310_AddApplications.cs index 4fced78..366fba3 100644 --- a/Foxnouns.Backend/Database/Migrations/20240528125310_AddApplications.cs +++ b/Foxnouns.Backend/Database/Migrations/20240528125310_AddApplications.cs @@ -26,7 +26,7 @@ namespace Foxnouns.Backend.Database.Migrations table: "tokens", type: "bytea", nullable: false, - defaultValue: Array.Empty() + defaultValue: new byte[0] ); migrationBuilder.CreateTable( @@ -40,7 +40,10 @@ namespace Foxnouns.Backend.Database.Migrations scopes = table.Column(type: "text[]", nullable: false), redirect_uris = table.Column(type: "text[]", nullable: false), }, - constraints: table => table.PrimaryKey("pk_applications", x => x.id) + constraints: table => + { + table.PrimaryKey("pk_applications", x => x.id); + } ); migrationBuilder.CreateIndex( diff --git a/Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.cs b/Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.cs index 931e8ab..7d131da 100644 --- a/Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.cs +++ b/Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.cs @@ -32,7 +32,10 @@ namespace Foxnouns.Backend.Database.Migrations nullable: false ), }, - constraints: table => table.PrimaryKey("pk_temporary_keys", x => x.id) + constraints: table => + { + table.PrimaryKey("pk_temporary_keys", x => x.id); + } ); migrationBuilder.CreateIndex( diff --git a/Foxnouns.Backend/Database/Models/Application.cs b/Foxnouns.Backend/Database/Models/Application.cs index ed8929f..90787de 100644 --- a/Foxnouns.Backend/Database/Models/Application.cs +++ b/Foxnouns.Backend/Database/Models/Application.cs @@ -18,8 +18,8 @@ public class Application : BaseModel string[] redirectUrls ) { - string clientId = RandomNumberGenerator.GetHexString(32, true); - string clientSecret = AuthUtils.RandomToken(); + var clientId = RandomNumberGenerator.GetHexString(32, true); + var clientSecret = AuthUtils.RandomToken(); if (scopes.Except(AuthUtils.ApplicationScopes).Any()) { diff --git a/Foxnouns.Backend/Database/Snowflake.cs b/Foxnouns.Backend/Database/Snowflake.cs index 07a37a4..1294dd4 100644 --- a/Foxnouns.Backend/Database/Snowflake.cs +++ b/Foxnouns.Backend/Database/Snowflake.cs @@ -59,7 +59,7 @@ public readonly struct Snowflake(ulong value) : IEquatable public static bool TryParse(string input, [NotNullWhen(true)] out Snowflake? snowflake) { snowflake = null; - if (!ulong.TryParse(input, out ulong res)) + if (!ulong.TryParse(input, out var res)) return false; snowflake = new Snowflake(res); return true; @@ -70,7 +70,10 @@ public readonly struct Snowflake(ulong value) : IEquatable public override bool Equals(object? obj) => obj is Snowflake other && Value == other.Value; - public bool Equals(Snowflake other) => Value == other.Value; + public bool Equals(Snowflake other) + { + return Value == other.Value; + } public override int GetHashCode() => Value.GetHashCode(); @@ -80,7 +83,11 @@ public readonly struct Snowflake(ulong value) : IEquatable /// An Entity Framework ValueConverter for Snowflakes to longs. /// // ReSharper disable once ClassNeverInstantiated.Global - public class ValueConverter() : ValueConverter(x => x, x => x); + public class ValueConverter() + : ValueConverter( + convertToProviderExpression: x => x, + convertFromProviderExpression: x => x + ); private class JsonConverter : JsonConverter { @@ -99,7 +106,10 @@ public readonly struct Snowflake(ulong value) : IEquatable Snowflake existingValue, bool hasExistingValue, JsonSerializer serializer - ) => ulong.Parse((string)reader.Value!); + ) + { + return ulong.Parse((string)reader.Value!); + } } private class TypeConverter : System.ComponentModel.TypeConverter @@ -116,6 +126,9 @@ public readonly struct Snowflake(ulong value) : IEquatable ITypeDescriptorContext? context, CultureInfo? culture, object value - ) => TryParse((string)value, out Snowflake? snowflake) ? snowflake : null; + ) + { + return TryParse((string)value, out var snowflake) ? snowflake : null; + } } } diff --git a/Foxnouns.Backend/Database/SnowflakeGenerator.cs b/Foxnouns.Backend/Database/SnowflakeGenerator.cs index e532e42..446fe7b 100644 --- a/Foxnouns.Backend/Database/SnowflakeGenerator.cs +++ b/Foxnouns.Backend/Database/SnowflakeGenerator.cs @@ -28,9 +28,9 @@ public class SnowflakeGenerator : ISnowflakeGenerator public Snowflake GenerateSnowflake(Instant? time = null) { time ??= SystemClock.Instance.GetCurrentInstant(); - long increment = Interlocked.Increment(ref _increment); - int threadId = Environment.CurrentManagedThreadId % 32; - long timestamp = time.Value.ToUnixTimeMilliseconds() - Snowflake.Epoch; + var increment = Interlocked.Increment(ref _increment); + var threadId = Environment.CurrentManagedThreadId % 32; + var timestamp = time.Value.ToUnixTimeMilliseconds() - Snowflake.Epoch; return (timestamp << 22) | (uint)(_processId << 17) @@ -44,5 +44,8 @@ public static class SnowflakeGeneratorServiceExtensions public static IServiceCollection AddSnowflakeGenerator( this IServiceCollection services, int? processId = null - ) => services.AddSingleton(new SnowflakeGenerator(processId)); + ) + { + return services.AddSingleton(new SnowflakeGenerator(processId)); + } } diff --git a/Foxnouns.Backend/ExpectedError.cs b/Foxnouns.Backend/ExpectedError.cs index c8d4d44..9d30a43 100644 --- a/Foxnouns.Backend/ExpectedError.cs +++ b/Foxnouns.Backend/ExpectedError.cs @@ -35,13 +35,13 @@ public class ApiError( public readonly ErrorCode ErrorCode = errorCode ?? ErrorCode.InternalServerError; public class Unauthorized(string message, ErrorCode errorCode = ErrorCode.AuthenticationError) - : ApiError(message, HttpStatusCode.Unauthorized, errorCode); + : ApiError(message, statusCode: HttpStatusCode.Unauthorized, errorCode: errorCode); public class Forbidden( string message, IEnumerable? scopes = null, ErrorCode errorCode = ErrorCode.Forbidden - ) : ApiError(message, HttpStatusCode.Forbidden, errorCode) + ) : ApiError(message, statusCode: HttpStatusCode.Forbidden, errorCode: errorCode) { public readonly string[] Scopes = scopes?.ToArray() ?? []; } @@ -49,7 +49,7 @@ public class ApiError( public class BadRequest( string message, IReadOnlyDictionary>? errors = null - ) : ApiError(message, HttpStatusCode.BadRequest) + ) : ApiError(message, statusCode: HttpStatusCode.BadRequest) { public BadRequest(string message, string field, object? actualValue) : this( @@ -72,7 +72,7 @@ public class ApiError( return o; var a = new JArray(); - foreach (KeyValuePair> error in errors) + foreach (var error in errors) { var errorObj = new JObject { @@ -92,7 +92,7 @@ public class ApiError( /// Any other methods should use instead. /// public class AspBadRequest(string message, ModelStateDictionary? modelState = null) - : ApiError(message, HttpStatusCode.BadRequest) + : ApiError(message, statusCode: HttpStatusCode.BadRequest) { public JObject ToJson() { @@ -106,11 +106,7 @@ public class ApiError( return o; var a = new JArray(); - foreach ( - KeyValuePair error in modelState.Where(e => - e.Value is { Errors.Count: > 0 } - ) - ) + foreach (var error in modelState.Where(e => e.Value is { Errors.Count: > 0 })) { var errorObj = new JObject { @@ -134,9 +130,10 @@ public class ApiError( } public class NotFound(string message, ErrorCode? code = null) - : ApiError(message, HttpStatusCode.NotFound, code); + : ApiError(message, statusCode: HttpStatusCode.NotFound, errorCode: code); - public class AuthenticationError(string message) : ApiError(message, HttpStatusCode.BadRequest); + public class AuthenticationError(string message) + : ApiError(message, statusCode: HttpStatusCode.BadRequest); } public enum ErrorCode @@ -178,27 +175,33 @@ public class ValidationError int minLength, int maxLength, int actualLength - ) => - new() + ) + { + return new ValidationError { Message = message, MinLength = minLength, MaxLength = maxLength, ActualLength = actualLength, }; + } public static ValidationError DisallowedValueError( string message, IEnumerable allowedValues, object actualValue - ) => - new() + ) + { + return new ValidationError { Message = message, AllowedValues = allowedValues, ActualValue = actualValue, }; + } - public static ValidationError GenericValidationError(string message, object? actualValue) => - new() { Message = message, ActualValue = actualValue }; + public static ValidationError GenericValidationError(string message, object? actualValue) + { + return new ValidationError { Message = message, ActualValue = actualValue }; + } } diff --git a/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs b/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs index f87aa0e..2126610 100644 --- a/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs +++ b/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs @@ -47,13 +47,13 @@ public static class ImageObjectExtensions if (!uri.StartsWith("data:image/")) throw new ArgumentException("Not a data URI", nameof(uri)); - string[] split = uri.Remove(0, "data:".Length).Split(";base64,"); - string contentType = split[0]; - string encoded = split[1]; + var split = uri.Remove(0, "data:".Length).Split(";base64,"); + var contentType = split[0]; + var encoded = split[1]; if (!ValidContentTypes.Contains(contentType)) throw new ArgumentException("Invalid content type for image", nameof(uri)); - if (!AuthUtils.TryFromBase64String(encoded, out byte[]? rawImage)) + if (!AuthUtils.TryFromBase64String(encoded, out var rawImage)) throw new ArgumentException("Invalid base64 string", nameof(uri)); var image = Image.Load(rawImage); @@ -74,7 +74,7 @@ public static class ImageObjectExtensions await image.SaveAsync(stream, new WebpEncoder { Quality = 95, NearLossless = false }); stream.Seek(0, SeekOrigin.Begin); - string hash = Convert.ToHexString(await SHA256.HashDataAsync(stream)).ToLower(); + var hash = Convert.ToHexString(await SHA256.HashDataAsync(stream)).ToLower(); stream.Seek(0, SeekOrigin.Begin); return (hash, stream); diff --git a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs index fa3a3d2..f3e1467 100644 --- a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs +++ b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs @@ -14,7 +14,7 @@ public static class KeyCacheExtensions CancellationToken ct = default ) { - string state = AuthUtils.RandomToken().Replace('+', '-').Replace('/', '_'); + var state = AuthUtils.RandomToken().Replace('+', '-').Replace('/', '_'); await keyCacheService.SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10), ct); return state; } @@ -25,7 +25,7 @@ public static class KeyCacheExtensions CancellationToken ct = default ) { - string? val = await keyCacheService.GetKeyAsync($"oauth_state:{state}", ct: ct); + var val = await keyCacheService.GetKeyAsync($"oauth_state:{state}", delete: true, ct); if (val == null) throw new ApiError.BadRequest("Invalid OAuth state"); } @@ -38,7 +38,7 @@ public static class KeyCacheExtensions ) { // This state is used in links, not just as JSON values, so make it URL-safe - string state = AuthUtils.RandomToken().Replace('+', '-').Replace('/', '_'); + var state = AuthUtils.RandomToken().Replace('+', '-').Replace('/', '_'); await keyCacheService.SetKeyAsync( $"email_state:{state}", new RegisterEmailState(email, userId), @@ -52,7 +52,12 @@ public static class KeyCacheExtensions this KeyCacheService keyCacheService, string state, CancellationToken ct = default - ) => await keyCacheService.GetKeyAsync($"email_state:{state}", ct: ct); + ) => + await keyCacheService.GetKeyAsync( + $"email_state:{state}", + delete: true, + ct + ); public static async Task GenerateAddExtraAccountStateAsync( this KeyCacheService keyCacheService, @@ -62,7 +67,7 @@ public static class KeyCacheExtensions CancellationToken ct = default ) { - string state = AuthUtils.RandomToken(); + var state = AuthUtils.RandomToken(); await keyCacheService.SetKeyAsync( $"add_account:{state}", new AddExtraAccountState(authType, userId, instance), @@ -76,7 +81,12 @@ public static class KeyCacheExtensions this KeyCacheService keyCacheService, string state, CancellationToken ct = default - ) => await keyCacheService.GetKeyAsync($"add_account:{state}", true, ct); + ) => + await keyCacheService.GetKeyAsync( + $"add_account:{state}", + delete: true, + ct + ); } public record RegisterEmailState( diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 892eef6..f71cbc2 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -24,9 +24,9 @@ public static class WebApplicationExtensions /// public static WebApplicationBuilder AddSerilog(this WebApplicationBuilder builder) { - Config config = builder.Configuration.Get() ?? new Config(); + var config = builder.Configuration.Get() ?? new(); - LoggerConfiguration logCfg = new LoggerConfiguration() + var logCfg = new LoggerConfiguration() .Enrich.FromLogContext() .MinimumLevel.Is(config.Logging.LogEventLevel) // ASP.NET's built in request logs are extremely verbose, so we use Serilog's instead. @@ -43,7 +43,10 @@ public static class WebApplicationExtensions if (config.Logging.SeqLogUrl != null) { - logCfg.WriteTo.Seq(config.Logging.SeqLogUrl, LogEventLevel.Verbose); + logCfg.WriteTo.Seq( + config.Logging.SeqLogUrl, + restrictedToMinimumLevel: LogEventLevel.Verbose + ); } // AddSerilog doesn't seem to add an ILogger to the service collection, so add that manually. @@ -57,19 +60,19 @@ public static class WebApplicationExtensions builder.Configuration.Sources.Clear(); builder.Configuration.AddConfiguration(); - Config config = builder.Configuration.Get() ?? new Config(); + var config = builder.Configuration.Get() ?? new(); builder.Services.AddSingleton(config); return config; } public static IConfigurationBuilder AddConfiguration(this IConfigurationBuilder builder) { - string file = Environment.GetEnvironmentVariable("FOXNOUNS_CONFIG_FILE") ?? "config.ini"; + var file = Environment.GetEnvironmentVariable("FOXNOUNS_CONFIG_FILE") ?? "config.ini"; return builder .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appSettings.json", true) - .AddIniFile(file, false, true) + .AddIniFile(file, optional: false, reloadOnChange: true) .AddEnvironmentVariables(); } @@ -139,15 +142,11 @@ public static class WebApplicationExtensions app.Services.ConfigureQueue() .LogQueuedTaskProgress(app.Services.GetRequiredService>()); - await using AsyncServiceScope scope = app.Services.CreateAsyncScope(); - - // The types of these variables are obvious from the methods being called to create them - // ReSharper disable SuggestVarOrType_SimpleTypes + await using var scope = app.Services.CreateAsyncScope(); var logger = scope .ServiceProvider.GetRequiredService() .ForContext(); var db = scope.ServiceProvider.GetRequiredService(); - // ReSharper restore SuggestVarOrType_SimpleTypes logger.Information( "Starting Foxnouns.NET {Version} ({Hash})", diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 794f2b9..d604012 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -30,10 +30,6 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs b/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs index 8e34d01..dea6fcf 100644 --- a/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs +++ b/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs @@ -40,7 +40,7 @@ public class CreateDataExportInvocable( private async Task InvokeAsync() { - User? user = await db + var user = await db .Users.Include(u => u.AuthMethods) .Include(u => u.Flags) .Include(u => u.ProfileFlags) @@ -57,7 +57,7 @@ public class CreateDataExportInvocable( _logger.Information("Generating data export for user {UserId}", user.Id); - await using var stream = new MemoryStream(); + using var stream = new MemoryStream(); using var zip = new ZipArchive(stream, ZipArchiveMode.Create, true); zip.Comment = $"This archive for {user.Username} ({user.Id}) was generated at {InstantPattern.General.Format(clock.GetCurrentInstant())}"; @@ -66,19 +66,25 @@ public class CreateDataExportInvocable( WriteJson( zip, "user.json", - await userRenderer.RenderUserInnerAsync(user, true, ["*"], false, true) + await userRenderer.RenderUserInnerAsync( + user, + true, + ["*"], + renderMembers: false, + renderAuthMethods: true + ) ); await WriteS3Object(zip, "user-avatar.webp", userRenderer.AvatarUrlFor(user)); - foreach (PrideFlag? flag in user.Flags) + foreach (var flag in user.Flags) await WritePrideFlag(zip, flag); - List members = await db + var members = await db .Members.Include(m => m.User) .Include(m => m.ProfileFlags) .Where(m => m.UserId == user.Id) .ToListAsync(); - foreach (Member? member in members) + foreach (var member in members) await WriteMember(zip, member); // We want to dispose the ZipArchive on an error, but we need to dispose it manually to upload to object storage. @@ -88,7 +94,7 @@ public class CreateDataExportInvocable( stream.Seek(0, SeekOrigin.Begin); // Upload the file! - string filename = AuthUtils.RandomToken().Replace('+', '-').Replace('/', '_'); + var filename = AuthUtils.RandomToken().Replace('+', '-').Replace('/', '_'); await objectStorageService.PutObjectAsync( ExportPath(user.Id, filename), stream, @@ -126,8 +132,8 @@ public class CreateDataExportInvocable( return; } - ZipArchiveEntry entry = zip.CreateEntry($"flag-{flag.Id}/flag.txt"); - await using Stream stream = entry.Open(); + var entry = zip.CreateEntry($"flag-{flag.Id}/flag.txt"); + await using var stream = entry.Open(); await using var writer = new StreamWriter(stream); await writer.WriteAsync(flagData); } @@ -158,7 +164,7 @@ public class CreateDataExportInvocable( private void WriteJson(ZipArchive zip, string filename, object data) { - string json = JsonConvert.SerializeObject(data, Formatting.Indented); + var json = JsonConvert.SerializeObject(data, Formatting.Indented); _logger.Debug( "Writing file {Filename} to archive with size {Length}", @@ -166,8 +172,8 @@ public class CreateDataExportInvocable( json.Length ); - ZipArchiveEntry entry = zip.CreateEntry(filename); - using Stream stream = entry.Open(); + var entry = zip.CreateEntry(filename); + using var stream = entry.Open(); using var writer = new StreamWriter(stream); writer.Write(json); } @@ -177,14 +183,14 @@ public class CreateDataExportInvocable( if (s3Path == null) return; - HttpResponseMessage resp = await Client.GetAsync(s3Path); + var resp = await Client.GetAsync(s3Path); if (resp.StatusCode != HttpStatusCode.OK) { _logger.Warning("S3 path {S3Path} returned a non-200 status, not saving file", s3Path); return; } - await using Stream respStream = await resp.Content.ReadAsStreamAsync(); + await using var respStream = await resp.Content.ReadAsStreamAsync(); _logger.Debug( "Writing file {Filename} to archive with size {Length}", @@ -192,8 +198,8 @@ public class CreateDataExportInvocable( respStream.Length ); - ZipArchiveEntry entry = zip.CreateEntry(filename); - await using Stream entryStream = entry.Open(); + var entry = zip.CreateEntry(filename); + await using var entryStream = entry.Open(); respStream.Seek(0, SeekOrigin.Begin); await respStream.CopyToAsync(entryStream); diff --git a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs index 5c5df2d..e7ce0e3 100644 --- a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs +++ b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs @@ -26,10 +26,10 @@ public class CreateFlagInvocable( try { - (string? hash, Stream? image) = await ImageObjectExtensions.ConvertBase64UriToImage( + var (hash, image) = await ImageObjectExtensions.ConvertBase64UriToImage( Payload.ImageData, - 256, - false + size: 256, + crop: false ); await objectStorageService.PutObjectAsync(Path(hash), image, "image/webp"); diff --git a/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs index 0a9792e..d2fd9ec 100644 --- a/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs +++ b/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs @@ -1,6 +1,5 @@ using Coravel.Invocable; using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Services; @@ -27,7 +26,7 @@ public class MemberAvatarUpdateInvocable( { _logger.Debug("Updating avatar for member {MemberId}", id); - Member? member = await db.Members.FindAsync(id); + var member = await db.Members.FindAsync(id); if (member == null) { _logger.Warning( @@ -39,12 +38,12 @@ public class MemberAvatarUpdateInvocable( try { - (string? hash, Stream? image) = await ImageObjectExtensions.ConvertBase64UriToImage( + var (hash, image) = await ImageObjectExtensions.ConvertBase64UriToImage( newAvatar, - 512, - true + size: 512, + crop: true ); - string? prevHash = member.Avatar; + var prevHash = member.Avatar; await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp"); @@ -70,7 +69,7 @@ public class MemberAvatarUpdateInvocable( { _logger.Debug("Clearing avatar for member {MemberId}", id); - Member? member = await db.Members.FindAsync(id); + var member = await db.Members.FindAsync(id); if (member == null) { _logger.Warning( diff --git a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs index 014fbed..f212cc3 100644 --- a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs +++ b/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs @@ -1,6 +1,5 @@ using Coravel.Invocable; using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Services; @@ -27,7 +26,7 @@ public class UserAvatarUpdateInvocable( { _logger.Debug("Updating avatar for user {MemberId}", id); - User? user = await db.Users.FindAsync(id); + var user = await db.Users.FindAsync(id); if (user == null) { _logger.Warning( @@ -39,13 +38,13 @@ public class UserAvatarUpdateInvocable( try { - (string? hash, Stream? image) = await ImageObjectExtensions.ConvertBase64UriToImage( + var (hash, image) = await ImageObjectExtensions.ConvertBase64UriToImage( newAvatar, - 512, - true + size: 512, + crop: true ); image.Seek(0, SeekOrigin.Begin); - string? prevHash = user.Avatar; + var prevHash = user.Avatar; await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp"); @@ -71,7 +70,7 @@ public class UserAvatarUpdateInvocable( { _logger.Debug("Clearing avatar for user {MemberId}", id); - User? user = await db.Users.FindAsync(id); + var user = await db.Users.FindAsync(id); if (user == null) { _logger.Warning( diff --git a/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs index f63d711..e842627 100644 --- a/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs @@ -8,8 +8,8 @@ public class AuthenticationMiddleware(DatabaseContext db) : IMiddleware { public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { - Endpoint? endpoint = ctx.GetEndpoint(); - AuthenticateAttribute? metadata = endpoint?.Metadata.GetMetadata(); + var endpoint = ctx.GetEndpoint(); + var metadata = endpoint?.Metadata.GetMetadata(); if (metadata == null) { @@ -18,17 +18,14 @@ public class AuthenticationMiddleware(DatabaseContext db) : IMiddleware } if ( - !AuthUtils.TryParseToken( - ctx.Request.Headers.Authorization.ToString(), - out byte[]? rawToken - ) + !AuthUtils.TryParseToken(ctx.Request.Headers.Authorization.ToString(), out var rawToken) ) { await next(ctx); return; } - Token? oauthToken = await db.GetToken(rawToken); + var oauthToken = await db.GetToken(rawToken); if (oauthToken == null) { await next(ctx); @@ -53,7 +50,7 @@ public static class HttpContextExtensions public static Token? GetToken(this HttpContext ctx) { - if (ctx.Items.TryGetValue(Key, out object? token)) + if (ctx.Items.TryGetValue(Key, out var token)) return token as Token; return null; } diff --git a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs index 114e870..4fb8dd1 100644 --- a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs @@ -7,8 +7,8 @@ public class AuthorizationMiddleware : IMiddleware { public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { - Endpoint? endpoint = ctx.GetEndpoint(); - AuthorizeAttribute? attribute = endpoint?.Metadata.GetMetadata(); + var endpoint = ctx.GetEndpoint(); + var attribute = endpoint?.Metadata.GetMetadata(); if (attribute == null) { @@ -16,27 +16,21 @@ public class AuthorizationMiddleware : IMiddleware return; } - Token? token = ctx.GetToken(); + var token = ctx.GetToken(); if (token == null) - { throw new ApiError.Unauthorized( "This endpoint requires an authenticated user.", ErrorCode.AuthenticationRequired ); - } - if ( attribute.Scopes.Length > 0 && attribute.Scopes.Except(token.Scopes.ExpandScopes()).Any() ) - { throw new ApiError.Forbidden( "This endpoint requires ungranted scopes.", attribute.Scopes.Except(token.Scopes.ExpandScopes()), ErrorCode.MissingScopes ); - } - if (attribute.RequireAdmin && token.User.Role != UserRole.Admin) throw new ApiError.Forbidden("This endpoint can only be used by admins."); if ( @@ -44,9 +38,7 @@ public class AuthorizationMiddleware : IMiddleware && token.User.Role != UserRole.Admin && token.User.Role != UserRole.Moderator ) - { throw new ApiError.Forbidden("This endpoint can only be used by moderators."); - } await next(ctx); } diff --git a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs index f033ede..4c14e5f 100644 --- a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs +++ b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs @@ -1,5 +1,4 @@ using System.Net; -using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Utils; using Newtonsoft.Json; @@ -15,9 +14,9 @@ public class ErrorHandlerMiddleware(ILogger baseLogger, IHub sentry) : IMiddlewa } catch (Exception e) { - Type type = e.TargetSite?.DeclaringType ?? typeof(ErrorHandlerMiddleware); - string typeName = e.TargetSite?.DeclaringType?.FullName ?? ""; - ILogger logger = baseLogger.ForContext(type); + var type = e.TargetSite?.DeclaringType ?? typeof(ErrorHandlerMiddleware); + var typeName = e.TargetSite?.DeclaringType?.FullName ?? ""; + var logger = baseLogger.ForContext(type); if (ctx.Response.HasStarted) { @@ -32,15 +31,13 @@ public class ErrorHandlerMiddleware(ILogger baseLogger, IHub sentry) : IMiddlewa e, scope => { - User? user = ctx.GetUser(); + var user = ctx.GetUser(); if (user != null) - { scope.User = new SentryUser { Id = user.Id.ToString(), Username = user.Username, }; - } } ); @@ -101,19 +98,17 @@ public class ErrorHandlerMiddleware(ILogger baseLogger, IHub sentry) : IMiddlewa logger.Error(e, "Exception in {ClassName} ({Path})", typeName, ctx.Request.Path); } - SentryId errorId = sentry.CaptureException( + var errorId = sentry.CaptureException( e, scope => { - User? user = ctx.GetUser(); + var user = ctx.GetUser(); if (user != null) - { scope.User = new SentryUser { Id = user.Id.ToString(), Username = user.Username, }; - } } ); diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index bc56ef9..17a56d9 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -9,9 +9,9 @@ using Prometheus; using Sentry.Extensibility; using Serilog; -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); +var builder = WebApplication.CreateBuilder(args); -Config config = builder.AddConfiguration(); +var config = builder.AddConfiguration(); builder.AddSerilog(); @@ -58,7 +58,7 @@ JsonConvert.DefaultSettings = () => builder.AddServices(config).AddCustomMiddleware().AddEndpointsApiExplorer().AddSwaggerGen(); -WebApplication app = builder.Build(); +var app = builder.Build(); await app.Initialize(args); diff --git a/Foxnouns.Backend/Services/Auth/AuthService.cs b/Foxnouns.Backend/Services/Auth/AuthService.cs index 221bb58..e3ec4c4 100644 --- a/Foxnouns.Backend/Services/Auth/AuthService.cs +++ b/Foxnouns.Backend/Services/Auth/AuthService.cs @@ -31,16 +31,6 @@ public class AuthService( CancellationToken ct = default ) { - // Validate username and whether it's not taken - ValidationUtils.Validate( - [ - ("username", ValidationUtils.ValidateUsername(username)), - ("password", ValidationUtils.ValidatePassword(password)), - ] - ); - if (await db.Users.AnyAsync(u => u.Username == username, ct)) - throw new ApiError.BadRequest("Username is already taken", "username", username); - var user = new User { Id = snowflakeGenerator.GenerateSnowflake(), @@ -59,7 +49,7 @@ public class AuthService( }; db.Add(user); - user.Password = await HashPasswordAsync(user, password, ct); + user.Password = await Task.Run(() => _passwordHasher.HashPassword(user, password), ct); return user; } @@ -80,8 +70,6 @@ public class AuthService( { AssertValidAuthType(authType, instance); - // Validate username and whether it's not taken - ValidationUtils.Validate([("username", ValidationUtils.ValidateUsername(username))]); if (await db.Users.AnyAsync(u => u.Username == username, ct)) throw new ApiError.BadRequest("Username is already taken", "username", username); @@ -123,30 +111,28 @@ public class AuthService( CancellationToken ct = default ) { - User? user = await db.Users.FirstOrDefaultAsync( + var user = await db.Users.FirstOrDefaultAsync( u => u.AuthMethods.Any(a => a.AuthType == AuthType.Email && a.RemoteId == email), ct ); if (user == null) - { throw new ApiError.NotFound( "No user with that email address found, or password is incorrect", ErrorCode.UserNotFound ); - } - PasswordVerificationResult pwResult = await VerifyHashedPasswordAsync(user, password, ct); + var pwResult = await Task.Run( + () => _passwordHasher.VerifyHashedPassword(user, user.Password!, password), + ct + ); if (pwResult == PasswordVerificationResult.Failed) // TODO: this seems to fail on some valid passwords? - { throw new ApiError.NotFound( "No user with that email address found, or password is incorrect", ErrorCode.UserNotFound ); - } - if (pwResult == PasswordVerificationResult.SuccessRehashNeeded) { - user.Password = await HashPasswordAsync(user, password, ct); + user.Password = await Task.Run(() => _passwordHasher.HashPassword(user, password), ct); await db.SaveChangesAsync(ct); } @@ -174,7 +160,10 @@ public class AuthService( throw new FoxnounsError("Password for user supplied to ValidatePasswordAsync was null"); } - PasswordVerificationResult pwResult = await VerifyHashedPasswordAsync(user, password, ct); + var pwResult = await Task.Run( + () => _passwordHasher.VerifyHashedPassword(user, user.Password!, password), + ct + ); return pwResult is PasswordVerificationResult.SuccessRehashNeeded or PasswordVerificationResult.Success; @@ -189,7 +178,7 @@ public class AuthService( CancellationToken ct = default ) { - user.Password = await HashPasswordAsync(user, password, ct); + user.Password = await Task.Run(() => _passwordHasher.HashPassword(user, password), ct); db.Update(user); } @@ -236,15 +225,13 @@ public class AuthService( AssertValidAuthType(authType, app); // This is already checked when - int currentCount = await db + var currentCount = await db .AuthMethods.Where(m => m.UserId == userId && m.AuthType == authType) .CountAsync(ct); if (currentCount >= AuthUtils.MaxAuthMethodsPerType) - { throw new ApiError.BadRequest( "Too many linked accounts of this type, maximum of 3 per account." ); - } var authMethod = new AuthMethod { @@ -269,15 +256,13 @@ public class AuthService( ) { if (!AuthUtils.ValidateScopes(application, scopes)) - { throw new ApiError.BadRequest( "Invalid scopes requested for this token", "scopes", scopes ); - } - (string? token, byte[]? hash) = GenerateToken(); + var (token, hash) = GenerateToken(); return ( token, new Token @@ -302,9 +287,9 @@ public class AuthService( CancellationToken ct = default ) { - Application frontendApp = await db.GetFrontendApplicationAsync(ct); + var frontendApp = await db.GetFrontendApplicationAsync(ct); - (string? tokenStr, Token? token) = GenerateToken( + var (tokenStr, token) = GenerateToken( user, frontendApp, ["*"], @@ -317,35 +302,24 @@ public class AuthService( await db.SaveChangesAsync(ct); return new CallbackResponse( - true, - null, - null, - await userRenderer.RenderUserAsync(user, user, renderMembers: false, ct: ct), - tokenStr, - token.ExpiresAt + HasAccount: true, + Ticket: null, + RemoteUsername: null, + User: await userRenderer.RenderUserAsync( + user, + selfUser: user, + renderMembers: false, + ct: ct + ), + Token: tokenStr, + ExpiresAt: token.ExpiresAt ); } - private Task HashPasswordAsync( - User user, - string password, - CancellationToken ct = default - ) => Task.Run(() => _passwordHasher.HashPassword(user, password), ct); - - private Task VerifyHashedPasswordAsync( - User user, - string providedPassword, - CancellationToken ct = default - ) => - Task.Run( - () => _passwordHasher.VerifyHashedPassword(user, user.Password!, providedPassword), - ct - ); - private static (string, byte[]) GenerateToken() { - string token = AuthUtils.RandomToken(); - byte[] hash = SHA512.HashData(Convert.FromBase64String(token)); + var token = AuthUtils.RandomToken(); + var hash = SHA512.HashData(Convert.FromBase64String(token)); return (token, hash); } diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs index 29c27b6..ba232bf 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs @@ -18,25 +18,22 @@ public partial class FediverseAuthService Snowflake? existingAppId = null ) { - HttpResponseMessage resp = await _client.PostAsJsonAsync( + var resp = await _client.PostAsJsonAsync( $"https://{instance}/api/v1/apps", new CreateMastodonApplicationRequest( - $"pronouns.cc (+{_config.BaseUrl})", - MastodonRedirectUri(instance), - "read read:accounts", - _config.BaseUrl + ClientName: $"pronouns.cc (+{_config.BaseUrl})", + RedirectUris: MastodonRedirectUri(instance), + Scopes: "read read:accounts", + Website: _config.BaseUrl ) ); resp.EnsureSuccessStatusCode(); - PartialMastodonApplication? mastodonApp = - await resp.Content.ReadFromJsonAsync(); + var mastodonApp = await resp.Content.ReadFromJsonAsync(); if (mastodonApp == null) - { throw new FoxnounsError( $"Application created on Mastodon-compatible instance {instance} was null" ); - } FediverseApplication app; @@ -78,7 +75,7 @@ public partial class FediverseAuthService if (state != null) await _keyCacheService.ValidateAuthStateAsync(state); - HttpResponseMessage tokenResp = await _client.PostAsync( + var tokenResp = await _client.PostAsync( MastodonTokenUri(app.Domain), new FormUrlEncodedContent( new Dictionary @@ -98,7 +95,7 @@ public partial class FediverseAuthService } tokenResp.EnsureSuccessStatusCode(); - string? token = ( + var token = ( await tokenResp.Content.ReadFromJsonAsync() )?.AccessToken; if (token == null) @@ -109,9 +106,9 @@ public partial class FediverseAuthService var req = new HttpRequestMessage(HttpMethod.Get, MastodonCurrentUserUri(app.Domain)); req.Headers.Add("Authorization", $"Bearer {token}"); - HttpResponseMessage currentUserResp = await _client.SendAsync(req); + var currentUserResp = await _client.SendAsync(req); currentUserResp.EnsureSuccessStatusCode(); - FediverseUser? user = await currentUserResp.Content.ReadFromJsonAsync(); + var user = await currentUserResp.Content.ReadFromJsonAsync(); if (user == null) { throw new FoxnounsError($"User response from instance {app.Domain} was invalid"); @@ -134,7 +131,7 @@ public partial class FediverseAuthService "An app credentials refresh was requested for {ApplicationId}, creating a new application", app.Id ); - app = await CreateMastodonApplicationAsync(app.Domain, app.Id); + app = await CreateMastodonApplicationAsync(app.Domain, existingAppId: app.Id); } state ??= HttpUtility.UrlEncode(await _keyCacheService.GenerateAuthStateAsync()); diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs index 43dab01..224c0a3 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs @@ -43,7 +43,7 @@ public partial class FediverseAuthService string? state = null ) { - FediverseApplication app = await GetApplicationAsync(instance); + var app = await GetApplicationAsync(instance); return await GenerateAuthUrlAsync(app, forceRefresh, state); } @@ -56,15 +56,13 @@ public partial class FediverseAuthService public async Task GetApplicationAsync(string instance) { - FediverseApplication? app = await _db.FediverseApplications.FirstOrDefaultAsync(a => - a.Domain == instance - ); + var app = await _db.FediverseApplications.FirstOrDefaultAsync(a => a.Domain == instance); if (app != null) return app; _logger.Debug("No application for fediverse instance {Instance}, creating it", instance); - string softwareName = await GetSoftwareNameAsync(instance); + var softwareName = await GetSoftwareNameAsync(instance); if (IsMastodonCompatible(softwareName)) { @@ -78,14 +76,13 @@ public partial class FediverseAuthService { _logger.Debug("Requesting software name for fediverse instance {Instance}", instance); - HttpResponseMessage wellKnownResp = await _client.GetAsync( + var wellKnownResp = await _client.GetAsync( new Uri($"https://{instance}/.well-known/nodeinfo") ); wellKnownResp.EnsureSuccessStatusCode(); - WellKnownResponse? wellKnown = - await wellKnownResp.Content.ReadFromJsonAsync(); - string? nodeInfoUrl = wellKnown?.Links.FirstOrDefault(l => l.Rel == NodeInfoRel)?.Href; + var wellKnown = await wellKnownResp.Content.ReadFromJsonAsync(); + var nodeInfoUrl = wellKnown?.Links.FirstOrDefault(l => l.Rel == NodeInfoRel)?.Href; if (nodeInfoUrl == null) { throw new FoxnounsError( @@ -93,10 +90,10 @@ public partial class FediverseAuthService ); } - HttpResponseMessage nodeInfoResp = await _client.GetAsync(nodeInfoUrl); + var nodeInfoResp = await _client.GetAsync(nodeInfoUrl); nodeInfoResp.EnsureSuccessStatusCode(); - PartialNodeInfo? nodeInfo = await nodeInfoResp.Content.ReadFromJsonAsync(); + var nodeInfo = await nodeInfoResp.Content.ReadFromJsonAsync(); return nodeInfo?.Software.Name ?? throw new FoxnounsError( $"Nodeinfo response for instance {instance} was invalid, no software name" diff --git a/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs index c3ca685..9b62a70 100644 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs +++ b/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs @@ -29,7 +29,7 @@ public class RemoteAuthService( ) { var redirectUri = $"{config.BaseUrl}/auth/callback/discord"; - HttpResponseMessage resp = await _httpClient.PostAsync( + var resp = await _httpClient.PostAsync( _discordTokenUri, new FormUrlEncodedContent( new Dictionary @@ -45,7 +45,7 @@ public class RemoteAuthService( ); if (!resp.IsSuccessStatusCode) { - string respBody = await resp.Content.ReadAsStringAsync(ct); + var respBody = await resp.Content.ReadAsStringAsync(ct); _logger.Error( "Received error status {StatusCode} when exchanging OAuth token: {ErrorBody}", (int)resp.StatusCode, @@ -55,18 +55,16 @@ public class RemoteAuthService( } resp.EnsureSuccessStatusCode(); - DiscordTokenResponse? token = await resp.Content.ReadFromJsonAsync( - ct - ); + var token = await resp.Content.ReadFromJsonAsync(ct); if (token == null) throw new FoxnounsError("Discord token response was null"); var req = new HttpRequestMessage(HttpMethod.Get, _discordUserUri); req.Headers.Add("Authorization", $"{token.token_type} {token.access_token}"); - HttpResponseMessage resp2 = await _httpClient.SendAsync(req, ct); + var resp2 = await _httpClient.SendAsync(req, ct); resp2.EnsureSuccessStatusCode(); - DiscordUserResponse? user = await resp2.Content.ReadFromJsonAsync(ct); + var user = await resp2.Content.ReadFromJsonAsync(ct); if (user == null) throw new FoxnounsError("Discord user response was null"); @@ -106,7 +104,7 @@ public class RemoteAuthService( string? instance = null ) { - int existingAccounts = await db + var existingAccounts = await db .AuthMethods.Where(m => m.UserId == userId && m.AuthType == authType) .CountAsync(); if (existingAccounts > AuthUtils.MaxAuthMethodsPerType) @@ -133,17 +131,13 @@ public class RemoteAuthService( string? instance = null ) { - AddExtraAccountState? accountState = await keyCacheService.GetAddExtraAccountStateAsync( - state - ); + var accountState = await keyCacheService.GetAddExtraAccountStateAsync(state); if ( accountState == null || accountState.AuthType != authType || accountState.UserId != userId || (instance != null && accountState.Instance != instance) ) - { throw new ApiError.BadRequest("Invalid state", "state", state); - } } } diff --git a/Foxnouns.Backend/Services/DataCleanupService.cs b/Foxnouns.Backend/Services/DataCleanupService.cs index b9d3afe..b89a399 100644 --- a/Foxnouns.Backend/Services/DataCleanupService.cs +++ b/Foxnouns.Backend/Services/DataCleanupService.cs @@ -28,9 +28,9 @@ public class DataCleanupService( private async Task CleanUsersAsync(CancellationToken ct = default) { - Instant selfDeleteExpires = clock.GetCurrentInstant() - User.DeleteAfter; - Instant suspendExpires = clock.GetCurrentInstant() - User.DeleteSuspendedAfter; - List users = await db + var selfDeleteExpires = clock.GetCurrentInstant() - User.DeleteAfter; + var suspendExpires = clock.GetCurrentInstant() - User.DeleteSuspendedAfter; + var users = await db .Users.Include(u => u.Members) .Include(u => u.DataExports) .Where(u => @@ -92,15 +92,13 @@ public class DataCleanupService( private async Task CleanExportsAsync(CancellationToken ct = default) { var minExpiredId = Snowflake.FromInstant(clock.GetCurrentInstant() - DataExport.Expiration); - List exports = await db - .DataExports.Where(d => d.Id < minExpiredId) - .ToListAsync(ct); + var exports = await db.DataExports.Where(d => d.Id < minExpiredId).ToListAsync(ct); if (exports.Count == 0) return; _logger.Debug("Deleting {Count} expired exports", exports.Count); - foreach (DataExport? export in exports) + foreach (var export in exports) { _logger.Debug("Deleting export {ExportId}", export.Id); await objectStorageService.RemoveObjectAsync( diff --git a/Foxnouns.Backend/Services/KeyCacheService.cs b/Foxnouns.Backend/Services/KeyCacheService.cs index 9d048f1..ef60e8e 100644 --- a/Foxnouns.Backend/Services/KeyCacheService.cs +++ b/Foxnouns.Backend/Services/KeyCacheService.cs @@ -41,7 +41,7 @@ public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger) CancellationToken ct = default ) { - TemporaryKey? value = await db.TemporaryKeys.FirstOrDefaultAsync(k => k.Key == key, ct); + var value = await db.TemporaryKeys.FirstOrDefaultAsync(k => k.Key == key, ct); if (value == null) return null; @@ -56,7 +56,7 @@ public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger) public async Task DeleteExpiredKeysAsync(CancellationToken ct) { - int count = await db + var count = await db .TemporaryKeys.Where(k => k.Expires < clock.GetCurrentInstant()) .ExecuteDeleteAsync(ct); if (count != 0) @@ -79,7 +79,7 @@ public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger) ) where T : class { - string value = JsonConvert.SerializeObject(obj); + var value = JsonConvert.SerializeObject(obj); await SetKeyAsync(key, value, expires, ct); } @@ -90,7 +90,7 @@ public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger) ) where T : class { - string? value = await GetKeyAsync(key, delete, ct); + var value = await GetKeyAsync(key, delete, ct); return value == null ? default : JsonConvert.DeserializeObject(value); } } diff --git a/Foxnouns.Backend/Services/MemberRendererService.cs b/Foxnouns.Backend/Services/MemberRendererService.cs index ea93e4d..336d189 100644 --- a/Foxnouns.Backend/Services/MemberRendererService.cs +++ b/Foxnouns.Backend/Services/MemberRendererService.cs @@ -10,11 +10,11 @@ public class MemberRendererService(DatabaseContext db, Config config) { public async Task> RenderUserMembersAsync(User user, Token? token) { - bool canReadHiddenMembers = + var canReadHiddenMembers = token != null && token.UserId == user.Id && token.HasScope("member.read"); - bool renderUnlisted = + var renderUnlisted = token != null && token.UserId == user.Id && token.HasScope("user.read_hidden"); - bool canReadMemberList = !user.ListHidden || canReadHiddenMembers; + var canReadMemberList = !user.ListHidden || canReadHiddenMembers; IEnumerable members = canReadMemberList ? await db.Members.Where(m => m.UserId == user.Id).OrderBy(m => m.Name).ToListAsync() @@ -30,7 +30,7 @@ public class MemberRendererService(DatabaseContext db, Config config) string? overrideSid = null ) { - bool renderUnlisted = token?.UserId == member.UserId && token.HasScope("user.read_hidden"); + var renderUnlisted = token?.UserId == member.UserId && token.HasScope("user.read_hidden"); return new MemberResponse( member.Id, diff --git a/Foxnouns.Backend/Services/MetricsCollectionService.cs b/Foxnouns.Backend/Services/MetricsCollectionService.cs index fdd888e..66172a6 100644 --- a/Foxnouns.Backend/Services/MetricsCollectionService.cs +++ b/Foxnouns.Backend/Services/MetricsCollectionService.cs @@ -3,7 +3,6 @@ using Foxnouns.Backend.Database; using Microsoft.EntityFrameworkCore; using NodaTime; using Prometheus; -using ITimer = Prometheus.ITimer; namespace Foxnouns.Backend.Services; @@ -17,23 +16,19 @@ public class MetricsCollectionService(ILogger logger, IServiceProvider services, public async Task CollectMetricsAsync(CancellationToken ct = default) { - ITimer timer = FoxnounsMetrics.MetricsCollectionTime.NewTimer(); - Instant now = clock.GetCurrentInstant(); + var timer = FoxnounsMetrics.MetricsCollectionTime.NewTimer(); + var now = clock.GetCurrentInstant(); - await using AsyncServiceScope scope = services.CreateAsyncScope(); - // ReSharper disable once SuggestVarOrType_SimpleTypes + await using var scope = services.CreateAsyncScope(); await using var db = scope.ServiceProvider.GetRequiredService(); - List? users = await db - .Users.Where(u => !u.Deleted) - .Select(u => u.LastActive) - .ToListAsync(ct); + var users = await db.Users.Where(u => !u.Deleted).Select(u => u.LastActive).ToListAsync(ct); FoxnounsMetrics.UsersCount.Set(users.Count); FoxnounsMetrics.UsersActiveMonthCount.Set(users.Count(i => i > now - Month)); FoxnounsMetrics.UsersActiveWeekCount.Set(users.Count(i => i > now - Week)); FoxnounsMetrics.UsersActiveDayCount.Set(users.Count(i => i > now - Day)); - int memberCount = await db + var memberCount = await db .Members.Include(m => m.User) .Where(m => !m.Unlisted && !m.User.ListHidden && !m.User.Deleted) .CountAsync(ct); diff --git a/Foxnouns.Backend/Services/ObjectStorageService.cs b/Foxnouns.Backend/Services/ObjectStorageService.cs index eac7ade..08d3f9b 100644 --- a/Foxnouns.Backend/Services/ObjectStorageService.cs +++ b/Foxnouns.Backend/Services/ObjectStorageService.cs @@ -1,5 +1,4 @@ using Minio; -using Minio.DataModel; using Minio.DataModel.Args; using Minio.Exceptions; @@ -49,4 +48,13 @@ public class ObjectStorageService(ILogger logger, Config config, IMinioClient mi ct ); } + + public async Task GetObjectAsync(string path, CancellationToken ct = default) + { + var stream = new MemoryStream(); + var resp = await minioClient.GetObjectAsync( + new GetObjectArgs().WithBucket(config.Storage.Bucket).WithObject(path), + ct + ); + } } diff --git a/Foxnouns.Backend/Services/PeriodicTasksService.cs b/Foxnouns.Backend/Services/PeriodicTasksService.cs index 84ae354..1e0e756 100644 --- a/Foxnouns.Backend/Services/PeriodicTasksService.cs +++ b/Foxnouns.Backend/Services/PeriodicTasksService.cs @@ -15,13 +15,10 @@ public class PeriodicTasksService(ILogger logger, IServiceProvider services) : B { _logger.Debug("Running periodic tasks"); - await using AsyncServiceScope scope = services.CreateAsyncScope(); + await using var scope = services.CreateAsyncScope(); - // The type is literally written on the same line, we can just use `var` - // ReSharper disable SuggestVarOrType_SimpleTypes var keyCacheService = scope.ServiceProvider.GetRequiredService(); var dataCleanupService = scope.ServiceProvider.GetRequiredService(); - // ReSharper restore SuggestVarOrType_SimpleTypes await keyCacheService.DeleteExpiredKeysAsync(ct); await dataCleanupService.InvokeAsync(ct); diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 9f6da8b..cb728d1 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -43,9 +43,9 @@ public class UserRendererService( ) { scopes = scopes.ExpandScopes(); - bool tokenCanReadHiddenMembers = scopes.Contains("member.read") && isSelfUser; - bool tokenHidden = scopes.Contains("user.read_hidden") && isSelfUser; - bool tokenPrivileged = scopes.Contains("user.read_privileged") && isSelfUser; + var tokenCanReadHiddenMembers = scopes.Contains("member.read") && isSelfUser; + var tokenHidden = scopes.Contains("user.read_hidden") && isSelfUser; + var tokenPrivileged = scopes.Contains("user.read_privileged") && isSelfUser; renderMembers = renderMembers && (!user.ListHidden || tokenCanReadHiddenMembers); renderAuthMethods = renderAuthMethods && tokenPrivileged; @@ -57,12 +57,12 @@ public class UserRendererService( if (!(isSelfUser && tokenCanReadHiddenMembers)) members = members.Where(m => !m.Unlisted); - List flags = await db + var flags = await db .UserFlags.Where(f => f.UserId == user.Id) .OrderBy(f => f.Id) .ToListAsync(ct); - List authMethods = renderAuthMethods + var authMethods = renderAuthMethods ? await db .AuthMethods.Where(a => a.UserId == user.Id) .Include(a => a.FediverseApplication) @@ -72,11 +72,9 @@ public class UserRendererService( int? utcOffset = null; if ( user.Timezone != null - && TimeZoneInfo.TryFindSystemTimeZoneById(user.Timezone, out TimeZoneInfo? tz) + && TimeZoneInfo.TryFindSystemTimeZoneById(user.Timezone, out var tz) ) - { utcOffset = (int)tz.GetUtcOffset(DateTimeOffset.UtcNow).TotalSeconds; - } return new UserResponse( user.Id, diff --git a/Foxnouns.Backend/Utils/AuthUtils.cs b/Foxnouns.Backend/Utils/AuthUtils.cs index bb9b05c..aaf0c08 100644 --- a/Foxnouns.Backend/Utils/AuthUtils.cs +++ b/Foxnouns.Backend/Utils/AuthUtils.cs @@ -69,8 +69,8 @@ public static class AuthUtils public static bool ValidateScopes(Application application, string[] scopes) { - string[] expandedScopes = scopes.ExpandScopes(); - string[] appScopes = application.Scopes.ExpandAppScopes(); + var expandedScopes = scopes.ExpandScopes(); + var appScopes = application.Scopes.ExpandAppScopes(); return !expandedScopes.Except(appScopes).Any(); } @@ -78,7 +78,7 @@ public static class AuthUtils { try { - string scheme = new Uri(uri).Scheme; + var scheme = new Uri(uri).Scheme; return !ForbiddenSchemes.Contains(scheme); } catch diff --git a/Foxnouns.Backend/Utils/PatchRequest.cs b/Foxnouns.Backend/Utils/PatchRequest.cs index 025eeae..a836efe 100644 --- a/Foxnouns.Backend/Utils/PatchRequest.cs +++ b/Foxnouns.Backend/Utils/PatchRequest.cs @@ -5,11 +5,10 @@ using Newtonsoft.Json.Serialization; namespace Foxnouns.Backend.Utils; /// -/// A base class used for PATCH requests which stores information on whether a key is explicitly set to null or not passed at all. -/// +/// A base class used for PATCH requests which stores information on whether a key is explicitly set to null or not passed at all. +/// /// HasProperty() should not be used for properties that cannot be set to null--a null value should be treated /// as an unset value in those cases. -/// /// public abstract class PatchRequest { @@ -31,7 +30,7 @@ public class PatchRequestContractResolver : DefaultContractResolver MemberSerialization memberSerialization ) { - JsonProperty prop = base.CreateProperty(member, memberSerialization); + var prop = base.CreateProperty(member, memberSerialization); prop.SetIsSpecified += (o, _) => { diff --git a/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs b/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs index 9cc08e0..4271459 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs @@ -39,7 +39,6 @@ public static partial class ValidationUtils var errors = new List<(string, ValidationError?)>(); if (fields.Count > 25) - { errors.Add( ( "fields", @@ -51,13 +50,11 @@ public static partial class ValidationUtils ) ) ); - } - // No overwhelming this function, thank you if (fields.Count > 100) return errors; - foreach ((Field? field, int index) in fields.Select((field, index) => (field, index))) + foreach (var (field, index) in fields.Select((field, index) => (field, index))) { switch (field.Name.Length) { @@ -114,7 +111,6 @@ public static partial class ValidationUtils var errors = new List<(string, ValidationError?)>(); if (entries.Length > Limits.FieldEntriesLimit) - { errors.Add( ( errorPrefix, @@ -126,19 +122,15 @@ public static partial class ValidationUtils ) ) ); - } // Same as above, no overwhelming this function with a ridiculous amount of entries if (entries.Length > Limits.FieldEntriesLimit + 50) return errors; - string[] customPreferenceIds = customPreferences.Keys.Select(id => id.ToString()).ToArray(); + var customPreferenceIds = + customPreferences?.Keys.Select(id => id.ToString()).ToArray() ?? []; - foreach ( - (FieldEntry? entry, int entryIdx) in entries.Select( - (entry, entryIdx) => (entry, entryIdx) - ) - ) + foreach (var (entry, entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx))) { switch (entry.Value.Length) { @@ -174,14 +166,12 @@ public static partial class ValidationUtils !DefaultStatusOptions.Contains(entry.Status) && !customPreferenceIds.Contains(entry.Status) ) - { errors.Add( ( $"{errorPrefix}.{entryIdx}.status", ValidationError.GenericValidationError("Invalid status", entry.Status) ) ); - } } return errors; @@ -198,7 +188,6 @@ public static partial class ValidationUtils var errors = new List<(string, ValidationError?)>(); if (entries.Length > Limits.FieldEntriesLimit) - { errors.Add( ( errorPrefix, @@ -210,17 +199,15 @@ public static partial class ValidationUtils ) ) ); - } // Same as above, no overwhelming this function with a ridiculous amount of entries if (entries.Length > Limits.FieldEntriesLimit + 50) return errors; - string[] customPreferenceIds = customPreferences.Keys.Select(id => id.ToString()).ToArray(); + var customPreferenceIds = + customPreferences?.Keys.Select(id => id.ToString()).ToList() ?? []; - foreach ( - (Pronoun? entry, int entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx)) - ) + foreach (var (entry, entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx))) { switch (entry.Value.Length) { @@ -289,14 +276,12 @@ public static partial class ValidationUtils !DefaultStatusOptions.Contains(entry.Status) && !customPreferenceIds.Contains(entry.Status) ) - { errors.Add( ( $"{errorPrefix}.{entryIdx}.status", ValidationError.GenericValidationError("Invalid status", entry.Status) ) ); - } } return errors; diff --git a/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs b/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs index a4109f2..379a552 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs @@ -29,7 +29,6 @@ public static partial class ValidationUtils var errors = new List<(string, ValidationError?)>(); if (preferences.Count > MaxCustomPreferences) - { errors.Add( ( "custom_preferences", @@ -41,29 +40,20 @@ public static partial class ValidationUtils ) ) ); - } - if (preferences.Count > 50) return errors; - foreach ( - (UsersController.CustomPreferenceUpdate? p, int i) in preferences.Select( - (p, i) => (p, i) - ) - ) + foreach (var (p, i) in preferences.Select((p, i) => (p, i))) { if (!BootstrapIcons.IsValid(p.Icon)) - { errors.Add( ( $"custom_preferences.{i}.icon", ValidationError.DisallowedValueError("Invalid icon name", [], p.Icon) ) ); - } if (p.Tooltip.Length is 1 or > MaxPreferenceTooltipLength) - { errors.Add( ( $"custom_preferences.{i}.tooltip", @@ -75,7 +65,6 @@ public static partial class ValidationUtils ) ) ); - } } return errors; diff --git a/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs b/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs index 4d5b444..0193b7e 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs @@ -46,7 +46,6 @@ public static partial class ValidationUtils public static ValidationError? ValidateUsername(string username) { if (!UsernameRegex().IsMatch(username)) - { return username.Length switch { < 2 => ValidationError.LengthError("Username is too short", 2, 40, username.Length), @@ -56,24 +55,19 @@ public static partial class ValidationUtils username ), }; - } if ( InvalidUsernames.Any(u => string.Equals(u, username, StringComparison.InvariantCultureIgnoreCase) ) ) - { return ValidationError.GenericValidationError("Username is not allowed", username); - } - return null; } public static ValidationError? ValidateMemberName(string memberName) { if (!MemberRegex().IsMatch(memberName)) - { return memberName.Length switch { < 1 => ValidationError.LengthError("Name is too short", 1, 100, memberName.Length), @@ -85,17 +79,13 @@ public static partial class ValidationUtils memberName ), }; - } if ( InvalidMemberNames.Any(u => string.Equals(u, memberName, StringComparison.InvariantCultureIgnoreCase) ) ) - { return ValidationError.GenericValidationError("Name is not allowed", memberName); - } - return null; } @@ -127,15 +117,13 @@ public static partial class ValidationUtils if (links == null) return []; if (links.Length > MaxLinks) - { return [ ("links", ValidationError.LengthError("Too many links", 0, MaxLinks, links.Length)), ]; - } var errors = new List<(string, ValidationError?)>(); - foreach ((string link, int idx) in links.Select((l, i) => (l, i))) + foreach (var (link, idx) in links.Select((l, i) => (l, i))) { switch (link.Length) { @@ -197,27 +185,6 @@ public static partial class ValidationUtils }; } - public const int MinimumPasswordLength = 12; - public const int MaximumPasswordLength = 1024; - - public static ValidationError? ValidatePassword(string password) => - password.Length switch - { - < MinimumPasswordLength => ValidationError.LengthError( - "Password is too short", - MinimumPasswordLength, - MaximumPasswordLength, - password.Length - ), - > MaximumPasswordLength => ValidationError.LengthError( - "Password is too long", - MinimumPasswordLength, - MaximumPasswordLength, - password.Length - ), - _ => null, - }; - [GeneratedRegex(@"^[a-zA-Z_0-9\-\.]{2,40}$", RegexOptions.IgnoreCase, "en-NL")] private static partial Regex UsernameRegex(); diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index e5940d3..3e0acd5 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -12,9 +12,9 @@ public static partial class ValidationUtils return; var errorDict = new Dictionary>(); - foreach ((string, ValidationError?) error in errors) + foreach (var error in errors) { - if (errorDict.TryGetValue(error.Item1, out IEnumerable? value)) + if (errorDict.TryGetValue(error.Item1, out var value)) errorDict[error.Item1] = value.Append(error.Item2!); errorDict.Add(error.Item1, [error.Item2!]); } diff --git a/Foxnouns.Backend/Views/Mail/AccountCreation.cshtml b/Foxnouns.Backend/Views/Mail/AccountCreation.cshtml index cf8d1bc..b2b0f2e 100644 --- a/Foxnouns.Backend/Views/Mail/AccountCreation.cshtml +++ b/Foxnouns.Backend/Views/Mail/AccountCreation.cshtml @@ -2,9 +2,9 @@

Please continue creating a new pronouns.cc account by using the following link: -
- Confirm your email address -
+
+ Confirm your email address +
Note that this link will expire in one hour.

diff --git a/Foxnouns.Backend/Views/Mail/AddEmail.cshtml b/Foxnouns.Backend/Views/Mail/AddEmail.cshtml index 2423434..dabef6c 100644 --- a/Foxnouns.Backend/Views/Mail/AddEmail.cshtml +++ b/Foxnouns.Backend/Views/Mail/AddEmail.cshtml @@ -2,9 +2,9 @@

Hello @@@Model.Username, please confirm adding this email address to your account by using the following link: -
- Confirm your email address -
+
+ Confirm your email address +
Note that this link will expire in one hour.

diff --git a/Foxnouns.Backend/Views/Mail/Layout.cshtml b/Foxnouns.Backend/Views/Mail/Layout.cshtml index 6b2a68a..b92faa5 100644 --- a/Foxnouns.Backend/Views/Mail/Layout.cshtml +++ b/Foxnouns.Backend/Views/Mail/Layout.cshtml @@ -2,9 +2,9 @@ - + - +