Add Ping backend

.NET 10 solution for Ping, a Discord style chat app: Domain, Application
and Infrastructure projects plus the Api host. SignalR for chat and
presence, LiveKit for call tokens, EF Core on Postgres, S3 compatible
storage for attachments, and OIDC auth shared with the Musify Zitadel
instance. Includes the Compose stack for local development.
This commit is contained in:
2026-09-21 19:32:55 +02:00
commit 0623875dfa
126 changed files with 5048 additions and 0 deletions
@@ -0,0 +1,49 @@
using ErrorOr;
using Mediator;
using Microsoft.EntityFrameworkCore;
using Ping.Application.Channels.Responses;
using Ping.Application.Contracts;
using Ping.Domain.Entities;
using Ping.Domain.ValueObjects;
namespace Ping.Application.Channels
{
public record CreateChannelCommand(long CurrentUserId, Guid ServerId, string Name, ChannelType Type)
: ICommand<ErrorOr<ChannelResponse>>;
public class CreateChannelCommandHandler(IDatabase database)
: ICommandHandler<CreateChannelCommand, ErrorOr<ChannelResponse>>
{
public async ValueTask<ErrorOr<ChannelResponse>> Handle(CreateChannelCommand request, CancellationToken cancellationToken)
{
var membership = await database.ServerMembers.FirstOrDefaultAsync(
m => m.ServerId == request.ServerId && m.UserId == request.CurrentUserId, cancellationToken);
if (membership is null)
return Error.NotFound(description: $"Server {request.ServerId} not found");
if (membership.Role != ServerMemberRole.Owner)
return Error.Forbidden(description: "Only the server owner can create channels");
if (request.Type == ChannelType.DirectMessage)
return Error.Validation(description: "Server channels cannot be direct messages");
var position = await database.Channels
.Where(c => c.ServerId == request.ServerId)
.CountAsync(cancellationToken);
var channel = new Channel
{
Id = Guid.NewGuid(),
ServerId = request.ServerId,
Name = request.Name,
Type = request.Type,
Position = position,
};
await database.Channels.AddAsync(channel, cancellationToken);
await database.SaveChangesAsync(cancellationToken);
return ChannelResponse.FromEntity(channel);
}
}
}