Initial import

This commit is contained in:
Stein Helge Riise
2025-11-17 08:32:46 +01:00
commit ede31fbb7e
129 changed files with 9514 additions and 0 deletions
@@ -0,0 +1,62 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using MinAttest.Contracts.Attests;
using MinAttest.Application.Abstractions;
using MinAttest.Domain.Entities;
namespace MinAttest.Application.Features.Attests.Commands;
public record CompleteUploadCommand(Guid UploadId, CompleteUploadRequest Request) : IRequest<Guid?>;
public class CompleteUploadCommandHandler(IAppDbContext db) : IRequestHandler<CompleteUploadCommand, Guid?>
{
public async Task<Guid?> Handle(CompleteUploadCommand request, CancellationToken cancellationToken)
{
var r = request.Request;
var personExists = await db.Persons.AnyAsync(p => p.Id == r.PersonId, cancellationToken);
if (!personExists) return null;
if (r.EmployerId is Guid eid)
{
var exists = await db.Employers.AnyAsync(e => e.Id == eid, cancellationToken);
if (!exists) return null;
}
byte[]? content = null;
long? length = null;
if (!string.IsNullOrWhiteSpace(r.ContentBase64))
{
try
{
content = Convert.FromBase64String(r.ContentBase64);
length = content.LongLength;
}
catch (FormatException)
{
// invalid base64 -> treat as bad request
return null;
}
}
var a = new Attest
{
Id = Guid.NewGuid(),
PersonId = r.PersonId,
EmployerId = r.EmployerId,
Title = r.Title,
From = r.From,
To = r.To,
Summary = r.Summary,
BlobPath = r.BlobPath,
BlobHash = r.BlobHash,
Content = content,
ContentType = r.ContentType,
ContentLength = length,
IssuedAt = DateTimeOffset.UtcNow
};
db.Attests.Add(a);
await db.SaveChangesAsync(cancellationToken);
return a.Id;
}
}
@@ -0,0 +1,23 @@
using FluentValidation;
namespace MinAttest.Application.Features.Attests.Commands;
public class CompleteUploadCommandValidator : AbstractValidator<CompleteUploadCommand>
{
public CompleteUploadCommandValidator()
{
RuleFor(x => x.Request.PersonId).NotEqual(Guid.Empty);
RuleFor(x => x.Request.Title).NotEmpty().MaximumLength(200);
RuleFor(x => x.Request.BlobPath).NotEmpty().MaximumLength(500);
// Allow either inline content or external reference
RuleFor(x => x.Request)
.Must(r => !string.IsNullOrWhiteSpace(r.ContentBase64) || !string.IsNullOrWhiteSpace(r.BlobPath))
.WithMessage("Either ContentBase64 or BlobPath must be provided");
When(x => !string.IsNullOrWhiteSpace(x.Request.ContentBase64), () =>
{
RuleFor(x => x.Request.ContentType).NotEmpty().MaximumLength(255);
});
RuleFor(x => x.Request.From).LessThanOrEqualTo(x => x.Request.To);
RuleFor(x => x.Request.Summary).MaximumLength(2000).When(x => !string.IsNullOrWhiteSpace(x.Request.Summary));
}
}
@@ -0,0 +1,52 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using MinAttest.Application.Abstractions;
using MinAttest.Contracts.Attests;
using MinAttest.Domain.Entities;
namespace MinAttest.Application.Features.Attests.Commands;
public record EmployerIssueAttestCommand(Guid EmployerId, EmployerAttestUploadRequest Request) : IRequest<Guid?>;
public class EmployerIssueAttestCommandHandler(IAppDbContext db) : IRequestHandler<EmployerIssueAttestCommand, Guid?>
{
public async Task<Guid?> Handle(EmployerIssueAttestCommand request, CancellationToken cancellationToken)
{
var r = request.Request;
var employerExists = await db.Employers.AnyAsync(e => e.Id == request.EmployerId, cancellationToken);
if (!employerExists) return null;
var personExists = await db.Persons.AnyAsync(p => p.Id == r.PersonId, cancellationToken);
if (!personExists) return null;
byte[]? content = null;
long? length = null;
if (!string.IsNullOrWhiteSpace(r.ContentBase64))
{
try { content = Convert.FromBase64String(r.ContentBase64); length = content.LongLength; }
catch (FormatException) { return null; }
}
var a = new Attest
{
Id = Guid.NewGuid(),
PersonId = r.PersonId,
EmployerId = request.EmployerId,
Title = r.Title,
From = r.From,
To = r.To,
Summary = r.Summary,
BlobPath = r.BlobPath,
BlobHash = r.BlobHash,
Content = content,
ContentType = r.ContentType,
ContentLength = length,
IssuedAt = DateTimeOffset.UtcNow
};
db.Attests.Add(a);
await db.SaveChangesAsync(cancellationToken);
return a.Id;
}
}
@@ -0,0 +1,23 @@
using FluentValidation;
namespace MinAttest.Application.Features.Attests.Commands;
public class EmployerIssueAttestCommandValidator : AbstractValidator<EmployerIssueAttestCommand>
{
public EmployerIssueAttestCommandValidator()
{
RuleFor(x => x.EmployerId).NotEqual(Guid.Empty);
RuleFor(x => x.Request.PersonId).NotEqual(Guid.Empty);
RuleFor(x => x.Request.Title).NotEmpty().MaximumLength(200);
RuleFor(x => x.Request.BlobPath).MaximumLength(500);
RuleFor(x => x.Request.From).LessThanOrEqualTo(x => x.Request.To);
RuleFor(x => x.Request)
.Must(r => !string.IsNullOrWhiteSpace(r.BlobPath) || !string.IsNullOrWhiteSpace(r.ContentBase64))
.WithMessage("Either BlobPath or ContentBase64 must be provided");
When(x => !string.IsNullOrWhiteSpace(x.Request.ContentBase64), () =>
{
RuleFor(x => x.Request.ContentType).NotEmpty().MaximumLength(255);
});
}
}
@@ -0,0 +1,49 @@
using MediatR;
using MinAttest.Application.Abstractions;
using MinAttest.Contracts.Attests;
using MinAttest.Domain.Entities;
namespace MinAttest.Application.Features.Attests.Commands;
public record PersonUploadAttestCommand(Guid PersonId, PersonAttestUploadRequest Request) : IRequest<Guid?>;
public class PersonUploadAttestCommandHandler(IAppDbContext db) : IRequestHandler<PersonUploadAttestCommand, Guid?>
{
public async Task<Guid?> Handle(PersonUploadAttestCommand request, CancellationToken cancellationToken)
{
var r = request.Request;
var person = await db.Persons.FindAsync([request.PersonId], cancellationToken);
if (person is null) return null;
byte[]? content = null;
long? length = null;
if (!string.IsNullOrWhiteSpace(r.ContentBase64))
{
try { content = Convert.FromBase64String(r.ContentBase64); length = content.LongLength; }
catch (FormatException) { return null; }
}
var a = new Attest
{
Id = Guid.NewGuid(),
PersonId = request.PersonId,
EmployerId = null,
Title = r.Title,
From = r.From,
To = r.To,
Summary = r.Summary,
BlobPath = r.BlobPath,
BlobHash = r.BlobHash,
Content = content,
ContentType = r.ContentType,
ContentLength = length,
IssuedAt = DateTimeOffset.UtcNow
};
db.Attests.Add(a);
await db.SaveChangesAsync(cancellationToken);
return a.Id;
}
}
@@ -0,0 +1,22 @@
using FluentValidation;
namespace MinAttest.Application.Features.Attests.Commands;
public class PersonUploadAttestCommandValidator : AbstractValidator<PersonUploadAttestCommand>
{
public PersonUploadAttestCommandValidator()
{
RuleFor(x => x.PersonId).NotEqual(Guid.Empty);
RuleFor(x => x.Request.Title).NotEmpty().MaximumLength(200);
RuleFor(x => x.Request.BlobPath).MaximumLength(500);
RuleFor(x => x.Request.From).LessThanOrEqualTo(x => x.Request.To);
RuleFor(x => x.Request)
.Must(r => !string.IsNullOrWhiteSpace(r.BlobPath) || !string.IsNullOrWhiteSpace(r.ContentBase64))
.WithMessage("Either BlobPath or ContentBase64 must be provided");
When(x => !string.IsNullOrWhiteSpace(x.Request.ContentBase64), () =>
{
RuleFor(x => x.Request.ContentType).NotEmpty().MaximumLength(255);
});
}
}