├── .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 |
--------------------------------------------------------------------------------