From 1958a6ae8d037fc9f01903f07e54a2f6f7aabafc Mon Sep 17 00:00:00 2001 From: Jelle Buning Date: Mon, 23 Feb 2026 20:15:45 +0100 Subject: [PATCH 1/8] added mediator --- .github/copilot-instructions.md | 130 ++++++ Directory.Packages.props | 9 +- Sentinel.slnx | 1 - .../Commands/Auth/Login/LoginCommand.cs | 6 + .../Auth/Login/LoginCommandHandler.cs | 19 + .../Auth/Login/LoginCommandValidator.cs | 16 + .../Auth/RefreshToken/RefreshTokenCommand.cs | 6 + .../RefreshTokenCommandHandler.cs | 19 + .../RefreshTokenCommandValidator.cs | 15 + .../Auth/VerifyTotp/VerifyTotpCommand.cs | 5 + .../VerifyTotp/VerifyTotpCommandHandler.cs | 22 + .../VerifyTotp/VerifyTotpCommandValidator.cs | 19 + .../ExecuteSecurityScanCommand.cs | 5 + .../ExecuteSecurityScanCommandHandler.cs | 14 + .../Devices/Ping/PingDeviceCommand.cs | 5 + .../Devices/Ping/PingDeviceCommandHandler.cs | 13 + .../Ping/PingDeviceCommandValidator.cs | 12 + .../Devices/Register/RegisterDeviceCommand.cs | 6 + .../Register/RegisterDeviceCommandHandler.cs | 15 + .../RegisterDeviceCommandValidator.cs | 16 + .../RequestRemoteAccessCommand.cs | 5 + .../RequestRemoteAccessCommandHandler.cs | 14 + .../Devices/Restart/RestartDeviceCommand.cs | 5 + .../Restart/RestartDeviceCommandHandler.cs | 14 + .../UpdateDeviceInformationCommand.cs | 6 + .../UpdateDeviceInformationCommandHandler.cs | 14 + ...UpdateDeviceInformationCommandValidator.cs | 15 + .../UpdateSecurityInformationCommand.cs | 6 + ...UpdateSecurityInformationCommandHandler.cs | 14 + .../UpdateSoftwareInformationCommand.cs | 6 + ...UpdateSoftwareInformationCommandHandler.cs | 14 + .../UpdateStorageInformationCommand.cs | 5 + .../UpdateStorageInformationCommandHandler.cs | 14 + .../Users/RegisterUser/RegisterUserCommand.cs | 5 + .../RegisterUserCommandHandler.cs | 20 + .../RegisterUserCommandValidator.cs | 17 + .../DTO/Device/DeviceTokenResponse.cs | 17 +- .../DTO/Device/GetDevicesResponse.cs | 26 +- .../DTO/Device/RegisterDeviceDto.cs | 13 +- .../DTO/User/VerifyUserDto.cs | 3 - .../DependencyInjection.cs | 35 +- .../Exceptions/BadValidationRequest.cs | 8 + .../Exceptions/DomainException.cs | 3 + .../Interfaces/IDeviceMessenger.cs | 8 + .../Interfaces/IDeviceRepository.cs | 31 +- .../Interfaces/IOrganisationRepository.cs | 11 +- .../Mediator/Behaviors/LoggingBehavior.cs | 37 ++ .../Behaviors/UnhandledExceptionBehavior.cs | 29 ++ .../Mediator/Behaviors/ValidationBehavior.cs | 33 ++ .../Exceptions/ValidationException.cs | 15 + .../DeviceInformationQuery.cs | 6 + .../DeviceInformationQueryHandler.cs | 15 + .../Queries/Devices/Devices/DevicesQuery.cs | 6 + .../Devices/Devices/DevicesQueryHandler.cs | 15 + .../SecurityInformationQuery.cs | 6 + .../SecurityInformationQueryHandler.cs | 15 + .../SoftwareInformationQuery.cs | 6 + .../SoftwareInformationQueryHandler.cs | 15 + .../StorageInformationQuery.cs | 6 + .../StorageInformationQueryHandler.cs | 15 + .../GetAllOrganisationsQuery.cs | 6 + .../GetAllOrganisationsQueryHandler.cs | 15 + .../Records/ExceptionResponse.cs | 3 + .../Sentinel.Api.Application.csproj | 7 + .../Services/Interfaces/IJwtTokenGenerator.cs | 9 + .../Services/Interfaces/ITokenService.cs | 11 - .../Services/TokenGenerator.cs | 38 ++ .../Services/TokenService.cs | 57 --- .../DependencyInjection.cs | 156 +++---- .../Exceptions/BadRequestException.cs | 9 +- .../Exceptions/ForbiddenException.cs | 9 +- .../Exceptions/InternalServerException.cs | 9 +- .../Exceptions/NotFoundException.cs | 9 +- .../Exceptions/ResponseManager.cs | 38 -- .../Exceptions/ResponseMessage.cs | 7 - .../Exceptions/UnauthorizedException.cs | 8 +- .../Middleware/ExceptionHandlingMiddleware.cs | 38 ++ .../Persistence/AppDbContext.cs | 21 +- .../Repositories/AuthRepository.cs | 22 +- .../Repositories/DeviceRepository.cs | 419 +++++++++--------- .../Repositories/OrganisationRepository.cs | 15 +- .../Repositories/UserRepository.cs | 2 +- .../Sentinel.Api.Infrastructure.csproj | 20 +- .../SignalR/Interfaces/IDeviceMessageHub.cs | 2 +- .../SignalR/SignalRDeviceMessenger.cs | 34 ++ .../Controllers/AuthController.cs | 59 +-- .../Controllers/DeviceAdminController.cs | 171 +++---- .../Controllers/DeviceController.cs | 110 ++--- .../Controllers/OrganisationController.cs | 23 +- .../Controllers/UserController.cs | 25 +- src/Sentinel.Api/Program.cs | 4 +- ...formation.cs => SecurityInformationDto.cs} | 10 +- .../DTO/Device/SoftwareInformation.cs | 12 - .../DTO/Device/SoftwareInformationDto.cs | 12 + ...nformation.cs => StorageInformationDto.cs} | 6 +- .../RemoteAccessMessage.cs | 2 +- .../RestartDeviceMessage.cs | 2 +- .../SecurityScanMessage.cs | 2 +- .../Api/AuthenticationDelegatingHandler.cs | 2 +- .../Api/AuthenticationHandler.cs | 2 +- .../Api/Extensions/HttpClientExtensions.cs | 21 +- .../Extensions/HttpContentExtensions.cs | 2 +- .../Api/Interfaces/IAuthenticationHandler.cs | 2 +- .../Api/SentinelApiService.cs | 15 +- .../Consumer/ConsumerBase.cs | 4 +- .../Consumer/Interfaces/IConsumerConfig.cs | 1 - .../Module/Interfaces/IModule.cs | 7 +- .../Interfaces/IScheduledModuleConfig.cs | 11 +- .../Module/Interfaces/IStartupModule.cs | 9 +- .../Module/ScheduledModuleBase.cs | 107 +++-- .../Module/ScheduledModuleConfig.cs | 9 +- .../FirewallSettingsRetriever.cs | 4 +- .../Interfaces/IFirewallSettingsRetriever.cs | 2 +- .../SecurityInformationRetriever.cs | 8 +- .../SoftwareInformationRetriever.cs | 6 +- .../StorageInformationRetriever.cs | 6 +- .../ISecurityInformationRetriever.cs | 2 +- .../ISoftwareInformationRetriever.cs | 2 +- .../IStorageInformationRetriever.cs | 2 +- .../RestartDevice/RestartDeviceModule.cs | 4 +- .../SecurityScan/SecurityScanModule.cs | 4 +- .../RemoteAccessModule.cs | 4 +- .../Extensions/ServiceCollectionExtensions.cs | 125 +++--- src/Sentinel.WorkerService/Program.cs | 2 +- .../Common/ApiFixtureExtensions.cs | 59 +++ .../Common/ClientExtensions.cs | 83 ---- .../Common/FactoryExtensions.cs | 52 --- .../Common/HttpHelpers.cs | 40 ++ .../Common/TestAssertions.cs | 41 ++ .../Common/TestScope.cs | 160 +++++++ .../Device/Authentication/RegisterTests.cs | 43 +- .../Device/Management/DeviceRetrievalTests.cs | 59 +-- .../Updates/UpdateDeviceInformationTests.cs | 57 +++ .../Updates/UpdateSecurityInformationTests.cs | 64 +++ .../Updates/UpdateSoftwareInformationTests.cs | 50 +++ .../Updates/UpdateStorageInformationTests.cs | 58 +++ .../Device/Worker/PingTaskTests.cs | 38 +- .../Organisation/OrganisationTests.cs | 35 +- .../Sentinel.Api.Integration.Tests.csproj | 2 +- .../User/Authentication/RefreshTokenTests.cs | 85 ++++ .../User/Authentication/RegisterTests.cs | 26 +- .../User/Authentication/SignInTests.cs | 37 +- .../User/Authentication/VerificationTests.cs | 30 +- 143 files changed, 2357 insertions(+), 1328 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 src/Sentinel.Api.Application/Commands/Auth/Login/LoginCommand.cs create mode 100644 src/Sentinel.Api.Application/Commands/Auth/Login/LoginCommandHandler.cs create mode 100644 src/Sentinel.Api.Application/Commands/Auth/Login/LoginCommandValidator.cs create mode 100644 src/Sentinel.Api.Application/Commands/Auth/RefreshToken/RefreshTokenCommand.cs create mode 100644 src/Sentinel.Api.Application/Commands/Auth/RefreshToken/RefreshTokenCommandHandler.cs create mode 100644 src/Sentinel.Api.Application/Commands/Auth/RefreshToken/RefreshTokenCommandValidator.cs create mode 100644 src/Sentinel.Api.Application/Commands/Auth/VerifyTotp/VerifyTotpCommand.cs create mode 100644 src/Sentinel.Api.Application/Commands/Auth/VerifyTotp/VerifyTotpCommandHandler.cs create mode 100644 src/Sentinel.Api.Application/Commands/Auth/VerifyTotp/VerifyTotpCommandValidator.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/ExecuteSecurityScan/ExecuteSecurityScanCommand.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/ExecuteSecurityScan/ExecuteSecurityScanCommandHandler.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/Ping/PingDeviceCommand.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/Ping/PingDeviceCommandHandler.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/Ping/PingDeviceCommandValidator.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/Register/RegisterDeviceCommand.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/Register/RegisterDeviceCommandHandler.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/Register/RegisterDeviceCommandValidator.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/RequestRemoteAccess/RequestRemoteAccessCommand.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/RequestRemoteAccess/RequestRemoteAccessCommandHandler.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/Restart/RestartDeviceCommand.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/Restart/RestartDeviceCommandHandler.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/Update/DeviceInformation/UpdateDeviceInformationCommand.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/Update/DeviceInformation/UpdateDeviceInformationCommandHandler.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/Update/DeviceInformation/UpdateDeviceInformationCommandValidator.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/Update/SecurityInformation/UpdateSecurityInformationCommand.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/Update/SecurityInformation/UpdateSecurityInformationCommandHandler.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/Update/SoftwareInformation/UpdateSoftwareInformationCommand.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/Update/SoftwareInformation/UpdateSoftwareInformationCommandHandler.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/Update/StorageInformation/UpdateStorageInformationCommand.cs create mode 100644 src/Sentinel.Api.Application/Commands/Devices/Update/StorageInformation/UpdateStorageInformationCommandHandler.cs create mode 100644 src/Sentinel.Api.Application/Commands/Users/RegisterUser/RegisterUserCommand.cs create mode 100644 src/Sentinel.Api.Application/Commands/Users/RegisterUser/RegisterUserCommandHandler.cs create mode 100644 src/Sentinel.Api.Application/Commands/Users/RegisterUser/RegisterUserCommandValidator.cs create mode 100644 src/Sentinel.Api.Application/Exceptions/BadValidationRequest.cs create mode 100644 src/Sentinel.Api.Application/Exceptions/DomainException.cs create mode 100644 src/Sentinel.Api.Application/Interfaces/IDeviceMessenger.cs create mode 100644 src/Sentinel.Api.Application/Mediator/Behaviors/LoggingBehavior.cs create mode 100644 src/Sentinel.Api.Application/Mediator/Behaviors/UnhandledExceptionBehavior.cs create mode 100644 src/Sentinel.Api.Application/Mediator/Behaviors/ValidationBehavior.cs create mode 100644 src/Sentinel.Api.Application/Mediator/Exceptions/ValidationException.cs create mode 100644 src/Sentinel.Api.Application/Queries/Devices/DeviceInformation/DeviceInformationQuery.cs create mode 100644 src/Sentinel.Api.Application/Queries/Devices/DeviceInformation/DeviceInformationQueryHandler.cs create mode 100644 src/Sentinel.Api.Application/Queries/Devices/Devices/DevicesQuery.cs create mode 100644 src/Sentinel.Api.Application/Queries/Devices/Devices/DevicesQueryHandler.cs create mode 100644 src/Sentinel.Api.Application/Queries/Devices/SecurityInformation/SecurityInformationQuery.cs create mode 100644 src/Sentinel.Api.Application/Queries/Devices/SecurityInformation/SecurityInformationQueryHandler.cs create mode 100644 src/Sentinel.Api.Application/Queries/Devices/SoftwareInformation/SoftwareInformationQuery.cs create mode 100644 src/Sentinel.Api.Application/Queries/Devices/SoftwareInformation/SoftwareInformationQueryHandler.cs create mode 100644 src/Sentinel.Api.Application/Queries/Devices/StorageInformation/StorageInformationQuery.cs create mode 100644 src/Sentinel.Api.Application/Queries/Devices/StorageInformation/StorageInformationQueryHandler.cs create mode 100644 src/Sentinel.Api.Application/Queries/Organisations/GetAllOrganisations/GetAllOrganisationsQuery.cs create mode 100644 src/Sentinel.Api.Application/Queries/Organisations/GetAllOrganisations/GetAllOrganisationsQueryHandler.cs create mode 100644 src/Sentinel.Api.Application/Records/ExceptionResponse.cs create mode 100644 src/Sentinel.Api.Application/Services/Interfaces/IJwtTokenGenerator.cs delete mode 100644 src/Sentinel.Api.Application/Services/Interfaces/ITokenService.cs create mode 100644 src/Sentinel.Api.Application/Services/TokenGenerator.cs delete mode 100644 src/Sentinel.Api.Application/Services/TokenService.cs delete mode 100644 src/Sentinel.Api.Infrastructure/Exceptions/ResponseManager.cs delete mode 100644 src/Sentinel.Api.Infrastructure/Exceptions/ResponseMessage.cs create mode 100644 src/Sentinel.Api.Infrastructure/Middleware/ExceptionHandlingMiddleware.cs create mode 100644 src/Sentinel.Api.Infrastructure/SignalR/SignalRDeviceMessenger.cs rename src/Sentinel.Common/DTO/Device/{SecurityInformation.cs => SecurityInformationDto.cs} (74%) delete mode 100644 src/Sentinel.Common/DTO/Device/SoftwareInformation.cs create mode 100644 src/Sentinel.Common/DTO/Device/SoftwareInformationDto.cs rename src/Sentinel.Common/DTO/Device/{StorageInformation.cs => StorageInformationDto.cs} (62%) rename src/Sentinel.Common/{Messages => SignalR}/RemoteAccessMessage.cs (74%) rename src/Sentinel.Common/{Messages => SignalR}/RestartDeviceMessage.cs (52%) rename src/Sentinel.Common/{Messages => SignalR}/SecurityScanMessage.cs (51%) rename src/Sentinel.WorkerService.Common/{ => Api}/Extensions/HttpContentExtensions.cs (89%) create mode 100644 tests/Sentinel.Api.Integration.Tests/Common/ApiFixtureExtensions.cs delete mode 100644 tests/Sentinel.Api.Integration.Tests/Common/ClientExtensions.cs delete mode 100644 tests/Sentinel.Api.Integration.Tests/Common/FactoryExtensions.cs create mode 100644 tests/Sentinel.Api.Integration.Tests/Common/HttpHelpers.cs create mode 100644 tests/Sentinel.Api.Integration.Tests/Common/TestAssertions.cs create mode 100644 tests/Sentinel.Api.Integration.Tests/Common/TestScope.cs create mode 100644 tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateDeviceInformationTests.cs create mode 100644 tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateSecurityInformationTests.cs create mode 100644 tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateSoftwareInformationTests.cs create mode 100644 tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateStorageInformationTests.cs create mode 100644 tests/Sentinel.Api.Integration.Tests/User/Authentication/RefreshTokenTests.cs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..f82d216 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,130 @@ +# Copilot Instructions (C#/.NET) — SOLID & Design Patterns First + +## Mission +Produce maintainable, testable, evolvable C# code. **Top priority is SOLID** and applying **appropriate design patterns** over “quick fixes”. + +## Highest Priority Rules (Always) +1. **Correctness > Maintainability > Performance** (unless explicitly told otherwise). +2. **SOLID first**: + - **S**RP: one reason to change per type/module. + - **O**CP: extend with new behavior via abstractions, not by editing many call sites. + - **L**SP: derived types must not break expectations of base types/interfaces. + - **I**SP: small, role-focused interfaces; avoid “god” interfaces. + - **D**IP: depend on abstractions; inject dependencies; avoid static/global state. +3. Prefer **composition over inheritance**. +4. Keep dependencies pointing inward (domain/core should not depend on infrastructure). +5. Make changes **small, reversible, and well-tested**. + +## Core Design Principles +- **KISS** (Keep It Simple, Stupid): Prefer straightforward solutions. Avoid unnecessary complexity, over-engineering, and "clever" code. Clarity and maintainability trump cleverness. +- **DRY** (Don't Repeat Yourself): Eliminate code duplication through abstractions, helper methods, and reusable components. Duplicated logic is a maintenance risk and divergence hazard. +- **YAGNI** (You Aren't Gonna Need It): Don't add features, parameters, or abstractions you don't need today. Speculative generalization introduces complexity; add them when you actually need them. + +## Domain-Driven Design (DDD) +Structure the domain model to reflect business reality: +- **Ubiquitous Language**: Use domain terms consistently across code, conversations, and documentation. Align naming with how domain experts speak. +- **Entities**: Objects with identity and lifecycle; contain business logic tied to their identity. +- **Value Objects**: Immutable, identity-less objects representing domain concepts (e.g., `Money`, `PhoneNumber`). Prefer these when identity doesn't matter. +- **Aggregates**: Cohesive clusters of entities/value objects with a root entity (`AggregateRoot`) that maintains consistency boundaries. + - Model only the aggregates and relationships the business needs; avoid god aggregates. + - Each aggregate should be independently testable. +- **Domain Services**: Stateless operations that don't naturally belong to an entity or value object; they orchestrate domain logic across aggregates. +- **Domain Events**: Capture significant business occurrences (e.g., `UserRegistered`, `OrderPlaced`). Publish them to trigger side effects or integration points. +- **Repositories**: Abstractions that represent collections of aggregates; hide persistence details. Retrieve/persist only aggregate roots. +- **Bounded Contexts**: Define clear boundaries where specific ubiquitous language applies. Different contexts may model the same concept differently; use Anti-Corruption Layers or Context Mappers at boundaries. + +## Architectural Defaults (Unless the repo says otherwise) +- Prefer **Clean Architecture / Onion** style aligned with **Domain-Driven Design**: + - **Domain/Core**: entities, value objects, domain services, domain events, aggregates, and business rules (no infrastructure dependencies). + - **Application**: use-cases, orchestration, application services, ports (interfaces), DTOs, and command/query handlers. + - **Infrastructure**: EF Core, HTTP clients, file system, external services, and repository implementations. + - **Presentation**: Web API / UI / Controllers. +- Use **dependency injection** (Microsoft.Extensions.DependencyInjection). +- Avoid leaking infrastructure types (e.g., EF `DbContext`, `IQueryable`) out of infrastructure. + +## Design Patterns: When to Use What +Apply patterns intentionally—don’t “pattern-fest”. + +- **Strategy**: interchangeable algorithms; choose at runtime (e.g., pricing rules). +- **Factory / Abstract Factory**: complex creation, invariants, environment-specific implementations. +- **Decorator**: cross-cutting behavior (caching, retries) without modifying core services. +- **Adapter**: wrap external SDKs to stable internal interfaces. +- **Facade**: simplify interactions with a complex subsystem. +- **Command**: represent actions/use-cases; supports logging, retries, queues. +- **Mediator (MediatR)**: for request/response or notifications in app layer (only if already used). +- **Repository**: only if it adds value over EF Core usage and boundaries are clear. +- **Unit of Work**: usually EF Core already is; avoid double-abstraction. +- **Specification**: reusable query predicates/validation rules. +- **Observer**: domain events, integration events, notifications. +- **State**: complex state transitions. + +## C#/.NET Coding Standards +- Use modern C# features appropriately: `record` for immutable DTOs/value objects, `init`, pattern matching. +- Prefer immutability by default. +- Prefer `IReadOnlyList` / `IReadOnlyCollection` for outward-facing collections. +- Avoid `async void` (except event handlers). Prefer `CancellationToken` on async APIs. +- **Never use `.Result` or `.Wait()` on tasks** — this causes deadlocks and defeats async/await benefits. Always `await` instead. +- Use expression-bodied members for simple getters/methods. +- Use `using` declarations for disposables when possible. +- Use guard clauses; validate public method arguments. +- Keep public APIs small; internal helpers private/internal. + +### Naming +- Types/Methods: PascalCase; locals: camelCase. +- Interfaces: `IThing`. +- Async methods: `ThingAsync`. +- Tests: `Method_Scenario_ExpectedResult` or `Given_When_Then`. + +## Error Handling & Results +- For application/service boundaries, prefer explicit result types: + - `Result` / `OneOf` / `ErrorOr` (use what the repo already uses). +- Use exceptions for truly exceptional conditions; include context. + +## Testing Expectations +- Prefer **unit tests** for business logic; integration tests for persistence/HTTP. +- Arrange-Act-Assert (AAA). +- Test behavior, not implementation details. +- For time/randomness/IO, inject abstractions (e.g., `ISystemClock`, `IRandom`, file interfaces). +- When changing behavior, **add/adjust tests first** where feasible. + +## Performance Guidance +- Don’t micro-optimize. +- Avoid unnecessary allocations in hot paths; use `Span` only when justified and readable. +- Use streaming for large payloads; avoid loading whole files into memory. +- Prefer `IAsyncEnumerable` for large sequences when appropriate. + +## Security & Reliability +- Treat external input as untrusted; validate and sanitize. +- Avoid string concatenation for SQL; use parameterized queries/EF. +- Use `HttpClientFactory`; set timeouts; handle transient failures (policies if used). +- Don’t log secrets/PII; prefer structured logging. + +## Git & Change Discipline +When asked to implement something: +1. **Clarify requirements** (inputs/outputs, edge cases, constraints) if ambiguous. +2. **Propose a minimal design**: + - responsibilities + - key abstractions/interfaces + - chosen pattern (if any) and why +3. **Implement in small commits** (or small logical steps) if working interactively. +4. **Add/adjust tests**. +5. **Explain tradeoffs** briefly (why this pattern, why these boundaries). + +## “Stop and Ask” Triggers +Ask before proceeding if: +- A choice changes public API shape, persistence schema, or cross-service contracts. +- A new dependency/package is needed. +- There are multiple plausible patterns/architectures and the repo conventions aren’t clear. +- Behavior impacts security, authz/authn, or financial logic. + +## Output Format Expectations +- Prefer producing complete compilable code with necessary `using`s and namespaces. +- Keep diffs focused; don’t reformat unrelated code. +- If unsure of existing conventions, follow EditorConfig / analyzers in the repo. + +## Quick SOLID Checklist (Self-Review) +- Does each class have a single responsibility? +- Are new behaviors added by extension rather than modifying many existing files? +- Can substitutes be used without surprises? +- Are interfaces minimal and role-based? +- Do higher-level modules depend on abstractions rather than concretions? \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index eaba8f4..4b363b1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,7 +3,11 @@ true - + + + + + @@ -33,6 +37,7 @@ - + + \ No newline at end of file diff --git a/Sentinel.slnx b/Sentinel.slnx index 404fe97..4dbf54c 100644 --- a/Sentinel.slnx +++ b/Sentinel.slnx @@ -11,7 +11,6 @@ - diff --git a/src/Sentinel.Api.Application/Commands/Auth/Login/LoginCommand.cs b/src/Sentinel.Api.Application/Commands/Auth/Login/LoginCommand.cs new file mode 100644 index 0000000..204159c --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Auth/Login/LoginCommand.cs @@ -0,0 +1,6 @@ +using Mediator; +using Sentinel.Api.Application.DTO.User; + +namespace Sentinel.Api.Application.Commands.Auth.Login; + +public record LoginCommand(string Email, string Password) : IRequest; diff --git a/src/Sentinel.Api.Application/Commands/Auth/Login/LoginCommandHandler.cs b/src/Sentinel.Api.Application/Commands/Auth/Login/LoginCommandHandler.cs new file mode 100644 index 0000000..c8be8a0 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Auth/Login/LoginCommandHandler.cs @@ -0,0 +1,19 @@ +using Mediator; +using Sentinel.Api.Application.DTO.User; +using Sentinel.Api.Application.Interfaces; + +namespace Sentinel.Api.Application.Commands.Auth.Login; + +public class LoginCommandHandler(IAuthRepository authRepository) : IRequestHandler +{ + public async ValueTask Handle(LoginCommand request, CancellationToken cancellationToken) + { + var signInDto = new SignInUserDto + { + Email = request.Email, + Password = request.Password + }; + + return await authRepository.AuthenticateAsync(signInDto); + } +} diff --git a/src/Sentinel.Api.Application/Commands/Auth/Login/LoginCommandValidator.cs b/src/Sentinel.Api.Application/Commands/Auth/Login/LoginCommandValidator.cs new file mode 100644 index 0000000..1e7510c --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Auth/Login/LoginCommandValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; + +namespace Sentinel.Api.Application.Commands.Auth.Login; + +public class LoginCommandValidator : AbstractValidator +{ + public LoginCommandValidator() + { + RuleFor(x => x.Email) + .NotEmpty().WithMessage("Email is required") + .EmailAddress().WithMessage("Email must be a valid email address"); + + RuleFor(x => x.Password) + .NotEmpty().WithMessage("Password is required"); + } +} diff --git a/src/Sentinel.Api.Application/Commands/Auth/RefreshToken/RefreshTokenCommand.cs b/src/Sentinel.Api.Application/Commands/Auth/RefreshToken/RefreshTokenCommand.cs new file mode 100644 index 0000000..9d2ea5a --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Auth/RefreshToken/RefreshTokenCommand.cs @@ -0,0 +1,6 @@ +using Mediator; +using Sentinel.Api.Application.DTO.Token; + +namespace Sentinel.Api.Application.Commands.Auth.RefreshToken; + +public record RefreshTokenCommand(string AccessToken, string RefreshToken) : IRequest; diff --git a/src/Sentinel.Api.Application/Commands/Auth/RefreshToken/RefreshTokenCommandHandler.cs b/src/Sentinel.Api.Application/Commands/Auth/RefreshToken/RefreshTokenCommandHandler.cs new file mode 100644 index 0000000..be478b5 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Auth/RefreshToken/RefreshTokenCommandHandler.cs @@ -0,0 +1,19 @@ +using Mediator; +using Sentinel.Api.Application.DTO.Token; +using Sentinel.Api.Application.Interfaces; + +namespace Sentinel.Api.Application.Commands.Auth.RefreshToken; + +public class RefreshTokenCommandHandler(IAuthRepository authRepository) : IRequestHandler +{ + public async ValueTask Handle(RefreshTokenCommand request, CancellationToken cancellationToken) + { + var tokenDto = new TokenDto + { + AccessToken = request.AccessToken, + RefreshToken = request.RefreshToken + }; + + return await authRepository.RefreshTokenAsync(tokenDto); + } +} diff --git a/src/Sentinel.Api.Application/Commands/Auth/RefreshToken/RefreshTokenCommandValidator.cs b/src/Sentinel.Api.Application/Commands/Auth/RefreshToken/RefreshTokenCommandValidator.cs new file mode 100644 index 0000000..aa2c571 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Auth/RefreshToken/RefreshTokenCommandValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; + +namespace Sentinel.Api.Application.Commands.Auth.RefreshToken; + +public class RefreshTokenCommandValidator : AbstractValidator +{ + public RefreshTokenCommandValidator() + { + RuleFor(x => x.AccessToken) + .NotEmpty().WithMessage("AccessToken is required"); + + RuleFor(x => x.RefreshToken) + .NotEmpty().WithMessage("RefreshToken is required"); + } +} diff --git a/src/Sentinel.Api.Application/Commands/Auth/VerifyTotp/VerifyTotpCommand.cs b/src/Sentinel.Api.Application/Commands/Auth/VerifyTotp/VerifyTotpCommand.cs new file mode 100644 index 0000000..e69c328 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Auth/VerifyTotp/VerifyTotpCommand.cs @@ -0,0 +1,5 @@ +using Mediator; + +namespace Sentinel.Api.Application.Commands.Auth.VerifyTotp; + +public record VerifyTotpCommand(int UserId, string AuthenticityToken, string OtpAttempt) : IRequest; diff --git a/src/Sentinel.Api.Application/Commands/Auth/VerifyTotp/VerifyTotpCommandHandler.cs b/src/Sentinel.Api.Application/Commands/Auth/VerifyTotp/VerifyTotpCommandHandler.cs new file mode 100644 index 0000000..e8215f7 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Auth/VerifyTotp/VerifyTotpCommandHandler.cs @@ -0,0 +1,22 @@ +using Mediator; +using Sentinel.Api.Application.DTO.Token; +using Sentinel.Api.Application.DTO.User; +using Sentinel.Api.Application.Interfaces; + +namespace Sentinel.Api.Application.Commands.Auth.VerifyTotp; + +public class VerifyTotpCommandHandler(IAuthRepository authRepository) : IRequestHandler +{ + public async ValueTask Handle(VerifyTotpCommand request, CancellationToken cancellationToken) + { + var verifyDto = new VerifyUserDto + { + UserId = request.UserId, + AuthenticityToken = request.AuthenticityToken, + OtpAttempt = request.OtpAttempt + }; + + var user = await authRepository.VerifyTotpAsync(verifyDto); + return await authRepository.GetTokenAsync(user); + } +} diff --git a/src/Sentinel.Api.Application/Commands/Auth/VerifyTotp/VerifyTotpCommandValidator.cs b/src/Sentinel.Api.Application/Commands/Auth/VerifyTotp/VerifyTotpCommandValidator.cs new file mode 100644 index 0000000..4ac78aa --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Auth/VerifyTotp/VerifyTotpCommandValidator.cs @@ -0,0 +1,19 @@ +using FluentValidation; + +namespace Sentinel.Api.Application.Commands.Auth.VerifyTotp; + +public class VerifyTotpCommandValidator : AbstractValidator +{ + public VerifyTotpCommandValidator() + { + RuleFor(x => x.UserId) + .GreaterThan(0).WithMessage("UserId must be greater than 0"); + + RuleFor(x => x.AuthenticityToken) + .NotEmpty().WithMessage("AuthenticityToken is required"); + + RuleFor(x => x.OtpAttempt) + .NotEmpty().WithMessage("OtpAttempt is required") + .Length(6).WithMessage("OtpAttempt must be 6 digits"); + } +} diff --git a/src/Sentinel.Api.Application/Commands/Devices/ExecuteSecurityScan/ExecuteSecurityScanCommand.cs b/src/Sentinel.Api.Application/Commands/Devices/ExecuteSecurityScan/ExecuteSecurityScanCommand.cs new file mode 100644 index 0000000..8b833fc --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/ExecuteSecurityScan/ExecuteSecurityScanCommand.cs @@ -0,0 +1,5 @@ +using Mediator; + +namespace Sentinel.Api.Application.Commands.Devices.ExecuteSecurityScan; + +public record ExecuteSecurityScanCommand(int DeviceId) : IRequest; diff --git a/src/Sentinel.Api.Application/Commands/Devices/ExecuteSecurityScan/ExecuteSecurityScanCommandHandler.cs b/src/Sentinel.Api.Application/Commands/Devices/ExecuteSecurityScan/ExecuteSecurityScanCommandHandler.cs new file mode 100644 index 0000000..d1ca3c5 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/ExecuteSecurityScan/ExecuteSecurityScanCommandHandler.cs @@ -0,0 +1,14 @@ +using Mediator; +using Sentinel.Api.Application.Interfaces; + +namespace Sentinel.Api.Application.Commands.Devices.ExecuteSecurityScan; + +public class ExecuteSecurityScanCommandHandler(IDeviceMessenger deviceMessenger) + : IRequestHandler +{ + public async ValueTask Handle(ExecuteSecurityScanCommand request, CancellationToken cancellationToken) + { + await deviceMessenger.SendSecurityScanRequestAsync(request.DeviceId, cancellationToken); + return Unit.Value; + } +} diff --git a/src/Sentinel.Api.Application/Commands/Devices/Ping/PingDeviceCommand.cs b/src/Sentinel.Api.Application/Commands/Devices/Ping/PingDeviceCommand.cs new file mode 100644 index 0000000..48e0ee8 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/Ping/PingDeviceCommand.cs @@ -0,0 +1,5 @@ +using Mediator; + +namespace Sentinel.Api.Application.Commands.Devices.Ping; + +public record PingDeviceCommand(int DeviceId) : IRequest; diff --git a/src/Sentinel.Api.Application/Commands/Devices/Ping/PingDeviceCommandHandler.cs b/src/Sentinel.Api.Application/Commands/Devices/Ping/PingDeviceCommandHandler.cs new file mode 100644 index 0000000..af31bf2 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/Ping/PingDeviceCommandHandler.cs @@ -0,0 +1,13 @@ +using Mediator; +using Sentinel.Api.Application.Interfaces; + +namespace Sentinel.Api.Application.Commands.Devices.Ping; + +public class PingDeviceCommandHandler(IDeviceRepository deviceRepository) : IRequestHandler +{ + public ValueTask Handle(PingDeviceCommand request, CancellationToken cancellationToken) + { + deviceRepository.Ping(request.DeviceId); + return ValueTask.FromResult(Unit.Value); + } +} diff --git a/src/Sentinel.Api.Application/Commands/Devices/Ping/PingDeviceCommandValidator.cs b/src/Sentinel.Api.Application/Commands/Devices/Ping/PingDeviceCommandValidator.cs new file mode 100644 index 0000000..624f1c1 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/Ping/PingDeviceCommandValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; + +namespace Sentinel.Api.Application.Commands.Devices.Ping; + +public class PingDeviceCommandValidator : AbstractValidator +{ + public PingDeviceCommandValidator() + { + RuleFor(x => x.DeviceId) + .GreaterThan(0).WithMessage("DeviceId must be greater than 0"); + } +} diff --git a/src/Sentinel.Api.Application/Commands/Devices/Register/RegisterDeviceCommand.cs b/src/Sentinel.Api.Application/Commands/Devices/Register/RegisterDeviceCommand.cs new file mode 100644 index 0000000..367d242 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/Register/RegisterDeviceCommand.cs @@ -0,0 +1,6 @@ +using Mediator; +using Sentinel.Api.Application.DTO.Device; + +namespace Sentinel.Api.Application.Commands.Devices.Register; + +public record RegisterDeviceCommand(Guid OrganisationHash, string Name) : IRequest; diff --git a/src/Sentinel.Api.Application/Commands/Devices/Register/RegisterDeviceCommandHandler.cs b/src/Sentinel.Api.Application/Commands/Devices/Register/RegisterDeviceCommandHandler.cs new file mode 100644 index 0000000..d3b48f3 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/Register/RegisterDeviceCommandHandler.cs @@ -0,0 +1,15 @@ +using Mediator; +using Sentinel.Api.Application.DTO.Device; +using Sentinel.Api.Application.Interfaces; + +namespace Sentinel.Api.Application.Commands.Devices.Register; + +public class RegisterDeviceCommandHandler(IDeviceRepository deviceRepository) + : IRequestHandler +{ + public ValueTask Handle(RegisterDeviceCommand request, CancellationToken cancellationToken) + { + var response = deviceRepository.Register(request.OrganisationHash, request.Name); + return ValueTask.FromResult(response); + } +} diff --git a/src/Sentinel.Api.Application/Commands/Devices/Register/RegisterDeviceCommandValidator.cs b/src/Sentinel.Api.Application/Commands/Devices/Register/RegisterDeviceCommandValidator.cs new file mode 100644 index 0000000..28dfa40 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/Register/RegisterDeviceCommandValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; + +namespace Sentinel.Api.Application.Commands.Devices.Register; + +public class RegisterDeviceCommandValidator : AbstractValidator +{ + public RegisterDeviceCommandValidator() + { + RuleFor(x => x.OrganisationHash) + .NotEmpty().WithMessage("OrganisationHash is required"); + + RuleFor(x => x.Name) + .NotEmpty().WithMessage("Name is required") + .MaximumLength(100).WithMessage("Name must not exceed 100 characters"); + } +} diff --git a/src/Sentinel.Api.Application/Commands/Devices/RequestRemoteAccess/RequestRemoteAccessCommand.cs b/src/Sentinel.Api.Application/Commands/Devices/RequestRemoteAccess/RequestRemoteAccessCommand.cs new file mode 100644 index 0000000..e8f89b4 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/RequestRemoteAccess/RequestRemoteAccessCommand.cs @@ -0,0 +1,5 @@ +using Mediator; + +namespace Sentinel.Api.Application.Commands.Devices.RequestRemoteAccess; + +public record RequestRemoteAccessCommand(int DeviceId) : IRequest; diff --git a/src/Sentinel.Api.Application/Commands/Devices/RequestRemoteAccess/RequestRemoteAccessCommandHandler.cs b/src/Sentinel.Api.Application/Commands/Devices/RequestRemoteAccess/RequestRemoteAccessCommandHandler.cs new file mode 100644 index 0000000..76993d7 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/RequestRemoteAccess/RequestRemoteAccessCommandHandler.cs @@ -0,0 +1,14 @@ +using Mediator; +using Sentinel.Api.Application.Interfaces; + +namespace Sentinel.Api.Application.Commands.Devices.RequestRemoteAccess; + +public class RequestRemoteAccessCommandHandler(IDeviceMessenger deviceMessenger) + : IRequestHandler +{ + public async ValueTask Handle(RequestRemoteAccessCommand request, CancellationToken cancellationToken) + { + await deviceMessenger.SendRemoteAccessRequestAsync(request.DeviceId, cancellationToken); + return Unit.Value; + } +} diff --git a/src/Sentinel.Api.Application/Commands/Devices/Restart/RestartDeviceCommand.cs b/src/Sentinel.Api.Application/Commands/Devices/Restart/RestartDeviceCommand.cs new file mode 100644 index 0000000..a27757d --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/Restart/RestartDeviceCommand.cs @@ -0,0 +1,5 @@ +using Mediator; + +namespace Sentinel.Api.Application.Commands.Devices.Restart; + +public record RestartDeviceCommand(int DeviceId) : IRequest; diff --git a/src/Sentinel.Api.Application/Commands/Devices/Restart/RestartDeviceCommandHandler.cs b/src/Sentinel.Api.Application/Commands/Devices/Restart/RestartDeviceCommandHandler.cs new file mode 100644 index 0000000..01c92b6 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/Restart/RestartDeviceCommandHandler.cs @@ -0,0 +1,14 @@ +using Mediator; +using Sentinel.Api.Application.Interfaces; + +namespace Sentinel.Api.Application.Commands.Devices.Restart; + +public class RestartDeviceCommandHandler(IDeviceMessenger deviceMessenger) + : IRequestHandler +{ + public async ValueTask Handle(RestartDeviceCommand request, CancellationToken cancellationToken) + { + await deviceMessenger.SendRestartRequestAsync(request.DeviceId, cancellationToken); + return Unit.Value; + } +} diff --git a/src/Sentinel.Api.Application/Commands/Devices/Update/DeviceInformation/UpdateDeviceInformationCommand.cs b/src/Sentinel.Api.Application/Commands/Devices/Update/DeviceInformation/UpdateDeviceInformationCommand.cs new file mode 100644 index 0000000..c250dbe --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/Update/DeviceInformation/UpdateDeviceInformationCommand.cs @@ -0,0 +1,6 @@ +using Mediator; +using Sentinel.Common.DTO.Device.Information; + +namespace Sentinel.Api.Application.Commands.Devices.Update.DeviceInformation; + +public record UpdateDeviceInformationCommand(int DeviceId, UpdateDeviceInformationDto DeviceInfo) : IRequest; diff --git a/src/Sentinel.Api.Application/Commands/Devices/Update/DeviceInformation/UpdateDeviceInformationCommandHandler.cs b/src/Sentinel.Api.Application/Commands/Devices/Update/DeviceInformation/UpdateDeviceInformationCommandHandler.cs new file mode 100644 index 0000000..38d6b76 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/Update/DeviceInformation/UpdateDeviceInformationCommandHandler.cs @@ -0,0 +1,14 @@ +using Mediator; +using Sentinel.Api.Application.Interfaces; + +namespace Sentinel.Api.Application.Commands.Devices.Update.DeviceInformation; + +public class UpdateDeviceInformationCommandHandler(IDeviceRepository deviceRepository) + : IRequestHandler +{ + public ValueTask Handle(UpdateDeviceInformationCommand request, CancellationToken cancellationToken) + { + deviceRepository.UpdateDeviceInformation(request.DeviceId, request.DeviceInfo); + return ValueTask.FromResult(Unit.Value); + } +} diff --git a/src/Sentinel.Api.Application/Commands/Devices/Update/DeviceInformation/UpdateDeviceInformationCommandValidator.cs b/src/Sentinel.Api.Application/Commands/Devices/Update/DeviceInformation/UpdateDeviceInformationCommandValidator.cs new file mode 100644 index 0000000..58ec0fc --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/Update/DeviceInformation/UpdateDeviceInformationCommandValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; + +namespace Sentinel.Api.Application.Commands.Devices.Update.DeviceInformation; + +public class UpdateDeviceInformationCommandValidator : AbstractValidator +{ + public UpdateDeviceInformationCommandValidator() + { + RuleFor(x => x.DeviceId) + .GreaterThan(0).WithMessage("DeviceId must be greater than 0"); + + RuleFor(x => x.DeviceInfo) + .NotNull().WithMessage("DeviceInfo is required"); + } +} diff --git a/src/Sentinel.Api.Application/Commands/Devices/Update/SecurityInformation/UpdateSecurityInformationCommand.cs b/src/Sentinel.Api.Application/Commands/Devices/Update/SecurityInformation/UpdateSecurityInformationCommand.cs new file mode 100644 index 0000000..2573dd7 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/Update/SecurityInformation/UpdateSecurityInformationCommand.cs @@ -0,0 +1,6 @@ +using Mediator; +using Sentinel.Common.DTO.Device; + +namespace Sentinel.Api.Application.Commands.Devices.Update.SecurityInformation; + +public record UpdateSecurityInformationCommand(int DeviceId, SecurityInformationDto SecurityInfo) : IRequest; diff --git a/src/Sentinel.Api.Application/Commands/Devices/Update/SecurityInformation/UpdateSecurityInformationCommandHandler.cs b/src/Sentinel.Api.Application/Commands/Devices/Update/SecurityInformation/UpdateSecurityInformationCommandHandler.cs new file mode 100644 index 0000000..222270e --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/Update/SecurityInformation/UpdateSecurityInformationCommandHandler.cs @@ -0,0 +1,14 @@ +using Mediator; +using Sentinel.Api.Application.Interfaces; + +namespace Sentinel.Api.Application.Commands.Devices.Update.SecurityInformation; + +public class UpdateSecurityInformationCommandHandler(IDeviceRepository deviceRepository) + : IRequestHandler +{ + public ValueTask Handle(UpdateSecurityInformationCommand request, CancellationToken cancellationToken) + { + deviceRepository.UpdateSecurityInfo(request.DeviceId, request.SecurityInfo); + return ValueTask.FromResult(Unit.Value); + } +} diff --git a/src/Sentinel.Api.Application/Commands/Devices/Update/SoftwareInformation/UpdateSoftwareInformationCommand.cs b/src/Sentinel.Api.Application/Commands/Devices/Update/SoftwareInformation/UpdateSoftwareInformationCommand.cs new file mode 100644 index 0000000..aad479e --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/Update/SoftwareInformation/UpdateSoftwareInformationCommand.cs @@ -0,0 +1,6 @@ +using Mediator; +using Sentinel.Common.DTO.Device; + +namespace Sentinel.Api.Application.Commands.Devices.Update.SoftwareInformation; + +public record UpdateSoftwareInformationCommand(int DeviceId, SoftwareInformationDto SoftwareInfo) : IRequest; diff --git a/src/Sentinel.Api.Application/Commands/Devices/Update/SoftwareInformation/UpdateSoftwareInformationCommandHandler.cs b/src/Sentinel.Api.Application/Commands/Devices/Update/SoftwareInformation/UpdateSoftwareInformationCommandHandler.cs new file mode 100644 index 0000000..db0dde0 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/Update/SoftwareInformation/UpdateSoftwareInformationCommandHandler.cs @@ -0,0 +1,14 @@ +using Mediator; +using Sentinel.Api.Application.Interfaces; + +namespace Sentinel.Api.Application.Commands.Devices.Update.SoftwareInformation; + +public class UpdateSoftwareInformationCommandHandler(IDeviceRepository deviceRepository) + : IRequestHandler +{ + public ValueTask Handle(UpdateSoftwareInformationCommand request, CancellationToken cancellationToken) + { + deviceRepository.UpdateSoftwareInfo(request.DeviceId, request.SoftwareInfo); + return ValueTask.FromResult(Unit.Value); + } +} diff --git a/src/Sentinel.Api.Application/Commands/Devices/Update/StorageInformation/UpdateStorageInformationCommand.cs b/src/Sentinel.Api.Application/Commands/Devices/Update/StorageInformation/UpdateStorageInformationCommand.cs new file mode 100644 index 0000000..8baefea --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/Update/StorageInformation/UpdateStorageInformationCommand.cs @@ -0,0 +1,5 @@ +using Mediator; + +namespace Sentinel.Api.Application.Commands.Devices.Update.StorageInformation; + +public record UpdateStorageInformationCommand(int DeviceId, Sentinel.Common.DTO.Device.StorageInformationDto StorageInfo) : IRequest; diff --git a/src/Sentinel.Api.Application/Commands/Devices/Update/StorageInformation/UpdateStorageInformationCommandHandler.cs b/src/Sentinel.Api.Application/Commands/Devices/Update/StorageInformation/UpdateStorageInformationCommandHandler.cs new file mode 100644 index 0000000..527c42c --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Devices/Update/StorageInformation/UpdateStorageInformationCommandHandler.cs @@ -0,0 +1,14 @@ +using Mediator; +using Sentinel.Api.Application.Interfaces; + +namespace Sentinel.Api.Application.Commands.Devices.Update.StorageInformation; + +public class UpdateStorageInformationCommandHandler(IDeviceRepository deviceRepository) + : IRequestHandler +{ + public ValueTask Handle(UpdateStorageInformationCommand request, CancellationToken cancellationToken) + { + deviceRepository.UpdateStorageInfo(request.DeviceId, request.StorageInfo); + return ValueTask.FromResult(Unit.Value); + } +} diff --git a/src/Sentinel.Api.Application/Commands/Users/RegisterUser/RegisterUserCommand.cs b/src/Sentinel.Api.Application/Commands/Users/RegisterUser/RegisterUserCommand.cs new file mode 100644 index 0000000..6c4b4fa --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Users/RegisterUser/RegisterUserCommand.cs @@ -0,0 +1,5 @@ +using Mediator; + +namespace Sentinel.Api.Application.Commands.Users.RegisterUser; + +public record RegisterUserCommand(string Email, string Password) : IRequest; diff --git a/src/Sentinel.Api.Application/Commands/Users/RegisterUser/RegisterUserCommandHandler.cs b/src/Sentinel.Api.Application/Commands/Users/RegisterUser/RegisterUserCommandHandler.cs new file mode 100644 index 0000000..6aa86d2 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Users/RegisterUser/RegisterUserCommandHandler.cs @@ -0,0 +1,20 @@ +using Mediator; +using Sentinel.Api.Application.DTO.User; +using Sentinel.Api.Application.Interfaces; + +namespace Sentinel.Api.Application.Commands.Users.RegisterUser; + +public class RegisterUserCommandHandler(IUserRepository userRepository) : IRequestHandler +{ + public async ValueTask Handle(RegisterUserCommand request, CancellationToken cancellationToken) + { + var registerDto = new RegisterUserDto + { + Email = request.Email, + Password = request.Password + }; + + await userRepository.Register(registerDto); + return Unit.Value; + } +} diff --git a/src/Sentinel.Api.Application/Commands/Users/RegisterUser/RegisterUserCommandValidator.cs b/src/Sentinel.Api.Application/Commands/Users/RegisterUser/RegisterUserCommandValidator.cs new file mode 100644 index 0000000..f8fda23 --- /dev/null +++ b/src/Sentinel.Api.Application/Commands/Users/RegisterUser/RegisterUserCommandValidator.cs @@ -0,0 +1,17 @@ +using FluentValidation; + +namespace Sentinel.Api.Application.Commands.Users.RegisterUser; + +public class RegisterUserCommandValidator : AbstractValidator +{ + public RegisterUserCommandValidator() + { + RuleFor(x => x.Email) + .NotEmpty().WithMessage("Email is required") + .EmailAddress().WithMessage("Email must be a valid email address"); + + RuleFor(x => x.Password) + .NotEmpty().WithMessage("Password is required") + .MinimumLength(8).WithMessage("Password must be at least 8 characters"); + } +} diff --git a/src/Sentinel.Api.Application/DTO/Device/DeviceTokenResponse.cs b/src/Sentinel.Api.Application/DTO/Device/DeviceTokenResponse.cs index bf5c643..4e40a02 100644 --- a/src/Sentinel.Api.Application/DTO/Device/DeviceTokenResponse.cs +++ b/src/Sentinel.Api.Application/DTO/Device/DeviceTokenResponse.cs @@ -1,10 +1,9 @@ -namespace Sentinel.Api.Application.DTO.Device +namespace Sentinel.Api.Application.DTO.Device; + +public class DeviceTokenResponse { - public class DeviceTokenResponse - { - public int Id { get; set; } - public required int OrganisationId { get; set; } - public required string AccessToken { get; set; } - public required string RefreshToken { get; set; } - } -} + public int Id { get; set; } + public required int OrganisationId { get; set; } + public required string AccessToken { get; set; } + public required string RefreshToken { get; set; } +} \ No newline at end of file diff --git a/src/Sentinel.Api.Application/DTO/Device/GetDevicesResponse.cs b/src/Sentinel.Api.Application/DTO/Device/GetDevicesResponse.cs index 466c688..c89dea6 100644 --- a/src/Sentinel.Api.Application/DTO/Device/GetDevicesResponse.cs +++ b/src/Sentinel.Api.Application/DTO/Device/GetDevicesResponse.cs @@ -1,20 +1,10 @@ -namespace Sentinel.Api.Application.DTO.Device -{ - public class GetDevicesResponse - { - - public int ActiveDevices { get; set; } - - - public int TotalDevices { get; set; } - - - public int Status { get; set; } +namespace Sentinel.Api.Application.DTO.Device; - - public Guid OrganisationHash { get; set; } - - - public required List Devices { get; set; } - } +public class GetDevicesResponse +{ + public int ActiveDevices { get; set; } + public int TotalDevices { get; set; } + public int Status { get; set; } + public Guid OrganisationHash { get; set; } + public required List Devices { get; set; } } \ No newline at end of file diff --git a/src/Sentinel.Api.Application/DTO/Device/RegisterDeviceDto.cs b/src/Sentinel.Api.Application/DTO/Device/RegisterDeviceDto.cs index 29d190e..dfb5cb6 100644 --- a/src/Sentinel.Api.Application/DTO/Device/RegisterDeviceDto.cs +++ b/src/Sentinel.Api.Application/DTO/Device/RegisterDeviceDto.cs @@ -1,8 +1,7 @@ -namespace Sentinel.Api.Application.DTO.Device +namespace Sentinel.Api.Application.DTO.Device; + +public class RegisterDeviceDto { - public class RegisterDeviceDto - { - public required Guid OrganisationHash { get; set; } - public required string Name { get; set; } - } -} + public required Guid OrganisationHash { get; set; } + public required string Name { get; set; } +} \ No newline at end of file diff --git a/src/Sentinel.Api.Application/DTO/User/VerifyUserDto.cs b/src/Sentinel.Api.Application/DTO/User/VerifyUserDto.cs index 471da1a..0fd09ae 100644 --- a/src/Sentinel.Api.Application/DTO/User/VerifyUserDto.cs +++ b/src/Sentinel.Api.Application/DTO/User/VerifyUserDto.cs @@ -2,10 +2,7 @@ public record VerifyUserDto { - public required int UserId { get; init; } - public required string AuthenticityToken { get; init; } - public required string OtpAttempt { get; init; } } \ No newline at end of file diff --git a/src/Sentinel.Api.Application/DependencyInjection.cs b/src/Sentinel.Api.Application/DependencyInjection.cs index 458dd9c..ee02c99 100644 --- a/src/Sentinel.Api.Application/DependencyInjection.cs +++ b/src/Sentinel.Api.Application/DependencyInjection.cs @@ -1,11 +1,40 @@ -using Microsoft.Extensions.DependencyInjection; +using FluentValidation; +using Mediator; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; +using Sentinel.Api.Application.Mediator.Behaviors; namespace Sentinel.Api.Application; public static class DependencyInjection { - public static IServiceCollection AddApplication(this IServiceCollection services) + extension(IServiceCollection services) { - return services; + public IServiceCollection AddApplication() + { + var assembly = typeof(DependencyInjection).Assembly; + + services.AddMediatorServices(assembly); + services.AddValidationServices(assembly); + + return services; + } + + private void AddMediatorServices(Assembly assembly) + { + services.AddMediator(options => + { + options.ServiceLifetime = ServiceLifetime.Transient; + }); + + services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehavior<,>)); + services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); + services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); + } + + private void AddValidationServices(Assembly assembly) + { + services.AddValidatorsFromAssembly(assembly); + } } } \ No newline at end of file diff --git a/src/Sentinel.Api.Application/Exceptions/BadValidationRequest.cs b/src/Sentinel.Api.Application/Exceptions/BadValidationRequest.cs new file mode 100644 index 0000000..45f6eb9 --- /dev/null +++ b/src/Sentinel.Api.Application/Exceptions/BadValidationRequest.cs @@ -0,0 +1,8 @@ +using Sentinel.Api.Infrastructure.Exceptions; + +namespace Sentinel.Api.Application.Exceptions; + +public class BadValidationRequest(string message) : DomainException(message) +{ + +} \ No newline at end of file diff --git a/src/Sentinel.Api.Application/Exceptions/DomainException.cs b/src/Sentinel.Api.Application/Exceptions/DomainException.cs new file mode 100644 index 0000000..bf7620c --- /dev/null +++ b/src/Sentinel.Api.Application/Exceptions/DomainException.cs @@ -0,0 +1,3 @@ +namespace Sentinel.Api.Infrastructure.Exceptions; + +public class DomainException(string message) : Exception(message); \ No newline at end of file diff --git a/src/Sentinel.Api.Application/Interfaces/IDeviceMessenger.cs b/src/Sentinel.Api.Application/Interfaces/IDeviceMessenger.cs new file mode 100644 index 0000000..b6f0b51 --- /dev/null +++ b/src/Sentinel.Api.Application/Interfaces/IDeviceMessenger.cs @@ -0,0 +1,8 @@ +namespace Sentinel.Api.Application.Interfaces; + +public interface IDeviceMessenger +{ + Task SendSecurityScanRequestAsync(int deviceId, CancellationToken cancellationToken = default); + Task SendRestartRequestAsync(int deviceId, CancellationToken cancellationToken = default); + Task SendRemoteAccessRequestAsync(int deviceId, CancellationToken cancellationToken = default); +} diff --git a/src/Sentinel.Api.Application/Interfaces/IDeviceRepository.cs b/src/Sentinel.Api.Application/Interfaces/IDeviceRepository.cs index c9487d0..f4c31da 100644 --- a/src/Sentinel.Api.Application/Interfaces/IDeviceRepository.cs +++ b/src/Sentinel.Api.Application/Interfaces/IDeviceRepository.cs @@ -2,20 +2,19 @@ using Sentinel.Common.DTO.Device; using Sentinel.Common.DTO.Device.Information; -namespace Sentinel.Api.Application.Interfaces +namespace Sentinel.Api.Application.Interfaces; + +public interface IDeviceRepository { - public interface IDeviceRepository - { - void Ping(int id); - DeviceTokenResponse Register(RegisterDeviceDto registerDevice); - GetDevicesResponse GetDevices(int userId); - GetDeviceInformationDto GetDeviceInformation(int id); - void UpdateDeviceInformation(int id, UpdateDeviceInformationDto update); - StorageInformation GetStorageInfo(int id); - void UpdateStorageInfo(int id, StorageInformation update); - SecurityInformation GetSecurityInfo(int id); - void UpdateSecurityInfo(int id, SecurityInformation update); - SoftwareInformation GetSoftwareInfo(int id); - void UpdateSoftwareInfo(int id, SoftwareInformation update); - } -} + void Ping(int id); + DeviceTokenResponse Register(Guid organisationHash, string name); + GetDevicesResponse GetDevices(int userId); + GetDeviceInformationDto GetDeviceInformation(int id); + void UpdateDeviceInformation(int id, UpdateDeviceInformationDto update); + StorageInformationDto GetStorageInfo(int id); + void UpdateStorageInfo(int id, StorageInformationDto update); + SecurityInformationDto GetSecurityInfo(int id); + void UpdateSecurityInfo(int id, SecurityInformationDto update); + SoftwareInformationDto GetSoftwareInfo(int id); + void UpdateSoftwareInfo(int id, SoftwareInformationDto update); +} \ No newline at end of file diff --git a/src/Sentinel.Api.Application/Interfaces/IOrganisationRepository.cs b/src/Sentinel.Api.Application/Interfaces/IOrganisationRepository.cs index 995b3a9..c07bef6 100644 --- a/src/Sentinel.Api.Application/Interfaces/IOrganisationRepository.cs +++ b/src/Sentinel.Api.Application/Interfaces/IOrganisationRepository.cs @@ -1,9 +1,8 @@ using Sentinel.Api.Domain.Entities; -namespace Sentinel.Api.Application.Interfaces +namespace Sentinel.Api.Application.Interfaces; + +public interface IOrganisationRepository { - public interface IOrganisationRepository - { - public List GetAll(); - } -} + public List GetAll(); +} \ No newline at end of file diff --git a/src/Sentinel.Api.Application/Mediator/Behaviors/LoggingBehavior.cs b/src/Sentinel.Api.Application/Mediator/Behaviors/LoggingBehavior.cs new file mode 100644 index 0000000..150b37f --- /dev/null +++ b/src/Sentinel.Api.Application/Mediator/Behaviors/LoggingBehavior.cs @@ -0,0 +1,37 @@ +using System.Diagnostics; +using Mediator; +using Microsoft.Extensions.Logging; + +namespace Sentinel.Api.Application.Mediator.Behaviors; + +public class LoggingBehavior(ILogger> logger) + : IPipelineBehavior + where TMessage : IMessage +{ + public async ValueTask Handle(TMessage message, MessageHandlerDelegate next, CancellationToken cancellationToken) + { + var messageName = typeof(TMessage).Name; + logger.LogInformation("Handling {MessageName}", messageName); + var stopwatch = Stopwatch.StartNew(); + + try + { + var response = await next(message, cancellationToken); + stopwatch.Stop(); + + logger.LogInformation("Handled {MessageName} in {ElapsedMilliseconds}ms", + messageName, stopwatch.ElapsedMilliseconds); + + return response; + } + catch (Exception ex) + { + stopwatch.Stop(); + + logger.LogError(ex, "Error handling {MessageName} after {ElapsedMilliseconds}ms", + messageName, stopwatch.ElapsedMilliseconds); + + throw; + } + } +} diff --git a/src/Sentinel.Api.Application/Mediator/Behaviors/UnhandledExceptionBehavior.cs b/src/Sentinel.Api.Application/Mediator/Behaviors/UnhandledExceptionBehavior.cs new file mode 100644 index 0000000..04c570d --- /dev/null +++ b/src/Sentinel.Api.Application/Mediator/Behaviors/UnhandledExceptionBehavior.cs @@ -0,0 +1,29 @@ +using FluentValidation; +using Mediator; +using Microsoft.Extensions.Logging; +using Sentinel.Api.Application.Exceptions; + +namespace Sentinel.Api.Application.Mediator.Behaviors; + +public class UnhandledExceptionBehavior( + ILogger> logger +) : IPipelineBehavior where TMessage : IMessage +{ + public async ValueTask Handle(TMessage message, MessageHandlerDelegate next, CancellationToken cancellationToken) + { + try + { + return await next(message, cancellationToken); + } + catch (Mediator.Exceptions.ValidationException ex) + { + throw new BadValidationRequest(ex.Message); + } + catch (Exception ex) + { + logger.LogError(ex, "Unhandled Exception for Request {MessageName} {@Message}", typeof(TMessage).Name, message); + throw; + } + } +} + diff --git a/src/Sentinel.Api.Application/Mediator/Behaviors/ValidationBehavior.cs b/src/Sentinel.Api.Application/Mediator/Behaviors/ValidationBehavior.cs new file mode 100644 index 0000000..e25fa0d --- /dev/null +++ b/src/Sentinel.Api.Application/Mediator/Behaviors/ValidationBehavior.cs @@ -0,0 +1,33 @@ +using FluentValidation; +using Mediator; + +namespace Sentinel.Api.Application.Mediator.Behaviors; + +public class ValidationBehavior(IEnumerable> validators) + : IPipelineBehavior + where TMessage : IMessage +{ + public async ValueTask Handle(TMessage message, MessageHandlerDelegate next, CancellationToken cancellationToken) + { + if (!validators.Any()) + { + return await next(message, cancellationToken); + } + + var context = new ValidationContext(message); + var validationResults = await Task.WhenAll( + validators.Select(v => v.ValidateAsync(context, cancellationToken))); + + var failures = validationResults + .Where(r => r.Errors.Any()) + .SelectMany(r => r.Errors) + .ToList(); + + if (failures.Any()) + { + throw new Exceptions.ValidationException(failures); + } + + return await next(message, cancellationToken); + } +} diff --git a/src/Sentinel.Api.Application/Mediator/Exceptions/ValidationException.cs b/src/Sentinel.Api.Application/Mediator/Exceptions/ValidationException.cs new file mode 100644 index 0000000..4966522 --- /dev/null +++ b/src/Sentinel.Api.Application/Mediator/Exceptions/ValidationException.cs @@ -0,0 +1,15 @@ +using FluentValidation.Results; + +namespace Sentinel.Api.Application.Mediator.Exceptions; + +public class ValidationException() : Exception("One or more validation failures have occurred.") +{ + public IDictionary Errors { get; } = new Dictionary(); + + public ValidationException(IEnumerable failures) : this() + { + Errors = failures + .GroupBy(e => e.PropertyName, e => e.ErrorMessage) + .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray()); + } +} diff --git a/src/Sentinel.Api.Application/Queries/Devices/DeviceInformation/DeviceInformationQuery.cs b/src/Sentinel.Api.Application/Queries/Devices/DeviceInformation/DeviceInformationQuery.cs new file mode 100644 index 0000000..d4deffc --- /dev/null +++ b/src/Sentinel.Api.Application/Queries/Devices/DeviceInformation/DeviceInformationQuery.cs @@ -0,0 +1,6 @@ +using Mediator; +using Sentinel.Common.DTO.Device.Information; + +namespace Sentinel.Api.Application.Queries.Devices.DeviceInformation; + +public record DeviceInformationQuery(int DeviceId) : IRequest; diff --git a/src/Sentinel.Api.Application/Queries/Devices/DeviceInformation/DeviceInformationQueryHandler.cs b/src/Sentinel.Api.Application/Queries/Devices/DeviceInformation/DeviceInformationQueryHandler.cs new file mode 100644 index 0000000..f5c27ea --- /dev/null +++ b/src/Sentinel.Api.Application/Queries/Devices/DeviceInformation/DeviceInformationQueryHandler.cs @@ -0,0 +1,15 @@ +using Mediator; +using Sentinel.Api.Application.Interfaces; +using Sentinel.Common.DTO.Device.Information; + +namespace Sentinel.Api.Application.Queries.Devices.DeviceInformation; + +public class DeviceInformationQueryHandler(IDeviceRepository deviceRepository) + : IRequestHandler +{ + public ValueTask Handle(DeviceInformationQuery request, CancellationToken cancellationToken) + { + var deviceInfo = deviceRepository.GetDeviceInformation(request.DeviceId); + return ValueTask.FromResult(deviceInfo); + } +} diff --git a/src/Sentinel.Api.Application/Queries/Devices/Devices/DevicesQuery.cs b/src/Sentinel.Api.Application/Queries/Devices/Devices/DevicesQuery.cs new file mode 100644 index 0000000..069f82e --- /dev/null +++ b/src/Sentinel.Api.Application/Queries/Devices/Devices/DevicesQuery.cs @@ -0,0 +1,6 @@ +using Mediator; +using Sentinel.Api.Application.DTO.Device; + +namespace Sentinel.Api.Application.Queries.Devices.Devices; + +public record DevicesQuery(int UserId) : IRequest; diff --git a/src/Sentinel.Api.Application/Queries/Devices/Devices/DevicesQueryHandler.cs b/src/Sentinel.Api.Application/Queries/Devices/Devices/DevicesQueryHandler.cs new file mode 100644 index 0000000..83f3abc --- /dev/null +++ b/src/Sentinel.Api.Application/Queries/Devices/Devices/DevicesQueryHandler.cs @@ -0,0 +1,15 @@ +using Mediator; +using Sentinel.Api.Application.DTO.Device; +using Sentinel.Api.Application.Interfaces; + +namespace Sentinel.Api.Application.Queries.Devices.Devices; + +public class DevicesQueryHandler(IDeviceRepository deviceRepository) + : IRequestHandler +{ + public ValueTask Handle(DevicesQuery request, CancellationToken cancellationToken) + { + var devices = deviceRepository.GetDevices(request.UserId); + return ValueTask.FromResult(devices); + } +} diff --git a/src/Sentinel.Api.Application/Queries/Devices/SecurityInformation/SecurityInformationQuery.cs b/src/Sentinel.Api.Application/Queries/Devices/SecurityInformation/SecurityInformationQuery.cs new file mode 100644 index 0000000..b03e372 --- /dev/null +++ b/src/Sentinel.Api.Application/Queries/Devices/SecurityInformation/SecurityInformationQuery.cs @@ -0,0 +1,6 @@ +using Mediator; +using Sentinel.Common.DTO.Device; + +namespace Sentinel.Api.Application.Queries.Devices.SecurityInformation; + +public record SecurityInformationQuery(int DeviceId) : IRequest; diff --git a/src/Sentinel.Api.Application/Queries/Devices/SecurityInformation/SecurityInformationQueryHandler.cs b/src/Sentinel.Api.Application/Queries/Devices/SecurityInformation/SecurityInformationQueryHandler.cs new file mode 100644 index 0000000..e612b2a --- /dev/null +++ b/src/Sentinel.Api.Application/Queries/Devices/SecurityInformation/SecurityInformationQueryHandler.cs @@ -0,0 +1,15 @@ +using Mediator; +using Sentinel.Api.Application.Interfaces; +using Sentinel.Common.DTO.Device; + +namespace Sentinel.Api.Application.Queries.Devices.SecurityInformation; + +public class SecurityInformationQueryHandler(IDeviceRepository deviceRepository) + : IRequestHandler +{ + public ValueTask Handle(SecurityInformationQuery request, CancellationToken cancellationToken) + { + var securityInfo = deviceRepository.GetSecurityInfo(request.DeviceId); + return ValueTask.FromResult(securityInfo); + } +} diff --git a/src/Sentinel.Api.Application/Queries/Devices/SoftwareInformation/SoftwareInformationQuery.cs b/src/Sentinel.Api.Application/Queries/Devices/SoftwareInformation/SoftwareInformationQuery.cs new file mode 100644 index 0000000..2c1b93b --- /dev/null +++ b/src/Sentinel.Api.Application/Queries/Devices/SoftwareInformation/SoftwareInformationQuery.cs @@ -0,0 +1,6 @@ +using Mediator; +using Sentinel.Common.DTO.Device; + +namespace Sentinel.Api.Application.Queries.Devices.SoftwareInformation; + +public record SoftwareInformationQuery(int DeviceId) : IRequest; diff --git a/src/Sentinel.Api.Application/Queries/Devices/SoftwareInformation/SoftwareInformationQueryHandler.cs b/src/Sentinel.Api.Application/Queries/Devices/SoftwareInformation/SoftwareInformationQueryHandler.cs new file mode 100644 index 0000000..b71f7b5 --- /dev/null +++ b/src/Sentinel.Api.Application/Queries/Devices/SoftwareInformation/SoftwareInformationQueryHandler.cs @@ -0,0 +1,15 @@ +using Mediator; +using Sentinel.Api.Application.Interfaces; +using Sentinel.Common.DTO.Device; + +namespace Sentinel.Api.Application.Queries.Devices.SoftwareInformation; + +public class SoftwareInformationQueryHandler(IDeviceRepository deviceRepository) + : IRequestHandler +{ + public ValueTask Handle(SoftwareInformationQuery request, CancellationToken cancellationToken) + { + var softwareInfo = deviceRepository.GetSoftwareInfo(request.DeviceId); + return ValueTask.FromResult(softwareInfo); + } +} diff --git a/src/Sentinel.Api.Application/Queries/Devices/StorageInformation/StorageInformationQuery.cs b/src/Sentinel.Api.Application/Queries/Devices/StorageInformation/StorageInformationQuery.cs new file mode 100644 index 0000000..2d5fcf3 --- /dev/null +++ b/src/Sentinel.Api.Application/Queries/Devices/StorageInformation/StorageInformationQuery.cs @@ -0,0 +1,6 @@ +using Mediator; +using Sentinel.Common.DTO.Device; + +namespace Sentinel.Api.Application.Queries.Devices.StorageInformation; + +public record StorageInformationQuery(int DeviceId) : IRequest; diff --git a/src/Sentinel.Api.Application/Queries/Devices/StorageInformation/StorageInformationQueryHandler.cs b/src/Sentinel.Api.Application/Queries/Devices/StorageInformation/StorageInformationQueryHandler.cs new file mode 100644 index 0000000..8c4d340 --- /dev/null +++ b/src/Sentinel.Api.Application/Queries/Devices/StorageInformation/StorageInformationQueryHandler.cs @@ -0,0 +1,15 @@ +using Mediator; +using Sentinel.Api.Application.Interfaces; +using Sentinel.Common.DTO.Device; + +namespace Sentinel.Api.Application.Queries.Devices.StorageInformation; + +public class StorageInformationQueryHandler(IDeviceRepository deviceRepository) + : IRequestHandler +{ + public ValueTask Handle(StorageInformationQuery request, CancellationToken cancellationToken) + { + var storageInfo = deviceRepository.GetStorageInfo(request.DeviceId); + return ValueTask.FromResult(storageInfo); + } +} diff --git a/src/Sentinel.Api.Application/Queries/Organisations/GetAllOrganisations/GetAllOrganisationsQuery.cs b/src/Sentinel.Api.Application/Queries/Organisations/GetAllOrganisations/GetAllOrganisationsQuery.cs new file mode 100644 index 0000000..40131a9 --- /dev/null +++ b/src/Sentinel.Api.Application/Queries/Organisations/GetAllOrganisations/GetAllOrganisationsQuery.cs @@ -0,0 +1,6 @@ +using Mediator; +using Sentinel.Api.Domain.Entities; + +namespace Sentinel.Api.Application.Queries.Organisations.GetAllOrganisations; + +public record GetAllOrganisationsQuery : IRequest>; diff --git a/src/Sentinel.Api.Application/Queries/Organisations/GetAllOrganisations/GetAllOrganisationsQueryHandler.cs b/src/Sentinel.Api.Application/Queries/Organisations/GetAllOrganisations/GetAllOrganisationsQueryHandler.cs new file mode 100644 index 0000000..952c7df --- /dev/null +++ b/src/Sentinel.Api.Application/Queries/Organisations/GetAllOrganisations/GetAllOrganisationsQueryHandler.cs @@ -0,0 +1,15 @@ +using Mediator; +using Sentinel.Api.Application.Interfaces; +using Sentinel.Api.Domain.Entities; + +namespace Sentinel.Api.Application.Queries.Organisations.GetAllOrganisations; + +public class GetAllOrganisationsQueryHandler(IOrganisationRepository organisationRepository) + : IRequestHandler> +{ + public ValueTask> Handle(GetAllOrganisationsQuery request, CancellationToken cancellationToken) + { + var organisations = organisationRepository.GetAll(); + return ValueTask.FromResult(organisations); + } +} diff --git a/src/Sentinel.Api.Application/Records/ExceptionResponse.cs b/src/Sentinel.Api.Application/Records/ExceptionResponse.cs new file mode 100644 index 0000000..8099ce0 --- /dev/null +++ b/src/Sentinel.Api.Application/Records/ExceptionResponse.cs @@ -0,0 +1,3 @@ +namespace Sentinel.Api.Application.Records; + +public record ExceptionResponse(int StatusCode, Exception Exception); \ No newline at end of file diff --git a/src/Sentinel.Api.Application/Sentinel.Api.Application.csproj b/src/Sentinel.Api.Application/Sentinel.Api.Application.csproj index c0fc090..e861265 100644 --- a/src/Sentinel.Api.Application/Sentinel.Api.Application.csproj +++ b/src/Sentinel.Api.Application/Sentinel.Api.Application.csproj @@ -7,6 +7,13 @@ + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Sentinel.Api.Application/Services/Interfaces/IJwtTokenGenerator.cs b/src/Sentinel.Api.Application/Services/Interfaces/IJwtTokenGenerator.cs new file mode 100644 index 0000000..86fad9e --- /dev/null +++ b/src/Sentinel.Api.Application/Services/Interfaces/IJwtTokenGenerator.cs @@ -0,0 +1,9 @@ +using System.Security.Claims; + +namespace Sentinel.Api.Application.Services.Interfaces; + +public interface IJwtTokenGenerator +{ + string GenerateAccessToken(IEnumerable claims); + string GenerateRefreshToken(); +} diff --git a/src/Sentinel.Api.Application/Services/Interfaces/ITokenService.cs b/src/Sentinel.Api.Application/Services/Interfaces/ITokenService.cs deleted file mode 100644 index ff687bc..0000000 --- a/src/Sentinel.Api.Application/Services/Interfaces/ITokenService.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Security.Claims; - -namespace Sentinel.Api.Application.Services.Interfaces -{ - public interface ITokenService - { - string GenerateAccessToken(IEnumerable claims); - string GenerateRefreshToken(); - ClaimsPrincipal GetPrincipalFromExpiredToken(string token); - } -} diff --git a/src/Sentinel.Api.Application/Services/TokenGenerator.cs b/src/Sentinel.Api.Application/Services/TokenGenerator.cs new file mode 100644 index 0000000..84639df --- /dev/null +++ b/src/Sentinel.Api.Application/Services/TokenGenerator.cs @@ -0,0 +1,38 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using Sentinel.Api.Application.Services.Interfaces; + +namespace Sentinel.Api.Application.Services; + +public class TokenGenerator(IConfiguration configuration) : IJwtTokenGenerator +{ + public string GenerateAccessToken(IEnumerable claims) + { + var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"]!)); + var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); + + var tokeOptions = new JwtSecurityToken( + issuer: configuration["Jwt:Issuer"], + audience: configuration["Jwt:Audience"], + claims: claims, + expires: DateTime.UtcNow.AddMinutes(3), + signingCredentials: signinCredentials + ); + + var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions); + return tokenString; + } + + public string GenerateRefreshToken() + { + using var rng = RandomNumberGenerator.Create(); + var randomNumber = new byte[32]; + rng.GetBytes(randomNumber); + + return Convert.ToBase64String(randomNumber); + } +} diff --git a/src/Sentinel.Api.Application/Services/TokenService.cs b/src/Sentinel.Api.Application/Services/TokenService.cs deleted file mode 100644 index 3e76421..0000000 --- a/src/Sentinel.Api.Application/Services/TokenService.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Text; -using Microsoft.Extensions.Configuration; -using Microsoft.IdentityModel.Tokens; -using Sentinel.Api.Application.Services.Interfaces; - -namespace Sentinel.Api.Application.Services -{ - public class TokenService(IConfiguration configuration) : ITokenService - { - public string GenerateAccessToken(IEnumerable claims) - { - var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"]!)); - var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); - - var tokeOptions = new JwtSecurityToken( - issuer: configuration["Jwt:Issuer"], - audience: configuration["Jwt:Audience"], - claims: claims, - expires: DateTime.UtcNow.AddMinutes(3), - signingCredentials: signinCredentials - ); - - var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions); - return tokenString; - } - - public string GenerateRefreshToken() - { - var randomNumber = new byte[32]; - using var rng = RandomNumberGenerator.Create(); - rng.GetBytes(randomNumber); - return Convert.ToBase64String(randomNumber); - } - - public ClaimsPrincipal GetPrincipalFromExpiredToken(string token) - { - var tokenValidationParameters = new TokenValidationParameters - { - ValidateAudience = false, //you might want to validate the audience and issuer depending on your use case - ValidateIssuer = false, - ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"]!)), - ValidateLifetime = false //here we are saying that we don't care about the token's expiration date - }; - - var tokenHandler = new JwtSecurityTokenHandler(); - var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken); - if (securityToken is not JwtSecurityToken jwtSecurityToken || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) - throw new SecurityTokenException("Invalid token"); - - return principal; - } - } -} diff --git a/src/Sentinel.Api.Infrastructure/DependencyInjection.cs b/src/Sentinel.Api.Infrastructure/DependencyInjection.cs index 66ab08d..07c54f4 100644 --- a/src/Sentinel.Api.Infrastructure/DependencyInjection.cs +++ b/src/Sentinel.Api.Infrastructure/DependencyInjection.cs @@ -9,104 +9,110 @@ using Sentinel.Api.Application.Interfaces; using Sentinel.Api.Application.Services; using Sentinel.Api.Application.Services.Interfaces; +using Sentinel.Api.Infrastructure.Exceptions; +using Sentinel.Api.Infrastructure.Middleware; using Sentinel.Api.Infrastructure.Persistence; using Sentinel.Api.Infrastructure.Repositories; using Serilog; using Serilog.Events; +using Sentinel.Api.Infrastructure.SignalR; namespace Sentinel.Api.Infrastructure; public static class DependencyInjection { - public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) + extension(IServiceCollection services) { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Information() - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .Enrich.FromLogContext() - .WriteTo.Console() - .CreateLogger(); - - services - .AddCors() - .AddServices() - .AddPersistence(configuration) - .AddAuthenticationAndAuthorization(configuration) - .AddHttpContextAccessor() - .AddAuthorization() - .AddSignalR(); + public IServiceCollection AddInfrastructure(IConfiguration configuration) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); - return services; - } + services + .AddCors() + .AddServices() + .AddPersistence(configuration) + .AddAuthenticationAndAuthorization(configuration) + .AddHttpContextAccessor() + .AddAuthorization() + .AddSignalR(); - private static IServiceCollection AddServices(this IServiceCollection services) - { - services.AddScoped(); - return services; - } + return services; + } - private static IServiceCollection AddPersistence(this IServiceCollection services, IConfiguration configuration) - { - services.AddDbContext(options => + private IServiceCollection AddServices() { - options.UseSqlServer(configuration.GetConnectionString("Database")); - }); + services.AddScoped(); + services.AddScoped(); + return services; + } - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + private IServiceCollection AddPersistence(IConfiguration configuration) + { + services.AddDbContext(options => + { + options.UseSqlServer(configuration.GetConnectionString("Database")); + }); - return services; - } + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } - private static IServiceCollection AddAuthenticationAndAuthorization(this IServiceCollection services, - IConfiguration configuration) - { - services.AddAuthorization(options => - { - options.AddPolicy("User", policy => policy.RequireRole("User")); - options.AddPolicy("Device", policy => policy.RequireRole("Device")); - }) - .AddAuthentication(options => - { - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - }) - .AddJwtBearer(options => - { - options.Events = new JwtBearerEvents + private IServiceCollection AddAuthenticationAndAuthorization(IConfiguration configuration) + { + services.AddAuthorization(options => + { + options.AddPolicy("User", policy => policy.RequireRole("User")); + options.AddPolicy("Device", policy => policy.RequireRole("Device")); + }) + .AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => { - OnChallenge = async context => + options.Events = new JwtBearerEvents { - context.HandleResponse(); - context.Response.StatusCode = 401; - - var token = context.Request.Headers["Authorization"].ToString(); - if (token.Contains("bearer")) + OnChallenge = async context => { - var jwtToken = token.Replace("bearer", "", StringComparison.OrdinalIgnoreCase).Trim(); - if (new JwtSecurityTokenHandler().ReadToken(jwtToken).ValidTo < DateTime.UtcNow) + context.HandleResponse(); + context.Response.StatusCode = 401; + + var token = context.Request.Headers["Authorization"].ToString(); + if (token.Contains("bearer")) { - await context.Response.WriteAsync("Expired JWT"); - return; + var jwtToken = token.Replace("bearer", "", StringComparison.OrdinalIgnoreCase).Trim(); + if (new JwtSecurityTokenHandler().ReadToken(jwtToken).ValidTo < DateTime.UtcNow) + { + await context.Response.WriteAsync("Expired JWT"); + return; + } } + await context.Response.WriteAsync("Invalid JWT"); } - await context.Response.WriteAsync("Invalid JWT"); - } - }; - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidateAudience = true, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - ValidIssuer = configuration["Jwt:Issuer"], - ValidAudience = configuration["Jwt:Audience"], - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"]!)) - }; - }); + }; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = configuration["Jwt:Issuer"], + ValidAudience = configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"]!)) + }; + }); - return services; + return services; + } } } \ No newline at end of file diff --git a/src/Sentinel.Api.Infrastructure/Exceptions/BadRequestException.cs b/src/Sentinel.Api.Infrastructure/Exceptions/BadRequestException.cs index 5d05f68..ad148f8 100644 --- a/src/Sentinel.Api.Infrastructure/Exceptions/BadRequestException.cs +++ b/src/Sentinel.Api.Infrastructure/Exceptions/BadRequestException.cs @@ -1,8 +1,3 @@ -namespace Sentinel.Api.Infrastructure.Exceptions -{ - public class BadRequestException : Exception - { - } -} - +namespace Sentinel.Api.Infrastructure.Exceptions; +public class BadRequestException(string message) : DomainException(message); \ No newline at end of file diff --git a/src/Sentinel.Api.Infrastructure/Exceptions/ForbiddenException.cs b/src/Sentinel.Api.Infrastructure/Exceptions/ForbiddenException.cs index 33dfc92..569fb83 100644 --- a/src/Sentinel.Api.Infrastructure/Exceptions/ForbiddenException.cs +++ b/src/Sentinel.Api.Infrastructure/Exceptions/ForbiddenException.cs @@ -1,6 +1,3 @@ -namespace Sentinel.Api.Infrastructure.Exceptions -{ - public class ForbiddenException : Exception - { - } -} +namespace Sentinel.Api.Infrastructure.Exceptions; + +public class ForbiddenException(string message) : DomainException(message); \ No newline at end of file diff --git a/src/Sentinel.Api.Infrastructure/Exceptions/InternalServerException.cs b/src/Sentinel.Api.Infrastructure/Exceptions/InternalServerException.cs index 5537f5a..86cfecd 100644 --- a/src/Sentinel.Api.Infrastructure/Exceptions/InternalServerException.cs +++ b/src/Sentinel.Api.Infrastructure/Exceptions/InternalServerException.cs @@ -1,6 +1,3 @@ -namespace Sentinel.Api.Infrastructure.Exceptions -{ - public class InternalServerException : Exception - { - } -} +namespace Sentinel.Api.Infrastructure.Exceptions; + +public class InternalServerException(string message) : DomainException(message); \ No newline at end of file diff --git a/src/Sentinel.Api.Infrastructure/Exceptions/NotFoundException.cs b/src/Sentinel.Api.Infrastructure/Exceptions/NotFoundException.cs index 06b3bf9..fc1d0fc 100644 --- a/src/Sentinel.Api.Infrastructure/Exceptions/NotFoundException.cs +++ b/src/Sentinel.Api.Infrastructure/Exceptions/NotFoundException.cs @@ -1,6 +1,3 @@ -namespace Sentinel.Api.Infrastructure.Exceptions -{ - public class NotFoundException : Exception - { - } -} +namespace Sentinel.Api.Infrastructure.Exceptions; + +public class NotFoundException(string message) : DomainException(message); \ No newline at end of file diff --git a/src/Sentinel.Api.Infrastructure/Exceptions/ResponseManager.cs b/src/Sentinel.Api.Infrastructure/Exceptions/ResponseManager.cs deleted file mode 100644 index 2bfc452..0000000 --- a/src/Sentinel.Api.Infrastructure/Exceptions/ResponseManager.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Sentinel.Api.Infrastructure.Exceptions -{ - public class ResponseManager : ControllerBase - { - // Returns status code based on thrown exception - public IActionResult ReturnResponse(Exception ex) - { - return ex.InnerException switch - { - NotFoundException => NotFound(new ResponseMessage - { - Message = ex.Message, - }), - UnauthorizedException => Unauthorized(new ResponseMessage - { - Message = ex.Message, - }), - BadRequestException => BadRequest(new ResponseMessage - { - Message = ex.Message, - }), - ForbiddenException => StatusCode(StatusCodes.Status403Forbidden, new ResponseMessage - { - Message = ex.Message, - }), - InternalServerException => StatusCode(StatusCodes.Status500InternalServerError, new ResponseMessage - { - Message = ex.Message, - }), - _ => StatusCode(StatusCodes.Status500InternalServerError), - }; - } - } -} - diff --git a/src/Sentinel.Api.Infrastructure/Exceptions/ResponseMessage.cs b/src/Sentinel.Api.Infrastructure/Exceptions/ResponseMessage.cs deleted file mode 100644 index c53e38d..0000000 --- a/src/Sentinel.Api.Infrastructure/Exceptions/ResponseMessage.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Sentinel.Api.Infrastructure.Exceptions -{ - public class ResponseMessage - { - public string? Message { get; set; } - } -} diff --git a/src/Sentinel.Api.Infrastructure/Exceptions/UnauthorizedException.cs b/src/Sentinel.Api.Infrastructure/Exceptions/UnauthorizedException.cs index 1a4824e..a0a7f2a 100644 --- a/src/Sentinel.Api.Infrastructure/Exceptions/UnauthorizedException.cs +++ b/src/Sentinel.Api.Infrastructure/Exceptions/UnauthorizedException.cs @@ -1,7 +1,3 @@ -namespace Sentinel.Api.Infrastructure.Exceptions -{ - public class UnauthorizedException : Exception - { - } -} +namespace Sentinel.Api.Infrastructure.Exceptions; +public class UnauthorizedException(string message) : DomainException(message); \ No newline at end of file diff --git a/src/Sentinel.Api.Infrastructure/Middleware/ExceptionHandlingMiddleware.cs b/src/Sentinel.Api.Infrastructure/Middleware/ExceptionHandlingMiddleware.cs new file mode 100644 index 0000000..1de80e5 --- /dev/null +++ b/src/Sentinel.Api.Infrastructure/Middleware/ExceptionHandlingMiddleware.cs @@ -0,0 +1,38 @@ +using FluentValidation; +using Microsoft.AspNetCore.Http; +using Sentinel.Api.Application.Exceptions; +using Sentinel.Api.Infrastructure.Exceptions; +namespace Sentinel.Api.Infrastructure.Middleware; + +public class ExceptionHandlingMiddleware(RequestDelegate next) +{ + public async Task InvokeAsync(HttpContext context) + { + try + { + await next(context); + } + catch (Exception ex) + { + await HandleExceptionAsync(context, ex); + } + } + + private static Task HandleExceptionAsync(HttpContext context, Exception exception) + { + var (statusCode, message) = exception switch + { + NotFoundException ex => (StatusCodes.Status404NotFound, ex.Message), + UnauthorizedException ex => (StatusCodes.Status401Unauthorized, ex.Message), + BadRequestException ex => (StatusCodes.Status400BadRequest, ex.Message), + BadValidationRequest ex => (StatusCodes.Status400BadRequest, ex.Message), + ForbiddenException ex => (StatusCodes.Status403Forbidden, ex.Message), + _ => (StatusCodes.Status500InternalServerError, "An unexpected error occurred.") + }; + + context.Response.ContentType = "application/json"; + context.Response.StatusCode = statusCode; + + return context.Response.WriteAsJsonAsync(new { Error = message }); + } +} \ No newline at end of file diff --git a/src/Sentinel.Api.Infrastructure/Persistence/AppDbContext.cs b/src/Sentinel.Api.Infrastructure/Persistence/AppDbContext.cs index b956cb9..8ad9086 100644 --- a/src/Sentinel.Api.Infrastructure/Persistence/AppDbContext.cs +++ b/src/Sentinel.Api.Infrastructure/Persistence/AppDbContext.cs @@ -1,16 +1,15 @@ using Microsoft.EntityFrameworkCore; using Sentinel.Api.Domain.Entities; -namespace Sentinel.Api.Infrastructure.Persistence +namespace Sentinel.Api.Infrastructure.Persistence; + +public class AppDbContext(DbContextOptions options) : DbContext(options) { - public class AppDbContext(DbContextOptions options) : DbContext(options) - { - public DbSet Organisations { get; init; } - public DbSet Users { get; init; } - public DbSet Devices { get; init; } - public DbSet DeviceDetails { get; init; } - public DbSet DeviceSecurities { get; init; } - public DbSet DeviceDisks { get; init; } - public DbSet DeviceSoftware { get; init; } - } + public DbSet Organisations { get; init; } + public DbSet Users { get; init; } + public DbSet Devices { get; init; } + public DbSet DeviceDetails { get; init; } + public DbSet DeviceSecurities { get; init; } + public DbSet DeviceDisks { get; init; } + public DbSet DeviceSoftware { get; init; } } \ No newline at end of file diff --git a/src/Sentinel.Api.Infrastructure/Repositories/AuthRepository.cs b/src/Sentinel.Api.Infrastructure/Repositories/AuthRepository.cs index e49e12a..707acc6 100644 --- a/src/Sentinel.Api.Infrastructure/Repositories/AuthRepository.cs +++ b/src/Sentinel.Api.Infrastructure/Repositories/AuthRepository.cs @@ -21,10 +21,10 @@ public class AuthRepository(AppDbContext dbContext, IConfiguration configuration public async Task AuthenticateAsync(SignInUserDto user) { var currentUser = await dbContext.Users.FirstOrDefaultAsync(x => x.Email.ToLower().Equals(user.Email.ToLower())); - if (currentUser == null) throw new Exception("Invalid login credentials", new UnauthorizedException()); + if (currentUser == null) throw new UnauthorizedException("Invalid login credentials"); var passwordValid = BCrypt.Net.BCrypt.Verify(user.Password, currentUser.Password); - if (!passwordValid) throw new Exception("Invalid login credentials", new UnauthorizedException()); + if (!passwordValid) throw new UnauthorizedException("Invalid login credentials"); var claims = new List { @@ -67,22 +67,22 @@ public async Task GetTokenAsync(User user) } catch { - throw new Exception($"Failed to create tokens", new InternalServerException()); + throw new InternalServerException($"Failed to create tokens"); } } public async Task VerifyTotpAsync(VerifyUserDto verifyUserDto) { var user = await dbContext.Users.FirstOrDefaultAsync(x => x.Id == verifyUserDto.UserId) ?? - throw new Exception($"Invalid user id", new BadRequestException()); + throw new BadRequestException($"Invalid user id"); if (user.AuthenticityToken != verifyUserDto.AuthenticityToken) - throw new Exception($"Invalid authenticity token", new UnauthorizedException()); + throw new UnauthorizedException($"Invalid authenticity token"); // TODO: check if token is not expired var totp = new Totp(Base32Encoding.ToBytes(user.TwoFactorToken), step: 30, mode: OtpHashMode.Sha1, totpSize: 6); var valid = totp.VerifyTotp(verifyUserDto.OtpAttempt, out var _, window: VerificationWindow.RfcSpecifiedNetworkDelay); - if (!valid) throw new Exception($"Invalid authenticator code", new UnauthorizedException()); + if (!valid) throw new UnauthorizedException($"Invalid authenticator code"); user.LastVerified = DateTime.Now; await dbContext.SaveChangesAsync(); @@ -92,8 +92,8 @@ public async Task VerifyTotpAsync(VerifyUserDto verifyUserDto) public async Task RefreshTokenAsync(TokenDto tokenDto) { var principal = GetPrincipalFromExpiredToken(tokenDto.AccessToken); - var claimId = principal.Claims.FirstOrDefault(c => c.Type == "Id")!.Value; - var role = principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role)!.Value; + var claimId = principal.Claims.SingleOrDefault(c => c.Type == "Id")!.Value; + var role = principal.Claims.SingleOrDefault(c => c.Type == ClaimTypes.Role)!.Value; var newAccessToken = GenerateAccessToken(principal.Claims); var newRefreshToken = GenerateRefreshToken(); @@ -102,10 +102,10 @@ public async Task RefreshTokenAsync(TokenDto tokenDto) { case "Admin": case "User": - var user = dbContext.Users.SingleOrDefault(x => x.Id.ToString() == principal.Claims.FirstOrDefault(c => c.Type == "Id")!.Value); + var user = dbContext.Users.SingleOrDefault(x => x.Id.ToString() == claimId); if (user == null || user.RefreshToken != tokenDto.RefreshToken || user.RefreshTokenExpiryTime <= DateTime.Now) { - throw new Exception("Invalid refresh token", new BadRequestException()); + throw new BadRequestException("Invalid refresh token"); } user.RefreshToken = newRefreshToken; break; @@ -113,7 +113,7 @@ public async Task RefreshTokenAsync(TokenDto tokenDto) var device = dbContext.Devices.SingleOrDefault(x => x.Id.ToString() == claimId); if (device == null || device.RefreshToken != tokenDto.RefreshToken) { - throw new Exception("Invalid refresh token", new BadRequestException()); + throw new BadRequestException("Invalid refresh token"); } device.RefreshToken = newRefreshToken; break; diff --git a/src/Sentinel.Api.Infrastructure/Repositories/DeviceRepository.cs b/src/Sentinel.Api.Infrastructure/Repositories/DeviceRepository.cs index a05c308..c28e31e 100644 --- a/src/Sentinel.Api.Infrastructure/Repositories/DeviceRepository.cs +++ b/src/Sentinel.Api.Infrastructure/Repositories/DeviceRepository.cs @@ -10,256 +10,255 @@ using Sentinel.Common.DTO.Device; using Sentinel.Common.DTO.Device.Information; -namespace Sentinel.Api.Infrastructure.Repositories +namespace Sentinel.Api.Infrastructure.Repositories; + +public class DeviceRepository( + AppDbContext dbContext, + IHttpContextAccessor httpContextAccessor, + IJwtTokenGenerator tokenGenerator) + : IDeviceRepository { - public class DeviceRepository( - AppDbContext dbContext, - IHttpContextAccessor httpContextAccessor, - ITokenService tokenService) - : IDeviceRepository + public DeviceTokenResponse Register(Guid organisationHash, string name) { - public DeviceTokenResponse Register(RegisterDeviceDto registerDeviceDto) - { - var organisation = - dbContext.Organisations.FirstOrDefault(o => o.Hash == registerDeviceDto.OrganisationHash) ?? - throw new Exception("Organisation not found", new NotFoundException()); + var organisation = + dbContext.Organisations.FirstOrDefault(o => o.Hash == organisationHash) ?? + throw new NotFoundException("Organisation not found"); - var device = new Device - { - Name = registerDeviceDto.Name, - CreatedOn = DateTime.Now, - LastActive = DateTime.Now, - RefreshToken = tokenService.GenerateRefreshToken(), - }; - organisation.Devices.Add(device); - dbContext.SaveChanges(); - - var claims = new List - { - new("Id", device.Id.ToString()), - new("Name", device.Name), - new(ClaimTypes.Role, "Device"), - }; - return new DeviceTokenResponse - { - Id = device.Id, - OrganisationId = organisation.Id, - AccessToken = tokenService.GenerateAccessToken(claims), - RefreshToken = device.RefreshToken, - }; - } + var device = new Device + { + Name = name, + CreatedOn = DateTime.Now, + LastActive = DateTime.Now, + RefreshToken = tokenGenerator.GenerateRefreshToken(), + }; + organisation.Devices.Add(device); + dbContext.SaveChanges(); - public GetDevicesResponse GetDevices(int userId) + var claims = new List { - var user = dbContext.Users.Include(user => user.Organisation).Single(x => x.Id == userId); - var devices = dbContext.Devices.Where(x => x.OrganisationId == user.OrganisationId).ToList(); - return new GetDevicesResponse() - { - OrganisationHash = user.Organisation.Hash, - ActiveDevices = - devices.Where(x => x.LastActive >= DateTime.Now.AddMinutes(-2)).ToList() - .Count, // Devices active in last two minutes - TotalDevices = devices.Count, - Status = 0, - Devices = devices - }; - } - - public void Ping(int id) + new("Id", device.Id.ToString()), + new("Name", device.Name), + new(ClaimTypes.Role, "Device"), + }; + return new DeviceTokenResponse { - var device = GetDeviceById(id); - device.LastActive = DateTime.Now; - dbContext.SaveChanges(); - } + Id = device.Id, + OrganisationId = organisation.Id, + AccessToken = tokenGenerator.GenerateAccessToken(claims), + RefreshToken = device.RefreshToken, + }; + } - public GetDeviceInformationDto GetDeviceInformation(int id) + public GetDevicesResponse GetDevices(int userId) + { + var user = dbContext.Users.Include(user => user.Organisation).Single(x => x.Id == userId); + var devices = dbContext.Devices.Where(x => x.OrganisationId == user.OrganisationId).ToList(); + return new GetDevicesResponse() { - var device = dbContext.Devices.Include(device => device.DeviceDetails).FirstOrDefault(x => x.Id == id) ?? - throw new Exception("Device not found", new NotFoundException()); + OrganisationHash = user.Organisation.Hash, + ActiveDevices = + devices.Where(x => x.LastActive >= DateTime.Now.AddMinutes(-2)).ToList() + .Count, // Devices active in last two minutes + TotalDevices = devices.Count, + Status = 0, + Devices = devices + }; + } + + public void Ping(int id) + { + var device = GetDeviceById(id); + device.LastActive = DateTime.Now; + dbContext.SaveChanges(); + } + + public GetDeviceInformationDto GetDeviceInformation(int id) + { + var device = dbContext.Devices.Include(device => device.DeviceDetails).FirstOrDefault(x => x.Id == id) ?? + throw new NotFoundException("Device not found"); - return new GetDeviceInformationDto - { - DeviceName = device.Name, - OsName = device.DeviceDetails.OsName, - OsVersion = device.DeviceDetails.OsVersion, - Version = device.DeviceDetails.Version, + return new GetDeviceInformationDto + { + DeviceName = device.Name, + OsName = device.DeviceDetails.OsName, + OsVersion = device.DeviceDetails.OsVersion, + Version = device.DeviceDetails.Version, - ProductName = device.DeviceDetails.ProductName, - Processor = device.DeviceDetails.Processor, - InstalledRam = device.DeviceDetails.InstalledRam, - GraphicsCard = device.DeviceDetails.GraphicsCard, - Manufacturer = device.DeviceDetails.Manufacturer, - }; - } + ProductName = device.DeviceDetails.ProductName, + Processor = device.DeviceDetails.Processor, + InstalledRam = device.DeviceDetails.InstalledRam, + GraphicsCard = device.DeviceDetails.GraphicsCard, + Manufacturer = device.DeviceDetails.Manufacturer, + }; + } - public void UpdateDeviceInformation(int id, UpdateDeviceInformationDto updateDto) - { - var device = dbContext.Devices.Include(device => device.DeviceDetails).FirstOrDefault(s => s.Id == id) ?? - throw new Exception("Device not found", new NotFoundException()); + public void UpdateDeviceInformation(int id, UpdateDeviceInformationDto updateDto) + { + var device = dbContext.Devices.Include(device => device.DeviceDetails).FirstOrDefault(s => s.Id == id) ?? + throw new NotFoundException("Device not found"); - device.Name = updateDto.DeviceName; - device.DeviceDetails.OsName = updateDto.OsName; - device.DeviceDetails.OsVersion = updateDto.OsVersion; - device.DeviceDetails.Version = updateDto.Version ; - device.DeviceDetails.Processor = updateDto.Processor; - device.DeviceDetails.InstalledRam = updateDto.InstalledRam; - device.DeviceDetails.GraphicsCard = updateDto.GraphicsCard; - device.DeviceDetails.Manufacturer = updateDto.Manufacturer; + device.Name = updateDto.DeviceName; + device.DeviceDetails.OsName = updateDto.OsName; + device.DeviceDetails.OsVersion = updateDto.OsVersion; + device.DeviceDetails.Version = updateDto.Version ; + device.DeviceDetails.Processor = updateDto.Processor; + device.DeviceDetails.InstalledRam = updateDto.InstalledRam; + device.DeviceDetails.GraphicsCard = updateDto.GraphicsCard; + device.DeviceDetails.Manufacturer = updateDto.Manufacturer; - dbContext.SaveChanges(); - } + dbContext.SaveChanges(); + } - public StorageInformation GetStorageInfo(int id) - { - var device = dbContext.Devices.Include(device => device.Disks).FirstOrDefault(x => x.Id == id) ?? - throw new Exception("Device not found", new NotFoundException()); + public StorageInformationDto GetStorageInfo(int id) + { + var device = dbContext.Devices.Include(device => device.Disks).FirstOrDefault(x => x.Id == id) ?? + throw new NotFoundException("Device not found"); - return new StorageInformation() + return new StorageInformationDto() + { + Disks = device.Disks.Select(disk => new DiskInformationDto { - Disks = device.Disks.Select(disk => new DiskInformation - { - Name = disk.Name, - IsOsDisk = disk.IsOsDisk, - Used = disk.Used, - Size = disk.Size - }).ToList() - }; - } + Name = disk.Name, + IsOsDisk = disk.IsOsDisk, + Used = disk.Used, + Size = disk.Size + }).ToList() + }; + } - public void UpdateStorageInfo(int id, StorageInformation updateDto) - { - var device = dbContext.Devices.Include(d => d.Disks).FirstOrDefault(s => s.Id == id) ?? - throw new Exception("Device not found", new NotFoundException()); + public void UpdateStorageInfo(int id, StorageInformationDto updateDto) + { + var device = dbContext.Devices.Include(d => d.Disks).FirstOrDefault(s => s.Id == id) ?? + throw new NotFoundException("Device not found"); - foreach (var updateDtoDisk in updateDto.Disks) + foreach (var updateDtoDisk in updateDto.Disks) + { + var disk = device.Disks.FirstOrDefault(deviceDisk => deviceDisk.Name == updateDtoDisk.Name); + if (disk == null) { - var disk = device.Disks.FirstOrDefault(deviceDisk => deviceDisk.Name == updateDtoDisk.Name); - if (disk == null) - { - device.Disks.Add(new DeviceDisk - { - Name = updateDtoDisk.Name, - Size = updateDtoDisk.Size, - IsOsDisk = updateDtoDisk.IsOsDisk, - Used = updateDtoDisk.Used - }); - } - else + device.Disks.Add(new DeviceDisk { - disk.Name = updateDtoDisk.Name; - disk.Size = updateDtoDisk.Size; - disk.IsOsDisk = updateDtoDisk.IsOsDisk; - disk.Used = updateDtoDisk.Used; - } + Name = updateDtoDisk.Name, + Size = updateDtoDisk.Size, + IsOsDisk = updateDtoDisk.IsOsDisk, + Used = updateDtoDisk.Used + }); + } + else + { + disk.Name = updateDtoDisk.Name; + disk.Size = updateDtoDisk.Size; + disk.IsOsDisk = updateDtoDisk.IsOsDisk; + disk.Used = updateDtoDisk.Used; } - - dbContext.SaveChanges(); } - public SecurityInformation GetSecurityInfo(int id) - { - var device = dbContext.Devices.Include(d => d.DeviceSecurity).FirstOrDefault(x => x.Id == id) ?? - throw new Exception("Device not found", new NotFoundException()); + dbContext.SaveChanges(); + } - var securityInfo = device.DeviceSecurity; - return new SecurityInformation - { - LastSecurityScan = new LastSecurityScan - { - LastScan = securityInfo.LastScan, - Duration = securityInfo.Duration - }, - AntivirusEnabled = securityInfo.AntivirusEnabled, - LastAntivirusUpdate = securityInfo.LastAntivirusUpdate, - LastAntispywareUpdate = securityInfo.LastAntispywareUpdate, - RealTimeProtectionEnabled = securityInfo.RealTimeProtectionEnabled, - NisEnabled = securityInfo.NisEnabled, - TamperProtectionEnabled = securityInfo.TamperProtectionEnabled, - AntispywareEnabled = securityInfo.AntispywareEnabled, - IsVirtualMachine = securityInfo.IsVirtualMachine, - FirewallSettings = new FirewallSettings - { - DomainFirewallEnabled = securityInfo.DomainFirewallEnabled, - PrivateFirewallEnabled = securityInfo.PrivateFirewallEnabled, - PublicFirewallEnabled = securityInfo.PublicFirewallEnabled - } - }; - } + public SecurityInformationDto GetSecurityInfo(int id) + { + var device = dbContext.Devices.Include(d => d.DeviceSecurity).FirstOrDefault(x => x.Id == id) ?? + throw new NotFoundException("Device not found"); - public void UpdateSecurityInfo(int id, SecurityInformation updateDto) + var securityInfo = device.DeviceSecurity; + return new SecurityInformationDto { - var device = dbContext.Devices.Include(d => d.DeviceSecurity).FirstOrDefault(s => s.Id == id) ?? - throw new Exception("Device not found", new NotFoundException()); + LastSecurityScanDto = new LastSecurityScanDto + { + LastScan = securityInfo.LastScan, + Duration = securityInfo.Duration + }, + AntivirusEnabled = securityInfo.AntivirusEnabled, + LastAntivirusUpdate = securityInfo.LastAntivirusUpdate, + LastAntispywareUpdate = securityInfo.LastAntispywareUpdate, + RealTimeProtectionEnabled = securityInfo.RealTimeProtectionEnabled, + NisEnabled = securityInfo.NisEnabled, + TamperProtectionEnabled = securityInfo.TamperProtectionEnabled, + AntispywareEnabled = securityInfo.AntispywareEnabled, + IsVirtualMachine = securityInfo.IsVirtualMachine, + FirewallSettingsDto = new FirewallSettingsDto + { + DomainFirewallEnabled = securityInfo.DomainFirewallEnabled, + PrivateFirewallEnabled = securityInfo.PrivateFirewallEnabled, + PublicFirewallEnabled = securityInfo.PublicFirewallEnabled + } + }; + } - device.DeviceSecurity.LastScan = updateDto.LastSecurityScan.LastScan; - device.DeviceSecurity.Duration = updateDto.LastSecurityScan.Duration; - device.DeviceSecurity.AntivirusEnabled = updateDto.AntivirusEnabled; - device.DeviceSecurity.LastAntivirusUpdate = updateDto.LastAntivirusUpdate; - device.DeviceSecurity.LastAntispywareUpdate = updateDto.LastAntispywareUpdate; - device.DeviceSecurity.RealTimeProtectionEnabled = updateDto.RealTimeProtectionEnabled; - device.DeviceSecurity.NisEnabled = updateDto.NisEnabled; - device.DeviceSecurity.TamperProtectionEnabled = updateDto.TamperProtectionEnabled; - device.DeviceSecurity.AntispywareEnabled = updateDto.AntispywareEnabled; - device.DeviceSecurity.IsVirtualMachine = updateDto.IsVirtualMachine; - device.DeviceSecurity.DomainFirewallEnabled = updateDto.FirewallSettings.DomainFirewallEnabled; - device.DeviceSecurity.PrivateFirewallEnabled = updateDto.FirewallSettings.PrivateFirewallEnabled; - device.DeviceSecurity.PublicFirewallEnabled = updateDto.FirewallSettings.PublicFirewallEnabled; + public void UpdateSecurityInfo(int id, SecurityInformationDto updateDto) + { + var device = dbContext.Devices.Include(d => d.DeviceSecurity).FirstOrDefault(s => s.Id == id) ?? + throw new NotFoundException("Device not found"); - dbContext.SaveChanges(); - } + device.DeviceSecurity.LastScan = updateDto.LastSecurityScanDto.LastScan; + device.DeviceSecurity.Duration = updateDto.LastSecurityScanDto.Duration; + device.DeviceSecurity.AntivirusEnabled = updateDto.AntivirusEnabled; + device.DeviceSecurity.LastAntivirusUpdate = updateDto.LastAntivirusUpdate; + device.DeviceSecurity.LastAntispywareUpdate = updateDto.LastAntispywareUpdate; + device.DeviceSecurity.RealTimeProtectionEnabled = updateDto.RealTimeProtectionEnabled; + device.DeviceSecurity.NisEnabled = updateDto.NisEnabled; + device.DeviceSecurity.TamperProtectionEnabled = updateDto.TamperProtectionEnabled; + device.DeviceSecurity.AntispywareEnabled = updateDto.AntispywareEnabled; + device.DeviceSecurity.IsVirtualMachine = updateDto.IsVirtualMachine; + device.DeviceSecurity.DomainFirewallEnabled = updateDto.FirewallSettingsDto.DomainFirewallEnabled; + device.DeviceSecurity.PrivateFirewallEnabled = updateDto.FirewallSettingsDto.PrivateFirewallEnabled; + device.DeviceSecurity.PublicFirewallEnabled = updateDto.FirewallSettingsDto.PublicFirewallEnabled; - public SoftwareInformation GetSoftwareInfo(int id) - { - var device = dbContext.Devices.Include(d => d.Software).FirstOrDefault(s => s.Id == id) ?? - throw new Exception("Device not found", new NotFoundException()); + dbContext.SaveChanges(); + } - return new SoftwareInformation - { - Software = device.Software.Select(deviceSoftware => new Software - { - Name = deviceSoftware.Name - }).OrderBy(x => x.Name).ToList() - }; - } + public SoftwareInformationDto GetSoftwareInfo(int id) + { + var device = dbContext.Devices.Include(d => d.Software).FirstOrDefault(s => s.Id == id) ?? + throw new NotFoundException("Device not found"); - public void UpdateSoftwareInfo(int id, SoftwareInformation updateDto) + return new SoftwareInformationDto { - var device = dbContext.Devices.Include(d => d.Software).FirstOrDefault(s => s.Id == id) ?? - throw new Exception("Device not found", new NotFoundException()); - - foreach (var updateDtoSoftware in updateDto.Software) + Software = device.Software.Select(deviceSoftware => new SoftwareDto { - var software = device.Software.FirstOrDefault(x => x.Name == updateDtoSoftware.Name); - if (software == null) - { - device.Software.Add(new DeviceSoftware - { - Name = updateDtoSoftware.Name - }); - } - else - { - software.Name = updateDtoSoftware.Name; - } - } + Name = deviceSoftware.Name + }).OrderBy(x => x.Name).ToList() + }; + } - dbContext.SaveChanges(); - } + public void UpdateSoftwareInfo(int id, SoftwareInformationDto updateDto) + { + var device = dbContext.Devices.Include(d => d.Software).FirstOrDefault(s => s.Id == id) ?? + throw new NotFoundException("Device not found"); - private Device GetDeviceById(int id) + foreach (var updateDtoSoftware in updateDto.Software) { - if (httpContextAccessor.HttpContext?.User == null) - throw new Exception($"Unauthorized", new UnauthorizedException()); - _ = int.TryParse(httpContextAccessor.HttpContext.User.Claims.FirstOrDefault(c => c.Type == "Id")?.Value, - out var tokenDeviceId); - if (tokenDeviceId != id) + var software = device.Software.FirstOrDefault(x => x.Name == updateDtoSoftware.Name); + if (software == null) { - throw new Exception("No access to other devices", new ForbiddenException()); + device.Software.Add(new DeviceSoftware + { + Name = updateDtoSoftware.Name + }); + } + else + { + software.Name = updateDtoSoftware.Name; } + } - return dbContext.Devices.FirstOrDefault(x => x.Id == id) ?? - throw new Exception("Device not found", new NotFoundException()); + dbContext.SaveChanges(); + } + + private Device GetDeviceById(int id) + { + if (httpContextAccessor.HttpContext?.User == null) + throw new UnauthorizedException($"Unauthorized"); + _ = int.TryParse(httpContextAccessor.HttpContext.User.Claims.FirstOrDefault(c => c.Type == "Id")?.Value, + out var tokenDeviceId); + if (tokenDeviceId != id) + { + throw new ForbiddenException("No access to other devices"); } + + return dbContext.Devices.FirstOrDefault(x => x.Id == id) ?? + throw new NotFoundException("Device not found"); } } \ No newline at end of file diff --git a/src/Sentinel.Api.Infrastructure/Repositories/OrganisationRepository.cs b/src/Sentinel.Api.Infrastructure/Repositories/OrganisationRepository.cs index 9883fdc..f206d10 100644 --- a/src/Sentinel.Api.Infrastructure/Repositories/OrganisationRepository.cs +++ b/src/Sentinel.Api.Infrastructure/Repositories/OrganisationRepository.cs @@ -3,14 +3,13 @@ using Sentinel.Api.Domain.Entities; using Sentinel.Api.Infrastructure.Persistence; -namespace Sentinel.Api.Infrastructure.Repositories +namespace Sentinel.Api.Infrastructure.Repositories; + +public class OrganisationRepository(AppDbContext dbContext) : IOrganisationRepository { - public class OrganisationRepository(AppDbContext dbContext) : IOrganisationRepository + public List GetAll() { - public List GetAll() - { - var orgs = dbContext.Organisations.Include(x => x.Devices).Include(y => y.Users).ToList(); - return orgs; - } + var orgs = dbContext.Organisations.Include(x => x.Devices).Include(y => y.Users).ToList(); + return orgs; } -} +} \ No newline at end of file diff --git a/src/Sentinel.Api.Infrastructure/Repositories/UserRepository.cs b/src/Sentinel.Api.Infrastructure/Repositories/UserRepository.cs index b1ce319..2240bf2 100644 --- a/src/Sentinel.Api.Infrastructure/Repositories/UserRepository.cs +++ b/src/Sentinel.Api.Infrastructure/Repositories/UserRepository.cs @@ -14,7 +14,7 @@ public async Task Register(RegisterUserDto user) var userExists = dbContext.Users.FirstOrDefault(x => x.Email.ToLower() == user.Email.ToLower()); if (userExists != null) { - throw new Exception("Email already in use", new ForbiddenException()); + throw new ForbiddenException("Email already in use"); } var key = KeyGeneration.GenerateRandomKey(20); diff --git a/src/Sentinel.Api.Infrastructure/Sentinel.Api.Infrastructure.csproj b/src/Sentinel.Api.Infrastructure/Sentinel.Api.Infrastructure.csproj index b6a950d..ba075ec 100644 --- a/src/Sentinel.Api.Infrastructure/Sentinel.Api.Infrastructure.csproj +++ b/src/Sentinel.Api.Infrastructure/Sentinel.Api.Infrastructure.csproj @@ -7,17 +7,23 @@ - - - - + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + diff --git a/src/Sentinel.Api.Infrastructure/SignalR/Interfaces/IDeviceMessageHub.cs b/src/Sentinel.Api.Infrastructure/SignalR/Interfaces/IDeviceMessageHub.cs index 346a1ba..2bab4c2 100644 --- a/src/Sentinel.Api.Infrastructure/SignalR/Interfaces/IDeviceMessageHub.cs +++ b/src/Sentinel.Api.Infrastructure/SignalR/Interfaces/IDeviceMessageHub.cs @@ -1,4 +1,4 @@ -using Sentinel.Common.Messages; +using Sentinel.Common.SignalR; namespace Sentinel.Api.Infrastructure.SignalR.Interfaces; diff --git a/src/Sentinel.Api.Infrastructure/SignalR/SignalRDeviceMessenger.cs b/src/Sentinel.Api.Infrastructure/SignalR/SignalRDeviceMessenger.cs new file mode 100644 index 0000000..f69b8e3 --- /dev/null +++ b/src/Sentinel.Api.Infrastructure/SignalR/SignalRDeviceMessenger.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.SignalR; +using Sentinel.Api.Application.Interfaces; +using Sentinel.Api.Infrastructure.SignalR.Interfaces; +using Sentinel.Common.SignalR; + +namespace Sentinel.Api.Infrastructure.SignalR; + +public class SignalRDeviceMessenger(IHubContext hubContext) + : IDeviceMessenger +{ + public async Task SendSecurityScanRequestAsync(int deviceId, CancellationToken cancellationToken = default) + { + // TODO: Replace UserHandler.ConnectedIds.First() with proper device connection lookup by deviceId + var connectionId = UserHandler.ConnectedIds.First(); + var client = hubContext.Clients.Client(connectionId); + await client.SecurityScanMessage(new SecurityScanMessage()); + } + + public async Task SendRestartRequestAsync(int deviceId, CancellationToken cancellationToken = default) + { + // TODO: Replace UserHandler.ConnectedIds.First() with proper device connection lookup by deviceId + var connectionId = UserHandler.ConnectedIds.First(); + var client = hubContext.Clients.Client(connectionId); + await client.RestartDeviceMessage(new RestartDeviceMessage()); + } + + public async Task SendRemoteAccessRequestAsync(int deviceId, CancellationToken cancellationToken = default) + { + // TODO: Replace UserHandler.ConnectedIds.First() with proper device connection lookup by deviceId + var connectionId = UserHandler.ConnectedIds.First(); + var client = hubContext.Clients.Client(connectionId); + await client.RemoteAccessMessage(new RemoteAccessMessage()); + } +} diff --git a/src/Sentinel.Api/Controllers/AuthController.cs b/src/Sentinel.Api/Controllers/AuthController.cs index f0e13c9..3da398a 100644 --- a/src/Sentinel.Api/Controllers/AuthController.cs +++ b/src/Sentinel.Api/Controllers/AuthController.cs @@ -1,64 +1,35 @@ -using Microsoft.AspNetCore.Authorization; +using Mediator; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Sentinel.Api.Application.DTO.Token; -using Sentinel.Api.Application.DTO.User; -using Sentinel.Api.Application.Interfaces; -using Sentinel.Api.Infrastructure.Exceptions; +using Sentinel.Api.Application.Commands.Auth.Login; +using Sentinel.Api.Application.Commands.Auth.RefreshToken; +using Sentinel.Api.Application.Commands.Auth.VerifyTotp; namespace Sentinel.Api.Controllers; [ApiController] [Route("/auth")] -public class AuthController(IAuthRepository authRepository) : ControllerBase +public class AuthController(ISender sender) : ControllerBase { [HttpPost("users/sign_in")] - public async Task Authenticate([FromBody] SignInUserDto singInUserDto) + public async Task Authenticate([FromBody] LoginCommand command) { - if (!ModelState.IsValid) - return BadRequest(ModelState); - - try - { - return Ok(await authRepository.AuthenticateAsync(singInUserDto)); - } - catch (Exception ex) - { - return new ResponseManager().ReturnResponse(ex); - } + var result = await sender.Send(command); + return Ok(result); } [HttpPost("users/verify")] - public async Task VerifyTotp([FromBody] VerifyUserDto verifyUserDto) + public async Task VerifyTotp([FromBody] VerifyTotpCommand command) { - if (!ModelState.IsValid) - return BadRequest(ModelState); - - try - { - var user = await authRepository.VerifyTotpAsync(verifyUserDto); - return Ok(await authRepository.GetTokenAsync(user)); - } - catch (Exception ex) - { - return new ResponseManager().ReturnResponse(ex); - } + var result = await sender.Send(command); + return Ok(result); } [HttpPost("refresh")] [AllowAnonymous] - public async Task RefreshToken(TokenDto tokenDto) + public async Task RefreshToken([FromBody] RefreshTokenCommand command) { - if (!ModelState.IsValid) - return BadRequest(ModelState); - - try - { - var response = await authRepository.RefreshTokenAsync(tokenDto); - return Ok(response); - } - catch (Exception ex) - { - return new ResponseManager().ReturnResponse(ex); - } + var result = await sender.Send(command); + return Ok(result); } } \ No newline at end of file diff --git a/src/Sentinel.Api/Controllers/DeviceAdminController.cs b/src/Sentinel.Api/Controllers/DeviceAdminController.cs index 8bf6f09..f6c5b9f 100644 --- a/src/Sentinel.Api/Controllers/DeviceAdminController.cs +++ b/src/Sentinel.Api/Controllers/DeviceAdminController.cs @@ -1,122 +1,77 @@ +using Mediator; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.SignalR; -using Sentinel.Api.Application.Interfaces; +using Sentinel.Api.Application.Commands.Devices.ExecuteSecurityScan; +using Sentinel.Api.Application.Commands.Devices.RequestRemoteAccess; +using Sentinel.Api.Application.Commands.Devices.Restart; +using Sentinel.Api.Application.Queries.Devices.DeviceInformation; +using Sentinel.Api.Application.Queries.Devices.Devices; +using Sentinel.Api.Application.Queries.Devices.SecurityInformation; +using Sentinel.Api.Application.Queries.Devices.SoftwareInformation; +using Sentinel.Api.Application.Queries.Devices.StorageInformation; using Sentinel.Api.Extensions; -using Sentinel.Api.Infrastructure.Exceptions; -using Sentinel.Api.Infrastructure.SignalR; -using Sentinel.Api.Infrastructure.SignalR.Interfaces; -using Sentinel.Common.Messages; -namespace Sentinel.Api.Controllers +namespace Sentinel.Api.Controllers; + +[ApiController] +[Authorize(Roles = "User")] +[Route("/devices")] +public class DeviceAdminController(ISender sender) : ControllerBase { - [ApiController] - [Authorize(Roles = "User")] - [Route("/devices")] - public class DeviceAdminController(IHubContext deviceMessageContext, IDeviceRepository deviceRepository) : ControllerBase + [HttpGet] + public async Task GetDevices() { - [HttpGet] - public IActionResult GetDevices() - { - if (!ModelState.IsValid) - return BadRequest(ModelState); - - try - { - var userId = User.GetId(); - return Ok(deviceRepository.GetDevices(userId)); - } - catch (Exception ex) - { - return new ResponseManager().ReturnResponse(ex); - } - } - - [HttpGet("{id}")] - public IActionResult GetDeviceInfo([FromRoute] int id) - { - if (!ModelState.IsValid) - return BadRequest(ModelState); - - try - { - return Ok(deviceRepository.GetDeviceInformation(id)); - } - catch (Exception ex) - { - return new ResponseManager().ReturnResponse(ex); - } - } + var userId = User.GetId(); + var result = await sender.Send(new DevicesQuery(userId)); + return Ok(result); + } - [HttpGet("{id}/storage")] - public IActionResult GetStorageInfo([FromRoute] int id) - { - if (!ModelState.IsValid) - return BadRequest(ModelState); + [HttpGet("{id}")] + public async Task GetDeviceInfo([FromRoute] int id) + { + var result = await sender.Send(new DeviceInformationQuery(id)); + return Ok(result); + } - try - { - return Ok(deviceRepository.GetStorageInfo(id)); - } - catch (Exception ex) - { - return new ResponseManager().ReturnResponse(ex); - } - } + [HttpGet("{id}/storage")] + public async Task GetStorageInfo([FromRoute] int id) + { + var result = await sender.Send(new StorageInformationQuery(id)); + return Ok(result); + } - [HttpGet("{id}/security")] - public IActionResult GetSecurityInfo([FromRoute] int id) - { - if (!ModelState.IsValid) - return BadRequest(ModelState); - - try - { - return Ok(deviceRepository.GetSecurityInfo(id)); - } - catch (Exception ex) - { - return new ResponseManager().ReturnResponse(ex); - } - } + [HttpGet("{id}/security")] + public async Task GetSecurityInfo([FromRoute] int id) + { + var result = await sender.Send(new SecurityInformationQuery(id)); + return Ok(result); + } - [HttpGet("{id}/software")] - public IActionResult GetSoftwareInfo([FromRoute] int id) - { - if (!ModelState.IsValid) - return BadRequest(ModelState); - - try - { - return Ok(deviceRepository.GetSoftwareInfo(id)); - } - catch (Exception ex) - { - return new ResponseManager().ReturnResponse(ex); - } - } + [HttpGet("{id}/software")] + public async Task GetSoftwareInfo([FromRoute] int id) + { + var result = await sender.Send(new SoftwareInformationQuery(id)); + return Ok(result); + } - [HttpPost("remoteAccess/{id}")] - public async Task RemoteAccessRequest([FromRoute] int id) - { - var client = deviceMessageContext.Clients.Client(UserHandler.ConnectedIds.First()); - var response = await client.RemoteAccessMessage(new RemoteAccessMessage()); - return Ok(response); - } + [HttpPost("remoteAccess/{id}")] + public async Task RemoteAccessRequest([FromRoute] int id) + { + await sender.Send(new RequestRemoteAccessCommand(id)); + return Ok(); + } - [HttpPost("securityScan/{id}")] - public async Task ExecuteSecurityScan([FromRoute] int id) - { - var client = deviceMessageContext.Clients.Client(UserHandler.ConnectedIds.First()); - await client.SecurityScanMessage(new SecurityScanMessage()); - return Ok(); - } + [HttpPost("securityScan/{id}")] + public async Task ExecuteSecurityScan([FromRoute] int id) + { + await sender.Send(new ExecuteSecurityScanCommand(id)); + return Ok(); + } - [HttpPost("restartDevice/{id}")] - public async Task RestartDevice([FromRoute] int id) - { - await deviceMessageContext.Clients.Client(UserHandler.ConnectedIds.First()).RestartDeviceMessage(new RestartDeviceMessage()); - return Ok(); - } + [HttpPost("restartDevice/{id}")] + public async Task RestartDevice([FromRoute] int id) + { + await sender.Send(new RestartDeviceCommand(id)); + return Ok(); } -} +} \ No newline at end of file diff --git a/src/Sentinel.Api/Controllers/DeviceController.cs b/src/Sentinel.Api/Controllers/DeviceController.cs index e38b59e..3defb30 100644 --- a/src/Sentinel.Api/Controllers/DeviceController.cs +++ b/src/Sentinel.Api/Controllers/DeviceController.cs @@ -1,8 +1,14 @@ -using Microsoft.AspNetCore.Authorization; +using Mediator; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Sentinel.Api.Application.Commands.Devices.Ping; +using Sentinel.Api.Application.Commands.Devices.Register; +using Sentinel.Api.Application.Commands.Devices.Update.DeviceInformation; +using Sentinel.Api.Application.Commands.Devices.Update.SecurityInformation; +using Sentinel.Api.Application.Commands.Devices.Update.SoftwareInformation; +using Sentinel.Api.Application.Commands.Devices.Update.StorageInformation; using Sentinel.Api.Application.DTO.Device; -using Sentinel.Api.Application.Interfaces; -using Sentinel.Api.Infrastructure.Exceptions; +using Sentinel.Api.Extensions; using Sentinel.Common.DTO.Device; using Sentinel.Common.DTO.Device.Information; @@ -11,108 +17,48 @@ namespace Sentinel.Api.Controllers; [ApiController] [Route("/devices")] [Authorize(Roles = "Device")] -public class DeviceController(IDeviceRepository deviceRepository) : ControllerBase +public class DeviceController(ISender sender) : ControllerBase { [HttpPost("register")] [AllowAnonymous] - public IActionResult RegisterDevice(RegisterDeviceDto deviceDto) + public async Task RegisterDevice([FromBody] RegisterDeviceDto registerDeviceDto) { - if (!ModelState.IsValid) - return BadRequest(ModelState); - - try - { - var response = deviceRepository.Register(deviceDto); - return Ok(response); - } - catch (Exception ex) - { - return new ResponseManager().ReturnResponse(ex); - } + var response = await sender.Send(new RegisterDeviceCommand(registerDeviceDto.OrganisationHash, registerDeviceDto.Name)); + return Ok(response); } [HttpPost("{id}/ping")] - public IActionResult Ping(int id) + public async Task Ping(int id) { - if (!ModelState.IsValid) - return BadRequest(ModelState); - - try - { - deviceRepository.Ping(id); - return Ok(); - } - catch (Exception ex) - { - return new ResponseManager().ReturnResponse(ex); - } + await sender.Send(new PingDeviceCommand(id)); + return Ok(); } [HttpPut("{id}")] - public IActionResult UpdateDeviceInfo(int id, UpdateDeviceInformationDto updateDto) + public async Task UpdateDeviceInfo(int id, [FromBody] UpdateDeviceInformationDto updateDto) { - if (!ModelState.IsValid) - return BadRequest(ModelState); - - try - { - deviceRepository.UpdateDeviceInformation(id, updateDto); - return Ok(); - } - catch (Exception ex) - { - return new ResponseManager().ReturnResponse(ex); - } + await sender.Send(new UpdateDeviceInformationCommand(id, updateDto)); + return Ok(); } [HttpPut("{id}/storage")] - public IActionResult UpdateStorageInfo(int id, StorageInformation updateDto) + public async Task UpdateStorageInfo(int id, [FromBody] StorageInformationDto updateDto) { - if (!ModelState.IsValid) - return BadRequest(ModelState); - - try - { - deviceRepository.UpdateStorageInfo(id, updateDto); - return Ok(); - } - catch (Exception ex) - { - return new ResponseManager().ReturnResponse(ex); - } + await sender.Send(new UpdateStorageInformationCommand(id, updateDto)); + return Ok(); } [HttpPut("{id}/security")] - public IActionResult UpdateSecurityInfo(int id, SecurityInformation updateDto) + public async Task UpdateSecurityInfo(int id, [FromBody] SecurityInformationDto updateDto) { - if (!ModelState.IsValid) - return BadRequest(ModelState); - - try - { - deviceRepository.UpdateSecurityInfo(id, updateDto); - return Ok(); - } - catch (Exception ex) - { - return new ResponseManager().ReturnResponse(ex); - } + await sender.Send(new UpdateSecurityInformationCommand(id, updateDto)); + return Ok(); } [HttpPut("{id}/software")] - public IActionResult UpdateSoftwareInfo(int id, SoftwareInformation updateDto) + public async Task UpdateSoftwareInfo(int id, [FromBody] SoftwareInformationDto updateDto) { - if (!ModelState.IsValid) - return BadRequest(ModelState); - - try - { - deviceRepository.UpdateSoftwareInfo(id, updateDto); - return Ok(); - } - catch (Exception ex) - { - return new ResponseManager().ReturnResponse(ex); - } + await sender.Send(new UpdateSoftwareInformationCommand(id, updateDto)); + return Ok(); } } \ No newline at end of file diff --git a/src/Sentinel.Api/Controllers/OrganisationController.cs b/src/Sentinel.Api/Controllers/OrganisationController.cs index abc473f..98877d5 100644 --- a/src/Sentinel.Api/Controllers/OrganisationController.cs +++ b/src/Sentinel.Api/Controllers/OrganisationController.cs @@ -1,7 +1,7 @@ -using Microsoft.AspNetCore.Authorization; +using Mediator; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Sentinel.Api.Application.Interfaces; -using Sentinel.Api.Infrastructure.Exceptions; +using Sentinel.Api.Application.Queries.Organisations.GetAllOrganisations; namespace Sentinel.Api.Controllers; @@ -9,21 +9,12 @@ namespace Sentinel.Api.Controllers; [ApiController] [Authorize(Roles = "User")] [Route("/organisations")] -public class OrganisationController(IOrganisationRepository organisationRepository) : Controller +public class OrganisationController(ISender sender) : Controller { [HttpGet] - public IActionResult GetOrganisations() + public async Task GetOrganisations() { - if (!ModelState.IsValid) - return BadRequest(ModelState); - - try - { - return Ok(organisationRepository.GetAll()); - } - catch (Exception ex) - { - return new ResponseManager().ReturnResponse(ex); - } + var result = await sender.Send(new GetAllOrganisationsQuery()); + return Ok(result); } } diff --git a/src/Sentinel.Api/Controllers/UserController.cs b/src/Sentinel.Api/Controllers/UserController.cs index 8a0a1ea..28395ec 100644 --- a/src/Sentinel.Api/Controllers/UserController.cs +++ b/src/Sentinel.Api/Controllers/UserController.cs @@ -1,28 +1,17 @@ -using Microsoft.AspNetCore.Mvc; -using Sentinel.Api.Application.DTO.User; -using Sentinel.Api.Application.Interfaces; -using Sentinel.Api.Infrastructure.Exceptions; +using Mediator; +using Microsoft.AspNetCore.Mvc; +using Sentinel.Api.Application.Commands.Users.RegisterUser; namespace Sentinel.Api.Controllers; [ApiController] [Route("/users")] -public class UserController(IUserRepository userRepository) : ControllerBase +public class UserController(ISender sender) : ControllerBase { [HttpPost("register")] - public async Task Register([FromBody] RegisterUserDto user) + public async Task Register([FromBody] RegisterUserCommand command) { - if (!ModelState.IsValid) - return BadRequest(ModelState); - - try - { - await userRepository.Register(user); - return Ok(); - } - catch (Exception ex) - { - return new ResponseManager().ReturnResponse(ex); - } + await sender.Send(command); + return Ok(); } } diff --git a/src/Sentinel.Api/Program.cs b/src/Sentinel.Api/Program.cs index b384345..1cfd757 100644 --- a/src/Sentinel.Api/Program.cs +++ b/src/Sentinel.Api/Program.cs @@ -4,6 +4,7 @@ using Sentinel.Api.Application; using Sentinel.Api.Extensions; using Sentinel.Api.Infrastructure; +using Sentinel.Api.Infrastructure.Middleware; using Sentinel.Api.Infrastructure.Persistence; using Sentinel.Api.Infrastructure.SignalR; using Serilog; @@ -20,6 +21,7 @@ Log.Information("Starting host"); var app = builder.Build(); + app.UseMiddleware(); if (app.Environment.IsDevelopment()) { app.MapOpenApi(); @@ -62,5 +64,5 @@ namespace Sentinel.Api { - public class Program; + public abstract class Program; } \ No newline at end of file diff --git a/src/Sentinel.Common/DTO/Device/SecurityInformation.cs b/src/Sentinel.Common/DTO/Device/SecurityInformationDto.cs similarity index 74% rename from src/Sentinel.Common/DTO/Device/SecurityInformation.cs rename to src/Sentinel.Common/DTO/Device/SecurityInformationDto.cs index ce52729..b5ee2d4 100644 --- a/src/Sentinel.Common/DTO/Device/SecurityInformation.cs +++ b/src/Sentinel.Common/DTO/Device/SecurityInformationDto.cs @@ -1,8 +1,8 @@ namespace Sentinel.Common.DTO.Device; -public class SecurityInformation +public class SecurityInformationDto { - public required LastSecurityScan LastSecurityScan { get; set; } + public required LastSecurityScanDto LastSecurityScanDto { get; set; } public bool AntivirusEnabled { get; set; } public DateTime? LastAntivirusUpdate { get; set; } public DateTime? LastAntispywareUpdate { get; set; } @@ -12,16 +12,16 @@ public class SecurityInformation public bool AntispywareEnabled { get; set; } public bool IsVirtualMachine { get; set; } - public FirewallSettings FirewallSettings { get; set; } = null!; + public FirewallSettingsDto FirewallSettingsDto { get; set; } = null!; } -public class LastSecurityScan +public class LastSecurityScanDto { public DateTime? LastScan { get; set; } public TimeSpan? Duration { get; set; } } -public class FirewallSettings +public class FirewallSettingsDto { public bool DomainFirewallEnabled { get; set; } public bool PrivateFirewallEnabled { get; set; } diff --git a/src/Sentinel.Common/DTO/Device/SoftwareInformation.cs b/src/Sentinel.Common/DTO/Device/SoftwareInformation.cs deleted file mode 100644 index a2e93ad..0000000 --- a/src/Sentinel.Common/DTO/Device/SoftwareInformation.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Sentinel.Common.DTO.Device; - -public class SoftwareInformation -{ - public List Software { get; set; } = []; -} - - -public class Software -{ - public required string Name { get; set; } -} \ No newline at end of file diff --git a/src/Sentinel.Common/DTO/Device/SoftwareInformationDto.cs b/src/Sentinel.Common/DTO/Device/SoftwareInformationDto.cs new file mode 100644 index 0000000..ff4fd32 --- /dev/null +++ b/src/Sentinel.Common/DTO/Device/SoftwareInformationDto.cs @@ -0,0 +1,12 @@ +namespace Sentinel.Common.DTO.Device; + +public class SoftwareInformationDto +{ + public List Software { get; set; } = []; +} + + +public class SoftwareDto +{ + public required string Name { get; set; } +} \ No newline at end of file diff --git a/src/Sentinel.Common/DTO/Device/StorageInformation.cs b/src/Sentinel.Common/DTO/Device/StorageInformationDto.cs similarity index 62% rename from src/Sentinel.Common/DTO/Device/StorageInformation.cs rename to src/Sentinel.Common/DTO/Device/StorageInformationDto.cs index bd5ff25..3eeb528 100644 --- a/src/Sentinel.Common/DTO/Device/StorageInformation.cs +++ b/src/Sentinel.Common/DTO/Device/StorageInformationDto.cs @@ -1,11 +1,11 @@ namespace Sentinel.Common.DTO.Device; -public class StorageInformation +public class StorageInformationDto { - public List Disks { get; set; } = []; + public List Disks { get; set; } = []; } -public class DiskInformation +public class DiskInformationDto { public string? Name { get; set; } public bool IsOsDisk { get; set; } = false; diff --git a/src/Sentinel.Common/Messages/RemoteAccessMessage.cs b/src/Sentinel.Common/SignalR/RemoteAccessMessage.cs similarity index 74% rename from src/Sentinel.Common/Messages/RemoteAccessMessage.cs rename to src/Sentinel.Common/SignalR/RemoteAccessMessage.cs index 7c582e3..093cc74 100644 --- a/src/Sentinel.Common/Messages/RemoteAccessMessage.cs +++ b/src/Sentinel.Common/SignalR/RemoteAccessMessage.cs @@ -1,4 +1,4 @@ -namespace Sentinel.Common.Messages; +namespace Sentinel.Common.SignalR; public sealed record RemoteAccessMessage; public sealed record RemoteAccessResponseMessage(string ConnectionId); \ No newline at end of file diff --git a/src/Sentinel.Common/Messages/RestartDeviceMessage.cs b/src/Sentinel.Common/SignalR/RestartDeviceMessage.cs similarity index 52% rename from src/Sentinel.Common/Messages/RestartDeviceMessage.cs rename to src/Sentinel.Common/SignalR/RestartDeviceMessage.cs index b2f361f..a02f150 100644 --- a/src/Sentinel.Common/Messages/RestartDeviceMessage.cs +++ b/src/Sentinel.Common/SignalR/RestartDeviceMessage.cs @@ -1,3 +1,3 @@ -namespace Sentinel.Common.Messages; +namespace Sentinel.Common.SignalR; public sealed record RestartDeviceMessage; \ No newline at end of file diff --git a/src/Sentinel.Common/Messages/SecurityScanMessage.cs b/src/Sentinel.Common/SignalR/SecurityScanMessage.cs similarity index 51% rename from src/Sentinel.Common/Messages/SecurityScanMessage.cs rename to src/Sentinel.Common/SignalR/SecurityScanMessage.cs index 4c74ea4..48e843c 100644 --- a/src/Sentinel.Common/Messages/SecurityScanMessage.cs +++ b/src/Sentinel.Common/SignalR/SecurityScanMessage.cs @@ -1,3 +1,3 @@ -namespace Sentinel.Common.Messages; +namespace Sentinel.Common.SignalR; public sealed record SecurityScanMessage; \ No newline at end of file diff --git a/src/Sentinel.WorkerService.Common/Api/AuthenticationDelegatingHandler.cs b/src/Sentinel.WorkerService.Common/Api/AuthenticationDelegatingHandler.cs index c30e23e..313f1ad 100644 --- a/src/Sentinel.WorkerService.Common/Api/AuthenticationDelegatingHandler.cs +++ b/src/Sentinel.WorkerService.Common/Api/AuthenticationDelegatingHandler.cs @@ -3,8 +3,8 @@ using System.Text; using System.Text.Json; using Microsoft.Extensions.Configuration; +using Sentinel.WorkerService.Common.Api.Extensions; using Sentinel.WorkerService.Common.DTO; -using Sentinel.WorkerService.Common.Extensions; using Sentinel.WorkerService.Common.Services.Interfaces; namespace Sentinel.WorkerService.Common.Api; diff --git a/src/Sentinel.WorkerService.Common/Api/AuthenticationHandler.cs b/src/Sentinel.WorkerService.Common/Api/AuthenticationHandler.cs index 9ba6fe1..5eced17 100644 --- a/src/Sentinel.WorkerService.Common/Api/AuthenticationHandler.cs +++ b/src/Sentinel.WorkerService.Common/Api/AuthenticationHandler.cs @@ -7,7 +7,7 @@ namespace Sentinel.WorkerService.Common.Api; public class AuthenticationHandler(SentinelApiService apiService, ICredentialManager credentialManager) : IAuthenticationHandler { - public async Task EnsureAuthenticated(Guid organisationHash, string name, + public async Task EnsureAuthenticatedAsync(Guid organisationHash, string name, CancellationToken cancellationToken) { var deviceToken = await credentialManager.GetDeviceDetailsAsync() diff --git a/src/Sentinel.WorkerService.Common/Api/Extensions/HttpClientExtensions.cs b/src/Sentinel.WorkerService.Common/Api/Extensions/HttpClientExtensions.cs index 413c3eb..4d35052 100644 --- a/src/Sentinel.WorkerService.Common/Api/Extensions/HttpClientExtensions.cs +++ b/src/Sentinel.WorkerService.Common/Api/Extensions/HttpClientExtensions.cs @@ -5,15 +5,18 @@ namespace Sentinel.WorkerService.Common.Api.Extensions; public static class HttpClientExtensions { - public static async Task PostAsync(this HttpClient client, string requestUri, object? payload = null, CancellationToken cancellationToken = default) + extension(HttpClient client) { - var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); - return await client.PostAsync(requestUri, content, cancellationToken); - } - - public static async Task PutAsync(this HttpClient client, string requestUri, object payload, CancellationToken cancellationToken = default) - { - var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); - return await client.PutAsync(requestUri, content, cancellationToken); + public async Task PostAsync(string requestUri, object? payload = null, CancellationToken cancellationToken = default) + { + var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); + return await client.PostAsync(requestUri, content, cancellationToken); + } + + public async Task PutAsync(string requestUri, object payload, CancellationToken cancellationToken = default) + { + var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); + return await client.PutAsync(requestUri, content, cancellationToken); + } } } \ No newline at end of file diff --git a/src/Sentinel.WorkerService.Common/Extensions/HttpContentExtensions.cs b/src/Sentinel.WorkerService.Common/Api/Extensions/HttpContentExtensions.cs similarity index 89% rename from src/Sentinel.WorkerService.Common/Extensions/HttpContentExtensions.cs rename to src/Sentinel.WorkerService.Common/Api/Extensions/HttpContentExtensions.cs index 614a300..b5fa1b6 100644 --- a/src/Sentinel.WorkerService.Common/Extensions/HttpContentExtensions.cs +++ b/src/Sentinel.WorkerService.Common/Api/Extensions/HttpContentExtensions.cs @@ -1,7 +1,7 @@ using System.Net.Http.Json; using System.Text.Json; -namespace Sentinel.WorkerService.Common.Extensions; +namespace Sentinel.WorkerService.Common.Api.Extensions; public static class HttpContentExtensions { diff --git a/src/Sentinel.WorkerService.Common/Api/Interfaces/IAuthenticationHandler.cs b/src/Sentinel.WorkerService.Common/Api/Interfaces/IAuthenticationHandler.cs index 4fd2692..0aa4dfd 100644 --- a/src/Sentinel.WorkerService.Common/Api/Interfaces/IAuthenticationHandler.cs +++ b/src/Sentinel.WorkerService.Common/Api/Interfaces/IAuthenticationHandler.cs @@ -4,5 +4,5 @@ namespace Sentinel.WorkerService.Common.Api.Interfaces; public interface IAuthenticationHandler { - public Task EnsureAuthenticated(Guid organisationHash, string name, CancellationToken cancellationToken); + public Task EnsureAuthenticatedAsync(Guid organisationHash, string name, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Sentinel.WorkerService.Common/Api/SentinelApiService.cs b/src/Sentinel.WorkerService.Common/Api/SentinelApiService.cs index d385198..de49c09 100644 --- a/src/Sentinel.WorkerService.Common/Api/SentinelApiService.cs +++ b/src/Sentinel.WorkerService.Common/Api/SentinelApiService.cs @@ -4,7 +4,6 @@ using Sentinel.Common.DTO.Device.Information; using Sentinel.WorkerService.Common.Api.Extensions; using Sentinel.WorkerService.Common.DTO; -using Sentinel.WorkerService.Common.Extensions; namespace Sentinel.WorkerService.Common.Api; @@ -22,7 +21,7 @@ public class SentinelApiService(HttpClient client, IConfiguration configuration, } catch (Exception ex) { - logger.LogError(ex.Message); + logger.LogError(ex, "Failed to register device. OrganisationHash: {OrganisationHash}, Name: {Name}", organisationHash, name); return null; } } @@ -39,21 +38,21 @@ public async Task UpdateDeviceInformationAsync(GetDeviceInformationDto getDevice result.EnsureSuccessStatusCode(); } - public async Task UpdateStorageInformationAsync(StorageInformation storageInformation) + public async Task UpdateStorageInformationAsync(StorageInformationDto storageInformationDto) { - var result = await client.PutAsync($"/devices/{configuration["Id"]}/storage", storageInformation); + var result = await client.PutAsync($"/devices/{configuration["Id"]}/storage", storageInformationDto); result.EnsureSuccessStatusCode(); } - public async Task UpdateSecurityInformationAsync(SecurityInformation securityInformation) + public async Task UpdateSecurityInformationAsync(SecurityInformationDto securityInformationDto) { - var result = await client.PutAsync($"/devices/{configuration["Id"]}/security", securityInformation); + var result = await client.PutAsync($"/devices/{configuration["Id"]}/security", securityInformationDto); result.EnsureSuccessStatusCode(); } - public async Task UpdateSoftwareInformationAsync(SoftwareInformation softwareInformation) + public async Task UpdateSoftwareInformationAsync(SoftwareInformationDto softwareInformationDto) { - var result = await client.PutAsync($"/devices/{configuration["Id"]}/software", softwareInformation); + var result = await client.PutAsync($"/devices/{configuration["Id"]}/software", softwareInformationDto); result.EnsureSuccessStatusCode(); } } \ No newline at end of file diff --git a/src/Sentinel.WorkerService.Common/Consumer/ConsumerBase.cs b/src/Sentinel.WorkerService.Common/Consumer/ConsumerBase.cs index dae7280..d6dfb52 100644 --- a/src/Sentinel.WorkerService.Common/Consumer/ConsumerBase.cs +++ b/src/Sentinel.WorkerService.Common/Consumer/ConsumerBase.cs @@ -8,7 +8,7 @@ namespace Sentinel.WorkerService.Common.Consumer; public abstract class ConsumerBase : IHostedService, IModule { - public abstract Task OnMessageReceived(TMessage context); + protected abstract Task OnMessageReceived(TMessage context); protected ConsumerBase(IConsumerConfig config, ILogger logger) { @@ -17,7 +17,7 @@ protected ConsumerBase(IConsumerConfig config, ILogger logge config.Connection.On(messageName, (TMessage message) => { - logger.LogInformation($"[*] {messageName} received"); + logger.LogInformation("[*] {MessageName} received", messageName); OnMessageReceived(message); }); } diff --git a/src/Sentinel.WorkerService.Common/Consumer/Interfaces/IConsumerConfig.cs b/src/Sentinel.WorkerService.Common/Consumer/Interfaces/IConsumerConfig.cs index 5fe8013..9490dc2 100644 --- a/src/Sentinel.WorkerService.Common/Consumer/Interfaces/IConsumerConfig.cs +++ b/src/Sentinel.WorkerService.Common/Consumer/Interfaces/IConsumerConfig.cs @@ -2,7 +2,6 @@ namespace Sentinel.WorkerService.Common.Consumer.Interfaces; -// ReSharper disable once UnusedTypeParameter public interface IConsumerConfig { HubConnection? Connection { get; set; } diff --git a/src/Sentinel.WorkerService.Common/Module/Interfaces/IModule.cs b/src/Sentinel.WorkerService.Common/Module/Interfaces/IModule.cs index d42fa89..6787710 100644 --- a/src/Sentinel.WorkerService.Common/Module/Interfaces/IModule.cs +++ b/src/Sentinel.WorkerService.Common/Module/Interfaces/IModule.cs @@ -1,4 +1,3 @@ -namespace Sentinel.WorkerService.Common.Module.Interfaces -{ - public interface IModule; -} \ No newline at end of file +namespace Sentinel.WorkerService.Common.Module.Interfaces; + +public interface IModule; \ No newline at end of file diff --git a/src/Sentinel.WorkerService.Common/Module/Interfaces/IScheduledModuleConfig.cs b/src/Sentinel.WorkerService.Common/Module/Interfaces/IScheduledModuleConfig.cs index 9728210..aee3dab 100644 --- a/src/Sentinel.WorkerService.Common/Module/Interfaces/IScheduledModuleConfig.cs +++ b/src/Sentinel.WorkerService.Common/Module/Interfaces/IScheduledModuleConfig.cs @@ -1,8 +1,7 @@ -namespace Sentinel.WorkerService.Common.Module.Interfaces +namespace Sentinel.WorkerService.Common.Module.Interfaces; + +// ReSharper disable once UnusedTypeParameter +public interface IScheduledModuleConfig { - // ReSharper disable once UnusedTypeParameter - public interface IScheduledModuleConfig - { - public TimeSpan Interval { get; set; } - } + public TimeSpan Interval { get; set; } } \ No newline at end of file diff --git a/src/Sentinel.WorkerService.Common/Module/Interfaces/IStartupModule.cs b/src/Sentinel.WorkerService.Common/Module/Interfaces/IStartupModule.cs index 6531145..5e4d7ec 100644 --- a/src/Sentinel.WorkerService.Common/Module/Interfaces/IStartupModule.cs +++ b/src/Sentinel.WorkerService.Common/Module/Interfaces/IStartupModule.cs @@ -1,7 +1,6 @@ -namespace Sentinel.WorkerService.Common.Module.Interfaces +namespace Sentinel.WorkerService.Common.Module.Interfaces; + +public interface IStartupModule : IModule { - public interface IStartupModule : IModule - { - public Task Execute(CancellationToken cancellationToken); - } + public Task Execute(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Sentinel.WorkerService.Common/Module/ScheduledModuleBase.cs b/src/Sentinel.WorkerService.Common/Module/ScheduledModuleBase.cs index 498bce1..34e5031 100644 --- a/src/Sentinel.WorkerService.Common/Module/ScheduledModuleBase.cs +++ b/src/Sentinel.WorkerService.Common/Module/ScheduledModuleBase.cs @@ -3,77 +3,76 @@ using Sentinel.WorkerService.Common.Module.Interfaces; using Timer = System.Timers.Timer; -namespace Sentinel.WorkerService.Common.Module +namespace Sentinel.WorkerService.Common.Module; + +public abstract class ScheduledModuleBase(ILogger logger, IScheduledModuleConfig config, bool runImmediately = false) : IHostedService, IModule { - public abstract class ScheduledModuleBase(ILogger logger, IScheduledModuleConfig config, bool runImmediately = false) : IHostedService, IModule - { - public bool RunImmediately { get; set; } = runImmediately; - private Timer? _timer; + public bool RunImmediately { get; set; } = runImmediately; + private Timer? _timer; - public string Name => typeof(T).Name; + public string Name => typeof(T).Name; - public virtual async Task StartAsync(CancellationToken cancellationToken) + public virtual async Task StartAsync(CancellationToken cancellationToken) + { + if (RunImmediately) { - if (RunImmediately) - { - await LogExecute(cancellationToken); - } - else - { - await Schedule(cancellationToken); - } + await LogExecute(cancellationToken); } + else + { + await Schedule(cancellationToken); + } + } - public abstract Task Execute(CancellationToken cancellationToken); + public abstract Task Execute(CancellationToken cancellationToken); - protected virtual async Task Schedule(CancellationToken cancellationToken) + protected virtual async Task Schedule(CancellationToken cancellationToken) + { + if (config.Interval.TotalMilliseconds <= 0) { - if (config.Interval.TotalMilliseconds <= 0) - { - await Schedule(cancellationToken); - } + await Schedule(cancellationToken); + } - _timer = new Timer(config.Interval.TotalMilliseconds); - _timer.Elapsed += async (_, _) => - { - _timer.Dispose(); - _timer = null; - if (cancellationToken.IsCancellationRequested) return; + _timer = new Timer(config.Interval.TotalMilliseconds); + _timer.Elapsed += async (_, _) => + { + _timer.Dispose(); + _timer = null; + if (cancellationToken.IsCancellationRequested) return; - await LogExecute(cancellationToken); - }; - _timer.Start(); + await LogExecute(cancellationToken); + }; + _timer.Start(); - await Task.CompletedTask; - } + await Task.CompletedTask; + } - private async Task LogExecute(CancellationToken cancellationToken) + private async Task LogExecute(CancellationToken cancellationToken) + { + logger.LogInformation("[{Name}] Executing", Name); + try { - logger.LogInformation($"[{Name}] Executing"); - try - { - await Execute(cancellationToken); - logger.LogInformation($"[{Name}] Executed successfully"); - } - catch (Exception ex) - { - logger.LogError($"[{Name}] Failed: \"{ex.Message}\""); - } - finally + await Execute(cancellationToken); + logger.LogInformation("[{Name}] Executed successfully", Name); + } + catch (Exception ex) + { + logger.LogError(ex, "[{Name}] Failed with message: {Message}", Name, ex.Message); + } + finally + { + if (!cancellationToken.IsCancellationRequested) { - if (!cancellationToken.IsCancellationRequested) - { - await Schedule(cancellationToken); - } + await Schedule(cancellationToken); } } + } - public virtual async Task StopAsync(CancellationToken cancellationToken) - { - logger.LogInformation($"Stopping {Name} task"); - _timer?.Stop(); - await Task.CompletedTask; - } + public virtual async Task StopAsync(CancellationToken cancellationToken) + { + logger.LogInformation("Stopping {Name} task", Name); + _timer?.Stop(); + await Task.CompletedTask; } } \ No newline at end of file diff --git a/src/Sentinel.WorkerService.Common/Module/ScheduledModuleConfig.cs b/src/Sentinel.WorkerService.Common/Module/ScheduledModuleConfig.cs index 85d9fbe..92c75f9 100644 --- a/src/Sentinel.WorkerService.Common/Module/ScheduledModuleConfig.cs +++ b/src/Sentinel.WorkerService.Common/Module/ScheduledModuleConfig.cs @@ -1,9 +1,8 @@ using Sentinel.WorkerService.Common.Module.Interfaces; -namespace Sentinel.WorkerService.Common.Module +namespace Sentinel.WorkerService.Common.Module; + +public class ScheduledModuleConfig : IScheduledModuleConfig { - public class ScheduledModuleConfig : IScheduledModuleConfig - { - public TimeSpan Interval { get; set; } - } + public TimeSpan Interval { get; set; } } \ No newline at end of file diff --git a/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/FirewallSettingsRetriever.cs b/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/FirewallSettingsRetriever.cs index 1fa1e73..eca9383 100644 --- a/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/FirewallSettingsRetriever.cs +++ b/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/FirewallSettingsRetriever.cs @@ -7,14 +7,14 @@ namespace Sentinel.WorkerService.Core.Windows.DeviceInformation; #pragma warning disable CA1416 public class FirewallSettingsRetriever : IFirewallSettingsRetriever { - public FirewallSettings Retrieve() + public FirewallSettingsDto Retrieve() { const string firewallProfileScope = @"\\.\root\StandardCimv2"; const string firewallProfileKey = "MSFT_NetFirewallProfile"; using var firewallObjectSearcher = new ManagementObjectSearcher(firewallProfileScope, "SELECT * FROM " + firewallProfileKey); var netFirewallProfiles = firewallObjectSearcher.Get().Cast().ToList(); - var firewallSettings = new FirewallSettings() + var firewallSettings = new FirewallSettingsDto() { DomainFirewallEnabled = netFirewallProfiles.Single(x => x["Name"]?.ToString() == "Domain")["Enabled"]?.ToString() == "1", PrivateFirewallEnabled = netFirewallProfiles.Single(x => x["Name"]?.ToString() == "Private")["Enabled"]?.ToString() == "1", diff --git a/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/Interfaces/IFirewallSettingsRetriever.cs b/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/Interfaces/IFirewallSettingsRetriever.cs index 9b334cd..14b200f 100644 --- a/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/Interfaces/IFirewallSettingsRetriever.cs +++ b/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/Interfaces/IFirewallSettingsRetriever.cs @@ -4,5 +4,5 @@ namespace Sentinel.WorkerService.Core.Windows.DeviceInformation.Interfaces; public interface IFirewallSettingsRetriever { - public FirewallSettings Retrieve(); + public FirewallSettingsDto Retrieve(); } \ No newline at end of file diff --git a/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/SecurityInformationRetriever.cs b/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/SecurityInformationRetriever.cs index 8b0ca04..ca006e0 100644 --- a/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/SecurityInformationRetriever.cs +++ b/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/SecurityInformationRetriever.cs @@ -9,7 +9,7 @@ namespace Sentinel.WorkerService.Core.Windows.DeviceInformation; #pragma warning disable CA1416 public class SecurityInformationRetriever(IFirewallSettingsRetriever firewallSettingsRetriever) : ISecurityInformationRetriever { - public SecurityInformation Retrieve() + public SecurityInformationDto Retrieve() { const string defenderScope = @"\\.\root\Microsoft\Windows\Defender"; const string computerStatusKey = "MSFT_MpComputerStatus"; @@ -18,7 +18,7 @@ public SecurityInformation Retrieve() var x = ParseExact(managementBaseObject["QuickScanStartTime"]); - var securityInformation = new SecurityInformation + var securityInformation = new SecurityInformationDto { AntivirusEnabled = (bool)managementBaseObject["AntiVirusEnabled"], LastAntivirusUpdate = ParseExact(managementBaseObject["AntivirusSignatureLastUpdated"]), @@ -28,13 +28,13 @@ public SecurityInformation Retrieve() TamperProtectionEnabled = (bool)managementBaseObject["IsTamperProtected"], AntispywareEnabled = (bool)managementBaseObject["AntiSpywareEnabled"], IsVirtualMachine = (bool)managementBaseObject["IsVirtualMachine"], - LastSecurityScan = new LastSecurityScan + LastSecurityScanDto = new LastSecurityScanDto { // TODO: fix, cant find the properties // LastScan = ParseExact(managementBaseObject["QuickScanStartTime"]), // Duration = ParseExact(managementBaseObject["QuickScanEndTime"]) - ParseExact(managementBaseObject["QuickScanStartTime"]) }, - FirewallSettings = firewallSettingsRetriever.Retrieve() + FirewallSettingsDto = firewallSettingsRetriever.Retrieve() }; return securityInformation; diff --git a/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/SoftwareInformationRetriever.cs b/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/SoftwareInformationRetriever.cs index 2cc00e3..c68c20c 100644 --- a/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/SoftwareInformationRetriever.cs +++ b/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/SoftwareInformationRetriever.cs @@ -7,16 +7,16 @@ namespace Sentinel.WorkerService.Core.Windows.DeviceInformation; #pragma warning disable CA1416 public class SoftwareInformationRetriever : ISoftwareInformationRetriever { - public SoftwareInformation Retrieve() + public SoftwareInformationDto Retrieve() { var profileKey = "Win32_Product"; var mos = new ManagementObjectSearcher("SELECT * FROM " + profileKey); - return new SoftwareInformation + return new SoftwareInformationDto { Software = mos.Get().Cast() .Where(x => x?["Name"]?.ToString() != null) - .Select(x => new Software + .Select(x => new SoftwareDto { Name = x["Name"].ToString()! }).ToList() diff --git a/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/StorageInformationRetriever.cs b/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/StorageInformationRetriever.cs index f34ef6c..80bdd97 100644 --- a/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/StorageInformationRetriever.cs +++ b/src/Sentinel.WorkerService.Core.Windows/DeviceInformation/StorageInformationRetriever.cs @@ -5,12 +5,12 @@ namespace Sentinel.WorkerService.Core.Windows.DeviceInformation; public class StorageInformationRetriever : IStorageInformationRetriever { - public StorageInformation Retrieve() + public StorageInformationDto Retrieve() { var osDir = Path.GetPathRoot(Environment.SystemDirectory); - return new StorageInformation + return new StorageInformationDto { - Disks = DriveInfo.GetDrives().Select(x => new DiskInformation() + Disks = DriveInfo.GetDrives().Select(x => new DiskInformationDto() { Name = x.Name.TrimEnd('\\'), Size = x.TotalSize, Used = x.TotalSize - x.TotalFreeSpace, IsOsDisk = osDir == x.Name diff --git a/src/Sentinel.WorkerService.Core/DeviceInformation/Interfaces/ISecurityInformationRetriever.cs b/src/Sentinel.WorkerService.Core/DeviceInformation/Interfaces/ISecurityInformationRetriever.cs index 09e3eff..c9f61cc 100644 --- a/src/Sentinel.WorkerService.Core/DeviceInformation/Interfaces/ISecurityInformationRetriever.cs +++ b/src/Sentinel.WorkerService.Core/DeviceInformation/Interfaces/ISecurityInformationRetriever.cs @@ -4,5 +4,5 @@ namespace Sentinel.WorkerService.Core.DeviceInformation.Interfaces; public interface ISecurityInformationRetriever { - public SecurityInformation Retrieve(); + public SecurityInformationDto Retrieve(); } \ No newline at end of file diff --git a/src/Sentinel.WorkerService.Core/DeviceInformation/Interfaces/ISoftwareInformationRetriever.cs b/src/Sentinel.WorkerService.Core/DeviceInformation/Interfaces/ISoftwareInformationRetriever.cs index 68aa09c..07a3674 100644 --- a/src/Sentinel.WorkerService.Core/DeviceInformation/Interfaces/ISoftwareInformationRetriever.cs +++ b/src/Sentinel.WorkerService.Core/DeviceInformation/Interfaces/ISoftwareInformationRetriever.cs @@ -4,5 +4,5 @@ namespace Sentinel.WorkerService.Core.DeviceInformation.Interfaces; public interface ISoftwareInformationRetriever { - public SoftwareInformation Retrieve(); + public SoftwareInformationDto Retrieve(); } \ No newline at end of file diff --git a/src/Sentinel.WorkerService.Core/DeviceInformation/Interfaces/IStorageInformationRetriever.cs b/src/Sentinel.WorkerService.Core/DeviceInformation/Interfaces/IStorageInformationRetriever.cs index 334cc57..9bfc17e 100644 --- a/src/Sentinel.WorkerService.Core/DeviceInformation/Interfaces/IStorageInformationRetriever.cs +++ b/src/Sentinel.WorkerService.Core/DeviceInformation/Interfaces/IStorageInformationRetriever.cs @@ -4,5 +4,5 @@ namespace Sentinel.WorkerService.Core.DeviceInformation.Interfaces; public interface IStorageInformationRetriever { - public StorageInformation Retrieve(); + public StorageInformationDto Retrieve(); } \ No newline at end of file diff --git a/src/Sentinel.WorkerService.Core/RestartDevice/RestartDeviceModule.cs b/src/Sentinel.WorkerService.Core/RestartDevice/RestartDeviceModule.cs index 42aa7c1..c7de8f2 100644 --- a/src/Sentinel.WorkerService.Core/RestartDevice/RestartDeviceModule.cs +++ b/src/Sentinel.WorkerService.Core/RestartDevice/RestartDeviceModule.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.Logging; -using Sentinel.Common.Messages; +using Sentinel.Common.SignalR; using Sentinel.WorkerService.Common.Consumer; using Sentinel.WorkerService.Common.Consumer.Interfaces; @@ -7,7 +7,7 @@ namespace Sentinel.WorkerService.Core.RestartDevice; public class RestartDeviceModule(IConsumerConfig config, ILogger logger) : ConsumerBase(config, logger) { - public override Task OnMessageReceived(RestartDeviceMessage context) + protected override Task OnMessageReceived(RestartDeviceMessage context) { try { diff --git a/src/Sentinel.WorkerService.Core/SecurityScan/SecurityScanModule.cs b/src/Sentinel.WorkerService.Core/SecurityScan/SecurityScanModule.cs index d068cbf..0a2a2e5 100644 --- a/src/Sentinel.WorkerService.Core/SecurityScan/SecurityScanModule.cs +++ b/src/Sentinel.WorkerService.Core/SecurityScan/SecurityScanModule.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.Logging; -using Sentinel.Common.Messages; +using Sentinel.Common.SignalR; using Sentinel.WorkerService.Common.Consumer; using Sentinel.WorkerService.Common.Consumer.Interfaces; @@ -7,7 +7,7 @@ namespace Sentinel.WorkerService.Core.SecurityScan; public class SecurityScanModule(IConsumerConfig config, ILogger logger, ISecurityScanner scanner) : ConsumerBase(config, logger) { - public override async Task OnMessageReceived(SecurityScanMessage context) + protected override async Task OnMessageReceived(SecurityScanMessage context) { try { diff --git a/src/Sentinel.WorkerService.RemoteAccess/RemoteAccessModule.cs b/src/Sentinel.WorkerService.RemoteAccess/RemoteAccessModule.cs index 16a6eb1..45b652f 100644 --- a/src/Sentinel.WorkerService.RemoteAccess/RemoteAccessModule.cs +++ b/src/Sentinel.WorkerService.RemoteAccess/RemoteAccessModule.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.Logging; -using Sentinel.Common.Messages; +using Sentinel.Common.SignalR; using Sentinel.WorkerService.Common.Consumer; using Sentinel.WorkerService.Common.Consumer.Interfaces; using Sentinel.WorkerService.RemoteAccess.Services.Interfaces; @@ -8,7 +8,7 @@ namespace Sentinel.WorkerService.RemoteAccess; public class RemoteAccessModule(IConsumerConfig config, ILogger logger, IRemoteAccessService remoteAccessService) : ConsumerBase(config, logger) { - public override Task OnMessageReceived(RemoteAccessMessage context) + protected override Task OnMessageReceived(RemoteAccessMessage context) { try { diff --git a/src/Sentinel.WorkerService/Extensions/ServiceCollectionExtensions.cs b/src/Sentinel.WorkerService/Extensions/ServiceCollectionExtensions.cs index 6e24a97..acf247b 100644 --- a/src/Sentinel.WorkerService/Extensions/ServiceCollectionExtensions.cs +++ b/src/Sentinel.WorkerService/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.SignalR.Client; -using Sentinel.Common.Messages; +using Sentinel.Common.SignalR; using Sentinel.WorkerService.Common.Consumer; using Sentinel.WorkerService.Common.Consumer.Interfaces; using Sentinel.WorkerService.Common.Module; @@ -18,83 +18,86 @@ namespace Sentinel.WorkerService.Extensions; public static class ServiceCollectionExtensions { - public static void AddModuleDependencies(this IServiceCollection services) + extension(IServiceCollection services) { - if (OperatingSystem.IsWindows()) + public void AddModuleDependencies() { - services.AddWindowsCoreDependencies(); + if (OperatingSystem.IsWindows()) + { + services.AddWindowsCoreDependencies(); + } + else if (OperatingSystem.IsLinux()) + { + services.AddLinuxCoreDependencies(); + } + else throw new Exception("This OS is not supported"); } - else if (OperatingSystem.IsLinux()) + + public IServiceCollection AddStartupModules() { - services.AddLinuxCoreDependencies(); + services + .AddStartupTask() + .AddStartupTask(); + return services; } - else throw new Exception("This OS is not supported"); - } - public static IServiceCollection AddStartupModules(this IServiceCollection services) - { - services - .AddStartupTask() - .AddStartupTask(); - return services; - } + public IServiceCollection AddScheduledModules() + { + services + .AddScheduledTask(scheduleConfig => scheduleConfig.Interval = TimeSpan.FromMinutes(1)) + .AddScheduledTask(scheduleConfig => scheduleConfig.Interval = TimeSpan.FromMinutes(5)) + .AddScheduledTask(scheduleConfig => scheduleConfig.Interval = TimeSpan.FromMinutes(10)) + .AddScheduledTask(scheduleConfig => scheduleConfig.Interval = TimeSpan.FromMinutes(2.5)) + .AddScheduledTask(scheduleConfig => scheduleConfig.Interval = TimeSpan.FromMinutes(30)); + return services; + } - public static IServiceCollection AddScheduledModules(this IServiceCollection services) - { - services - .AddScheduledTask(scheduleConfig => scheduleConfig.Interval = TimeSpan.FromMinutes(1)) - .AddScheduledTask(scheduleConfig => scheduleConfig.Interval = TimeSpan.FromMinutes(5)) - .AddScheduledTask(scheduleConfig => scheduleConfig.Interval = TimeSpan.FromMinutes(10)) - .AddScheduledTask(scheduleConfig => scheduleConfig.Interval = TimeSpan.FromMinutes(2.5)) - .AddScheduledTask(scheduleConfig => scheduleConfig.Interval = TimeSpan.FromMinutes(30)); - return services; - } + public IServiceCollection AddConsumers(HubConnection hubConnection) + { + services + .AddConsumer(config => config.Connection = hubConnection) + .AddConsumer(config => config.Connection = hubConnection) + .AddConsumer(config => config.Connection = hubConnection); - public static IServiceCollection AddConsumers(this IServiceCollection services, HubConnection hubConnection) - { - services - .AddConsumer(config => config.Connection = hubConnection) - .AddConsumer(config => config.Connection = hubConnection) - .AddConsumer(config => config.Connection = hubConnection); + return services; + } - return services; - } + private IServiceCollection AddStartupTask() + where T : class, IStartupModule, IModule + { + if (!LicenseManager.IsLicensed()) return services; - private static IServiceCollection AddStartupTask(this IServiceCollection services) - where T : class, IStartupModule, IModule - { - if (!LicenseManager.IsLicensed()) return services; + services.AddSingleton(); + return services; + } - services.AddSingleton(); - return services; - } + private IServiceCollection AddConsumer(Action> consumerConfig) where TConsumer : class, IHostedService, IModule + { + if (consumerConfig == null) throw new ArgumentNullException(nameof(consumerConfig), "Please provide consumer configuration"); + if (!LicenseManager.IsLicensed()) return services; - private static IServiceCollection AddConsumer(this IServiceCollection services, Action> consumerConfig) where TConsumer : class, IHostedService, IModule - { - if (consumerConfig == null) throw new ArgumentNullException(nameof(consumerConfig), "Please provide consumer configuration"); - if (!LicenseManager.IsLicensed()) return services; + var config = new ConsumerConfig(); + consumerConfig(config); - var config = new ConsumerConfig(); - consumerConfig(config); + services.AddSingleton>(config); + services.AddHostedService(); + return services; + } - services.AddSingleton>(config); - services.AddHostedService(); - return services; - } + private IServiceCollection AddScheduledTask(Action> scheduledTaskConfig) where T : ScheduledModuleBase, IModule + { + if (scheduledTaskConfig == null) throw new ArgumentNullException(nameof(scheduledTaskConfig), "Please provide scheduled task configuration"); + if (!LicenseManager.IsLicensed()) return services; - private static IServiceCollection AddScheduledTask(this IServiceCollection services, Action> scheduledTaskConfig) where T : ScheduledModuleBase, IModule - { - if (scheduledTaskConfig == null) throw new ArgumentNullException(nameof(scheduledTaskConfig), "Please provide scheduled task configuration"); - if (!LicenseManager.IsLicensed()) return services; + var config = new ScheduledModuleConfig(); + scheduledTaskConfig.Invoke(config); - var config = new ScheduledModuleConfig(); - scheduledTaskConfig.Invoke(config); + services.AddSingleton>(config); + services.AddHostedService(); - services.AddSingleton>(config); - services.AddHostedService(); + return services; + } - return services; + public void Build() { } } - - public static void Build(this IServiceCollection _) { } } \ No newline at end of file diff --git a/src/Sentinel.WorkerService/Program.cs b/src/Sentinel.WorkerService/Program.cs index f83b3a8..312d769 100644 --- a/src/Sentinel.WorkerService/Program.cs +++ b/src/Sentinel.WorkerService/Program.cs @@ -32,7 +32,7 @@ // Build the service provider to resolve the authentication handler var authenticationHandler = services.BuildServiceProvider().GetRequiredService(); - authenticationHandler.EnsureAuthenticated(Guid.Parse(args[0]), Environment.MachineName, CancellationToken.None).Wait(); + authenticationHandler.EnsureAuthenticatedAsync(Guid.Parse(args[0]), Environment.MachineName, CancellationToken.None).Wait(); // SignalR var deviceHubConnection = HubManager.Initialize("DeviceMessageHub", hostContext); diff --git a/tests/Sentinel.Api.Integration.Tests/Common/ApiFixtureExtensions.cs b/tests/Sentinel.Api.Integration.Tests/Common/ApiFixtureExtensions.cs new file mode 100644 index 0000000..26aca04 --- /dev/null +++ b/tests/Sentinel.Api.Integration.Tests/Common/ApiFixtureExtensions.cs @@ -0,0 +1,59 @@ +using Microsoft.Extensions.DependencyInjection; +using Sentinel.Api.Application.DTO.Device; +using Sentinel.Api.Application.DTO.User; +using Sentinel.Api.Infrastructure.Persistence; + +namespace Sentinel.Api.Integration.Tests.Common; + +public static class ApiFixtureExtensions +{ + extension(ApiFixture fixture) + { + public AppDbContext GetDbContext() + { + var provider = fixture.Services.CreateScope().ServiceProvider; + var dbContext = provider.GetRequiredService(); + dbContext.Database.EnsureCreated(); + return dbContext; + } + + public async Task<(HttpClient client, SignInUserResponse user)> CreateAuthenticatedUserAsync() + { + var client = fixture.CreateClient(); + var user = await client.AuthenticateUserAsync(); + return (client, user); + } + + public async Task<(HttpClient client, DeviceTokenResponse device)> CreateAuthenticatedDeviceAsync(Guid organisationHash) + { + var client = fixture.CreateClient(); + var device = await client.RegisterDeviceAsync(organisationHash); + return (client, device); + } + + public async Task AddOrganisationAsync(Guid organisationHash) + { + var provider = fixture.Services.CreateScope().ServiceProvider; + await using var dbContext = provider.GetRequiredService(); + await dbContext.Database.EnsureCreatedAsync(); + + var organisation = new Domain.Entities.Organisation + { + Hash = organisationHash + }; + dbContext.Organisations.Add(organisation); + await dbContext.SaveChangesAsync(); + return organisation; + } + + public async Task AddDeviceAsync(Domain.Entities.Device device) + { + var provider = fixture.Services.CreateScope().ServiceProvider; + await using var dbContext = provider.GetRequiredService(); + await dbContext.Database.EnsureCreatedAsync(); + + dbContext.Devices.Add(device); + await dbContext.SaveChangesAsync(); + } + } +} \ No newline at end of file diff --git a/tests/Sentinel.Api.Integration.Tests/Common/ClientExtensions.cs b/tests/Sentinel.Api.Integration.Tests/Common/ClientExtensions.cs deleted file mode 100644 index 73f978a..0000000 --- a/tests/Sentinel.Api.Integration.Tests/Common/ClientExtensions.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Net.Http.Headers; -using System.Text; -using System.Text.Json; -using OtpNet; -using Sentinel.Api.Application.DTO.Device; -using Sentinel.Api.Application.DTO.Token; -using Sentinel.Api.Application.DTO.User; -using Sentinel.WorkerService.Common.Api.Extensions; -using Sentinel.WorkerService.Common.Extensions; - -namespace Sentinel.Api.Integration.Tests.Common; - -public static class ClientExtensions -{ - public static async Task AuthenticateUserAsync(this HttpClient client) - { - try - { - var signInUserResponse = await client.SignInUserAsync(); - var totp = new Totp(Base32Encoding.ToBytes(signInUserResponse.TwoFactorToken), step: 30, - mode: OtpHashMode.Sha1, totpSize: 6); - - // verify - var verifyUserDto = new VerifyUserDto() - { - UserId = signInUserResponse.UserId, - AuthenticityToken = signInUserResponse.AuthenticityToken, - OtpAttempt = totp.ComputeTotp(), - }; - - var verificationResult = await client.PostAsync("/auth/users/verify", JsonSerializer.Serialize(verifyUserDto)); - var userTokenResponse = await verificationResult.Content.DeserializeAsync() ?? throw new Exception("verification result was null"); - client.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue("Bearer", $"{userTokenResponse.AccessToken}"); - return signInUserResponse; - } - catch (Exception e) - { - Console.WriteLine(e); - return null!; - } - } - - public static async Task RegisterDeviceAsync(this HttpClient client, Guid organisationHash) - { - try - { - var verificationResult = await client.PostAsync("/devices/register", new RegisterDeviceDto - { - Name = "John Doe", - OrganisationHash = organisationHash, - }); - var deviceTokenResponse = await verificationResult.Content.DeserializeAsync() ?? throw new Exception("verification result was null"); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", $"{deviceTokenResponse.AccessToken}"); - return deviceTokenResponse; - } - catch (Exception e) - { - Console.WriteLine(e); - return null!; - } - } - - private static async Task SignInUserAsync(this HttpClient client) - { - _ = await client.PostAsync("/users/register", JsonSerializer.Serialize(new RegisterUserDto { Email = "test@test.com", Password = "password", })); - var signInResult = await client.PostAsync("/auth/users/sign_in", JsonSerializer.Serialize(new SignInUserDto { Email = "test@test.com", Password = "password", })); - var response = await signInResult.Content.DeserializeAsync(); - if(response == null) throw new Exception("sign_in_response is null"); - return response; - } - - private static async Task PostAsync(this HttpClient client, string path, string? content) - { - - ByteArrayContent? byteContent = null; - if (content == null) return await client.PostAsync(path, byteContent); - var buffer = Encoding.UTF8.GetBytes(content); - byteContent = new ByteArrayContent(buffer); - byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - return await client.PostAsync(path, byteContent); - } -} \ No newline at end of file diff --git a/tests/Sentinel.Api.Integration.Tests/Common/FactoryExtensions.cs b/tests/Sentinel.Api.Integration.Tests/Common/FactoryExtensions.cs deleted file mode 100644 index a71314e..0000000 --- a/tests/Sentinel.Api.Integration.Tests/Common/FactoryExtensions.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Sentinel.Api.Application.DTO.Device; -using Sentinel.Api.Application.DTO.User; -using Sentinel.Api.Infrastructure.Persistence; - -namespace Sentinel.Api.Integration.Tests.Common; - -public static class FactoryExtensions -{ - public static AppDbContext GetDbContext(this ApiFixture fixture) - { - var provider = fixture.Services.CreateScope().ServiceProvider; - var dbContext = provider.GetRequiredService(); - dbContext.Database.EnsureCreated(); - return dbContext; - } - - public static HttpClient CreateAuthenticatedUser(this ApiFixture fixture, out SignInUserResponse user) - { - var client = fixture.CreateClient(); - user = client.AuthenticateUserAsync().Result; - return client; - } - - public static HttpClient CreateAuthenticatedDevice(this ApiFixture fixture, Guid organisationHash, out DeviceTokenResponse device) - { - var client = fixture.CreateClient(); - device = client.RegisterDeviceAsync(organisationHash).Result; - return client; - } - - public static async Task AddOrganisationAsync(this ApiFixture fixture, Domain.Entities.Organisation organisation) - { - var provider = fixture.Services.CreateScope().ServiceProvider; - await using var dbContext = provider.GetRequiredService(); - await dbContext.Database.EnsureCreatedAsync(); - - dbContext.Organisations.Add(organisation); - await dbContext.SaveChangesAsync(); - return organisation; - } - - public static async Task AddDeviceAsync(this ApiFixture fixture, Domain.Entities.Device device) - { - var provider = fixture.Services.CreateScope().ServiceProvider; - await using var dbContext = provider.GetRequiredService(); - await dbContext.Database.EnsureCreatedAsync(); - - dbContext.Devices.Add(device); - await dbContext.SaveChangesAsync(); - } -} \ No newline at end of file diff --git a/tests/Sentinel.Api.Integration.Tests/Common/HttpHelpers.cs b/tests/Sentinel.Api.Integration.Tests/Common/HttpHelpers.cs new file mode 100644 index 0000000..26b0c2f --- /dev/null +++ b/tests/Sentinel.Api.Integration.Tests/Common/HttpHelpers.cs @@ -0,0 +1,40 @@ +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; + +namespace Sentinel.Api.Integration.Tests.Common; + +public static class HttpClientExtensions +{ + extension(HttpClient client) + { + public async Task PostAsync(string requestUri, object? payload = null, CancellationToken cancellationToken = default) + { + var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); + try + { + return await client.PostAsync(requestUri, content, cancellationToken); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + public async Task PutAsync(string requestUri, object payload, CancellationToken cancellationToken = default) + { + var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); + return await client.PutAsync(requestUri, content, cancellationToken); + } + } +} + +public static class HttpContentExtensions +{ + public static Task DeserializeAsync(this HttpContent content, CancellationToken cancellationToken = default, JsonSerializerOptions? serializerOptions = null) + { + var options = serializerOptions ?? new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + return content.ReadFromJsonAsync(options, cancellationToken); + } +} diff --git a/tests/Sentinel.Api.Integration.Tests/Common/TestAssertions.cs b/tests/Sentinel.Api.Integration.Tests/Common/TestAssertions.cs new file mode 100644 index 0000000..1e0bfe3 --- /dev/null +++ b/tests/Sentinel.Api.Integration.Tests/Common/TestAssertions.cs @@ -0,0 +1,41 @@ +using System.Net; +using NUnit.Framework; + +namespace Sentinel.Api.Integration.Tests.Common; + +public static class HttpResponseAssertions +{ + extension(HttpResponseMessage response) + { + public void ShouldBeOk() + { + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), + $"Expected OK but got {response.StatusCode}"); + } + + public void ShouldBeUnauthorized() + { + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized), + $"Expected Unauthorized but got {response.StatusCode}"); + } + + public void ShouldBeBadRequest() + { + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest), + $"Expected BadRequest but got {response.StatusCode}"); + } + + public void ShouldBeForbidden() + { + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden), + $"Expected Forbidden but got {response.StatusCode}"); + } + + public async Task ShouldDeserializeTo() + { + var content = await response.Content.DeserializeAsync(); + Assert.That(content, Is.Not.Null, $"Response content could not be deserialized to {typeof(T).Name}"); + return content!; + } + } +} diff --git a/tests/Sentinel.Api.Integration.Tests/Common/TestScope.cs b/tests/Sentinel.Api.Integration.Tests/Common/TestScope.cs new file mode 100644 index 0000000..ccd4c1c --- /dev/null +++ b/tests/Sentinel.Api.Integration.Tests/Common/TestScope.cs @@ -0,0 +1,160 @@ +using System.Net.Http.Headers; +using Microsoft.EntityFrameworkCore; +using OtpNet; +using Sentinel.Api.Application.DTO.Device; +using Sentinel.Api.Application.DTO.Token; +using Sentinel.Api.Application.DTO.User; +using Sentinel.Api.Infrastructure.Persistence; + +namespace Sentinel.Api.Integration.Tests.Common; + +public sealed class TestScope : IAsyncDisposable +{ + private readonly ApiFixture _fixture; + private Guid? _organisationHash = null; + + public HttpClient Client { get; set; } + public AppDbContext DbContext => _fixture.GetDbContext(); + public ApiFixture Fixture => _fixture; + + public Domain.Entities.Organisation Organisation => DbContext.Organisations + .Include(x => x.Devices) + .Include(x => x.Users) + .Single(x => x.Hash == _organisationHash); + + public SignInUserResponse? User { get; private set; } + + public TestScope() + { + _fixture = new ApiFixture(); + Client = _fixture.CreateClient(); + } + + public async Task AuthenticateAsUserAsync() + { + var (client, user) = await _fixture.CreateAuthenticatedUserAsync(); + _organisationHash = DbContext.Organisations.Single(x => x.Id == user.OrganisationId).Hash; + Client = client; + User = user; + + return this; + } + + public async Task AuthenticateAsDeviceAsync() + { + if (_organisationHash == null) + await AddOrganisationAsync(); + + var (client, device) = await _fixture.CreateAuthenticatedDeviceAsync(_organisationHash ?? throw new Exception("organisation is null")); + Client = client; + + return this; + } + + public async Task AddOrganisationAsync() + { + var hash = Guid.NewGuid(); + await Fixture.AddOrganisationAsync(hash); + _organisationHash = hash; + return this; + } + + public async Task AddDeviceAsync() + { + var device = new Domain.Entities.Device + { + Name = "Test Device", + OrganisationId = Organisation?.Id ?? throw new Exception("Organisation is null. Ensure organisation is added."), + }; + await Fixture.AddDeviceAsync(device); + return this; + } + + public async ValueTask DisposeAsync() + { + await _fixture.DisposeAsync(); + + } +} + +public static class HttpClientAuthExtensions +{ + extension(HttpClient client) + { + public async Task AuthenticateUserAsync() + { + try + { + var signInUserResponse = await client.SignInUserAsync(); + var totp = new Totp(Base32Encoding.ToBytes(signInUserResponse.TwoFactorToken), step: 30, + mode: OtpHashMode.Sha1, totpSize: 6); + + var verifyUserDto = new VerifyUserDto() + { + UserId = signInUserResponse.UserId, + AuthenticityToken = signInUserResponse.AuthenticityToken, + OtpAttempt = totp.ComputeTotp(), + }; + + var verificationResult = await client.PostAsync("/auth/users/verify", verifyUserDto); + var userTokenResponse = await verificationResult.Content.DeserializeAsync() + ?? throw new Exception("verification result was null"); + + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", $"{userTokenResponse.AccessToken}"); + + return signInUserResponse; + } + catch (Exception e) + { + Console.WriteLine(e); + return null!; + } + } + + public async Task RegisterDeviceAsync(Guid organisationHash) + { + try + { + var verificationResult = await client.PostAsync("/devices/register", new RegisterDeviceDto + { + Name = "John Doe", + OrganisationHash = organisationHash, + }); + + var deviceTokenResponse = await verificationResult.Content.DeserializeAsync() + ?? throw new Exception("verification result was null"); + + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", $"{deviceTokenResponse.AccessToken}"); + + return deviceTokenResponse; + } + catch (Exception e) + { + Console.WriteLine(e); + return null!; + } + } + + private async Task SignInUserAsync() + { + _ = await client.PostAsync("/users/register", new RegisterUserDto + { + Email = "test@test.com", + Password = "password" + }); + + var signInResult = await client.PostAsync("/auth/users/sign_in", new SignInUserDto + { + Email = "test@test.com", + Password = "password" + }); + + var response = await signInResult.Content.DeserializeAsync(); + if (response == null) throw new Exception("sign_in_response is null"); + + return response; + } + } +} diff --git a/tests/Sentinel.Api.Integration.Tests/Device/Authentication/RegisterTests.cs b/tests/Sentinel.Api.Integration.Tests/Device/Authentication/RegisterTests.cs index 14443e8..e46fe3e 100644 --- a/tests/Sentinel.Api.Integration.Tests/Device/Authentication/RegisterTests.cs +++ b/tests/Sentinel.Api.Integration.Tests/Device/Authentication/RegisterTests.cs @@ -1,57 +1,38 @@ -using System.Net; -using NUnit.Framework; +using NUnit.Framework; using Sentinel.Api.Application.DTO.Device; using Sentinel.Api.Integration.Tests.Common; -using Sentinel.WorkerService.Common.Api.Extensions; namespace Sentinel.Api.Integration.Tests.Device.Authentication; public class RegisterTests { - private ApiFixture _fixture = null!; - private Domain.Entities.Organisation _organisation = null!; - - [SetUp] - public async Task Setup() - { - _fixture = new ApiFixture(); - _organisation = await _fixture.AddOrganisationAsync(new Domain.Entities.Organisation{ Hash = Guid.NewGuid()}); - } - - [TearDown] - public async Task TearDown() => await _fixture.DisposeAsync(); - [Test] public async Task Correct_Registration_ShouldReturnOK() { - // Arrange - using var client = _fixture.CreateClient(); - - // Act - var result = await client.PostAsync("/devices/register", new RegisterDeviceDto + await using var scope = new TestScope(); + await scope.AuthenticateAsUserAsync(); + + var result = await scope.Client.PostAsync("/devices/register", new RegisterDeviceDto { Name = "John Doe", - OrganisationHash = _organisation.Hash, + OrganisationHash = scope.Organisation.Hash, }); - // Assert - Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + result.ShouldBeOk(); } [Test] public async Task InvalidOrganisationHash_Registration_ShouldReturnNotFound() { - // Arrange - using var client = _fixture.CreateClient(); - - // Act - var result = await client.PostAsync("/devices/register", new RegisterDeviceDto + await using var scope = new TestScope(); + await scope.AuthenticateAsUserAsync(); + + var result = await scope.Client.PostAsync("/devices/register", new RegisterDeviceDto { Name = "John Doe", OrganisationHash = Guid.Empty, }); - // Assert - Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); + result.ShouldBeBadRequest(); } } \ No newline at end of file diff --git a/tests/Sentinel.Api.Integration.Tests/Device/Management/DeviceRetrievalTests.cs b/tests/Sentinel.Api.Integration.Tests/Device/Management/DeviceRetrievalTests.cs index 0c59273..c6456c5 100644 --- a/tests/Sentinel.Api.Integration.Tests/Device/Management/DeviceRetrievalTests.cs +++ b/tests/Sentinel.Api.Integration.Tests/Device/Management/DeviceRetrievalTests.cs @@ -1,59 +1,35 @@ -using System.Net; -using NUnit.Framework; +using NUnit.Framework; using Sentinel.Api.Application.DTO.Device; using Sentinel.Api.Integration.Tests.Common; -using Sentinel.WorkerService.Common.Extensions; namespace Sentinel.Api.Integration.Tests.Device.Management; public class DeviceRetrievalTests { - private ApiFixture _fixture = null!; - - [SetUp] - public Task Setup() - { - _fixture = new ApiFixture(); - return Task.CompletedTask; - } - - [TearDown] - public async Task TearDown() => await _fixture.DisposeAsync(); - [Test] public async Task Unauthorized_GetDevices_ShouldReturnUnauthorized() { - // Arrange - using var client = _fixture.CreateClient(); - - // Act - var result = await client.GetAsync("/devices"); - - // Assert - Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized)); + await using var scope = new TestScope(); + var result = await scope.Client.GetAsync("/devices"); + result.ShouldBeUnauthorized(); } [Test] public async Task Authorized_GetDevices_ShouldReturnOK() { - // Arrange - using var client = _fixture.CreateAuthenticatedUser(out _); - - // Act - var result = await client.GetAsync("/devices"); + await using var scope = await new TestScope().AuthenticateAsUserAsync(); + + var result = await scope.Client.GetAsync("/devices"); - // Assert - Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + result.ShouldBeOk(); } [Test] public async Task Authorized_GetDevices_ShouldOnlyReturnOrganisationDevices() { - // Arrange - using var client = _fixture.CreateAuthenticatedUser(out var user); - await using var dbContext = _fixture.GetDbContext(); + await using var scope = await new TestScope().AuthenticateAsUserAsync(); + var organisation = scope.DbContext.Organisations.Single(x => x.Id == scope.User!.OrganisationId); - var organisation = dbContext.Organisations.Single(x => x.Id == user.OrganisationId); var authorizedDevice = new Domain.Entities.Device { OrganisationId = organisation.Id, @@ -64,16 +40,13 @@ public async Task Authorized_GetDevices_ShouldOnlyReturnOrganisationDevices() OrganisationId = organisation.Id + 1, Name = "UnauthorizedOrganisationDevice" }; - await _fixture.AddDeviceAsync(authorizedDevice); - await _fixture.AddDeviceAsync(unauthorizedDevice); - - // Act - var result = await client.GetAsync("/devices"); - var devices = await result.Content.DeserializeAsync(); + await scope.Fixture.AddDeviceAsync(authorizedDevice); + await scope.Fixture.AddDeviceAsync(unauthorizedDevice); - // Assert - Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.OK)); - Assert.That(devices!.Devices.Count, Is.EqualTo(1)); + var result = await scope.Client.GetAsync("/devices"); + var devices = await result.ShouldDeserializeTo(); + result.ShouldBeOk(); + Assert.That(devices.Devices.Count, Is.EqualTo(1)); Assert.That(devices.Devices.Any(d => d.Name == authorizedDevice.Name), Is.True); Assert.That(devices.Devices.Any(d => d.Name == unauthorizedDevice.Name), Is.False); } diff --git a/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateDeviceInformationTests.cs b/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateDeviceInformationTests.cs new file mode 100644 index 0000000..978365b --- /dev/null +++ b/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateDeviceInformationTests.cs @@ -0,0 +1,57 @@ +using NUnit.Framework; +using Sentinel.Api.Integration.Tests.Common; +using Sentinel.Common.DTO.Device.Information; + +namespace Sentinel.Api.Integration.Tests.Device.Updates; + +public class UpdateDeviceInformationTests +{ + [Test] + public async Task AuthorizedDevice_UpdateDeviceInformation_ShouldReturnOK() + { + await using var scope = new TestScope(); + await scope.AuthenticateAsDeviceAsync(); + + var updateDto = new UpdateDeviceInformationDto + { + DeviceName = "Updated Device", + OsName = "Windows", + OsVersion = "11", + Version = "22H2", + ProductName = "Windows 11 Pro", + Processor = "Intel Core i7", + InstalledRam = "16GB", + GraphicsCard = "NVIDIA RTX 3080", + Manufacturer = "Dell" + }; + + var device = scope.Organisation.Devices.Single(); + + var result = await scope.Client.PutAsync($"/devices/{device.Id}", updateDto); + + result.ShouldBeOk(); + } + + [Test] + public async Task UnauthorizedDevice_UpdateDeviceInformation_ShouldReturnUnauthorized() + { + await using var scope = new TestScope(); + + var updateDto = new UpdateDeviceInformationDto + { + DeviceName = "Updated Device", + OsName = "Windows", + OsVersion = "11", + Version = "22H2", + ProductName = "Windows 11 Pro", + Processor = "Intel Core i7", + InstalledRam = "16GB", + GraphicsCard = "NVIDIA RTX 3080", + Manufacturer = "Dell" + }; + + var result = await scope.Client.PutAsync("/devices/1", updateDto); + + result.ShouldBeUnauthorized(); + } +} diff --git a/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateSecurityInformationTests.cs b/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateSecurityInformationTests.cs new file mode 100644 index 0000000..be47ad7 --- /dev/null +++ b/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateSecurityInformationTests.cs @@ -0,0 +1,64 @@ +using NUnit.Framework; +using Sentinel.Api.Integration.Tests.Common; +using Sentinel.Common.DTO.Device; + +namespace Sentinel.Api.Integration.Tests.Device.Updates; + +public class UpdateSecurityInformationTests +{ + [Test] + public async Task AuthorizedDevice_UpdateSecurityInformation_ShouldReturnOK() + { + await using var scope = new TestScope(); + await scope.AuthenticateAsDeviceAsync(); + + var updateDto = new SecurityInformationDto + { + LastSecurityScanDto = new LastSecurityScanDto + { + LastScan = DateTime.UtcNow.AddDays(-1), + Duration = TimeSpan.FromMinutes(30) + }, + AntivirusEnabled = true, + RealTimeProtectionEnabled = true, + FirewallSettingsDto = new FirewallSettingsDto + { + DomainFirewallEnabled = true, + PrivateFirewallEnabled = true, + PublicFirewallEnabled = true + } + }; + + var device = scope.Organisation.Devices.Single(); + + var result = await scope.Client.PutAsync($"/devices/{device!.Id}/security", updateDto); + result.ShouldBeOk(); + } + + [Test] + public async Task UnauthorizedDevice_UpdateSecurityInformation_ShouldReturnUnauthorized() + { + await using var scope = new TestScope(); + + var updateDto = new SecurityInformationDto + { + LastSecurityScanDto = new LastSecurityScanDto + { + LastScan = DateTime.UtcNow.AddDays(-1), + Duration = TimeSpan.FromMinutes(30) + }, + AntivirusEnabled = true, + RealTimeProtectionEnabled = true, + FirewallSettingsDto = new FirewallSettingsDto + { + DomainFirewallEnabled = true, + PrivateFirewallEnabled = true, + PublicFirewallEnabled = true + } + }; + + var result = await scope.Client.PutAsync("/devices/1/security", updateDto); + + result.ShouldBeUnauthorized(); + } +} diff --git a/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateSoftwareInformationTests.cs b/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateSoftwareInformationTests.cs new file mode 100644 index 0000000..93900f7 --- /dev/null +++ b/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateSoftwareInformationTests.cs @@ -0,0 +1,50 @@ +using NUnit.Framework; +using Sentinel.Api.Integration.Tests.Common; +using Sentinel.Common.DTO.Device; + +namespace Sentinel.Api.Integration.Tests.Device.Updates; + +public class UpdateSoftwareInformationTests +{ + [Test] + public async Task AuthorizedDevice_UpdateSoftwareInformation_ShouldReturnOK() + { + await using var scope = new TestScope(); + await scope.AuthenticateAsDeviceAsync(); + + var updateDto = new SoftwareInformationDto + { + Software = new List + { + new() { Name = "Google Chrome" }, + new() { Name = "Mozilla Firefox" }, + new() { Name = "Visual Studio Code" } + } + }; + var device = scope.Organisation.Devices.Single(); + + var result = await scope.Client.PutAsync($"/devices/{device!.Id}/software", updateDto); + + result.ShouldBeOk(); + } + + [Test] + public async Task UnauthorizedDevice_UpdateSoftwareInformation_ShouldReturnUnauthorized() + { + await using var scope = new TestScope(); + + var updateDto = new SoftwareInformationDto + { + Software = new List + { + new() { Name = "Google Chrome" }, + new() { Name = "Mozilla Firefox" }, + new() { Name = "Visual Studio Code" } + } + }; + + var result = await scope.Client.PutAsync("/devices/1/software", updateDto); + + result.ShouldBeUnauthorized(); + } +} diff --git a/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateStorageInformationTests.cs b/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateStorageInformationTests.cs new file mode 100644 index 0000000..1ba523d --- /dev/null +++ b/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateStorageInformationTests.cs @@ -0,0 +1,58 @@ +using NUnit.Framework; +using Sentinel.Api.Integration.Tests.Common; +using Sentinel.Common.DTO.Device; + +namespace Sentinel.Api.Integration.Tests.Device.Updates; + +public class UpdateStorageInformationTests +{ + [Test] + public async Task AuthorizedDevice_UpdateStorageInformation_ShouldReturnOK() + { + await using var scope = new TestScope(); + await scope.AuthenticateAsDeviceAsync(); + + var updateDto = new StorageInformationDto + { + Disks = new List + { + new() + { + Name = "C:", + IsOsDisk = true, + Used = 250.5, + Size = 500.0 + } + } + }; + var device = scope.DbContext.Devices.Single(); + + var result = await scope.Client.PutAsync($"/devices/{device.Id}/storage", updateDto); + + result.ShouldBeOk(); + } + + [Test] + public async Task UnauthorizedDevice_UpdateStorageInformation_ShouldReturnUnauthorized() + { + await using var scope = new TestScope(); + + var updateDto = new StorageInformationDto + { + Disks = new List + { + new() + { + Name = "C:", + IsOsDisk = true, + Used = 250.5, + Size = 500.0 + } + } + }; + + var result = await scope.Client.PutAsync("/devices/1/storage", updateDto); + + result.ShouldBeUnauthorized(); + } +} diff --git a/tests/Sentinel.Api.Integration.Tests/Device/Worker/PingTaskTests.cs b/tests/Sentinel.Api.Integration.Tests/Device/Worker/PingTaskTests.cs index 2390021..32cdbb6 100644 --- a/tests/Sentinel.Api.Integration.Tests/Device/Worker/PingTaskTests.cs +++ b/tests/Sentinel.Api.Integration.Tests/Device/Worker/PingTaskTests.cs @@ -1,35 +1,27 @@ -using System.Net; -using NUnit.Framework; +using NUnit.Framework; using Sentinel.Api.Integration.Tests.Common; -using Sentinel.WorkerService.Common.Api.Extensions; namespace Sentinel.Api.Integration.Tests.Device.Worker; public class PingTaskTests { - private ApiFixture _fixture = null!; - private Domain.Entities.Organisation _organisation = null!; - - [SetUp] - public async Task Setup() - { - _fixture = new ApiFixture(); - _organisation = await _fixture.AddOrganisationAsync(new Domain.Entities.Organisation{ Hash = Guid.NewGuid()}); - } - - [TearDown] - public async Task TearDown() => await _fixture.DisposeAsync(); - [Test] public async Task Authorized_TaskExecution_ShouldReturnOk() { - // Arrange - var client = _fixture.CreateAuthenticatedDevice(_organisation.Hash, out var device); + await using var scope = new TestScope(); + await scope.AuthenticateAsDeviceAsync(); + + var device = scope.DbContext.Devices.Single(x => x.OrganisationId == scope.Organisation.Id); - // Act - var result = await client.PostAsync($"/devices/{device.Id}/ping"); - - // // Assert - Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + var result = await scope.Client.PostAsync($"/devices/{device.Id}/ping"); + result.ShouldBeOk(); + } + + [Test] + public async Task UnAuthorized_TaskExecution_ShouldReturnUnauthorized() + { + var scope = new TestScope(); + var result = await scope.Client.PostAsync($"/devices/1/ping"); + result.ShouldBeUnauthorized(); } } \ No newline at end of file diff --git a/tests/Sentinel.Api.Integration.Tests/Organisation/OrganisationTests.cs b/tests/Sentinel.Api.Integration.Tests/Organisation/OrganisationTests.cs index 36fdc61..c9de031 100644 --- a/tests/Sentinel.Api.Integration.Tests/Organisation/OrganisationTests.cs +++ b/tests/Sentinel.Api.Integration.Tests/Organisation/OrganisationTests.cs @@ -1,46 +1,25 @@ -using System.Net; -using NUnit.Framework; +using NUnit.Framework; using Sentinel.Api.Integration.Tests.Common; namespace Sentinel.Api.Integration.Tests.Organisation; public class OrganisationTests { - private ApiFixture _fixture = null!; - - [SetUp] - public Task Setup() - { - _fixture = new ApiFixture(); - return Task.CompletedTask; - } - - [TearDown] - public async Task TearDown() => await _fixture.DisposeAsync(); - [Test] public async Task Authorized_GetAll_ShouldReturnOK() { - // Arrange - using var client = _fixture.CreateAuthenticatedUser(out _); + await using var scope = await new TestScope().AuthenticateAsUserAsync(); - // Act - var result = await client.GetAsync("/organisations"); + var result = await scope.Client.GetAsync("/organisations"); - // Assert - Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + result.ShouldBeOk(); } [Test] public async Task UnAuthorized_GetAll_ShouldReturnUnauthorized() { - // Arrange - using var client = _fixture.CreateClient(); - - // Act - var result = await client.GetAsync("/organisations"); - - // Assert - Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized)); + await using var scope = new TestScope(); + var result = await scope.Client.GetAsync("/organisations"); + result.ShouldBeUnauthorized(); } } \ No newline at end of file diff --git a/tests/Sentinel.Api.Integration.Tests/Sentinel.Api.Integration.Tests.csproj b/tests/Sentinel.Api.Integration.Tests/Sentinel.Api.Integration.Tests.csproj index 7830091..fb8e3c1 100644 --- a/tests/Sentinel.Api.Integration.Tests/Sentinel.Api.Integration.Tests.csproj +++ b/tests/Sentinel.Api.Integration.Tests/Sentinel.Api.Integration.Tests.csproj @@ -13,6 +13,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -21,7 +22,6 @@ - diff --git a/tests/Sentinel.Api.Integration.Tests/User/Authentication/RefreshTokenTests.cs b/tests/Sentinel.Api.Integration.Tests/User/Authentication/RefreshTokenTests.cs new file mode 100644 index 0000000..3707a43 --- /dev/null +++ b/tests/Sentinel.Api.Integration.Tests/User/Authentication/RefreshTokenTests.cs @@ -0,0 +1,85 @@ +using NUnit.Framework; +using OtpNet; +using Sentinel.Api.Application.Commands.Auth.RefreshToken; +using Sentinel.Api.Application.DTO.Token; +using Sentinel.Api.Application.DTO.User; +using Sentinel.Api.Integration.Tests.Common; + +namespace Sentinel.Api.Integration.Tests.User.Authentication; + +public class RefreshTokenTests +{ + [Test] + public async Task ValidRefreshToken_ShouldReturnNewTokens() + { + await using var scope = new TestScope(); + + var registerDto = new RegisterUserDto { Email = "test@test.com", Password = "password" }; + await scope.Client.PostAsync("/users/register", registerDto); + + var signInDto = new SignInUserDto { Email = "test@test.com", Password = "password" }; + var signInResponse = await scope.Client.PostAsync("/auth/users/sign_in", signInDto); + var signInResult = await signInResponse.Content.DeserializeAsync(); + + var totp = new Totp(Base32Encoding.ToBytes(signInResult!.TwoFactorToken), step: 30, + mode: OtpHashMode.Sha1, totpSize: 6); + + var verifyDto = new VerifyUserDto + { + UserId = signInResult.UserId, + AuthenticityToken = signInResult.AuthenticityToken, + OtpAttempt = totp.ComputeTotp() + }; + var verifyResponse = await scope.Client.PostAsync("/auth/users/verify", verifyDto); + + verifyResponse.ShouldBeOk(); + + var tokens = await verifyResponse.Content.DeserializeAsync(); + Assert.That(tokens, Is.Not.Null, "Tokens should not be null"); + Assert.That(tokens!.AccessToken, Is.Not.Null.And.Not.Empty); + Assert.That(tokens.RefreshToken, Is.Not.Null.And.Not.Empty); + + var refreshCommand = new RefreshTokenCommand( + tokens.AccessToken, + tokens.RefreshToken + ); + + var result = await scope.Client.PostAsync("/auth/refresh", refreshCommand); + + result.ShouldBeOk(); + var newTokens = await result.ShouldDeserializeTo(); + Assert.That(newTokens.AccessToken, Is.Not.Null.And.Not.Empty); + Assert.That(newTokens.RefreshToken, Is.Not.Null.And.Not.Empty); + Assert.That(newTokens.AccessToken, Is.Not.EqualTo(tokens.AccessToken)); + } + + [Test] + public async Task InvalidRefreshToken_ShouldReturnError() + { + await using var scope = new TestScope(); + + var refreshCommand = new RefreshTokenCommand( + "invalid-access-token", + "invalid-refresh-token" + ); + + var result = await scope.Client.PostAsync("/auth/refresh", refreshCommand); + + Assert.That(result.IsSuccessStatusCode, Is.False, "Invalid tokens should not return success"); + } + + [Test] + public async Task EmptyTokens_ShouldReturnBadRequest() + { + await using var scope = new TestScope(); + + var refreshCommand = new RefreshTokenCommand( + string.Empty, + string.Empty + ); + + var result = await scope.Client.PostAsync("/auth/refresh", refreshCommand); + + result.ShouldBeBadRequest(); + } +} diff --git a/tests/Sentinel.Api.Integration.Tests/User/Authentication/RegisterTests.cs b/tests/Sentinel.Api.Integration.Tests/User/Authentication/RegisterTests.cs index 1eec752..dfabbf4 100644 --- a/tests/Sentinel.Api.Integration.Tests/User/Authentication/RegisterTests.cs +++ b/tests/Sentinel.Api.Integration.Tests/User/Authentication/RegisterTests.cs @@ -1,8 +1,6 @@ -using System.Net; -using NUnit.Framework; +using NUnit.Framework; using Sentinel.Api.Application.DTO.User; using Sentinel.Api.Integration.Tests.Common; -using Sentinel.WorkerService.Common.Api.Extensions; namespace Sentinel.Api.Integration.Tests.User.Authentication; @@ -11,38 +9,32 @@ public class RegisterTests [Test] public async Task Correct_Registration_ShouldReturnOK() { - // Arrange - using var client = new ApiFixture().CreateClient(); + await using var scope = new TestScope(); - // Act - var result = await client.PostAsync("/users/register", new RegisterUserDto + var result = await scope.Client.PostAsync("/users/register", new RegisterUserDto { Email = "test@test.com", Password = "password", }); - // Assert - Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + result.ShouldBeOk(); } [Test] public async Task DuplicateEmail_Registration_ShouldReturnForbidden() { - // Arrange - using var client = new ApiFixture().CreateClient(); + await using var scope = new TestScope(); - // Act var user = new RegisterUserDto { Email = "test@test.com", Password = "password", }; - var result1 = await client.PostAsync("/users/register", user); - var result2 = await client.PostAsync("/users/register", user); + var result1 = await scope.Client.PostAsync("/users/register", user); + var result2 = await scope.Client.PostAsync("/users/register", user); - // Assert - Assert.That(result1.StatusCode, Is.EqualTo(HttpStatusCode.OK)); - Assert.That(result2.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden)); + result1.ShouldBeOk(); + result2.ShouldBeForbidden(); } } \ No newline at end of file diff --git a/tests/Sentinel.Api.Integration.Tests/User/Authentication/SignInTests.cs b/tests/Sentinel.Api.Integration.Tests/User/Authentication/SignInTests.cs index 53dc4dc..151aec6 100644 --- a/tests/Sentinel.Api.Integration.Tests/User/Authentication/SignInTests.cs +++ b/tests/Sentinel.Api.Integration.Tests/User/Authentication/SignInTests.cs @@ -1,8 +1,6 @@ -using System.Net; -using NUnit.Framework; +using NUnit.Framework; using Sentinel.Api.Application.DTO.User; using Sentinel.Api.Integration.Tests.Common; -using Sentinel.WorkerService.Common.Api.Extensions; namespace Sentinel.Api.Integration.Tests.User.Authentication; @@ -11,42 +9,33 @@ public class SignInTests [Test] public async Task Correct_Login_ShouldReturnOK() { - // Arrange - using var client = new ApiFixture().CreateClient(); + await using var scope = new TestScope(); - // Act - _ = await client.PostAsync("/users/register", new RegisterUserDto { Email = "test@test.com", Password = "password" }); - var result = await client.PostAsync("/auth/users/sign_in", new SignInUserDto { Email = "test@test.com", Password = "password" }); + _ = await scope.Client.PostAsync("/users/register", new RegisterUserDto { Email = "test@test.com", Password = "password" }); + var result = await scope.Client.PostAsync("/auth/users/sign_in", new SignInUserDto { Email = "test@test.com", Password = "password" }); - // Assert - Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + result.ShouldBeOk(); } [Test] public async Task InvalidPassword_Login_ShouldReturnUnauthorized() { - // Arrange - using var client = new ApiFixture().CreateClient(); + await using var scope = new TestScope(); - // Act - _ = await client.PostAsync("/users/register", new RegisterUserDto { Email = "test@test.com", Password = "password" }); - var result = await client.PostAsync("/auth/users/sign_in", new SignInUserDto { Email = "test@test.com", Password = "hl;asdfljasdjfdaflha;sihjefkldj;aslfjkdsa;dfjasd" }); + _ = await scope.Client.PostAsync("/users/register", new RegisterUserDto { Email = "test@test.com", Password = "password" }); + var result = await scope.Client.PostAsync("/auth/users/sign_in", new SignInUserDto { Email = "test@test.com", Password = "hl;asdfljasdjfdaflha;sihjefkldj;aslfjkdsa;dfjasd" }); - // Assert - Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized)); + result.ShouldBeUnauthorized(); } [Test] public async Task InvalidEmail_Login_ShouldReturnUnauthorized() { - // Arrange - using var client = new ApiFixture().CreateClient(); + await using var scope = new TestScope(); - // Act - _ = await client.PostAsync("/users/register", new RegisterUserDto { Email = "test@test.com", Password = "password" }); - var result = await client.PostAsync("/auth/users/sign_in", new SignInUserDto { Email = "ahjfdkenfine@test.com", Password = "password" }); + _ = await scope.Client.PostAsync("/users/register", new RegisterUserDto { Email = "test@test.com", Password = "password" }); + var result = await scope.Client.PostAsync("/auth/users/sign_in", new SignInUserDto { Email = "ahjfdkenfine@test.com", Password = "password" }); - // Assert - Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized)); + result.ShouldBeUnauthorized(); } } \ No newline at end of file diff --git a/tests/Sentinel.Api.Integration.Tests/User/Authentication/VerificationTests.cs b/tests/Sentinel.Api.Integration.Tests/User/Authentication/VerificationTests.cs index 036f430..94d44ac 100644 --- a/tests/Sentinel.Api.Integration.Tests/User/Authentication/VerificationTests.cs +++ b/tests/Sentinel.Api.Integration.Tests/User/Authentication/VerificationTests.cs @@ -1,10 +1,7 @@ -using System.Net; -using NUnit.Framework; +using NUnit.Framework; using OtpNet; using Sentinel.Api.Application.DTO.User; using Sentinel.Api.Integration.Tests.Common; -using Sentinel.WorkerService.Common.Api.Extensions; -using Sentinel.WorkerService.Common.Extensions; namespace Sentinel.Api.Integration.Tests.User.Authentication; @@ -13,36 +10,31 @@ public class VerificationTests [Test] public async Task Correct_Login_ShouldReturnOK() { - // Arrange - using var client = new ApiFixture().CreateClient(); + await using var scope = new TestScope(); - // Act - _ = await client.PostAsync("/users/register", new RegisterUserDto { Email = "test@test.com", Password = "password" }); - var signInResponse = await client.PostAsync("/auth/users/sign_in", new SignInUserDto { Email = "test@test.com", Password = "password" }); + _ = await scope.Client.PostAsync("/users/register", new RegisterUserDto { Email = "test@test.com", Password = "password" }); + var signInResponse = await scope.Client.PostAsync("/auth/users/sign_in", new SignInUserDto { Email = "test@test.com", Password = "password" }); var signInUserResponse = await signInResponse.Content.DeserializeAsync() ?? throw new Exception("verification response was null") ; var totp = new Totp(Base32Encoding.ToBytes(signInUserResponse.TwoFactorToken), step: 30, mode: OtpHashMode.Sha1, totpSize: 6); - var result = await client.PostAsync("/auth/users/verify", new VerifyUserDto + var result = await scope.Client.PostAsync("/auth/users/verify", new VerifyUserDto { UserId = signInUserResponse.UserId, AuthenticityToken = signInUserResponse.AuthenticityToken, OtpAttempt = totp.ComputeTotp(), }); - // Assert - Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + result.ShouldBeOk(); } [Test] public async Task InvalidPassword_Login_ShouldReturnUnauthorized() { - // Arrange - using var client = new ApiFixture().CreateClient(); - // Act - _ = await client.PostAsync("/users/register", new RegisterUserDto { Email = "test@test.com", Password = "password" }); - var result = await client.PostAsync("/auth/users/sign_in", new SignInUserDto { Email = "test@test.com", Password = "hl;asdfljasdjfdaflha;sihjefkldj;aslfjkdsa;dfjasd" }); + await using var scope = new TestScope(); + + _ = await scope.Client.PostAsync("/users/register", new RegisterUserDto { Email = "test@test.com", Password = "password" }); + var result = await scope.Client.PostAsync("/auth/users/sign_in", new SignInUserDto { Email = "test@test.com", Password = "hl;asdfljasdjfdaflha;sihjefkldj;aslfjkdsa;dfjasd" }); - // Assert - Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized)); + result.ShouldBeUnauthorized(); } } \ No newline at end of file From beb5f8f5a9a490883323f78b3684a368d9ba01cc Mon Sep 17 00:00:00 2001 From: Jelle Buning Date: Wed, 25 Feb 2026 08:40:10 +0100 Subject: [PATCH 2/8] Update Sentinel.Api.Integration.Tests.csproj --- .../Sentinel.Api.Integration.Tests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Sentinel.Api.Integration.Tests/Sentinel.Api.Integration.Tests.csproj b/tests/Sentinel.Api.Integration.Tests/Sentinel.Api.Integration.Tests.csproj index fb8e3c1..d3af567 100644 --- a/tests/Sentinel.Api.Integration.Tests/Sentinel.Api.Integration.Tests.csproj +++ b/tests/Sentinel.Api.Integration.Tests/Sentinel.Api.Integration.Tests.csproj @@ -13,7 +13,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive From 3f9f68d1f4b280c24e21e4b929347ad690843f2b Mon Sep 17 00:00:00 2001 From: Jelle Buning Date: Thu, 5 Mar 2026 17:00:54 +0100 Subject: [PATCH 3/8] minor readme change --- .github/README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/README.md b/.github/README.md index 23715ef..aab21bf 100644 --- a/.github/README.md +++ b/.github/README.md @@ -48,7 +48,6 @@
  • Getting Started
  • @@ -90,8 +89,6 @@ Setting up this solution on your local machine is straightforward and will enabl Before beginning, ensure that your development environment is properly configured. Having the required software and dependencies installed will prevent common issues and streamline the process. -### Prerequisites - ### Installation This installation method utilizes Docker Compose for a streamlined setup. Ensure you have Docker and Docker Compose installed on your system. From 658e085c3d600768ada3c97170d6eb97d9d4afca Mon Sep 17 00:00:00 2001 From: Jelle Buning Date: Mon, 16 Mar 2026 12:05:30 +0100 Subject: [PATCH 4/8] Test improve --- .github/FUNDING.yml | 24 ++- .github/copilot-instructions.md | 149 +++--------------- .github/dependabot.yml | 11 +- .../instructions/architecture.instructions.md | 12 ++ .github/instructions/code.instructions.md | 15 ++ .github/instructions/testing.instructions.md | 7 + Directory.Packages.props | 5 + .../Common/ApiFixtureExtensions.cs | 4 +- .../Common/TestScope.cs | 76 ++++----- .../Device/Management/DeviceRetrievalTests.cs | 2 +- .../Updates/UpdateSoftwareInformationTests.cs | 12 +- .../Updates/UpdateStorageInformationTests.cs | 12 +- .../Device/Worker/PingTaskTests.cs | 2 +- .../User/Authentication/VerificationTests.cs | 13 +- .../User/Modification/UpdateUserTests.cs | 1 + 15 files changed, 120 insertions(+), 225 deletions(-) create mode 100644 .github/instructions/architecture.instructions.md create mode 100644 .github/instructions/code.instructions.md create mode 100644 .github/instructions/testing.instructions.md diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 2c9d09d..9a5b47a 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,13 +1,11 @@ -# These are supported funding model platforms - -github: JelleBuning # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] +github: JelleBuning +patreon: +open_collective: +ko_fi: +tidelift: +community_bridge: +liberapay: +issuehunt: +otechie: +lfx_crowdfunding: +custom: diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f82d216..1f3dd4b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,130 +1,19 @@ -# Copilot Instructions (C#/.NET) — SOLID & Design Patterns First - -## Mission -Produce maintainable, testable, evolvable C# code. **Top priority is SOLID** and applying **appropriate design patterns** over “quick fixes”. - -## Highest Priority Rules (Always) -1. **Correctness > Maintainability > Performance** (unless explicitly told otherwise). -2. **SOLID first**: - - **S**RP: one reason to change per type/module. - - **O**CP: extend with new behavior via abstractions, not by editing many call sites. - - **L**SP: derived types must not break expectations of base types/interfaces. - - **I**SP: small, role-focused interfaces; avoid “god” interfaces. - - **D**IP: depend on abstractions; inject dependencies; avoid static/global state. -3. Prefer **composition over inheritance**. -4. Keep dependencies pointing inward (domain/core should not depend on infrastructure). -5. Make changes **small, reversible, and well-tested**. - -## Core Design Principles -- **KISS** (Keep It Simple, Stupid): Prefer straightforward solutions. Avoid unnecessary complexity, over-engineering, and "clever" code. Clarity and maintainability trump cleverness. -- **DRY** (Don't Repeat Yourself): Eliminate code duplication through abstractions, helper methods, and reusable components. Duplicated logic is a maintenance risk and divergence hazard. -- **YAGNI** (You Aren't Gonna Need It): Don't add features, parameters, or abstractions you don't need today. Speculative generalization introduces complexity; add them when you actually need them. - -## Domain-Driven Design (DDD) -Structure the domain model to reflect business reality: -- **Ubiquitous Language**: Use domain terms consistently across code, conversations, and documentation. Align naming with how domain experts speak. -- **Entities**: Objects with identity and lifecycle; contain business logic tied to their identity. -- **Value Objects**: Immutable, identity-less objects representing domain concepts (e.g., `Money`, `PhoneNumber`). Prefer these when identity doesn't matter. -- **Aggregates**: Cohesive clusters of entities/value objects with a root entity (`AggregateRoot`) that maintains consistency boundaries. - - Model only the aggregates and relationships the business needs; avoid god aggregates. - - Each aggregate should be independently testable. -- **Domain Services**: Stateless operations that don't naturally belong to an entity or value object; they orchestrate domain logic across aggregates. -- **Domain Events**: Capture significant business occurrences (e.g., `UserRegistered`, `OrderPlaced`). Publish them to trigger side effects or integration points. -- **Repositories**: Abstractions that represent collections of aggregates; hide persistence details. Retrieve/persist only aggregate roots. -- **Bounded Contexts**: Define clear boundaries where specific ubiquitous language applies. Different contexts may model the same concept differently; use Anti-Corruption Layers or Context Mappers at boundaries. - -## Architectural Defaults (Unless the repo says otherwise) -- Prefer **Clean Architecture / Onion** style aligned with **Domain-Driven Design**: - - **Domain/Core**: entities, value objects, domain services, domain events, aggregates, and business rules (no infrastructure dependencies). - - **Application**: use-cases, orchestration, application services, ports (interfaces), DTOs, and command/query handlers. - - **Infrastructure**: EF Core, HTTP clients, file system, external services, and repository implementations. - - **Presentation**: Web API / UI / Controllers. -- Use **dependency injection** (Microsoft.Extensions.DependencyInjection). -- Avoid leaking infrastructure types (e.g., EF `DbContext`, `IQueryable`) out of infrastructure. - -## Design Patterns: When to Use What -Apply patterns intentionally—don’t “pattern-fest”. - -- **Strategy**: interchangeable algorithms; choose at runtime (e.g., pricing rules). -- **Factory / Abstract Factory**: complex creation, invariants, environment-specific implementations. -- **Decorator**: cross-cutting behavior (caching, retries) without modifying core services. -- **Adapter**: wrap external SDKs to stable internal interfaces. -- **Facade**: simplify interactions with a complex subsystem. -- **Command**: represent actions/use-cases; supports logging, retries, queues. -- **Mediator (MediatR)**: for request/response or notifications in app layer (only if already used). -- **Repository**: only if it adds value over EF Core usage and boundaries are clear. -- **Unit of Work**: usually EF Core already is; avoid double-abstraction. -- **Specification**: reusable query predicates/validation rules. -- **Observer**: domain events, integration events, notifications. -- **State**: complex state transitions. - -## C#/.NET Coding Standards -- Use modern C# features appropriately: `record` for immutable DTOs/value objects, `init`, pattern matching. -- Prefer immutability by default. -- Prefer `IReadOnlyList` / `IReadOnlyCollection` for outward-facing collections. -- Avoid `async void` (except event handlers). Prefer `CancellationToken` on async APIs. -- **Never use `.Result` or `.Wait()` on tasks** — this causes deadlocks and defeats async/await benefits. Always `await` instead. -- Use expression-bodied members for simple getters/methods. -- Use `using` declarations for disposables when possible. -- Use guard clauses; validate public method arguments. -- Keep public APIs small; internal helpers private/internal. - -### Naming -- Types/Methods: PascalCase; locals: camelCase. -- Interfaces: `IThing`. -- Async methods: `ThingAsync`. -- Tests: `Method_Scenario_ExpectedResult` or `Given_When_Then`. - -## Error Handling & Results -- For application/service boundaries, prefer explicit result types: - - `Result` / `OneOf` / `ErrorOr` (use what the repo already uses). -- Use exceptions for truly exceptional conditions; include context. - -## Testing Expectations -- Prefer **unit tests** for business logic; integration tests for persistence/HTTP. -- Arrange-Act-Assert (AAA). -- Test behavior, not implementation details. -- For time/randomness/IO, inject abstractions (e.g., `ISystemClock`, `IRandom`, file interfaces). -- When changing behavior, **add/adjust tests first** where feasible. - -## Performance Guidance -- Don’t micro-optimize. -- Avoid unnecessary allocations in hot paths; use `Span` only when justified and readable. -- Use streaming for large payloads; avoid loading whole files into memory. -- Prefer `IAsyncEnumerable` for large sequences when appropriate. - -## Security & Reliability -- Treat external input as untrusted; validate and sanitize. -- Avoid string concatenation for SQL; use parameterized queries/EF. -- Use `HttpClientFactory`; set timeouts; handle transient failures (policies if used). -- Don’t log secrets/PII; prefer structured logging. - -## Git & Change Discipline -When asked to implement something: -1. **Clarify requirements** (inputs/outputs, edge cases, constraints) if ambiguous. -2. **Propose a minimal design**: - - responsibilities - - key abstractions/interfaces - - chosen pattern (if any) and why -3. **Implement in small commits** (or small logical steps) if working interactively. -4. **Add/adjust tests**. -5. **Explain tradeoffs** briefly (why this pattern, why these boundaries). - -## “Stop and Ask” Triggers -Ask before proceeding if: -- A choice changes public API shape, persistence schema, or cross-service contracts. -- A new dependency/package is needed. -- There are multiple plausible patterns/architectures and the repo conventions aren’t clear. -- Behavior impacts security, authz/authn, or financial logic. - -## Output Format Expectations -- Prefer producing complete compilable code with necessary `using`s and namespaces. -- Keep diffs focused; don’t reformat unrelated code. -- If unsure of existing conventions, follow EditorConfig / analyzers in the repo. - -## Quick SOLID Checklist (Self-Review) -- Does each class have a single responsibility? -- Are new behaviors added by extension rather than modifying many existing files? -- Can substitutes be used without surprises? -- Are interfaces minimal and role-based? -- Do higher-level modules depend on abstractions rather than concretions? \ No newline at end of file +# Core Philosophy +- Mission: Produce maintainable, testable, evolvable C# code. +- Priority: SOLID first, Design Patterns over "quick fixes". +- Hierarchy: Correctness > Maintainability > Performance. +- Principles: KISS, DRY, YAGNI. + +# Change Discipline +1. Clarify requirements (inputs, outputs, edge cases). +2. Propose a minimal design (responsibilities, abstractions, patterns). +3. Implement in small, reversible, well-tested steps. +4. Explain tradeoffs. + +# Stop and Ask +Ask before: changing public APIs, adding dependencies, choosing between multiple patterns, or impacting security/financial logic. + +# Copilot-specific directives +- When responding to code or design requests, actively consult the other instruction files in `.github/instructions/` (code, architecture, testing, etc.). +- Prioritize the repository’s explicit guidelines over generic advice and ask clarifying questions if the instructions seem to conflict or are incomplete. +- Mention the relevant instruction file in your response when you use its guidance, to reinforce awareness. \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1a87aca..89d3adb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,8 +1,3 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - version: 2 updates: - package-ecosystem: "github-actions" @@ -14,9 +9,9 @@ updates: patterns: - "*" - - package-ecosystem: "nuget" # See documentation for possible values - directory: "/" # Location of package manifests - target-branch: "main" # The branch that dependabot will target + - package-ecosystem: "nuget" + directory: "/" + target-branch: "main" schedule: interval: "weekly" groups: diff --git a/.github/instructions/architecture.instructions.md b/.github/instructions/architecture.instructions.md new file mode 100644 index 0000000..7548312 --- /dev/null +++ b/.github/instructions/architecture.instructions.md @@ -0,0 +1,12 @@ +# Architecture: Clean / Onion +- Domain: entities, value objects, domain services, events (no infrastructure dependencies). +- Application: use-cases, orchestration, ports (interfaces), DTOs, handlers. +- Infrastructure: EF Core, HTTP clients, implementations (hide these from domain). +- Presentation: Web API / UI. + +# DDD Standards +- Ubiquitous Language. +- Aggregates: Use root entities to maintain consistency boundaries. +- Value Objects: Prefer immutable objects for identity-less data. +- Domain Services: For logic that doesn't fit in an entity. +- Bounded Contexts: Clear separation using ACLs if necessary. \ No newline at end of file diff --git a/.github/instructions/code.instructions.md b/.github/instructions/code.instructions.md new file mode 100644 index 0000000..1c0d32b --- /dev/null +++ b/.github/instructions/code.instructions.md @@ -0,0 +1,15 @@ +# Coding Standards +- Modern C#: Use records, pattern matching, init-only properties. +- Immutability: Prefer by default. +- Collections: Use IReadOnlyList/Collection for public APIs. +- Async: No .Result or .Wait(). Use CancellationToken. +- Guard Clauses: Validate arguments. + +# Patterns & Error Handling +- Use patterns (Strategy, Factory, Decorator, etc.) intentionally. +- Results: Use explicit Result or ErrorOr types. +- Exceptions: Only for truly exceptional conditions. + +# Performance +- No micro-optimization. +- Use Span or IAsyncEnumerable only when justified and readable. \ No newline at end of file diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md new file mode 100644 index 0000000..b128756 --- /dev/null +++ b/.github/instructions/testing.instructions.md @@ -0,0 +1,7 @@ +# Testing Strategy +- Unit tests: Business logic. +- Integration tests: Persistence/HTTP layers. +- Pattern: Arrange-Act-Assert (AAA). +- Goal: Test behavior, not implementation. +- Abstractions: Inject interfaces for ISystemClock, IRandom, and IO. +- TDD: Add/adjust tests before implementation when feasible. \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 46158eb..72c2ca2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,6 +4,11 @@ + + + + + diff --git a/tests/Sentinel.Api.Integration.Tests/Common/ApiFixtureExtensions.cs b/tests/Sentinel.Api.Integration.Tests/Common/ApiFixtureExtensions.cs index 26aca04..b1e8ae6 100644 --- a/tests/Sentinel.Api.Integration.Tests/Common/ApiFixtureExtensions.cs +++ b/tests/Sentinel.Api.Integration.Tests/Common/ApiFixtureExtensions.cs @@ -11,8 +11,8 @@ public static class ApiFixtureExtensions { public AppDbContext GetDbContext() { - var provider = fixture.Services.CreateScope().ServiceProvider; - var dbContext = provider.GetRequiredService(); + using var scope = fixture.Services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); dbContext.Database.EnsureCreated(); return dbContext; } diff --git a/tests/Sentinel.Api.Integration.Tests/Common/TestScope.cs b/tests/Sentinel.Api.Integration.Tests/Common/TestScope.cs index ccd4c1c..de70618 100644 --- a/tests/Sentinel.Api.Integration.Tests/Common/TestScope.cs +++ b/tests/Sentinel.Api.Integration.Tests/Common/TestScope.cs @@ -13,7 +13,7 @@ public sealed class TestScope : IAsyncDisposable private readonly ApiFixture _fixture; private Guid? _organisationHash = null; - public HttpClient Client { get; set; } + public HttpClient Client { get; private set; } public AppDbContext DbContext => _fixture.GetDbContext(); public ApiFixture Fixture => _fixture; @@ -83,58 +83,42 @@ public static class HttpClientAuthExtensions { public async Task AuthenticateUserAsync() { - try - { - var signInUserResponse = await client.SignInUserAsync(); - var totp = new Totp(Base32Encoding.ToBytes(signInUserResponse.TwoFactorToken), step: 30, - mode: OtpHashMode.Sha1, totpSize: 6); - - var verifyUserDto = new VerifyUserDto() - { - UserId = signInUserResponse.UserId, - AuthenticityToken = signInUserResponse.AuthenticityToken, - OtpAttempt = totp.ComputeTotp(), - }; - - var verificationResult = await client.PostAsync("/auth/users/verify", verifyUserDto); - var userTokenResponse = await verificationResult.Content.DeserializeAsync() - ?? throw new Exception("verification result was null"); - - client.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue("Bearer", $"{userTokenResponse.AccessToken}"); - - return signInUserResponse; - } - catch (Exception e) + var signInUserResponse = await client.SignInUserAsync(); + var totp = new Totp(Base32Encoding.ToBytes(signInUserResponse.TwoFactorToken), step: 30, + mode: OtpHashMode.Sha1, totpSize: 6); + + var verifyUserDto = new VerifyUserDto() { - Console.WriteLine(e); - return null!; - } + UserId = signInUserResponse.UserId, + AuthenticityToken = signInUserResponse.AuthenticityToken, + OtpAttempt = totp.ComputeTotp(), + }; + + var verificationResult = await client.PostAsync("/auth/users/verify", verifyUserDto); + var userTokenResponse = await verificationResult.Content.DeserializeAsync() + ?? throw new Exception("verification result was null"); + + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", $"{userTokenResponse.AccessToken}"); + + return signInUserResponse; } public async Task RegisterDeviceAsync(Guid organisationHash) { - try + var verificationResult = await client.PostAsync("/devices/register", new RegisterDeviceDto { - var verificationResult = await client.PostAsync("/devices/register", new RegisterDeviceDto - { - Name = "John Doe", - OrganisationHash = organisationHash, - }); + Name = "John Doe", + OrganisationHash = organisationHash, + }); + + var deviceTokenResponse = await verificationResult.Content.DeserializeAsync() + ?? throw new Exception("verification result was null"); - var deviceTokenResponse = await verificationResult.Content.DeserializeAsync() - ?? throw new Exception("verification result was null"); - - client.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue("Bearer", $"{deviceTokenResponse.AccessToken}"); - - return deviceTokenResponse; - } - catch (Exception e) - { - Console.WriteLine(e); - return null!; - } + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", $"{deviceTokenResponse.AccessToken}"); + + return deviceTokenResponse; } private async Task SignInUserAsync() diff --git a/tests/Sentinel.Api.Integration.Tests/Device/Management/DeviceRetrievalTests.cs b/tests/Sentinel.Api.Integration.Tests/Device/Management/DeviceRetrievalTests.cs index c6456c5..f0e6b05 100644 --- a/tests/Sentinel.Api.Integration.Tests/Device/Management/DeviceRetrievalTests.cs +++ b/tests/Sentinel.Api.Integration.Tests/Device/Management/DeviceRetrievalTests.cs @@ -44,8 +44,8 @@ public async Task Authorized_GetDevices_ShouldOnlyReturnOrganisationDevices() await scope.Fixture.AddDeviceAsync(unauthorizedDevice); var result = await scope.Client.GetAsync("/devices"); - var devices = await result.ShouldDeserializeTo(); result.ShouldBeOk(); + var devices = await result.ShouldDeserializeTo(); Assert.That(devices.Devices.Count, Is.EqualTo(1)); Assert.That(devices.Devices.Any(d => d.Name == authorizedDevice.Name), Is.True); Assert.That(devices.Devices.Any(d => d.Name == unauthorizedDevice.Name), Is.False); diff --git a/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateSoftwareInformationTests.cs b/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateSoftwareInformationTests.cs index 93900f7..c70ca13 100644 --- a/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateSoftwareInformationTests.cs +++ b/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateSoftwareInformationTests.cs @@ -14,12 +14,12 @@ public async Task AuthorizedDevice_UpdateSoftwareInformation_ShouldReturnOK() var updateDto = new SoftwareInformationDto { - Software = new List - { + Software = + [ new() { Name = "Google Chrome" }, new() { Name = "Mozilla Firefox" }, new() { Name = "Visual Studio Code" } - } + ] }; var device = scope.Organisation.Devices.Single(); @@ -35,12 +35,12 @@ public async Task UnauthorizedDevice_UpdateSoftwareInformation_ShouldReturnUnaut var updateDto = new SoftwareInformationDto { - Software = new List - { + Software = + [ new() { Name = "Google Chrome" }, new() { Name = "Mozilla Firefox" }, new() { Name = "Visual Studio Code" } - } + ] }; var result = await scope.Client.PutAsync("/devices/1/software", updateDto); diff --git a/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateStorageInformationTests.cs b/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateStorageInformationTests.cs index 1ba523d..2ad23ba 100644 --- a/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateStorageInformationTests.cs +++ b/tests/Sentinel.Api.Integration.Tests/Device/Updates/UpdateStorageInformationTests.cs @@ -14,8 +14,8 @@ public async Task AuthorizedDevice_UpdateStorageInformation_ShouldReturnOK() var updateDto = new StorageInformationDto { - Disks = new List - { + Disks = + [ new() { Name = "C:", @@ -23,7 +23,7 @@ public async Task AuthorizedDevice_UpdateStorageInformation_ShouldReturnOK() Used = 250.5, Size = 500.0 } - } + ] }; var device = scope.DbContext.Devices.Single(); @@ -39,8 +39,8 @@ public async Task UnauthorizedDevice_UpdateStorageInformation_ShouldReturnUnauth var updateDto = new StorageInformationDto { - Disks = new List - { + Disks = + [ new() { Name = "C:", @@ -48,7 +48,7 @@ public async Task UnauthorizedDevice_UpdateStorageInformation_ShouldReturnUnauth Used = 250.5, Size = 500.0 } - } + ] }; var result = await scope.Client.PutAsync("/devices/1/storage", updateDto); diff --git a/tests/Sentinel.Api.Integration.Tests/Device/Worker/PingTaskTests.cs b/tests/Sentinel.Api.Integration.Tests/Device/Worker/PingTaskTests.cs index 32cdbb6..3b5732f 100644 --- a/tests/Sentinel.Api.Integration.Tests/Device/Worker/PingTaskTests.cs +++ b/tests/Sentinel.Api.Integration.Tests/Device/Worker/PingTaskTests.cs @@ -20,7 +20,7 @@ public async Task Authorized_TaskExecution_ShouldReturnOk() [Test] public async Task UnAuthorized_TaskExecution_ShouldReturnUnauthorized() { - var scope = new TestScope(); + await using var scope = new TestScope(); var result = await scope.Client.PostAsync($"/devices/1/ping"); result.ShouldBeUnauthorized(); } diff --git a/tests/Sentinel.Api.Integration.Tests/User/Authentication/VerificationTests.cs b/tests/Sentinel.Api.Integration.Tests/User/Authentication/VerificationTests.cs index 94d44ac..9a667ae 100644 --- a/tests/Sentinel.Api.Integration.Tests/User/Authentication/VerificationTests.cs +++ b/tests/Sentinel.Api.Integration.Tests/User/Authentication/VerificationTests.cs @@ -14,7 +14,7 @@ public async Task Correct_Login_ShouldReturnOK() _ = await scope.Client.PostAsync("/users/register", new RegisterUserDto { Email = "test@test.com", Password = "password" }); var signInResponse = await scope.Client.PostAsync("/auth/users/sign_in", new SignInUserDto { Email = "test@test.com", Password = "password" }); - var signInUserResponse = await signInResponse.Content.DeserializeAsync() ?? throw new Exception("verification response was null") ; + var signInUserResponse = await signInResponse.Content.DeserializeAsync() ?? throw new Exception("verification response was null"); var totp = new Totp(Base32Encoding.ToBytes(signInUserResponse.TwoFactorToken), step: 30, mode: OtpHashMode.Sha1, totpSize: 6); var result = await scope.Client.PostAsync("/auth/users/verify", new VerifyUserDto @@ -26,15 +26,4 @@ public async Task Correct_Login_ShouldReturnOK() result.ShouldBeOk(); } - - [Test] - public async Task InvalidPassword_Login_ShouldReturnUnauthorized() - { - await using var scope = new TestScope(); - - _ = await scope.Client.PostAsync("/users/register", new RegisterUserDto { Email = "test@test.com", Password = "password" }); - var result = await scope.Client.PostAsync("/auth/users/sign_in", new SignInUserDto { Email = "test@test.com", Password = "hl;asdfljasdjfdaflha;sihjefkldj;aslfjkdsa;dfjasd" }); - - result.ShouldBeUnauthorized(); - } } \ No newline at end of file diff --git a/tests/Sentinel.Api.Integration.Tests/User/Modification/UpdateUserTests.cs b/tests/Sentinel.Api.Integration.Tests/User/Modification/UpdateUserTests.cs index ebc7c07..1944a25 100644 --- a/tests/Sentinel.Api.Integration.Tests/User/Modification/UpdateUserTests.cs +++ b/tests/Sentinel.Api.Integration.Tests/User/Modification/UpdateUserTests.cs @@ -5,6 +5,7 @@ namespace Sentinel.Api.Integration.Tests.User.Modification; public class UpdateUserTests { [Test] + [Ignore("Placeholder — not yet implemented")] public void PlaceholderTest() { From 7005374167a20a9db21f9433bbca079c14dc9462 Mon Sep 17 00:00:00 2001 From: Jelle Buning Date: Mon, 16 Mar 2026 16:27:08 +0100 Subject: [PATCH 5/8] removed redundant domainexception introduction --- .../Exceptions/BadValidationRequest.cs | 9 ++------- .../Exceptions/DomainException.cs | 3 --- .../Exceptions/BadRequestException.cs | 2 +- .../Exceptions/ForbiddenException.cs | 2 +- .../Exceptions/InternalServerException.cs | 2 +- .../Exceptions/NotFoundException.cs | 2 +- .../Exceptions/UnauthorizedException.cs | 2 +- 7 files changed, 7 insertions(+), 15 deletions(-) delete mode 100644 src/Sentinel.Api.Application/Exceptions/DomainException.cs diff --git a/src/Sentinel.Api.Application/Exceptions/BadValidationRequest.cs b/src/Sentinel.Api.Application/Exceptions/BadValidationRequest.cs index 45f6eb9..52ab302 100644 --- a/src/Sentinel.Api.Application/Exceptions/BadValidationRequest.cs +++ b/src/Sentinel.Api.Application/Exceptions/BadValidationRequest.cs @@ -1,8 +1,3 @@ -using Sentinel.Api.Infrastructure.Exceptions; +namespace Sentinel.Api.Application.Exceptions; -namespace Sentinel.Api.Application.Exceptions; - -public class BadValidationRequest(string message) : DomainException(message) -{ - -} \ No newline at end of file +public class BadValidationRequest(string message) : Exception(message); diff --git a/src/Sentinel.Api.Application/Exceptions/DomainException.cs b/src/Sentinel.Api.Application/Exceptions/DomainException.cs deleted file mode 100644 index bf7620c..0000000 --- a/src/Sentinel.Api.Application/Exceptions/DomainException.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Sentinel.Api.Infrastructure.Exceptions; - -public class DomainException(string message) : Exception(message); \ No newline at end of file diff --git a/src/Sentinel.Api.Infrastructure/Exceptions/BadRequestException.cs b/src/Sentinel.Api.Infrastructure/Exceptions/BadRequestException.cs index ad148f8..1f67bdc 100644 --- a/src/Sentinel.Api.Infrastructure/Exceptions/BadRequestException.cs +++ b/src/Sentinel.Api.Infrastructure/Exceptions/BadRequestException.cs @@ -1,3 +1,3 @@ namespace Sentinel.Api.Infrastructure.Exceptions; -public class BadRequestException(string message) : DomainException(message); \ No newline at end of file +public class BadRequestException(string message) : Exception(message); \ No newline at end of file diff --git a/src/Sentinel.Api.Infrastructure/Exceptions/ForbiddenException.cs b/src/Sentinel.Api.Infrastructure/Exceptions/ForbiddenException.cs index 569fb83..3e2ac44 100644 --- a/src/Sentinel.Api.Infrastructure/Exceptions/ForbiddenException.cs +++ b/src/Sentinel.Api.Infrastructure/Exceptions/ForbiddenException.cs @@ -1,3 +1,3 @@ namespace Sentinel.Api.Infrastructure.Exceptions; -public class ForbiddenException(string message) : DomainException(message); \ No newline at end of file +public class ForbiddenException(string message) : Exception(message); \ No newline at end of file diff --git a/src/Sentinel.Api.Infrastructure/Exceptions/InternalServerException.cs b/src/Sentinel.Api.Infrastructure/Exceptions/InternalServerException.cs index 86cfecd..bd6dece 100644 --- a/src/Sentinel.Api.Infrastructure/Exceptions/InternalServerException.cs +++ b/src/Sentinel.Api.Infrastructure/Exceptions/InternalServerException.cs @@ -1,3 +1,3 @@ namespace Sentinel.Api.Infrastructure.Exceptions; -public class InternalServerException(string message) : DomainException(message); \ No newline at end of file +public class InternalServerException(string message) : Exception(message); \ No newline at end of file diff --git a/src/Sentinel.Api.Infrastructure/Exceptions/NotFoundException.cs b/src/Sentinel.Api.Infrastructure/Exceptions/NotFoundException.cs index fc1d0fc..087232d 100644 --- a/src/Sentinel.Api.Infrastructure/Exceptions/NotFoundException.cs +++ b/src/Sentinel.Api.Infrastructure/Exceptions/NotFoundException.cs @@ -1,3 +1,3 @@ namespace Sentinel.Api.Infrastructure.Exceptions; -public class NotFoundException(string message) : DomainException(message); \ No newline at end of file +public class NotFoundException(string message) : Exception(message); \ No newline at end of file diff --git a/src/Sentinel.Api.Infrastructure/Exceptions/UnauthorizedException.cs b/src/Sentinel.Api.Infrastructure/Exceptions/UnauthorizedException.cs index a0a7f2a..310f472 100644 --- a/src/Sentinel.Api.Infrastructure/Exceptions/UnauthorizedException.cs +++ b/src/Sentinel.Api.Infrastructure/Exceptions/UnauthorizedException.cs @@ -1,3 +1,3 @@ namespace Sentinel.Api.Infrastructure.Exceptions; -public class UnauthorizedException(string message) : DomainException(message); \ No newline at end of file +public class UnauthorizedException(string message) : Exception(message); \ No newline at end of file From 0ae1fff3155ed63ccbb26dd4a4d2115140a7a01c Mon Sep 17 00:00:00 2001 From: Jelle Buning Date: Thu, 19 Mar 2026 15:46:48 +0100 Subject: [PATCH 6/8] fixed tests --- .../Common/ApiFixtureExtensions.cs | 16 ++++------------ .../Common/TestScope.cs | 9 +++++++-- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/tests/Sentinel.Api.Integration.Tests/Common/ApiFixtureExtensions.cs b/tests/Sentinel.Api.Integration.Tests/Common/ApiFixtureExtensions.cs index b1e8ae6..b90ffd5 100644 --- a/tests/Sentinel.Api.Integration.Tests/Common/ApiFixtureExtensions.cs +++ b/tests/Sentinel.Api.Integration.Tests/Common/ApiFixtureExtensions.cs @@ -9,14 +9,6 @@ public static class ApiFixtureExtensions { extension(ApiFixture fixture) { - public AppDbContext GetDbContext() - { - using var scope = fixture.Services.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - dbContext.Database.EnsureCreated(); - return dbContext; - } - public async Task<(HttpClient client, SignInUserResponse user)> CreateAuthenticatedUserAsync() { var client = fixture.CreateClient(); @@ -33,8 +25,8 @@ public AppDbContext GetDbContext() public async Task AddOrganisationAsync(Guid organisationHash) { - var provider = fixture.Services.CreateScope().ServiceProvider; - await using var dbContext = provider.GetRequiredService(); + using var scope = fixture.Services.CreateScope(); + await using var dbContext = scope.ServiceProvider.GetRequiredService(); await dbContext.Database.EnsureCreatedAsync(); var organisation = new Domain.Entities.Organisation @@ -48,8 +40,8 @@ public AppDbContext GetDbContext() public async Task AddDeviceAsync(Domain.Entities.Device device) { - var provider = fixture.Services.CreateScope().ServiceProvider; - await using var dbContext = provider.GetRequiredService(); + using var scope = fixture.Services.CreateScope(); + await using var dbContext = scope.ServiceProvider.GetRequiredService(); await dbContext.Database.EnsureCreatedAsync(); dbContext.Devices.Add(device); diff --git a/tests/Sentinel.Api.Integration.Tests/Common/TestScope.cs b/tests/Sentinel.Api.Integration.Tests/Common/TestScope.cs index de70618..b199b0d 100644 --- a/tests/Sentinel.Api.Integration.Tests/Common/TestScope.cs +++ b/tests/Sentinel.Api.Integration.Tests/Common/TestScope.cs @@ -1,5 +1,6 @@ using System.Net.Http.Headers; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using OtpNet; using Sentinel.Api.Application.DTO.Device; using Sentinel.Api.Application.DTO.Token; @@ -11,10 +12,11 @@ namespace Sentinel.Api.Integration.Tests.Common; public sealed class TestScope : IAsyncDisposable { private readonly ApiFixture _fixture; + private readonly IServiceScope _scope; private Guid? _organisationHash = null; public HttpClient Client { get; private set; } - public AppDbContext DbContext => _fixture.GetDbContext(); + public AppDbContext DbContext { get; } public ApiFixture Fixture => _fixture; public Domain.Entities.Organisation Organisation => DbContext.Organisations @@ -27,6 +29,9 @@ public sealed class TestScope : IAsyncDisposable public TestScope() { _fixture = new ApiFixture(); + _scope = _fixture.Services.CreateScope(); + DbContext = _scope.ServiceProvider.GetRequiredService(); + DbContext.Database.EnsureCreated(); Client = _fixture.CreateClient(); } @@ -72,8 +77,8 @@ public async Task AddDeviceAsync() public async ValueTask DisposeAsync() { + _scope.Dispose(); await _fixture.DisposeAsync(); - } } From 6810eb73c447a8319c0411d310e0e98a36901cdd Mon Sep 17 00:00:00 2001 From: Jelle Buning Date: Thu, 19 Mar 2026 16:01:13 +0100 Subject: [PATCH 7/8] small improvements --- .../DependencyInjection.cs | 2 +- .../GetAllOrganisationsQuery.cs | 6 ------ .../GetAllOrganisationsQueryHandler.cs | 15 --------------- .../Queries/Organisations/OrganisationsQuery.cs | 6 ++++++ .../Organisations/OrganisationsQueryHandler.cs | 15 +++++++++++++++ .../Records/ExceptionResponse.cs | 3 --- .../SignalR/UserHandler.cs | 6 ++++++ .../Controllers/OrganisationController.cs | 4 ++-- 8 files changed, 30 insertions(+), 27 deletions(-) delete mode 100644 src/Sentinel.Api.Application/Queries/Organisations/GetAllOrganisations/GetAllOrganisationsQuery.cs delete mode 100644 src/Sentinel.Api.Application/Queries/Organisations/GetAllOrganisations/GetAllOrganisationsQueryHandler.cs create mode 100644 src/Sentinel.Api.Application/Queries/Organisations/OrganisationsQuery.cs create mode 100644 src/Sentinel.Api.Application/Queries/Organisations/OrganisationsQueryHandler.cs delete mode 100644 src/Sentinel.Api.Application/Records/ExceptionResponse.cs create mode 100644 src/Sentinel.Api.Infrastructure/SignalR/UserHandler.cs diff --git a/src/Sentinel.Api.Application/DependencyInjection.cs b/src/Sentinel.Api.Application/DependencyInjection.cs index ee02c99..122fda0 100644 --- a/src/Sentinel.Api.Application/DependencyInjection.cs +++ b/src/Sentinel.Api.Application/DependencyInjection.cs @@ -20,7 +20,7 @@ public IServiceCollection AddApplication() return services; } - private void AddMediatorServices(Assembly assembly) + private void AddMediatorServices(Assembly _) { services.AddMediator(options => { diff --git a/src/Sentinel.Api.Application/Queries/Organisations/GetAllOrganisations/GetAllOrganisationsQuery.cs b/src/Sentinel.Api.Application/Queries/Organisations/GetAllOrganisations/GetAllOrganisationsQuery.cs deleted file mode 100644 index 40131a9..0000000 --- a/src/Sentinel.Api.Application/Queries/Organisations/GetAllOrganisations/GetAllOrganisationsQuery.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Mediator; -using Sentinel.Api.Domain.Entities; - -namespace Sentinel.Api.Application.Queries.Organisations.GetAllOrganisations; - -public record GetAllOrganisationsQuery : IRequest>; diff --git a/src/Sentinel.Api.Application/Queries/Organisations/GetAllOrganisations/GetAllOrganisationsQueryHandler.cs b/src/Sentinel.Api.Application/Queries/Organisations/GetAllOrganisations/GetAllOrganisationsQueryHandler.cs deleted file mode 100644 index 952c7df..0000000 --- a/src/Sentinel.Api.Application/Queries/Organisations/GetAllOrganisations/GetAllOrganisationsQueryHandler.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Mediator; -using Sentinel.Api.Application.Interfaces; -using Sentinel.Api.Domain.Entities; - -namespace Sentinel.Api.Application.Queries.Organisations.GetAllOrganisations; - -public class GetAllOrganisationsQueryHandler(IOrganisationRepository organisationRepository) - : IRequestHandler> -{ - public ValueTask> Handle(GetAllOrganisationsQuery request, CancellationToken cancellationToken) - { - var organisations = organisationRepository.GetAll(); - return ValueTask.FromResult(organisations); - } -} diff --git a/src/Sentinel.Api.Application/Queries/Organisations/OrganisationsQuery.cs b/src/Sentinel.Api.Application/Queries/Organisations/OrganisationsQuery.cs new file mode 100644 index 0000000..5dd3e20 --- /dev/null +++ b/src/Sentinel.Api.Application/Queries/Organisations/OrganisationsQuery.cs @@ -0,0 +1,6 @@ +using Mediator; +using Sentinel.Api.Domain.Entities; + +namespace Sentinel.Api.Application.Queries.Organisations; + +public record OrganisationsQuery : IRequest>; diff --git a/src/Sentinel.Api.Application/Queries/Organisations/OrganisationsQueryHandler.cs b/src/Sentinel.Api.Application/Queries/Organisations/OrganisationsQueryHandler.cs new file mode 100644 index 0000000..e968690 --- /dev/null +++ b/src/Sentinel.Api.Application/Queries/Organisations/OrganisationsQueryHandler.cs @@ -0,0 +1,15 @@ +using Mediator; +using Sentinel.Api.Application.Interfaces; +using Sentinel.Api.Domain.Entities; + +namespace Sentinel.Api.Application.Queries.Organisations; + +public class OrganisationsQueryHandler(IOrganisationRepository organisationRepository) + : IRequestHandler> +{ + public ValueTask> Handle(OrganisationsQuery request, CancellationToken cancellationToken) + { + var organisations = organisationRepository.GetAll(); + return ValueTask.FromResult(organisations); + } +} diff --git a/src/Sentinel.Api.Application/Records/ExceptionResponse.cs b/src/Sentinel.Api.Application/Records/ExceptionResponse.cs deleted file mode 100644 index 8099ce0..0000000 --- a/src/Sentinel.Api.Application/Records/ExceptionResponse.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Sentinel.Api.Application.Records; - -public record ExceptionResponse(int StatusCode, Exception Exception); \ No newline at end of file diff --git a/src/Sentinel.Api.Infrastructure/SignalR/UserHandler.cs b/src/Sentinel.Api.Infrastructure/SignalR/UserHandler.cs new file mode 100644 index 0000000..bc781ce --- /dev/null +++ b/src/Sentinel.Api.Infrastructure/SignalR/UserHandler.cs @@ -0,0 +1,6 @@ +namespace Sentinel.Api.Infrastructure.SignalR; + +public static class UserHandler +{ + public static readonly HashSet ConnectedIds = []; +} \ No newline at end of file diff --git a/src/Sentinel.Api/Controllers/OrganisationController.cs b/src/Sentinel.Api/Controllers/OrganisationController.cs index 98877d5..b8a43ee 100644 --- a/src/Sentinel.Api/Controllers/OrganisationController.cs +++ b/src/Sentinel.Api/Controllers/OrganisationController.cs @@ -1,7 +1,7 @@ using Mediator; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Sentinel.Api.Application.Queries.Organisations.GetAllOrganisations; +using Sentinel.Api.Application.Queries.Organisations; namespace Sentinel.Api.Controllers; @@ -14,7 +14,7 @@ public class OrganisationController(ISender sender) : Controller [HttpGet] public async Task GetOrganisations() { - var result = await sender.Send(new GetAllOrganisationsQuery()); + var result = await sender.Send(new OrganisationsQuery()); return Ok(result); } } From 073fa0b3d2bb98b1fd68e5853d18fdf40c59741b Mon Sep 17 00:00:00 2001 From: Jelle Buning Date: Thu, 19 Mar 2026 16:06:18 +0100 Subject: [PATCH 8/8] Moved userhandler back --- .../Mediator/Behaviors/UnhandledExceptionBehavior.cs | 1 - src/Sentinel.Api.Infrastructure/DependencyInjection.cs | 2 -- .../Middleware/ExceptionHandlingMiddleware.cs | 3 +-- src/Sentinel.Api.Infrastructure/SignalR/UserHandler.cs | 6 ------ src/Sentinel.Api/Controllers/DeviceController.cs | 1 - 5 files changed, 1 insertion(+), 12 deletions(-) delete mode 100644 src/Sentinel.Api.Infrastructure/SignalR/UserHandler.cs diff --git a/src/Sentinel.Api.Application/Mediator/Behaviors/UnhandledExceptionBehavior.cs b/src/Sentinel.Api.Application/Mediator/Behaviors/UnhandledExceptionBehavior.cs index 04c570d..67dc570 100644 --- a/src/Sentinel.Api.Application/Mediator/Behaviors/UnhandledExceptionBehavior.cs +++ b/src/Sentinel.Api.Application/Mediator/Behaviors/UnhandledExceptionBehavior.cs @@ -1,4 +1,3 @@ -using FluentValidation; using Mediator; using Microsoft.Extensions.Logging; using Sentinel.Api.Application.Exceptions; diff --git a/src/Sentinel.Api.Infrastructure/DependencyInjection.cs b/src/Sentinel.Api.Infrastructure/DependencyInjection.cs index 07c54f4..d931b59 100644 --- a/src/Sentinel.Api.Infrastructure/DependencyInjection.cs +++ b/src/Sentinel.Api.Infrastructure/DependencyInjection.cs @@ -9,8 +9,6 @@ using Sentinel.Api.Application.Interfaces; using Sentinel.Api.Application.Services; using Sentinel.Api.Application.Services.Interfaces; -using Sentinel.Api.Infrastructure.Exceptions; -using Sentinel.Api.Infrastructure.Middleware; using Sentinel.Api.Infrastructure.Persistence; using Sentinel.Api.Infrastructure.Repositories; using Serilog; diff --git a/src/Sentinel.Api.Infrastructure/Middleware/ExceptionHandlingMiddleware.cs b/src/Sentinel.Api.Infrastructure/Middleware/ExceptionHandlingMiddleware.cs index 1de80e5..b1d11e5 100644 --- a/src/Sentinel.Api.Infrastructure/Middleware/ExceptionHandlingMiddleware.cs +++ b/src/Sentinel.Api.Infrastructure/Middleware/ExceptionHandlingMiddleware.cs @@ -1,5 +1,4 @@ -using FluentValidation; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Sentinel.Api.Application.Exceptions; using Sentinel.Api.Infrastructure.Exceptions; namespace Sentinel.Api.Infrastructure.Middleware; diff --git a/src/Sentinel.Api.Infrastructure/SignalR/UserHandler.cs b/src/Sentinel.Api.Infrastructure/SignalR/UserHandler.cs deleted file mode 100644 index bc781ce..0000000 --- a/src/Sentinel.Api.Infrastructure/SignalR/UserHandler.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Sentinel.Api.Infrastructure.SignalR; - -public static class UserHandler -{ - public static readonly HashSet ConnectedIds = []; -} \ No newline at end of file diff --git a/src/Sentinel.Api/Controllers/DeviceController.cs b/src/Sentinel.Api/Controllers/DeviceController.cs index 3defb30..fda888f 100644 --- a/src/Sentinel.Api/Controllers/DeviceController.cs +++ b/src/Sentinel.Api/Controllers/DeviceController.cs @@ -8,7 +8,6 @@ using Sentinel.Api.Application.Commands.Devices.Update.SoftwareInformation; using Sentinel.Api.Application.Commands.Devices.Update.StorageInformation; using Sentinel.Api.Application.DTO.Device; -using Sentinel.Api.Extensions; using Sentinel.Common.DTO.Device; using Sentinel.Common.DTO.Device.Information;