├── .dockerignore ├── .env ├── .gitattributes ├── .github ├── actions │ ├── build-app │ │ └── action.yml │ ├── kubernetes-rollout-restart │ │ └── action.yml │ ├── test-app │ │ └── action.yml │ └── watchtower-update │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── ci-build.yml │ ├── ci-tag.yml │ ├── codeql.yml │ └── update-cloudflare-proxies.yml ├── .gitignore ├── API.IntegrationTests ├── API.IntegrationTests.csproj ├── AccountTests.cs ├── BaseIntegrationTest.cs ├── HttpMessageHandlers │ ├── InterceptedHttpMessageHandler.cs │ └── InterceptedHttpMessageHandlerBuilder.cs └── IntegrationTestWebAppFactory.cs ├── API ├── API.csproj ├── Controller │ ├── Account │ │ ├── Authenticated │ │ │ ├── ChangeEmail.cs │ │ │ ├── ChangePassword.cs │ │ │ ├── ChangeUsername.cs │ │ │ ├── Deactivate.cs │ │ │ └── _ApiController.cs │ │ ├── CheckUsername.cs │ │ ├── Login.cs │ │ ├── LoginV2.cs │ │ ├── Logout.cs │ │ ├── PasswordResetCheckValid.cs │ │ ├── PasswordResetComplete.cs │ │ ├── PasswordResetInitiate.cs │ │ ├── PasswordResetInitiateV2.cs │ │ ├── Signup.cs │ │ ├── SignupV2.cs │ │ └── _ApiController.cs │ ├── Admin │ │ ├── DTOs │ │ │ └── AddWebhookDto.cs │ │ ├── DeactivateUser.cs │ │ ├── DeleteUser.cs │ │ ├── GetOnlineDevices.cs │ │ ├── GetUsers.cs │ │ ├── ReactivateUser.cs │ │ ├── WebhookAdd.cs │ │ ├── WebhookList.cs │ │ ├── WebhookRemove.cs │ │ └── _ApiController.cs │ ├── Device │ │ ├── AssignLCG.cs │ │ ├── AssignLCGV2.cs │ │ ├── GetSelf.cs │ │ ├── Pair.cs │ │ └── _ApiController.cs │ ├── Devices │ │ ├── DeviceOtaController.cs │ │ ├── DevicesController.cs │ │ ├── GetShockers.cs │ │ └── _ApiController.cs │ ├── Public │ │ ├── GetStats.cs │ │ ├── PublicShareController.cs │ │ └── _ApiController.cs │ ├── Sessions │ │ ├── DeleteSessions.cs │ │ ├── ListSessions.cs │ │ ├── SessionSelf.cs │ │ └── _ApiController.cs │ ├── Shares │ │ ├── DeleteShareCode.cs │ │ ├── LinkShareCode.cs │ │ ├── Links │ │ │ ├── AddShocker.cs │ │ │ ├── CreatePublicShare.cs │ │ │ ├── DeletePublicShare.cs │ │ │ ├── EditShocker.cs │ │ │ ├── List.cs │ │ │ ├── PauseShocker.cs │ │ │ ├── RemoveShocker.cs │ │ │ └── _ApiController.cs │ │ ├── V2CreateShareInvite.cs │ │ ├── V2GetShares.cs │ │ ├── V2Invites.cs │ │ └── _ApiController.cs │ ├── Shockers │ │ ├── EditShocker.cs │ │ ├── GetShockerById.cs │ │ ├── GetShockerLogs.cs │ │ ├── ListSharedShockers.cs │ │ ├── ListShockers.cs │ │ ├── PauseShocker.cs │ │ ├── RegisterShocker.cs │ │ ├── RemoveShocker.cs │ │ ├── SendControl.cs │ │ ├── ShockerShares.cs │ │ └── _ApiController.cs │ ├── Tokens │ │ ├── DeleteToken.cs │ │ ├── GetTokenSelf.cs │ │ ├── ReportTokens.cs │ │ ├── Tokens.cs │ │ └── _ApiController.cs │ ├── Users │ │ ├── GetSelf.cs │ │ └── _ApiController.cs │ └── Version │ │ └── _ApiController.cs ├── Models │ ├── Requests │ │ ├── ChangeEmailRequest.cs │ │ ├── ChangePasswordRequest.cs │ │ ├── ChangeUsernameRequest.cs │ │ ├── CreateShareRequest.cs │ │ ├── CreateTokenRequest.cs │ │ ├── EditTokenRequest.cs │ │ ├── HubCreateRequest.cs │ │ ├── HubEditRequest.cs │ │ ├── Login.cs │ │ ├── LoginV2.cs │ │ ├── NewShocker.cs │ │ ├── PasswordResetRequestV2.cs │ │ ├── PauseRequest.cs │ │ ├── PublicShareCreate.cs │ │ ├── PublicShareEditShocker.cs │ │ ├── ReportTokensRequest.cs │ │ ├── ShockerPermLimitPair.cs │ │ ├── ShockerPermLimitPairWithId.cs │ │ ├── Signup.cs │ │ └── SignupV2.cs │ └── Response │ │ ├── DeviceSelfResponse.cs │ │ ├── LcgNodeResponse.cs │ │ ├── LcgNodeResponseV2.cs │ │ ├── LogEntry.cs │ │ ├── LoginSessionResponse.cs │ │ ├── OwnPublicShareResponse.cs │ │ ├── OwnerShockerResponse.cs │ │ ├── PublicShareDevice.cs │ │ ├── PublicShareResponse.cs │ │ ├── PublicShareShocker.cs │ │ ├── RequestShareInfo.cs │ │ ├── ResponseDevice.cs │ │ ├── ResponseDeviceWithShockers.cs │ │ ├── ResponseDeviceWithToken.cs │ │ ├── ShareInfo.cs │ │ ├── ShockerLimits.cs │ │ ├── ShockerPermissions.cs │ │ ├── ShockerResponse.cs │ │ ├── ShockerWithDevice.cs │ │ ├── TokenCreatedResponse.cs │ │ ├── TokenResponse.cs │ │ ├── UserSharesResponse.cs │ │ └── V2UserSharesListItem.cs ├── Options │ ├── MailJetOptions.cs │ ├── MailOptions.cs │ └── SmtpOptions.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Realtime │ └── RedisSubscriberService.cs ├── Services │ ├── Account │ │ ├── AccountService.cs │ │ ├── IAccountService.cs │ │ └── LoginContext.cs │ ├── DeviceUpdateService.cs │ ├── Email │ │ ├── EmailServiceExtension.cs │ │ ├── EmailServiceUtils.cs │ │ ├── IEmailService.cs │ │ ├── Mailjet │ │ │ ├── Mail │ │ │ │ ├── Contact.cs │ │ │ │ ├── MailBase.cs │ │ │ │ ├── MailsWrap.cs │ │ │ │ └── TemplateMail.cs │ │ │ ├── MailjetEmailService.cs │ │ │ └── MailjetEmailServiceExtension.cs │ │ ├── NoneEmailService.cs │ │ └── Smtp │ │ │ ├── SmtpEmailService.cs │ │ │ ├── SmtpEmailServiceExtension.cs │ │ │ ├── SmtpServiceTemplates.cs │ │ │ └── SmtpTemplate.cs │ └── IDeviceUpdateService.cs ├── SmtpTemplates │ ├── EmailVerification.liquid │ └── PasswordReset.liquid ├── Utils │ ├── OneWayPolymorphicJsonConverter.cs │ └── PublicShareUtils.cs ├── appsettings.Development.json ├── appsettings.json └── devcert.pfx ├── CODE_OF_CONDUCT.md ├── Common.Tests ├── Common.Tests.csproj ├── Geo │ ├── Alpha2CountryCodeTests.cs │ └── DistanceLookupTests.cs ├── Query │ ├── ExpressionBuilderTests.cs │ └── QueryStringTokenizerTests.cs ├── Utils │ └── HashingUtilsTests.cs └── Validation │ ├── CharsetMatchersTests.cs │ ├── DataSets │ ├── BlackList.txt │ └── WhiteList.txt │ └── UsernameValidatorTests.cs ├── Common ├── Authentication │ ├── Attributes │ │ └── TokenPermissionAttribute.cs │ ├── AuthenticationHandlers │ │ ├── ApiTokenAuthentication.cs │ │ ├── HubAuthentication.cs │ │ └── UserSessionAuthentication.cs │ ├── ControllerBase │ │ ├── AuthenticatedHubControllerBase.cs │ │ └── AuthenticatedSessionControllerBase.cs │ ├── OpenShockAuthClaims.cs │ ├── OpenShockAuthPolicies.cs │ ├── OpenShockAuthSchemas.cs │ ├── OpenShockAuthorizationMiddlewareResultHandler.cs │ ├── Requirements │ │ └── ApiTokenPermissionRequirement.cs │ └── Services │ │ ├── ClientAuthService.cs │ │ └── UserReferenceService.cs ├── Common.csproj ├── Constants │ ├── AuthConstants.cs │ ├── Constants.cs │ ├── Distance.cs │ └── HardLimits.cs ├── DataAnnotations │ ├── EmailAddressAttribute.cs │ ├── Interfaces │ │ ├── IOperationAttribute.cs │ │ └── IParameterAttribute.cs │ ├── OpenApiSchemas.cs │ ├── PasswordAttribute.cs │ ├── StringCollectionItemMaxLengthAttribute.cs │ └── UsernameAttribute.cs ├── DeviceControl │ ├── ControlLogic.cs │ ├── ControlShockerObj.cs │ └── NotAllShockersSucceeded.cs ├── Errors │ ├── AccountActivationError.cs │ ├── AccountError.cs │ ├── AdminError.cs │ ├── ApiTokenError.cs │ ├── AssignLcgError.cs │ ├── AuthResultError.cs │ ├── AuthorizationError.cs │ ├── DeviceError.cs │ ├── ExceptionError.cs │ ├── ExpressionError.cs │ ├── LoginError.cs │ ├── PairError.cs │ ├── PasswordResetError.cs │ ├── PublicShareError.cs │ ├── SessionError.cs │ ├── ShareCodeError.cs │ ├── ShareError.cs │ ├── ShockerControlError.cs │ ├── ShockerError.cs │ ├── SignupError.cs │ ├── TurnstileError.cs │ ├── UserError.cs │ └── WebsocketError.cs ├── ExceptionHandle │ ├── ExceptionHandler.cs │ └── RequestInfo.cs ├── Extensions │ ├── AssemblyExtensions.cs │ ├── ConfigurationExtensions.cs │ ├── DictionaryExtensions.cs │ ├── IQueryableExtensions.cs │ ├── ISignalRServerBuilderExtensions.cs │ ├── PropertyBuilderExtension.cs │ ├── SemVersionExtensions.cs │ ├── SemaphoreSlimExtensions.cs │ └── UserExtensions.cs ├── Geo │ ├── Alpha2CountryCode.cs │ ├── Alpha2CountryCodeAttribute.cs │ ├── CountryInfo.cs │ └── DistanceLookup.cs ├── Hubs │ ├── IPublicShareHub.cs │ ├── IUserHub.cs │ ├── PublicShareHub.cs │ └── UserHub.cs ├── JsonSerialization │ ├── CustomJsonStringEnumConverter.cs │ ├── PermissionTypeConverter.cs │ ├── SemVersionJsonConverter.cs │ ├── SlSerializer.cs │ └── UnixMillisecondsDateTimeOffsetConverter.cs ├── Migrations │ ├── 20240123062040_Initial.Designer.cs │ ├── 20240123062040_Initial.cs │ ├── 20240304081753_AccountActivation.Designer.cs │ ├── 20240304081753_AccountActivation.cs │ ├── 20240308054253_AccountService.Designer.cs │ ├── 20240308054253_AccountService.cs │ ├── 20240319160003_DbCleanup.Designer.cs │ ├── 20240319160003_DbCleanup.cs │ ├── 20240327034706_Petrainer998DR.Designer.cs │ ├── 20240327034706_Petrainer998DR.cs │ ├── 20240503020144_AddPermissionTypes1.Designer.cs │ ├── 20240503020144_AddPermissionTypes1.cs │ ├── 20240709221359_Add API Token last used.Designer.cs │ ├── 20240709221359_Add API Token last used.cs │ ├── 20240710020052_Fix last used.Designer.cs │ ├── 20240710020052_Fix last used.cs │ ├── 20240710204029_DefaultValueForTokenLastUsed.Designer.cs │ ├── 20240710204029_DefaultValueForTokenLastUsed.cs │ ├── 20240731200212_Add Device_Auth Permission.Designer.cs │ ├── 20240731200212_Add Device_Auth Permission.cs │ ├── 20241012222620_Add username change table.Designer.cs │ ├── 20241012222620_Add username change table.cs │ ├── 20241029174336_AddAndOrgIndexes.Designer.cs │ ├── 20241029174336_AddAndOrgIndexes.cs │ ├── 20241029221207_AddShareRequests.Designer.cs │ ├── 20241029221207_AddShareRequests.cs │ ├── 20241031153812_AddAdminUsersView.Designer.cs │ ├── 20241031153812_AddAdminUsersView.cs │ ├── 20241105235041_RestrictFieldLengths.Designer.cs │ ├── 20241105235041_RestrictFieldLengths.cs │ ├── 20241122214013_Fix Petrainer998DR RFIDs.Designer.cs │ ├── 20241122214013_Fix Petrainer998DR RFIDs.cs │ ├── 20241123181710_Hash API tokens.Designer.cs │ ├── 20241123181710_Hash API tokens.cs │ ├── 20241123214013_Type and naming cleanup.Designer.cs │ ├── 20241123214013_Type and naming cleanup.cs │ ├── 20241219115917_FixAdminUsersView.Designer.cs │ ├── 20241219115917_FixAdminUsersView.cs │ ├── 20250203224107_RanksToRoles.Designer.cs │ ├── 20250203224107_RanksToRoles.cs │ ├── 20250513130906_RenameDateTimeColumns.Designer.cs │ ├── 20250513130906_RenameDateTimeColumns.cs │ ├── 20250513140345_ConsolidateSafetySettings.Designer.cs │ ├── 20250513140345_ConsolidateSafetySettings.cs │ ├── 20250513144159_ForeignKeyAndIdNamingCleanup.Designer.cs │ ├── 20250513144159_ForeignKeyAndIdNamingCleanup.cs │ ├── 20250513145559_TableNamingCleanup.Designer.cs │ ├── 20250513145559_TableNamingCleanup.cs │ ├── 20250513153901_RenameShareLinksToPublicShares.Designer.cs │ ├── 20250513153901_RenameShareLinksToPublicShares.cs │ ├── 20250516120830_RenameShockerSharesToUserShares.Designer.cs │ ├── 20250516120830_RenameShockerSharesToUserShares.cs │ ├── 20250520130050_RemoveRedundantAnnotationAssignment.Designer.cs │ ├── 20250520130050_RemoveRedundantAnnotationAssignment.cs │ ├── 20250521130616_ReworkUserActivationsAndDeactivations.Designer.cs │ ├── 20250521130616_ReworkUserActivationsAndDeactivations.cs │ ├── 20250525165800_AddWebhooksTable.Designer.cs │ ├── 20250525165800_AddWebhooksTable.cs │ ├── 20250525220709_AddApiTokenReports.Designer.cs │ ├── 20250525220709_AddApiTokenReports.cs │ └── OpenShockContextModelSnapshot.cs ├── Models │ ├── BasicShockerInfo.cs │ ├── BasicUserInfo.cs │ ├── ControlLogAdditionalItem.cs │ ├── ControlLogSender.cs │ ├── ControlRequest.cs │ ├── ControlType.cs │ ├── DeviceUpdateType.cs │ ├── Error.cs │ ├── FirmwareVersion.cs │ ├── LcgResponse.cs │ ├── LegacyDataResponse.cs │ ├── LegacyEmptyResponse.cs │ ├── LiveControlPacketSender.cs │ ├── OtaUpdateStatus.cs │ ├── Paginated.cs │ ├── PasswordHashingAlgorithm.cs │ ├── PauseReason.cs │ ├── PermissionType.cs │ ├── RoleType.cs │ ├── Services │ │ └── Ota │ │ │ └── OtaItem.cs │ ├── SharePermsAndLimits.cs │ ├── ShockerModelType.cs │ ├── WebSocket │ │ ├── BaseRequest.cs │ │ ├── ControlResponse.cs │ │ ├── Device │ │ │ ├── RequestType.cs │ │ │ └── ResponseType.cs │ │ ├── DeviceOnlineState.cs │ │ ├── LCG │ │ │ ├── ClientLiveFrame.cs │ │ │ ├── LatencyAnnounceData.cs │ │ │ ├── LcgLiveControlPing.cs │ │ │ ├── LiveRequestType.cs │ │ │ ├── LiveResponseType.cs │ │ │ └── TpsData.cs │ │ ├── LiveControlResponse.cs │ │ ├── MessageTooLongException.cs │ │ └── User │ │ │ ├── CaptiveControl.cs │ │ │ ├── Control.cs │ │ │ ├── ControlLog.cs │ │ │ ├── RequestType.cs │ │ │ └── ResponseType.cs │ └── WebhookDto.cs ├── OpenShockApplication.cs ├── OpenShockControllerBase.cs ├── OpenShockDb │ ├── AdminUsersView.cs │ ├── ApiToken.cs │ ├── ApiTokenReport.cs │ ├── Device.cs │ ├── DeviceOtaUpdate.cs │ ├── DiscordWebhook.cs │ ├── OpenShockContext.cs │ ├── PublicShare.cs │ ├── PublicShareShocker.cs │ ├── SafetySettings.cs │ ├── Shocker.cs │ ├── ShockerControlLog.cs │ ├── ShockerShareCode.cs │ ├── User.cs │ ├── UserActivationRequest.cs │ ├── UserDeactivation.cs │ ├── UserEmailChange.cs │ ├── UserNameChange.cs │ ├── UserPasswordReset.cs │ ├── UserShare.cs │ ├── UserShareInvite.cs │ └── UserShareInviteShocker.cs ├── OpenShockMiddlewareHelper.cs ├── OpenShockRedisHubLifetimeManager.cs ├── OpenShockServiceHelper.cs ├── Options │ ├── CloudflareTurnstileOptions.cs │ ├── DatabaseOptions.cs │ ├── FrontendOptions.cs │ ├── MetricsOptions.cs │ └── RedisOptions.cs ├── Problems │ ├── CustomProblems │ │ ├── PolicyNotMetProblem.cs │ │ ├── ShockerControlProblem.cs │ │ ├── ShockersNotFoundProblem.cs │ │ └── TokenPermissionProblem.cs │ ├── ExceptionProblem.cs │ ├── OpenShockProblem.cs │ └── ValidationProblem.cs ├── Query │ ├── DBExpressionBuilder.cs │ ├── DBExpressionBuilderUtils.cs │ ├── OrderByQueryBuilder.cs │ └── QueryStringTokenizer.cs ├── Redis │ ├── DeviceOnline.cs │ ├── DevicePair.cs │ ├── LcgNode.cs │ ├── LoginSessions.cs │ └── PubSub │ │ ├── CaptiveMessage.cs │ │ ├── ControlMessage.cs │ │ ├── DeviceOtaInstallMessage.cs │ │ └── DeviceUpdatedMessage.cs ├── Scripts │ └── ReScaffold.ps1 ├── Services │ ├── BatchUpdate │ │ ├── BatchUpdateService.cs │ │ └── IBatchUpdateService.cs │ ├── Device │ │ ├── DeviceService.cs │ │ └── IDeviceService.cs │ ├── LCGNodeProvisioner │ │ ├── ILCGNodeProvisioner.cs │ │ └── LCGNodeProvisioner.cs │ ├── Ota │ │ ├── IOtaService.cs │ │ └── OtaService.cs │ ├── RedisPubSub │ │ ├── IRedisPubService.cs │ │ ├── RedisChannels.cs │ │ └── RedisPubService.cs │ ├── Session │ │ ├── ISessionService.cs │ │ └── SessionService.cs │ ├── Turnstile │ │ ├── CloduflareTurnstileError.cs │ │ ├── CloudflareTurnstileService.cs │ │ ├── CloudflareTurnstileServiceExtensions.cs │ │ ├── CloudflareTurnstileVerifyResponseDto.cs │ │ └── ICloudflareTurnstileService.cs │ └── Webhook │ │ ├── IWebhookService.cs │ │ └── WebhookService.cs ├── Swagger │ ├── AttributeFilter.cs │ └── SwaggerGenExtensions.cs ├── Utils │ ├── AuthUtils.cs │ ├── ConfigureSwaggerOptions.cs │ ├── ConnectionDetailsFetcher.cs │ ├── CryptoUtils.cs │ ├── GitHashAttribute.cs │ ├── GravatarUtils.cs │ ├── HashingUtils.cs │ ├── JsonWebSocketUtils.cs │ ├── MathUtils.cs │ ├── OpenShockEnricher.cs │ ├── OsTask.cs │ ├── PBKDF2PasswordHasher.cs │ ├── StringUtils.cs │ └── TrustedProxiesFetcher.cs ├── Validation │ ├── ChatsetMatchers.cs │ └── UsernameValidator.cs ├── Websocket │ ├── IWebsocketController.cs │ └── WebsockBaseController.cs └── cloudflare-ips.txt ├── Cron ├── Attributes │ └── CronJobAttribute.cs ├── Cron.csproj ├── DashboardAdminAuth.cs ├── Jobs │ ├── ClearOldPasswordResetsJob.cs │ ├── ClearOldShockerControlLogs.cs │ └── OtaTimeoutJob.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Utils │ └── CronJobCollector.cs ├── appsettings.Development.json ├── appsettings.json └── devcert.pfx ├── Dev ├── README.md ├── devSecrets.json ├── docker-compose.yml ├── setupTestData.sh ├── setupUsersecrets.sh └── testData.sql ├── Framework.props ├── LICENSE ├── LiveControlGateway ├── Controllers │ ├── HubControllerBase.cs │ ├── HubV1Controller.cs │ ├── HubV2Controller.cs │ ├── IHubController.cs │ ├── InstanceDetailsController.cs │ └── LiveControlController.cs ├── LcgKeepAlive.cs ├── LifetimeManager │ ├── HubLifetime.cs │ ├── HubLifetimeManager.cs │ ├── HubLifetimeState.cs │ └── ShockerState.cs ├── LiveControlGateway.csproj ├── Models │ └── LiveShockerPermission.cs ├── Options │ └── LcgOptions.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── PubSub │ └── RedisSubscriberService.cs ├── Websocket │ ├── FlatbufferWebSocketUtils.cs │ ├── FlatbuffersWebsocketBaseController.cs │ └── MessageTooLongException.cs ├── appsettings.Development.json ├── appsettings.json └── devcert.pfx ├── MigrationHelper ├── MigrationHelper.csproj └── Program.cs ├── OpenShockBackend.slnx ├── README.md ├── Shared.props ├── charts └── openshock │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── configmap-webui.yaml │ ├── deployment-webui.yaml │ ├── deployments-backend.yaml │ ├── ingresses.yaml │ ├── services.yaml │ └── tests │ │ └── test-connection.yaml │ └── values.yaml ├── docker-compose.yml └── docker ├── API.Dockerfile ├── Base.Dockerfile ├── Cron.Dockerfile ├── LiveControlGateway.Dockerfile ├── appsettings.API.json ├── appsettings.Cron.json ├── appsettings.LiveControlGateway.json └── entrypoint.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | # directories 2 | **/bin/ 3 | **/obj/ 4 | **/out/ 5 | 6 | # files 7 | **/appsettings.Development.json 8 | Dockerfile* 9 | **/*.md 10 | dev/ -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Required variables (uncomment and set values!) 2 | #PG_PASS=someSecurePassword 3 | 4 | # Compose variables 5 | OPENSHOCK_DOMAIN=openshock.local # your public base domain 6 | OPENSHOCK_GATEWAY_SUBDOMAIN=gateway # subdomain for the included gateway 7 | OPENSHOCK_API_SUBDOMAIN=api # subdomain for the api 8 | 9 | #global email config 10 | OPENSHOCK__MAIL__SENDER__NAME=OpenShock System 11 | OPENSHOCK__MAIL__SENDER__EMAIL=system@openshock.app 12 | 13 | #mail configs. uncomment one of the 2 sections below and make your config changes 14 | 15 | #MailJet 16 | #OPENSHOCK__MAIL__TYPE: MAILJET # MAILJET or SMTP, check Documentation 17 | #OPENSHOCK__MAIL__MAILJET__KEY: mailjetkey 18 | #OPENSHOCK__MAIL__MAILJET__SECRET: mailjetsecret 19 | #OPENSHOCK__MAIL__MAILJET__TEMPLATE__PASSWORDRESET: 9999999 20 | 21 | #SMTP 22 | OPENSHOCK__MAIL__TYPE=SMTP # MAILJET or SMTP, check Documentation 23 | OPENSHOCK__MAIL__SMTP__HOST=mail.domain.zap 24 | OPENSHOCK__MAIL__SMTP__USERNAME=open@shock.zap 25 | OPENSHOCK__MAIL__SMTP__PASSWORD=SMTPPASSWORD 26 | OPENSHOCK__MAIL__SMTP__ENABLESSL=true 27 | OPENSHOCK__MAIL__SMTP__VERIFYCERTIFICATE=true 28 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.mmdb filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.github/actions/test-app/action.yml: -------------------------------------------------------------------------------- 1 | name: test-app 2 | description: Build and Test in Docker 3 | inputs: 4 | dockerfile: 5 | required: true 6 | description: Dockerfile path 7 | image: 8 | required: true 9 | description: Image name 10 | target: 11 | required: true 12 | description: Target to run in Dockerfile 13 | 14 | runs: 15 | using: composite 16 | 17 | steps: 18 | - name: Build Test Image 19 | uses: docker/build-push-action@v6 20 | with: 21 | context: . 22 | file: ${{ inputs.dockerfile }} 23 | tags: ${{ inputs.image }} 24 | target: ${{ inputs.target }} 25 | load: true 26 | push: false 27 | cache-from: | 28 | type=gha 29 | cache-to: | 30 | type=gha 31 | - name: Run Test Image 32 | shell: bash 33 | run: | 34 | docker run --rm \ 35 | -v /var/run/docker.sock:/var/run/docker.sock \ 36 | ${{ inputs.image }} -------------------------------------------------------------------------------- /.github/actions/watchtower-update/action.yml: -------------------------------------------------------------------------------- 1 | name: watchtower-update 2 | description: Trigger a watchtower update 3 | inputs: 4 | url: 5 | required: true 6 | description: Watchtower HTTP API URL (e.g. `http://example.org:8080/v1/update`) 7 | token: 8 | required: true 9 | description: Bearer Token for Authentication 10 | 11 | runs: 12 | using: composite 13 | 14 | steps: 15 | - name: Trigger Watchtower Update 16 | shell: bash 17 | run: | 18 | echo "Deploying to watchtower..." 19 | curl -k -f -o /dev/null -X POST "${{ inputs.url }}" \ 20 | --header "Authorization: Bearer ${{ inputs.token }}" \ 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | 9 | # Check for Github Actions version updates (for CI/CD) 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: 'weekly' 14 | day: 'monday' 15 | time: '06:00' 16 | 17 | # Check for Nuget package updates 18 | - package-ecosystem: "nuget" 19 | directory: "/" 20 | schedule: 21 | interval: 'weekly' 22 | day: 'monday' 23 | time: '06:00' 24 | groups: 25 | nuget-dependencies: 26 | patterns: 27 | - '*' # Group all updates together -------------------------------------------------------------------------------- /.github/workflows/ci-tag.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - '[0-9]+.[0-9]+.[0-9]+' 5 | - '[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+' 6 | 7 | name: ci-tag 8 | 9 | env: 10 | DOTNET_VERSION: 9.0.x 11 | REGISTRY: ghcr.io 12 | IMAGE_NAME: ${{ github.repository_owner }}/api 13 | 14 | jobs: 15 | 16 | # Pre-job to find the latest tag 17 | get-latest-tag: 18 | runs-on: ubuntu-latest 19 | outputs: 20 | latest-tag: ${{ steps.latest-tag.outputs.tag }} 21 | steps: 22 | - name: Find latest tag 23 | id: latest-tag 24 | uses: oprypin/find-latest-tag@v1 25 | with: 26 | repository: ${{ github.repository }} 27 | regex: '^\d+\.\d+\.\d+$' 28 | releases-only: false 29 | 30 | # Delegate building and containerizing to a single workflow. 31 | build-and-containerize: 32 | needs: get-latest-tag 33 | uses: ./.github/workflows/ci-build.yml 34 | with: 35 | platforms: linux/amd64,linux/arm64 36 | latest: ${{ needs.get-latest-tag.outputs.latest-tag == github.ref_name }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | .idea 3 | obj/ 4 | bin/ 5 | *.user 6 | **/launchSettings.json 7 | Dev/dragonfly 8 | Dev/postgres -------------------------------------------------------------------------------- /API.IntegrationTests/API.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /API.IntegrationTests/AccountTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Text; 3 | using System.Text.Json; 4 | 5 | namespace OpenShock.API.IntegrationTests; 6 | 7 | public class AccountTests : BaseIntegrationTest 8 | { 9 | [Test] 10 | public async Task CreateAccount_ShouldAdd_NewUserToDatabase() 11 | { 12 | using var client = WebAppFactory.CreateClient(); 13 | 14 | var requestBody = JsonSerializer.Serialize(new 15 | { 16 | username = "Bob", 17 | password = "SecurePassword123#", 18 | email = "bob@example.com", 19 | turnstileresponse = "valid-token" 20 | }); 21 | 22 | 23 | var response = await client.PostAsync("/2/account/signup", new StringContent(requestBody, Encoding.UTF8, "application/json")); 24 | 25 | var content = await response.Content.ReadAsStringAsync(); 26 | 27 | await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /API.IntegrationTests/BaseIntegrationTest.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.IntegrationTests; 2 | 3 | public abstract class BaseIntegrationTest 4 | { 5 | [ClassDataSource(Shared = SharedType.PerTestSession)] 6 | public required IntegrationTestWebAppFactory WebAppFactory { get; init; } 7 | } -------------------------------------------------------------------------------- /API.IntegrationTests/HttpMessageHandlers/InterceptedHttpMessageHandlerBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Http; 2 | 3 | namespace OpenShock.API.IntegrationTests.HttpMessageHandlers; 4 | 5 | sealed class InterceptedHttpMessageHandlerBuilder : HttpMessageHandlerBuilder 6 | { 7 | public override string? Name { get; set; } 8 | public override required HttpMessageHandler PrimaryHandler { get; set; } 9 | public override IList AdditionalHandlers => []; 10 | 11 | 12 | public override HttpMessageHandler Build() 13 | { 14 | return new InterceptedHttpMessageHandler(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /API/Controller/Account/Authenticated/ChangeEmail.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Mime; 2 | using Microsoft.AspNetCore.Mvc; 3 | using OpenShock.API.Models.Requests; 4 | using OpenShock.Common.Models; 5 | 6 | namespace OpenShock.API.Controller.Account.Authenticated; 7 | 8 | public sealed partial class AuthenticatedAccountController 9 | { 10 | /// 11 | /// Change the password of the current user 12 | /// 13 | /// 14 | /// 15 | /// 16 | [HttpPost("email")] 17 | [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] 18 | public Task ChangeEmail(ChangeEmailRequest data) 19 | { 20 | throw new NotImplementedException(); 21 | } 22 | } -------------------------------------------------------------------------------- /API/Controller/Account/Authenticated/ChangePassword.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using OpenShock.API.Models.Requests; 3 | using OpenShock.Common.Errors; 4 | using OpenShock.Common.Utils; 5 | 6 | namespace OpenShock.API.Controller.Account.Authenticated; 7 | 8 | public sealed partial class AuthenticatedAccountController 9 | { 10 | /// 11 | /// Change the password of the current user 12 | /// 13 | /// 14 | /// 15 | /// 16 | [HttpPost("password")] 17 | [ProducesResponseType(StatusCodes.Status200OK)] 18 | public async Task ChangePassword(ChangePasswordRequest data) 19 | { 20 | if (!HashingUtils.VerifyPassword(data.OldPassword, CurrentUser.PasswordHash).Verified) 21 | { 22 | return Problem(AccountError.PasswordChangeInvalidPassword); 23 | } 24 | 25 | var result = await _accountService.ChangePassword(CurrentUser.Id, data.NewPassword); 26 | 27 | return result.Match(success => Ok(), 28 | notFound => throw new Exception("Unexpected result, apparently our current user does not exist...")); 29 | } 30 | } -------------------------------------------------------------------------------- /API/Controller/Account/Authenticated/_ApiController.cs: -------------------------------------------------------------------------------- 1 | using Asp.Versioning; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | using OpenShock.API.Services.Account; 5 | using OpenShock.Common.Authentication; 6 | using OpenShock.Common.Authentication.ControllerBase; 7 | 8 | namespace OpenShock.API.Controller.Account.Authenticated; 9 | 10 | /// 11 | /// User account management 12 | /// 13 | [ApiController] 14 | [Tags("Account")] 15 | [ApiVersion("1")] 16 | [Route("/{version:apiVersion}/account")] 17 | [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] 18 | public sealed partial class AuthenticatedAccountController : AuthenticatedSessionControllerBase 19 | { 20 | private readonly IAccountService _accountService; 21 | private readonly ILogger _logger; 22 | 23 | public AuthenticatedAccountController( 24 | IAccountService accountService, 25 | ILogger logger 26 | ) 27 | { 28 | _accountService = accountService; 29 | _logger = logger; 30 | } 31 | } -------------------------------------------------------------------------------- /API/Controller/Account/PasswordResetInitiate.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using OpenShock.Common.Models; 3 | using Asp.Versioning; 4 | 5 | namespace OpenShock.API.Controller.Account; 6 | 7 | public sealed partial class AccountController 8 | { 9 | /// 10 | /// Initiate a password reset 11 | /// 12 | /// Password reset email sent if the email is associated to an registered account 13 | [HttpPost("reset")] 14 | [MapToApiVersion("1")] 15 | public async Task PasswordResetInitiate([FromBody] ResetRequest body) 16 | { 17 | await _accountService.CreatePasswordReset(body.Email); 18 | return new LegacyEmptyResponse("Password reset has been sent via email if the email is associated to an registered account"); 19 | } 20 | 21 | public sealed class ResetRequest 22 | { 23 | public required string Email { get; init; } 24 | } 25 | } -------------------------------------------------------------------------------- /API/Controller/Account/Signup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using OpenShock.API.Models.Requests; 3 | using System.Net.Mime; 4 | using Asp.Versioning; 5 | using OpenShock.Common.Errors; 6 | using OpenShock.Common.Problems; 7 | using OpenShock.Common.Models; 8 | 9 | namespace OpenShock.API.Controller.Account; 10 | 11 | public sealed partial class AccountController 12 | { 13 | /// 14 | /// Signs up a new user 15 | /// 16 | /// 17 | /// User successfully signed up 18 | /// Username or email already exists 19 | [HttpPost("signup")] 20 | [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] 21 | [ProducesResponseType(StatusCodes.Status409Conflict, MediaTypeNames.Application.ProblemJson)] // EmailOrUsernameAlreadyExists 22 | [MapToApiVersion("1")] 23 | public async Task SignUp([FromBody] SignUp body) 24 | { 25 | var creationAction = await _accountService.CreateAccount(body.Email, body.Username, body.Password); 26 | if (creationAction.IsT1) return Problem(SignupError.EmailAlreadyExists); 27 | 28 | return LegacyEmptyOk("Successfully signed up"); 29 | } 30 | } -------------------------------------------------------------------------------- /API/Controller/Account/_ApiController.cs: -------------------------------------------------------------------------------- 1 | using Asp.Versioning; 2 | using Microsoft.AspNetCore.Mvc; 3 | using OpenShock.Common; 4 | using OpenShock.API.Services.Account; 5 | 6 | namespace OpenShock.API.Controller.Account; 7 | 8 | /// 9 | /// User account management 10 | /// 11 | [ApiController] 12 | [Tags("Account")] 13 | [ApiVersion("1"), ApiVersion("2")] 14 | [Route("/{version:apiVersion}/account")] 15 | public sealed partial class AccountController : OpenShockControllerBase 16 | { 17 | private readonly IAccountService _accountService; 18 | private readonly ILogger _logger; 19 | 20 | public AccountController(IAccountService accountService, ILogger logger) 21 | { 22 | _accountService = accountService; 23 | _logger = logger; 24 | } 25 | } -------------------------------------------------------------------------------- /API/Controller/Admin/DTOs/AddWebhookDto.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Controller.Admin.DTOs; 2 | 3 | public sealed class AddWebhookDto 4 | { 5 | public required string Name { get; set; } 6 | public required Uri Url { get; set; } 7 | } -------------------------------------------------------------------------------- /API/Controller/Admin/DeactivateUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.EntityFrameworkCore; 3 | using OpenShock.API.Services.Account; 4 | using OpenShock.Common.Errors; 5 | using OpenShock.Common.Models; 6 | using Z.EntityFramework.Plus; 7 | 8 | namespace OpenShock.API.Controller.Admin; 9 | 10 | public sealed partial class AdminController 11 | { 12 | /// 13 | /// Deactivates a user 14 | /// 15 | /// OK 16 | /// Unauthorized 17 | [HttpPut("users/{userId}/deactivate")] 18 | [ProducesResponseType(StatusCodes.Status200OK)] 19 | public async Task DeactivateUser([FromRoute] Guid userId, [FromQuery(Name="deleteLater")] bool deleteLater, IAccountService accountService) 20 | { 21 | var deactivationResult = await accountService.DeactivateAccount(CurrentUser.Id, userId, deleteLater); 22 | return deactivationResult.Match( 23 | success => Ok("Account deactivated"), 24 | cannotDeactivatePrivledged => Problem(AccountActivationError.CannotDeactivateOrDeletePrivledgedAccount), 25 | alreadyDeactivated => Problem(AccountActivationError.AlreadyDeactivated), 26 | unauthorized => Problem(AccountActivationError.Unauthorized), 27 | notFound => NotFound("User not found") 28 | ); 29 | } 30 | } -------------------------------------------------------------------------------- /API/Controller/Admin/DeleteUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.EntityFrameworkCore; 3 | using OpenShock.API.Services.Account; 4 | using OpenShock.Common.Errors; 5 | using OpenShock.Common.Models; 6 | using Z.EntityFramework.Plus; 7 | 8 | namespace OpenShock.API.Controller.Admin; 9 | 10 | public sealed partial class AdminController 11 | { 12 | /// 13 | /// Deletes a user 14 | /// 15 | /// OK 16 | /// Unauthorized 17 | [HttpDelete("users/{userId}")] 18 | [ProducesResponseType(StatusCodes.Status200OK)] 19 | public async Task DeleteUser([FromRoute] Guid userId, IAccountService accountService) 20 | { 21 | var result = await accountService.DeleteAccount(CurrentUser.Id, userId); 22 | return result.Match( 23 | success => Ok("Account deleted"), 24 | cannotDeletePrivledged => Problem(AccountActivationError.CannotDeactivateOrDeletePrivledgedAccount), 25 | unauthorized => Problem(AccountActivationError.Unauthorized), 26 | notFound => NotFound("User not found") 27 | ); 28 | } 29 | } -------------------------------------------------------------------------------- /API/Controller/Admin/ReactivateUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.EntityFrameworkCore; 3 | using OpenShock.API.Services.Account; 4 | using OpenShock.Common.Errors; 5 | using OpenShock.Common.Models; 6 | using Z.EntityFramework.Plus; 7 | 8 | namespace OpenShock.API.Controller.Admin; 9 | 10 | public sealed partial class AdminController 11 | { 12 | /// 13 | /// Reactivates a user 14 | /// 15 | /// OK 16 | /// Unauthorized 17 | [HttpPut("users/{userId}/reactivate")] 18 | [ProducesResponseType(StatusCodes.Status200OK)] 19 | public async Task ReactivateUser([FromRoute] Guid userId, IAccountService accountService) 20 | { 21 | var reactivationResult = await accountService.ReactivateAccount(CurrentUser.Id, userId); 22 | return reactivationResult.Match( 23 | success => Ok("Account reactivated"), 24 | unauthorized => Problem(AccountActivationError.Unauthorized), 25 | notFound => NotFound("User not found") 26 | ); 27 | } 28 | } -------------------------------------------------------------------------------- /API/Controller/Admin/WebhookAdd.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using OpenShock.API.Controller.Admin.DTOs; 3 | using OpenShock.Common.Errors; 4 | using OpenShock.Common.Services.Webhook; 5 | 6 | namespace OpenShock.API.Controller.Admin; 7 | 8 | public sealed partial class AdminController 9 | { 10 | /// 11 | /// Creates a webhook 12 | /// 13 | /// OK 14 | /// Unauthorized 15 | [HttpPost("webhooks")] 16 | [ProducesResponseType(StatusCodes.Status200OK)] 17 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 18 | public async Task AddWebhook([FromBody] AddWebhookDto body, [FromServices] IWebhookService webhookService) 19 | { 20 | var result = await webhookService.AddWebhook(body.Name, body.Url); 21 | return result.Match( 22 | success => Ok(success.Value), 23 | unsupported => Problem(AdminError.WebhookOnlyDiscord) 24 | ); 25 | } 26 | } -------------------------------------------------------------------------------- /API/Controller/Admin/WebhookList.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.EntityFrameworkCore; 3 | using OpenShock.Common.Models; 4 | using OpenShock.Common.Services.Webhook; 5 | 6 | namespace OpenShock.API.Controller.Admin; 7 | 8 | public sealed partial class AdminController 9 | { 10 | /// 11 | /// List webhooks 12 | /// 13 | /// OK 14 | /// Unauthorized 15 | [HttpGet("webhooks")] 16 | public async Task ListWebhooks([FromServices] IWebhookService webhookService) 17 | { 18 | return await webhookService.GetWebhooks(); 19 | } 20 | } -------------------------------------------------------------------------------- /API/Controller/Admin/WebhookRemove.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.EntityFrameworkCore; 3 | using OpenShock.Common.Errors; 4 | using OpenShock.Common.Services.Webhook; 5 | 6 | namespace OpenShock.API.Controller.Admin; 7 | 8 | public sealed partial class AdminController 9 | { 10 | /// 11 | /// Removes a webhook 12 | /// 13 | /// OK 14 | /// Unauthorized 15 | [HttpDelete("webhooks/{id}")] 16 | [ProducesResponseType(StatusCodes.Status200OK)] 17 | [ProducesResponseType(StatusCodes.Status404NotFound)] 18 | public async Task RemoveWebhook([FromRoute] Guid id, [FromServices] IWebhookService webhookService) 19 | { 20 | bool removed = await webhookService.RemoveWebhook(id); 21 | return removed ? Ok() : Problem(AdminError.WebhookNotFound); 22 | } 23 | } -------------------------------------------------------------------------------- /API/Controller/Admin/_ApiController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using OpenShock.Common.Authentication; 4 | using OpenShock.Common.Authentication.ControllerBase; 5 | using OpenShock.Common.OpenShockDb; 6 | using Redis.OM.Contracts; 7 | 8 | namespace OpenShock.API.Controller.Admin; 9 | 10 | [ApiController] 11 | [Tags("Admin")] 12 | [Route("/{version:apiVersion}/admin")] 13 | [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie, Roles = "Admin")] 14 | public sealed partial class AdminController : AuthenticatedSessionControllerBase 15 | { 16 | private readonly OpenShockContext _db; 17 | private readonly IRedisConnectionProvider _redis; 18 | private readonly ILogger _logger; 19 | 20 | public AdminController(OpenShockContext db, IRedisConnectionProvider redis, ILogger logger) 21 | { 22 | _db = db; 23 | _redis = redis; 24 | _logger = logger; 25 | } 26 | } -------------------------------------------------------------------------------- /API/Controller/Device/GetSelf.cs: -------------------------------------------------------------------------------- 1 | using Asp.Versioning; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.EntityFrameworkCore; 4 | using OpenShock.API.Models.Response; 5 | using OpenShock.Common.Models; 6 | 7 | namespace OpenShock.API.Controller.Device; 8 | 9 | public sealed partial class DeviceController 10 | { 11 | /// 12 | /// Gets information about the authenticated device. 13 | /// 14 | /// The device information was successfully retrieved. 15 | [HttpGet("self")] 16 | [MapToApiVersion("1")] 17 | public async Task> GetSelf() 18 | { 19 | var shockers = await _db.Shockers.Where(x => x.DeviceId == CurrentDevice.Id).Select(x => new MinimalShocker 20 | { 21 | Id = x.Id, 22 | RfId = x.RfId, 23 | Model = x.Model 24 | }).ToArrayAsync(); 25 | 26 | return new(new DeviceSelfResponse 27 | { 28 | Id = CurrentDevice.Id, 29 | Name = CurrentDevice.Name, 30 | Shockers = shockers 31 | } 32 | ); 33 | } 34 | } -------------------------------------------------------------------------------- /API/Controller/Device/_ApiController.cs: -------------------------------------------------------------------------------- 1 | using Asp.Versioning; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | using OpenShock.Common.Authentication; 5 | using OpenShock.Common.Authentication.ControllerBase; 6 | using OpenShock.Common.OpenShockDb; 7 | using Redis.OM.Contracts; 8 | 9 | namespace OpenShock.API.Controller.Device; 10 | 11 | /// 12 | /// For devices (ESP's) 13 | /// 14 | [ApiController] 15 | [ApiVersion("1")] 16 | [ApiVersion("2")] 17 | [Tags("Hub Endpoints")] 18 | [Route("/{version:apiVersion}/device")] 19 | [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.HubToken)] 20 | public sealed partial class DeviceController : AuthenticatedHubControllerBase 21 | { 22 | private readonly OpenShockContext _db; 23 | private readonly IRedisConnectionProvider _redis; 24 | private readonly ILogger _logger; 25 | 26 | public DeviceController(OpenShockContext db, IRedisConnectionProvider redis, ILogger logger) 27 | { 28 | _db = db; 29 | _redis = redis; 30 | _logger = logger; 31 | } 32 | } -------------------------------------------------------------------------------- /API/Controller/Devices/_ApiController.cs: -------------------------------------------------------------------------------- 1 | using Asp.Versioning; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | using OpenShock.Common.Authentication; 5 | using OpenShock.Common.Authentication.ControllerBase; 6 | using OpenShock.Common.OpenShockDb; 7 | using Redis.OM.Contracts; 8 | 9 | namespace OpenShock.API.Controller.Devices; 10 | 11 | /// 12 | /// Device management 13 | /// 14 | [ApiController] 15 | [Tags("Hub Management")] 16 | [ApiVersion("1"), ApiVersion("2")] 17 | [Route("/{version:apiVersion}/devices")] 18 | [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionApiTokenCombo)] 19 | public sealed partial class DevicesController : AuthenticatedSessionControllerBase 20 | { 21 | private readonly OpenShockContext _db; 22 | private readonly IRedisConnectionProvider _redis; 23 | private readonly ILogger _logger; 24 | 25 | public DevicesController(OpenShockContext db, IRedisConnectionProvider redis, ILogger logger) 26 | { 27 | _db = db; 28 | _redis = redis; 29 | _logger = logger; 30 | } 31 | } -------------------------------------------------------------------------------- /API/Controller/Public/GetStats.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using NRedisStack.RedisStackCommands; 3 | using OpenShock.Common.Models; 4 | using OpenShock.Common.Redis; 5 | using StackExchange.Redis; 6 | 7 | namespace OpenShock.API.Controller.Public; 8 | 9 | public sealed partial class PublicController 10 | { 11 | /// 12 | /// Gets online devices statistics 13 | /// 14 | /// The statistics were successfully retrieved. 15 | [HttpGet("stats")] 16 | [Tags("Meta")] 17 | public async Task> GetOnlineDevicesStatistics([FromServices] IConnectionMultiplexer redisConnectionMultiplexer) 18 | { 19 | var ft = redisConnectionMultiplexer.GetDatabase().FT(); 20 | var deviceOnlineInfo = await ft.InfoAsync(DeviceOnline.IndexName); 21 | 22 | return new(new StatsResponse 23 | { 24 | DevicesOnline = deviceOnlineInfo.NumDocs 25 | }); 26 | } 27 | } 28 | 29 | public sealed class StatsResponse 30 | { 31 | public required long DevicesOnline { get; set; } 32 | } -------------------------------------------------------------------------------- /API/Controller/Public/_ApiController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using OpenShock.Common; 3 | using OpenShock.Common.OpenShockDb; 4 | using Redis.OM.Contracts; 5 | 6 | namespace OpenShock.API.Controller.Public; 7 | 8 | [ApiController] 9 | [Route("/{version:apiVersion}/public")] 10 | public sealed partial class PublicController : OpenShockControllerBase 11 | { 12 | private readonly OpenShockContext _db; 13 | private readonly IRedisConnectionProvider _redis; 14 | private readonly ILogger _logger; 15 | 16 | public PublicController(OpenShockContext db, IRedisConnectionProvider redis, ILogger logger) 17 | { 18 | _db = db; 19 | _redis = redis; 20 | _logger = logger; 21 | } 22 | } -------------------------------------------------------------------------------- /API/Controller/Sessions/DeleteSessions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using OpenShock.Common.Errors; 3 | using OpenShock.Common.Extensions; 4 | using OpenShock.Common.Models; 5 | using OpenShock.Common.Problems; 6 | using System.Net.Mime; 7 | 8 | namespace OpenShock.API.Controller.Sessions; 9 | 10 | public sealed partial class SessionsController 11 | { 12 | [HttpDelete("{sessionId}")] 13 | [ProducesResponseType(StatusCodes.Status200OK)] 14 | [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // SessionNotFound 15 | public async Task DeleteSession(Guid sessionId) 16 | { 17 | var loginSession = await _sessionService.GetSessionById(sessionId); 18 | 19 | // If the session was not found, or the user does not have the privledges to access it, return NotFound 20 | if (loginSession == null || !CurrentUser.IsUserOrRole(loginSession.UserId, RoleType.Admin)) 21 | { 22 | return Problem(SessionError.SessionNotFound); 23 | } 24 | 25 | await _sessionService.DeleteSession(loginSession); 26 | 27 | return Ok(); 28 | } 29 | } -------------------------------------------------------------------------------- /API/Controller/Sessions/ListSessions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using OpenShock.API.Models.Response; 3 | 4 | namespace OpenShock.API.Controller.Sessions; 5 | 6 | public sealed partial class SessionsController 7 | { 8 | [HttpGet] 9 | public async Task> ListSessions() 10 | { 11 | var sessions = await _sessionService.ListSessionsByUserId(CurrentUser.Id); 12 | 13 | return sessions.Select(LoginSessionResponse.MapFrom); 14 | } 15 | } -------------------------------------------------------------------------------- /API/Controller/Sessions/SessionSelf.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using OpenShock.API.Models.Response; 3 | using OpenShock.Common.Authentication.Services; 4 | 5 | namespace OpenShock.API.Controller.Sessions; 6 | 7 | public sealed partial class SessionsController 8 | { 9 | /// 10 | /// Gets information about the current token used to access this endpoint 11 | /// 12 | /// 13 | /// 14 | /// 15 | [HttpGet("self")] 16 | public LoginSessionResponse GetSelfSession([FromServices] IUserReferenceService userReferenceService) 17 | { 18 | var x = userReferenceService.AuthReference; 19 | 20 | if (x == null) throw new Exception("This should not be reachable due to AuthenticatedSession requirement"); 21 | if (!x.Value.IsT0) throw new Exception("This should not be reachable due to the [UserSessionOnly] attribute"); 22 | 23 | var session = x.Value.AsT0; 24 | 25 | return LoginSessionResponse.MapFrom(session); 26 | } 27 | } -------------------------------------------------------------------------------- /API/Controller/Sessions/_ApiController.cs: -------------------------------------------------------------------------------- 1 | using Asp.Versioning; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | using OpenShock.Common.Authentication; 5 | using OpenShock.Common.Authentication.ControllerBase; 6 | using OpenShock.Common.Services.Session; 7 | 8 | namespace OpenShock.API.Controller.Sessions; 9 | 10 | /// 11 | /// Session management 12 | /// 13 | [ApiController] 14 | [Tags("Sessions")] 15 | [ApiVersion("1")] 16 | [Route("/{version:apiVersion}/sessions")] 17 | [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] 18 | public sealed partial class SessionsController : AuthenticatedSessionControllerBase 19 | { 20 | private readonly ISessionService _sessionService; 21 | 22 | /// 23 | /// DI constructor 24 | /// 25 | /// 26 | public SessionsController(ISessionService sessionService) 27 | { 28 | _sessionService = sessionService; 29 | } 30 | 31 | 32 | } -------------------------------------------------------------------------------- /API/Controller/Shares/Links/CreatePublicShare.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using OpenShock.API.Models.Requests; 3 | using OpenShock.Common.Models; 4 | using OpenShock.Common.OpenShockDb; 5 | 6 | namespace OpenShock.API.Controller.Shares.Links; 7 | 8 | public sealed partial class ShareLinksController 9 | { 10 | /// 11 | /// Create a new public share 12 | /// 13 | /// The created public share 14 | [HttpPost(Name = "CreatePublicShare")] 15 | public async Task> CreatePublicShare([FromBody] PublicShareCreate body) 16 | { 17 | var entity = new PublicShare 18 | { 19 | Id = Guid.CreateVersion7(), 20 | OwnerId = CurrentUser.Id, 21 | Name = body.Name, 22 | ExpiresAt = body.ExpiresOn == null ? null : DateTime.SpecifyKind(body.ExpiresOn.Value, DateTimeKind.Utc) 23 | }; 24 | _db.PublicShares.Add(entity); 25 | await _db.SaveChangesAsync(); 26 | 27 | return new(entity.Id); 28 | } 29 | } -------------------------------------------------------------------------------- /API/Controller/Shares/Links/List.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.EntityFrameworkCore; 3 | using OpenShock.API.Models.Response; 4 | using OpenShock.Common.Models; 5 | 6 | namespace OpenShock.API.Controller.Shares.Links; 7 | 8 | public sealed partial class ShareLinksController 9 | { 10 | /// 11 | /// Get all public shares for the current user 12 | /// 13 | /// All public shares for the current user 14 | [HttpGet] 15 | public LegacyDataResponse> List() 16 | { 17 | var ownPublicShares = _db.PublicShares 18 | .Where(x => x.OwnerId == CurrentUser.Id) 19 | .Select(x => OwnPublicShareResponse.GetFromEf(x)) 20 | .AsAsyncEnumerable(); 21 | 22 | return new(ownPublicShares); 23 | } 24 | } -------------------------------------------------------------------------------- /API/Controller/Shares/Links/_ApiController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using OpenShock.Common.Authentication; 4 | using OpenShock.Common.Authentication.ControllerBase; 5 | using OpenShock.Common.OpenShockDb; 6 | 7 | namespace OpenShock.API.Controller.Shares.Links; 8 | 9 | /// 10 | /// Public shares management 11 | /// 12 | [ApiController] 13 | [Tags("Public Shocker Shares")] 14 | [Route("/{version:apiVersion}/shares/links")] 15 | [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionApiTokenCombo)] 16 | public sealed partial class ShareLinksController : AuthenticatedSessionControllerBase 17 | { 18 | private readonly OpenShockContext _db; 19 | 20 | public ShareLinksController(OpenShockContext db) 21 | { 22 | _db = db; 23 | } 24 | } -------------------------------------------------------------------------------- /API/Controller/Shares/_ApiController.cs: -------------------------------------------------------------------------------- 1 | using Asp.Versioning; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | using OpenShock.Common.Authentication; 5 | using OpenShock.Common.Authentication.ControllerBase; 6 | using OpenShock.Common.OpenShockDb; 7 | 8 | namespace OpenShock.API.Controller.Shares; 9 | 10 | /// 11 | /// Shocker share management 12 | /// 13 | [ApiController] 14 | [Tags("Shocker Shares")] 15 | [ApiVersion("1"), ApiVersion("2")] 16 | [Route("/{version:apiVersion}/shares")] 17 | [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionApiTokenCombo)] 18 | public sealed partial class SharesController : AuthenticatedSessionControllerBase 19 | { 20 | private readonly OpenShockContext _db; 21 | 22 | public SharesController(OpenShockContext db) 23 | { 24 | _db = db; 25 | } 26 | } -------------------------------------------------------------------------------- /API/Controller/Shockers/_ApiController.cs: -------------------------------------------------------------------------------- 1 | using Asp.Versioning; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | using OpenShock.Common.Authentication; 5 | using OpenShock.Common.Authentication.ControllerBase; 6 | using OpenShock.Common.OpenShockDb; 7 | 8 | namespace OpenShock.API.Controller.Shockers; 9 | 10 | /// 11 | /// Shocker management 12 | /// 13 | [ApiController] 14 | [Tags("Shockers")] 15 | [ApiVersion("1"), ApiVersion("2")] 16 | [Route("/{version:apiVersion}/shockers")] 17 | [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionApiTokenCombo)] 18 | public sealed partial class ShockerController : AuthenticatedSessionControllerBase 19 | { 20 | private readonly OpenShockContext _db; 21 | private readonly ILogger _logger; 22 | 23 | public ShockerController(OpenShockContext db, ILogger logger) 24 | { 25 | _db = db; 26 | _logger = logger; 27 | } 28 | } -------------------------------------------------------------------------------- /API/Controller/Tokens/_ApiController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using OpenShock.Common.Authentication; 4 | using OpenShock.Common.Authentication.ControllerBase; 5 | using OpenShock.Common.OpenShockDb; 6 | 7 | namespace OpenShock.API.Controller.Tokens; 8 | 9 | [ApiController] 10 | [Tags("API Tokens")] 11 | [Route("/{version:apiVersion}/tokens")] 12 | [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] 13 | public sealed partial class TokensController : AuthenticatedSessionControllerBase 14 | { 15 | private readonly OpenShockContext _db; 16 | private readonly ILogger _logger; 17 | 18 | public TokensController(OpenShockContext db, ILogger logger) 19 | { 20 | _db = db; 21 | _logger = logger; 22 | } 23 | } -------------------------------------------------------------------------------- /API/Controller/Users/GetSelf.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using OpenShock.Common.Extensions; 3 | using OpenShock.Common.Models; 4 | 5 | namespace OpenShock.API.Controller.Users; 6 | 7 | public sealed partial class UsersController 8 | { 9 | /// 10 | /// Get the current user's information. 11 | /// 12 | /// The user's information was successfully retrieved. 13 | [HttpGet("self")] 14 | public LegacyDataResponse GetSelf() 15 | { 16 | return new( 17 | new UserSelfResponse 18 | { 19 | Id = CurrentUser.Id, 20 | Name = CurrentUser.Name, 21 | Email = CurrentUser.Email, 22 | Image = CurrentUser.GetImageUrl(), 23 | Roles = CurrentUser.Roles, 24 | Rank = CurrentUser.Roles.Count > 0 ? CurrentUser.Roles.Max().ToString() : "User" 25 | } 26 | ); 27 | } 28 | 29 | public sealed class UserSelfResponse 30 | { 31 | public required Guid Id { get; set; } 32 | public required string Name { get; set; } 33 | public required string Email { get; set; } 34 | public required Uri Image { get; set; } 35 | public required List Roles { get; set; } 36 | public required string Rank { get; set; } 37 | } 38 | } -------------------------------------------------------------------------------- /API/Controller/Users/_ApiController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using OpenShock.Common.Authentication; 4 | using OpenShock.Common.Authentication.ControllerBase; 5 | using OpenShock.Common.OpenShockDb; 6 | using Redis.OM.Contracts; 7 | 8 | namespace OpenShock.API.Controller.Users; 9 | 10 | [ApiController] 11 | [Tags("Users")] 12 | [Route("/{version:apiVersion}/users")] 13 | [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionApiTokenCombo)] 14 | public sealed partial class UsersController : AuthenticatedSessionControllerBase 15 | { 16 | private readonly OpenShockContext _db; 17 | private readonly IRedisConnectionProvider _redis; 18 | private readonly ILogger _logger; 19 | 20 | public UsersController(OpenShockContext db, IRedisConnectionProvider redis, ILogger logger) 21 | { 22 | _db = db; 23 | _redis = redis; 24 | _logger = logger; 25 | } 26 | } -------------------------------------------------------------------------------- /API/Models/Requests/ChangeEmailRequest.cs: -------------------------------------------------------------------------------- 1 |  2 | using OpenShock.Common.DataAnnotations; 3 | 4 | namespace OpenShock.API.Models.Requests; 5 | 6 | public sealed class ChangeEmailRequest 7 | { 8 | [EmailAddress(true)] 9 | public required string Email { get; set; } 10 | } -------------------------------------------------------------------------------- /API/Models/Requests/ChangePasswordRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using OpenShock.Common.DataAnnotations; 3 | 4 | namespace OpenShock.API.Models.Requests; 5 | 6 | public sealed class ChangePasswordRequest 7 | { 8 | [Required(AllowEmptyStrings = false)] 9 | public required string OldPassword { get; set; } 10 | 11 | [Password(true)] 12 | public required string NewPassword { get; set; } 13 | } -------------------------------------------------------------------------------- /API/Models/Requests/ChangeUsernameRequest.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.DataAnnotations; 2 | 3 | namespace OpenShock.API.Models.Requests; 4 | 5 | public sealed class ChangeUsernameRequest 6 | { 7 | [Username(true)] 8 | public required string Username { get; init; } 9 | } -------------------------------------------------------------------------------- /API/Models/Requests/CreateShareRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using OpenShock.Common.Constants; 3 | 4 | namespace OpenShock.API.Models.Requests; 5 | 6 | public sealed class CreateShareRequest 7 | { 8 | [MaxLength(HardLimits.CreateShareRequestMaxShockers)] 9 | public required ShockerPermLimitPairWithId[] Shockers { get; set; } 10 | public Guid? User { get; set; } = null; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /API/Models/Requests/CreateTokenRequest.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Models.Requests; 2 | 3 | public sealed class CreateTokenRequest : EditTokenRequest 4 | { 5 | public DateTime? ValidUntil { get; set; } = null; 6 | } -------------------------------------------------------------------------------- /API/Models/Requests/EditTokenRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using OpenShock.Common.Constants; 3 | using OpenShock.Common.Models; 4 | 5 | namespace OpenShock.API.Models.Requests; 6 | 7 | public class EditTokenRequest 8 | { 9 | [StringLength(HardLimits.ApiKeyNameMaxLength, MinimumLength = 1, ErrorMessage = "API token length must be between {1} and {2}")] 10 | public required string Name { get; set; } 11 | 12 | [MaxLength(HardLimits.ApiKeyMaxPermissions, ErrorMessage = "API token permissions must be between {1} and {2}")] 13 | public List Permissions { get; set; } = [PermissionType.Shockers_Use]; 14 | } -------------------------------------------------------------------------------- /API/Models/Requests/HubCreateRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using OpenShock.Common.Constants; 3 | 4 | namespace OpenShock.API.Models.Requests; 5 | 6 | public sealed class HubCreateRequest 7 | { 8 | [Required(AllowEmptyStrings = false)] 9 | [StringLength(HardLimits.HubNameMaxLength, MinimumLength = HardLimits.HubNameMinLength)] 10 | public required string Name { get; init; } 11 | } -------------------------------------------------------------------------------- /API/Models/Requests/HubEditRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using OpenShock.Common.Constants; 3 | 4 | namespace OpenShock.API.Models.Requests; 5 | 6 | public sealed class HubEditRequest 7 | { 8 | [Required(AllowEmptyStrings = false)] 9 | [StringLength(HardLimits.HubNameMaxLength, MinimumLength = HardLimits.HubNameMinLength)] 10 | public required string Name { get; set; } 11 | } -------------------------------------------------------------------------------- /API/Models/Requests/Login.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace OpenShock.API.Models.Requests; 4 | 5 | public sealed class Login 6 | { 7 | [Required(AllowEmptyStrings = false)] 8 | public required string Password { get; set; } 9 | 10 | [Required(AllowEmptyStrings = false)] 11 | public required string Email { get; set; } 12 | } -------------------------------------------------------------------------------- /API/Models/Requests/LoginV2.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace OpenShock.API.Models.Requests; 4 | 5 | public sealed class LoginV2 6 | { 7 | [Required(AllowEmptyStrings = false)] 8 | public required string Password { get; set; } 9 | 10 | [Required(AllowEmptyStrings = false)] 11 | public required string UsernameOrEmail { get; set; } 12 | 13 | [Required(AllowEmptyStrings = false)] 14 | public required string TurnstileResponse { get; set; } 15 | } -------------------------------------------------------------------------------- /API/Models/Requests/NewShocker.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using OpenShock.Common.Constants; 3 | using OpenShock.Common.Models; 4 | 5 | namespace OpenShock.API.Models.Requests; 6 | 7 | public sealed class NewShocker 8 | { 9 | [Required(AllowEmptyStrings = false)] 10 | [StringLength(HardLimits.ShockerNameMaxLength, MinimumLength = HardLimits.ShockerNameMinLength)] 11 | public required string Name { get; set; } 12 | public required ushort RfId { get; set; } 13 | public required Guid Device { get; set; } 14 | public required ShockerModelType Model { get; set; } 15 | } -------------------------------------------------------------------------------- /API/Models/Requests/PasswordResetRequestV2.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.DataAnnotations; 2 | 3 | namespace OpenShock.API.Models.Requests; 4 | 5 | public sealed class PasswordResetRequestV2 6 | { 7 | [EmailAddress(true)] 8 | public required string Email { get; set; } 9 | 10 | [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = false)] 11 | public required string TurnstileResponse { get; set; } 12 | } -------------------------------------------------------------------------------- /API/Models/Requests/PauseRequest.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Models.Requests; 2 | 3 | public sealed class PauseRequest 4 | { 5 | public required bool Pause { get; set; } 6 | } -------------------------------------------------------------------------------- /API/Models/Requests/PublicShareCreate.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using OpenShock.Common.Constants; 3 | 4 | namespace OpenShock.API.Models.Requests; 5 | 6 | public sealed class PublicShareCreate 7 | { 8 | [Required(AllowEmptyStrings = false)] 9 | [StringLength(HardLimits.PublicShareNameMaxLength, MinimumLength = HardLimits.PublicShareNameMinLength)] 10 | public required string Name { get; set; } 11 | public DateTime? ExpiresOn { get; set; } = null; 12 | } -------------------------------------------------------------------------------- /API/Models/Requests/PublicShareEditShocker.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using OpenShock.API.Models.Response; 3 | using OpenShock.Common.Constants; 4 | 5 | namespace OpenShock.API.Models.Requests; 6 | 7 | public sealed class PublicShareEditShocker 8 | { 9 | public required ShockerPermissions Permissions { get; set; } 10 | public required ShockerLimits Limits { get; set; } 11 | 12 | [Range(HardLimits.MinControlDuration, HardLimits.MaxControlDuration)] 13 | public ushort? Cooldown { get; set; } 14 | } -------------------------------------------------------------------------------- /API/Models/Requests/ReportTokensRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using OpenShock.Common.DataAnnotations; 3 | 4 | namespace OpenShock.API.Models.Requests; 5 | 6 | public class ReportTokensRequest 7 | { 8 | [Required(AllowEmptyStrings = false)] 9 | public required string TurnstileResponse { get; set; } 10 | 11 | [MaxLength(512)] 12 | [StringCollectionItemMaxLength(64)] 13 | public required string[] Secrets { get; set; } 14 | } -------------------------------------------------------------------------------- /API/Models/Requests/ShockerPermLimitPair.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.API.Models.Response; 2 | 3 | namespace OpenShock.API.Models.Requests; 4 | 5 | public class ShockerPermLimitPair 6 | { 7 | public required ShockerPermissions Permissions { get; set; } 8 | public required ShockerLimits Limits { get; set; } 9 | } -------------------------------------------------------------------------------- /API/Models/Requests/ShockerPermLimitPairWithId.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Models.Requests; 2 | 3 | public class ShockerPermLimitPairWithId : ShockerPermLimitPair 4 | { 5 | public Guid Id { get; set; } 6 | } -------------------------------------------------------------------------------- /API/Models/Requests/Signup.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.DataAnnotations; 2 | 3 | namespace OpenShock.API.Models.Requests; 4 | 5 | public sealed class SignUp 6 | { 7 | [Username(true)] 8 | public required string Username { get; set; } 9 | 10 | [Password(true)] 11 | public required string Password { get; set; } 12 | 13 | [EmailAddress(true)] 14 | public required string Email { get; set; } 15 | } -------------------------------------------------------------------------------- /API/Models/Requests/SignupV2.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.DataAnnotations; 2 | 3 | namespace OpenShock.API.Models.Requests; 4 | 5 | public sealed class SignUpV2 6 | { 7 | [Username(true)] 8 | public required string Username { get; set; } 9 | 10 | [Password(true)] 11 | public required string Password { get; set; } 12 | 13 | [EmailAddress(true)] 14 | public required string Email { get; set; } 15 | 16 | [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = false)] 17 | public required string TurnstileResponse { get; set; } 18 | } -------------------------------------------------------------------------------- /API/Models/Response/DeviceSelfResponse.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Models.Response; 2 | 3 | public sealed class DeviceSelfResponse 4 | { 5 | public required Guid Id { get; set; } 6 | public required string Name { get; set; } 7 | public required MinimalShocker[] Shockers { get; set; } 8 | } -------------------------------------------------------------------------------- /API/Models/Response/LcgNodeResponse.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Models.Response; 2 | 3 | public sealed class LcgNodeResponse 4 | { 5 | public required string Fqdn { get; set; } 6 | public required string Country { get; set; } 7 | } -------------------------------------------------------------------------------- /API/Models/Response/LcgNodeResponseV2.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Models.Response; 2 | 3 | public sealed class LcgNodeResponseV2 4 | { 5 | public required string Host { get; set; } 6 | public required ushort Port { get; set; } 7 | public required string Path { get; set; } 8 | public required string Country { get; set; } 9 | } -------------------------------------------------------------------------------- /API/Models/Response/LogEntry.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Models; 2 | 3 | namespace OpenShock.API.Models.Response; 4 | 5 | public sealed class LogEntry 6 | { 7 | public required Guid Id { get; set; } 8 | 9 | public required DateTime CreatedOn { get; set; } 10 | 11 | public required ControlType Type { get; set; } 12 | 13 | public required ControlLogSenderLight ControlledBy { get; set; } 14 | 15 | public required byte Intensity { get; set; } 16 | 17 | public required uint Duration { get; set; } 18 | } -------------------------------------------------------------------------------- /API/Models/Response/LoginSessionResponse.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Redis; 2 | 3 | namespace OpenShock.API.Models.Response; 4 | 5 | public sealed class LoginSessionResponse 6 | { 7 | public static LoginSessionResponse MapFrom(LoginSession session) 8 | { 9 | return new LoginSessionResponse 10 | { 11 | Id = session.PublicId!.Value, 12 | Ip = session.Ip, 13 | UserAgent = session.UserAgent, 14 | Created = session.Created!.Value, 15 | Expires = session.Expires!.Value, 16 | LastUsed = session.LastUsed 17 | }; 18 | } 19 | 20 | public required Guid Id { get; set; } 21 | public required string Ip { get; set; } 22 | public required string UserAgent { get; set; } 23 | public required DateTimeOffset Created { get; set; } 24 | public required DateTimeOffset Expires { get; set; } 25 | public required DateTimeOffset? LastUsed { get; set; } 26 | } -------------------------------------------------------------------------------- /API/Models/Response/OwnPublicShareResponse.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.OpenShockDb; 2 | 3 | namespace OpenShock.API.Models.Response; 4 | 5 | public sealed class OwnPublicShareResponse 6 | { 7 | public required Guid Id { get; set; } 8 | public required string Name { get; set; } 9 | public required DateTime CreatedOn { get; set; } 10 | public DateTime? ExpiresOn { get; set; } 11 | 12 | public static OwnPublicShareResponse GetFromEf(PublicShare x) => new() 13 | { 14 | Id = x.Id, 15 | Name = x.Name, 16 | CreatedOn = x.CreatedAt, 17 | ExpiresOn = x.ExpiresAt 18 | }; 19 | } -------------------------------------------------------------------------------- /API/Models/Response/OwnerShockerResponse.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Models.Response; 2 | 3 | public sealed class OwnerShockerResponse 4 | { 5 | public required Guid Id { get; set; } 6 | public required string Name { get; set; } 7 | public required Uri Image { get; set; } 8 | public required SharedDevice[] Devices { get; set; } 9 | 10 | public sealed class SharedDevice 11 | { 12 | public required Guid Id { get; set; } 13 | public required string Name { get; set; } 14 | // ReSharper disable once CollectionNeverQueried.Global 15 | public required SharedShocker[] Shockers { get; set; } 16 | 17 | public sealed class SharedShocker 18 | { 19 | public required Guid Id { get; set; } 20 | public required string Name { get; set; } 21 | public required bool IsPaused { get; set; } 22 | public required ShockerPermissions Permissions { get; set; } 23 | public required ShockerLimits Limits { get; set; } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /API/Models/Response/PublicShareDevice.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Models.Response; 2 | 3 | public sealed class PublicShareDevice 4 | { 5 | public required Guid Id { get; set; } 6 | public required string Name { get; set; } 7 | public required IList Shockers { get; set; } 8 | } -------------------------------------------------------------------------------- /API/Models/Response/PublicShareResponse.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Models; 2 | 3 | namespace OpenShock.API.Models.Response; 4 | 5 | public sealed class PublicShareResponse 6 | { 7 | public required Guid Id { get; set; } 8 | public required string Name { get; set; } 9 | 10 | public required DateTime CreatedOn { get; set; } 11 | public DateTime? ExpiresOn { get; set; } 12 | public required BasicUserInfo Author { get; set; } 13 | 14 | public IList Devices { get; set; } = 15 | new List(); 16 | } -------------------------------------------------------------------------------- /API/Models/Response/PublicShareShocker.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Models; 2 | 3 | namespace OpenShock.API.Models.Response; 4 | 5 | public sealed class PublicShareShocker 6 | { 7 | public required Guid Id { get; set; } 8 | public required string Name { get; set; } 9 | public required ShockerPermissions Permissions { get; set; } 10 | public required ShockerLimits Limits { get; set; } 11 | public required PauseReason Paused { get; set; } 12 | } -------------------------------------------------------------------------------- /API/Models/Response/RequestShareInfo.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.API.Models.Requests; 2 | using OpenShock.Common.Models; 3 | 4 | namespace OpenShock.API.Models.Response; 5 | 6 | public sealed class RequestShareInfo : ShockerPermLimitPairWithId 7 | { 8 | public required BasicUserInfo? SharedWith { get; set; } 9 | public required DateTime CreatedOn { get; set; } 10 | } -------------------------------------------------------------------------------- /API/Models/Response/ResponseDevice.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Models.Response; 2 | 3 | public class ResponseDevice 4 | { 5 | public required Guid Id { get; set; } 6 | public required string Name { get; set; } 7 | public required DateTime CreatedOn { get; set; } 8 | } -------------------------------------------------------------------------------- /API/Models/Response/ResponseDeviceWithShockers.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Models.Response; 2 | 3 | public sealed class ResponseDeviceWithShockers : ResponseDevice 4 | { 5 | public required ShockerResponse[] Shockers { get; set; } 6 | } -------------------------------------------------------------------------------- /API/Models/Response/ResponseDeviceWithToken.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Models.Response; 2 | 3 | public sealed class ResponseDeviceWithToken : ResponseDevice 4 | { 5 | public required string? Token { get; set; } 6 | } -------------------------------------------------------------------------------- /API/Models/Response/ShareInfo.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Models; 2 | 3 | namespace OpenShock.API.Models.Response; 4 | 5 | public sealed class ShareInfo 6 | { 7 | public required BasicUserInfo SharedWith { get; set; } 8 | public required DateTime CreatedOn { get; set; } 9 | public required ShockerPermissions Permissions { get; set; } 10 | public required ShockerLimits Limits { get; set; } 11 | public required bool Paused { get; set; } 12 | } -------------------------------------------------------------------------------- /API/Models/Response/ShockerLimits.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using OpenShock.Common.Constants; 3 | 4 | namespace OpenShock.API.Models.Response; 5 | 6 | public sealed class ShockerLimits 7 | { 8 | [Range(HardLimits.MinControlIntensity, HardLimits.MaxControlIntensity)] 9 | public required byte? Intensity { get; set; } 10 | 11 | [Range(HardLimits.MinControlDuration, HardLimits.MaxControlDuration)] 12 | public required ushort? Duration { get; set; } 13 | } -------------------------------------------------------------------------------- /API/Models/Response/ShockerPermissions.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Models.Response; 2 | 3 | public sealed class ShockerPermissions 4 | { 5 | public required bool Vibrate { get; set; } 6 | public required bool Sound { get; set; } 7 | public required bool Shock { get; set; } 8 | public bool Live { get; set; } = false; 9 | } -------------------------------------------------------------------------------- /API/Models/Response/ShockerResponse.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Models; 2 | 3 | namespace OpenShock.API.Models.Response; 4 | 5 | public class MinimalShocker 6 | { 7 | public required Guid Id { get; set; } 8 | public required ushort RfId { get; set; } 9 | public required ShockerModelType Model { get; set; } 10 | } 11 | 12 | public class ShockerResponse : MinimalShocker 13 | { 14 | public required string Name { get; set; } 15 | public required bool IsPaused { get; set; } 16 | public required DateTime CreatedOn { get; set; } 17 | } -------------------------------------------------------------------------------- /API/Models/Response/ShockerWithDevice.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Models.Response; 2 | 3 | public sealed class ShockerWithDevice : ShockerResponse 4 | { 5 | public required Guid Device { get; set; } 6 | } -------------------------------------------------------------------------------- /API/Models/Response/TokenCreatedResponse.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Models.Response; 2 | 3 | public sealed class TokenCreatedResponse 4 | { 5 | public required string Token { get; set; } 6 | public required Guid Id { get; set; } 7 | } -------------------------------------------------------------------------------- /API/Models/Response/TokenResponse.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Models; 2 | 3 | namespace OpenShock.API.Models.Response; 4 | 5 | public sealed class TokenResponse 6 | { 7 | public required Guid Id { get; set; } 8 | 9 | public required string Name { get; set; } 10 | 11 | public required DateTime CreatedOn { get; set; } 12 | 13 | public required DateTime? ValidUntil { get; set; } 14 | 15 | public required DateTime LastUsed { get; set; } 16 | 17 | public required List Permissions { get; set; } 18 | } -------------------------------------------------------------------------------- /API/Models/Response/UserSharesResponse.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Models.Response; 2 | 3 | public sealed class UserShareInfo 4 | { 5 | public required Guid Id { get; set; } 6 | public required string Name { get; set; } 7 | public required DateTime CreatedOn { get; set; } 8 | public required ShockerPermissions Permissions { get; set; } 9 | public required ShockerLimits Limits { get; set; } 10 | public required bool Paused { get; set; } 11 | } -------------------------------------------------------------------------------- /API/Models/Response/V2UserSharesListItem.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Models.Response; 2 | 3 | public sealed class V2UserSharesListItem 4 | { 5 | public required Guid Id { get; set; } 6 | public required string Name { get; set; } 7 | public required Uri Image { get; set; } 8 | public required IEnumerable Shares { get; init; } 9 | } -------------------------------------------------------------------------------- /API/Options/MailJetOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace OpenShock.API.Options; 5 | 6 | public sealed class MailJetOptions 7 | { 8 | public const string SectionName = MailOptions.SectionName + ":Mailjet"; 9 | 10 | [Required(AllowEmptyStrings = false)] 11 | public required string Key { get; init; } 12 | 13 | [Required(AllowEmptyStrings = false)] 14 | public required string Secret { get; init; } 15 | 16 | [Required] 17 | [ValidateObjectMembers] 18 | public required MailjetTemplateOptions Template { get; init; } 19 | 20 | public sealed class MailjetTemplateOptions 21 | { 22 | [Required] 23 | public required ulong PasswordReset { get; init; } 24 | 25 | [Required] 26 | public required ulong PasswordResetComplete { get; init; } 27 | 28 | [Required] 29 | public required ulong VerifyEmail { get; init; } 30 | 31 | [Required] 32 | public required ulong VerifyEmailComplete { get; init; } 33 | } 34 | } 35 | 36 | [OptionsValidator] 37 | public partial class MailJetOptionsValidator : IValidateOptions 38 | { 39 | } -------------------------------------------------------------------------------- /API/Options/MailOptions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using OpenShock.API.Services.Email.Mailjet.Mail; 3 | 4 | namespace OpenShock.API.Options; 5 | 6 | public sealed class MailOptions 7 | { 8 | public const string SectionName = "OpenShock:Mail"; 9 | public const string SenderSectionName = SectionName + ":Sender"; 10 | 11 | [Required] 12 | public required MailType Type { get; init; } 13 | 14 | public enum MailType 15 | { 16 | Mailjet = 0, 17 | Smtp = 1, 18 | None = 2 19 | } 20 | 21 | public sealed class MailSenderContact : Contact; 22 | } 23 | -------------------------------------------------------------------------------- /API/Options/SmtpOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace OpenShock.API.Options; 5 | 6 | public sealed class SmtpOptions 7 | { 8 | public const string SectionName = MailOptions.SectionName + ":Smtp"; 9 | 10 | [Required(AllowEmptyStrings = false)] 11 | public required string Host { get; init; } 12 | 13 | public ushort Port { get; init; } = 587; 14 | 15 | public string Username { get; init; } = string.Empty; 16 | 17 | public string Password { get; init; } = string.Empty; 18 | 19 | public bool EnableSsl { get; init; } = true; 20 | 21 | public bool VerifyCertificate { get; init; } = true; 22 | } 23 | 24 | [OptionsValidator] 25 | public partial class SmtpOptionsValidator : IValidateOptions 26 | { 27 | } -------------------------------------------------------------------------------- /API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "API": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /API/Services/Account/LoginContext.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Services.Account; 2 | 3 | public readonly record struct LoginContext(string UserAgent, string Ip); -------------------------------------------------------------------------------- /API/Services/Email/EmailServiceUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Mail; 2 | using MimeKit; 3 | using OpenShock.API.Services.Email.Mailjet.Mail; 4 | 5 | namespace OpenShock.API.Services.Email; 6 | 7 | public static class EmailServiceUtils 8 | { 9 | public static MailboxAddress ToMailAddress(this Contact contact) => new(contact.Name, contact.Email); 10 | public static Contact ToContact(this MailAddress mailAddress) => new() { Email = mailAddress.Address, Name = mailAddress.DisplayName }; 11 | 12 | } -------------------------------------------------------------------------------- /API/Services/Email/IEmailService.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.API.Services.Email.Mailjet.Mail; 2 | 3 | namespace OpenShock.API.Services.Email; 4 | 5 | public interface IEmailService 6 | { 7 | /// 8 | /// Send a password reset email 9 | /// 10 | /// 11 | /// 12 | /// 13 | /// 14 | public Task PasswordReset(Contact to, Uri resetLink, CancellationToken cancellationToken = default); 15 | 16 | /// 17 | /// When a user uses the signup form we send this email to let them activate their email 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// 23 | public Task VerifyEmail(Contact to, Uri activationLink, CancellationToken cancellationToken = default); 24 | } 25 | -------------------------------------------------------------------------------- /API/Services/Email/Mailjet/Mail/Contact.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace OpenShock.API.Services.Email.Mailjet.Mail; 5 | 6 | public class Contact 7 | { 8 | [Required(AllowEmptyStrings = false)] 9 | public required string Email { get; set; } 10 | 11 | [Required(AllowEmptyStrings = false)] 12 | public required string Name { get; set; } 13 | 14 | public Contact() { } 15 | 16 | [SetsRequiredMembers] 17 | public Contact(string email, string name) 18 | { 19 | Email = email; 20 | Name = name; 21 | } 22 | 23 | // public static readonly Contact AccountManagement = new() 24 | // { 25 | // Email = "system@shocklink.net", 26 | // Name = "OpenShock System" 27 | // }; 28 | } -------------------------------------------------------------------------------- /API/Services/Email/Mailjet/Mail/MailBase.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using OpenShock.API.Utils; 3 | 4 | namespace OpenShock.API.Services.Email.Mailjet.Mail; 5 | 6 | [JsonConverter(typeof(OneWayPolymorphicJsonConverter))] 7 | public abstract class MailBase 8 | { 9 | public required Contact From { get; set; } 10 | public required Contact[] To { get; set; } 11 | public required string Subject { get; set; } 12 | public Dictionary? Variables { get; set; } 13 | } -------------------------------------------------------------------------------- /API/Services/Email/Mailjet/Mail/MailsWrap.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Services.Email.Mailjet.Mail; 2 | 3 | public sealed class MailsWrap 4 | { 5 | public required MailBase[] Messages { get; set; } 6 | } -------------------------------------------------------------------------------- /API/Services/Email/Mailjet/Mail/TemplateMail.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Services.Email.Mailjet.Mail; 2 | 3 | public sealed class TemplateMail : MailBase 4 | { 5 | public bool TemplateLanguage { get; set; } = true; 6 | public required ulong TemplateId { get; set; } 7 | public new required Dictionary Variables { get; set; } = new(); 8 | } -------------------------------------------------------------------------------- /API/Services/Email/Mailjet/MailjetEmailServiceExtension.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.API.Options; 2 | using System.Net.Http.Headers; 3 | using System.Text; 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace OpenShock.API.Services.Email.Mailjet; 7 | 8 | public static class MailjetEmailServiceExtension 9 | { 10 | public static WebApplicationBuilder AddMailjetEmailService(this WebApplicationBuilder builder) 11 | { 12 | var section = builder.Configuration.GetRequiredSection(MailJetOptions.SectionName); 13 | 14 | builder.Services.Configure(section); 15 | builder.Services.AddSingleton, MailJetOptionsValidator>(); 16 | 17 | var options = section.Get() ?? throw new NullReferenceException("MailJetOptions is null!"); 18 | var basicAuthValue = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{options.Key}:{options.Secret}")); 19 | 20 | builder.Services.AddHttpClient(httpclient => 21 | { 22 | httpclient.BaseAddress = new Uri("https://api.mailjet.com/v3.1/"); 23 | httpclient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", basicAuthValue); 24 | }); 25 | 26 | return builder; 27 | } 28 | } -------------------------------------------------------------------------------- /API/Services/Email/NoneEmailService.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.API.Services.Email.Mailjet.Mail; 2 | 3 | namespace OpenShock.API.Services.Email; 4 | 5 | /// 6 | /// This is a noop implementation of the email service. It does nothing. 7 | /// Consumers should properly handle when this service is used, so realistaically this should never be used. 8 | /// But we need it for DI satisfaction. 9 | /// 10 | public class NoneEmailService : IEmailService 11 | { 12 | private readonly ILogger _logger; 13 | 14 | public NoneEmailService(ILogger logger) 15 | { 16 | _logger = logger; 17 | } 18 | 19 | public Task PasswordReset(Contact to, Uri resetLink, CancellationToken cancellationToken = default) 20 | { 21 | _logger.LogError("Password reset email not sent, this is a noop implementation of the email service"); 22 | return Task.CompletedTask; 23 | } 24 | 25 | public Task VerifyEmail(Contact to, Uri activationLink, CancellationToken cancellationToken = default) 26 | { 27 | _logger.LogError("Email verification email not sent, this is a noop implementation of the email service"); 28 | return Task.CompletedTask; 29 | } 30 | } -------------------------------------------------------------------------------- /API/Services/Email/Smtp/SmtpEmailServiceExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using OpenShock.API.Options; 3 | 4 | namespace OpenShock.API.Services.Email.Smtp; 5 | 6 | public static class SmtpEmailServiceExtension 7 | { 8 | public static WebApplicationBuilder AddSmtpEmailService(this WebApplicationBuilder builder) 9 | { 10 | var section = builder.Configuration.GetRequiredSection(SmtpOptions.SectionName); 11 | 12 | builder.Services.Configure(section); 13 | builder.Services.AddSingleton, SmtpOptionsValidator>(); 14 | 15 | builder.Services.AddSingleton(new SmtpServiceTemplates 16 | { 17 | PasswordReset = SmtpTemplate.ParseFromFileThrow("SmtpTemplates/PasswordReset.liquid").Result, 18 | EmailVerification = SmtpTemplate.ParseFromFileThrow("SmtpTemplates/EmailVerification.liquid").Result 19 | }); 20 | 21 | builder.Services.AddSingleton(); 22 | 23 | return builder; 24 | } 25 | } -------------------------------------------------------------------------------- /API/Services/Email/Smtp/SmtpServiceTemplates.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.API.Services.Email.Smtp; 2 | 3 | public sealed class SmtpServiceTemplates 4 | { 5 | public required SmtpTemplate PasswordReset { get; set; } 6 | public required SmtpTemplate EmailVerification { get; set; } 7 | } -------------------------------------------------------------------------------- /API/Utils/OneWayPolymorphicJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace OpenShock.API.Utils; 5 | 6 | public sealed class OneWayPolymorphicJsonConverter : JsonConverter 7 | { 8 | public override bool CanConvert(Type typeToConvert) 9 | { 10 | return typeof(T) == typeToConvert; //.IsAssignableFrom(typeToConvert); 11 | } 12 | 13 | public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => 14 | throw new NotSupportedException(); 15 | 16 | public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => 17 | JsonSerializer.Serialize(writer, value, value!.GetType()); 18 | } -------------------------------------------------------------------------------- /API/Utils/PublicShareUtils.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Models; 2 | 3 | namespace OpenShock.API.Utils; 4 | 5 | public static class PublicShareUtils 6 | { 7 | public static PauseReason GetPausedReason(bool publicShareLevel, bool shockerLevel) => publicShareLevel switch 8 | { 9 | true when shockerLevel => PauseReason.Shocker | PauseReason.ShareLink, 10 | true => PauseReason.ShareLink, 11 | _ => shockerLevel ? PauseReason.Shocker : PauseReason.None 12 | }; 13 | } -------------------------------------------------------------------------------- /API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Kestrel": { 3 | "Endpoints": { 4 | "Https": { 5 | "Url": "https://*:443" 6 | } 7 | } 8 | }, 9 | "Serilog": { 10 | "MinimumLevel": { 11 | "Default": "Verbose", 12 | "Override": { 13 | "Microsoft.Hosting.Lifetime": "Verbose", 14 | "Microsoft.AspNetCore.Hosting.Diagnostics": "Verbose", 15 | "Serilog.AspNetCore.RequestLoggingMiddleware": "Information", 16 | "Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker": "Warning", 17 | "OpenShock": "Verbose" 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "Kestrel": { 4 | "Endpoints": { 5 | "Http": { 6 | "Url": "http://*:80" 7 | } 8 | } 9 | }, 10 | "Serilog": { 11 | "Using": [ 12 | "Serilog.Sinks.Console", 13 | "Serilog.Sinks.Grafana.Loki", 14 | "OpenShock.Common" 15 | ], 16 | "MinimumLevel": { 17 | "Default": "Warning", 18 | "Override": { 19 | "Microsoft.Hosting.Lifetime": "Information", 20 | "Microsoft.AspNetCore.Hosting.Diagnostics": "Warning", 21 | "Serilog.AspNetCore.RequestLoggingMiddleware": "Information", 22 | "OpenShock": "Information" 23 | } 24 | }, 25 | "WriteTo": [ 26 | { 27 | "Name": "Console", 28 | "Args": { 29 | "outputTemplate": "[{Timestamp:HH:mm:ss.fff}] [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}" 30 | } 31 | } 32 | ], 33 | "Enrich": [ 34 | "FromLogContext", 35 | "WithOpenShockEnricher" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /API/devcert.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShock/API/2553c9de2f678d42ea6c89ac21a1489279752e87/API/devcert.pfx -------------------------------------------------------------------------------- /Common.Tests/Common.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Always 19 | 20 | 21 | Always 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Common.Tests/Geo/DistanceLookupTests.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Constants; 2 | using OpenShock.Common.Geo; 3 | 4 | namespace OpenShock.Common.Tests.Geo; 5 | 6 | public class DistanceLookupTests 7 | { 8 | [Test] 9 | [Arguments("US", "US", 0f)] 10 | [Arguments("US", "DE", 7861.5f)] 11 | public async Task TryGetDistanceBetween_ValidCountries(string str1, string str2, float expectedDistance) 12 | { 13 | // Act 14 | var result = DistanceLookup.TryGetDistanceBetween(str1, str2, out var distance); 15 | 16 | // Assert 17 | await Assert.That(result).IsTrue(); 18 | await Assert.That(distance).IsEqualTo(expectedDistance).Within(0.1f); 19 | } 20 | 21 | [Test] 22 | [Arguments("US", "XX")] 23 | [Arguments("XX", "US")] 24 | [Arguments("XX", "XX")] 25 | [Arguments("EZ", "PZ")] 26 | public async Task TryGetDistanceBetween_UnknownCountry(string str1, string str2) 27 | { 28 | // Act 29 | var result = DistanceLookup.TryGetDistanceBetween(str1, str2, out var distance); 30 | 31 | // Assert 32 | await Assert.That(result).IsFalse(); 33 | await Assert.That(distance).IsEqualTo(Distance.DistanceToAndromedaGalaxyInKm); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Common.Tests/Utils/HashingUtilsTests.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Utils; 2 | 3 | namespace OpenShock.Common.Tests.Utils; 4 | 5 | public class HashingUtilsTests 6 | { 7 | [Test] 8 | [Arguments("test", "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08")] 9 | [Arguments("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in", "2fac5f5f1d048a84fbb75c389f4596e05023ac17da4fcf45a5954d2d9a394301")] 10 | public async Task HashSha256(string str, string expectedHash) 11 | { 12 | // Act 13 | var result = HashingUtils.HashSha256(str); 14 | 15 | // Assert 16 | await Assert.That(result).IsEqualTo(expectedHash); 17 | } 18 | } -------------------------------------------------------------------------------- /Common/Authentication/ControllerBase/AuthenticatedHubControllerBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.Filters; 4 | using OpenShock.Common.Authentication.Services; 5 | using OpenShock.Common.OpenShockDb; 6 | 7 | namespace OpenShock.Common.Authentication.ControllerBase; 8 | 9 | [Authorize(AuthenticationSchemes = OpenShockAuthSchemas.HubToken)] 10 | public class AuthenticatedHubControllerBase : OpenShockControllerBase, IActionFilter 11 | { 12 | public Device CurrentDevice = null!; 13 | 14 | [NonAction] 15 | public void OnActionExecuting(ActionExecutingContext context) 16 | { 17 | CurrentDevice = ControllerContext.HttpContext.RequestServices.GetRequiredService>() 18 | .CurrentClient; 19 | } 20 | 21 | [NonAction] 22 | public void OnActionExecuted(ActionExecutedContext context) 23 | { 24 | } 25 | } -------------------------------------------------------------------------------- /Common/Authentication/OpenShockAuthClaims.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Authentication; 2 | 3 | public class OpenShockAuthClaims 4 | { 5 | public const string ApiTokenId = "openshock.apiTokenId"; 6 | public const string ApiTokenPermission = "openshock.ApiTokenPermission"; 7 | public const string HubId = "openshock.hubId"; 8 | } 9 | -------------------------------------------------------------------------------- /Common/Authentication/OpenShockAuthPolicies.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Authentication; 2 | 3 | public static class OpenShockAuthPolicies 4 | { 5 | public const string RankAdmin = "AdminOnly"; 6 | } 7 | -------------------------------------------------------------------------------- /Common/Authentication/OpenShockAuthSchemas.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Authentication; 2 | 3 | public static class OpenShockAuthSchemas 4 | { 5 | public const string UserSessionCookie = "UserSessionCookie"; 6 | public const string ApiToken = "ApiToken"; 7 | public const string HubToken = "HubToken"; 8 | 9 | public const string UserSessionApiTokenCombo = $"{UserSessionCookie},{ApiToken}"; 10 | } -------------------------------------------------------------------------------- /Common/Authentication/Requirements/ApiTokenPermissionRequirement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using OpenShock.Common.Models; 3 | 4 | namespace OpenShock.Common.Authentication.Requirements; 5 | 6 | public class ApiTokenPermissionRequirement : IAuthorizationRequirement 7 | { 8 | public ApiTokenPermissionRequirement(PermissionType requiredPermission) 9 | { 10 | RequiredPermission = requiredPermission; 11 | } 12 | 13 | public PermissionType RequiredPermission { get; init; } 14 | } 15 | -------------------------------------------------------------------------------- /Common/Authentication/Services/ClientAuthService.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Authentication.Services; 2 | 3 | public interface IClientAuthService 4 | { 5 | public T CurrentClient { get; set; } 6 | } 7 | 8 | public sealed class ClientAuthService : IClientAuthService where T : class 9 | { 10 | public T CurrentClient { get; set; } = null!; 11 | } -------------------------------------------------------------------------------- /Common/Authentication/Services/UserReferenceService.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.OpenShockDb; 2 | using OpenShock.Common.Redis; 3 | using OneOf; 4 | 5 | namespace OpenShock.Common.Authentication.Services; 6 | 7 | public interface IUserReferenceService 8 | { 9 | public OneOf? AuthReference { get; set; } 10 | } 11 | 12 | public sealed class UserReferenceService : IUserReferenceService 13 | { 14 | public OneOf? AuthReference { get; set; } = null; 15 | } -------------------------------------------------------------------------------- /Common/Constants/AuthConstants.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Constants; 2 | 3 | public static class AuthConstants 4 | { 5 | public const string UserSessionCookieName = "openShockSession"; 6 | public const string UserSessionHeaderName = "OpenShockSession"; 7 | public const string ApiTokenHeaderName = "OpenShockToken"; 8 | public const string HubTokenHeaderName = "DeviceToken"; 9 | 10 | public const int GeneratedTokenLength = 32; 11 | public const int ApiTokenLength = 64; 12 | } 13 | -------------------------------------------------------------------------------- /Common/Constants/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Constants; 2 | 3 | public static class Duration 4 | { 5 | public static readonly TimeSpan AuditRetentionTime = TimeSpan.FromDays(90); 6 | public static readonly TimeSpan ShockerControlLogRetentionTime = TimeSpan.FromDays(365); 7 | 8 | public static readonly TimeSpan PasswordResetRequestLifetime = TimeSpan.FromHours(1); 9 | 10 | public static readonly TimeSpan NameChangeCooldown = TimeSpan.FromDays(7); 11 | 12 | public static readonly TimeSpan LoginSessionLifetime = TimeSpan.FromDays(30); 13 | public static readonly TimeSpan LoginSessionExpansionAfter = TimeSpan.FromDays(1); 14 | 15 | public static readonly TimeSpan DevicePingInitialDelay = TimeSpan.FromSeconds(5); 16 | public static readonly TimeSpan DevicePingPeriod = TimeSpan.FromSeconds(15); 17 | public static readonly TimeSpan DeviceKeepAliveInitialTimeout = TimeSpan.FromSeconds(65); 18 | public static readonly TimeSpan DeviceKeepAliveTimeout = TimeSpan.FromSeconds(35); 19 | } 20 | -------------------------------------------------------------------------------- /Common/Constants/Distance.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Constants; 2 | 3 | public static class Distance 4 | { 5 | public const float DistanceToAndromedaGalaxyInKm = 2.401E19f; 6 | } -------------------------------------------------------------------------------- /Common/DataAnnotations/Interfaces/IOperationAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.OpenApi.Models; 2 | 3 | namespace OpenShock.Common.DataAnnotations.Interfaces; 4 | 5 | /// 6 | /// Represents an interface for operation attributes that can be applied to an OpenApiOperation instance. 7 | /// 8 | public interface IOperationAttribute 9 | { 10 | /// 11 | /// Applies the operation attribute to the given OpenApiOperation instance. 12 | /// 13 | /// The OpenApiOperation instance to apply the attribute to. 14 | void Apply(OpenApiOperation operation); 15 | } -------------------------------------------------------------------------------- /Common/DataAnnotations/Interfaces/IParameterAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.OpenApi.Models; 2 | 3 | namespace OpenShock.Common.DataAnnotations.Interfaces; 4 | 5 | /// 6 | /// Represents an interface for parameter attributes that can be applied to an OpenApiSchema or OpenApiParameter instance. 7 | /// 8 | public interface IParameterAttribute 9 | { 10 | /// 11 | /// Applies the parameter attribute to the given OpenApiSchema instance. 12 | /// 13 | /// The OpenApiSchema instance to apply the attribute to. 14 | void Apply(OpenApiSchema schema); 15 | 16 | /// 17 | /// Applies the parameter attribute to the given OpenApiParameter instance. 18 | /// 19 | /// The OpenApiParameter instance to apply the attribute to. 20 | void Apply(OpenApiParameter parameter); 21 | } -------------------------------------------------------------------------------- /Common/DataAnnotations/OpenApiSchemas.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.OpenApi.Any; 2 | using Microsoft.OpenApi.Models; 3 | using OpenShock.Common.Models; 4 | 5 | namespace OpenShock.Common.DataAnnotations; 6 | 7 | public static class OpenApiSchemas 8 | { 9 | public static OpenApiSchema SemVerSchema => new OpenApiSchema { 10 | Title = "SemVer", 11 | Type = "string", 12 | Pattern = /* lang=regex */ "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", 13 | Example = new OpenApiString("1.0.0-dev+a16f2") 14 | }; 15 | 16 | public static OpenApiSchema PauseReasonEnumSchema => new OpenApiSchema { 17 | Title = nameof(PauseReason), 18 | Type = "integer", 19 | Description = """ 20 | An integer representing the reason(s) for the shocker being paused, expressed as a bitfield where reasons are OR'd together. 21 | 22 | Each bit corresponds to: 23 | - 1: Shocker 24 | - 2: UserShare 25 | - 4: PublicShare 26 | 27 | For example, a value of 6 (2 | 4) indicates both 'UserShare' and 'PublicShare' reasons. 28 | """, 29 | Example = new OpenApiInteger(6) 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /Common/DataAnnotations/StringCollectionItemMaxLengthAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace OpenShock.Common.DataAnnotations; 4 | 5 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)] 6 | public sealed class StringCollectionItemMaxLengthAttribute : ValidationAttribute 7 | { 8 | public StringCollectionItemMaxLengthAttribute(int maxLength) 9 | { 10 | MaxLength = maxLength; 11 | } 12 | 13 | public int MaxLength { get; } 14 | 15 | public override bool IsValid(object? value) 16 | { 17 | return value is IEnumerable items && items.All(item => item.Length <= MaxLength); 18 | } 19 | } -------------------------------------------------------------------------------- /Common/DeviceControl/ControlShockerObj.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Models; 2 | 3 | namespace OpenShock.Common.DeviceControl; 4 | 5 | public sealed class ControlShockerObj 6 | { 7 | public required Guid Id { get; set; } 8 | public required string Name { get; set; } 9 | public required ushort RfId { get; set; } 10 | public required Guid Device { get; set; } 11 | public required Guid Owner { get; set; } 12 | public required ShockerModelType Model { get; set; } 13 | public required bool Paused { get; set; } 14 | public required SharePermsAndLimits? PermsAndLimits { get; set; } 15 | } -------------------------------------------------------------------------------- /Common/DeviceControl/NotAllShockersSucceeded.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.DeviceControl; 2 | 3 | public readonly struct NotAllShockersSucceeded; -------------------------------------------------------------------------------- /Common/Errors/AccountActivationError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems; 3 | 4 | namespace OpenShock.Common.Errors; 5 | 6 | public static class AccountActivationError 7 | { 8 | public static OpenShockProblem CannotDeactivateOrDeletePrivledgedAccount => new OpenShockProblem( 9 | "Account.Deactivate.DeniedPrivileged", "Privileged accounts cannot be deactivated/deleted", HttpStatusCode.Forbidden); 10 | public static OpenShockProblem AlreadyDeactivated => new OpenShockProblem( 11 | "Account.Deactivate.AlreadyDeactivated", "Account is already deactivated", HttpStatusCode.Forbidden); 12 | public static OpenShockProblem Unauthorized => new OpenShockProblem( 13 | "Account.Deactivate.Unauthorized", "You are not allowed to do this", HttpStatusCode.Unauthorized); 14 | } -------------------------------------------------------------------------------- /Common/Errors/AccountError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems; 3 | using OpenShock.Common.Validation; 4 | 5 | namespace OpenShock.Common.Errors; 6 | 7 | public static class AccountError 8 | { 9 | public static OpenShockProblem UsernameTaken => new OpenShockProblem("Account.Username.Taken", 10 | "This username is already in use", HttpStatusCode.Conflict); 11 | 12 | public static OpenShockProblem UsernameInvalid(UsernameError usernameError) => new OpenShockProblem( 13 | "Account.Username.Invalid", 14 | "This username is invalid", HttpStatusCode.BadRequest) 15 | { 16 | Extensions = new Dictionary 17 | { 18 | { "usernameError", usernameError } 19 | } 20 | }; 21 | 22 | public static OpenShockProblem PasswordChangeInvalidPassword => new OpenShockProblem( 23 | "Account.Password.OldPasswordInvalid", "The old password is invalid", HttpStatusCode.Forbidden); 24 | 25 | public static OpenShockProblem UsernameRecentlyChanged => new OpenShockProblem( 26 | "Account.Username.RecentlyChanged", "You have recently changed your username. You can only change your username every 7 days", HttpStatusCode.Forbidden); 27 | } -------------------------------------------------------------------------------- /Common/Errors/AdminError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems; 3 | 4 | namespace OpenShock.Common.Errors; 5 | 6 | public static class AdminError 7 | { 8 | public static OpenShockProblem CannotDeletePrivledgedAccount => new OpenShockProblem("User.Privileged.DeleteDenied", 9 | "You cannot delete a privileged user", HttpStatusCode.Forbidden); 10 | 11 | public static OpenShockProblem UserNotFound => new("User.NotFound", "User not found", HttpStatusCode.NotFound); 12 | 13 | public static OpenShockProblem WebhookNotFound => new OpenShockProblem("Webhook.NotFound", "Webhook not found", HttpStatusCode.NotFound); 14 | public static OpenShockProblem WebhookOnlyDiscord => new OpenShockProblem("Webhook.Unsupported", "Only discord webhooks work as of now! Make sure to use discord.com as the host, and not canary or ptb.", HttpStatusCode.BadRequest); 15 | } -------------------------------------------------------------------------------- /Common/Errors/ApiTokenError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems; 3 | 4 | namespace OpenShock.Common.Errors; 5 | 6 | public static class ApiTokenError 7 | { 8 | public static OpenShockProblem ApiTokenNotFound => new("ApiToken.NotFound", "Api token not found", HttpStatusCode.NotFound); 9 | public static OpenShockProblem ApiTokenCanOnlyDelete => new("ApiToken.CanOnlyDelete own", "You can only delete your own api token in token authentication scope", HttpStatusCode.Forbidden); 10 | } -------------------------------------------------------------------------------- /Common/Errors/AssignLcgError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems; 3 | 4 | namespace OpenShock.Common.Errors; 5 | 6 | public static class AssignLcgError 7 | { 8 | public static OpenShockProblem NoLcgNodesAvailable => new("AssignLcg.NoLcgAvailable", "No LCG node available", HttpStatusCode.ServiceUnavailable); 9 | public static OpenShockProblem BadSchemaVersion => new("AssignLcg.BadSchemaVersion", "This schema version does not exist", HttpStatusCode.BadRequest); 10 | } -------------------------------------------------------------------------------- /Common/Errors/AuthResultError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems; 3 | 4 | namespace OpenShock.Common.Errors; 5 | 6 | public static class AuthResultError 7 | { 8 | public static OpenShockProblem UnknownError => new("Authentication.UnknownError", "An unknown error occurred.", HttpStatusCode.InternalServerError); 9 | public static OpenShockProblem CookieMissingOrInvalid => new("Authentication.CookieMissingOrInvalid", "Missing or invalid authentication cookie.", HttpStatusCode.Unauthorized); 10 | public static OpenShockProblem HeaderMissingOrInvalid => new("Authentication.HeaderMissingOrInvalid", "Missing or invalid authentication header.", HttpStatusCode.Unauthorized); 11 | 12 | public static OpenShockProblem SessionInvalid => new("Authentication.SessionInvalid", "The session is invalid", HttpStatusCode.Unauthorized); 13 | public static OpenShockProblem TokenInvalid => new("Authentication.TokenInvalid", "The token is invalid", HttpStatusCode.Unauthorized); 14 | } -------------------------------------------------------------------------------- /Common/Errors/DeviceError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems; 3 | 4 | namespace OpenShock.Common.Errors; 5 | 6 | public static class DeviceError 7 | { 8 | public static OpenShockProblem DeviceNotFound => new("Device.NotFound", "Device not found", HttpStatusCode.NotFound); 9 | public static OpenShockProblem DeviceIsNotOnline => new("Device.NotOnline", "Device is not online", HttpStatusCode.NotFound); 10 | public static OpenShockProblem DeviceNotConnectedToGateway => new("Device.NotConnectedToGateway", "Device is not connected to a gateway", HttpStatusCode.PreconditionFailed, "Device is online but not connected to a LCG node, you might need to upgrade your firmware to use this feature"); 11 | 12 | public static OpenShockProblem TooManyShockers => new("Device.TooManyShockers", "Device has too many shockers", HttpStatusCode.BadRequest, "You have reached the maximum number of shockers for this device (11)"); 13 | 14 | } -------------------------------------------------------------------------------- /Common/Errors/ExceptionError.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Problems; 2 | 3 | namespace OpenShock.Common.Errors; 4 | 5 | public static class ExceptionError 6 | { 7 | public static ExceptionProblem Exception => new ExceptionProblem(); 8 | } -------------------------------------------------------------------------------- /Common/Errors/ExpressionError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems; 3 | 4 | namespace OpenShock.Common.Errors; 5 | 6 | public static class ExpressionError 7 | { 8 | public static OpenShockProblem QueryStringInvalidError(string details) => new OpenShockProblem("ExpressionError", "Query string is invalid", HttpStatusCode.BadRequest, details); 9 | public static OpenShockProblem ExpressionExceptionError(string details) => new OpenShockProblem("ExpressionError", "An error occured while processing the expression", HttpStatusCode.BadRequest, details); 10 | } -------------------------------------------------------------------------------- /Common/Errors/LoginError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems; 3 | 4 | namespace OpenShock.Common.Errors; 5 | 6 | public static class LoginError 7 | { 8 | public static OpenShockProblem InvalidCredentials => new OpenShockProblem("Login.InvalidCredentials", "Invalid credentials provided", HttpStatusCode.Unauthorized); 9 | public static OpenShockProblem InvalidDomain => new OpenShockProblem("Login.InvalidDomain", "The url you are requesting a login from is not whitelisted", HttpStatusCode.Forbidden); 10 | } -------------------------------------------------------------------------------- /Common/Errors/PairError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems; 3 | 4 | namespace OpenShock.Common.Errors; 5 | 6 | public static class PairError 7 | { 8 | public static OpenShockProblem PairCodeNotFound => new("Pair.CodeNotFound", "Pair code not found", HttpStatusCode.NotFound); 9 | } -------------------------------------------------------------------------------- /Common/Errors/PasswordResetError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems; 3 | 4 | namespace OpenShock.Common.Errors; 5 | 6 | public static class PasswordResetError 7 | { 8 | public static OpenShockProblem PasswordResetNotFound => new("PasswordReset.NotFound", "Password reset request not found", HttpStatusCode.NotFound); 9 | } -------------------------------------------------------------------------------- /Common/Errors/PublicShareError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems; 3 | 4 | namespace OpenShock.Common.Errors; 5 | 6 | public static class PublicShareError 7 | { 8 | public static OpenShockProblem PublicShareNotFound => new("ShareLink.NotFound", "Public share not found", HttpStatusCode.NotFound); 9 | 10 | // Add shocker errors 11 | public static OpenShockProblem ShockerAlreadyInPublicShare => new("ShareLink.ShockerAlreadyInShareLink", "Shocker already exists in public share", HttpStatusCode.Conflict); 12 | 13 | // Remove shocker errors 14 | public static OpenShockProblem ShockerNotInPublicShare => new("ShareLink.ShockerNotInShareLink", "Shocker does not exist in public share", HttpStatusCode.NotFound); 15 | } -------------------------------------------------------------------------------- /Common/Errors/SessionError.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Problems; 2 | 3 | namespace OpenShock.Common.Errors; 4 | 5 | public static class SessionError 6 | { 7 | public static OpenShockProblem SessionNotFound => new OpenShockProblem("Session.NotFound", 8 | "The session was not found", System.Net.HttpStatusCode.NotFound); 9 | } -------------------------------------------------------------------------------- /Common/Errors/ShareCodeError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems; 3 | 4 | namespace OpenShock.Common.Errors; 5 | 6 | public static class ShareCodeError 7 | { 8 | public static OpenShockProblem ShareCodeNotFound => new("ShareCode.NotFound", "Share code not found", HttpStatusCode.NotFound); 9 | 10 | public static OpenShockProblem CantLinkOwnShareCode => new("ShareCode.CantLinkOwnShareCode", "Cant link your own share code to your account", HttpStatusCode.BadRequest); 11 | 12 | public static OpenShockProblem ShockerAlreadyLinked => new("ShareCode.AlreadyLinked", 13 | "Shocker already linked to your account", HttpStatusCode.BadRequest); 14 | } -------------------------------------------------------------------------------- /Common/Errors/ShareError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems; 3 | using OpenShock.Common.Problems.CustomProblems; 4 | 5 | namespace OpenShock.Common.Errors; 6 | 7 | public static class ShareError 8 | { 9 | public static OpenShockProblem ShareRequestNotFound => new("Share.Request.NotFound", "Share request not found", HttpStatusCode.NotFound); 10 | public static OpenShockProblem ShareRequestCreateCannotShareWithSelf => new("Share.Request.Create.CannotShareWithSelf", "You cannot share something with yourself", HttpStatusCode.BadRequest); 11 | public static ShockersNotFoundProblem ShareCreateShockerNotFound(IReadOnlyList missingShockers) => new("Share.Request.Create.ShockerNotFound", "One or multiple of the provided shocker's were not found or do not belong to you", missingShockers, HttpStatusCode.NotFound); 12 | 13 | public static OpenShockProblem ShareGetNoShares => new("Share.Get.NoShares", "You have no shares with the specified user, or the user doesnt exist", HttpStatusCode.NotFound); 14 | } -------------------------------------------------------------------------------- /Common/Errors/ShockerControlError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems.CustomProblems; 3 | 4 | namespace OpenShock.Common.Errors; 5 | 6 | public static class ShockerControlError 7 | { 8 | public static ShockerControlProblem ShockerControlNotFound(Guid shockerId) => 9 | new("Shocker.Control.NotFound", "Shocker control not found", shockerId, HttpStatusCode.NotFound); 10 | public static ShockerControlProblem ShockerControlPaused(Guid shockerId) => 11 | new("Shocker.Control.Paused", "Shocker is paused", shockerId, HttpStatusCode.PreconditionFailed); 12 | public static ShockerControlProblem ShockerControlNoPermission(Guid shockerId) => 13 | new("Shocker.Control.NoPermission", "You don't have permission to control this shocker", shockerId, HttpStatusCode.Forbidden); 14 | } -------------------------------------------------------------------------------- /Common/Errors/ShockerError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems; 3 | 4 | namespace OpenShock.Common.Errors; 5 | 6 | public static class ShockerError 7 | { 8 | public static OpenShockProblem ShockerNotFound => new("Shocker.NotFound", "Shocker not found", HttpStatusCode.NotFound); 9 | } -------------------------------------------------------------------------------- /Common/Errors/SignupError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems; 3 | 4 | namespace OpenShock.Common.Errors; 5 | 6 | public static class SignupError 7 | { 8 | public static OpenShockProblem EmailAlreadyExists => new("Signup.EmailOrUsernameAlreadyExists", "Email or username already exists", HttpStatusCode.Conflict); 9 | } -------------------------------------------------------------------------------- /Common/Errors/TurnstileError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems; 3 | 4 | namespace OpenShock.Common.Errors; 5 | 6 | public static class TurnstileError 7 | { 8 | public static OpenShockProblem InvalidTurnstile => new("Turnstile.Invalid", "Invalid turnstile response", HttpStatusCode.Forbidden); 9 | } -------------------------------------------------------------------------------- /Common/Errors/UserError.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Problems; 3 | 4 | namespace OpenShock.Common.Errors; 5 | 6 | public static class UserError 7 | { 8 | public static OpenShockProblem UserNotFound => new("User.NotFound", "User not found", HttpStatusCode.NotFound); 9 | } -------------------------------------------------------------------------------- /Common/ExceptionHandle/RequestInfo.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable UnusedAutoPropertyAccessor.Global 2 | namespace OpenShock.Common.ExceptionHandle; 3 | 4 | public sealed class RequestInfo 5 | { 6 | public required string? Path { get; set; } 7 | public required IDictionary Query { get; set; } 8 | public required string Body { get; set; } 9 | public required string Method { get; set; } 10 | public required string TraceId { get; set; } 11 | public required IDictionary Headers { get; set; } 12 | } -------------------------------------------------------------------------------- /Common/Extensions/AssemblyExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using System.Reflection; 3 | 4 | namespace OpenShock.Common.Extensions; 5 | 6 | public static class AssemblyExtensions 7 | { 8 | public static IEnumerable GetAllControllers(this Assembly assembly) 9 | { 10 | return assembly 11 | .GetTypes() 12 | .Where(type => type.IsClass && typeof(ControllerBase).IsAssignableFrom(type)); 13 | } 14 | 15 | public static IEnumerable GetAllControllerEndpointAttributes(this Assembly assembly) where TAttribute : Attribute 16 | { 17 | return GetAllControllers(assembly).SelectMany(type => type.GetCustomAttributes(true).Concat(type.GetMethods(BindingFlags.Instance).SelectMany(m => m.GetCustomAttributes()))); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Common/Extensions/ConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using OpenShock.Common.Options; 3 | 4 | namespace OpenShock.Common.Extensions; 5 | 6 | public static class ConfigurationExtensions 7 | { 8 | public static WebApplicationBuilder RegisterCommonOpenShockOptions(this WebApplicationBuilder builder) 9 | { 10 | if (builder.Environment.IsDevelopment()) 11 | { 12 | Console.WriteLine(builder.Configuration.GetDebugView()); 13 | } 14 | 15 | builder.Services.Configure(builder.Configuration.GetRequiredSection(DatabaseOptions.SectionName)); 16 | builder.Services.AddSingleton, DatabaseOptionsValidator>(); 17 | 18 | builder.Services.Configure(builder.Configuration.GetRequiredSection(RedisOptions.SectionName)); 19 | builder.Services.AddSingleton, RedisOptionsValidator>(); 20 | 21 | builder.Services.Configure(builder.Configuration.GetSection(MetricsOptions.SectionName)); 22 | builder.Services.AddSingleton, MetricsOptionsValidator>(); 23 | 24 | return builder; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Common/Extensions/DictionaryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OpenShock.Common.Extensions; 4 | 5 | public static class DictionaryExtensions 6 | { 7 | public static TValue GetValueOrAddDefault(this Dictionary dictionary, TKey key, 8 | TValue defaultValue) where TKey : notnull 9 | { 10 | ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out var exists); 11 | if (exists) 12 | { 13 | return value!; 14 | } 15 | 16 | value = defaultValue; 17 | return value; 18 | } 19 | } -------------------------------------------------------------------------------- /Common/Extensions/ISignalRServerBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.SignalR; 2 | using Microsoft.AspNetCore.SignalR.StackExchangeRedis; 3 | 4 | namespace OpenShock.Common.Extensions; 5 | 6 | public static class ISignalRServerBuilderExtensions 7 | { 8 | /// 9 | /// Adds scale-out to a , using a shared Redis server. 10 | /// 11 | /// The . 12 | /// A callback to configure the Redis options. 13 | /// The same instance of the for chaining. 14 | public static ISignalRServerBuilder AddOpenShockStackExchangeRedis(this ISignalRServerBuilder signalrBuilder, Action configure) 15 | { 16 | signalrBuilder.Services.Configure(configure); 17 | signalrBuilder.Services.AddSingleton(typeof(HubLifetimeManager<>), typeof(OpenShockRedisHubLifetimeManager<>)); 18 | return signalrBuilder; 19 | } 20 | } -------------------------------------------------------------------------------- /Common/Extensions/PropertyBuilderExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | 4 | namespace OpenShock.Common.Extensions; 5 | 6 | public static class PropertyBuilderExtension 7 | { 8 | public static PropertyBuilder VarCharWithLength(this PropertyBuilder propertyBuilder, int length) 9 | { 10 | return propertyBuilder.HasColumnType($"character varying({length})").HasMaxLength(length); 11 | } 12 | } -------------------------------------------------------------------------------- /Common/Extensions/SemVersionExtensions.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Serialization.Types; 2 | using Semver; 3 | 4 | namespace OpenShock.Common.Extensions; 5 | 6 | public static class SemVersionExtensions 7 | { 8 | public static SemVer ToSemVer(this SemVersion version) => new() 9 | { 10 | Major = (ushort)version.Major, 11 | Minor = (ushort)version.Minor, 12 | Patch = (ushort)version.Patch, 13 | Prerelease = version.Prerelease, 14 | Build = version.Metadata 15 | }; 16 | 17 | public static SemVersion ToSemVersion(this SemVer version) => 18 | SemVersion.ParsedFrom(version.Major, version.Minor, version.Patch, version.Prerelease ?? string.Empty, version.Build ?? string.Empty); 19 | } -------------------------------------------------------------------------------- /Common/Extensions/SemaphoreSlimExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Extensions; 2 | 3 | public static class SemaphoreSlimExtensions 4 | { 5 | public static async Task LockAsyncScoped(this SemaphoreSlim semaphore) 6 | { 7 | await semaphore.WaitAsync(); 8 | return new Releaser(semaphore); 9 | } 10 | public static async Task LockAsyncScoped(this SemaphoreSlim semaphore, CancellationToken cancellationToken) 11 | { 12 | await semaphore.WaitAsync(cancellationToken); 13 | return new Releaser(semaphore); 14 | } 15 | 16 | private sealed class Releaser(SemaphoreSlim semaphore) : IDisposable 17 | { 18 | public void Dispose() => semaphore.Release(); 19 | } 20 | } -------------------------------------------------------------------------------- /Common/Extensions/UserExtensions.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Models; 2 | using OpenShock.Common.OpenShockDb; 3 | using OpenShock.Common.Utils; 4 | 5 | namespace OpenShock.Common.Extensions; 6 | 7 | public static class UserExtensions 8 | { 9 | public static Uri GetImageUrl(this User user) => GravatarUtils.GetUserImageUrl(user.Email); 10 | 11 | public static bool IsUser(this User user, Guid otherUserId) 12 | { 13 | return user.Id == otherUserId; 14 | } 15 | 16 | public static bool IsUser(this User user, User otherUser) 17 | { 18 | return user == otherUser || user.Id == otherUser.Id; 19 | } 20 | 21 | public static bool IsRole(this User user, RoleType role) 22 | { 23 | return user.Roles.Contains(role); 24 | } 25 | 26 | public static bool IsUserOrRole(this User user, Guid otherUserId, RoleType role) 27 | { 28 | return user.IsUser(otherUserId) || user.IsRole(role); 29 | } 30 | 31 | public static bool IsUserOrRole(this User user, User otherUser, RoleType role) 32 | { 33 | return user.IsUser(otherUser) || user.IsRole(role); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Common/Geo/Alpha2CountryCodeAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace OpenShock.Common.Geo; 4 | 5 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] 6 | public sealed class Alpha2CountryCodeAttribute : ValidationAttribute 7 | { 8 | /// 9 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) 10 | { 11 | if (value is null) 12 | return new ValidationResult("Value is null"); 13 | 14 | if (value is not string asString) 15 | return new ValidationResult("Input type must be string"); 16 | 17 | if (asString.Length != 2) 18 | return new ValidationResult("Input string must be exactly 2 characters long"); 19 | 20 | if (asString[0] is not > 'A' and < 'Z' || asString[1] is not > 'A' and < 'Z') 21 | return new ValidationResult("Characters must be uppercase"); 22 | 23 | if (!Alpha2CountryCode.TryParse(asString, out var countryCode)) 24 | return new ValidationResult($"Failed to create {nameof(Alpha2CountryCode)}"); 25 | 26 | if (!CountryInfo.CodeDictionary.ContainsKey(countryCode)) 27 | return new ValidationResult("Country does not exist in mapping"); 28 | 29 | return ValidationResult.Success; 30 | } 31 | } -------------------------------------------------------------------------------- /Common/Hubs/IPublicShareHub.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Hubs; 2 | 3 | public interface IPublicShareHub 4 | { 5 | Task Welcome(PublicShareHub.AuthType authType); 6 | Task Updated(); 7 | } -------------------------------------------------------------------------------- /Common/Hubs/IUserHub.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Models; 2 | using OpenShock.Common.Models.WebSocket; 3 | using OpenShock.Common.Models.WebSocket.User; 4 | using OpenShock.Serialization.Types; 5 | using Semver; 6 | 7 | namespace OpenShock.Common.Hubs; 8 | 9 | public interface IUserHub 10 | { 11 | Task Welcome(string connectionId); 12 | Task DeviceStatus(IList deviceOnlineStates); 13 | Task Log(ControlLogSender sender, IEnumerable logs); 14 | Task DeviceUpdate(Guid deviceId, DeviceUpdateType type); 15 | 16 | // OTA 17 | Task OtaInstallStarted(Guid deviceId, int updateId, SemVersion version); 18 | Task OtaInstallProgress(Guid deviceId, int updateId, OtaUpdateProgressTask task, float progress); 19 | Task OtaInstallFailed(Guid deviceId, int updateId, bool fatal, string message); 20 | Task OtaRollback(Guid deviceId, int updateId); 21 | Task OtaInstallSucceeded(Guid deviceId, int updateId); 22 | } -------------------------------------------------------------------------------- /Common/JsonSerialization/CustomJsonStringEnumConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace OpenShock.Common.JsonSerialization; 5 | 6 | public sealed class CustomJsonStringEnumConverter : JsonConverterFactory 7 | { 8 | private static readonly JsonStringEnumConverter JsonStringEnumConverter = new(); 9 | 10 | public override bool CanConvert(Type typeToConvert) => 11 | !typeToConvert.IsDefined(typeof(FlagsAttribute), false) && 12 | JsonStringEnumConverter.CanConvert(typeToConvert); 13 | 14 | public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => 15 | JsonStringEnumConverter.CreateConverter(typeToConvert, options); 16 | } -------------------------------------------------------------------------------- /Common/JsonSerialization/PermissionTypeConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using OpenShock.Common.Models; 4 | 5 | namespace OpenShock.Common.JsonSerialization; 6 | 7 | public sealed class PermissionTypeConverter : JsonConverter 8 | { 9 | public override PermissionType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 10 | { 11 | return PermissionTypeBindings.NameToPermissionType[reader.GetString()!].PermissionType; 12 | } 13 | 14 | public override void Write(Utf8JsonWriter writer, PermissionType value, JsonSerializerOptions options) 15 | { 16 | writer.WriteStringValue(PermissionTypeBindings.PermissionTypeToName[value].Name); 17 | } 18 | } -------------------------------------------------------------------------------- /Common/JsonSerialization/SemVersionJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using Semver; 4 | 5 | namespace OpenShock.Common.JsonSerialization; 6 | 7 | public sealed class SemVersionJsonConverter : JsonConverter 8 | { 9 | public override SemVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 10 | { 11 | if (reader.TokenType == JsonTokenType.Null) 12 | { 13 | throw new JsonException("SemVer cannot be null"); 14 | } 15 | 16 | if (reader.TokenType != JsonTokenType.String) 17 | { 18 | throw new JsonException("SemVer must be a string"); 19 | } 20 | 21 | string? str = reader.GetString(); 22 | if (string.IsNullOrEmpty(str)) 23 | { 24 | throw new JsonException("SemVer cannot be empty"); 25 | } 26 | 27 | if (!SemVersion.TryParse(str, SemVersionStyles.Strict, out SemVersion? version)) 28 | { 29 | throw new JsonException("String is not a valid SemVer"); 30 | } 31 | 32 | return version; 33 | } 34 | 35 | public override void Write(Utf8JsonWriter writer, SemVersion value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString()); 36 | } -------------------------------------------------------------------------------- /Common/JsonSerialization/SlSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace OpenShock.Common.JsonSerialization; 4 | 5 | public static class SlSerializer 6 | { 7 | private static readonly JsonSerializerOptions DefaultSerializerSettings = new() 8 | { 9 | PropertyNameCaseInsensitive = true 10 | }; 11 | 12 | static SlSerializer() 13 | { 14 | DefaultSerializerSettings.Converters.Add(new CustomJsonStringEnumConverter()); 15 | } 16 | 17 | private static readonly JsonSerializerOptions NewDefaultSerializerSettings = new() 18 | { 19 | PropertyNameCaseInsensitive = true 20 | }; 21 | 22 | public static T? Deserialize(this string json) => JsonSerializer.Deserialize(json, DefaultSerializerSettings); 23 | public static ValueTask DeserializeAsync(this Stream stream) => JsonSerializer.DeserializeAsync(stream, DefaultSerializerSettings); 24 | public static T? Deserialize(this ReadOnlySpan data) => JsonSerializer.Deserialize(data, DefaultSerializerSettings); 25 | 26 | 27 | public static TValue? NewSlDeserialize(this JsonDocument? document) 28 | { 29 | return document is null ? default : document.Deserialize(NewDefaultSerializerSettings); 30 | } 31 | } -------------------------------------------------------------------------------- /Common/JsonSerialization/UnixMillisecondsDateTimeOffsetConverter.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.JsonSerialization; 2 | 3 | using System; 4 | using System.Text.Json; 5 | using System.Text.Json.Serialization; 6 | 7 | public class UnixMillisecondsDateTimeOffsetConverter : JsonConverter 8 | { 9 | // Serialize DateTimeOffset to Unix time in milliseconds 10 | public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) 11 | { 12 | var unixTimeMilliseconds = value.ToUnixTimeMilliseconds(); 13 | writer.WriteNumberValue(unixTimeMilliseconds); 14 | } 15 | 16 | // Deserialize Unix time in milliseconds to DateTimeOffset 17 | public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 18 | { 19 | if (reader.TokenType != JsonTokenType.Number) 20 | { 21 | throw new JsonException("Expected number token for Unix time in milliseconds."); 22 | } 23 | 24 | var unixTimeMilliseconds = reader.GetInt64(); 25 | return DateTimeOffset.FromUnixTimeMilliseconds(unixTimeMilliseconds); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Common/Migrations/20241122214013_Fix Petrainer998DR RFIDs.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace OpenShock.Common.Migrations 6 | { 7 | /// 8 | public partial class FixPetrainer998DRRFIDs : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.Sql( 14 | $""" 15 | UPDATE shockers 16 | SET 17 | rf_id = ((rf_id)::bit(32) << 1)::integer 18 | WHERE 19 | model = 'petrainer998DR' 20 | """, 21 | true 22 | ); 23 | } 24 | 25 | /// 26 | protected override void Down(MigrationBuilder migrationBuilder) 27 | { 28 | migrationBuilder.Sql( 29 | $""" 30 | UPDATE shockers 31 | SET 32 | rf_id = ((rf_id)::bit(32) >> 1)::integer 33 | WHERE 34 | model = 'petrainer998DR' 35 | """, 36 | true 37 | ); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Common/Migrations/20241123181710_Hash API tokens.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace OpenShock.Common.Migrations 6 | { 7 | /// 8 | public partial class HashAPItokens : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.RenameColumn( 14 | name: "token", 15 | table: "api_tokens", 16 | newName: "token_hash"); 17 | 18 | migrationBuilder.RenameIndex( 19 | name: "IX_api_tokens_token", 20 | table: "api_tokens", 21 | newName: "IX_api_tokens_token_hash"); 22 | 23 | migrationBuilder.Sql( 24 | $""" 25 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 26 | UPDATE api_tokens SET token_hash = encode(digest(token_hash, 'sha256'), 'hex'); 27 | """ 28 | ); 29 | } 30 | 31 | /// 32 | protected override void Down(MigrationBuilder migrationBuilder) 33 | { 34 | throw new InvalidOperationException("This migration cannot be reverted because token hashing is irreversible."); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Common/Migrations/20241219115917_FixAdminUsersView.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace OpenShock.Common.Migrations 6 | { 7 | /// 8 | public partial class FixAdminUsersView : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.Sql( 14 | """ 15 | DO $$ 16 | BEGIN 17 | IF EXISTS ( 18 | SELECT 1 19 | FROM information_schema.columns 20 | WHERE table_name = 'admin_users_view' 21 | AND column_name = 'email_actived' 22 | ) THEN 23 | ALTER VIEW admin_users_view RENAME COLUMN email_actived TO email_activated; 24 | END IF; 25 | END $$; 26 | """ 27 | ); 28 | } 29 | 30 | /// 31 | protected override void Down(MigrationBuilder migrationBuilder) 32 | { 33 | migrationBuilder.Sql("ALTER VIEW admin_users_view RENAME COLUMN email_activated TO email_actived"); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Common/Models/BasicShockerInfo.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models; 2 | 3 | public sealed class BasicShockerInfo 4 | { 5 | public required Guid Id { get; set; } 6 | public required string Name { get; set; } 7 | } -------------------------------------------------------------------------------- /Common/Models/BasicUserInfo.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models; 2 | 3 | public sealed class BasicUserInfo 4 | { 5 | public required Guid Id { get; set; } 6 | public required string Name { get; set; } 7 | public required Uri Image { get; set; } 8 | } -------------------------------------------------------------------------------- /Common/Models/ControlLogAdditionalItem.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Authentication; 2 | 3 | namespace OpenShock.Common.Models; 4 | 5 | public static class ControlLogAdditionalItem 6 | { 7 | public const string ApiTokenId = OpenShockAuthClaims.ApiTokenId; 8 | public const string PublicShareId = "shareLinkId"; 9 | } -------------------------------------------------------------------------------- /Common/Models/ControlLogSender.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models; 2 | 3 | public class ControlLogSenderLight 4 | { 5 | public required Guid Id { get; set; } 6 | public required string Name { get; set; } 7 | public required Uri Image { get; set; } 8 | public required string? CustomName { get; set; } 9 | } 10 | 11 | public class ControlLogSender : ControlLogSenderLight 12 | { 13 | public required string ConnectionId { get; set; } 14 | public required IDictionary AdditionalItems { get; set; } 15 | } -------------------------------------------------------------------------------- /Common/Models/ControlRequest.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models; 2 | 3 | public sealed class ControlRequest 4 | { 5 | public required IReadOnlyList Shocks { get; set; } 6 | public string? CustomName { get; set; } = null; 7 | } -------------------------------------------------------------------------------- /Common/Models/ControlType.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models; 2 | 3 | public enum ControlType 4 | { 5 | Stop = 0, 6 | Shock = 1, 7 | Vibrate = 2, 8 | Sound = 3 9 | } -------------------------------------------------------------------------------- /Common/Models/DeviceUpdateType.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models; 2 | 3 | public enum DeviceUpdateType 4 | { 5 | Created, // Whenever a new device is created 6 | Updated, // Whenever name or something else directly related to the device is updated 7 | ShockerUpdated, // Whenever a shocker is updated, name or limits for a person 8 | Deleted // Whenever a device is deleted 9 | 10 | } -------------------------------------------------------------------------------- /Common/Models/Error.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models; 2 | 3 | public sealed class Error where TError : Enum 4 | { 5 | public required TError Type { get; set; } 6 | } -------------------------------------------------------------------------------- /Common/Models/FirmwareVersion.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models; 2 | 3 | public sealed class FirmwareVersion 4 | { 5 | public required Version Version { get; set; } 6 | public required Uri DownloadUri { get; set; } 7 | } -------------------------------------------------------------------------------- /Common/Models/LcgResponse.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models; 2 | 3 | public sealed class LcgResponse 4 | { 5 | public required string Gateway { get; set; } 6 | public required string Country { get; set; } 7 | } -------------------------------------------------------------------------------- /Common/Models/LegacyDataResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace OpenShock.Common.Models; 4 | 5 | public sealed class LegacyDataResponse 6 | { 7 | [SetsRequiredMembers] 8 | public LegacyDataResponse(T data, string message = "") 9 | { 10 | Message = message; 11 | Data = data; 12 | } 13 | 14 | public required string Message { get; set; } 15 | public required T Data { get; set; } 16 | } -------------------------------------------------------------------------------- /Common/Models/LegacyEmptyResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace OpenShock.Common.Models; 4 | 5 | public sealed class LegacyEmptyResponse 6 | { 7 | [SetsRequiredMembers] 8 | public LegacyEmptyResponse(string message, object? data = null) 9 | { 10 | Message = message; 11 | Data = data; 12 | } 13 | 14 | public required string Message { get; set; } 15 | public object? Data { get; set; } 16 | } -------------------------------------------------------------------------------- /Common/Models/LiveControlPacketSender.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models; 2 | 3 | public sealed class LiveControlPacketSender 4 | { 5 | public required Guid Id { get; set; } 6 | public required string Name { get; set; } 7 | public required Uri Image { get; set; } 8 | public required string? CustomName { get; set; } 9 | } -------------------------------------------------------------------------------- /Common/Models/OtaUpdateStatus.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models; 2 | 3 | public enum OtaUpdateStatus 4 | { 5 | Started, 6 | Running, 7 | Finished, 8 | Error, 9 | Timeout 10 | } -------------------------------------------------------------------------------- /Common/Models/Paginated.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models; 2 | 3 | public sealed class Paginated 4 | { 5 | public required int Offset { get; set; } 6 | public required int Limit { get; set; } 7 | public required long Total { get; set; } 8 | public required IReadOnlyList Data { get; set; } 9 | } -------------------------------------------------------------------------------- /Common/Models/PasswordHashingAlgorithm.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable InconsistentNaming 2 | namespace OpenShock.Common.Models; 3 | 4 | public enum PasswordHashingAlgorithm 5 | { 6 | Unknown = -1, 7 | BCrypt = 0, 8 | PBKDF2 = 1, 9 | }; -------------------------------------------------------------------------------- /Common/Models/PauseReason.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models; 2 | 3 | [Flags] 4 | public enum PauseReason 5 | { 6 | None = 0, // 0 7 | Shocker = 1 << 0, // 1 8 | Share = 1 << 1, // 2 9 | ShareLink = 1 << 2 // 4 10 | } -------------------------------------------------------------------------------- /Common/Models/RoleType.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models; 2 | 3 | public enum RoleType 4 | { 5 | Support, 6 | Staff, 7 | Admin, 8 | System 9 | } -------------------------------------------------------------------------------- /Common/Models/Services/Ota/OtaItem.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using OpenShock.Common.JsonSerialization; 3 | using Semver; 4 | 5 | namespace OpenShock.Common.Models.Services.Ota; 6 | 7 | public sealed class OtaItem 8 | { 9 | public required int Id { get; init; } 10 | public required DateTimeOffset StartedAt { get; init; } 11 | public required OtaUpdateStatus Status { get; init; } 12 | [JsonConverter(typeof(SemVersionJsonConverter))] 13 | public required SemVersion Version { get; init; } 14 | public required string? Message { get; init; } 15 | } -------------------------------------------------------------------------------- /Common/Models/SharePermsAndLimits.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models; 2 | 3 | public class SharePermsAndLimits 4 | { 5 | public required bool Sound { get; set; } 6 | public required bool Vibrate { get; set; } 7 | public required bool Shock { get; set; } 8 | public required ushort? Duration { get; set; } 9 | public required byte? Intensity { get; set; } 10 | } 11 | 12 | public sealed class SharePermsAndLimitsLive : SharePermsAndLimits 13 | { 14 | public required bool Live { get; set; } 15 | } -------------------------------------------------------------------------------- /Common/Models/ShockerModelType.cs: -------------------------------------------------------------------------------- 1 | using NpgsqlTypes; 2 | 3 | namespace OpenShock.Common.Models; 4 | 5 | public enum ShockerModelType 6 | { 7 | [PgName("caiXianlin")] CaiXianlin = 0, 8 | [PgName("petTrainer")] PetTrainer = 1, // Misspelled, should be "petrainer", 9 | [PgName("petrainer998DR")] Petrainer998DR = 2, 10 | } -------------------------------------------------------------------------------- /Common/Models/WebSocket/BaseRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace OpenShock.Common.Models.WebSocket; 4 | 5 | public sealed class BaseRequest 6 | { 7 | public required T RequestType { get; set; } 8 | public JsonDocument? Data { get; set; } 9 | } -------------------------------------------------------------------------------- /Common/Models/WebSocket/ControlResponse.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models.WebSocket; 2 | 3 | public sealed class ControlResponse 4 | { 5 | public required ushort Id { get; set; } 6 | public required ControlType Type { get; set; } 7 | public required byte Intensity { get; set; } 8 | public required uint Duration { get; set; } 9 | public required ShockerModelType Model { get; set; } 10 | } -------------------------------------------------------------------------------- /Common/Models/WebSocket/Device/RequestType.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models.WebSocket.Device; 2 | 3 | public enum RequestType 4 | { 5 | KeepAlive = 0 6 | } -------------------------------------------------------------------------------- /Common/Models/WebSocket/Device/ResponseType.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models.WebSocket.Device; 2 | 3 | public enum ResponseType 4 | { 5 | Control = 0, 6 | CaptiveControl = 1 7 | } -------------------------------------------------------------------------------- /Common/Models/WebSocket/DeviceOnlineState.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using OpenShock.Common.JsonSerialization; 3 | using Semver; 4 | 5 | namespace OpenShock.Common.Models.WebSocket; 6 | 7 | public sealed class DeviceOnlineState 8 | { 9 | public required Guid Device { get; set; } 10 | public required bool Online { get; set; } 11 | [JsonConverter(typeof(SemVersionJsonConverter))] 12 | public required SemVersion? FirmwareVersion { get; set; } 13 | } -------------------------------------------------------------------------------- /Common/Models/WebSocket/LCG/ClientLiveFrame.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace OpenShock.Common.Models.WebSocket.LCG; 4 | 5 | public sealed class ClientLiveFrame 6 | { 7 | public required Guid Shocker { get; set; } 8 | public required byte Intensity { get; set; } 9 | [JsonConverter(typeof(JsonStringEnumConverter))] 10 | public required ControlType Type { get; set; } 11 | } -------------------------------------------------------------------------------- /Common/Models/WebSocket/LCG/LatencyAnnounceData.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models.WebSocket.LCG; 2 | 3 | public sealed class LatencyAnnounceData 4 | { 5 | public required ushort DeviceLatency { get; set; } 6 | public required ushort OwnLatency { get; set; } 7 | } -------------------------------------------------------------------------------- /Common/Models/WebSocket/LCG/LcgLiveControlPing.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models.WebSocket.LCG; 2 | 3 | public sealed class LcgLiveControlPing 4 | { 5 | public required long Timestamp { get; set; } // Was used for latency calculation, latency calculation is now done serverside 6 | } -------------------------------------------------------------------------------- /Common/Models/WebSocket/LCG/LiveRequestType.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models.WebSocket.LCG; 2 | 3 | public enum LiveRequestType 4 | { 5 | Frame = 0, 6 | BulkFrame = 1, 7 | 8 | Pong = 1000 9 | } -------------------------------------------------------------------------------- /Common/Models/WebSocket/LCG/LiveResponseType.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace OpenShock.Common.Models.WebSocket.LCG; 4 | 5 | [JsonConverter(typeof(JsonStringEnumConverter))] 6 | public enum LiveResponseType 7 | { 8 | Frame = 0, 9 | 10 | // TPS for the client to send 11 | // ReSharper disable once InconsistentNaming 12 | TPS = 50, 13 | 14 | DeviceNotConnected = 100, 15 | DeviceConnected = 101, 16 | ShockerNotFound = 150, 17 | ShockerMissingLivePermission = 151, 18 | ShockerMissingPermission = 152, 19 | ShockerPaused = 153, 20 | ShockerExclusive = 154, 21 | 22 | InvalidData = 200, 23 | RequestTypeNotFound = 201, 24 | 25 | Ping = 1000, 26 | LatencyAnnounce = 1001 27 | } -------------------------------------------------------------------------------- /Common/Models/WebSocket/LCG/TpsData.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models.WebSocket.LCG; 2 | 3 | public sealed class TpsData 4 | { 5 | public required byte Client { get; set; } 6 | } -------------------------------------------------------------------------------- /Common/Models/WebSocket/LiveControlResponse.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable UnusedAutoPropertyAccessor.Global 2 | 3 | namespace OpenShock.Common.Models.WebSocket; 4 | 5 | public sealed class LiveControlResponse where T : Enum 6 | { 7 | public required T ResponseType { get; set; } 8 | public object? Data { get; set; } 9 | 10 | } -------------------------------------------------------------------------------- /Common/Models/WebSocket/MessageTooLongException.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models.WebSocket; 2 | 3 | /// 4 | /// Indicates that the websocket message received or to be sent is larger than the defined limit. 5 | /// 6 | public sealed class MessageTooLongException : Exception 7 | { 8 | /// 9 | public MessageTooLongException() 10 | { 11 | } 12 | 13 | /// 14 | public MessageTooLongException(string message) : base(message) 15 | { 16 | } 17 | } -------------------------------------------------------------------------------- /Common/Models/WebSocket/User/CaptiveControl.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models.WebSocket.User; 2 | 3 | public sealed class CaptiveControl 4 | { 5 | public required Guid DeviceId { get; set; } 6 | public required bool Enabled { get; set; } 7 | } -------------------------------------------------------------------------------- /Common/Models/WebSocket/User/Control.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using OpenShock.Common.Constants; 3 | 4 | namespace OpenShock.Common.Models.WebSocket.User; 5 | 6 | // ReSharper disable once ClassNeverInstantiated.Global 7 | public sealed class Control 8 | { 9 | public required Guid Id { get; set; } 10 | 11 | [EnumDataType(typeof(ControlType))] 12 | public required ControlType Type { get; set; } 13 | 14 | [Range(HardLimits.MinControlIntensity, HardLimits.MaxControlIntensity)] 15 | public required byte Intensity { get; set; } 16 | 17 | [Range(HardLimits.MinControlDuration, HardLimits.MaxControlDuration)] 18 | public required ushort Duration { get; set; } 19 | 20 | /// 21 | /// If true, overrides livecontrol 22 | /// 23 | public bool Exclusive { get; set; } = false; 24 | } -------------------------------------------------------------------------------- /Common/Models/WebSocket/User/ControlLog.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using OpenShock.Common.Constants; 3 | 4 | namespace OpenShock.Common.Models.WebSocket.User; 5 | 6 | // ReSharper disable once ClassNeverInstantiated.Global 7 | public sealed class ControlLog 8 | { 9 | public required BasicShockerInfo Shocker { get; set; } 10 | 11 | public required ControlType Type { get; set; } 12 | 13 | [Range(HardLimits.MinControlIntensity, HardLimits.MaxControlIntensity)] 14 | public required byte Intensity { get; set; } 15 | 16 | [Range(HardLimits.MinControlDuration, HardLimits.MaxControlDuration)] 17 | public required uint Duration { get; set; } 18 | 19 | public required DateTime ExecutedAt { get; set; } 20 | } -------------------------------------------------------------------------------- /Common/Models/WebSocket/User/RequestType.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models.WebSocket.User; 2 | 3 | public enum RequestType 4 | { 5 | Control = 0 6 | } -------------------------------------------------------------------------------- /Common/Models/WebSocket/User/ResponseType.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models.WebSocket.User; 2 | 3 | public enum ResponseType 4 | { 5 | } -------------------------------------------------------------------------------- /Common/Models/WebhookDto.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Models; 2 | 3 | public sealed class WebhookDto 4 | { 5 | public required Guid Id { get; set; } 6 | public required string Name { get; set; } 7 | public required string Url { get; set; } 8 | public required DateTimeOffset CreatedAt { get; set; } 9 | } -------------------------------------------------------------------------------- /Common/OpenShockControllerBase.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Mime; 2 | using Microsoft.AspNetCore.Mvc; 3 | using OpenShock.Common.Models; 4 | using OpenShock.Common.Problems; 5 | 6 | namespace OpenShock.Common; 7 | 8 | [Consumes(MediaTypeNames.Application.Json)] 9 | public class OpenShockControllerBase : ControllerBase 10 | { 11 | [NonAction] 12 | public ObjectResult Problem(OpenShockProblem problem) => problem.ToObjectResult(HttpContext); 13 | 14 | [NonAction] 15 | public OkObjectResult LegacyDataOk(T data) 16 | { 17 | return Ok(new LegacyDataResponse(data)); 18 | } 19 | 20 | [NonAction] 21 | public CreatedResult LegacyDataCreated(string? uri, T data) 22 | { 23 | return Created(uri, new LegacyDataResponse(data)); 24 | } 25 | 26 | [NonAction] 27 | public OkObjectResult LegacyEmptyOk(string message = "") 28 | { 29 | return Ok(new LegacyEmptyResponse(message)); 30 | } 31 | } -------------------------------------------------------------------------------- /Common/OpenShockDb/ApiToken.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Models; 3 | 4 | namespace OpenShock.Common.OpenShockDb; 5 | 6 | public sealed class ApiToken 7 | { 8 | public required Guid Id { get; set; } 9 | 10 | public required Guid UserId { get; set; } 11 | 12 | public required string Name { get; set; } 13 | 14 | public required string TokenHash { get; set; } 15 | 16 | public required IPAddress CreatedByIp { get; set; } 17 | 18 | public required List Permissions { get; set; } 19 | 20 | public DateTime? ValidUntil { get; set; } 21 | 22 | public DateTime CreatedAt { get; set; } 23 | 24 | public DateTime LastUsed { get; set; } 25 | 26 | // Navigations 27 | public User User { get; set; } = null!; 28 | } 29 | -------------------------------------------------------------------------------- /Common/OpenShockDb/ApiTokenReport.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace OpenShock.Common.OpenShockDb; 4 | 5 | public class ApiTokenReport 6 | { 7 | public required Guid Id { get; set; } 8 | 9 | public required int SubmittedCount { get; set; } 10 | 11 | public required int AffectedCount { get; set; } 12 | 13 | public required Guid UserId { get; set; } 14 | 15 | public required IPAddress IpAddress { get; set; } 16 | 17 | public required string? IpCountry { get; set; } 18 | 19 | public DateTime CreatedAt { get; set; } 20 | 21 | public User ReportedByUser { get; set; } = null!; 22 | } 23 | -------------------------------------------------------------------------------- /Common/OpenShockDb/Device.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.OpenShockDb; 2 | 3 | public sealed class Device 4 | { 5 | public required Guid Id { get; set; } 6 | 7 | public required Guid OwnerId { get; set; } 8 | 9 | public required string Name { get; set; } 10 | 11 | public required string Token { get; set; } 12 | 13 | public DateTime CreatedAt { get; set; } 14 | 15 | // Navigations 16 | public User Owner { get; set; } = null!; 17 | public ICollection Shockers { get; } = []; 18 | public ICollection OtaUpdates { get; } = []; 19 | } 20 | -------------------------------------------------------------------------------- /Common/OpenShockDb/DeviceOtaUpdate.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Models; 2 | 3 | namespace OpenShock.Common.OpenShockDb; 4 | 5 | public sealed class DeviceOtaUpdate 6 | { 7 | public required Guid DeviceId { get; set; } 8 | 9 | public required int UpdateId { get; set; } 10 | 11 | public required OtaUpdateStatus Status { get; set; } 12 | 13 | public required string Version { get; set; } 14 | 15 | public string? Message { get; set; } 16 | 17 | public DateTime CreatedAt { get; set; } 18 | 19 | // Navigations 20 | public Device Device { get; set; } = null!; 21 | } 22 | -------------------------------------------------------------------------------- /Common/OpenShockDb/DiscordWebhook.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.OpenShockDb; 2 | 3 | public sealed class DiscordWebhook 4 | { 5 | public required Guid Id { get; set; } 6 | 7 | public required string Name { get; set; } 8 | 9 | public required long WebhookId { get; set; } 10 | 11 | public required string WebhookToken { get; set; } 12 | 13 | public DateTime CreatedAt { get; set; } 14 | } -------------------------------------------------------------------------------- /Common/OpenShockDb/PublicShare.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.OpenShockDb; 2 | 3 | public sealed class PublicShare 4 | { 5 | public required Guid Id { get; set; } 6 | 7 | public required Guid OwnerId { get; set; } 8 | 9 | public required string Name { get; set; } 10 | 11 | public DateTime? ExpiresAt { get; set; } 12 | 13 | public DateTime CreatedAt { get; set; } 14 | 15 | // Navigations 16 | public User Owner { get; set; } = null!; 17 | public ICollection ShockerMappings { get; } = []; 18 | } 19 | -------------------------------------------------------------------------------- /Common/OpenShockDb/PublicShareShocker.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.OpenShockDb; 2 | 3 | public sealed class PublicShareShocker : SafetySettings 4 | { 5 | public required Guid PublicShareId { get; set; } 6 | 7 | public required Guid ShockerId { get; set; } 8 | 9 | public int? Cooldown { get; set; } 10 | 11 | // Navigations 12 | public PublicShare PublicShare { get; set; } = null!; 13 | public Shocker Shocker { get; set; } = null!; 14 | } 15 | -------------------------------------------------------------------------------- /Common/OpenShockDb/SafetySettings.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.OpenShockDb; 2 | 3 | public class SafetySettings 4 | { 5 | public required bool AllowShock { get; set; } 6 | 7 | public required bool AllowVibrate { get; set; } 8 | 9 | public required bool AllowSound { get; set; } 10 | 11 | public required bool AllowLiveControl { get; set; } 12 | 13 | public byte? MaxIntensity { get; set; } 14 | 15 | public ushort? MaxDuration { get; set; } 16 | 17 | public required bool IsPaused { get; set; } 18 | } -------------------------------------------------------------------------------- /Common/OpenShockDb/Shocker.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Models; 2 | 3 | namespace OpenShock.Common.OpenShockDb; 4 | 5 | public sealed class Shocker 6 | { 7 | public required Guid Id { get; set; } 8 | 9 | public required string Name { get; set; } 10 | 11 | public required ShockerModelType Model { get; set; } 12 | 13 | public required ushort RfId { get; set; } 14 | 15 | public required Guid DeviceId { get; set; } 16 | 17 | public bool IsPaused { get; set; } 18 | 19 | public DateTime CreatedAt { get; set; } 20 | 21 | // Navigations 22 | public Device Device { get; set; } = null!; 23 | public ICollection UserShareInviteShockerMappings { get; } = []; 24 | public ICollection ShockerControlLogs { get; } = []; 25 | public ICollection ShockerShareCodes { get; } = []; 26 | public ICollection UserShares { get; } = []; 27 | public ICollection PublicShareMappings { get; } = []; 28 | } 29 | -------------------------------------------------------------------------------- /Common/OpenShockDb/ShockerControlLog.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Models; 2 | 3 | namespace OpenShock.Common.OpenShockDb; 4 | 5 | public sealed class ShockerControlLog 6 | { 7 | public required Guid Id { get; set; } 8 | 9 | public required Guid ShockerId { get; set; } 10 | 11 | public required Guid? ControlledByUserId { get; set; } 12 | 13 | public required byte Intensity { get; set; } 14 | 15 | public required uint Duration { get; set; } 16 | 17 | public required ControlType Type { get; set; } 18 | 19 | public required string? CustomName { get; set; } 20 | 21 | public bool LiveControl { get; set; } 22 | 23 | public required DateTime CreatedAt { get; set; } 24 | 25 | // Navigations 26 | public Shocker Shocker { get; set; } = null!; 27 | public User? ControlledByUser { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /Common/OpenShockDb/ShockerShareCode.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.OpenShockDb; 2 | 3 | public sealed class ShockerShareCode : SafetySettings 4 | { 5 | public Guid Id { get; set; } 6 | 7 | public Guid ShockerId { get; set; } 8 | 9 | public DateTime CreatedAt { get; set; } 10 | 11 | public Shocker Shocker { get; set; } = null!; 12 | } 13 | -------------------------------------------------------------------------------- /Common/OpenShockDb/UserActivationRequest.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.OpenShockDb; 2 | 3 | public sealed class UserActivationRequest 4 | { 5 | public required Guid UserId { get; set; } 6 | 7 | public required string SecretHash { get; set; } 8 | 9 | public int EmailSendAttempts { get; set; } 10 | 11 | public DateTime CreatedAt { get; set; } 12 | 13 | // Navigations 14 | public User User { get; set; } = null!; 15 | } 16 | -------------------------------------------------------------------------------- /Common/OpenShockDb/UserDeactivation.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.OpenShockDb; 2 | 3 | public sealed class UserDeactivation 4 | { 5 | public required Guid DeactivatedUserId { get; set; } 6 | 7 | public required Guid DeactivatedByUserId { get; set; } 8 | 9 | public required bool DeleteLater { get; set; } 10 | 11 | public Guid? UserModerationId { get; set; } 12 | 13 | public DateTime CreatedAt { get; set; } 14 | 15 | // Navigations 16 | public User DeactivatedUser { get; set; } = null!; 17 | public User DeactivatedByUser { get; set; } = null!; 18 | } 19 | -------------------------------------------------------------------------------- /Common/OpenShockDb/UserEmailChange.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.OpenShockDb; 2 | 3 | public sealed class UserEmailChange 4 | { 5 | public required Guid Id { get; set; } 6 | 7 | public required Guid UserId { get; set; } 8 | 9 | public string Email { get; set; } = null!; 10 | 11 | public string SecretHash { get; set; } = null!; 12 | 13 | public DateTime? UsedAt { get; set; } 14 | 15 | public DateTime CreatedAt { get; set; } 16 | 17 | // Navigations 18 | public User User { get; set; } = null!; 19 | } 20 | -------------------------------------------------------------------------------- /Common/OpenShockDb/UserNameChange.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.OpenShockDb; 2 | 3 | public sealed class UserNameChange 4 | { 5 | public int Id { get; set; } // TODO: Make this Guid 6 | 7 | public required Guid UserId { get; set; } 8 | 9 | public required string OldName { get; set; } 10 | 11 | public DateTime CreatedAt { get; set; } 12 | 13 | // Navigations 14 | public User User { get; set; } = null!; 15 | } 16 | -------------------------------------------------------------------------------- /Common/OpenShockDb/UserPasswordReset.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.OpenShockDb; 2 | 3 | public sealed class UserPasswordReset 4 | { 5 | public required Guid Id { get; set; } 6 | 7 | public required Guid UserId { get; set; } 8 | 9 | public required string SecretHash { get; set; } 10 | 11 | public DateTime? UsedAt { get; set; } 12 | 13 | public DateTime CreatedAt { get; set; } 14 | 15 | // Navigations 16 | public User User { get; set; } = null!; 17 | } 18 | -------------------------------------------------------------------------------- /Common/OpenShockDb/UserShare.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.OpenShockDb; 2 | 3 | public sealed class UserShare : SafetySettings 4 | { 5 | public required Guid SharedWithUserId { get; set; } 6 | 7 | public required Guid ShockerId { get; set; } 8 | 9 | public DateTime CreatedAt { get; set; } 10 | 11 | // Navigations 12 | public User SharedWithUser { get; set; } = null!; 13 | public Shocker Shocker { get; set; } = null!; 14 | } 15 | -------------------------------------------------------------------------------- /Common/OpenShockDb/UserShareInvite.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.OpenShockDb; 2 | 3 | public sealed class UserShareInvite 4 | { 5 | public required Guid Id { get; set; } 6 | 7 | public required Guid OwnerId { get; set; } 8 | 9 | public Guid? RecipientUserId { get; set; } 10 | 11 | public DateTime CreatedAt { get; set; } 12 | 13 | // Navigations 14 | public User Owner { get; set; } = null!; 15 | public User? RecipientUser { get; set; } 16 | public ICollection ShockerMappings { get; } = []; 17 | } 18 | -------------------------------------------------------------------------------- /Common/OpenShockDb/UserShareInviteShocker.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.OpenShockDb; 2 | 3 | public sealed class UserShareInviteShocker : SafetySettings 4 | { 5 | public required Guid InviteId { get; set; } 6 | 7 | public required Guid ShockerId { get; set; } 8 | 9 | // Navigations 10 | public UserShareInvite Invite { get; set; } = null!; 11 | public Shocker Shocker { get; set; } = null!; 12 | } 13 | -------------------------------------------------------------------------------- /Common/Options/CloudflareTurnstileOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace OpenShock.Common.Options; 4 | 5 | public sealed class CloudflareTurnstileOptions 6 | { 7 | public const string Turnstile = "OpenShock:Turnstile"; 8 | 9 | public required bool Enabled { get; set; } 10 | public required string SiteKey { get; set; } 11 | public required string SecretKey { get; set; } 12 | } 13 | 14 | public sealed class CloudflareTurnstileOptionsValidator : IValidateOptions 15 | { 16 | public ValidateOptionsResult Validate(string? name, CloudflareTurnstileOptions options) 17 | { 18 | ValidateOptionsResultBuilder builder = new ValidateOptionsResultBuilder(); 19 | 20 | if (options.Enabled) 21 | { 22 | if (string.IsNullOrEmpty(options.SiteKey)) 23 | { 24 | builder.AddError("SiteKey must be populated if Enabled is true", nameof(options.SiteKey)); 25 | } 26 | 27 | if (string.IsNullOrEmpty(options.SecretKey)) 28 | { 29 | builder.AddError("SecretKey must be populated if Enabled is true", nameof(options.SecretKey)); 30 | } 31 | } 32 | 33 | return builder.Build(); 34 | } 35 | } -------------------------------------------------------------------------------- /Common/Options/DatabaseOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace OpenShock.Common.Options; 5 | 6 | public sealed class DatabaseOptions 7 | { 8 | public const string SectionName = "OpenShock:DB"; 9 | 10 | [Required(AllowEmptyStrings = false)] 11 | public required string Conn { get; init; } 12 | public bool SkipMigration { get; init; } = false; 13 | public bool Debug { get; init; } = false; 14 | } 15 | 16 | [OptionsValidator] 17 | public partial class DatabaseOptionsValidator : IValidateOptions 18 | { 19 | } -------------------------------------------------------------------------------- /Common/Options/FrontendOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace OpenShock.Common.Options; 5 | 6 | public sealed class FrontendOptions 7 | { 8 | public const string SectionName = "OpenShock:Frontend"; 9 | 10 | [Required] 11 | public required Uri BaseUrl { get; init; } 12 | 13 | [Required] 14 | public required Uri ShortUrl { get; init; } 15 | 16 | [Required(AllowEmptyStrings = false)] 17 | public required string CookieDomain { get; init; } 18 | } 19 | 20 | [OptionsValidator] 21 | public partial class FrontendOptionsValidator : IValidateOptions 22 | { 23 | } -------------------------------------------------------------------------------- /Common/Options/MetricsOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using OpenShock.Common.Utils; 3 | 4 | namespace OpenShock.Common.Options; 5 | 6 | public sealed class MetricsOptions 7 | { 8 | public const string SectionName = "OpenShock:Metrics"; 9 | 10 | public IReadOnlyCollection AllowedNetworks { get; init; } = TrustedProxiesFetcher.PrivateNetworks; 11 | } 12 | 13 | public class MetricsOptionsValidator : IValidateOptions 14 | { 15 | public ValidateOptionsResult Validate(string? name, MetricsOptions options) 16 | { 17 | var builder = new ValidateOptionsResultBuilder(); 18 | 19 | return builder.Build(); 20 | } 21 | } -------------------------------------------------------------------------------- /Common/Options/RedisOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace OpenShock.Common.Options; 4 | 5 | public sealed class RedisOptions 6 | { 7 | public const string SectionName = "OpenShock:Redis"; 8 | 9 | public required string Conn { get; set; } 10 | public required string Host { get; init; } = string.Empty; 11 | public string User { get; init; } = string.Empty; 12 | public string Password { get; init; } = string.Empty; 13 | public ushort Port { get; init; } = 6379; 14 | } 15 | 16 | public sealed class RedisOptionsValidator : IValidateOptions 17 | { 18 | public ValidateOptionsResult Validate(string? name, RedisOptions options) 19 | { 20 | ValidateOptionsResultBuilder builder = new ValidateOptionsResultBuilder(); 21 | 22 | if (string.IsNullOrEmpty(options.Conn)) 23 | { 24 | if (string.IsNullOrEmpty(options.Host)) builder.AddError("Host field is required if no connectionstring is specified", nameof(options.Host)); 25 | if (string.IsNullOrEmpty(options.User)) builder.AddError("User field is required if no connectionstring is specified", nameof(options.Host)); 26 | if (!string.IsNullOrEmpty(options.Password)) builder.AddError("Password field is required if no connectionstring is specified", nameof(options.Host)); 27 | } 28 | 29 | return builder.Build(); 30 | } 31 | } -------------------------------------------------------------------------------- /Common/Problems/CustomProblems/PolicyNotMetProblem.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace OpenShock.Common.Problems.CustomProblems; 4 | 5 | public class PolicyNotMetProblem : OpenShockProblem 6 | { 7 | public PolicyNotMetProblem(IEnumerable failedRequirements) : base( 8 | "Authorization.Policy.NotMet", 9 | "One or multiple policies were not met", HttpStatusCode.Forbidden, string.Empty) 10 | { 11 | FailedRequirements = failedRequirements; 12 | } 13 | 14 | public IEnumerable FailedRequirements { get; set; } 15 | } -------------------------------------------------------------------------------- /Common/Problems/CustomProblems/ShockerControlProblem.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace OpenShock.Common.Problems.CustomProblems; 4 | 5 | public sealed class ShockerControlProblem( 6 | string type, 7 | string title, 8 | Guid shockerId, 9 | HttpStatusCode status = HttpStatusCode.BadRequest, 10 | string? detail = null) 11 | : OpenShockProblem(type, title, status, detail) 12 | { 13 | public Guid ShockerId { get; set; } = shockerId; 14 | } -------------------------------------------------------------------------------- /Common/Problems/CustomProblems/ShockersNotFoundProblem.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace OpenShock.Common.Problems.CustomProblems; 4 | 5 | public sealed class ShockersNotFoundProblem( 6 | string type, 7 | string title, 8 | IReadOnlyList missingShockers, 9 | HttpStatusCode status = HttpStatusCode.BadRequest, 10 | string? detail = null) 11 | : OpenShockProblem(type, title, status, detail) 12 | { 13 | public IReadOnlyList MissingShockers { get; set; } = missingShockers; 14 | } -------------------------------------------------------------------------------- /Common/Problems/CustomProblems/TokenPermissionProblem.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OpenShock.Common.Models; 3 | 4 | namespace OpenShock.Common.Problems.CustomProblems; 5 | 6 | public sealed class TokenPermissionProblem( 7 | string type, 8 | string title, 9 | PermissionType requiredPermission, 10 | IEnumerable grantedPermissions, 11 | HttpStatusCode status = HttpStatusCode.BadRequest, 12 | string? detail = null) 13 | : OpenShockProblem(type, title, status, detail) 14 | { 15 | public PermissionType RequiredPermission { get; set; } = requiredPermission; 16 | public IEnumerable GrantedPermissions { get; set; } = grantedPermissions; 17 | } -------------------------------------------------------------------------------- /Common/Problems/ExceptionProblem.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace OpenShock.Common.Problems; 4 | 5 | public sealed class ExceptionProblem : OpenShockProblem 6 | { 7 | public ExceptionProblem() : base("Exception", "An unknown exception occurred", HttpStatusCode.InternalServerError, "An unknown error occurred. Please try again later. If the issue persists reach out to support.") 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /Common/Redis/DeviceOnline.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using OpenShock.Common.JsonSerialization; 3 | using Redis.OM.Modeling; 4 | using Semver; 5 | 6 | namespace OpenShock.Common.Redis; 7 | 8 | [Document(StorageType = StorageType.Json, IndexName = IndexName)] 9 | public sealed class DeviceOnline 10 | { 11 | public const string IndexName = "device-online"; 12 | 13 | [RedisIdField] [Indexed(IndexEmptyAndMissing = false)] public required Guid Id { get; set; } 14 | [Indexed(IndexEmptyAndMissing = false)] public required Guid Owner { get; set; } 15 | [JsonConverter(typeof(SemVersionJsonConverter))] 16 | public required SemVersion FirmwareVersion { get; set; } 17 | public required string Gateway { get; set; } 18 | public required DateTimeOffset ConnectedAt { get; set; } 19 | public string? UserAgent { get; set; } = null; 20 | 21 | public DateTimeOffset BootedAt { get; set; } 22 | public ushort? LatencyMs { get; set; } 23 | public int? Rssi { get; set; } 24 | } -------------------------------------------------------------------------------- /Common/Redis/DevicePair.cs: -------------------------------------------------------------------------------- 1 | using Redis.OM.Modeling; 2 | 3 | namespace OpenShock.Common.Redis; 4 | 5 | [Document(StorageType = StorageType.Json, IndexName = "device-pair")] 6 | public sealed class DevicePair 7 | { 8 | [RedisIdField] [Indexed(IndexEmptyAndMissing = false)] public required Guid Id { get; set; } 9 | [Indexed(IndexEmptyAndMissing = false)] public required string PairCode { get; set; } 10 | } -------------------------------------------------------------------------------- /Common/Redis/LcgNode.cs: -------------------------------------------------------------------------------- 1 | using Redis.OM.Modeling; 2 | 3 | namespace OpenShock.Common.Redis; 4 | 5 | [Document(StorageType = StorageType.Json, IndexName = "lcg-online-v4")] 6 | public sealed class LcgNode 7 | { 8 | [RedisIdField] [Indexed(IndexEmptyAndMissing = false)] public required string Fqdn { get; set; } 9 | [Indexed(IndexEmptyAndMissing = false)] public required string Country { get; set; } 10 | [Indexed(Sortable = true, IndexEmptyAndMissing = false)] public required byte Load { get; set; } 11 | [Indexed(IndexEmptyAndMissing = false)] public string Environment { get; set; } = "Production"; 12 | 13 | } -------------------------------------------------------------------------------- /Common/Redis/LoginSessions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using OpenShock.Common.JsonSerialization; 3 | using Redis.OM.Modeling; 4 | 5 | namespace OpenShock.Common.Redis; 6 | 7 | [Document(StorageType = StorageType.Json, IndexName = "login-session-v2")] 8 | public sealed class LoginSession 9 | { 10 | [RedisIdField] [Indexed(IndexEmptyAndMissing = false)] public required string Id { get; set; } 11 | [Indexed(IndexEmptyAndMissing = false)] public required Guid UserId { get; set; } 12 | [Indexed(IndexEmptyAndMissing = false)] public required string Ip { get; set; } 13 | [Indexed(IndexEmptyAndMissing = false)] public required string UserAgent { get; set; } 14 | [Indexed(IndexEmptyAndMissing = false)] public Guid? PublicId { get; set; } 15 | [JsonConverter(typeof(UnixMillisecondsDateTimeOffsetConverter))] 16 | public DateTimeOffset? Created { get; set; } 17 | [JsonConverter(typeof(UnixMillisecondsDateTimeOffsetConverter))] 18 | public DateTimeOffset? Expires { get; set; } 19 | [JsonConverter(typeof(UnixMillisecondsDateTimeOffsetConverter))] 20 | public DateTimeOffset? LastUsed { get; set; } 21 | } -------------------------------------------------------------------------------- /Common/Redis/PubSub/CaptiveMessage.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable UnusedAutoPropertyAccessor.Global 2 | 3 | namespace OpenShock.Common.Redis.PubSub; 4 | 5 | public sealed class CaptiveMessage 6 | { 7 | public required Guid DeviceId { get; set; } 8 | public required bool Enabled { get; set; } 9 | } -------------------------------------------------------------------------------- /Common/Redis/PubSub/ControlMessage.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Models; 2 | 3 | // ReSharper disable UnusedAutoPropertyAccessor.Global 4 | 5 | namespace OpenShock.Common.Redis.PubSub; 6 | 7 | public sealed class ControlMessage 8 | { 9 | public required Guid Sender { get; set; } 10 | 11 | /// 12 | /// Guid is the device id 13 | /// 14 | public required IDictionary> ControlMessages { get; set; } 15 | 16 | public sealed class ShockerControlInfo 17 | { 18 | public required Guid Id { get; set; } 19 | public required ushort RfId { get; set; } 20 | public required byte Intensity { get; set; } 21 | public required ushort Duration { get; set; } 22 | public required ControlType Type { get; set; } 23 | public required ShockerModelType Model { get; set; } 24 | public bool Exclusive { get; set; } = false; 25 | } 26 | } -------------------------------------------------------------------------------- /Common/Redis/PubSub/DeviceOtaInstallMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using OpenShock.Common.JsonSerialization; 3 | using Semver; 4 | 5 | namespace OpenShock.Common.Redis.PubSub; 6 | 7 | public sealed class DeviceOtaInstallMessage 8 | { 9 | public required Guid Id { get; set; } 10 | [JsonConverter(typeof(SemVersionJsonConverter))] 11 | public required SemVersion Version { get; set; } 12 | } -------------------------------------------------------------------------------- /Common/Redis/PubSub/DeviceUpdatedMessage.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Redis.PubSub; 2 | 3 | public sealed class DeviceUpdatedMessage 4 | { 5 | public required Guid Id { get; set; } 6 | } -------------------------------------------------------------------------------- /Common/Scripts/ReScaffold.ps1: -------------------------------------------------------------------------------- 1 | dotnet ef dbcontext scaffold "Host=docker-node;Port=1337;Database=openshock-app;Username=root;Password=root" Npgsql.EntityFrameworkCore.PostgreSQL -c OpenShockContext -o OpenShockDb -f --schema public 2 | -------------------------------------------------------------------------------- /Common/Services/BatchUpdate/IBatchUpdateService.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Services.BatchUpdate; 2 | 3 | public interface IBatchUpdateService 4 | { 5 | /// 6 | /// Update time of last used for a token 7 | /// 8 | /// 9 | public void UpdateApiTokenLastUsed(Guid apiTokenId); 10 | public void UpdateSessionLastUsed(string sessionToken, DateTimeOffset lastUsed); 11 | } -------------------------------------------------------------------------------- /Common/Services/Device/DeviceService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using OpenShock.Common.OpenShockDb; 3 | 4 | namespace OpenShock.Common.Services.Device; 5 | 6 | public sealed class DeviceService : IDeviceService 7 | { 8 | private readonly OpenShockContext _db; 9 | 10 | /// 11 | /// DI Constructor 12 | /// 13 | /// 14 | public DeviceService(OpenShockContext db) 15 | { 16 | _db = db; 17 | } 18 | 19 | /// 20 | public async Task> GetSharedUsers(Guid deviceId) 21 | { 22 | var sharedUsers = await _db.UserShares.AsNoTracking().Where(x => x.Shocker.DeviceId == deviceId).GroupBy(x => x.SharedWithUserId) 23 | .Select(x => x.Key) 24 | .ToListAsync(); 25 | return sharedUsers; 26 | } 27 | } -------------------------------------------------------------------------------- /Common/Services/Device/IDeviceService.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Services.Device; 2 | 3 | public interface IDeviceService 4 | { 5 | /// 6 | /// Get all users that have a share (for a shocker) within the device 7 | /// 8 | /// 9 | /// 10 | public Task> GetSharedUsers(Guid deviceId); 11 | } -------------------------------------------------------------------------------- /Common/Services/LCGNodeProvisioner/ILCGNodeProvisioner.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Geo; 2 | using OpenShock.Common.Redis; 3 | 4 | namespace OpenShock.Common.Services.LCGNodeProvisioner; 5 | 6 | public interface ILCGNodeProvisioner 7 | { 8 | public Task GetOptimalNode(string environment = "Production"); 9 | public Task GetOptimalNode(Alpha2CountryCode countryCode, string environment = "Production"); 10 | } -------------------------------------------------------------------------------- /Common/Services/RedisPubSub/RedisChannels.cs: -------------------------------------------------------------------------------- 1 | using StackExchange.Redis; 2 | 3 | namespace OpenShock.Common.Services.RedisPubSub; 4 | 5 | public static class RedisChannels 6 | { 7 | public static readonly RedisChannel KeyEventExpired = new("__keyevent@0__:expired", RedisChannel.PatternMode.Literal); 8 | 9 | public static readonly RedisChannel DeviceControl = new("msg-device-control", RedisChannel.PatternMode.Literal); 10 | public static readonly RedisChannel DeviceCaptive = new("msg-device-control-captive", RedisChannel.PatternMode.Literal); 11 | public static readonly RedisChannel DeviceUpdate = new("msg-device-update", RedisChannel.PatternMode.Literal); 12 | public static readonly RedisChannel DeviceOnlineStatus = new("msg-device-online-status", RedisChannel.PatternMode.Literal); 13 | 14 | // OTA 15 | public static readonly RedisChannel DeviceOtaInstall = new("msg-device-ota-install", RedisChannel.PatternMode.Literal); 16 | } -------------------------------------------------------------------------------- /Common/Services/Session/ISessionService.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Redis; 2 | 3 | namespace OpenShock.Common.Services.Session; 4 | 5 | public interface ISessionService 6 | { 7 | public Task CreateSessionAsync(Guid userId, string userAgent, string ipAddress); 8 | 9 | public Task> ListSessionsByUserId(Guid userId); 10 | 11 | public Task GetSessionByToken(string sessionToken); 12 | 13 | public Task GetSessionById(Guid sessionId); 14 | 15 | public Task UpdateSession(LoginSession loginSession, TimeSpan ttl); 16 | 17 | public Task DeleteSessionByToken(string sessionToken); 18 | 19 | public Task DeleteSessionById(Guid sessionId); 20 | 21 | public Task DeleteSession(LoginSession loginSession); 22 | } 23 | 24 | public sealed record CreateSessionResult(Guid Id, string Token); -------------------------------------------------------------------------------- /Common/Services/Turnstile/CloduflareTurnstileError.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Services.Turnstile; 2 | 3 | public enum CloduflareTurnstileError 4 | { 5 | MissingSecret, 6 | InvalidSecret, 7 | MissingResponse, 8 | InvalidResponse, 9 | BadRequest, 10 | TimeoutOrDuplicate, 11 | InternalServerError, 12 | } -------------------------------------------------------------------------------- /Common/Services/Turnstile/CloudflareTurnstileServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using OpenShock.Common.Options; 3 | 4 | namespace OpenShock.Common.Services.Turnstile; 5 | 6 | public static class CloudflareTurnstileServiceExtensions 7 | { 8 | public static WebApplicationBuilder AddCloudflareTurnstileService(this WebApplicationBuilder builder) 9 | { 10 | var section = builder.Configuration.GetRequiredSection(CloudflareTurnstileOptions.Turnstile); 11 | 12 | builder.Services.Configure(section); 13 | builder.Services.AddSingleton, CloudflareTurnstileOptionsValidator>(); 14 | 15 | builder.Services.AddHttpClient(client => 16 | { 17 | client.BaseAddress = new Uri("https://challenges.cloudflare.com/turnstile/v0/"); 18 | }); 19 | 20 | return builder; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Common/Services/Turnstile/CloudflareTurnstileVerifyResponseDto.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace OpenShock.Common.Services.Turnstile; 4 | 5 | public readonly struct CloudflareTurnstileVerifyResponseDto 6 | { 7 | [JsonPropertyName("success")] 8 | public bool Success { get; init; } 9 | 10 | [JsonPropertyName("challenge_ts")] 11 | public DateTime ChallengeTimeStamp { get; init; } 12 | 13 | [JsonPropertyName("hostname")] 14 | public string? Hostname { get; init; } 15 | 16 | [JsonPropertyName("error-codes")] 17 | public IReadOnlyList ErrorCodes { get; init; } 18 | 19 | [JsonPropertyName("action")] 20 | public string? Action { get; init; } 21 | 22 | [JsonPropertyName("cdata")] 23 | public string? CData { get; init; } 24 | } -------------------------------------------------------------------------------- /Common/Services/Turnstile/ICloudflareTurnstileService.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using OneOf.Types; 3 | 4 | namespace OpenShock.Common.Services.Turnstile; 5 | 6 | public interface ICloudflareTurnstileService 7 | { 8 | /// 9 | /// Verify a users turnstile response token 10 | /// 11 | /// 12 | /// 13 | /// 14 | /// Success, No response token was supplied, internal error in cloudflare turnstile, business logic error on turnstile validation 15 | public Task>> VerifyUserResponseToken( 16 | string responseToken, IPAddress? remoteIpAddress, CancellationToken cancellationToken = default); 17 | } -------------------------------------------------------------------------------- /Common/Services/Webhook/IWebhookService.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using OneOf; 3 | using OneOf.Types; 4 | using OpenShock.Common.Models; 5 | 6 | namespace OpenShock.Common.Services.Webhook; 7 | 8 | public interface IWebhookService 9 | { 10 | public Task, UnsupportedWebhookUrl>> AddWebhook(string name, Uri webhookUrl); 11 | public Task RemoveWebhook(Guid webhookId); 12 | public Task GetWebhooks(); 13 | 14 | public Task> SendWebhook(string webhookName, string title, string content, Color color); 15 | } 16 | 17 | public struct UnsupportedWebhookUrl; 18 | public struct WebhookTimeout; -------------------------------------------------------------------------------- /Common/Utils/CryptoUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | namespace OpenShock.Common.Utils; 4 | 5 | public static class CryptoUtils 6 | { 7 | public static string RandomString(int length) => RandomNumberGenerator.GetString("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", length); 8 | public static string RandomNumericString(int length) => RandomNumberGenerator.GetString("0123456789", length); 9 | } -------------------------------------------------------------------------------- /Common/Utils/GitHashAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace OpenShock.Common.Utils; 4 | 5 | [AttributeUsage(AttributeTargets.Assembly)] 6 | public sealed class GitHashAttribute(string hash) : Attribute 7 | { 8 | private string Hash { get; } = hash; 9 | 10 | public static readonly string FullHash = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.Hash ?? "error"; 11 | } 12 | -------------------------------------------------------------------------------- /Common/Utils/GravatarUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Web; 2 | 3 | namespace OpenShock.Common.Utils; 4 | 5 | public static class GravatarUtils 6 | { 7 | private static readonly string DefaultImageUrl = HttpUtility.UrlEncode("https://openshock.app/static/images/Icon512.png"); 8 | 9 | private static Uri GetImageUrl(string id) => new($"https://www.gravatar.com/avatar/{id}?d={DefaultImageUrl}"); 10 | 11 | public static readonly Uri GuestImageUrl = GetImageUrl("0"); 12 | 13 | public static Uri GetUserImageUrl(string email) => GetImageUrl(HashingUtils.HashSha256(email)); 14 | } -------------------------------------------------------------------------------- /Common/Utils/MathUtils.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Utils; 2 | 3 | public static class MathUtils 4 | { 5 | private const float EarthRadius = 6371f; 6 | private const float DegToRad = MathF.PI / 180f; 7 | 8 | /// 9 | /// Calculates the distance between two points on the Earth's surface using the Haversine formula. 10 | /// 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | public static float CalculateHaversineDistance(float lat1, float lon1, float lat2, float lon2) 17 | { 18 | 19 | float latDist = (lat2 - lat1) * DegToRad; 20 | float lonDist = (lon2 - lon1) * DegToRad; 21 | 22 | float latVal = MathF.Sin(latDist / 2f); 23 | float lonVal = MathF.Sin(lonDist / 2f); 24 | float otherVal = MathF.Cos(lat1 * DegToRad) * MathF.Cos(lat2 * DegToRad); 25 | 26 | float a = latVal * latVal + otherVal * (lonVal * lonVal); 27 | float b = 2f * MathF.Atan2(MathF.Sqrt(a), MathF.Sqrt(1f - a)); 28 | 29 | return EarthRadius * b; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Common/Utils/OsTask.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace OpenShock.Common.Utils; 5 | 6 | public static class OsTask 7 | { 8 | public static Task Run(Func function, [CallerFilePath] string file = "", 9 | [CallerMemberName] string member = "", [CallerLineNumber] int line = -1) 10 | { 11 | var task = Task.Run(function); 12 | task.ContinueWith(t => ErrorHandleTask(file, member, line, t), TaskContinuationOptions.OnlyOnFaulted); 13 | return task; 14 | } 15 | 16 | private static void ErrorHandleTask(string file, string member, int line, Task t) 17 | { 18 | if (!t.IsFaulted) return; 19 | var index = file.LastIndexOf('\\'); 20 | if (index == -1) index = file.LastIndexOf('/'); 21 | Log.Error(t.Exception, 22 | "Error during task execution. {File}::{Member}:{Line} - Stack: {Stack}", 23 | file[(index + 1)..], member, line, t.Exception?.StackTrace); 24 | } 25 | 26 | public static Task Run(Task? function, [CallerFilePath] string file = "", 27 | [CallerMemberName] string member = "", [CallerLineNumber] int line = -1) 28 | { 29 | var task = Task.Run(() => function); 30 | task.ContinueWith( 31 | t => ErrorHandleTask(file, member, line, t), TaskContinuationOptions.OnlyOnFaulted); 32 | return task; 33 | } 34 | } -------------------------------------------------------------------------------- /Common/Utils/StringUtils.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Common.Utils; 2 | 3 | public static class StringUtils 4 | { 5 | public static string Truncate(this string input, int maxLength) 6 | { 7 | return input.Length <= maxLength ? input : input[..maxLength]; 8 | } 9 | } -------------------------------------------------------------------------------- /Common/Websocket/IWebsocketController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace OpenShock.Common.Websocket; 4 | 5 | /// 6 | /// Interface description for a websocket controller with any type 7 | /// 8 | /// 9 | public interface IWebsocketController 10 | { 11 | /// 12 | /// Main identifier for the websocket connection, this might be a user or device id 13 | /// 14 | public Guid Id { get; } 15 | 16 | /// 17 | /// Queue a message to be sent to the client, usually instant 18 | /// 19 | /// 20 | /// ValueTask 21 | [NonAction] 22 | public ValueTask QueueMessage(T data); 23 | } -------------------------------------------------------------------------------- /Common/cloudflare-ips.txt: -------------------------------------------------------------------------------- 1 | 173.245.48.0/20 2 | 103.21.244.0/22 3 | 103.22.200.0/22 4 | 103.31.4.0/22 5 | 141.101.64.0/18 6 | 108.162.192.0/18 7 | 190.93.240.0/20 8 | 188.114.96.0/20 9 | 197.234.240.0/22 10 | 198.41.128.0/17 11 | 162.158.0.0/15 12 | 104.16.0.0/13 13 | 104.24.0.0/14 14 | 172.64.0.0/13 15 | 131.0.72.0/22 16 | 2400:cb00::/32 17 | 2606:4700::/32 18 | 2803:f800::/32 19 | 2405:b500::/32 20 | 2405:8100::/32 21 | 2a06:98c0::/29 22 | 2c0f:f248::/32 -------------------------------------------------------------------------------- /Cron/Attributes/CronJobAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.Cron.Attributes; 2 | 3 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] 4 | public sealed class CronJobAttribute : Attribute 5 | { 6 | public CronJobAttribute(string shcedule, string? jobName = null) 7 | { 8 | Schedule = shcedule; 9 | JobName = jobName; 10 | } 11 | 12 | public string Schedule { get; } 13 | public string? JobName { get; } 14 | } 15 | -------------------------------------------------------------------------------- /Cron/Cron.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Always 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Cron/Jobs/OtaTimeoutJob.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using OpenShock.Common.Models; 3 | using OpenShock.Common.OpenShockDb; 4 | using OpenShock.Cron.Attributes; 5 | 6 | namespace OpenShock.Cron.Jobs; 7 | 8 | [CronJob("0 */5 * * * ?")] 9 | public sealed class OtaTimeoutJob 10 | { 11 | private readonly OpenShockContext _db; 12 | 13 | /// 14 | /// DI constructor 15 | /// 16 | /// 17 | public OtaTimeoutJob(OpenShockContext db) 18 | { 19 | _db = db; 20 | } 21 | 22 | public async Task Execute() 23 | { 24 | var time = DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(10)); 25 | await _db.DeviceOtaUpdates 26 | .Where(x => (x.Status == OtaUpdateStatus.Started || x.Status == OtaUpdateStatus.Running) && 27 | x.CreatedAt < time) 28 | .ExecuteUpdateAsync(calls => 29 | calls.SetProperty(x => x.Status, OtaUpdateStatus.Timeout) 30 | .SetProperty(x => x.Message, "Timeout reached")); 31 | } 32 | } -------------------------------------------------------------------------------- /Cron/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Cron": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "environmentVariables": { 7 | "DOTNET_ENVIRONMENT": "Development" 8 | } 9 | } 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /Cron/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Kestrel": { 3 | "Endpoints": { 4 | "Http": { 5 | "Url": "http://*:780" 6 | }, 7 | "Https": { 8 | "Url": "https://*:7443" 9 | } 10 | } 11 | }, 12 | "Serilog": { 13 | "MinimumLevel": { 14 | "Default": "Verbose", 15 | "Override": { 16 | "OpenShock": "Verbose", 17 | "Hangfire": "Verbose" 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Cron/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "Kestrel": { 4 | "Endpoints": { 5 | "Http": { 6 | "Url": "http://*:80" 7 | } 8 | } 9 | }, 10 | "Serilog": { 11 | "Using": [ 12 | "Serilog.Sinks.Console", 13 | "Serilog.Sinks.Grafana.Loki", 14 | "OpenShock.Common" 15 | ], 16 | "MinimumLevel": { 17 | "Default": "Warning", 18 | "Override": { 19 | "OpenShock": "Information", 20 | "Hangfire": "Information" 21 | } 22 | }, 23 | "WriteTo": [ 24 | { 25 | "Name": "Console", 26 | "Args": { 27 | "outputTemplate": "[{Timestamp:HH:mm:ss.fff}] [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}" 28 | } 29 | } 30 | ], 31 | "Enrich": [ 32 | "FromLogContext", 33 | "WithOpenShockEnricher" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Cron/devcert.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShock/API/2553c9de2f678d42ea6c89ac21a1489279752e87/Cron/devcert.pfx -------------------------------------------------------------------------------- /Dev/README.md: -------------------------------------------------------------------------------- 1 | # Development Setup 2 | 3 | See instructions [here on the wiki](https://wiki.openshock.org/dev/contributing/backend/). -------------------------------------------------------------------------------- /Dev/devSecrets.json: -------------------------------------------------------------------------------- 1 | { 2 | "OPENSHOCK:DB:CONN": "Host=localhost;Port=5432;Database=openshock;Username=openshock;Password=openshock", 3 | "OPENSHOCK:REDIS:HOST": "localhost", 4 | "OPENSHOCK:FRONTEND:SHORTURL": "https://openshock.local", 5 | "OPENSHOCK:FRONTEND:BASEURL": "https://openshock.local", 6 | "OPENSHOCK:FRONTEND:COOKIEDOMAIN": "openshock.local,localhost", 7 | "OPENSHOCK:TURNSTILE:ENABLE": "false", 8 | "OPENSHOCK:MAIL:TYPE": "NONE", 9 | "OPENSHOCK:LCG:COUNTRYCODE": "DE" 10 | } 11 | -------------------------------------------------------------------------------- /Dev/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # THIS IS MEANT FOR DEVELOPMENT; NOT PRODUCTION; DATA SAVED BY THIS STACK IS NOT CONSIDERED SECRET AND SAFE 2 | 3 | services: 4 | postgres: 5 | image: postgres:17 6 | container_name: openshock-postgres 7 | healthcheck: 8 | test: [ "CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}" ] 9 | start_period: 20s 10 | interval: 30s 11 | retries: 5 12 | timeout: 5s 13 | networks: 14 | - openshock 15 | environment: 16 | POSTGRES_PASSWORD: openshock # This is not safe for production 17 | POSTGRES_USER: openshock 18 | POSTGRES_DB: openshock 19 | volumes: 20 | - ./postgres:/var/lib/postgresql/data 21 | ports: 22 | - 5432:5432 23 | 24 | dragonfly: 25 | image: ghcr.io/dragonflydb/dragonfly:latest 26 | container_name: openshock-dragonfly 27 | command: '--notify_keyspace_events=Ex' 28 | volumes: 29 | - ./dragonfly:/data 30 | networks: 31 | - openshock 32 | ports: 33 | - 6379:6379 34 | 35 | webui: 36 | image: ghcr.io/openshock/webui:latest 37 | environment: 38 | OPENSHOCK_NAME: "OpenShock Local" 39 | OPENSHOCK_URL: "http://localhost:8080" 40 | OPENSHOCK_API_URL: "http://localhost:80" 41 | OPENSHOCK_SHARE_URL: "http://localhost:8080" 42 | ports: 43 | - 8080:80 44 | 45 | networks: 46 | openshock: -------------------------------------------------------------------------------- /Dev/setupTestData.sh: -------------------------------------------------------------------------------- 1 | cat ./testData.sql | docker exec -i openshock-postgres psql -U openshock -d openshock -a -------------------------------------------------------------------------------- /Dev/setupUsersecrets.sh: -------------------------------------------------------------------------------- 1 | cat ./devSecrets.json | dotnet user-secrets -p ../Common/Common.csproj set 2 | 3 | hostname=$(hostname) 4 | 5 | echo "Enter your local machines IP address / Hostname [$hostname]" 6 | read -r ip 7 | 8 | if [ -z "$ip" ]; then 9 | ip=$hostname 10 | fi 11 | 12 | echo "Setting OPENSHOCK:LCG:FQDN to $ip:5443" 13 | dotnet user-secrets -p ../Common/Common.csproj set "OPENSHOCK:LCG:FQDN" "$ip:5443" 14 | 15 | -------------------------------------------------------------------------------- /Dev/testData.sql: -------------------------------------------------------------------------------- 1 | -- Insert test user with password OpenShock123! 2 | 3 | INSERT INTO public.users (id, name, email, password_hash, email_activated, roles) 4 | VALUES ('50e14f43-dd4e-412f-864d-78943ea28d91', 5 | 'OpenShock-Test', 6 | 'test@openshock.org', 7 | 'bcrypt:$2a$11$bCkcqpsNgFt1.DB33OuLhOsqVbDUp.BIvKVOIYvEO8Hyf26fV6B4y', --- OpenShock123! 8 | true, 9 | ARRAY['admin']::role_type[]); 10 | 11 | INSERT INTO public.devices (id, owner, name, token) 12 | VALUES ('7472cba2-6037-488f-b5aa-53b1c39fe450', 13 | '50e14f43-dd4e-412f-864d-78943ea28d91', 14 | 'Test Hub', 15 | 'ro6DglfhzM@hH1*P5&TOBsY4ipLMSEI4CbY!yNit4V%W&nO*Z9N@H$JzO$mh3D2PvpKL7Sde#6azOs7lBCQq0CovcCg#pX*m&Gt^4S$gCDP@f8eBPB8*q^q*dgdECXKRro6DglfhzM@hH1*P5&TOBsY4ipLMSEI4CbY!yNit4V%W&nO*Z9N@H$JzO$mh3D2PvpKL7Sde#6azOs7lBCQq0CovcCg#pX*m&Gt^4S$gCDP@f8eBPB8*q^q*dgdECXKR'); 16 | 17 | 18 | INSERT INTO public.shockers (id, name, rf_id, device, model) 19 | VALUES ('f73b3d99-44f4-4fbc-9e23-17a310202b07', 20 | 'Test Shocker', 21 | '12345', 22 | '7472cba2-6037-488f-b5aa-53b1c39fe450', 23 | 'caiXianlin'::shocker_model_type); -------------------------------------------------------------------------------- /Framework.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | 5 | -------------------------------------------------------------------------------- /LiveControlGateway/Controllers/IHubController.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Serialization.Gateway; 2 | using Semver; 3 | 4 | namespace OpenShock.LiveControlGateway.Controllers; 5 | 6 | /// 7 | /// 8 | /// 9 | public interface IHubController : IAsyncDisposable 10 | { 11 | /// 12 | /// The hub ID, unique across all hubs 13 | /// 14 | public Guid Id { get; } 15 | 16 | /// 17 | /// Control shockers 18 | /// 19 | /// 20 | /// 21 | public ValueTask Control(List controlCommands); 22 | 23 | /// 24 | /// Turn the captive portal on or off 25 | /// 26 | /// 27 | /// 28 | public ValueTask CaptivePortal(bool enable); 29 | 30 | /// 31 | /// Start an OTA install 32 | /// 33 | /// 34 | /// 35 | public ValueTask OtaInstall(SemVersion version); 36 | 37 | /// 38 | /// Disconnect the old connection in favor of the new one 39 | /// 40 | /// 41 | public Task DisconnectOld(); 42 | } -------------------------------------------------------------------------------- /LiveControlGateway/LifetimeManager/HubLifetimeState.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.LiveControlGateway.LifetimeManager; 2 | 3 | /// 4 | /// State of a hub lifetime 5 | /// 6 | public enum HubLifetimeState 7 | { 8 | /// 9 | /// Normal operation 10 | /// 11 | Idle, 12 | 13 | /// 14 | /// Initial state 15 | /// 16 | SettingUp, 17 | 18 | /// 19 | /// Swapping to a new hub controller 20 | /// 21 | Swapping, 22 | 23 | /// 24 | /// Hub controller is disconnecting, shutting down the lifetime 25 | /// 26 | Removing 27 | } -------------------------------------------------------------------------------- /LiveControlGateway/LiveControlGateway.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <_Parameter1>$(SourceRevisionId) 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Always 27 | 28 | 29 | -------------------------------------------------------------------------------- /LiveControlGateway/Models/LiveShockerPermission.cs: -------------------------------------------------------------------------------- 1 | using OpenShock.Common.Models; 2 | 3 | namespace OpenShock.LiveControlGateway.Models; 4 | 5 | /// 6 | /// Permissions and limits for a live shocker 7 | /// 8 | public sealed class LiveShockerPermission 9 | { 10 | /// 11 | /// Is the live shocker paused 12 | /// 13 | public required bool Paused { get; set; } 14 | 15 | /// 16 | /// Perms and limits for the live shocker 17 | /// 18 | public required SharePermsAndLimitsLive PermsAndLimits { get; set; } 19 | } -------------------------------------------------------------------------------- /LiveControlGateway/Options/LcgOptions.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable InconsistentNaming 2 | 3 | using Microsoft.Extensions.Options; 4 | using OpenShock.Common.Geo; 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace OpenShock.LiveControlGateway.Options; 8 | 9 | /// 10 | /// Config for the LCG 11 | /// 12 | public sealed class LcgOptions 13 | { 14 | /// 15 | /// IConfiguration section path 16 | /// 17 | public const string SectionName = "OpenShock:LCG"; 18 | 19 | /// 20 | /// FQDN of the LCG 21 | /// 22 | [Required(AllowEmptyStrings = false)] 23 | public required string Fqdn { get; set; } 24 | 25 | /// 26 | /// A valid country code by ISO 3166-1 alpha-2 https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 27 | /// 28 | [Alpha2CountryCode] 29 | public required string CountryCode { get; set; } 30 | } 31 | 32 | /// 33 | /// Options validator for 34 | /// 35 | [OptionsValidator] 36 | public partial class LcgOptionsValidator : IValidateOptions 37 | { 38 | } -------------------------------------------------------------------------------- /LiveControlGateway/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "LiveControlGateway": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LiveControlGateway/Websocket/MessageTooLongException.cs: -------------------------------------------------------------------------------- 1 | namespace OpenShock.LiveControlGateway.Websocket; 2 | 3 | /// 4 | /// Indicates that the websocket message received or to be sent is larger than the defined limit. 5 | /// 6 | public sealed class MessageTooLongException : Exception 7 | { 8 | /// 9 | public MessageTooLongException() 10 | { 11 | } 12 | 13 | /// 14 | public MessageTooLongException(string message) : base(message) 15 | { 16 | } 17 | } -------------------------------------------------------------------------------- /LiveControlGateway/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Kestrel": { 3 | "Endpoints": { 4 | "Http": { 5 | "Url": "http://*:580" 6 | }, 7 | "Https": { 8 | "Url": "https://*:5443" 9 | } 10 | } 11 | }, 12 | "Serilog": { 13 | "MinimumLevel": { 14 | "Default": "Verbose", 15 | "Override": { 16 | "Microsoft.Hosting.Lifetime": "Verbose", 17 | "Microsoft.AspNetCore.Hosting.Diagnostics": "Verbose", 18 | "Serilog.AspNetCore.RequestLoggingMiddleware": "Information", 19 | "Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker": "Warning", 20 | "OpenShock": "Verbose" 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LiveControlGateway/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Kestrel": { 3 | "Endpoints": { 4 | "Http": { 5 | "Url": "http://*:80" 6 | } 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "Serilog": { 11 | "Using": [ 12 | "Serilog.Sinks.Console", 13 | "Serilog.Sinks.Grafana.Loki", 14 | "OpenShock.Common" 15 | ], 16 | "MinimumLevel": { 17 | "Default": "Warning", 18 | "Override": { 19 | "Microsoft.Hosting.Lifetime": "Information", 20 | "Microsoft.AspNetCore.Hosting.Diagnostics": "Warning", 21 | "Serilog.AspNetCore.RequestLoggingMiddleware": "Information", 22 | "OpenShock": "Information" 23 | } 24 | }, 25 | "WriteTo": [ 26 | { 27 | "Name": "Console", 28 | "Args": { 29 | "outputTemplate": "[{Timestamp:HH:mm:ss.fff}] [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}" 30 | } 31 | } 32 | ], 33 | "Enrich": [ 34 | "FromLogContext", 35 | "WithOpenShockEnricher" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LiveControlGateway/devcert.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShock/API/2553c9de2f678d42ea6c89ac21a1489279752e87/LiveControlGateway/devcert.pfx -------------------------------------------------------------------------------- /MigrationHelper/MigrationHelper.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Exe 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /MigrationHelper/Program.cs: -------------------------------------------------------------------------------- 1 | Console.WriteLine("This is an empty project to add / remove migrations <3"); -------------------------------------------------------------------------------- /OpenShockBackend.slnx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /charts/openshock/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /charts/openshock/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: openshock 3 | description: A Helm chart for the OpenShock. 4 | type: application 5 | version: 0.1.0 6 | appVersion: "3.2.0" 7 | -------------------------------------------------------------------------------- /charts/openshock/templates/services.yaml: -------------------------------------------------------------------------------- 1 | {{- range $name, $values := dict "api" .Values.api "cron" .Values.cron "lcg" .Values.liveControllerGateway "webui" .Values.webUi -}} 2 | {{- if $values.enabled -}} 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | name: '{{ include "openshock.fullname" $ }}-{{ $name }}' 7 | labels: 8 | {{- include "openshock.labels" $ | nindent 4 }} 9 | app.kubernetes.io/component: {{ $name }} 10 | spec: 11 | type: {{ $values.service.type }} 12 | ports: 13 | - port: {{ $values.service.port }} 14 | targetPort: http 15 | protocol: TCP 16 | name: http 17 | selector: 18 | {{- include "openshock.selectorLabels" $ | nindent 4 }} 19 | app.kubernetes.io/component: {{ $name }} 20 | --- 21 | {{- end }} 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /charts/openshock/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "openshock.fullname" . }}-api-test-connection" 5 | labels: 6 | {{- include "openshock.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "openshock.fullname" . }}-api:{{ .Values.api.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /docker/API.Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = devthefuture/dockerfile-x 2 | 3 | FROM ./docker/Base.Dockerfile#build-common AS build-api 4 | 5 | COPY --link API/*.csproj API/ 6 | RUN dotnet restore API/API.csproj 7 | 8 | COPY --link API/. API/ 9 | 10 | RUN dotnet publish --no-restore -c Release API/API.csproj -o /app 11 | 12 | # Integration test stage 13 | FROM build-api AS integration-test-api 14 | 15 | COPY --link API.IntegrationTests/*.csproj API.IntegrationTests/ 16 | RUN dotnet restore API.IntegrationTests/API.IntegrationTests.csproj 17 | 18 | COPY --link API.IntegrationTests/. API.IntegrationTests/ 19 | 20 | RUN dotnet build -c Release API.IntegrationTests/API.IntegrationTests.csproj 21 | 22 | ENTRYPOINT ["dotnet", "test", "--no-build", "-c", "Release", "API.IntegrationTests/API.IntegrationTests.csproj"] 23 | 24 | # final is the final runtime stage for running the app 25 | FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final-api 26 | WORKDIR /app 27 | 28 | COPY docker/entrypoint.sh /entrypoint.sh 29 | RUN chmod +x /entrypoint.sh 30 | RUN apk update && apk add --no-cache openssl 31 | 32 | COPY --link --from=build-api /app . 33 | COPY docker/appsettings.API.json /app/appsettings.Container.json 34 | 35 | ENTRYPOINT ["/bin/ash", "/entrypoint.sh", "OpenShock.API.dll"] -------------------------------------------------------------------------------- /docker/Base.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build-common 2 | WORKDIR /src 3 | 4 | COPY --link Common/*.csproj Common/ 5 | COPY --link *.props . 6 | RUN dotnet restore Common/Common.csproj 7 | 8 | COPY --link Common/. Common/ 9 | COPY --link .git/ . 10 | 11 | RUN dotnet build --no-restore -c Release Common/Common.csproj 12 | 13 | FROM build-common AS test-common 14 | WORKDIR /src 15 | 16 | COPY --link Common.Tests/*.csproj Common.Tests/ 17 | RUN dotnet restore Common.Tests/Common.Tests.csproj 18 | COPY --link Common.Tests/. Common.Tests/ 19 | RUN dotnet build --no-restore -c Release Common.Tests/Common.Tests.csproj 20 | ENTRYPOINT ["dotnet", "test", "--no-build", "-c", "Release", "Common.Tests/Common.Tests.csproj"] 21 | 22 | -------------------------------------------------------------------------------- /docker/Cron.Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = devthefuture/dockerfile-x 2 | 3 | FROM ./docker/Base.Dockerfile#build-common AS build-cron 4 | 5 | COPY --link Cron/*.csproj Cron/ 6 | RUN dotnet restore Cron/Cron.csproj 7 | 8 | COPY --link Cron/. Cron/ 9 | 10 | RUN dotnet publish --no-restore -c Release Cron/Cron.csproj -o /app 11 | 12 | # final is the final runtime stage for running the app 13 | FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final-cron 14 | WORKDIR /app 15 | 16 | COPY docker/entrypoint.sh /entrypoint.sh 17 | RUN chmod +x /entrypoint.sh 18 | RUN apk update && apk add --no-cache openssl 19 | 20 | COPY --link --from=build-cron /app . 21 | COPY docker/appsettings.Cron.json /app/appsettings.Container.json 22 | 23 | ENTRYPOINT ["/bin/ash", "/entrypoint.sh", "OpenShock.Cron.dll"] -------------------------------------------------------------------------------- /docker/LiveControlGateway.Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = devthefuture/dockerfile-x 2 | 3 | FROM ./docker/Base.Dockerfile#build-common AS build-gateway 4 | 5 | COPY --link LiveControlGateway/*.csproj LiveControlGateway/ 6 | RUN dotnet restore LiveControlGateway/LiveControlGateway.csproj 7 | 8 | COPY --link LiveControlGateway/. LiveControlGateway/ 9 | 10 | RUN dotnet publish --no-restore -c Release LiveControlGateway/LiveControlGateway.csproj -o /app 11 | 12 | # final is the final runtime stage for running the app 13 | FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final-gateway 14 | WORKDIR /app 15 | 16 | COPY docker/entrypoint.sh /entrypoint.sh 17 | RUN chmod +x /entrypoint.sh 18 | RUN apk update && apk add --no-cache openssl 19 | 20 | COPY --link --from=build-gateway /app . 21 | COPY docker/appsettings.LiveControlGateway.json /app/appsettings.Container.json 22 | 23 | ENTRYPOINT ["/bin/ash", "/entrypoint.sh", "OpenShock.LiveControlGateway.dll"] -------------------------------------------------------------------------------- /docker/appsettings.API.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "Kestrel": { 4 | "Endpoints": { 5 | "Https": { 6 | "Protocols": "Http1AndHttp2AndHttp3", 7 | "Url": "https://*:443", 8 | "Certificate": { 9 | "Path": "/defaultcert.pfx" 10 | } 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docker/appsettings.Cron.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "Kestrel": { 4 | "Endpoints": { 5 | "Https": { 6 | "Protocols": "Http1AndHttp2AndHttp3", 7 | "Url": "https://*:443", 8 | "Certificate": { 9 | "Path": "/defaultcert.pfx" 10 | } 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docker/appsettings.LiveControlGateway.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "Kestrel": { 4 | "Endpoints": { 5 | "Https": { 6 | "Protocols": "Http1AndHttp2AndHttp3", 7 | "Url": "https://*:443", 8 | "Certificate": { 9 | "Path": "/defaultcert.pfx" 10 | } 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/ash 2 | set -e 3 | 4 | DLL_NAME="$1" 5 | 6 | CERT_PATH="/defaultcert.pfx" 7 | 8 | if [ ! -f "$CERT_PATH" ]; then 9 | echo "Generating https cert" 10 | # Generate key and certificate using OpenSSL 11 | openssl req -x509 -nodes -newkey rsa:2048 \ 12 | -keyout key.pem -out cert.pem \ 13 | -days 3650 \ 14 | -subj "/CN=localhost" 15 | # Export to PFX format with an empty password 16 | openssl pkcs12 -export -out "$CERT_PATH" \ 17 | -inkey key.pem -in cert.pem -passout pass: 18 | # Clean up temporary files 19 | rm key.pem cert.pem 20 | fi 21 | 22 | exec dotnet "$DLL_NAME" 23 | --------------------------------------------------------------------------------