refactor(backend): use explicit types instead of var by default
This commit is contained in:
		
							parent
							
								
									bc7fd6d804
								
							
						
					
					
						commit
						649988db25
					
				
					 52 changed files with 506 additions and 420 deletions
				
			
		|  | @ -33,14 +33,16 @@ public class AuthController( | |||
|             config.GoogleAuth.Enabled, | ||||
|             config.TumblrAuth.Enabled | ||||
|         ); | ||||
|         var state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync(ct)); | ||||
|         string 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" | ||||
|                 + $"&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)); | ||||
|     } | ||||
|  | @ -86,7 +88,7 @@ public class AuthController( | |||
|     )] | ||||
|     public async Task<IActionResult> GetAuthMethodAsync(Snowflake id) | ||||
|     { | ||||
|         var authMethod = await db | ||||
|         AuthMethod? authMethod = await db | ||||
|             .AuthMethods.Include(a => a.FediverseApplication) | ||||
|             .FirstOrDefaultAsync(a => a.UserId == CurrentUser!.Id && a.Id == id); | ||||
|         if (authMethod == null) | ||||
|  | @ -99,17 +101,19 @@ public class AuthController( | |||
|     [Authorize("*")] | ||||
|     public async Task<IActionResult> DeleteAuthMethodAsync(Snowflake id) | ||||
|     { | ||||
|         var authMethods = await db | ||||
|         List<AuthMethod> 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 | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         var authMethod = authMethods.FirstOrDefault(a => a.Id == id); | ||||
|         AuthMethod? authMethod = authMethods.FirstOrDefault(a => a.Id == id); | ||||
|         if (authMethod == null) | ||||
|             throw new ApiError.NotFound("No authentication method with that ID found."); | ||||
| 
 | ||||
|  |  | |||
|  | @ -34,8 +34,10 @@ public class DiscordAuthController( | |||
|         CheckRequirements(); | ||||
|         await keyCacheService.ValidateAuthStateAsync(req.State); | ||||
| 
 | ||||
|         var remoteUser = await remoteAuthService.RequestDiscordTokenAsync(req.Code); | ||||
|         var user = await authService.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id); | ||||
|         RemoteAuthService.RemoteUser remoteUser = await remoteAuthService.RequestDiscordTokenAsync( | ||||
|             req.Code | ||||
|         ); | ||||
|         User? user = await authService.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id); | ||||
|         if (user != null) | ||||
|             return Ok(await authService.GenerateUserTokenAsync(user)); | ||||
| 
 | ||||
|  | @ -45,23 +47,14 @@ public class DiscordAuthController( | |||
|             remoteUser.Id | ||||
|         ); | ||||
| 
 | ||||
|         var ticket = AuthUtils.RandomToken(); | ||||
|         string ticket = AuthUtils.RandomToken(); | ||||
|         await keyCacheService.SetKeyAsync( | ||||
|             $"discord:{ticket}", | ||||
|             remoteUser, | ||||
|             Duration.FromMinutes(20) | ||||
|         ); | ||||
| 
 | ||||
|         return Ok( | ||||
|             new CallbackResponse( | ||||
|                 HasAccount: false, | ||||
|                 Ticket: ticket, | ||||
|                 RemoteUsername: remoteUser.Username, | ||||
|                 User: null, | ||||
|                 Token: null, | ||||
|                 ExpiresAt: null | ||||
|             ) | ||||
|         ); | ||||
|         return Ok(new CallbackResponse(false, ticket, remoteUser.Username, null, null, null)); | ||||
|     } | ||||
| 
 | ||||
|     [HttpPost("register")] | ||||
|  | @ -70,9 +63,10 @@ public class DiscordAuthController( | |||
|         [FromBody] AuthController.OauthRegisterRequest req | ||||
|     ) | ||||
|     { | ||||
|         var remoteUser = await keyCacheService.GetKeyAsync<RemoteAuthService.RemoteUser>( | ||||
|             $"discord:{req.Ticket}" | ||||
|         ); | ||||
|         RemoteAuthService.RemoteUser? remoteUser = | ||||
|             await keyCacheService.GetKeyAsync<RemoteAuthService.RemoteUser>( | ||||
|                 $"discord:{req.Ticket}" | ||||
|             ); | ||||
|         if (remoteUser == null) | ||||
|             throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); | ||||
|         if ( | ||||
|  | @ -88,7 +82,7 @@ public class DiscordAuthController( | |||
|             throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); | ||||
|         } | ||||
| 
 | ||||
|         var user = await authService.CreateUserWithRemoteAuthAsync( | ||||
|         User user = await authService.CreateUserWithRemoteAuthAsync( | ||||
|             req.Username, | ||||
|             AuthType.Discord, | ||||
|             remoteUser.Id, | ||||
|  | @ -104,12 +98,12 @@ public class DiscordAuthController( | |||
|     { | ||||
|         CheckRequirements(); | ||||
| 
 | ||||
|         var state = await remoteAuthService.ValidateAddAccountRequestAsync( | ||||
|         string state = await remoteAuthService.ValidateAddAccountRequestAsync( | ||||
|             CurrentUser!.Id, | ||||
|             AuthType.Discord | ||||
|         ); | ||||
| 
 | ||||
|         var url = | ||||
|         string url = | ||||
|             $"https://discord.com/oauth2/authorize?response_type=code" | ||||
|             + $"&client_id={config.DiscordAuth.ClientId}&scope=identify" | ||||
|             + $"&prompt=none&state={state}" | ||||
|  | @ -132,10 +126,12 @@ public class DiscordAuthController( | |||
|             AuthType.Discord | ||||
|         ); | ||||
| 
 | ||||
|         var remoteUser = await remoteAuthService.RequestDiscordTokenAsync(req.Code); | ||||
|         RemoteAuthService.RemoteUser remoteUser = await remoteAuthService.RequestDiscordTokenAsync( | ||||
|             req.Code | ||||
|         ); | ||||
|         try | ||||
|         { | ||||
|             var authMethod = await authService.AddAuthMethodAsync( | ||||
|             AuthMethod authMethod = await authService.AddAuthMethodAsync( | ||||
|                 CurrentUser.Id, | ||||
|                 AuthType.Discord, | ||||
|                 remoteUser.Id, | ||||
|  | @ -169,8 +165,10 @@ public class DiscordAuthController( | |||
|     private void CheckRequirements() | ||||
|     { | ||||
|         if (!config.DiscordAuth.Enabled) | ||||
|         { | ||||
|             throw new ApiError.BadRequest( | ||||
|                 "Discord authentication is not enabled on this instance." | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -39,11 +39,7 @@ public class EmailAuthController( | |||
|         if (!req.Email.Contains('@')) | ||||
|             throw new ApiError.BadRequest("Email is invalid", "email", req.Email); | ||||
| 
 | ||||
|         var state = await keyCacheService.GenerateRegisterEmailStateAsync( | ||||
|             req.Email, | ||||
|             userId: null, | ||||
|             ct | ||||
|         ); | ||||
|         string state = await keyCacheService.GenerateRegisterEmailStateAsync(req.Email, null, ct); | ||||
| 
 | ||||
|         // If there's already a user with that email address, pretend we sent an email but actually ignore it | ||||
|         if ( | ||||
|  | @ -63,23 +59,14 @@ public class EmailAuthController( | |||
|     { | ||||
|         CheckRequirements(); | ||||
| 
 | ||||
|         var state = await keyCacheService.GetRegisterEmailStateAsync(req.State); | ||||
|         RegisterEmailState? state = await keyCacheService.GetRegisterEmailStateAsync(req.State); | ||||
|         if (state is not { ExistingUserId: null }) | ||||
|             throw new ApiError.BadRequest("Invalid state", "state", req.State); | ||||
| 
 | ||||
|         var ticket = AuthUtils.RandomToken(); | ||||
|         string ticket = AuthUtils.RandomToken(); | ||||
|         await keyCacheService.SetKeyAsync($"email:{ticket}", state.Email, Duration.FromMinutes(20)); | ||||
| 
 | ||||
|         return Ok( | ||||
|             new CallbackResponse( | ||||
|                 HasAccount: false, | ||||
|                 Ticket: ticket, | ||||
|                 RemoteUsername: state.Email, | ||||
|                 User: null, | ||||
|                 Token: null, | ||||
|                 ExpiresAt: null | ||||
|             ) | ||||
|         ); | ||||
|         return Ok(new CallbackResponse(false, ticket, state.Email, null, null, null)); | ||||
|     } | ||||
| 
 | ||||
|     [HttpPost("register")] | ||||
|  | @ -89,14 +76,18 @@ public class EmailAuthController( | |||
|     { | ||||
|         CheckRequirements(); | ||||
| 
 | ||||
|         var email = await keyCacheService.GetKeyAsync($"email:{req.Ticket}"); | ||||
|         string? email = await keyCacheService.GetKeyAsync($"email:{req.Ticket}"); | ||||
|         if (email == null) | ||||
|             throw new ApiError.BadRequest("Unknown ticket", "ticket", req.Ticket); | ||||
| 
 | ||||
|         var user = await authService.CreateUserWithPasswordAsync(req.Username, email, req.Password); | ||||
|         var frontendApp = await db.GetFrontendApplicationAsync(); | ||||
|         User user = await authService.CreateUserWithPasswordAsync( | ||||
|             req.Username, | ||||
|             email, | ||||
|             req.Password | ||||
|         ); | ||||
|         Application frontendApp = await db.GetFrontendApplicationAsync(); | ||||
| 
 | ||||
|         var (tokenStr, token) = authService.GenerateToken( | ||||
|         (string? tokenStr, Token? token) = authService.GenerateToken( | ||||
|             user, | ||||
|             frontendApp, | ||||
|             ["*"], | ||||
|  | @ -110,7 +101,7 @@ public class EmailAuthController( | |||
| 
 | ||||
|         return Ok( | ||||
|             new AuthController.AuthResponse( | ||||
|                 await userRenderer.RenderUserAsync(user, selfUser: user, renderMembers: false), | ||||
|                 await userRenderer.RenderUserAsync(user, user, renderMembers: false), | ||||
|                 tokenStr, | ||||
|                 token.ExpiresAt | ||||
|             ) | ||||
|  | @ -126,19 +117,16 @@ public class EmailAuthController( | |||
|     { | ||||
|         CheckRequirements(); | ||||
| 
 | ||||
|         var (user, authenticationResult) = await authService.AuthenticateUserAsync( | ||||
|             req.Email, | ||||
|             req.Password, | ||||
|             ct | ||||
|         ); | ||||
|         (User? user, AuthService.EmailAuthenticationResult authenticationResult) = | ||||
|             await authService.AuthenticateUserAsync(req.Email, req.Password, ct); | ||||
|         if (authenticationResult == AuthService.EmailAuthenticationResult.MfaRequired) | ||||
|             throw new NotImplementedException("MFA is not implemented yet"); | ||||
| 
 | ||||
|         var frontendApp = await db.GetFrontendApplicationAsync(ct); | ||||
|         Application frontendApp = await db.GetFrontendApplicationAsync(ct); | ||||
| 
 | ||||
|         _logger.Debug("Logging user {Id} in with email and password", user.Id); | ||||
| 
 | ||||
|         var (tokenStr, token) = authService.GenerateToken( | ||||
|         (string? tokenStr, Token? token) = authService.GenerateToken( | ||||
|             user, | ||||
|             frontendApp, | ||||
|             ["*"], | ||||
|  | @ -152,12 +140,7 @@ public class EmailAuthController( | |||
| 
 | ||||
|         return Ok( | ||||
|             new AuthController.AuthResponse( | ||||
|                 await userRenderer.RenderUserAsync( | ||||
|                     user, | ||||
|                     selfUser: user, | ||||
|                     renderMembers: false, | ||||
|                     ct: ct | ||||
|                 ), | ||||
|                 await userRenderer.RenderUserAsync(user, user, renderMembers: false, ct: ct), | ||||
|                 tokenStr, | ||||
|                 token.ExpiresAt | ||||
|             ) | ||||
|  | @ -184,7 +167,7 @@ public class EmailAuthController( | |||
|     { | ||||
|         CheckRequirements(); | ||||
| 
 | ||||
|         var emails = await db | ||||
|         List<AuthMethod> emails = await db | ||||
|             .AuthMethods.Where(m => m.UserId == CurrentUser!.Id && m.AuthType == AuthType.Email) | ||||
|             .ToListAsync(); | ||||
|         if (emails.Count > AuthUtils.MaxAuthMethodsPerType) | ||||
|  | @ -207,12 +190,12 @@ public class EmailAuthController( | |||
|             await db.SaveChangesAsync(); | ||||
|         } | ||||
| 
 | ||||
|         var state = await keyCacheService.GenerateRegisterEmailStateAsync( | ||||
|         string state = await keyCacheService.GenerateRegisterEmailStateAsync( | ||||
|             req.Email, | ||||
|             userId: CurrentUser!.Id | ||||
|             CurrentUser!.Id | ||||
|         ); | ||||
| 
 | ||||
|         var emailExists = await db | ||||
|         bool emailExists = await db | ||||
|             .AuthMethods.Where(m => m.AuthType == AuthType.Email && m.RemoteId == req.Email) | ||||
|             .AnyAsync(); | ||||
|         if (emailExists) | ||||
|  | @ -230,13 +213,13 @@ public class EmailAuthController( | |||
|     { | ||||
|         CheckRequirements(); | ||||
| 
 | ||||
|         var state = await keyCacheService.GetRegisterEmailStateAsync(req.State); | ||||
|         RegisterEmailState? state = await keyCacheService.GetRegisterEmailStateAsync(req.State); | ||||
|         if (state?.ExistingUserId != CurrentUser!.Id) | ||||
|             throw new ApiError.BadRequest("Invalid state", "state", req.State); | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
|             var authMethod = await authService.AddAuthMethodAsync( | ||||
|             AuthMethod authMethod = await authService.AddAuthMethodAsync( | ||||
|                 CurrentUser.Id, | ||||
|                 AuthType.Email, | ||||
|                 state.Email | ||||
|  | @ -252,7 +235,7 @@ public class EmailAuthController( | |||
|                     authMethod.Id, | ||||
|                     AuthType.Email, | ||||
|                     authMethod.RemoteId, | ||||
|                     RemoteUsername: null | ||||
|                     null | ||||
|                 ) | ||||
|             ); | ||||
|         } | ||||
|  |  | |||
|  | @ -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); | ||||
| 
 | ||||
|         var url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh); | ||||
|         string url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh); | ||||
|         return Ok(new AuthController.SingleUrlResponse(url)); | ||||
|     } | ||||
| 
 | ||||
|  | @ -42,22 +42,19 @@ public class FediverseAuthController( | |||
|     [ProducesResponseType<CallbackResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> FediverseCallbackAsync([FromBody] CallbackRequest req) | ||||
|     { | ||||
|         var app = await fediverseAuthService.GetApplicationAsync(req.Instance); | ||||
|         var remoteUser = await fediverseAuthService.GetRemoteFediverseUserAsync( | ||||
|             app, | ||||
|             req.Code, | ||||
|             req.State | ||||
|         ); | ||||
|         FediverseApplication app = await fediverseAuthService.GetApplicationAsync(req.Instance); | ||||
|         FediverseAuthService.FediverseUser remoteUser = | ||||
|             await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code, req.State); | ||||
| 
 | ||||
|         var user = await authService.AuthenticateUserAsync( | ||||
|         User? user = await authService.AuthenticateUserAsync( | ||||
|             AuthType.Fediverse, | ||||
|             remoteUser.Id, | ||||
|             instance: app | ||||
|             app | ||||
|         ); | ||||
|         if (user != null) | ||||
|             return Ok(await authService.GenerateUserTokenAsync(user)); | ||||
| 
 | ||||
|         var ticket = AuthUtils.RandomToken(); | ||||
|         string ticket = AuthUtils.RandomToken(); | ||||
|         await keyCacheService.SetKeyAsync( | ||||
|             $"fediverse:{ticket}", | ||||
|             new FediverseTicketData(app.Id, remoteUser), | ||||
|  | @ -66,12 +63,12 @@ public class FediverseAuthController( | |||
| 
 | ||||
|         return Ok( | ||||
|             new CallbackResponse( | ||||
|                 HasAccount: false, | ||||
|                 Ticket: ticket, | ||||
|                 RemoteUsername: $"@{remoteUser.Username}@{app.Domain}", | ||||
|                 User: null, | ||||
|                 Token: null, | ||||
|                 ExpiresAt: null | ||||
|                 false, | ||||
|                 ticket, | ||||
|                 $"@{remoteUser.Username}@{app.Domain}", | ||||
|                 null, | ||||
|                 null, | ||||
|                 null | ||||
|             ) | ||||
|         ); | ||||
|     } | ||||
|  | @ -82,14 +79,16 @@ public class FediverseAuthController( | |||
|         [FromBody] AuthController.OauthRegisterRequest req | ||||
|     ) | ||||
|     { | ||||
|         var ticketData = await keyCacheService.GetKeyAsync<FediverseTicketData>( | ||||
|         FediverseTicketData? ticketData = await keyCacheService.GetKeyAsync<FediverseTicketData>( | ||||
|             $"fediverse:{req.Ticket}", | ||||
|             delete: true | ||||
|             true | ||||
|         ); | ||||
|         if (ticketData == null) | ||||
|             throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); | ||||
| 
 | ||||
|         var app = await db.FediverseApplications.FindAsync(ticketData.ApplicationId); | ||||
|         FediverseApplication? app = await db.FediverseApplications.FindAsync( | ||||
|             ticketData.ApplicationId | ||||
|         ); | ||||
|         if (app == null) | ||||
|             throw new FoxnounsError("Null application found for ticket"); | ||||
| 
 | ||||
|  | @ -111,12 +110,12 @@ public class FediverseAuthController( | |||
|             throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); | ||||
|         } | ||||
| 
 | ||||
|         var user = await authService.CreateUserWithRemoteAuthAsync( | ||||
|         User user = await authService.CreateUserWithRemoteAuthAsync( | ||||
|             req.Username, | ||||
|             AuthType.Fediverse, | ||||
|             ticketData.User.Id, | ||||
|             ticketData.User.Username, | ||||
|             instance: app | ||||
|             app | ||||
|         ); | ||||
| 
 | ||||
|         return Ok(await authService.GenerateUserTokenAsync(user)); | ||||
|  | @ -132,13 +131,13 @@ public class FediverseAuthController( | |||
|         if (instance.Any(c => c is '@' or ':' or '/') || !instance.Contains('.')) | ||||
|             throw new ApiError.BadRequest("Not a valid domain.", "instance", instance); | ||||
| 
 | ||||
|         var state = await remoteAuthService.ValidateAddAccountRequestAsync( | ||||
|         string state = await remoteAuthService.ValidateAddAccountRequestAsync( | ||||
|             CurrentUser!.Id, | ||||
|             AuthType.Fediverse, | ||||
|             instance | ||||
|         ); | ||||
| 
 | ||||
|         var url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh, state); | ||||
|         string url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh, state); | ||||
|         return Ok(new AuthController.SingleUrlResponse(url)); | ||||
|     } | ||||
| 
 | ||||
|  | @ -153,11 +152,12 @@ public class FediverseAuthController( | |||
|             req.Instance | ||||
|         ); | ||||
| 
 | ||||
|         var app = await fediverseAuthService.GetApplicationAsync(req.Instance); | ||||
|         var remoteUser = await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code); | ||||
|         FediverseApplication app = await fediverseAuthService.GetApplicationAsync(req.Instance); | ||||
|         FediverseAuthService.FediverseUser remoteUser = | ||||
|             await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code); | ||||
|         try | ||||
|         { | ||||
|             var authMethod = await authService.AddAuthMethodAsync( | ||||
|             AuthMethod authMethod = await authService.AddAuthMethodAsync( | ||||
|                 CurrentUser.Id, | ||||
|                 AuthType.Fediverse, | ||||
|                 remoteUser.Id, | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ public class ExportsController( | |||
|     [HttpGet] | ||||
|     public async Task<IActionResult> GetDataExportsAsync() | ||||
|     { | ||||
|         var export = await db | ||||
|         DataExport? export = await db | ||||
|             .DataExports.Where(d => d.UserId == CurrentUser!.Id) | ||||
|             .OrderByDescending(d => d.Id) | ||||
|             .FirstOrDefaultAsync(); | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| 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; | ||||
|  | @ -7,6 +8,7 @@ using Foxnouns.Backend.Services; | |||
| using Foxnouns.Backend.Utils; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Storage; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Controllers; | ||||
| 
 | ||||
|  | @ -29,7 +31,9 @@ public class FlagsController( | |||
|     )] | ||||
|     public async Task<IActionResult> GetFlagsAsync(CancellationToken ct = default) | ||||
|     { | ||||
|         var flags = await db.PrideFlags.Where(f => f.UserId == CurrentUser!.Id).ToListAsync(ct); | ||||
|         List<PrideFlag> flags = await db | ||||
|             .PrideFlags.Where(f => f.UserId == CurrentUser!.Id) | ||||
|             .ToListAsync(ct); | ||||
| 
 | ||||
|         return Ok(flags.Select(userRenderer.RenderPrideFlag)); | ||||
|     } | ||||
|  | @ -43,7 +47,7 @@ public class FlagsController( | |||
|     { | ||||
|         ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, req.Image)); | ||||
| 
 | ||||
|         var id = snowflakeGenerator.GenerateSnowflake(); | ||||
|         Snowflake id = snowflakeGenerator.GenerateSnowflake(); | ||||
| 
 | ||||
|         queue.QueueInvocableWithPayload<CreateFlagInvocable, CreateFlagPayload>( | ||||
|             new CreateFlagPayload(id, CurrentUser!.Id, req.Name, req.Image, req.Description) | ||||
|  | @ -62,7 +66,7 @@ public class FlagsController( | |||
|     { | ||||
|         ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, null)); | ||||
| 
 | ||||
|         var flag = await db.PrideFlags.FirstOrDefaultAsync(f => | ||||
|         PrideFlag? flag = await db.PrideFlags.FirstOrDefaultAsync(f => | ||||
|             f.Id == id && f.UserId == CurrentUser!.Id | ||||
|         ); | ||||
|         if (flag == null) | ||||
|  | @ -90,20 +94,20 @@ public class FlagsController( | |||
|     [Authorize("user.update")] | ||||
|     public async Task<IActionResult> DeleteFlagAsync(Snowflake id) | ||||
|     { | ||||
|         await using var tx = await db.Database.BeginTransactionAsync(); | ||||
|         await using IDbContextTransaction tx = await db.Database.BeginTransactionAsync(); | ||||
| 
 | ||||
|         var flag = await db.PrideFlags.FirstOrDefaultAsync(f => | ||||
|         PrideFlag? 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."); | ||||
| 
 | ||||
|         var hash = flag.Hash; | ||||
|         string hash = flag.Hash; | ||||
| 
 | ||||
|         db.PrideFlags.Remove(flag); | ||||
|         await db.SaveChangesAsync(); | ||||
| 
 | ||||
|         var flagCount = await db.PrideFlags.CountAsync(f => f.Hash == flag.Hash); | ||||
|         int flagCount = await db.PrideFlags.CountAsync(f => f.Hash == flag.Hash); | ||||
|         if (flagCount == 0) | ||||
|         { | ||||
|             try | ||||
|  |  | |||
|  | @ -44,21 +44,22 @@ public partial class InternalController(DatabaseContext db) : ControllerBase | |||
|     [HttpPost("request-data")] | ||||
|     public async Task<IActionResult> GetRequestDataAsync([FromBody] RequestDataRequest req) | ||||
|     { | ||||
|         var endpoint = GetEndpoint(HttpContext, req.Path, req.Method); | ||||
|         RouteEndpoint? endpoint = GetEndpoint(HttpContext, req.Path, req.Method); | ||||
|         if (endpoint == null) | ||||
|             throw new ApiError.BadRequest("Path/method combination is invalid"); | ||||
| 
 | ||||
|         var actionDescriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>(); | ||||
|         var template = actionDescriptor?.AttributeRouteInfo?.Template; | ||||
|         ControllerActionDescriptor? actionDescriptor = | ||||
|             endpoint.Metadata.GetMetadata<ControllerActionDescriptor>(); | ||||
|         string? 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 var rawToken)) | ||||
|         if (!AuthUtils.TryParseToken(req.Token, out byte[]? rawToken)) | ||||
|             return Ok(new RequestDataResponse(null, template)); | ||||
| 
 | ||||
|         var userId = await db.GetTokenUserId(rawToken); | ||||
|         Snowflake? userId = await db.GetTokenUserId(rawToken); | ||||
|         return Ok(new RequestDataResponse(userId, template)); | ||||
|     } | ||||
| 
 | ||||
|  | @ -72,12 +73,13 @@ public partial class InternalController(DatabaseContext db) : ControllerBase | |||
|         string requestMethod | ||||
|     ) | ||||
|     { | ||||
|         var endpointDataSource = httpContext.RequestServices.GetService<EndpointDataSource>(); | ||||
|         EndpointDataSource? endpointDataSource = | ||||
|             httpContext.RequestServices.GetService<EndpointDataSource>(); | ||||
|         if (endpointDataSource == null) | ||||
|             return null; | ||||
|         var endpoints = endpointDataSource.Endpoints.OfType<RouteEndpoint>(); | ||||
|         IEnumerable<RouteEndpoint> endpoints = endpointDataSource.Endpoints.OfType<RouteEndpoint>(); | ||||
| 
 | ||||
|         foreach (var endpoint in endpoints) | ||||
|         foreach (RouteEndpoint? endpoint in endpoints) | ||||
|         { | ||||
|             if (endpoint.RoutePattern.RawText == null) | ||||
|                 continue; | ||||
|  | @ -86,9 +88,10 @@ public partial class InternalController(DatabaseContext db) : ControllerBase | |||
|                 TemplateParser.Parse(endpoint.RoutePattern.RawText), | ||||
|                 new RouteValueDictionary() | ||||
|             ); | ||||
|             if (!templateMatcher.TryMatch(url, new())) | ||||
|             if (!templateMatcher.TryMatch(url, new RouteValueDictionary())) | ||||
|                 continue; | ||||
|             var httpMethodAttribute = endpoint.Metadata.GetMetadata<HttpMethodAttribute>(); | ||||
|             HttpMethodAttribute? httpMethodAttribute = | ||||
|                 endpoint.Metadata.GetMetadata<HttpMethodAttribute>(); | ||||
|             if ( | ||||
|                 httpMethodAttribute != null | ||||
|                 && !httpMethodAttribute.HttpMethods.Any(x => | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ 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; | ||||
|  | @ -32,7 +33,7 @@ public class MembersController( | |||
|     )] | ||||
|     public async Task<IActionResult> GetMembersAsync(string userRef, CancellationToken ct = default) | ||||
|     { | ||||
|         var user = await db.ResolveUserAsync(userRef, CurrentToken, ct); | ||||
|         User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); | ||||
|         return Ok(await memberRenderer.RenderUserMembersAsync(user, CurrentToken)); | ||||
|     } | ||||
| 
 | ||||
|  | @ -44,7 +45,7 @@ public class MembersController( | |||
|         CancellationToken ct = default | ||||
|     ) | ||||
|     { | ||||
|         var member = await db.ResolveMemberAsync(userRef, memberRef, CurrentToken, ct); | ||||
|         Member member = await db.ResolveMemberAsync(userRef, memberRef, CurrentToken, ct); | ||||
|         return Ok(memberRenderer.RenderMember(member, CurrentToken)); | ||||
|     } | ||||
| 
 | ||||
|  | @ -78,7 +79,7 @@ public class MembersController( | |||
|             ] | ||||
|         ); | ||||
| 
 | ||||
|         var memberCount = await db.Members.CountAsync(m => m.UserId == CurrentUser.Id, ct); | ||||
|         int memberCount = await db.Members.CountAsync(m => m.UserId == CurrentUser.Id, ct); | ||||
|         if (memberCount >= MaxMemberCount) | ||||
|             throw new ApiError.BadRequest("Maximum number of members reached"); | ||||
| 
 | ||||
|  | @ -120,9 +121,11 @@ public class MembersController( | |||
|         } | ||||
| 
 | ||||
|         if (req.Avatar != null) | ||||
|         { | ||||
|             queue.QueueInvocableWithPayload<MemberAvatarUpdateInvocable, AvatarUpdatePayload>( | ||||
|                 new AvatarUpdatePayload(member.Id, req.Avatar) | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         return Ok(memberRenderer.RenderMember(member, CurrentToken)); | ||||
|     } | ||||
|  | @ -134,8 +137,8 @@ public class MembersController( | |||
|         [FromBody] UpdateMemberRequest req | ||||
|     ) | ||||
|     { | ||||
|         await using var tx = await db.Database.BeginTransactionAsync(); | ||||
|         var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); | ||||
|         await using IDbContextTransaction tx = await db.Database.BeginTransactionAsync(); | ||||
|         Member member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); | ||||
|         var errors = new List<(string, ValidationError?)>(); | ||||
| 
 | ||||
|         // We might add extra validations for names later down the line. | ||||
|  | @ -197,7 +200,11 @@ public class MembersController( | |||
| 
 | ||||
|         if (req.Flags != null) | ||||
|         { | ||||
|             var flagError = await db.SetMemberFlagsAsync(CurrentUser!.Id, member.Id, req.Flags); | ||||
|             ValidationError? flagError = await db.SetMemberFlagsAsync( | ||||
|                 CurrentUser!.Id, | ||||
|                 member.Id, | ||||
|                 req.Flags | ||||
|             ); | ||||
|             if (flagError != null) | ||||
|                 errors.Add(("flags", flagError)); | ||||
|         } | ||||
|  | @ -210,9 +217,12 @@ 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<MemberAvatarUpdateInvocable, AvatarUpdatePayload>( | ||||
|                 new AvatarUpdatePayload(member.Id, req.Avatar) | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
|             await db.SaveChangesAsync(); | ||||
|  | @ -254,8 +264,8 @@ public class MembersController( | |||
|     [Authorize("member.update")] | ||||
|     public async Task<IActionResult> DeleteMemberAsync(string memberRef) | ||||
|     { | ||||
|         var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); | ||||
|         var deleteCount = await db | ||||
|         Member member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); | ||||
|         int deleteCount = await db | ||||
|             .Members.Where(m => m.UserId == CurrentUser!.Id && m.Id == member.Id) | ||||
|             .ExecuteDeleteAsync(); | ||||
|         if (deleteCount == 0) | ||||
|  | @ -289,9 +299,9 @@ public class MembersController( | |||
|     [ProducesResponseType<UserRendererService.UserResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> RerollSidAsync(string memberRef) | ||||
|     { | ||||
|         var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); | ||||
|         Member member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); | ||||
| 
 | ||||
|         var minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1); | ||||
|         Instant minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1); | ||||
|         if (CurrentUser!.LastSidReroll > minTimeAgo) | ||||
|             throw new ApiError.BadRequest("Cannot reroll short ID yet"); | ||||
| 
 | ||||
|  | @ -308,7 +318,10 @@ public class MembersController( | |||
|             ); | ||||
| 
 | ||||
|         // Fetch the new sid then pass that to RenderMember | ||||
|         var newSid = await db.Members.Where(m => m.Id == member.Id).Select(m => m.Sid).FirstAsync(); | ||||
|         string newSid = await db | ||||
|             .Members.Where(m => m.Id == member.Id) | ||||
|             .Select(m => m.Sid) | ||||
|             .FirstAsync(); | ||||
|         return Ok(memberRenderer.RenderMember(member, CurrentToken, newSid)); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -10,9 +10,8 @@ public class MetaController : ApiControllerBase | |||
| 
 | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType<MetaResponse>(StatusCodes.Status200OK)] | ||||
|     public IActionResult GetMeta() | ||||
|     { | ||||
|         return Ok( | ||||
|     public IActionResult GetMeta() => | ||||
|         Ok( | ||||
|             new MetaResponse( | ||||
|                 Repository, | ||||
|                 BuildInfo.Version, | ||||
|  | @ -25,14 +24,13 @@ public class MetaController : ApiControllerBase | |||
|                     (int)FoxnounsMetrics.UsersActiveDayCount.Value | ||||
|                 ), | ||||
|                 new Limits( | ||||
|                     MemberCount: MembersController.MaxMemberCount, | ||||
|                     BioLength: ValidationUtils.MaxBioLength, | ||||
|                     CustomPreferences: ValidationUtils.MaxCustomPreferences, | ||||
|                     MaxAuthMethods: AuthUtils.MaxAuthMethodsPerType | ||||
|                     MembersController.MaxMemberCount, | ||||
|                     ValidationUtils.MaxBioLength, | ||||
|                     ValidationUtils.MaxCustomPreferences, | ||||
|                     AuthUtils.MaxAuthMethodsPerType | ||||
|                 ) | ||||
|             ) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     [HttpGet("/api/v2/coffee")] | ||||
|     public IActionResult BrewCoffee() => | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ public class SidController(Config config, DatabaseContext db) : ApiControllerBas | |||
| 
 | ||||
|     private async Task<IActionResult> ResolveUserSidAsync(string id, CancellationToken ct = default) | ||||
|     { | ||||
|         var username = await db | ||||
|         string? username = await db | ||||
|             .Users.Where(u => u.Sid == id.ToLowerInvariant() && !u.Deleted) | ||||
|             .Select(u => u.Username) | ||||
|             .FirstOrDefaultAsync(ct); | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ 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; | ||||
|  | @ -29,16 +30,9 @@ public class UsersController( | |||
|     [ProducesResponseType<UserRendererService.UserResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> GetUserAsync(string userRef, CancellationToken ct = default) | ||||
|     { | ||||
|         var user = await db.ResolveUserAsync(userRef, CurrentToken, ct); | ||||
|         User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); | ||||
|         return Ok( | ||||
|             await userRenderer.RenderUserAsync( | ||||
|                 user, | ||||
|                 selfUser: CurrentUser, | ||||
|                 token: CurrentToken, | ||||
|                 renderMembers: true, | ||||
|                 renderAuthMethods: true, | ||||
|                 ct: ct | ||||
|             ) | ||||
|             await userRenderer.RenderUserAsync(user, CurrentUser, CurrentToken, true, true, ct: ct) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|  | @ -50,8 +44,8 @@ public class UsersController( | |||
|         CancellationToken ct = default | ||||
|     ) | ||||
|     { | ||||
|         await using var tx = await db.Database.BeginTransactionAsync(ct); | ||||
|         var user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct); | ||||
|         await using IDbContextTransaction tx = await db.Database.BeginTransactionAsync(ct); | ||||
|         User 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) | ||||
|  | @ -108,7 +102,7 @@ public class UsersController( | |||
| 
 | ||||
|         if (req.Flags != null) | ||||
|         { | ||||
|             var flagError = await db.SetUserFlagsAsync(CurrentUser!.Id, req.Flags); | ||||
|             ValidationError? flagError = await db.SetUserFlagsAsync(CurrentUser!.Id, req.Flags); | ||||
|             if (flagError != null) | ||||
|                 errors.Add(("flags", flagError)); | ||||
|         } | ||||
|  | @ -143,12 +137,14 @@ public class UsersController( | |||
|                 if (TimeZoneInfo.TryFindSystemTimeZoneById(req.Timezone, out _)) | ||||
|                     user.Timezone = req.Timezone; | ||||
|                 else | ||||
|                 { | ||||
|                     errors.Add( | ||||
|                         ( | ||||
|                             "timezone", | ||||
|                             ValidationError.GenericValidationError("Invalid timezone", req.Timezone) | ||||
|                         ) | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -157,9 +153,11 @@ 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<UserAvatarUpdateInvocable, AvatarUpdatePayload>( | ||||
|                 new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar) | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
|  | @ -202,12 +200,12 @@ public class UsersController( | |||
|     { | ||||
|         ValidationUtils.Validate(ValidationUtils.ValidateCustomPreferences(req)); | ||||
| 
 | ||||
|         var user = await db.ResolveUserAsync(CurrentUser!.Id, ct); | ||||
|         User user = await db.ResolveUserAsync(CurrentUser!.Id, ct); | ||||
|         var preferences = user | ||||
|             .CustomPreferences.Where(x => req.Any(r => r.Id == x.Key)) | ||||
|             .ToDictionary(); | ||||
| 
 | ||||
|         foreach (var r in req) | ||||
|         foreach (CustomPreferenceUpdate? r in req) | ||||
|         { | ||||
|             if (r.Id != null && preferences.ContainsKey(r.Id.Value)) | ||||
|             { | ||||
|  | @ -271,7 +269,7 @@ public class UsersController( | |||
|     [ProducesResponseType<UserSettings>(statusCode: StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> GetUserSettingsAsync(CancellationToken ct = default) | ||||
|     { | ||||
|         var user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct); | ||||
|         User user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct); | ||||
|         return Ok(user.Settings); | ||||
|     } | ||||
| 
 | ||||
|  | @ -283,7 +281,7 @@ public class UsersController( | |||
|         CancellationToken ct = default | ||||
|     ) | ||||
|     { | ||||
|         var user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct); | ||||
|         User user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct); | ||||
| 
 | ||||
|         if (req.HasProperty(nameof(req.DarkMode))) | ||||
|             user.Settings.DarkMode = req.DarkMode; | ||||
|  | @ -304,7 +302,7 @@ public class UsersController( | |||
|     [ProducesResponseType<UserRendererService.UserResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> RerollSidAsync() | ||||
|     { | ||||
|         var minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1); | ||||
|         Instant minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1); | ||||
|         if (CurrentUser!.LastSidReroll > minTimeAgo) | ||||
|             throw new ApiError.BadRequest("Cannot reroll short ID yet"); | ||||
| 
 | ||||
|  | @ -318,18 +316,18 @@ public class UsersController( | |||
|             ); | ||||
| 
 | ||||
|         // Get the user's new sid | ||||
|         var newSid = await db | ||||
|         string newSid = await db | ||||
|             .Users.Where(u => u.Id == CurrentUser.Id) | ||||
|             .Select(u => u.Sid) | ||||
|             .FirstAsync(); | ||||
| 
 | ||||
|         var user = await db.ResolveUserAsync(CurrentUser.Id); | ||||
|         User user = await db.ResolveUserAsync(CurrentUser.Id); | ||||
|         return Ok( | ||||
|             await userRenderer.RenderUserAsync( | ||||
|                 CurrentUser, | ||||
|                 user, | ||||
|                 CurrentUser, | ||||
|                 CurrentToken, | ||||
|                 renderMembers: false, | ||||
|                 false, | ||||
|                 overrideSid: newSid | ||||
|             ) | ||||
|         ); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue