diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 5b8d2ed1..c20514c0 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -32,7 +32,7 @@ name: ci-build env: REGISTRY: ghcr.io - DOTNET_VERSION: 10.0.x + DOTNET_VERSION: 11.0.x COVERAGE_MIN_PERCENT: '85' jobs: diff --git a/.github/workflows/ci-tag.yml b/.github/workflows/ci-tag.yml index 9f78b9f9..1dacbadb 100644 --- a/.github/workflows/ci-tag.yml +++ b/.github/workflows/ci-tag.yml @@ -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 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bbf05c2b..221b4e40 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -9,7 +9,7 @@ on: - cron: '0 6 * * 1' env: - DOTNET_VERSION: 10.x.x + DOTNET_VERSION: 11.x.x jobs: analyze: diff --git a/.github/workflows/update-cloudflare-proxies.yml b/.github/workflows/update-cloudflare-proxies.yml index 0831a29d..53c4b358 100644 --- a/.github/workflows/update-cloudflare-proxies.yml +++ b/.github/workflows/update-cloudflare-proxies.yml @@ -1,5 +1,3 @@ -name: Update Cloudflare Proxies - on: schedule: - cron: '0 0 1 * *' # runs at 00:00 UTC on the 1st day of every month @@ -9,6 +7,11 @@ on: - '.github/workflows/update-cloudflare-proxies.yml' - 'Common/CloudflareIPs.targets' +name: Update Cloudflare Proxies + +env: + DOTNET_VERSION: 11.0.x + jobs: update-proxies: runs-on: ubuntu-latest diff --git a/API/Controller/Account/Authenticated/ChangeEmail.cs b/API/Controller/Account/Authenticated/ChangeEmail.cs index 4538b769..52c3e281 100644 --- a/API/Controller/Account/Authenticated/ChangeEmail.cs +++ b/API/Controller/Account/Authenticated/ChangeEmail.cs @@ -2,9 +2,12 @@ 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.Results; using OpenShock.Common.Utils; +using Results = OpenShock.Common.Results; namespace OpenShock.API.Controller.Account.Authenticated; @@ -38,13 +41,16 @@ public async Task ChangeEmail([FromBody] ChangeEmailRequest body) var result = await _accountService.CreateEmailChangeFlowAsync(CurrentUser.Id, body.Email, actorId: CurrentUser.Id); - return result.Match( - 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 + { + 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() + }; } } diff --git a/API/Controller/Account/Authenticated/ChangePassword.cs b/API/Controller/Account/Authenticated/ChangePassword.cs index bdd4ae61..fd5da7dc 100644 --- a/API/Controller/Account/Authenticated/ChangePassword.cs +++ b/API/Controller/Account/Authenticated/ChangePassword.cs @@ -2,9 +2,12 @@ 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.Results; using OpenShock.Common.Utils; +using Results = OpenShock.Common.Results; namespace OpenShock.API.Controller.Account.Authenticated; @@ -38,10 +41,13 @@ public async Task ChangePassword([FromBody] ChangePasswordRequest var result = await _accountService.ChangePasswordAsync(CurrentUser.Id, body.NewPassword, actorId: CurrentUser.Id); - return result.Match( - 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 + { + 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() + }; } } diff --git a/API/Controller/Account/Authenticated/ChangeUsername.cs b/API/Controller/Account/Authenticated/ChangeUsername.cs index da212791..13710b16 100644 --- a/API/Controller/Account/Authenticated/ChangeUsername.cs +++ b/API/Controller/Account/Authenticated/ChangeUsername.cs @@ -1,9 +1,14 @@ -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.Results; +using OpenShock.Common.Validation; +using Results = OpenShock.Common.Results; namespace OpenShock.API.Controller.Account.Authenticated; @@ -26,12 +31,15 @@ public async Task 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( - 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 + { + 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() + }; } } \ No newline at end of file diff --git a/API/Controller/Account/Authenticated/Deactivate.cs b/API/Controller/Account/Authenticated/Deactivate.cs index 6e7e8efc..65bc628f 100644 --- a/API/Controller/Account/Authenticated/Deactivate.cs +++ b/API/Controller/Account/Authenticated/Deactivate.cs @@ -1,7 +1,12 @@ 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 OpenShock.Common.Results; +using AccountSvc = OpenShock.API.Services.Account; +using Results = OpenShock.Common.Results; namespace OpenShock.API.Controller.Account.Authenticated; @@ -17,12 +22,14 @@ public sealed partial class AuthenticatedAccountController public async Task Deactivate() { var deactivationResult = await _accountService.DeactivateAccountAsync(CurrentUser.Id, CurrentUser.Id, deleteLater: true); - return deactivationResult.Match( - 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 + { + 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() + }; } } \ No newline at end of file diff --git a/API/Controller/Account/CheckUsername.cs b/API/Controller/Account/CheckUsername.cs index 60df27fa..b533c87e 100644 --- a/API/Controller/Account/CheckUsername.cs +++ b/API/Controller/Account/CheckUsername.cs @@ -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; @@ -20,11 +23,13 @@ public async Task 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() + }; } } diff --git a/API/Controller/Account/LoginV2.cs b/API/Controller/Account/LoginV2.cs index 43ae9c7c..c32413bf 100644 --- a/API/Controller/Account/LoginV2.cs +++ b/API/Controller/Account/LoginV2.cs @@ -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; namespace OpenShock.API.Controller.Account; @@ -36,14 +40,16 @@ public async Task 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); diff --git a/API/Controller/Account/PasswordResetCheckValid.cs b/API/Controller/Account/PasswordResetCheckValid.cs index b5f1312f..63f58720 100644 --- a/API/Controller/Account/PasswordResetCheckValid.cs +++ b/API/Controller/Account/PasswordResetCheckValid.cs @@ -1,11 +1,15 @@ 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 OpenShock.Common.Results; +using Results = OpenShock.Common.Results; namespace OpenShock.API.Controller.Account; @@ -39,10 +43,12 @@ public IActionResult PasswordResetCheckValidLegacy([FromRoute] Guid passwordRese public async Task 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 + { + Success => LegacyEmptyOk("Valid password reset process"), + Results.NotFound => Problem(PasswordResetError.PasswordResetNotFound), + SecretInvalid => Problem(PasswordResetError.PasswordResetNotFound), + _ => throw new UnreachableException() + }; } } \ No newline at end of file diff --git a/API/Controller/Account/PasswordResetComplete.cs b/API/Controller/Account/PasswordResetComplete.cs index 78f6f615..fbab4520 100644 --- a/API/Controller/Account/PasswordResetComplete.cs +++ b/API/Controller/Account/PasswordResetComplete.cs @@ -1,11 +1,15 @@ 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 OpenShock.Common.Results; +using Results = OpenShock.Common.Results; namespace OpenShock.API.Controller.Account; @@ -42,13 +46,15 @@ public async Task 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 + { + 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() + }; } diff --git a/API/Controller/Account/SignupV2.cs b/API/Controller/Account/SignupV2.cs index cf3c2f72..6cf12e92 100644 --- a/API/Controller/Account/SignupV2.cs +++ b/API/Controller/Account/SignupV2.cs @@ -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; namespace OpenShock.API.Controller.Account; @@ -41,9 +45,11 @@ public async Task SignUpV2( if (turnstileError is not null) return turnstileError; var creationAction = await _accountService.CreateAccountWithActivationFlowAsync(body.Email, body.Username, body.Password); - return creationAction.Match( - _ => Ok(), - _ => Problem(SignupError.UsernameOrEmailExists) - ); + return creationAction switch + { + Success => Ok(), + AccountWithEmailOrUsernameExists => Problem(SignupError.UsernameOrEmailExists), + _ => throw new UnreachableException() + }; } } \ No newline at end of file diff --git a/API/Controller/Account/VerifyEmail.cs b/API/Controller/Account/VerifyEmail.cs index 93b840d0..e3e2bdfb 100644 --- a/API/Controller/Account/VerifyEmail.cs +++ b/API/Controller/Account/VerifyEmail.cs @@ -1,9 +1,13 @@ 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 OpenShock.Common.Results; +using Results = OpenShock.Common.Results; namespace OpenShock.API.Controller.Account; @@ -42,9 +46,12 @@ private async Task VerifyPendingEmailChange(string token, Cancell { var result = await _accountService.TryVerifyEmailAsync(token, cancellationToken); - return result.Match( - success => Ok(), - notFound => Problem(AccountError.EmailChangeNotFound), - emailTaken => Problem(AccountError.EmailChangeAlreadyInUse)); + return result switch + { + Success<(Guid UserId, string OldEmail, string NewEmail)> => Ok(), + Results.NotFound => Problem(AccountError.EmailChangeNotFound), + EmailAlreadyInUse => Problem(AccountError.EmailChangeAlreadyInUse), + _ => throw new UnreachableException() + }; } } \ No newline at end of file diff --git a/API/Controller/Account/_Turnstile.cs b/API/Controller/Account/_Turnstile.cs index d01baaaa..47d6d519 100644 --- a/API/Controller/Account/_Turnstile.cs +++ b/API/Controller/Account/_Turnstile.cs @@ -3,6 +3,7 @@ using OpenShock.API.Errors; using OpenShock.API.Services.Turnstile; using OpenShock.Common.Problems; +using OpenShock.Common.Results; using OpenShock.Common.Utils; namespace OpenShock.API.Controller.Account; @@ -16,9 +17,9 @@ public sealed partial class AccountController private async Task 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 Error error) return null; - var cfErrors = turnStile.AsT1.Value; + var cfErrors = error.Value; if (cfErrors.All(err => err.IsClientError())) return Problem(TurnstileError.InvalidTurnstile); diff --git a/API/Controller/Admin/Configuration.cs b/API/Controller/Admin/Configuration.cs index 07b43995..e5079871 100644 --- a/API/Controller/Admin/Configuration.cs +++ b/API/Controller/Admin/Configuration.cs @@ -3,8 +3,10 @@ using OpenShock.API.Controller.Admin.DTOs; using OpenShock.Common.Errors; using OpenShock.Common.Problems; +using OpenShock.Common.Results; using OpenShock.Common.Services.Configuration; using System.Net.Mime; +using Results = OpenShock.Common.Results; namespace OpenShock.API.Controller.Admin; @@ -58,12 +60,13 @@ public async Task ConfigurationAdd([FromBody] ConfigurationAddIte body.Value ); - return result.Match( - success => Ok(), - alreadyExists => Problem(ConfigurationError.AlreadyExists(body.Name)), - invalidName => Problem(ConfigurationError.InvalidNameFormat(body.Name)), - invalidValue => Problem(ConfigurationError.InvalidValueFormat(body.Value)) - ); + return result switch + { + Success => Ok(), + AlreadyExists => Problem(ConfigurationError.AlreadyExists(body.Name)), + InvalidNameFormat => Problem(ConfigurationError.InvalidNameFormat(body.Name)), + InvalidValueFormat => Problem(ConfigurationError.InvalidValueFormat(body.Value)) + }; } /// @@ -89,12 +92,13 @@ public async Task ConfigurationUpdate([FromBody] ConfigurationUpd body.Value ); - return result.Match( - success => Ok(), - notFound => Problem(ConfigurationError.NotFound(body.Name)), - invalidName => Problem(ConfigurationError.InvalidNameFormat(body.Name)), - invalidValue => Problem(ConfigurationError.InvalidValueFormat(body.Value!)) - ); + return result switch + { + Success => Ok(), + Results.NotFound => Problem(ConfigurationError.NotFound(body.Name)), + InvalidNameFormat => Problem(ConfigurationError.InvalidNameFormat(body.Name)), + InvalidValueFormat => Problem(ConfigurationError.InvalidValueFormat(body.Value!)) + }; } /// @@ -114,10 +118,11 @@ public async Task ConfigurationDelete([FromRoute] string name, [F var result = await configurationService.TryDeleteItemAsync(name); - return result.Match( - success => Ok(), - notFound => Problem(ConfigurationError.NotFound(name)), - invalidName => Problem(ConfigurationError.InvalidNameFormat(name)) - ); + return result switch + { + Success => Ok(), + Results.NotFound => Problem(ConfigurationError.NotFound(name)), + InvalidNameFormat => Problem(ConfigurationError.InvalidNameFormat(name)) + }; } } \ No newline at end of file diff --git a/API/Controller/Admin/DeactivateUser.cs b/API/Controller/Admin/DeactivateUser.cs index 25a5e7ce..4ebbfefc 100644 --- a/API/Controller/Admin/DeactivateUser.cs +++ b/API/Controller/Admin/DeactivateUser.cs @@ -1,8 +1,12 @@ using System.ComponentModel.DataAnnotations; +using System.Diagnostics; using Microsoft.AspNetCore.Mvc; using OpenShock.API.Services.Account; using OpenShock.Common.Constants; using OpenShock.Common.Errors; +using OpenShock.Common.Results; +using AccountSvc = OpenShock.API.Services.Account; +using Results = OpenShock.Common.Results; namespace OpenShock.API.Controller.Admin; @@ -24,12 +28,14 @@ public async Task DeactivateUser( PedanticallyEnsureAdmin(); var deactivationResult = await accountService.DeactivateAccountAsync(CurrentUser.Id, userId, deleteLater, reason); - return deactivationResult.Match( - success => Ok("Account deactivated"), - cannotDeactivatePrivledged => Problem(AccountActivationError.CannotDeactivateOrDeletePrivledgedAccount), - alreadyDeactivated => Problem(AccountActivationError.AlreadyDeactivated), - unauthorized => Problem(AccountActivationError.Unauthorized), - notFound => NotFound("User not found") - ); + return deactivationResult switch + { + Success => Ok("Account deactivated"), + CannotDeactivatePrivilegedAccount => Problem(AccountActivationError.CannotDeactivateOrDeletePrivledgedAccount), + AccountDeactivationAlreadyInProgress => Problem(AccountActivationError.AlreadyDeactivated), + AccountSvc.Unauthorized => Problem(AccountActivationError.Unauthorized), + Results.NotFound => NotFound("User not found"), + _ => throw new UnreachableException() + }; } } \ No newline at end of file diff --git a/API/Controller/Admin/DeleteUser.cs b/API/Controller/Admin/DeleteUser.cs index 45349527..3fc6e433 100644 --- a/API/Controller/Admin/DeleteUser.cs +++ b/API/Controller/Admin/DeleteUser.cs @@ -1,8 +1,12 @@ using System.ComponentModel.DataAnnotations; +using System.Diagnostics; using Microsoft.AspNetCore.Mvc; using OpenShock.API.Services.Account; using OpenShock.Common.Constants; using OpenShock.Common.Errors; +using OpenShock.Common.Results; +using AccountSvc = OpenShock.API.Services.Account; +using Results = OpenShock.Common.Results; namespace OpenShock.API.Controller.Admin; @@ -23,11 +27,13 @@ public async Task DeleteUser( PedanticallyEnsureAdmin(); var result = await accountService.DeleteAccountAsync(CurrentUser.Id, userId, reason); - return result.Match( - success => Ok("Account deleted"), - cannotDeletePrivledged => Problem(AccountActivationError.CannotDeactivateOrDeletePrivledgedAccount), - unauthorized => Problem(AccountActivationError.Unauthorized), - notFound => NotFound("User not found") - ); + return result switch + { + Success => Ok("Account deleted"), + CannotDeletePrivilegedAccount => Problem(AccountActivationError.CannotDeactivateOrDeletePrivledgedAccount), + AccountSvc.Unauthorized => Problem(AccountActivationError.Unauthorized), + Results.NotFound => NotFound("User not found"), + _ => throw new UnreachableException() + }; } } \ No newline at end of file diff --git a/API/Controller/Admin/ReactivateUser.cs b/API/Controller/Admin/ReactivateUser.cs index 190eacb5..f298f466 100644 --- a/API/Controller/Admin/ReactivateUser.cs +++ b/API/Controller/Admin/ReactivateUser.cs @@ -1,8 +1,12 @@ using System.ComponentModel.DataAnnotations; +using System.Diagnostics; using Microsoft.AspNetCore.Mvc; using OpenShock.API.Services.Account; using OpenShock.Common.Constants; using OpenShock.Common.Errors; +using OpenShock.Common.Results; +using AccountSvc = OpenShock.API.Services.Account; +using Results = OpenShock.Common.Results; namespace OpenShock.API.Controller.Admin; @@ -23,10 +27,12 @@ public async Task ReactivateUser( PedanticallyEnsureAdmin(); var reactivationResult = await accountService.ReactivateAccountAsync(CurrentUser.Id, userId, reason); - return reactivationResult.Match( - success => Ok("Account reactivated"), - unauthorized => Problem(AccountActivationError.Unauthorized), - notFound => NotFound("User not found") - ); + return reactivationResult switch + { + Success => Ok("Account reactivated"), + AccountSvc.Unauthorized => Problem(AccountActivationError.Unauthorized), + Results.NotFound => NotFound("User not found"), + _ => throw new UnreachableException() + }; } } \ No newline at end of file diff --git a/API/Controller/Admin/WebhookAdd.cs b/API/Controller/Admin/WebhookAdd.cs index 1123e659..e8bee8bc 100644 --- a/API/Controller/Admin/WebhookAdd.cs +++ b/API/Controller/Admin/WebhookAdd.cs @@ -2,6 +2,8 @@ using Microsoft.AspNetCore.Mvc; using OpenShock.API.Controller.Admin.DTOs; using OpenShock.Common.Errors; +using OpenShock.Common.Models; +using OpenShock.Common.Results; using OpenShock.Common.Services.Webhook; namespace OpenShock.API.Controller.Admin; @@ -22,9 +24,10 @@ public async Task AddWebhook([FromBody] AddWebhookDto body, [From PedanticallyEnsureAdmin(); var result = await webhookService.AddWebhookAsync(body.Name, body.Url); - return result.Match( - success => Ok(success.Value), - unsupported => Problem(AdminError.WebhookOnlyDiscord) - ); + return result switch + { + Success success => Ok(success.Value), + UnsupportedWebhookUrl => Problem(AdminError.WebhookOnlyDiscord) + }; } } \ No newline at end of file diff --git a/API/Controller/Devices/DevicesController.cs b/API/Controller/Devices/DevicesController.cs index 38cfacb5..1d5f0bd6 100644 --- a/API/Controller/Devices/DevicesController.cs +++ b/API/Controller/Devices/DevicesController.cs @@ -3,7 +3,6 @@ using Asp.Versioning; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using OneOf; using OpenShock.API.Models.Requests; using OpenShock.API.Models.Response; using OpenShock.API.Services.DeviceUpdate; @@ -15,6 +14,7 @@ using OpenShock.Common.OpenShockDb; using OpenShock.Common.Problems; using OpenShock.Common.Redis; +using OpenShock.Common.Results; using OpenShock.Common.Utils; using Redis.OM; @@ -237,7 +237,7 @@ public async Task GetPairCode([FromRoute] Guid deviceId) public async Task GetLiveControlGatewayInfo([FromRoute] Guid deviceId) { var result = await ResolveDeviceGatewayAsync(deviceId); - if (result.TryPickT1(out var problem, out var gateway)) return Problem(problem); + if (result is not LcgNode gateway) return Problem((OpenShockProblem)result.Value!); return LegacyDataOk(new LcgResponse { @@ -259,7 +259,7 @@ public async Task GetLiveControlGatewayInfo([FromRoute] Guid devi public async Task GetLiveControlGatewayInfoV2([FromRoute] Guid deviceId) { var result = await ResolveDeviceGatewayAsync(deviceId); - if (result.TryPickT1(out var problem, out var gateway)) return Problem(problem); + if (result is not LcgNode gateway) return Problem((OpenShockProblem)result.Value!); return Ok(new LcgResponseV2 { @@ -275,7 +275,7 @@ public async Task GetLiveControlGatewayInfoV2([FromRoute] Guid de /// when the caller lacks access, the hub is offline, or it has /// no gateway. /// - private async Task> ResolveDeviceGatewayAsync(Guid deviceId) + private async Task> ResolveDeviceGatewayAsync(Guid deviceId) { // Check if user owns device or has a share var deviceExistsAndYouHaveAccess = await _db.Devices.AnyAsync(x => diff --git a/API/Controller/OAuth/HandOff.cs b/API/Controller/OAuth/HandOff.cs index 2e31b499..ccff2fc3 100644 --- a/API/Controller/OAuth/HandOff.cs +++ b/API/Controller/OAuth/HandOff.cs @@ -29,8 +29,9 @@ public async Task OAuthHandOff( CancellationToken cancellationToken) { var result = await ValidateOAuthFlowAsync(); - if (!result.TryPickT0(out var auth, out var error)) + if (result is not ValidatedFlowContext auth) { + var error = (OAuthValidationError)result.Value!; return error switch { OAuthValidationError.FlowStateMissing => RedirectFrontendError("oauthFlowNotStarted"), diff --git a/API/Controller/OAuth/SignupFinalize.cs b/API/Controller/OAuth/SignupFinalize.cs index 9aedbeaa..15ba65dc 100644 --- a/API/Controller/OAuth/SignupFinalize.cs +++ b/API/Controller/OAuth/SignupFinalize.cs @@ -7,7 +7,9 @@ using OpenShock.API.Services.OAuthConnection; using OpenShock.Common.Errors; using OpenShock.Common.Extensions; +using OpenShock.Common.OpenShockDb; using OpenShock.Common.Options; +using OpenShock.Common.Results; using System.Security.Claims; namespace OpenShock.API.Controller.OAuth; @@ -48,8 +50,9 @@ public async Task OAuthSignupFinalize( } var result = await ValidateOAuthFlowAsync(); - if (!result.TryPickT0(out var auth, out var error)) + if (result is not ValidatedFlowContext auth) { + var error = (OAuthValidationError)result.Value!; return error switch { OAuthValidationError.FlowStateMissing => Problem(OAuthError.FlowNotFound), @@ -99,7 +102,7 @@ public async Task OAuthSignupFinalize( isEmailTrusted ); - if (!created.TryPickT0(out var newUser, out _)) + if (created is not Success newUser) { // Username or email already exists — conflict. // Do NOT clear the flow cookie so the frontend can retry with a different username. diff --git a/API/Controller/OAuth/SignupGetData.cs b/API/Controller/OAuth/SignupGetData.cs index 0bfc3b47..7fcb82cb 100644 --- a/API/Controller/OAuth/SignupGetData.cs +++ b/API/Controller/OAuth/SignupGetData.cs @@ -31,8 +31,9 @@ public async Task OAuthSignupGetData([FromRoute] string provider) } var result = await ValidateOAuthFlowAsync(); - if (!result.TryPickT0(out var auth, out var error)) + if (result is not ValidatedFlowContext auth) { + var error = (OAuthValidationError)result.Value!; return error switch { OAuthValidationError.FlowStateMissing => Problem(OAuthError.FlowNotFound), diff --git a/API/Controller/OAuth/_ApiController.cs b/API/Controller/OAuth/_ApiController.cs index 05486d31..e575d196 100644 --- a/API/Controller/OAuth/_ApiController.cs +++ b/API/Controller/OAuth/_ApiController.cs @@ -2,10 +2,10 @@ using Asp.Versioning; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; -using OneOf; using OpenShock.API.OAuth; using OpenShock.API.Services.Account; using OpenShock.Common; +using OpenShock.Common.Results; namespace OpenShock.API.Controller.OAuth; @@ -39,7 +39,7 @@ private enum OAuthValidationError /// Validates: provider exists, temp cookie auth present, scheme matches, flow parsable. /// On success returns ValidatedFlowContext; on failure returns IActionResult with proper problem details. /// - private async Task> ValidateOAuthFlowAsync() + private async Task> ValidateOAuthFlowAsync() { // 1) authenticate temp cookie var auth = await HttpContext.AuthenticateAsync(OAuthConstants.FlowScheme); diff --git a/API/Controller/Sessions/SessionSelf.cs b/API/Controller/Sessions/SessionSelf.cs index 9e7a7cf4..bf54fb18 100644 --- a/API/Controller/Sessions/SessionSelf.cs +++ b/API/Controller/Sessions/SessionSelf.cs @@ -17,7 +17,7 @@ public sealed partial class SessionsController [HttpGet("self")] public LoginSessionResponse GetSelfSession([FromServices] IUserReferenceService userReferenceService) { - if (!userReferenceService.AuthReference.TryPickT0(out LoginSession? session, out _)) + if (userReferenceService.AuthReference is not LoginSession session) throw new UnreachableException("the [UserSessionOnly] attribute should have blocked caller"); return LoginSessionResponse.MapFrom(session); diff --git a/API/Controller/Shockers/SendControl.cs b/API/Controller/Shockers/SendControl.cs index df039fa2..25151713 100644 --- a/API/Controller/Shockers/SendControl.cs +++ b/API/Controller/Shockers/SendControl.cs @@ -1,4 +1,5 @@ - using System.Net.Mime; + using System.Diagnostics; +using System.Net.Mime; using Asp.Versioning; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; @@ -10,6 +11,7 @@ using OpenShock.Common.Models; using OpenShock.Common.OpenShockDb; using OpenShock.Common.Problems; +using OpenShock.Common.Results; using OpenShock.Common.Services; namespace OpenShock.API.Controller.Shockers; @@ -45,7 +47,7 @@ public async Task SendControl( }; ApiTokenControlLimits? tokenLimits = null; - if (userReferenceService.AuthReference.TryPickT1(out ApiToken? apiToken, out _)) + if (userReferenceService.AuthReference is ApiToken apiToken) { // A paused token may not control shockers. if (apiToken.ShockerControlPaused) return Problem(ApiTokenError.ApiTokenPaused); @@ -54,11 +56,14 @@ public async Task SendControl( } var controlAction = await controlSender.ControlByUser(body.Shocks, sender, userHub.Clients, tokenLimits); - return controlAction.Match( - success => LegacyEmptyOk("Successfully sent control messages"), - notFound => Problem(ShockerControlError.ShockerControlNotFound(notFound.Value)), - paused => Problem(ShockerControlError.ShockerControlPaused(paused.Value)), - noPermission => Problem(ShockerControlError.ShockerControlNoPermission(noPermission.Value))); + return controlAction switch + { + Success => LegacyEmptyOk("Successfully sent control messages"), + ShockerNotFoundOrNoAccess notFound => Problem(ShockerControlError.ShockerControlNotFound(notFound.Value)), + ShockerPaused paused => Problem(ShockerControlError.ShockerControlPaused(paused.Value)), + ShockerNoPermission noPermission => Problem(ShockerControlError.ShockerControlNoPermission(noPermission.Value)), + _ => throw new UnreachableException() + }; } /// diff --git a/API/Controller/Tokens/GetTokenSelf.cs b/API/Controller/Tokens/GetTokenSelf.cs index a250f918..d20d3779 100644 --- a/API/Controller/Tokens/GetTokenSelf.cs +++ b/API/Controller/Tokens/GetTokenSelf.cs @@ -57,7 +57,7 @@ public TokenResponseV2 GetSelfTokenV2([FromServices] IUserReferenceService userR private static ApiToken GetSelfTokenDto(IUserReferenceService userReferenceService) { - if (!userReferenceService.AuthReference.TryPickT1(out ApiToken? apiToken, out _)) + if (userReferenceService.AuthReference is not ApiToken apiToken) throw new UnreachableException("the [TokenOnly] attribute should have blocked caller"); return apiToken; diff --git a/API/Controller/Tokens/ReportTokens.cs b/API/Controller/Tokens/ReportTokens.cs index b91c2956..3ba1d2c8 100644 --- a/API/Controller/Tokens/ReportTokens.cs +++ b/API/Controller/Tokens/ReportTokens.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.RateLimiting; using OpenShock.API.Errors; using OpenShock.API.Services.Turnstile; +using OpenShock.Common.Results; using OpenShock.Common.Services.Webhook; namespace OpenShock.API.Controller.Tokens; @@ -36,9 +37,9 @@ public async Task ReportTokens( var remoteIP = HttpContext.GetRemoteIP(); var turnStile = await turnstileService.VerifyUserResponseTokenAsync(body.TurnstileResponse, remoteIP, cancellationToken); - if (!turnStile.IsT0) + if (turnStile is Error turnStileError) { - var cfErrors = turnStile.AsT1.Value; + var cfErrors = turnStileError.Value; if (cfErrors.All(err => err == CloudflareTurnstileError.InvalidResponse)) return Problem(TurnstileError.InvalidTurnstile); diff --git a/API/Services/Account/AccountService.cs b/API/Services/Account/AccountService.cs index 9dbe4324..0ebf41f7 100644 --- a/API/Services/Account/AccountService.cs +++ b/API/Services/Account/AccountService.cs @@ -1,11 +1,10 @@ using System.Net.Mail; using Microsoft.EntityFrameworkCore; using Npgsql; -using OneOf; -using OneOf.Types; using OpenShock.Common.Constants; using OpenShock.Common.Models; using OpenShock.Common.OpenShockDb; +using OpenShock.Common.Results; using OpenShock.Common.Services.Audit; using OpenShock.Common.Services.RedisPubSub; using OpenShock.Common.Services.Session; @@ -85,7 +84,7 @@ private async Task IsEmailProviderBlacklisted(string email) return await _db.EmailProviderBlacklists.AnyAsync(e => e.Domain == domain); } - private async Task, AccountWithEmailOrUsernameExists>> CreateAccount(string email, string username, string password, bool verifyOnCreation) + private async Task, AccountWithEmailOrUsernameExists>> CreateAccount(string email, string username, string password, bool verifyOnCreation) { email = email.ToLowerInvariant(); @@ -118,12 +117,12 @@ await _db.Users } /// - public async Task, AccountWithEmailOrUsernameExists>> CreateAccountWithActivationFlowAsync(string email, string username, string password) + public async Task, AccountWithEmailOrUsernameExists>> CreateAccountWithActivationFlowAsync(string email, string username, string password) { var accountCreate = await CreateAccount(email, username, password, false); - if (accountCreate.IsT1) return accountCreate; + if (accountCreate.Value is not Success created) return accountCreate; - var user = accountCreate.AsT0.Value; + var user = created.Value; // The real activation token is minted by the outbox delivery job at send time; here we record the // request (with a seeded hash) and durably enqueue the email. @@ -147,7 +146,7 @@ public Task IsEmailRegisteredAsync(string email, CancellationToken cancell return _db.Users.AnyAsync(u => u.Email == email, cancellationToken); } - public async Task, AccountWithEmailOrUsernameExists>> CreateOAuthOnlyAccountAsync( + public async Task, AccountWithEmailOrUsernameExists>> CreateOAuthOnlyAccountAsync( string email, string username, string provider, @@ -250,7 +249,7 @@ public async Task TryActivateAccountAsync(string secret, CancellationToken } /// - public async Task> DeactivateAccountAsync(Guid executingUserId, Guid userId, bool deleteLater, string? reason = null) + public async Task> DeactivateAccountAsync(Guid executingUserId, Guid userId, bool deleteLater, string? reason = null) { if (executingUserId != userId) { @@ -309,7 +308,7 @@ await _auditService.LogAsync( } /// - public async Task> ReactivateAccountAsync(Guid executingUserId, Guid userId, string? reason = null) + public async Task> ReactivateAccountAsync(Guid executingUserId, Guid userId, string? reason = null) { var user = await _db.Users.Include(u => u.UserDeactivation).FirstOrDefaultAsync(u => u.Id == userId && u.UserDeactivation != null); if (user is null) return new NotFound(); @@ -353,7 +352,7 @@ await _auditService.LogAsync( } /// - public async Task> DeleteAccountAsync(Guid executingUserId, Guid userId, string? reason = null) + public async Task> DeleteAccountAsync(Guid executingUserId, Guid userId, string? reason = null) { var isPrivileged = await _db.Users .Where(u => u.Id == executingUserId) @@ -396,7 +395,7 @@ await _auditService.LogAsync( } /// - public async Task> GetAccountByCredentialsAsync(string usernameOrEmail, string password, CancellationToken cancellationToken) + public async Task> GetAccountByCredentialsAsync(string usernameOrEmail, string password, CancellationToken cancellationToken) { var lowercaseUsernameOrEmail = usernameOrEmail.ToLowerInvariant(); var user = await _db.Users @@ -434,7 +433,7 @@ public async Task - public async Task> CheckPasswordResetExistsAsync(Guid passwordResetId, string secret, + public async Task> CheckPasswordResetExistsAsync(Guid passwordResetId, string secret, CancellationToken cancellationToken = default) { var validSince = DateTime.UtcNow - Duration.PasswordResetRequestLifetime; @@ -452,7 +451,7 @@ public async Task> CheckPasswordResetExi } /// - public async Task> CreatePasswordResetFlowAsync(string email) + public async Task> CreatePasswordResetFlowAsync(string email) { var validSince = DateTime.UtcNow - Duration.PasswordResetRequestLifetime; var lowerCaseEmail = email.ToLowerInvariant(); @@ -489,7 +488,7 @@ public async Task - public async Task> CompletePasswordResetFlowAsync(Guid passwordResetId, + public async Task> CompletePasswordResetFlowAsync(Guid passwordResetId, string secret, string newPassword) { var validSince = DateTime.UtcNow - Duration.PasswordResetRequestLifetime; @@ -543,12 +542,12 @@ await _auditService.LogAsync( } /// - public async Task> CheckUsernameAvailabilityAsync(string username, + public async Task> CheckUsernameAvailabilityAsync(string username, CancellationToken cancellationToken = default) { var validationResult = UsernameValidator.Validate(username); - if (validationResult.IsT1) - return validationResult.AsT1; + if (validationResult.Value is UsernameError usernameError) + return usernameError; if (await IsUserNameBlacklisted(username)) return new UsernameTaken(); // Don't inform the user about when the blacklist is hit @@ -560,7 +559,7 @@ public async Task> CheckUsernameAva } /// - public async Task> ChangeUsernameAsync(Guid userId, string username, Guid? actorId, bool ignoreLimit = false, CancellationToken cancellationToken = default) + public async Task> ChangeUsernameAsync(Guid userId, string username, Guid? actorId, bool ignoreLimit = false, CancellationToken cancellationToken = default) { if (!ignoreLimit) { @@ -572,8 +571,11 @@ public async Task u.UserDeactivation).FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); if (user is null) return new NotFound(); @@ -610,7 +612,7 @@ await _auditService.LogAsync( /// - public async Task> ChangePasswordAsync(Guid userId, string newPassword, Guid? actorId) + public async Task> ChangePasswordAsync(Guid userId, string newPassword, Guid? actorId) { var user = await _db.Users.Include(u => u.UserDeactivation).FirstOrDefaultAsync(x => x.Id == userId); if (user is null) return new NotFound(); @@ -636,7 +638,7 @@ await _auditService.LogAsync( } /// - public async Task> CreateEmailChangeFlowAsync(Guid userId, string newEmail, Guid? actorId) + public async Task> CreateEmailChangeFlowAsync(Guid userId, string newEmail, Guid? actorId) { var validSince = DateTime.UtcNow - Duration.EmailChangeRequestLifetime; @@ -703,7 +705,7 @@ await _auditService.LogAsync( return new Success(); } - public async Task, NotFound, EmailAlreadyInUse>> TryVerifyEmailAsync(string token, CancellationToken cancellationToken = default) + public async Task, NotFound, EmailAlreadyInUse>> TryVerifyEmailAsync(string token, CancellationToken cancellationToken = default) { var hash = HashingUtils.HashToken(token); var validSince = DateTime.UtcNow - Duration.EmailChangeRequestLifetime; diff --git a/API/Services/Account/IAccountService.cs b/API/Services/Account/IAccountService.cs index 71c6fce7..a3b6affe 100644 --- a/API/Services/Account/IAccountService.cs +++ b/API/Services/Account/IAccountService.cs @@ -1,6 +1,5 @@ -using OneOf; -using OneOf.Types; -using OpenShock.Common.OpenShockDb; +using OpenShock.Common.OpenShockDb; +using OpenShock.Common.Results; using OpenShock.Common.Validation; namespace OpenShock.API.Services.Account; @@ -17,7 +16,7 @@ public interface IAccountService /// /// /// - public Task, AccountWithEmailOrUsernameExists>> CreateAccountWithActivationFlowAsync(string email, string username, string password); + public Task, AccountWithEmailOrUsernameExists>> CreateAccountWithActivationFlowAsync(string email, string username, string password); /// /// Creates an OAuth-only (passwordless) account and links the external identity in a single transaction. @@ -31,7 +30,7 @@ public interface IAccountService /// display name from provider /// /// Success with the created user, or AccountWithEmailOrUsernameExists when taken/blocked. - Task, AccountWithEmailOrUsernameExists>> CreateOAuthOnlyAccountAsync(string email, string username, string provider, string providerAccountId, string? providerAccountName, bool isEmailTrusted); + Task, AccountWithEmailOrUsernameExists>> CreateOAuthOnlyAccountAsync(string email, string username, string provider, string providerAccountId, string? providerAccountName, bool isEmailTrusted); /// /// Returns true if the given email is already associated with an existing user account. @@ -46,11 +45,11 @@ public interface IAccountService /// Task TryActivateAccountAsync(string token, CancellationToken cancellationToken = default); - public Task> DeactivateAccountAsync(Guid executingUserId, Guid userId, bool deleteLater = true, string? reason = null); + public Task> DeactivateAccountAsync(Guid executingUserId, Guid userId, bool deleteLater = true, string? reason = null); - public Task> ReactivateAccountAsync(Guid executingUserId, Guid userId, string? reason = null); + public Task> ReactivateAccountAsync(Guid executingUserId, Guid userId, string? reason = null); - public Task> DeleteAccountAsync(Guid executingUserId, Guid userId, string? reason = null); + public Task> DeleteAccountAsync(Guid executingUserId, Guid userId, string? reason = null); /// /// Get a user by credentials @@ -59,7 +58,7 @@ public interface IAccountService /// /// /// - public Task> GetAccountByCredentialsAsync(string usernameOrEmail, string password, CancellationToken cancellationToken = default); + public Task> GetAccountByCredentialsAsync(string usernameOrEmail, string password, CancellationToken cancellationToken = default); /// /// Check if a password reset request exists and the secret is valid @@ -68,14 +67,14 @@ public interface IAccountService /// /// /// - public Task> CheckPasswordResetExistsAsync(Guid passwordResetId, string secret, CancellationToken cancellationToken = default); + public Task> CheckPasswordResetExistsAsync(Guid passwordResetId, string secret, CancellationToken cancellationToken = default); /// /// Creates a new password reset request and send the email if successful /// /// /// - public Task> CreatePasswordResetFlowAsync(string email); + public Task> CreatePasswordResetFlowAsync(string email); /// /// Completes a password reset process, sets a new password @@ -84,7 +83,7 @@ public interface IAccountService /// /// /// - public Task> CompletePasswordResetFlowAsync(Guid passwordResetId, string secret, string newPassword); + public Task> CompletePasswordResetFlowAsync(Guid passwordResetId, string secret, string newPassword); /// /// Check the availability of a username @@ -92,7 +91,7 @@ public interface IAccountService /// /// /// - public Task> CheckUsernameAvailabilityAsync(string username, CancellationToken cancellationToken = default); + public Task> CheckUsernameAvailabilityAsync(string username, CancellationToken cancellationToken = default); /// /// Change the username of a user @@ -103,7 +102,7 @@ public interface IAccountService /// Ignore the username change limit, set this to true when an admin is changing the username /// /// only returns when the result is != Available - public Task> ChangeUsernameAsync(Guid userId, string username, Guid? actorId, bool ignoreLimit = false, CancellationToken cancellationToken = default); + public Task> ChangeUsernameAsync(Guid userId, string username, Guid? actorId, bool ignoreLimit = false, CancellationToken cancellationToken = default); /// /// Change the password of a user @@ -112,7 +111,7 @@ public interface IAccountService /// /// User that performed this change /// - public Task> ChangePasswordAsync(Guid userId, string newPassword, Guid? actorId); + public Task> ChangePasswordAsync(Guid userId, string newPassword, Guid? actorId); /// /// Creates a new email change request and sends a verification email to the new address. @@ -122,7 +121,7 @@ public interface IAccountService /// Requested new email address. /// User that called this /// - public Task> CreateEmailChangeFlowAsync(Guid userId, string newEmail, Guid? actorId); + public Task> CreateEmailChangeFlowAsync(Guid userId, string newEmail, Guid? actorId); /// /// Verifies a pending email change using the supplied token. On success the user's email is updated. @@ -133,7 +132,7 @@ public interface IAccountService /// /// /// - Task, NotFound, EmailAlreadyInUse>> TryVerifyEmailAsync(string token, CancellationToken cancellationToken = default); + Task, NotFound, EmailAlreadyInUse>> TryVerifyEmailAsync(string token, CancellationToken cancellationToken = default); } public readonly struct AccountIsOAuthOnly; diff --git a/API/Services/Turnstile/CloudflareTurnstileService.cs b/API/Services/Turnstile/CloudflareTurnstileService.cs index 0b53e23e..c46c747d 100644 --- a/API/Services/Turnstile/CloudflareTurnstileService.cs +++ b/API/Services/Turnstile/CloudflareTurnstileService.cs @@ -1,7 +1,6 @@ using System.Net; -using OneOf; -using OneOf.Types; using OpenShock.API.Options; +using OpenShock.Common.Results; namespace OpenShock.API.Services.Turnstile; @@ -43,7 +42,7 @@ private static CloudflareTurnstileError MapCfError(string error) } /// - public async Task>> VerifyUserResponseTokenAsync( + public async Task>> VerifyUserResponseTokenAsync( string responseToken, IPAddress? remoteIpAddress, CancellationToken cancellationToken = default) { if (!_options.Enabled) return new Success(); diff --git a/API/Services/Turnstile/ICloudflareTurnstileService.cs b/API/Services/Turnstile/ICloudflareTurnstileService.cs index 0b167058..4a6ff191 100644 --- a/API/Services/Turnstile/ICloudflareTurnstileService.cs +++ b/API/Services/Turnstile/ICloudflareTurnstileService.cs @@ -1,5 +1,5 @@ using System.Net; -using OneOf.Types; +using OpenShock.Common.Results; namespace OpenShock.API.Services.Turnstile; @@ -12,6 +12,6 @@ public interface ICloudflareTurnstileService /// /// /// Success, No response token was supplied, internal error in cloudflare turnstile, business logic error on turnstile validation - public Task>> VerifyUserResponseTokenAsync( + public Task>> VerifyUserResponseTokenAsync( string responseToken, IPAddress? remoteIpAddress, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/Common.Tests/Validation/UsernameValidatorTests.cs b/Common.Tests/Validation/UsernameValidatorTests.cs index d91dc7e6..e40550eb 100644 --- a/Common.Tests/Validation/UsernameValidatorTests.cs +++ b/Common.Tests/Validation/UsernameValidatorTests.cs @@ -1,4 +1,5 @@ -using OpenShock.Common.Validation; +using OpenShock.Common.Results; +using OpenShock.Common.Validation; namespace OpenShock.Common.Tests.Validation; @@ -14,7 +15,7 @@ public async Task Validate_ValidUsername_ReturnsSuccess() var result = UsernameValidator.Validate(username); // Assert - await Assert.That(result.IsT0).IsTrue(); + await Assert.That(result is Success).IsTrue(); } [Test] @@ -27,8 +28,8 @@ public async Task Validate_TooShort_ReturnsError() var result = UsernameValidator.Validate(username); // Assert - await Assert.That(result.IsT1).IsTrue(); - await Assert.That(result.AsT1.Type).IsEqualTo(UsernameErrorType.TooShort); + await Assert.That(result is UsernameError).IsTrue(); + await Assert.That(((UsernameError)result.Value!).Type).IsEqualTo(UsernameErrorType.TooShort); } [Test] @@ -41,8 +42,8 @@ public async Task Validate_TooLong_ReturnsError() var result = UsernameValidator.Validate(username); // Assert - await Assert.That(result.IsT1).IsTrue(); - await Assert.That(result.AsT1.Type).IsEqualTo(UsernameErrorType.TooLong); + await Assert.That(result is UsernameError).IsTrue(); + await Assert.That(((UsernameError)result.Value!).Type).IsEqualTo(UsernameErrorType.TooLong); } [Test] @@ -55,8 +56,8 @@ public async Task Validate_StartWithWhitespace_ReturnsError() var result = UsernameValidator.Validate(username); // Assert - await Assert.That(result.IsT1).IsTrue(); - await Assert.That(result.AsT1.Type).IsEqualTo(UsernameErrorType.StartOrEndWithWhitespace); + await Assert.That(result is UsernameError).IsTrue(); + await Assert.That(((UsernameError)result.Value!).Type).IsEqualTo(UsernameErrorType.StartOrEndWithWhitespace); } [Test] @@ -69,8 +70,8 @@ public async Task Validate_EndWithWhitespace_ReturnsError() var result = UsernameValidator.Validate(username); // Assert - await Assert.That(result.IsT1).IsTrue(); - await Assert.That(result.AsT1.Type).IsEqualTo(UsernameErrorType.StartOrEndWithWhitespace); + await Assert.That(result is UsernameError).IsTrue(); + await Assert.That(((UsernameError)result.Value!).Type).IsEqualTo(UsernameErrorType.StartOrEndWithWhitespace); } [Test] @@ -83,8 +84,8 @@ public async Task Validate_ResembleEmail_ReturnsError() var result = UsernameValidator.Validate(username); // Assert - await Assert.That(result.IsT1).IsTrue(); - await Assert.That(result.AsT1.Type).IsEqualTo(UsernameErrorType.ResembleEmail); + await Assert.That(result is UsernameError).IsTrue(); + await Assert.That(((UsernameError)result.Value!).Type).IsEqualTo(UsernameErrorType.ResembleEmail); } [Test] @@ -97,8 +98,8 @@ public async Task Validate_ContainsObnoxiousCharacters_ReturnsError() var result = UsernameValidator.Validate(username); // Assert - await Assert.That(result.IsT1).IsTrue(); - await Assert.That(result.AsT1.Type).IsEqualTo(UsernameErrorType.ObnoxiousCharacters); + await Assert.That(result is UsernameError).IsTrue(); + await Assert.That(((UsernameError)result.Value!).Type).IsEqualTo(UsernameErrorType.ObnoxiousCharacters); } // --- Boundary value tests --- @@ -108,7 +109,7 @@ public async Task Validate_ExactMinLength_ReturnsSuccess() { // HardLimits.UsernameMinLength = 3 var result = UsernameValidator.Validate("abc"); - await Assert.That(result.IsT0).IsTrue(); + await Assert.That(result is Success).IsTrue(); } [Test] @@ -116,8 +117,8 @@ public async Task Validate_OneBelowMinLength_ReturnsTooShort() { // 2 chars = below min of 3 var result = UsernameValidator.Validate("ab"); - await Assert.That(result.IsT1).IsTrue(); - await Assert.That(result.AsT1.Type).IsEqualTo(UsernameErrorType.TooShort); + await Assert.That(result is UsernameError).IsTrue(); + await Assert.That(((UsernameError)result.Value!).Type).IsEqualTo(UsernameErrorType.TooShort); } [Test] @@ -125,7 +126,7 @@ public async Task Validate_ExactMaxLength_ReturnsSuccess() { // HardLimits.UsernameMaxLength = 32 var result = UsernameValidator.Validate(new string('a', 32)); - await Assert.That(result.IsT0).IsTrue(); + await Assert.That(result is Success).IsTrue(); } [Test] @@ -133,22 +134,22 @@ public async Task Validate_OneAboveMaxLength_ReturnsTooLong() { // 33 chars = above max of 32 var result = UsernameValidator.Validate(new string('a', 33)); - await Assert.That(result.IsT1).IsTrue(); - await Assert.That(result.AsT1.Type).IsEqualTo(UsernameErrorType.TooLong); + await Assert.That(result is UsernameError).IsTrue(); + await Assert.That(((UsernameError)result.Value!).Type).IsEqualTo(UsernameErrorType.TooLong); } [Test] public async Task Validate_WithHyphensAndUnderscores_ReturnsSuccess() { var result = UsernameValidator.Validate("test-user_123"); - await Assert.That(result.IsT0).IsTrue(); + await Assert.That(result is Success).IsTrue(); } [Test] public async Task Validate_WithDots_ReturnsSuccess() { var result = UsernameValidator.Validate("test.user"); - await Assert.That(result.IsT0).IsTrue(); + await Assert.That(result is Success).IsTrue(); } [Test] @@ -156,23 +157,23 @@ public async Task Validate_WithMiddleSpaces_ReturnsSuccess() { // Middle spaces are allowed, only leading/trailing are rejected var result = UsernameValidator.Validate("test user"); - await Assert.That(result.IsT0).IsTrue(); + await Assert.That(result is Success).IsTrue(); } [Test] public async Task Validate_TabAtStart_ReturnsWhitespaceError() { var result = UsernameValidator.Validate("\tTestUser"); - await Assert.That(result.IsT1).IsTrue(); - await Assert.That(result.AsT1.Type).IsEqualTo(UsernameErrorType.StartOrEndWithWhitespace); + await Assert.That(result is UsernameError).IsTrue(); + await Assert.That(((UsernameError)result.Value!).Type).IsEqualTo(UsernameErrorType.StartOrEndWithWhitespace); } [Test] public async Task Validate_AtSignInMiddle_ReturnsResembleEmail() { var result = UsernameValidator.Validate("user@name"); - await Assert.That(result.IsT1).IsTrue(); - await Assert.That(result.AsT1.Type).IsEqualTo(UsernameErrorType.ResembleEmail); + await Assert.That(result is UsernameError).IsTrue(); + await Assert.That(((UsernameError)result.Value!).Type).IsEqualTo(UsernameErrorType.ResembleEmail); } [Test] @@ -180,8 +181,8 @@ public async Task Validate_ZeroWidthJoiner_ReturnsObnoxious() { // Zero-width joiner U+200D var result = UsernameValidator.Validate("test\u200Duser"); - await Assert.That(result.IsT1).IsTrue(); - await Assert.That(result.AsT1.Type).IsEqualTo(UsernameErrorType.ObnoxiousCharacters); + await Assert.That(result is UsernameError).IsTrue(); + await Assert.That(((UsernameError)result.Value!).Type).IsEqualTo(UsernameErrorType.ObnoxiousCharacters); } [Test] @@ -189,7 +190,7 @@ public async Task Validate_RightToLeftOverride_ReturnsObnoxious() { // U+202E Right-to-left override var result = UsernameValidator.Validate("test\u202Euser"); - await Assert.That(result.IsT1).IsTrue(); - await Assert.That(result.AsT1.Type).IsEqualTo(UsernameErrorType.ObnoxiousCharacters); + await Assert.That(result is UsernameError).IsTrue(); + await Assert.That(((UsernameError)result.Value!).Type).IsEqualTo(UsernameErrorType.ObnoxiousCharacters); } } diff --git a/Common/Authentication/Attributes/TokenPermissionAttribute.cs b/Common/Authentication/Attributes/TokenPermissionAttribute.cs index 438af984..b8f04156 100644 --- a/Common/Authentication/Attributes/TokenPermissionAttribute.cs +++ b/Common/Authentication/Attributes/TokenPermissionAttribute.cs @@ -1,11 +1,11 @@ using Microsoft.AspNetCore.Mvc.Filters; -using OneOf.Types; using OpenShock.Common.Authentication.Services; using OpenShock.Common.Errors; using OpenShock.Common.Models; using OpenShock.Common.OpenShockDb; using OpenShock.Common.Problems; using OpenShock.Common.Redis; +using OpenShock.Common.Results; namespace OpenShock.Common.Authentication.Attributes; @@ -43,7 +43,12 @@ public void OnAuthorization(AuthorizationFilterContext context) { var userReference = context.HttpContext.RequestServices.GetRequiredService(); - var problem = userReference.AuthReference.Match(LoginSessionMatch, ApiTokenMatch, NoneMatch); + var problem = userReference.AuthReference switch + { + LoginSession loginSession => LoginSessionMatch(loginSession), + ApiToken apiToken => ApiTokenMatch(apiToken), + None none => NoneMatch(none) + }; if (problem is not null) { diff --git a/Common/Authentication/ControllerBase/AuthenticatedSessionControllerBase.cs b/Common/Authentication/ControllerBase/AuthenticatedSessionControllerBase.cs index 421abd91..56e1c62c 100644 --- a/Common/Authentication/ControllerBase/AuthenticatedSessionControllerBase.cs +++ b/Common/Authentication/ControllerBase/AuthenticatedSessionControllerBase.cs @@ -1,11 +1,11 @@ using System.Diagnostics; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using OneOf.Types; using OpenShock.Common.Authentication.Services; using OpenShock.Common.Models; using OpenShock.Common.OpenShockDb; using OpenShock.Common.Redis; +using OpenShock.Common.Results; namespace OpenShock.Common.Authentication.ControllerBase; @@ -28,10 +28,11 @@ public void OnActionExecuted(ActionExecutedContext context) protected bool IsAllowed(PermissionType requiredType) { var userReferenceService = HttpContext.RequestServices.GetRequiredService(); - return userReferenceService.AuthReference.Match( - (LoginSession _) => true, // We are in a session - (ApiToken apiToken) => requiredType.IsAllowed(apiToken.Permissions), - (None _) => throw new UnreachableException("User should be authenticated here") - ); + return userReferenceService.AuthReference switch + { + LoginSession => true, // We are in a session + ApiToken apiToken => requiredType.IsAllowed(apiToken.Permissions), + None => throw new UnreachableException("User should be authenticated here") + }; } } \ No newline at end of file diff --git a/Common/Authentication/Services/UserReferenceService.cs b/Common/Authentication/Services/UserReferenceService.cs index 5b689bae..bd654ca1 100644 --- a/Common/Authentication/Services/UserReferenceService.cs +++ b/Common/Authentication/Services/UserReferenceService.cs @@ -1,16 +1,15 @@ using OpenShock.Common.OpenShockDb; using OpenShock.Common.Redis; -using OneOf; -using OneOf.Types; +using OpenShock.Common.Results; namespace OpenShock.Common.Authentication.Services; public interface IUserReferenceService { - public OneOf AuthReference { get; set; } + public Union3 AuthReference { get; set; } } public sealed class UserReferenceService : IUserReferenceService { - public OneOf AuthReference { get; set; } = new None(); + public Union3 AuthReference { get; set; } = new None(); } \ No newline at end of file diff --git a/Common/Common.csproj b/Common/Common.csproj index a1eb241f..dabf1022 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -28,7 +28,6 @@ - diff --git a/Common/DataAnnotations/UsernameAttribute.cs b/Common/DataAnnotations/UsernameAttribute.cs index c0b366d7..47f552e4 100644 --- a/Common/DataAnnotations/UsernameAttribute.cs +++ b/Common/DataAnnotations/UsernameAttribute.cs @@ -1,7 +1,9 @@ using System.ComponentModel.DataAnnotations; +using System.Diagnostics; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; using OpenShock.Common.DataAnnotations.Interfaces; +using OpenShock.Common.Results; using OpenShock.Common.Validation; namespace OpenShock.Common.DataAnnotations; @@ -44,11 +46,13 @@ public sealed class UsernameAttribute : ValidationAttribute, IParameterAttribute if (value is not string displayName) return new ValidationResult(ErrMsgMustBeString); var result = UsernameValidator.Validate(displayName); - - return result.Match( - _ => ValidationResult.Success, - error => new ValidationResult($"{error.Type} - {error.Message}") - ); + + return result switch + { + Success => ValidationResult.Success, + UsernameError error => new ValidationResult($"{error.Type} - {error.Message}"), + _ => throw new UnreachableException() + }; } /// diff --git a/Common/Hubs/PublicShareHub.cs b/Common/Hubs/PublicShareHub.cs index 15fcf4f4..a559dd7a 100644 --- a/Common/Hubs/PublicShareHub.cs +++ b/Common/Hubs/PublicShareHub.cs @@ -54,7 +54,7 @@ public override async Task OnConnectedAsync() } } - _tokenPermissions = _userReferenceService.AuthReference.TryPickT1(out ApiToken? apiToken, out _) ? apiToken.Permissions : null; + _tokenPermissions = _userReferenceService.AuthReference is ApiToken apiToken ? apiToken.Permissions : null; var exists = await _db.PublicShares.AnyAsync(x => x.Id == id && (x.ExpiresAt == null || x.ExpiresAt > DateTime.UtcNow)); if (!exists) diff --git a/Common/Hubs/UserHub.cs b/Common/Hubs/UserHub.cs index 940868ac..76136015 100644 --- a/Common/Hubs/UserHub.cs +++ b/Common/Hubs/UserHub.cs @@ -39,7 +39,7 @@ public UserHub(ILogger logger, OpenShockContext db, IRedisConnectionPro public override async Task OnConnectedAsync() { - _tokenPermissions = _userReferenceService.AuthReference.TryPickT1(out ApiToken? apiToken, out _) ? apiToken.Permissions : null; + _tokenPermissions = _userReferenceService.AuthReference is ApiToken apiToken ? apiToken.Permissions : null; await Clients.Caller.Welcome(Context.ConnectionId); var devicesOnline = _provider.RedisCollection(false); @@ -93,7 +93,7 @@ public async Task ControlV2(IReadOnlyList shocks, }).FirstAsync(); ApiTokenControlLimits? tokenLimits = null; - if (_userReferenceService.AuthReference.TryPickT1(out ApiToken? apiToken, out _)) + if (_userReferenceService.AuthReference is ApiToken apiToken) { // A paused token may not control shockers. if (apiToken.ShockerControlPaused) return; diff --git a/Common/Results/CommonResultCases.cs b/Common/Results/CommonResultCases.cs new file mode 100644 index 00000000..7c77e474 --- /dev/null +++ b/Common/Results/CommonResultCases.cs @@ -0,0 +1,22 @@ +namespace OpenShock.Common.Results; + +// Shared case types for use in C# union declarations, replacing the equivalent +// marker types formerly provided by OneOf.Types (OneOf package). + +public readonly struct Success; + +public readonly struct Success(T value) +{ + public T Value { get; } = value; +} + +public readonly struct NotFound; + +public readonly struct Error; + +public readonly struct Error(T value) +{ + public T Value { get; } = value; +} + +public readonly struct None; diff --git a/Common/Results/Unions.cs b/Common/Results/Unions.cs new file mode 100644 index 00000000..47f3fe87 --- /dev/null +++ b/Common/Results/Unions.cs @@ -0,0 +1,19 @@ +namespace OpenShock.Common.Results; + +// Generic structural unions used throughout the codebase in place of OneOf. +// These compose arbitrary existing types into a closed, exhaustively-matchable set, +// mirroring how OneOf was used. See: https://github.com/dotnet/csharplang/blob/main/proposals/unions.md + +public union Union2(T0, T1); + +public union Union3(T0, T1, T2); + +public union Union4(T0, T1, T2, T3); + +public union Union5(T0, T1, T2, T3, T4); + +public union Union6(T0, T1, T2, T3, T4, T5); + +public union Union7(T0, T1, T2, T3, T4, T5, T6); + +public union Union8(T0, T1, T2, T3, T4, T5, T6, T7); diff --git a/Common/Services/Configuration/ConfigurationService.cs b/Common/Services/Configuration/ConfigurationService.cs index 5a2757ad..f19d11ed 100644 --- a/Common/Services/Configuration/ConfigurationService.cs +++ b/Common/Services/Configuration/ConfigurationService.cs @@ -1,13 +1,12 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Hybrid; using Npgsql; -using OneOf; -using OneOf.Types; using OpenShock.Common.OpenShockDb; using System.Buffers; using System.Globalization; using System.Text.Json; using OpenShock.Common.JsonSerialization; +using OpenShock.Common.Results; namespace OpenShock.Common.Services.Configuration; @@ -60,7 +59,7 @@ private static bool IsValidValueFormat(ConfigurationValueType type, string value }; } - public async Task> TryAddItemAsync(string name, string description, ConfigurationValueType type, string value) + public async Task> TryAddItemAsync(string name, string description, ConfigurationValueType type, string value) { // Validate name (only uppercase letters and underscores) if (!IsValidName(name)) @@ -102,7 +101,7 @@ public async Task> TryUpdateItemAsync(string name, string? description, string? value) + public async Task> TryUpdateItemAsync(string name, string? description, string? value) { // Validate name if (!IsValidName(name)) @@ -148,7 +147,7 @@ await _cache.SetAsync( return new Success(); } - public async Task> TryDeleteItemAsync(string name) + public async Task> TryDeleteItemAsync(string name) { // Find the item var item = await _db.Configuration.FirstOrDefaultAsync(ci => ci.Name == name); @@ -223,7 +222,7 @@ private async Task SetValueAsync(string name, string newValue, Configurati return true; } - public async Task> TryGetStringAsync(string name) + public async Task> TryGetStringAsync(string name) { var pair = await TryGetTypeValuePair(name); if (pair is null) return new NotFound(); @@ -234,7 +233,7 @@ public async Task> TryGetStringAsync(s public Task TrySetStringAsync(string name, string value) => SetValueAsync(name, value, ConfigurationValueType.String); - public async Task> TryGetBoolAsync(string name) + public async Task> TryGetBoolAsync(string name) { var pair = await TryGetTypeValuePair(name); if (pair is null) return new NotFound(); @@ -250,7 +249,7 @@ public async Task> T public Task TrySetBoolAsync(string name, bool value) => SetValueAsync(name, value.ToString(), ConfigurationValueType.Bool); - public async Task> TryGetIntAsync(string name) + public async Task> TryGetIntAsync(string name) { var pair = await TryGetTypeValuePair(name); if (pair is null) return new NotFound(); @@ -266,7 +265,7 @@ public async Task> Tr public Task TrySetIntAsync(string name, int value) => SetValueAsync(name, value.ToString(), ConfigurationValueType.Int); - public async Task> TryGetFloatAsync(string name) + public async Task> TryGetFloatAsync(string name) { var pair = await TryGetTypeValuePair(name); if (pair is null) return new NotFound(); @@ -282,7 +281,7 @@ public async Task> public Task TrySetFloatAsync(string name, float value) => SetValueAsync(name, value.ToString("R"), ConfigurationValueType.Float); - public async Task> TryGetJsonAsync(string name) + public async Task> TryGetJsonAsync(string name) { var pair = await TryGetTypeValuePair(name); if (pair is null) return new NotFound(); diff --git a/Common/Services/Configuration/IConfigurationService.cs b/Common/Services/Configuration/IConfigurationService.cs index 89356c6d..286f5a52 100644 --- a/Common/Services/Configuration/IConfigurationService.cs +++ b/Common/Services/Configuration/IConfigurationService.cs @@ -1,6 +1,5 @@ -using OneOf; -using OneOf.Types; -using OpenShock.Common.OpenShockDb; +using OpenShock.Common.OpenShockDb; +using OpenShock.Common.Results; namespace OpenShock.Common.Services.Configuration; @@ -8,23 +7,23 @@ public interface IConfigurationService { IQueryable GetAllItemsQuery(); - Task> TryAddItemAsync(string name, string description, ConfigurationValueType type, string value); - Task> TryUpdateItemAsync(string name, string? description, string? value); - Task> TryDeleteItemAsync(string name); + Task> TryAddItemAsync(string name, string description, ConfigurationValueType type, string value); + Task> TryUpdateItemAsync(string name, string? description, string? value); + Task> TryDeleteItemAsync(string name); - Task> TryGetStringAsync(string name); + Task> TryGetStringAsync(string name); Task TrySetStringAsync(string name, string value); - Task> TryGetBoolAsync(string name); + Task> TryGetBoolAsync(string name); Task TrySetBoolAsync(string name, bool value); - Task> TryGetIntAsync(string name); + Task> TryGetIntAsync(string name); Task TrySetIntAsync(string name, int value); - Task> TryGetFloatAsync(string name); + Task> TryGetFloatAsync(string name); Task TrySetFloatAsync(string name, float value); - Task> TryGetJsonAsync(string name); + Task> TryGetJsonAsync(string name); Task TrySetJsonAsync(string name, T value); } diff --git a/Common/Services/ControlSender.cs b/Common/Services/ControlSender.cs index 92698913..d274590a 100644 --- a/Common/Services/ControlSender.cs +++ b/Common/Services/ControlSender.cs @@ -1,7 +1,5 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; -using OneOf; -using OneOf.Types; using OpenShock.Common.Constants; using OpenShock.Common.DeviceControl; using OpenShock.Common.Extensions; @@ -10,6 +8,7 @@ using OpenShock.Common.Models.WebSocket.User; using OpenShock.Common.OpenShockDb; using OpenShock.Common.Redis.PubSub; +using OpenShock.Common.Results; using OpenShock.Common.Services.RedisPubSub; using OpenShock.Common.Utils; @@ -26,7 +25,7 @@ public ControlSender(OpenShockContext db, IRedisPubService publisher) _publisher = publisher; } - public async Task> ControlByUser(IReadOnlyList controls,ControlLogSender sender, IHubClients hubClients, ApiTokenControlLimits? tokenLimits = null) + public async Task> ControlByUser(IReadOnlyList controls,ControlLogSender sender, IHubClients hubClients, ApiTokenControlLimits? tokenLimits = null) { var shockers = await _db.Shockers .AsNoTracking() @@ -61,7 +60,7 @@ public async Task> ControlPublicShare(IReadOnlyList controls, ControlLogSender sender, IHubClients hubClients, Guid publicShareId) + public async Task> ControlPublicShare(IReadOnlyList controls, ControlLogSender sender, IHubClients hubClients, Guid publicShareId) { var publicShareShockers = await _db.PublicShareShockerMappings .AsNoTracking() @@ -99,7 +98,7 @@ private static void Clamp(Control control, SharePermsAndLimits? limits) control.Duration = Math.Clamp(control.Duration, HardLimits.MinControlDuration, durationMax); } - private async Task> ControlInternal(IReadOnlyList controls, ControlLogSender sender, IHubClients hubClients, ControlShockerObj[] allowedShockers, ApiTokenControlLimits? tokenLimits = null) + private async Task> ControlInternal(IReadOnlyList controls, ControlLogSender sender, IHubClients hubClients, ControlShockerObj[] allowedShockers, ApiTokenControlLimits? tokenLimits = null) { var shockersById = allowedShockers.ToDictionary(s => s.ShockerId, s => s); diff --git a/Common/Services/IControlSender.cs b/Common/Services/IControlSender.cs index 28c25d1d..70c806dd 100644 --- a/Common/Services/IControlSender.cs +++ b/Common/Services/IControlSender.cs @@ -1,17 +1,16 @@ using Microsoft.AspNetCore.SignalR; -using OneOf; -using OneOf.Types; using OpenShock.Common.Hubs; using OpenShock.Common.Models; using OpenShock.Common.Models.WebSocket.User; +using OpenShock.Common.Results; namespace OpenShock.Common.Services; public interface IControlSender { - public Task> ControlByUser(IReadOnlyList controls, ControlLogSender sender, IHubClients hubClients, ApiTokenControlLimits? tokenLimits = null); + public Task> ControlByUser(IReadOnlyList controls, ControlLogSender sender, IHubClients hubClients, ApiTokenControlLimits? tokenLimits = null); - public Task> ControlPublicShare(IReadOnlyList controls, ControlLogSender sender, IHubClients hubClients, Guid publicShareId); + public Task> ControlPublicShare(IReadOnlyList controls, ControlLogSender sender, IHubClients hubClients, Guid publicShareId); } public readonly record struct ShockerNotFoundOrNoAccess(Guid Value); diff --git a/Common/Services/Webhook/IWebhookService.cs b/Common/Services/Webhook/IWebhookService.cs index 10dd7603..f3618cb6 100644 --- a/Common/Services/Webhook/IWebhookService.cs +++ b/Common/Services/Webhook/IWebhookService.cs @@ -1,17 +1,16 @@ using System.Drawing; -using OneOf; -using OneOf.Types; using OpenShock.Common.Models; +using OpenShock.Common.Results; namespace OpenShock.Common.Services.Webhook; public interface IWebhookService { - public Task, UnsupportedWebhookUrl>> AddWebhookAsync(string name, Uri webhookUrl); + public Task, UnsupportedWebhookUrl>> AddWebhookAsync(string name, Uri webhookUrl); public Task RemoveWebhookAsync(Guid webhookId); public Task GetWebhooksAsync(); - public Task> SendWebhookAsync(string webhookName, string title, string content, Color color); + public Task> SendWebhookAsync(string webhookName, string title, string content, Color color); } public struct UnsupportedWebhookUrl; diff --git a/Common/Services/Webhook/WebhookService.cs b/Common/Services/Webhook/WebhookService.cs index d1acf67b..06cad8d8 100644 --- a/Common/Services/Webhook/WebhookService.cs +++ b/Common/Services/Webhook/WebhookService.cs @@ -1,9 +1,8 @@ using System.Drawing; using Microsoft.EntityFrameworkCore; -using OneOf; -using OneOf.Types; using OpenShock.Common.Models; using OpenShock.Common.OpenShockDb; +using OpenShock.Common.Results; namespace OpenShock.Common.Services.Webhook; @@ -21,7 +20,7 @@ public WebhookService(OpenShockContext db, HttpClient httpClient) private static string GetWebhookUrl(long webhookId, string webhookToken) => $"https://discord.com/api/webhooks/{webhookId}/{webhookToken}"; - public async Task, UnsupportedWebhookUrl>> AddWebhookAsync(string name, Uri webhookUrl) + public async Task, UnsupportedWebhookUrl>> AddWebhookAsync(string name, Uri webhookUrl) { if (webhookUrl is not { @@ -76,7 +75,7 @@ public async Task GetWebhooksAsync() .ToArrayAsync(); } - public async Task> SendWebhookAsync(string webhookName, string title, string content, Color color) + public async Task> SendWebhookAsync(string webhookName, string title, string content, Color color) { var webhook = await _db.DiscordWebhooks .Where(w => w.Name == webhookName) diff --git a/Common/Utils/JsonWebSocketUtils.cs b/Common/Utils/JsonWebSocketUtils.cs index 9dc519e2..50061986 100644 --- a/Common/Utils/JsonWebSocketUtils.cs +++ b/Common/Utils/JsonWebSocketUtils.cs @@ -4,16 +4,17 @@ using Microsoft.IO; using OpenShock.Common.JsonSerialization; using OpenShock.Common.Models.WebSocket; +using OpenShock.Common.Results; namespace OpenShock.Common.Utils; public static class JsonWebSocketUtils { private const uint MaxMessageSize = 512_000; // 512 000 bytes - + public static readonly RecyclableMemoryStreamManager RecyclableMemory = new(); - public static async Task> ReceiveFullMessageAsyncNonAlloc(WebSocket socket, CancellationToken cancellationToken = default) + public static async Task> ReceiveFullMessageAsyncNonAlloc(WebSocket socket, CancellationToken cancellationToken = default) { var buffer = ArrayPool.Shared.Rent(4096); try diff --git a/Common/Validation/UsernameValidator.cs b/Common/Validation/UsernameValidator.cs index 2faca0a0..e83d8c9b 100644 --- a/Common/Validation/UsernameValidator.cs +++ b/Common/Validation/UsernameValidator.cs @@ -1,13 +1,12 @@ using System.Diagnostics.CodeAnalysis; -using OneOf; -using OneOf.Types; using OpenShock.Common.Constants; +using OpenShock.Common.Results; namespace OpenShock.Common.Validation; public static class UsernameValidator { - public static OneOf Validate(string username) + public static Union2 Validate(string username) { if (username.Length < HardLimits.UsernameMinLength) { diff --git a/Common/Websocket/WebsockBaseController.cs b/Common/Websocket/WebsockBaseController.cs index 78b29601..fad82886 100644 --- a/Common/Websocket/WebsockBaseController.cs +++ b/Common/Websocket/WebsockBaseController.cs @@ -2,10 +2,9 @@ using System.Text.Json; using System.Threading.Channels; using Microsoft.AspNetCore.Mvc; -using OneOf; -using OneOf.Types; using OpenShock.Common.Errors; using OpenShock.Common.Problems; +using OpenShock.Common.Results; using OpenShock.Common.Utils; namespace OpenShock.Common.Websocket; @@ -120,9 +119,9 @@ public async Task Get([FromServices] IHostApplicationLifetime lifetime, Cancella } var connectionPrecondition = await ConnectionPrecondition(); - if (connectionPrecondition.IsT1) + if (connectionPrecondition.Value is Error connectionError) { - await connectionPrecondition.AsT1.Value.WriteAsJsonAsync(HttpContext, LinkedToken); + await connectionError.Value.WriteAsJsonAsync(HttpContext, LinkedToken); return; } @@ -292,6 +291,6 @@ protected async Task ForceClose(WebSocketCloseStatus closeStatus, string? status /// Action when the websocket connection is destroyed to unregister the connection to a websocket manager /// [NonAction] - protected virtual Task>> ConnectionPrecondition() => - Task.FromResult(OneOf>.FromT0(new Success())); + protected virtual Task>> ConnectionPrecondition() => + Task.FromResult>>(new Success()); } \ No newline at end of file diff --git a/Cron/Services/Email/EmailTemplate.cs b/Cron/Services/Email/EmailTemplate.cs index aa01b548..13e1d68e 100644 --- a/Cron/Services/Email/EmailTemplate.cs +++ b/Cron/Services/Email/EmailTemplate.cs @@ -1,5 +1,6 @@ using System.Text.Encodings.Web; using Fluid; +using OpenShock.Common.Results; using OpenShock.Cron.Services.Email.Mailjet.Mail; namespace OpenShock.Cron.Services.Email; @@ -30,13 +31,13 @@ private static TemplateOptions CreateOptions() public static async Task ParseFromFileThrow(string filePath) { var result = await ParseFromFile(filePath); - return result.IsT0 ? result.AsT0 : throw new InvalidDataException(result.AsT1); + return result is EmailTemplate template ? template : throw new InvalidDataException((string)result.Value!); } - private static Task> ParseFromFile(string filePath) => + private static Task> ParseFromFile(string filePath) => ParseFromFile(File.OpenRead(filePath)); - private static async Task> ParseFromFile(FileStream fileStream) + private static async Task> ParseFromFile(FileStream fileStream) { using var streamReader = new StreamReader(fileStream); var subject = await streamReader.ReadLineAsync(); diff --git a/Directory.Build.props b/Directory.Build.props index a92375ae..f1b72012 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -22,12 +22,12 @@ - net10.0 + net11.0 enable - Latest + preview enable $(Product) $(Product.Replace(" ", "_")) @@ -35,6 +35,9 @@ $(Version) a2109c1e-fb11-44d7-8127-346ef60cb9a5 true + + $(Features);runtime-async=on diff --git a/Directory.Packages.props b/Directory.Packages.props index de7548c2..51a916a0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,6 @@ - diff --git a/LiveControlGateway/Controllers/HubControllerBase.cs b/LiveControlGateway/Controllers/HubControllerBase.cs index aab9f028..c702a2c8 100644 --- a/LiveControlGateway/Controllers/HubControllerBase.cs +++ b/LiveControlGateway/Controllers/HubControllerBase.cs @@ -2,11 +2,10 @@ using FlatSharp; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using OneOf; -using OneOf.Types; using OpenShock.Common.Constants; using OpenShock.Common.Errors; using OpenShock.Common.Problems; +using OpenShock.Common.Results; using OpenShock.Common.Utils; using OpenShock.LiveControlGateway.LifetimeManager; using OpenShock.LiveControlGateway.Options; @@ -123,7 +122,7 @@ ILogger> logger private SemVersion? _firmwareVersion; /// - protected override async Task>> ConnectionPrecondition() + protected override async Task>> ConnectionPrecondition() { _connected = DateTimeOffset.UtcNow; @@ -141,20 +140,19 @@ protected override async Task>> Connectio _userAgent = HttpContext.Request.Headers.UserAgent.ToString().Truncate(256); var hubLifetimeResult = await _hubLifetimeManager.TryAddDeviceConnection(5, this, LinkedToken); - if (hubLifetimeResult.IsT1) + switch (hubLifetimeResult) { - Logger.LogWarning("Hub lifetime busy, closing connection"); - return new Error(WebsocketError.WebsocketHubLifetimeBusy); + case HubLifetimeManager.Busy: + Logger.LogWarning("Hub lifetime busy, closing connection"); + return new Error(WebsocketError.WebsocketHubLifetimeBusy); + case Error: + Logger.LogError("Hub lifetime error, closing connection"); + return new Error(ExceptionError.Exception); + case LifetimeManager.HubLifetime hubLifetime: + HubLifetime = hubLifetime; + break; } - - if (hubLifetimeResult.IsT2) - { - Logger.LogError("Hub lifetime error, closing connection"); - return new Error(ExceptionError.Exception); - } - - HubLifetime = hubLifetimeResult.AsT0; - + return new Success(); } diff --git a/LiveControlGateway/Controllers/LiveControlController.cs b/LiveControlGateway/Controllers/LiveControlController.cs index 44d9e0b9..8be7133e 100644 --- a/LiveControlGateway/Controllers/LiveControlController.cs +++ b/LiveControlGateway/Controllers/LiveControlController.cs @@ -5,8 +5,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.EntityFrameworkCore; -using OneOf; -using OneOf.Types; using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.Attributes; using OpenShock.Common.Authentication.Services; @@ -17,12 +15,14 @@ using OpenShock.Common.Models.WebSocket.LCG; using OpenShock.Common.OpenShockDb; using OpenShock.Common.Problems; +using OpenShock.Common.Results; using OpenShock.Common.Utils; using OpenShock.Common.Websocket; using OpenShock.LiveControlGateway.LifetimeManager; using OpenShock.LiveControlGateway.Models; using OpenShock.LiveControlGateway.PubSub; using JsonOptions = OpenShock.Common.JsonSerialization.JsonOptions; +using Results = OpenShock.Common.Results; using Timer = System.Timers.Timer; namespace OpenShock.LiveControlGateway.Controllers; @@ -177,11 +177,11 @@ public async Task UpdatePermissions(OpenShockContext db) /// We get the id from the route, check if its valid, check if the user has access to the shocker / hub /// /// - protected override async Task>> ConnectionPrecondition() + protected override async Task>> ConnectionPrecondition() { if (HttpContext.GetRouteValue("hubId") is not string param || !Guid.TryParse(param, out var id)) { - return new OneOf.Types.Error(WebsocketError.WebsocketLiveControlHubIdInvalid); + return new Results.Error(WebsocketError.WebsocketLiveControlHubIdInvalid); } HubId = id; @@ -194,7 +194,7 @@ public async Task UpdatePermissions(OpenShockContext db) if (!hubExistsAndYouHaveAccess) { - return new OneOf.Types.Error(WebsocketError.WebsocketLiveControlHubNotFound); + return new Results.Error(WebsocketError.WebsocketLiveControlHubNotFound); } _device = await db.Devices.FirstOrDefaultAsync(x => x.Id == HubId); @@ -216,21 +216,19 @@ public async Task UpdatePermissions(OpenShockContext db) var hubLifetimeResult = await _hubLifetimeManager.AddLiveControlConnection(this); - if (hubLifetimeResult.IsT1) + switch (hubLifetimeResult) { - _logger.LogDebug("No such hub with id [{HubId}] connected", HubId); - return new OneOf.Types.Error(WebsocketError.WebsocketLiveControlHubNotConnected); + case Results.NotFound: + _logger.LogDebug("No such hub with id [{HubId}] connected", HubId); + return new Results.Error(WebsocketError.WebsocketLiveControlHubNotConnected); + case LifetimeManager.HubLifetimeManager.Busy: + _logger.LogDebug("Hub is busy, cannot connect [{HubId}]", HubId); + return new Results.Error(WebsocketError.WebsocketLiveControlHubLifetimeBusy); + case LifetimeManager.HubLifetime hubLifetime: + _hubLifetime = hubLifetime; + break; } - if (hubLifetimeResult.IsT2) - { - _logger.LogDebug("Hub is busy, cannot connect [{HubId}]", HubId); - return new OneOf.Types.Error(WebsocketError.WebsocketLiveControlHubLifetimeBusy); - } - - _hubLifetime = hubLifetimeResult.AsT0; - - return new Success(); } @@ -245,7 +243,7 @@ public void OnActionExecuting(ActionExecutingContext context) // When authenticated via an API token, the token may scope/pause shocker control. // Session auth carries no such limits. - if (_userReferenceService.AuthReference.TryPickT1(out var apiToken, out _)) + if (_userReferenceService.AuthReference is ApiToken apiToken) { _tokenId = apiToken.Id; _tokenPaused = apiToken.ShockerControlPaused; @@ -312,8 +310,17 @@ protected override async Task HandleReceive(CancellationToken cancellation LinkedToken ); - var continueLoop = await message.Match(async request => - { + switch (message) + { + case DeserializeFailed failed: + Logger.LogWarning(failed.Exception, "Deserialization failed for websocket message"); + await ForceClose(WebSocketCloseStatus.InvalidPayloadData, "Invalid json message received"); + return false; + case WebsocketClosure: + Logger.LogTrace("Client sent closure"); + return false; + default: + var request = (BaseRequest?)message.Value; if (request?.Data is null) { Logger.LogWarning("Received null data from client"); @@ -324,19 +331,7 @@ protected override async Task HandleReceive(CancellationToken cancellation await ProcessResult(request); return true; - }, - async failed => - { - Logger.LogWarning(failed.Exception, "Deserialization failed for websocket message"); - await ForceClose(WebSocketCloseStatus.InvalidPayloadData, "Invalid json message received"); - return false; - }, closure => - { - Logger.LogTrace("Client sent closure"); - return Task.FromResult(false); - }); - - return continueLoop; + } } private Task ProcessResult(BaseRequest request) @@ -479,18 +474,20 @@ await QueueMessage(new LiveControlResponse } var permCheck = CheckFramePermissions(frame.Shocker, frame.Type); - if (!permCheck.TryPickT0(out var perms, out var error)) + if (permCheck is not SharePermsAndLimits perms) { await QueueMessage(new LiveControlResponse { - ResponseType = error.Match( - notFound => LiveResponseType.ShockerNotFound, - liveNotEnabled => LiveResponseType.ShockerMissingLivePermission, - noPermission => LiveResponseType.ShockerMissingPermission, - shockerPaused => LiveResponseType.ShockerPaused - ) + ResponseType = permCheck switch + { + Results.NotFound => LiveResponseType.ShockerNotFound, + LiveNotEnabled => LiveResponseType.ShockerMissingLivePermission, + NoPermission => LiveResponseType.ShockerMissingPermission, + ShockerPaused => LiveResponseType.ShockerPaused, + _ => throw new UnreachableException() + } }); - + return; } @@ -501,25 +498,28 @@ await QueueMessage(new LiveControlResponse var result = HubLifetime.ReceiveFrame(frame.Shocker, frame.Type, intensity, _tps); - await result.Match( - _ => - { + switch (result) + { + case Success: Logger.LogTrace("Successfully received frame"); - return ValueTask.CompletedTask; - }, - _ => QueueMessage(new LiveControlResponse - { - ResponseType = LiveResponseType.ShockerNotFound - }), - shockerExclusive => QueueMessage(new LiveControlResponse - { - ResponseType = LiveResponseType.ShockerExclusive, - Data = shockerExclusive.Until - }) - ); + break; + case Results.NotFound: + await QueueMessage(new LiveControlResponse + { + ResponseType = LiveResponseType.ShockerNotFound + }); + break; + case ShockerExclusive shockerExclusive: + await QueueMessage(new LiveControlResponse + { + ResponseType = LiveResponseType.ShockerExclusive, + Data = shockerExclusive.Until + }); + break; + } } - private OneOf CheckFramePermissions(Guid shocker, ControlType controlType) + private Union5 CheckFramePermissions(Guid shocker, ControlType controlType) { if (!_sharedShockers.TryGetValue(shocker, out var shockerShare)) return new NotFound(); @@ -605,16 +605,16 @@ protected override async ValueTask DisposeControllerAsync() } /// -/// OneOf +/// Union case /// public readonly struct LiveNotEnabled; /// -/// OneOf +/// Union case /// public readonly struct NoPermission; /// -/// OneOf +/// Union case /// public readonly struct ShockerPaused; \ No newline at end of file diff --git a/LiveControlGateway/LifetimeManager/HubLifetime.cs b/LiveControlGateway/LifetimeManager/HubLifetime.cs index bd912706..f25e429c 100644 --- a/LiveControlGateway/LifetimeManager/HubLifetime.cs +++ b/LiveControlGateway/LifetimeManager/HubLifetime.cs @@ -1,13 +1,12 @@ using MessagePack; using Microsoft.EntityFrameworkCore; -using OneOf; -using OneOf.Types; using OpenShock.Common.Constants; using OpenShock.Common.Extensions; using OpenShock.Common.Models; using OpenShock.Common.OpenShockDb; using OpenShock.Common.Redis; using OpenShock.Common.Redis.PubSub; +using OpenShock.Common.Results; using OpenShock.Common.Services.RedisPubSub; using OpenShock.Common.Utils; using OpenShock.LiveControlGateway.Controllers; @@ -392,7 +391,7 @@ private async Task UpdateShockers(OpenShockContext db, CancellationToken cancell /// /// /// - public OneOf ReceiveFrame(Guid shocker, ControlType type, byte intensity, + public Union3 ReceiveFrame(Guid shocker, ControlType type, byte intensity, byte tps) { if (!_shockerStates.TryGetValue(shocker, out var state)) return new NotFound(); @@ -465,7 +464,7 @@ public ValueTask Control(IReadOnlyList commands) /// /// /// - public async Task> Online(Guid device, SelfOnlineData data) + public async Task> Online(Guid device, SelfOnlineData data) { var deviceOnline = _redisConnectionProvider.RedisCollection(); var deviceId = device.ToString(); diff --git a/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs b/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs index 8493a18e..52ab5717 100644 --- a/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs +++ b/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs @@ -1,10 +1,10 @@ using System.Diagnostics.Metrics; using Microsoft.EntityFrameworkCore; -using OneOf.Types; using OpenShock.Common.Extensions; using OpenShock.Common.Models; using OpenShock.Common.OpenShockDb; using OpenShock.Common.Redis.PubSub; +using OpenShock.Common.Results; using OpenShock.Common.Services.RedisPubSub; using OpenShock.LiveControlGateway.Controllers; using Redis.OM.Contracts; @@ -75,7 +75,7 @@ public HubLifetimeManager( /// /// /// - public async Task> TryAddDeviceConnection(byte tps, IHubController hubController, + public async Task> TryAddDeviceConnection(byte tps, IHubController hubController, CancellationToken cancellationToken) { _logger.LogDebug("Adding hub lifetime [{HubId}]", hubController.Id); @@ -201,7 +201,7 @@ public async Task RemoveDeviceConnection(IHubController hubController) /// /// /// - public async Task> AddLiveControlConnection(LiveControlController liveControlController) + public async Task> AddLiveControlConnection(LiveControlController liveControlController) { if (!liveControlController.HubId.HasValue) throw new ArgumentException("LiveControlController does not have a hubId", nameof(liveControlController)); @@ -225,7 +225,7 @@ public async Task RemoveDeviceConnection(IHubController hubController) /// /// /// - public async Task> UpdateDevice(Guid device) + public async Task> UpdateDevice(Guid device) { if (!_lifetimes.TryGetValue(device, out var deviceLifetime)) return new DeviceNotFound(); await deviceLifetime.UpdateDevice(); @@ -238,7 +238,7 @@ public async Task RemoveDeviceConnection(IHubController hubController) /// /// /// - public async Task> Control(Guid device, IReadOnlyList shocks) + public async Task> Control(Guid device, IReadOnlyList shocks) { if (!_lifetimes.TryGetValue(device, out var deviceLifetime)) return new DeviceNotFound(); await deviceLifetime.Control(shocks); @@ -251,7 +251,7 @@ public async Task RemoveDeviceConnection(IHubController hubController) /// /// /// - public async Task> ControlCaptive(Guid device, bool enabled) + public async Task> ControlCaptive(Guid device, bool enabled) { if (!_lifetimes.TryGetValue(device, out var deviceLifetime)) return new DeviceNotFound(); await deviceLifetime.ControlCaptive(enabled); @@ -263,7 +263,7 @@ public async Task RemoveDeviceConnection(IHubController hubController) /// /// /// - public async Task> EmergencyStop(Guid device) + public async Task> EmergencyStop(Guid device) { if (!_lifetimes.TryGetValue(device, out var deviceLifetime)) return new DeviceNotFound(); bool ok = await deviceLifetime.EmergencyStop(); @@ -276,7 +276,7 @@ public async Task RemoveDeviceConnection(IHubController hubController) /// /// /// - public async Task> OtaInstall(Guid device, SemVersion version) + public async Task> OtaInstall(Guid device, SemVersion version) { if (!_lifetimes.TryGetValue(device, out var deviceLifetime)) return new DeviceNotFound(); await deviceLifetime.OtaInstall(version); @@ -288,7 +288,7 @@ public async Task RemoveDeviceConnection(IHubController hubController) /// /// /// - public async Task> Reboot(Guid device) + public async Task> Reboot(Guid device) { if (!_lifetimes.TryGetValue(device, out var deviceLifetime)) return new DeviceNotFound(); bool ok = await deviceLifetime.Reboot(); @@ -300,7 +300,7 @@ public async Task RemoveDeviceConnection(IHubController hubController) /// /// /// - public async Task> DeviceOnline(Guid device, SelfOnlineData data) + public async Task> DeviceOnline(Guid device, SelfOnlineData data) { if (!_lifetimes.TryGetValue(device, out var deviceLifetime)) return new DeviceNotFound(); await deviceLifetime.Online(device, data); @@ -309,12 +309,12 @@ public async Task RemoveDeviceConnection(IHubController hubController) } /// -/// OneOf +/// Union case /// public readonly struct DeviceNotFound; /// -/// OneOf +/// Union case /// public readonly record struct ShockerExclusive(DateTimeOffset Until); diff --git a/LiveControlGateway/Websocket/FlatbufferWebSocketUtils.cs b/LiveControlGateway/Websocket/FlatbufferWebSocketUtils.cs index 82a47ff8..1b6f638b 100644 --- a/LiveControlGateway/Websocket/FlatbufferWebSocketUtils.cs +++ b/LiveControlGateway/Websocket/FlatbufferWebSocketUtils.cs @@ -1,7 +1,7 @@ using System.Buffers; using System.Net.WebSockets; using FlatSharp; -using OneOf; +using OpenShock.Common.Results; using OpenShock.Common.Utils; namespace OpenShock.LiveControlGateway.Websocket; @@ -22,7 +22,7 @@ public static class FlatbufferWebSocketUtils /// /// /// - public static async Task> ReceiveFullMessageAsyncNonAlloc( + public static async Task> ReceiveFullMessageAsyncNonAlloc( WebSocket socket, ISerializer serializer, CancellationToken cancellationToken) where T : class, IFlatBufferSerializable { diff --git a/LiveControlGateway/Websocket/FlatbuffersWebsocketBaseController.cs b/LiveControlGateway/Websocket/FlatbuffersWebsocketBaseController.cs index d73e05e3..6a3cf40a 100644 --- a/LiveControlGateway/Websocket/FlatbuffersWebsocketBaseController.cs +++ b/LiveControlGateway/Websocket/FlatbuffersWebsocketBaseController.cs @@ -53,20 +53,19 @@ protected override async Task HandleReceive(CancellationToken cancellation var message = await FlatbufferWebSocketUtils.ReceiveFullMessageAsyncNonAlloc(WebSocket!, _incomingSerializer, cancellationToken); - - var continueLoop = await message.Match( - Handle, - async _ => - { + + switch (message) + { + case DeserializeFailed: await ForceClose(WebSocketCloseStatus.InvalidPayloadData, "Invalid flatbuffers message"); return false; - }, - _ => - { + case WebsocketClosure: Logger.LogTrace("Client sent closure"); - return Task.FromResult(false); - }); - - return continueLoop; + return false; + case TIn data: + return await Handle(data); + default: + return false; + } } } \ No newline at end of file diff --git a/docker/Base.Dockerfile b/docker/Base.Dockerfile index 0768d286..29f36a76 100644 --- a/docker/Base.Dockerfile +++ b/docker/Base.Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build-common +FROM mcr.microsoft.com/dotnet/sdk:11.0-alpine AS build-common WORKDIR /src COPY --link Common/*.csproj Common/ diff --git a/global.json b/global.json index 7cceb992..4cb6a54c 100644 --- a/global.json +++ b/global.json @@ -1,8 +1,8 @@ { "sdk": { - "version": "10.0.100", + "version": "11.0.100-preview.6.26359.118", "rollForward": "latestMinor", - "allowPrerelease": false + "allowPrerelease": true }, "test": { "runner": "Microsoft.Testing.Platform"