feat(backend): Implement complete Project Management Module with multi-tenant support
Day 12 implementation - Complete CRUD operations with tenant isolation and SignalR integration.
**Domain Layer**:
- Added TenantId value object for strong typing
- Updated Project entity to include TenantId field
- Modified Project.Create factory method to require tenantId parameter
- Updated ProjectCreatedEvent to include TenantId
**Application Layer**:
- Created UpdateProjectCommand, Handler, and Validator for project updates
- Created ArchiveProjectCommand, Handler, and Validator for archiving projects
- Updated CreateProjectCommand to include TenantId
- Modified CreateProjectCommandValidator to remove OwnerId validation (set from JWT)
- Created IProjectNotificationService interface for SignalR abstraction
- Implemented ProjectCreatedEventHandler with SignalR notifications
- Implemented ProjectUpdatedEventHandler with SignalR notifications
- Implemented ProjectArchivedEventHandler with SignalR notifications
**Infrastructure Layer**:
- Updated PMDbContext to inject IHttpContextAccessor
- Configured Global Query Filter for automatic tenant isolation
- Added TenantId property mapping in ProjectConfiguration
- Created TenantId index for query performance
**API Layer**:
- Updated ProjectsController with [Authorize] attribute
- Implemented PUT /api/v1/projects/{id} for updates
- Implemented DELETE /api/v1/projects/{id} for archiving
- Added helper methods to extract TenantId and UserId from JWT claims
- Extended IRealtimeNotificationService with Project-specific methods
- Implemented RealtimeNotificationService with tenant-aware SignalR groups
- Created ProjectNotificationServiceAdapter to bridge layers
- Registered IProjectNotificationService in Program.cs
**Features Implemented**:
- Complete CRUD operations (Create, Read, Update, Archive)
- Multi-tenant isolation via EF Core Global Query Filter
- JWT-based authorization on all endpoints
- SignalR real-time notifications for all Project events
- Clean Architecture with proper layer separation
- Domain Event pattern with MediatR
**Database Migration**:
- Migration created (not applied yet): AddTenantIdToProject
**Test Scripts**:
- Created comprehensive test scripts (test-project-simple.ps1)
- Tests cover full CRUD lifecycle and tenant isolation
**Note**: API hot reload required to apply CreateProjectCommandValidator fix.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,13 @@ public class ProjectConfiguration : IEntityTypeConfiguration<Project>
|
||||
.IsRequired()
|
||||
.ValueGeneratedNever();
|
||||
|
||||
// TenantId conversion
|
||||
builder.Property(p => p.TenantId)
|
||||
.HasConversion(
|
||||
id => id.Value,
|
||||
value => TenantId.From(value))
|
||||
.IsRequired();
|
||||
|
||||
// Basic properties
|
||||
builder.Property(p => p.Name)
|
||||
.HasMaxLength(200)
|
||||
@@ -77,6 +84,7 @@ public class ProjectConfiguration : IEntityTypeConfiguration<Project>
|
||||
// Indexes for performance
|
||||
builder.HasIndex(p => p.CreatedAt);
|
||||
builder.HasIndex(p => p.OwnerId);
|
||||
builder.HasIndex(p => p.TenantId);
|
||||
|
||||
// Ignore DomainEvents (handled separately)
|
||||
builder.Ignore(p => p.DomainEvents);
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Project Management Module DbContext
|
||||
/// </summary>
|
||||
public class PMDbContext(DbContextOptions<PMDbContext> options) : DbContext(options)
|
||||
public class PMDbContext : DbContext
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public PMDbContext(DbContextOptions<PMDbContext> options, IHttpContextAccessor httpContextAccessor)
|
||||
: base(options)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public DbSet<Project> Projects => Set<Project>();
|
||||
public DbSet<Epic> Epics => Set<Epic>();
|
||||
public DbSet<Story> Stories => Set<Story>();
|
||||
@@ -23,5 +33,24 @@ public class PMDbContext(DbContextOptions<PMDbContext> options) : DbContext(opti
|
||||
|
||||
// Apply all entity configurations from this assembly
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
|
||||
// Multi-tenant Global Query Filter for Project
|
||||
modelBuilder.Entity<Project>().HasQueryFilter(p =>
|
||||
p.TenantId == GetCurrentTenantId());
|
||||
}
|
||||
|
||||
private TenantId GetCurrentTenantId()
|
||||
{
|
||||
var tenantIdClaim = _httpContextAccessor?.HttpContext?.User
|
||||
.FindFirst("tenant_id")?.Value;
|
||||
|
||||
if (Guid.TryParse(tenantIdClaim, out var tenantId) && tenantId != Guid.Empty)
|
||||
{
|
||||
return TenantId.From(tenantId);
|
||||
}
|
||||
|
||||
// Return a dummy value for queries outside HTTP context (e.g., migrations)
|
||||
// These will return no results due to the filter
|
||||
return TenantId.From(Guid.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user