refactor(backend): use explicit types instead of var by default

This commit is contained in:
sam 2024-12-08 15:07:25 +01:00
parent bc7fd6d804
commit 649988db25
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
52 changed files with 506 additions and 420 deletions

View file

@ -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.");

View file

@ -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."
);
}
}
}

View file

@ -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
)
);
}

View file

@ -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,

View file

@ -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();

View file

@ -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

View file

@ -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 =>

View file

@ -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));
}
}

View file

@ -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() =>

View file

@ -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);

View file

@ -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
)
);