chore: moving towards agentic development
Some checks failed
Backend CI/CD / build_and_deploy (push) Has been cancelled
Frontend CI/CD / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-04-24 21:12:26 -04:00
parent df3e602015
commit b6eb692c27
179 changed files with 2880 additions and 866 deletions

View File

@@ -0,0 +1,14 @@
namespace Socialize.Modules.Clients.Data;
public class Client
{
public Guid Id { get; init; }
public Guid WorkspaceId { get; set; }
public required string Name { get; set; }
public required string Status { get; set; }
public string? PortraitUrl { get; set; }
public string? PrimaryContactName { get; set; }
public string? PrimaryContactEmail { get; set; }
public string? PrimaryContactPortraitUrl { get; set; }
public DateTimeOffset CreatedAt { get; init; }
}

View File

@@ -0,0 +1,12 @@
using Socialize.Modules.Clients.Data;
namespace Socialize.Modules.Clients;
public static class DependencyInjection
{
public static WebApplicationBuilder AddClientsModule(
this WebApplicationBuilder builder)
{
return builder;
}
}

View File

@@ -0,0 +1,65 @@
using Socialize.Infrastructure.BlobStorage.Contracts;
using Socialize.Infrastructure.Security;
using Socialize.Modules.Clients.Data;
namespace Socialize.Modules.Clients.Handlers;
public record ChangeClientPortraitRequest(
IFormFile File);
public record ChangeClientPortraitResponse(
string BlobUrl);
public sealed class ChangeClientPortraitRequestValidator : Validator<ChangeClientPortraitRequest>
{
public ChangeClientPortraitRequestValidator()
{
RuleFor(x => x.File)
.NotNull()
.NotEmpty();
}
}
public class ChangeClientPortraitHandler(
AppDbContext clientsDbContext,
IBlobStorage blobStorage,
AccessScopeService accessScopeService)
: Endpoint<ChangeClientPortraitRequest, ChangeClientPortraitResponse>
{
public override void Configure()
{
Post("/api/clients/{id}/portrait");
Options(o => o.WithTags("Clients"));
AllowFileUploads();
}
public override async Task HandleAsync(ChangeClientPortraitRequest request, CancellationToken ct)
{
Guid id = Route<Guid>("id");
Client? client = await clientsDbContext.Clients.SingleOrDefaultAsync(candidate => candidate.Id == id, ct);
if (client is null)
{
await SendNotFoundAsync(ct);
return;
}
if (!accessScopeService.CanManageWorkspace(User, client.WorkspaceId))
{
await SendForbiddenAsync(ct);
return;
}
string blobUrl = await blobStorage.UploadFileAsync(
ContainerNames.Clients,
$"{client.Id}/{SubDirectoryNames.Profile}/{CommonFileNames.LogoPicture}",
request.File.OpenReadStream(),
request.File.ContentType,
ct);
client.PortraitUrl = blobUrl;
await clientsDbContext.SaveChangesAsync(ct);
await SendOkAsync(new ChangeClientPortraitResponse(blobUrl), ct);
}
}

View File

@@ -0,0 +1,101 @@
using Socialize.Infrastructure.Security;
namespace Socialize.Modules.Clients.Handlers;
public record CreateClientRequest(
Guid WorkspaceId,
string Name,
string? PortraitUrl,
string? PrimaryContactName,
string? PrimaryContactEmail,
string? PrimaryContactPortraitUrl);
public class CreateClientRequestValidator
: Validator<CreateClientRequest>
{
public CreateClientRequestValidator()
{
RuleFor(x => x.WorkspaceId).NotEmpty();
RuleFor(x => x.Name).NotEmpty().MaximumLength(256);
RuleFor(x => x.PortraitUrl).MaximumLength(2048);
RuleFor(x => x.PrimaryContactName).MaximumLength(256);
RuleFor(x => x.PrimaryContactEmail).MaximumLength(256).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.PrimaryContactEmail));
RuleFor(x => x.PrimaryContactPortraitUrl).MaximumLength(2048);
}
}
public class CreateClientHandler(
AppDbContext dbContext,
AccessScopeService accessScopeService)
: Endpoint<CreateClientRequest, ClientDto>
{
public override void Configure()
{
Post("/api/clients");
Options(o => o.WithTags("Clients"));
}
public override async Task HandleAsync(CreateClientRequest request, CancellationToken ct)
{
if (!accessScopeService.CanManageWorkspace(User, request.WorkspaceId))
{
await SendForbiddenAsync(ct);
return;
}
bool workspaceExists = await dbContext.Workspaces
.AnyAsync(workspace => workspace.Id == request.WorkspaceId, ct);
if (!workspaceExists)
{
AddError(request => request.WorkspaceId, "The selected workspace does not exist.");
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
return;
}
string normalizedName = request.Name.Trim();
string? normalizedPortraitUrl = request.PortraitUrl?.Trim();
string? normalizedPrimaryContactName = request.PrimaryContactName?.Trim();
string? normalizedPrimaryContactEmail = request.PrimaryContactEmail?.Trim();
string? normalizedPrimaryContactPortraitUrl = request.PrimaryContactPortraitUrl?.Trim();
bool duplicateClient = await dbContext.Clients
.AnyAsync(
client => client.WorkspaceId == request.WorkspaceId && client.Name == normalizedName,
ct);
if (duplicateClient)
{
AddError(request => request.Name, "A client with this name already exists in the active workspace.");
await SendErrorsAsync(StatusCodes.Status409Conflict, ct);
return;
}
Client client = new()
{
Id = Guid.NewGuid(),
WorkspaceId = request.WorkspaceId,
Name = normalizedName,
Status = "Active",
PortraitUrl = string.IsNullOrWhiteSpace(normalizedPortraitUrl) ? null : normalizedPortraitUrl,
PrimaryContactName = string.IsNullOrWhiteSpace(normalizedPrimaryContactName) ? null : normalizedPrimaryContactName,
PrimaryContactEmail = string.IsNullOrWhiteSpace(normalizedPrimaryContactEmail) ? null : normalizedPrimaryContactEmail,
PrimaryContactPortraitUrl = string.IsNullOrWhiteSpace(normalizedPrimaryContactPortraitUrl) ? null : normalizedPrimaryContactPortraitUrl,
CreatedAt = DateTimeOffset.UtcNow,
};
dbContext.Clients.Add(client);
await dbContext.SaveChangesAsync(ct);
ClientDto dto = new(
client.Id,
client.WorkspaceId,
client.Name,
client.Status,
client.PortraitUrl,
client.PrimaryContactName,
client.PrimaryContactEmail,
client.PrimaryContactPortraitUrl);
await SendAsync(dto, StatusCodes.Status201Created, ct);
}
}

View File

@@ -0,0 +1,73 @@
using Socialize.Infrastructure.Security;
using Socialize.Modules.Clients.Data;
namespace Socialize.Modules.Clients.Handlers;
public record GetClientsRequest(Guid? WorkspaceId);
public record ClientDto(
Guid Id,
Guid WorkspaceId,
string Name,
string Status,
string? PortraitUrl,
string? PrimaryContactName,
string? PrimaryContactEmail,
string? PrimaryContactPortraitUrl);
public class GetClientsHandler(
AppDbContext dbContext,
AccessScopeService accessScopeService)
: Endpoint<GetClientsRequest, IReadOnlyCollection<ClientDto>>
{
public override void Configure()
{
Get("/api/clients");
Options(o => o.WithTags("Clients"));
}
public override async Task HandleAsync(GetClientsRequest request, CancellationToken ct)
{
IQueryable<Client> query = dbContext.Clients.AsQueryable();
if (accessScopeService.IsManager(User))
{
if (request.WorkspaceId.HasValue)
{
query = query.Where(client => client.WorkspaceId == request.WorkspaceId.Value);
}
}
else
{
IReadOnlyCollection<Guid> workspaceScopeIds = User.GetWorkspaceScopeIds();
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
query = query.Where(client => workspaceScopeIds.Contains(client.WorkspaceId));
if (clientScopeIds.Count > 0)
{
query = query.Where(client => clientScopeIds.Contains(client.Id));
}
if (request.WorkspaceId.HasValue)
{
query = query.Where(client => client.WorkspaceId == request.WorkspaceId.Value);
}
}
List<ClientDto> clients = await query
.OrderBy(client => client.Name)
.Select(client => new ClientDto(
client.Id,
client.WorkspaceId,
client.Name,
client.Status,
client.PortraitUrl,
client.PrimaryContactName,
client.PrimaryContactEmail,
client.PrimaryContactPortraitUrl))
.ToListAsync(ct);
await SendOkAsync(clients, ct);
}
}

View File

@@ -0,0 +1,98 @@
using Socialize.Infrastructure.Security;
using Socialize.Modules.Clients.Data;
namespace Socialize.Modules.Clients.Handlers;
public record UpdateClientRequest(
string Name,
string? PortraitUrl,
string Status,
string? PrimaryContactName,
string? PrimaryContactEmail,
string? PrimaryContactPortraitUrl);
public class UpdateClientRequestValidator
: Validator<UpdateClientRequest>
{
public UpdateClientRequestValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(256);
RuleFor(x => x.PortraitUrl).MaximumLength(2048);
RuleFor(x => x.Status).NotEmpty().MaximumLength(64);
RuleFor(x => x.PrimaryContactName).MaximumLength(256);
RuleFor(x => x.PrimaryContactEmail).MaximumLength(256).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.PrimaryContactEmail));
RuleFor(x => x.PrimaryContactPortraitUrl).MaximumLength(2048);
}
}
public class UpdateClientHandler(
AppDbContext clientsDbContext,
AccessScopeService accessScopeService)
: Endpoint<UpdateClientRequest, ClientDto>
{
public override void Configure()
{
Put("/api/clients/{id}");
Options(o => o.WithTags("Clients"));
}
public override async Task HandleAsync(UpdateClientRequest request, CancellationToken ct)
{
Guid id = Route<Guid>("id");
Client? client = await clientsDbContext.Clients.SingleOrDefaultAsync(candidate => candidate.Id == id, ct);
if (client is null)
{
await SendNotFoundAsync(ct);
return;
}
if (!accessScopeService.CanManageWorkspace(User, client.WorkspaceId))
{
await SendForbiddenAsync(ct);
return;
}
string normalizedName = request.Name.Trim();
string normalizedStatus = request.Status.Trim();
string? normalizedPortraitUrl = request.PortraitUrl?.Trim();
string? normalizedPrimaryContactName = request.PrimaryContactName?.Trim();
string? normalizedPrimaryContactEmail = request.PrimaryContactEmail?.Trim();
string? normalizedPrimaryContactPortraitUrl = request.PrimaryContactPortraitUrl?.Trim();
bool duplicateClient = await clientsDbContext.Clients
.AnyAsync(
candidate => candidate.Id != id
&& candidate.WorkspaceId == client.WorkspaceId
&& candidate.Name == normalizedName,
ct);
if (duplicateClient)
{
AddError(request => request.Name, "A client with this name already exists in the active workspace.");
await SendErrorsAsync(StatusCodes.Status409Conflict, ct);
return;
}
client.Name = normalizedName;
client.Status = normalizedStatus;
client.PortraitUrl = string.IsNullOrWhiteSpace(normalizedPortraitUrl) ? null : normalizedPortraitUrl;
client.PrimaryContactName = string.IsNullOrWhiteSpace(normalizedPrimaryContactName) ? null : normalizedPrimaryContactName;
client.PrimaryContactEmail = string.IsNullOrWhiteSpace(normalizedPrimaryContactEmail) ? null : normalizedPrimaryContactEmail;
client.PrimaryContactPortraitUrl = string.IsNullOrWhiteSpace(normalizedPrimaryContactPortraitUrl) ? null : normalizedPrimaryContactPortraitUrl;
await clientsDbContext.SaveChangesAsync(ct);
ClientDto dto = new(
client.Id,
client.WorkspaceId,
client.Name,
client.Status,
client.PortraitUrl,
client.PrimaryContactName,
client.PrimaryContactEmail,
client.PrimaryContactPortraitUrl);
await SendOkAsync(dto, ct);
}
}