feat(infrastructure): add data access and authentication services
This commit is contained in:
20
src/IncidentOps.Infrastructure/Auth/IPasswordService.cs
Normal file
20
src/IncidentOps.Infrastructure/Auth/IPasswordService.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace IncidentOps.Infrastructure.Auth;
|
||||
|
||||
public interface IPasswordService
|
||||
{
|
||||
string HashPassword(string password);
|
||||
bool VerifyPassword(string password, string hash);
|
||||
}
|
||||
|
||||
public class PasswordService : IPasswordService
|
||||
{
|
||||
public string HashPassword(string password)
|
||||
{
|
||||
return BCrypt.Net.BCrypt.HashPassword(password);
|
||||
}
|
||||
|
||||
public bool VerifyPassword(string password, string hash)
|
||||
{
|
||||
return BCrypt.Net.BCrypt.Verify(password, hash);
|
||||
}
|
||||
}
|
||||
63
src/IncidentOps.Infrastructure/Auth/ITokenService.cs
Normal file
63
src/IncidentOps.Infrastructure/Auth/ITokenService.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using IncidentOps.Domain.Enums;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace IncidentOps.Infrastructure.Auth;
|
||||
|
||||
public interface ITokenService
|
||||
{
|
||||
string GenerateAccessToken(Guid userId, Guid orgId, OrgRole orgRole);
|
||||
string GenerateRefreshToken();
|
||||
string HashToken(string token);
|
||||
}
|
||||
|
||||
public class TokenService : ITokenService
|
||||
{
|
||||
private readonly JwtSettings _settings;
|
||||
|
||||
public TokenService(JwtSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public string GenerateAccessToken(Guid userId, Guid orgId, OrgRole orgRole)
|
||||
{
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.SigningKey));
|
||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
|
||||
new Claim("org_id", orgId.ToString()),
|
||||
new Claim("org_role", orgRole.ToString().ToLowerInvariant()),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||
};
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: _settings.Issuer,
|
||||
audience: _settings.Audience,
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.AddMinutes(_settings.AccessTokenExpirationMinutes),
|
||||
signingCredentials: credentials
|
||||
);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
public string GenerateRefreshToken()
|
||||
{
|
||||
var randomBytes = new byte[64];
|
||||
using var rng = RandomNumberGenerator.Create();
|
||||
rng.GetBytes(randomBytes);
|
||||
return Convert.ToBase64String(randomBytes);
|
||||
}
|
||||
|
||||
public string HashToken(string token)
|
||||
{
|
||||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token));
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
}
|
||||
10
src/IncidentOps.Infrastructure/Auth/JwtSettings.cs
Normal file
10
src/IncidentOps.Infrastructure/Auth/JwtSettings.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace IncidentOps.Infrastructure.Auth;
|
||||
|
||||
public class JwtSettings
|
||||
{
|
||||
public required string Issuer { get; set; }
|
||||
public required string Audience { get; set; }
|
||||
public required string SigningKey { get; set; }
|
||||
public int AccessTokenExpirationMinutes { get; set; } = 15;
|
||||
public int RefreshTokenExpirationDays { get; set; } = 7;
|
||||
}
|
||||
24
src/IncidentOps.Infrastructure/Data/DbConnectionFactory.cs
Normal file
24
src/IncidentOps.Infrastructure/Data/DbConnectionFactory.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Data;
|
||||
using Npgsql;
|
||||
|
||||
namespace IncidentOps.Infrastructure.Data;
|
||||
|
||||
public interface IDbConnectionFactory
|
||||
{
|
||||
IDbConnection CreateConnection();
|
||||
}
|
||||
|
||||
public class DbConnectionFactory : IDbConnectionFactory
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
|
||||
public DbConnectionFactory(string connectionString)
|
||||
{
|
||||
_connectionString = connectionString;
|
||||
}
|
||||
|
||||
public IDbConnection CreateConnection()
|
||||
{
|
||||
return new NpgsqlConnection(_connectionString);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using IncidentOps.Domain.Entities;
|
||||
|
||||
namespace IncidentOps.Infrastructure.Data.Repositories;
|
||||
|
||||
public interface IIncidentEventRepository
|
||||
{
|
||||
Task<IReadOnlyList<IncidentEvent>> GetByIncidentIdAsync(Guid incidentId);
|
||||
Task<IncidentEvent> CreateAsync(IncidentEvent incidentEvent);
|
||||
}
|
||||
|
||||
public class IncidentEventRepository : IIncidentEventRepository
|
||||
{
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
public IncidentEventRepository(IDbConnectionFactory connectionFactory)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<IncidentEvent>> GetByIncidentIdAsync(Guid incidentId)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = "SELECT * FROM incident_events WHERE incident_id = @IncidentId ORDER BY created_at ASC";
|
||||
var result = await Dapper.SqlMapper.QueryAsync<IncidentEvent>(connection, sql, new { IncidentId = incidentId });
|
||||
return result.ToList();
|
||||
}
|
||||
|
||||
public async Task<IncidentEvent> CreateAsync(IncidentEvent incidentEvent)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = @"
|
||||
INSERT INTO incident_events (id, incident_id, event_type, actor_user_id, payload, created_at)
|
||||
VALUES (@Id, @IncidentId, @EventType, @ActorUserId, @Payload, @CreatedAt)
|
||||
RETURNING *";
|
||||
return await Dapper.SqlMapper.QuerySingleAsync<IncidentEvent>(connection, sql, new
|
||||
{
|
||||
incidentEvent.Id,
|
||||
incidentEvent.IncidentId,
|
||||
EventType = incidentEvent.EventType.ToString().ToLowerInvariant(),
|
||||
incidentEvent.ActorUserId,
|
||||
incidentEvent.Payload,
|
||||
incidentEvent.CreatedAt
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using IncidentOps.Domain.Entities;
|
||||
using IncidentOps.Domain.Enums;
|
||||
|
||||
namespace IncidentOps.Infrastructure.Data.Repositories;
|
||||
|
||||
public interface IIncidentRepository
|
||||
{
|
||||
Task<Incident?> GetByIdAsync(Guid id, Guid orgId);
|
||||
Task<IReadOnlyList<Incident>> GetByOrgIdAsync(Guid orgId, IncidentStatus? status, int limit, string? cursor);
|
||||
Task<Incident> CreateAsync(Incident incident);
|
||||
Task<bool> TransitionAsync(Guid id, Guid orgId, int expectedVersion, IncidentStatus newStatus, DateTime timestamp);
|
||||
}
|
||||
|
||||
public class IncidentRepository : IIncidentRepository
|
||||
{
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
public IncidentRepository(IDbConnectionFactory connectionFactory)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public async Task<Incident?> GetByIdAsync(Guid id, Guid orgId)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = "SELECT * FROM incidents WHERE id = @Id AND org_id = @OrgId";
|
||||
return await Dapper.SqlMapper.QuerySingleOrDefaultAsync<Incident>(connection, sql, new { Id = id, OrgId = orgId });
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Incident>> GetByOrgIdAsync(Guid orgId, IncidentStatus? status, int limit, string? cursor)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
var sql = "SELECT * FROM incidents WHERE org_id = @OrgId";
|
||||
if (status.HasValue)
|
||||
sql += " AND status = @Status";
|
||||
if (!string.IsNullOrEmpty(cursor))
|
||||
sql += " AND created_at < @Cursor";
|
||||
sql += " ORDER BY created_at DESC LIMIT @Limit";
|
||||
|
||||
var result = await Dapper.SqlMapper.QueryAsync<Incident>(connection, sql, new
|
||||
{
|
||||
OrgId = orgId,
|
||||
Status = status?.ToString().ToLowerInvariant(),
|
||||
Cursor = cursor != null ? DateTime.Parse(cursor) : (DateTime?)null,
|
||||
Limit = limit
|
||||
});
|
||||
return result.ToList();
|
||||
}
|
||||
|
||||
public async Task<Incident> CreateAsync(Incident incident)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = @"
|
||||
INSERT INTO incidents (id, org_id, service_id, title, description, status, version, created_at)
|
||||
VALUES (@Id, @OrgId, @ServiceId, @Title, @Description, @Status, @Version, @CreatedAt)
|
||||
RETURNING *";
|
||||
return await Dapper.SqlMapper.QuerySingleAsync<Incident>(connection, sql, new
|
||||
{
|
||||
incident.Id,
|
||||
incident.OrgId,
|
||||
incident.ServiceId,
|
||||
incident.Title,
|
||||
incident.Description,
|
||||
Status = incident.Status.ToString().ToLowerInvariant(),
|
||||
incident.Version,
|
||||
incident.CreatedAt
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<bool> TransitionAsync(Guid id, Guid orgId, int expectedVersion, IncidentStatus newStatus, DateTime timestamp)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
var timestampColumn = newStatus switch
|
||||
{
|
||||
IncidentStatus.Acknowledged => "acknowledged_at",
|
||||
IncidentStatus.Mitigated => "mitigated_at",
|
||||
IncidentStatus.Resolved => "resolved_at",
|
||||
_ => null
|
||||
};
|
||||
|
||||
var sql = $@"
|
||||
UPDATE incidents
|
||||
SET status = @Status, version = version + 1{(timestampColumn != null ? $", {timestampColumn} = @Timestamp" : "")}
|
||||
WHERE id = @Id AND org_id = @OrgId AND version = @ExpectedVersion";
|
||||
|
||||
var affected = await Dapper.SqlMapper.ExecuteAsync(connection, sql, new
|
||||
{
|
||||
Id = id,
|
||||
OrgId = orgId,
|
||||
Status = newStatus.ToString().ToLowerInvariant(),
|
||||
ExpectedVersion = expectedVersion,
|
||||
Timestamp = timestamp
|
||||
});
|
||||
|
||||
return affected > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using IncidentOps.Domain.Entities;
|
||||
|
||||
namespace IncidentOps.Infrastructure.Data.Repositories;
|
||||
|
||||
public interface INotificationTargetRepository
|
||||
{
|
||||
Task<IReadOnlyList<NotificationTarget>> GetByOrgIdAsync(Guid orgId);
|
||||
Task<IReadOnlyList<NotificationTarget>> GetEnabledByOrgIdAsync(Guid orgId);
|
||||
Task<NotificationTarget> CreateAsync(NotificationTarget target);
|
||||
}
|
||||
|
||||
public class NotificationTargetRepository : INotificationTargetRepository
|
||||
{
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
public NotificationTargetRepository(IDbConnectionFactory connectionFactory)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<NotificationTarget>> GetByOrgIdAsync(Guid orgId)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = "SELECT * FROM notification_targets WHERE org_id = @OrgId ORDER BY name";
|
||||
var result = await Dapper.SqlMapper.QueryAsync<NotificationTarget>(connection, sql, new { OrgId = orgId });
|
||||
return result.ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<NotificationTarget>> GetEnabledByOrgIdAsync(Guid orgId)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = "SELECT * FROM notification_targets WHERE org_id = @OrgId AND is_enabled = true ORDER BY name";
|
||||
var result = await Dapper.SqlMapper.QueryAsync<NotificationTarget>(connection, sql, new { OrgId = orgId });
|
||||
return result.ToList();
|
||||
}
|
||||
|
||||
public async Task<NotificationTarget> CreateAsync(NotificationTarget target)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = @"
|
||||
INSERT INTO notification_targets (id, org_id, name, target_type, configuration, is_enabled, created_at)
|
||||
VALUES (@Id, @OrgId, @Name, @TargetType, @Configuration, @IsEnabled, @CreatedAt)
|
||||
RETURNING *";
|
||||
return await Dapper.SqlMapper.QuerySingleAsync<NotificationTarget>(connection, sql, new
|
||||
{
|
||||
target.Id,
|
||||
target.OrgId,
|
||||
target.Name,
|
||||
TargetType = target.TargetType.ToString().ToLowerInvariant(),
|
||||
target.Configuration,
|
||||
target.IsEnabled,
|
||||
target.CreatedAt
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using IncidentOps.Domain.Entities;
|
||||
using IncidentOps.Domain.Enums;
|
||||
|
||||
namespace IncidentOps.Infrastructure.Data.Repositories;
|
||||
|
||||
public interface IOrgMemberRepository
|
||||
{
|
||||
Task<OrgMember?> GetByUserAndOrgAsync(Guid userId, Guid orgId);
|
||||
Task<IReadOnlyList<OrgMember>> GetByOrgIdAsync(Guid orgId);
|
||||
Task<OrgMember> CreateAsync(OrgMember member);
|
||||
}
|
||||
|
||||
public class OrgMemberRepository : IOrgMemberRepository
|
||||
{
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
public OrgMemberRepository(IDbConnectionFactory connectionFactory)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public async Task<OrgMember?> GetByUserAndOrgAsync(Guid userId, Guid orgId)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = "SELECT * FROM org_members WHERE user_id = @UserId AND org_id = @OrgId";
|
||||
return await Dapper.SqlMapper.QuerySingleOrDefaultAsync<OrgMember>(connection, sql, new { UserId = userId, OrgId = orgId });
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<OrgMember>> GetByOrgIdAsync(Guid orgId)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = "SELECT * FROM org_members WHERE org_id = @OrgId";
|
||||
var result = await Dapper.SqlMapper.QueryAsync<OrgMember>(connection, sql, new { OrgId = orgId });
|
||||
return result.ToList();
|
||||
}
|
||||
|
||||
public async Task<OrgMember> CreateAsync(OrgMember member)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = @"
|
||||
INSERT INTO org_members (id, org_id, user_id, role, created_at)
|
||||
VALUES (@Id, @OrgId, @UserId, @Role, @CreatedAt)
|
||||
RETURNING *";
|
||||
return await Dapper.SqlMapper.QuerySingleAsync<OrgMember>(connection, sql, member);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace IncidentOps.Infrastructure.Data.Repositories;
|
||||
|
||||
public interface IOrgRepository
|
||||
{
|
||||
Task<Domain.Entities.Org?> GetByIdAsync(Guid id);
|
||||
Task<Domain.Entities.Org> CreateAsync(Domain.Entities.Org org);
|
||||
Task<IReadOnlyList<Domain.Entities.Org>> GetByUserIdAsync(Guid userId);
|
||||
}
|
||||
|
||||
public class OrgRepository : IOrgRepository
|
||||
{
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
public OrgRepository(IDbConnectionFactory connectionFactory)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public async Task<Domain.Entities.Org?> GetByIdAsync(Guid id)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = "SELECT * FROM orgs WHERE id = @Id";
|
||||
return await Dapper.SqlMapper.QuerySingleOrDefaultAsync<Domain.Entities.Org>(connection, sql, new { Id = id });
|
||||
}
|
||||
|
||||
public async Task<Domain.Entities.Org> CreateAsync(Domain.Entities.Org org)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = @"
|
||||
INSERT INTO orgs (id, name, slug, created_at)
|
||||
VALUES (@Id, @Name, @Slug, @CreatedAt)
|
||||
RETURNING *";
|
||||
return await Dapper.SqlMapper.QuerySingleAsync<Domain.Entities.Org>(connection, sql, org);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Domain.Entities.Org>> GetByUserIdAsync(Guid userId)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = @"
|
||||
SELECT o.* FROM orgs o
|
||||
INNER JOIN org_members om ON o.id = om.org_id
|
||||
WHERE om.user_id = @UserId
|
||||
ORDER BY o.name";
|
||||
var result = await Dapper.SqlMapper.QueryAsync<Domain.Entities.Org>(connection, sql, new { UserId = userId });
|
||||
return result.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using IncidentOps.Domain.Entities;
|
||||
|
||||
namespace IncidentOps.Infrastructure.Data.Repositories;
|
||||
|
||||
public interface IRefreshTokenRepository
|
||||
{
|
||||
Task<RefreshToken?> GetByHashAsync(string tokenHash);
|
||||
Task<RefreshToken> CreateAsync(RefreshToken token);
|
||||
Task RevokeAsync(Guid id);
|
||||
Task UpdateActiveOrgAsync(Guid id, Guid activeOrgId, string newTokenHash, DateTime newExpiresAt);
|
||||
}
|
||||
|
||||
public class RefreshTokenRepository : IRefreshTokenRepository
|
||||
{
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
public RefreshTokenRepository(IDbConnectionFactory connectionFactory)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public async Task<RefreshToken?> GetByHashAsync(string tokenHash)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = "SELECT * FROM refresh_tokens WHERE token_hash = @TokenHash AND revoked_at IS NULL AND expires_at > NOW()";
|
||||
return await Dapper.SqlMapper.QuerySingleOrDefaultAsync<RefreshToken>(connection, sql, new { TokenHash = tokenHash });
|
||||
}
|
||||
|
||||
public async Task<RefreshToken> CreateAsync(RefreshToken token)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = @"
|
||||
INSERT INTO refresh_tokens (id, user_id, token_hash, active_org_id, expires_at, created_at)
|
||||
VALUES (@Id, @UserId, @TokenHash, @ActiveOrgId, @ExpiresAt, @CreatedAt)
|
||||
RETURNING *";
|
||||
return await Dapper.SqlMapper.QuerySingleAsync<RefreshToken>(connection, sql, token);
|
||||
}
|
||||
|
||||
public async Task RevokeAsync(Guid id)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = "UPDATE refresh_tokens SET revoked_at = NOW() WHERE id = @Id";
|
||||
await Dapper.SqlMapper.ExecuteAsync(connection, sql, new { Id = id });
|
||||
}
|
||||
|
||||
public async Task UpdateActiveOrgAsync(Guid id, Guid activeOrgId, string newTokenHash, DateTime newExpiresAt)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = @"
|
||||
UPDATE refresh_tokens
|
||||
SET active_org_id = @ActiveOrgId, token_hash = @NewTokenHash, expires_at = @NewExpiresAt
|
||||
WHERE id = @Id";
|
||||
await Dapper.SqlMapper.ExecuteAsync(connection, sql, new { Id = id, ActiveOrgId = activeOrgId, NewTokenHash = newTokenHash, NewExpiresAt = newExpiresAt });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using IncidentOps.Domain.Entities;
|
||||
|
||||
namespace IncidentOps.Infrastructure.Data.Repositories;
|
||||
|
||||
public interface IServiceRepository
|
||||
{
|
||||
Task<Service?> GetByIdAsync(Guid id, Guid orgId);
|
||||
Task<IReadOnlyList<Service>> GetByOrgIdAsync(Guid orgId);
|
||||
Task<Service> CreateAsync(Service service);
|
||||
}
|
||||
|
||||
public class ServiceRepository : IServiceRepository
|
||||
{
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
public ServiceRepository(IDbConnectionFactory connectionFactory)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public async Task<Service?> GetByIdAsync(Guid id, Guid orgId)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = "SELECT * FROM services WHERE id = @Id AND org_id = @OrgId";
|
||||
return await Dapper.SqlMapper.QuerySingleOrDefaultAsync<Service>(connection, sql, new { Id = id, OrgId = orgId });
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Service>> GetByOrgIdAsync(Guid orgId)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = "SELECT * FROM services WHERE org_id = @OrgId ORDER BY name";
|
||||
var result = await Dapper.SqlMapper.QueryAsync<Service>(connection, sql, new { OrgId = orgId });
|
||||
return result.ToList();
|
||||
}
|
||||
|
||||
public async Task<Service> CreateAsync(Service service)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = @"
|
||||
INSERT INTO services (id, org_id, name, slug, description, created_at)
|
||||
VALUES (@Id, @OrgId, @Name, @Slug, @Description, @CreatedAt)
|
||||
RETURNING *";
|
||||
return await Dapper.SqlMapper.QuerySingleAsync<Service>(connection, sql, service);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using IncidentOps.Domain.Entities;
|
||||
|
||||
namespace IncidentOps.Infrastructure.Data.Repositories;
|
||||
|
||||
public interface IUserRepository
|
||||
{
|
||||
Task<User?> GetByIdAsync(Guid id);
|
||||
Task<User?> GetByEmailAsync(string email);
|
||||
Task<User> CreateAsync(User user);
|
||||
}
|
||||
|
||||
public class UserRepository : IUserRepository
|
||||
{
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
public UserRepository(IDbConnectionFactory connectionFactory)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public async Task<User?> GetByIdAsync(Guid id)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = "SELECT * FROM users WHERE id = @Id";
|
||||
return await Dapper.SqlMapper.QuerySingleOrDefaultAsync<User>(connection, sql, new { Id = id });
|
||||
}
|
||||
|
||||
public async Task<User?> GetByEmailAsync(string email)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = "SELECT * FROM users WHERE email = @Email";
|
||||
return await Dapper.SqlMapper.QuerySingleOrDefaultAsync<User>(connection, sql, new { Email = email.ToLowerInvariant() });
|
||||
}
|
||||
|
||||
public async Task<User> CreateAsync(User user)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
const string sql = @"
|
||||
INSERT INTO users (id, email, password_hash, display_name, created_at)
|
||||
VALUES (@Id, @Email, @PasswordHash, @DisplayName, @CreatedAt)
|
||||
RETURNING *";
|
||||
return await Dapper.SqlMapper.QuerySingleAsync<User>(connection, sql, user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\IncidentOps.Domain\IncidentOps.Domain.csproj" />
|
||||
<ProjectReference Include="..\IncidentOps.Contracts\IncidentOps.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="FluentMigrator" Version="7.2.0" />
|
||||
<PackageReference Include="FluentMigrator.Runner" Version="7.2.0" />
|
||||
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="7.2.0" />
|
||||
<PackageReference Include="Hangfire.Core" Version="1.8.22" />
|
||||
<PackageReference Include="Hangfire.Redis.StackExchange" Version="1.12.0" />
|
||||
<PackageReference Include="Npgsql" Version="10.0.1" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.10.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user