Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a6f9d7f
chore: bump target framework and SDK to .NET 11
hhvrc Jul 17, 2026
be9a120
feat: replace OneOf with .NET 11 discriminated unions
hhvrc Jul 17, 2026
9989a18
feat: opt in to .NET 11 runtime-native async
hhvrc Jul 17, 2026
765fcc5
actions to 11
hhvrc Jul 17, 2026
ba5626e
Update global.json
hhvrc Jul 17, 2026
f7310b6
Update Base.Dockerfile
hhvrc Jul 17, 2026
a116dc4
Update global.json
hhvrc Jul 17, 2026
d045268
Update Directory.Packages.props
hhvrc Jul 27, 2026
da3de94
Update Directory.Packages.props
hhvrc Jul 27, 2026
c6788dd
fix: pin correct .NET 11 preview image tags in Docker builds
hhvrc Jul 27, 2026
e317cb6
Merge remote-tracking branch 'origin/develop' into feat/dotnet11
hhvrc Jul 27, 2026
91aee17
fix: correct DOTNET_VERSION format in codeql workflow
hhvrc Jul 27, 2026
a3bf305
refactor: drop redundant OpenShock.Common.Results using directive
hhvrc Jul 27, 2026
98e4b3a
refactor: use reference-type union cases and drop redundant result wr…
hhvrc Jul 27, 2026
57f5a96
refactor: replace unsafe union cast in EmailTemplate with proper patt…
hhvrc Jul 27, 2026
3ded5f8
fix: match VerifyEmailSuccess instead of the stale Success<(...)> tup…
hhvrc Jul 27, 2026
2f3abf9
Merge remote-tracking branch 'origin/develop' into feat/dotnet11
hhvrc Aug 14, 2026
aa9851e
fix: match the union's underlying value in FlatbuffersWebsocketBaseCo…
hhvrc Aug 14, 2026
5d11be0
chore: bump .NET 11 SDK and preview pins to preview.7
hhvrc Aug 14, 2026
a783ca1
fix: pin StackExchange.Redis to 3.1.13 for .NET 11 compatibility
hhvrc Aug 14, 2026
44a5f6f
Merge branch 'develop' into feat/dotnet11
hhvrc Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ API/SmtpTemplates/dist/
**/appsettings.Development.json
Dockerfile*
**/*.md
dev/
Dev/
2 changes: 1 addition & 1 deletion .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ name: ci-build

env:
REGISTRY: ghcr.io
DOTNET_VERSION: 10.0.x
DOTNET_VERSION: 11.0.x

jobs:

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
name: ci-tag

env:
DOTNET_VERSION: 10.0.x
DOTNET_VERSION: 11.0.x
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/api

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
- cron: '0 6 * * 1'

env:
DOTNET_VERSION: 10.x.x
DOTNET_VERSION: 11.0.x

Comment thread
hhvrc marked this conversation as resolved.
jobs:
analyze:
Expand Down
21 changes: 13 additions & 8 deletions API/Controller/Account/Authenticated/ChangeEmail.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
using System.Net.Mime;
using Microsoft.AspNetCore.Mvc;
using OpenShock.API.Models.Requests;
using OpenShock.API.Services.Account;
using OpenShock.Common.Errors;
using OpenShock.Common.Problems;
using OpenShock.Common.Utils;
using Results = OpenShock.Common.Results;

using OpenShock.Internal.Common.Utils;

Expand Down Expand Up @@ -42,13 +44,16 @@ public async Task<IActionResult> ChangeEmail([FromBody] ChangeEmailRequest body)

var result = await _accountService.CreateEmailChangeFlowAsync(CurrentUser.Id, body.Email, actorId: CurrentUser.Id);

return result.Match<IActionResult>(
success => Ok(),
alreadyInUse => Problem(AccountError.EmailChangeAlreadyInUse),
unchanged => Problem(AccountError.EmailChangeUnchanged),
tooMany => Problem(AccountError.EmailChangeTooMany),
notActivated => throw new UnreachableException("Authenticated user is not activated"),
deactivated => throw new UnreachableException("Authenticated user is deactivated"),
notFound => throw new UnreachableException("Authenticated user not found in database"));
return result switch
{
Results.Success => Ok(),
EmailAlreadyInUse => Problem(AccountError.EmailChangeAlreadyInUse),
EmailUnchanged => Problem(AccountError.EmailChangeUnchanged),
TooManyEmailChanges => Problem(AccountError.EmailChangeTooMany),
AccountNotActivated => throw new UnreachableException("Authenticated user is not activated"),
AccountDeactivated => throw new UnreachableException("Authenticated user is deactivated"),
Results.NotFound => throw new UnreachableException("Authenticated user not found in database"),
_ => throw new UnreachableException()
};
}
}
15 changes: 10 additions & 5 deletions API/Controller/Account/Authenticated/ChangePassword.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
using System.Net.Mime;
using Microsoft.AspNetCore.Mvc;
using OpenShock.API.Models.Requests;
using OpenShock.API.Services.Account;
using OpenShock.Common.Errors;
using OpenShock.Common.Problems;
using OpenShock.Common.Utils;
using Results = OpenShock.Common.Results;

using OpenShock.Internal.Common.Utils;

Expand Down Expand Up @@ -42,10 +44,13 @@ public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordRequest

var result = await _accountService.ChangePasswordAsync(CurrentUser.Id, body.NewPassword, actorId: CurrentUser.Id);

return result.Match<IActionResult>(
success => Ok(),
notActivated => throw new UnreachableException("Authenticated user is not activated"),
deactivated => throw new UnreachableException("Authenticated user is deactivated"),
notFound => throw new UnreachableException("Authenticated user not found in database"));
return result switch
{
Results.Success => Ok(),
AccountNotActivated => throw new UnreachableException("Authenticated user is not activated"),
AccountDeactivated => throw new UnreachableException("Authenticated user is deactivated"),
Results.NotFound => throw new UnreachableException("Authenticated user not found in database"),
_ => throw new UnreachableException()
};
}
}
23 changes: 15 additions & 8 deletions API/Controller/Account/Authenticated/ChangeUsername.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
using System.Net.Mime;
using System.Diagnostics;
using System.Net.Mime;
using Microsoft.AspNetCore.Mvc;
using OpenShock.API.Models.Requests;
using OpenShock.API.Services.Account;
using OpenShock.Common.Errors;
using OpenShock.Common.OpenShockDb;
using OpenShock.Common.Problems;
using OpenShock.Common.Validation;
using Results = OpenShock.Common.Results;

using OpenShock.Internal.Common.Problems;

Expand All @@ -28,12 +32,15 @@ public async Task<IActionResult> ChangeUsername([FromBody] ChangeUsernameRequest
var result = await _accountService.ChangeUsernameAsync(CurrentUser.Id, body.Username, actorId: CurrentUser.Id,
ignoreLimit: CurrentUser.Roles.Any(r => r is RoleType.Staff or RoleType.Admin or RoleType.System));

return result.Match<IActionResult>(
success => Ok(),
usernametaken => Problem(AccountError.UsernameTaken),
usernameerror => Problem(AccountError.UsernameInvalid(usernameerror)),
recentlychanged => Problem(AccountError.UsernameRecentlyChanged),
accountdeactivated => Problem(AccountError.AccountDeactivated),
notfound => throw new Exception("Unexpected result, apparently our current user does not exist..."));
return result switch
{
Results.Success => Ok(),
UsernameTaken => Problem(AccountError.UsernameTaken),
UsernameError usernameError => Problem(AccountError.UsernameInvalid(usernameError)),
RecentlyChanged => Problem(AccountError.UsernameRecentlyChanged),
AccountDeactivated => Problem(AccountError.AccountDeactivated),
Results.NotFound => throw new Exception("Unexpected result, apparently our current user does not exist..."),
_ => throw new UnreachableException()
};
}
}
20 changes: 13 additions & 7 deletions API/Controller/Account/Authenticated/Deactivate.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using System.Net.Mime;
using OpenShock.API.Services.Account;
using OpenShock.Common.Errors;
using OpenShock.Common.Problems;
using AccountSvc = OpenShock.API.Services.Account;
using Results = OpenShock.Common.Results;

using OpenShock.Internal.Common.Problems;

Expand All @@ -19,12 +23,14 @@ public sealed partial class AuthenticatedAccountController
public async Task<IActionResult> Deactivate()
{
var deactivationResult = await _accountService.DeactivateAccountAsync(CurrentUser.Id, CurrentUser.Id, deleteLater: true);
return deactivationResult.Match<IActionResult>(
success => NoContent(),
cannotDeactivatePrivledged => Problem(AccountActivationError.CannotDeactivateOrDeletePrivledgedAccount),
alreadyDeactivated => Problem(AccountActivationError.AlreadyDeactivated),
unauthorized => Problem(AccountActivationError.Unauthorized),
notFound => throw new Exception("This is not supposed to happen, wtf?")
);
return deactivationResult switch
{
Results.Success => NoContent(),
CannotDeactivatePrivilegedAccount => Problem(AccountActivationError.CannotDeactivateOrDeletePrivledgedAccount),
AccountDeactivationAlreadyInProgress => Problem(AccountActivationError.AlreadyDeactivated),
AccountSvc.Unauthorized => Problem(AccountActivationError.Unauthorized),
Results.NotFound => throw new Exception("This is not supposed to happen, wtf?"),
_ => throw new UnreachableException()
};
}
}
17 changes: 11 additions & 6 deletions API/Controller/Account/CheckUsername.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net.Mime;
using Microsoft.AspNetCore.Mvc;
using OpenShock.API.Models.Requests;
using OpenShock.API.Services.Account;
using OpenShock.Common.Results;
using OpenShock.Common.Validation;

namespace OpenShock.API.Controller.Account;
Expand All @@ -20,11 +23,13 @@ public async Task<UsernameCheckResponse> CheckUsername([FromBody] ChangeUsername
{
var result = await _accountService.CheckUsernameAvailabilityAsync(body.Username, cancellationToken);

return result.Match(
success => new UsernameCheckResponse(UsernameAvailability.Available),
taken => new UsernameCheckResponse(UsernameAvailability.Taken),
invalid => new UsernameCheckResponse(UsernameAvailability.Invalid, invalid)
);
return result switch
{
Success => new UsernameCheckResponse(UsernameAvailability.Available),
UsernameTaken => new UsernameCheckResponse(UsernameAvailability.Taken),
UsernameError invalid => new UsernameCheckResponse(UsernameAvailability.Invalid, invalid),
_ => throw new UnreachableException()
};
}
}

Expand Down
20 changes: 13 additions & 7 deletions API/Controller/Account/LoginV2.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
using Microsoft.AspNetCore.Mvc;
using OpenShock.API.Models.Requests;
using System.Diagnostics;
using System.Net.Mime;
using Asp.Versioning;
using Microsoft.AspNetCore.RateLimiting;
using OpenShock.API.Services.Account;
using OpenShock.Common.Errors;
using OpenShock.Common.OpenShockDb;
using OpenShock.Common.Problems;
using OpenShock.API.Models.Response;
using OpenShock.API.Services.Turnstile;
using Results = OpenShock.Common.Results;

using OpenShock.Internal.Common.Problems;

Expand Down Expand Up @@ -38,14 +42,16 @@ public async Task<IActionResult> LoginV2(
if (turnstileError is not null) return turnstileError;

var getAccountResult = await _accountService.GetAccountByCredentialsAsync(body.UsernameOrEmail, body.Password, cancellationToken);
if (!getAccountResult.TryPickT0(out var account, out var errors))
if (getAccountResult is not User account)
{
return errors.Match(
notFound => Problem(LoginError.InvalidCredentials),
deactivated => Problem(AccountError.AccountDeactivated),
notActivated => Problem(AccountError.AccountNotActivated),
oauthOnly => Problem(AccountError.AccountOAuthOnly)
);
return getAccountResult switch
{
Results.NotFound => Problem(LoginError.InvalidCredentials),
AccountDeactivated => Problem(AccountError.AccountDeactivated),
AccountNotActivated => Problem(AccountError.AccountNotActivated),
AccountIsOAuthOnly => Problem(AccountError.AccountOAuthOnly),
_ => throw new UnreachableException()
};
}

await CreateSession(account.Id, cookieDomain);
Expand Down
15 changes: 10 additions & 5 deletions API/Controller/Account/PasswordResetCheckValid.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using System;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using System.Net.Mime;
using Asp.Versioning;
using Microsoft.AspNetCore.RateLimiting;
using OpenShock.API.Services.Account;
using OpenShock.Common.Errors;
using OpenShock.Common.Problems;
using OpenShock.Common.Models;
using Results = OpenShock.Common.Results;

using OpenShock.Internal.Common.Problems;

Expand Down Expand Up @@ -41,10 +44,12 @@ public IActionResult PasswordResetCheckValidLegacy([FromRoute] Guid passwordRese
public async Task<IActionResult> PasswordResetCheckValid([FromRoute] Guid passwordResetId, [FromRoute] string secret, CancellationToken cancellationToken)
{
var passwordResetExists = await _accountService.CheckPasswordResetExistsAsync(passwordResetId, secret, cancellationToken);
return passwordResetExists.Match(
success => LegacyEmptyOk("Valid password reset process"),
notFound => Problem(PasswordResetError.PasswordResetNotFound),
invalid => Problem(PasswordResetError.PasswordResetNotFound)
);
return passwordResetExists switch
{
Results.Success => LegacyEmptyOk("Valid password reset process"),
Results.NotFound => Problem(PasswordResetError.PasswordResetNotFound),
SecretInvalid => Problem(PasswordResetError.PasswordResetNotFound),
_ => throw new UnreachableException()
};
}
}
19 changes: 12 additions & 7 deletions API/Controller/Account/PasswordResetComplete.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using System;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using System.Net.Mime;
using Asp.Versioning;
using Microsoft.AspNetCore.RateLimiting;
using OpenShock.API.Services.Account;
using OpenShock.Common.Errors;
using OpenShock.Common.Problems;
using OpenShock.Common.Models;
using Results = OpenShock.Common.Results;

using OpenShock.Internal.Common.Problems;

Expand Down Expand Up @@ -44,13 +47,15 @@ public async Task<IActionResult> PasswordResetComplete([FromRoute] Guid password
{
var passwordResetComplete = await _accountService.CompletePasswordResetFlowAsync(passwordResetId, secret, body.Password);

return passwordResetComplete.Match(
success => LegacyEmptyOk("Password successfully changed"),
notFound => Problem(PasswordResetError.PasswordResetNotFound),
notActivated => Problem(AccountError.AccountNotActivated),
deactivated => Problem(AccountError.AccountDeactivated),
invalid => Problem(PasswordResetError.PasswordResetNotFound)
);
return passwordResetComplete switch
{
Results.Success => LegacyEmptyOk("Password successfully changed"),
Results.NotFound => Problem(PasswordResetError.PasswordResetNotFound),
AccountNotActivated => Problem(AccountError.AccountNotActivated),
AccountDeactivated => Problem(AccountError.AccountDeactivated),
SecretInvalid => Problem(PasswordResetError.PasswordResetNotFound),
_ => throw new UnreachableException()
};
}


Expand Down
14 changes: 10 additions & 4 deletions API/Controller/Account/SignupV2.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
using Microsoft.AspNetCore.Mvc;
using OpenShock.API.Models.Requests;
using OpenShock.API.Services.Account;
using System.Diagnostics;
using System.Net.Mime;
using Asp.Versioning;
using Microsoft.AspNetCore.RateLimiting;
using OpenShock.API.Services.Turnstile;
using OpenShock.Common.Errors;
using OpenShock.Common.OpenShockDb;
using OpenShock.Common.Options;
using OpenShock.Common.Problems;
using OpenShock.Common.Results;

using OpenShock.Internal.Common.Problems;

Expand Down Expand Up @@ -43,9 +47,11 @@ public async Task<IActionResult> SignUpV2(
if (turnstileError is not null) return turnstileError;

var creationAction = await _accountService.CreateAccountWithActivationFlowAsync(body.Email, body.Username, body.Password);
return creationAction.Match<IActionResult>(
_ => Ok(),
_ => Problem(SignupError.UsernameOrEmailExists)
);
return creationAction switch
{
User _ => Ok(),
AccountWithEmailOrUsernameExists => Problem(SignupError.UsernameOrEmailExists),
_ => throw new UnreachableException()
};
}
}
14 changes: 10 additions & 4 deletions API/Controller/Account/VerifyEmail.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using System;
using System.Diagnostics;
using System.Net.Mime;
using Microsoft.AspNetCore.Mvc;
using Asp.Versioning;
using OpenShock.API.Services.Account;
using OpenShock.Common.Errors;
using OpenShock.Common.Problems;
using Results = OpenShock.Common.Results;

using OpenShock.Internal.Common.Problems;

Expand Down Expand Up @@ -44,9 +47,12 @@ private async Task<IActionResult> VerifyPendingEmailChange(string token, Cancell
{
var result = await _accountService.TryVerifyEmailAsync(token, cancellationToken);

return result.Match<IActionResult>(
success => Ok(),
notFound => Problem(AccountError.EmailChangeNotFound),
emailTaken => Problem(AccountError.EmailChangeAlreadyInUse));
return result switch
{
VerifyEmailSuccess => Ok(),
Results.NotFound => Problem(AccountError.EmailChangeNotFound),
EmailAlreadyInUse => Problem(AccountError.EmailChangeAlreadyInUse),
_ => throw new UnreachableException()
};
}
}
3 changes: 1 addition & 2 deletions API/Controller/Account/_Turnstile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ public sealed partial class AccountController
private async Task<IActionResult?> VerifyTurnstileAsync(ICloudflareTurnstileService turnstileService, string turnstileResponse, CancellationToken cancellationToken)
{
var turnStile = await turnstileService.VerifyUserResponseTokenAsync(turnstileResponse, HttpContext.GetRemoteIP(), cancellationToken);
if (turnStile.IsT0) return null;
if (turnStile is not CloudflareTurnstileError[] cfErrors) return null;

var cfErrors = turnStile.AsT1.Value;
if (cfErrors.All(err => err.IsClientError()))
return Problem(TurnstileError.InvalidTurnstile);

Expand Down
Loading
Loading