Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -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:
3 changes: 0 additions & 3 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
<li>
<a href="#getting-started">Getting Started</a>
<ul>
<li><a href="#prerequisites">Prerequisites</a></li>
<li><a href="#installation">Installation</a></li>
</ul>
</li>
Expand Down Expand Up @@ -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.

Expand Down
19 changes: 19 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 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.
11 changes: 3 additions & 8 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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:
Expand Down
12 changes: 12 additions & 0 deletions .github/instructions/architecture.instructions.md
Original file line number Diff line number Diff line change
@@ -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.
15 changes: 15 additions & 0 deletions .github/instructions/code.instructions.md
Original file line number Diff line number Diff line change
@@ -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<T> or ErrorOr<T> types.
- Exceptions: Only for truly exceptional conditions.

# Performance
- No micro-optimization.
- Use Span<T> or IAsyncEnumerable<T> only when justified and readable.
7 changes: 7 additions & 0 deletions .github/instructions/testing.instructions.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
<PackageVersion Include="Microsoft.Extensions.Hosting.Systemd" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.5" />
<PackageVersion Include="EntityFrameworkCore.CommonTools" Version="2.0.2" />
<PackageVersion Include="FluentValidation" Version="11.11.0" />
<PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
<PackageVersion Include="Mediator.Abstractions" Version="3.0.1" />
<PackageVersion Include="Mediator.SourceGenerator" Version="3.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
<PackageVersion Include="Otp.NET" Version="1.4.1" />
<PackageVersion Include="BCrypt.Net-Next" Version="4.1.0" />
Expand Down
1 change: 0 additions & 1 deletion Sentinel.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
<Project Path="src/Sentinel.WorkerService.Core/Sentinel.WorkerService.Core.csproj" />
<Project Path="src/Sentinel.WorkerService.RemoteAccess/Sentinel.WorkerService.RemoteAccess.csproj" />
<Project Path="src/Sentinel.WorkerService/Sentinel.WorkerService.csproj" />
<File Path="src/Directory.Packages.props" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/Sentinel.Api.Integration.Tests/Sentinel.Api.Integration.Tests.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SignInUserResponse>;
Original file line number Diff line number Diff line change
@@ -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<LoginCommand, SignInUserResponse>
{
public async ValueTask<SignInUserResponse> Handle(LoginCommand request, CancellationToken cancellationToken)
{
var signInDto = new SignInUserDto
{
Email = request.Email,
Password = request.Password
};

return await authRepository.AuthenticateAsync(signInDto);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using FluentValidation;

namespace Sentinel.Api.Application.Commands.Auth.Login;

public class LoginCommandValidator : AbstractValidator<LoginCommand>
{
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");
}
}
Original file line number Diff line number Diff line change
@@ -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<TokenDto>;
Original file line number Diff line number Diff line change
@@ -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<RefreshTokenCommand, TokenDto>
{
public async ValueTask<TokenDto> Handle(RefreshTokenCommand request, CancellationToken cancellationToken)
{
var tokenDto = new TokenDto
{
AccessToken = request.AccessToken,
RefreshToken = request.RefreshToken
};

return await authRepository.RefreshTokenAsync(tokenDto);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using FluentValidation;

namespace Sentinel.Api.Application.Commands.Auth.RefreshToken;

public class RefreshTokenCommandValidator : AbstractValidator<RefreshTokenCommand>
{
public RefreshTokenCommandValidator()
{
RuleFor(x => x.AccessToken)
.NotEmpty().WithMessage("AccessToken is required");

RuleFor(x => x.RefreshToken)
.NotEmpty().WithMessage("RefreshToken is required");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Mediator;

namespace Sentinel.Api.Application.Commands.Auth.VerifyTotp;

public record VerifyTotpCommand(int UserId, string AuthenticityToken, string OtpAttempt) : IRequest<DTO.Token.TokenDto>;
Original file line number Diff line number Diff line change
@@ -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<VerifyTotpCommand, TokenDto>
{
public async ValueTask<TokenDto> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using FluentValidation;

namespace Sentinel.Api.Application.Commands.Auth.VerifyTotp;

public class VerifyTotpCommandValidator : AbstractValidator<VerifyTotpCommand>
{
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");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Mediator;

namespace Sentinel.Api.Application.Commands.Devices.ExecuteSecurityScan;

public record ExecuteSecurityScanCommand(int DeviceId) : IRequest;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Mediator;
using Sentinel.Api.Application.Interfaces;

namespace Sentinel.Api.Application.Commands.Devices.ExecuteSecurityScan;

public class ExecuteSecurityScanCommandHandler(IDeviceMessenger deviceMessenger)
: IRequestHandler<ExecuteSecurityScanCommand>
{
public async ValueTask<Unit> Handle(ExecuteSecurityScanCommand request, CancellationToken cancellationToken)
{
await deviceMessenger.SendSecurityScanRequestAsync(request.DeviceId, cancellationToken);
return Unit.Value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Mediator;

namespace Sentinel.Api.Application.Commands.Devices.Ping;

public record PingDeviceCommand(int DeviceId) : IRequest;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Mediator;
using Sentinel.Api.Application.Interfaces;

namespace Sentinel.Api.Application.Commands.Devices.Ping;

public class PingDeviceCommandHandler(IDeviceRepository deviceRepository) : IRequestHandler<PingDeviceCommand>
{
public ValueTask<Unit> Handle(PingDeviceCommand request, CancellationToken cancellationToken)
{
deviceRepository.Ping(request.DeviceId);
return ValueTask.FromResult(Unit.Value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using FluentValidation;

namespace Sentinel.Api.Application.Commands.Devices.Ping;

public class PingDeviceCommandValidator : AbstractValidator<PingDeviceCommand>
{
public PingDeviceCommandValidator()
{
RuleFor(x => x.DeviceId)
.GreaterThan(0).WithMessage("DeviceId must be greater than 0");
}
}
Original file line number Diff line number Diff line change
@@ -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<DeviceTokenResponse>;
Original file line number Diff line number Diff line change
@@ -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<RegisterDeviceCommand, DeviceTokenResponse>
{
public ValueTask<DeviceTokenResponse> Handle(RegisterDeviceCommand request, CancellationToken cancellationToken)
{
var response = deviceRepository.Register(request.OrganisationHash, request.Name);
return ValueTask.FromResult(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using FluentValidation;

namespace Sentinel.Api.Application.Commands.Devices.Register;

public class RegisterDeviceCommandValidator : AbstractValidator<RegisterDeviceCommand>
{
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");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Mediator;

namespace Sentinel.Api.Application.Commands.Devices.RequestRemoteAccess;

public record RequestRemoteAccessCommand(int DeviceId) : IRequest;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Mediator;
using Sentinel.Api.Application.Interfaces;

namespace Sentinel.Api.Application.Commands.Devices.RequestRemoteAccess;

public class RequestRemoteAccessCommandHandler(IDeviceMessenger deviceMessenger)
: IRequestHandler<RequestRemoteAccessCommand>
{
public async ValueTask<Unit> Handle(RequestRemoteAccessCommand request, CancellationToken cancellationToken)
{
await deviceMessenger.SendRemoteAccessRequestAsync(request.DeviceId, cancellationToken);
return Unit.Value;
}
}
Loading