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

@ -123,22 +123,27 @@ public class AuthService(
CancellationToken ct = default
)
{
var user = await db.Users.FirstOrDefaultAsync(
User? 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
);
}
var pwResult = await VerifyHashedPasswordAsync(user, password, ct);
PasswordVerificationResult pwResult = await VerifyHashedPasswordAsync(user, 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);
@ -169,7 +174,7 @@ public class AuthService(
throw new FoxnounsError("Password for user supplied to ValidatePasswordAsync was null");
}
var pwResult = await VerifyHashedPasswordAsync(user, password, ct);
PasswordVerificationResult pwResult = await VerifyHashedPasswordAsync(user, password, ct);
return pwResult
is PasswordVerificationResult.SuccessRehashNeeded
or PasswordVerificationResult.Success;
@ -231,13 +236,15 @@ public class AuthService(
AssertValidAuthType(authType, app);
// This is already checked when
var currentCount = await db
int 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
{
@ -262,13 +269,15 @@ public class AuthService(
)
{
if (!AuthUtils.ValidateScopes(application, scopes))
{
throw new ApiError.BadRequest(
"Invalid scopes requested for this token",
"scopes",
scopes
);
}
var (token, hash) = GenerateToken();
(string? token, byte[]? hash) = GenerateToken();
return (
token,
new Token
@ -293,9 +302,9 @@ public class AuthService(
CancellationToken ct = default
)
{
var frontendApp = await db.GetFrontendApplicationAsync(ct);
Application frontendApp = await db.GetFrontendApplicationAsync(ct);
var (tokenStr, token) = GenerateToken(
(string? tokenStr, Token? token) = GenerateToken(
user,
frontendApp,
["*"],
@ -308,17 +317,12 @@ public class AuthService(
await db.SaveChangesAsync(ct);
return new CallbackResponse(
HasAccount: true,
Ticket: null,
RemoteUsername: null,
User: await userRenderer.RenderUserAsync(
user,
selfUser: user,
renderMembers: false,
ct: ct
),
Token: tokenStr,
ExpiresAt: token.ExpiresAt
true,
null,
null,
await userRenderer.RenderUserAsync(user, user, renderMembers: false, ct: ct),
tokenStr,
token.ExpiresAt
);
}
@ -340,8 +344,8 @@ public class AuthService(
private static (string, byte[]) GenerateToken()
{
var token = AuthUtils.RandomToken();
var hash = SHA512.HashData(Convert.FromBase64String(token));
string token = AuthUtils.RandomToken();
byte[] hash = SHA512.HashData(Convert.FromBase64String(token));
return (token, hash);
}

View file

@ -18,22 +18,25 @@ public partial class FediverseAuthService
Snowflake? existingAppId = null
)
{
var resp = await _client.PostAsJsonAsync(
HttpResponseMessage resp = await _client.PostAsJsonAsync(
$"https://{instance}/api/v1/apps",
new CreateMastodonApplicationRequest(
ClientName: $"pronouns.cc (+{_config.BaseUrl})",
RedirectUris: MastodonRedirectUri(instance),
Scopes: "read read:accounts",
Website: _config.BaseUrl
$"pronouns.cc (+{_config.BaseUrl})",
MastodonRedirectUri(instance),
"read read:accounts",
_config.BaseUrl
)
);
resp.EnsureSuccessStatusCode();
var mastodonApp = await resp.Content.ReadFromJsonAsync<PartialMastodonApplication>();
PartialMastodonApplication? mastodonApp =
await resp.Content.ReadFromJsonAsync<PartialMastodonApplication>();
if (mastodonApp == null)
{
throw new FoxnounsError(
$"Application created on Mastodon-compatible instance {instance} was null"
);
}
FediverseApplication app;
@ -75,7 +78,7 @@ public partial class FediverseAuthService
if (state != null)
await _keyCacheService.ValidateAuthStateAsync(state);
var tokenResp = await _client.PostAsync(
HttpResponseMessage tokenResp = await _client.PostAsync(
MastodonTokenUri(app.Domain),
new FormUrlEncodedContent(
new Dictionary<string, string>
@ -95,7 +98,7 @@ public partial class FediverseAuthService
}
tokenResp.EnsureSuccessStatusCode();
var token = (
string? token = (
await tokenResp.Content.ReadFromJsonAsync<MastodonTokenResponse>()
)?.AccessToken;
if (token == null)
@ -106,9 +109,9 @@ public partial class FediverseAuthService
var req = new HttpRequestMessage(HttpMethod.Get, MastodonCurrentUserUri(app.Domain));
req.Headers.Add("Authorization", $"Bearer {token}");
var currentUserResp = await _client.SendAsync(req);
HttpResponseMessage currentUserResp = await _client.SendAsync(req);
currentUserResp.EnsureSuccessStatusCode();
var user = await currentUserResp.Content.ReadFromJsonAsync<FediverseUser>();
FediverseUser? user = await currentUserResp.Content.ReadFromJsonAsync<FediverseUser>();
if (user == null)
{
throw new FoxnounsError($"User response from instance {app.Domain} was invalid");
@ -131,7 +134,7 @@ public partial class FediverseAuthService
"An app credentials refresh was requested for {ApplicationId}, creating a new application",
app.Id
);
app = await CreateMastodonApplicationAsync(app.Domain, existingAppId: app.Id);
app = await CreateMastodonApplicationAsync(app.Domain, app.Id);
}
state ??= HttpUtility.UrlEncode(await _keyCacheService.GenerateAuthStateAsync());

View file

@ -43,7 +43,7 @@ public partial class FediverseAuthService
string? state = null
)
{
var app = await GetApplicationAsync(instance);
FediverseApplication app = await GetApplicationAsync(instance);
return await GenerateAuthUrlAsync(app, forceRefresh, state);
}
@ -56,13 +56,15 @@ public partial class FediverseAuthService
public async Task<FediverseApplication> GetApplicationAsync(string instance)
{
var app = await _db.FediverseApplications.FirstOrDefaultAsync(a => a.Domain == instance);
FediverseApplication? 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);
var softwareName = await GetSoftwareNameAsync(instance);
string softwareName = await GetSoftwareNameAsync(instance);
if (IsMastodonCompatible(softwareName))
{
@ -76,13 +78,14 @@ public partial class FediverseAuthService
{
_logger.Debug("Requesting software name for fediverse instance {Instance}", instance);
var wellKnownResp = await _client.GetAsync(
HttpResponseMessage wellKnownResp = await _client.GetAsync(
new Uri($"https://{instance}/.well-known/nodeinfo")
);
wellKnownResp.EnsureSuccessStatusCode();
var wellKnown = await wellKnownResp.Content.ReadFromJsonAsync<WellKnownResponse>();
var nodeInfoUrl = wellKnown?.Links.FirstOrDefault(l => l.Rel == NodeInfoRel)?.Href;
WellKnownResponse? wellKnown =
await wellKnownResp.Content.ReadFromJsonAsync<WellKnownResponse>();
string? nodeInfoUrl = wellKnown?.Links.FirstOrDefault(l => l.Rel == NodeInfoRel)?.Href;
if (nodeInfoUrl == null)
{
throw new FoxnounsError(
@ -90,10 +93,10 @@ public partial class FediverseAuthService
);
}
var nodeInfoResp = await _client.GetAsync(nodeInfoUrl);
HttpResponseMessage nodeInfoResp = await _client.GetAsync(nodeInfoUrl);
nodeInfoResp.EnsureSuccessStatusCode();
var nodeInfo = await nodeInfoResp.Content.ReadFromJsonAsync<PartialNodeInfo>();
PartialNodeInfo? nodeInfo = await nodeInfoResp.Content.ReadFromJsonAsync<PartialNodeInfo>();
return nodeInfo?.Software.Name
?? throw new FoxnounsError(
$"Nodeinfo response for instance {instance} was invalid, no software name"

View file

@ -29,7 +29,7 @@ public class RemoteAuthService(
)
{
var redirectUri = $"{config.BaseUrl}/auth/callback/discord";
var resp = await _httpClient.PostAsync(
HttpResponseMessage resp = await _httpClient.PostAsync(
_discordTokenUri,
new FormUrlEncodedContent(
new Dictionary<string, string>
@ -45,7 +45,7 @@ public class RemoteAuthService(
);
if (!resp.IsSuccessStatusCode)
{
var respBody = await resp.Content.ReadAsStringAsync(ct);
string respBody = await resp.Content.ReadAsStringAsync(ct);
_logger.Error(
"Received error status {StatusCode} when exchanging OAuth token: {ErrorBody}",
(int)resp.StatusCode,
@ -55,16 +55,18 @@ public class RemoteAuthService(
}
resp.EnsureSuccessStatusCode();
var token = await resp.Content.ReadFromJsonAsync<DiscordTokenResponse>(ct);
DiscordTokenResponse? token = await resp.Content.ReadFromJsonAsync<DiscordTokenResponse>(
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}");
var resp2 = await _httpClient.SendAsync(req, ct);
HttpResponseMessage resp2 = await _httpClient.SendAsync(req, ct);
resp2.EnsureSuccessStatusCode();
var user = await resp2.Content.ReadFromJsonAsync<DiscordUserResponse>(ct);
DiscordUserResponse? user = await resp2.Content.ReadFromJsonAsync<DiscordUserResponse>(ct);
if (user == null)
throw new FoxnounsError("Discord user response was null");
@ -104,7 +106,7 @@ public class RemoteAuthService(
string? instance = null
)
{
var existingAccounts = await db
int existingAccounts = await db
.AuthMethods.Where(m => m.UserId == userId && m.AuthType == authType)
.CountAsync();
if (existingAccounts > AuthUtils.MaxAuthMethodsPerType)
@ -131,7 +133,9 @@ public class RemoteAuthService(
string? instance = null
)
{
var accountState = await keyCacheService.GetAddExtraAccountStateAsync(state);
AddExtraAccountState? accountState = await keyCacheService.GetAddExtraAccountStateAsync(
state
);
if (
accountState == null
|| accountState.AuthType != authType

View file

@ -28,9 +28,9 @@ public class DataCleanupService(
private async Task CleanUsersAsync(CancellationToken ct = default)
{
var selfDeleteExpires = clock.GetCurrentInstant() - User.DeleteAfter;
var suspendExpires = clock.GetCurrentInstant() - User.DeleteSuspendedAfter;
var users = await db
Instant selfDeleteExpires = clock.GetCurrentInstant() - User.DeleteAfter;
Instant suspendExpires = clock.GetCurrentInstant() - User.DeleteSuspendedAfter;
List<User> users = await db
.Users.Include(u => u.Members)
.Include(u => u.DataExports)
.Where(u =>
@ -92,13 +92,15 @@ public class DataCleanupService(
private async Task CleanExportsAsync(CancellationToken ct = default)
{
var minExpiredId = Snowflake.FromInstant(clock.GetCurrentInstant() - DataExport.Expiration);
var exports = await db.DataExports.Where(d => d.Id < minExpiredId).ToListAsync(ct);
List<DataExport> 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 (var export in exports)
foreach (DataExport? export in exports)
{
_logger.Debug("Deleting export {ExportId}", export.Id);
await objectStorageService.RemoveObjectAsync(

View file

@ -41,7 +41,7 @@ public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger)
CancellationToken ct = default
)
{
var value = await db.TemporaryKeys.FirstOrDefaultAsync(k => k.Key == key, ct);
TemporaryKey? 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)
{
var count = await db
int 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
{
var value = JsonConvert.SerializeObject(obj);
string 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
{
var value = await GetKeyAsync(key, delete, ct);
string? value = await GetKeyAsync(key, delete, ct);
return value == null ? default : JsonConvert.DeserializeObject<T>(value);
}
}

View file

@ -10,11 +10,11 @@ public class MemberRendererService(DatabaseContext db, Config config)
{
public async Task<IEnumerable<PartialMember>> RenderUserMembersAsync(User user, Token? token)
{
var canReadHiddenMembers =
bool canReadHiddenMembers =
token != null && token.UserId == user.Id && token.HasScope("member.read");
var renderUnlisted =
bool renderUnlisted =
token != null && token.UserId == user.Id && token.HasScope("user.read_hidden");
var canReadMemberList = !user.ListHidden || canReadHiddenMembers;
bool canReadMemberList = !user.ListHidden || canReadHiddenMembers;
IEnumerable<Member> 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
)
{
var renderUnlisted = token?.UserId == member.UserId && token.HasScope("user.read_hidden");
bool renderUnlisted = token?.UserId == member.UserId && token.HasScope("user.read_hidden");
return new MemberResponse(
member.Id,

View file

@ -3,6 +3,7 @@ using Foxnouns.Backend.Database;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using Prometheus;
using ITimer = Prometheus.ITimer;
namespace Foxnouns.Backend.Services;
@ -16,19 +17,23 @@ public class MetricsCollectionService(ILogger logger, IServiceProvider services,
public async Task CollectMetricsAsync(CancellationToken ct = default)
{
var timer = FoxnounsMetrics.MetricsCollectionTime.NewTimer();
var now = clock.GetCurrentInstant();
ITimer timer = FoxnounsMetrics.MetricsCollectionTime.NewTimer();
Instant now = clock.GetCurrentInstant();
await using var scope = services.CreateAsyncScope();
await using AsyncServiceScope scope = services.CreateAsyncScope();
// ReSharper disable once SuggestVarOrType_SimpleTypes
await using var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
var users = await db.Users.Where(u => !u.Deleted).Select(u => u.LastActive).ToListAsync(ct);
List<Instant>? 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));
var memberCount = await db
int memberCount = await db
.Members.Include(m => m.User)
.Where(m => !m.Unlisted && !m.User.ListHidden && !m.User.Deleted)
.CountAsync(ct);

View file

@ -1,4 +1,5 @@
using Minio;
using Minio.DataModel;
using Minio.DataModel.Args;
using Minio.Exceptions;
@ -48,13 +49,4 @@ 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
);
}
}

View file

@ -15,10 +15,13 @@ public class PeriodicTasksService(ILogger logger, IServiceProvider services) : B
{
_logger.Debug("Running periodic tasks");
await using var scope = services.CreateAsyncScope();
await using AsyncServiceScope 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<KeyCacheService>();
var dataCleanupService = scope.ServiceProvider.GetRequiredService<DataCleanupService>();
// ReSharper restore SuggestVarOrType_SimpleTypes
await keyCacheService.DeleteExpiredKeysAsync(ct);
await dataCleanupService.InvokeAsync(ct);

View file

@ -43,9 +43,9 @@ public class UserRendererService(
)
{
scopes = scopes.ExpandScopes();
var tokenCanReadHiddenMembers = scopes.Contains("member.read") && isSelfUser;
var tokenHidden = scopes.Contains("user.read_hidden") && isSelfUser;
var tokenPrivileged = scopes.Contains("user.read_privileged") && isSelfUser;
bool tokenCanReadHiddenMembers = scopes.Contains("member.read") && isSelfUser;
bool tokenHidden = scopes.Contains("user.read_hidden") && isSelfUser;
bool 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);
var flags = await db
List<UserFlag> flags = await db
.UserFlags.Where(f => f.UserId == user.Id)
.OrderBy(f => f.Id)
.ToListAsync(ct);
var authMethods = renderAuthMethods
List<AuthMethod> authMethods = renderAuthMethods
? await db
.AuthMethods.Where(a => a.UserId == user.Id)
.Include(a => a.FediverseApplication)
@ -72,7 +72,7 @@ public class UserRendererService(
int? utcOffset = null;
if (
user.Timezone != null
&& TimeZoneInfo.TryFindSystemTimeZoneById(user.Timezone, out var tz)
&& TimeZoneInfo.TryFindSystemTimeZoneById(user.Timezone, out TimeZoneInfo? tz)
)
utcOffset = (int)tz.GetUtcOffset(DateTimeOffset.UtcNow).TotalSeconds;