Files
ColaFlow/colaflow-api/src/ColaFlow.API/Program.cs
Yaojia Wang 9ada0cac4a 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>
2025-11-04 10:13:04 +01:00

192 lines
6.1 KiB
C#

using ColaFlow.API.Extensions;
using ColaFlow.API.Handlers;
using ColaFlow.API.Hubs;
using ColaFlow.API.Middleware;
using ColaFlow.API.Services;
using ColaFlow.Modules.Identity.Application;
using ColaFlow.Modules.Identity.Infrastructure;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Scalar.AspNetCore;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Register ProjectManagement Module
builder.Services.AddProjectManagementModule(builder.Configuration, builder.Environment);
// Register Identity Module
builder.Services.AddIdentityApplication();
builder.Services.AddIdentityInfrastructure(builder.Configuration, builder.Environment);
// Add Response Caching
builder.Services.AddResponseCaching();
builder.Services.AddMemoryCache();
// Add Response Compression (Gzip and Brotli)
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<Microsoft.AspNetCore.ResponseCompression.BrotliCompressionProvider>();
options.Providers.Add<Microsoft.AspNetCore.ResponseCompression.GzipCompressionProvider>();
});
builder.Services.Configure<Microsoft.AspNetCore.ResponseCompression.BrotliCompressionProviderOptions>(options =>
{
options.Level = System.IO.Compression.CompressionLevel.Fastest;
});
builder.Services.Configure<Microsoft.AspNetCore.ResponseCompression.GzipCompressionProviderOptions>(options =>
{
options.Level = System.IO.Compression.CompressionLevel.Fastest;
});
// Add controllers
builder.Services.AddControllers();
// Configure exception handling (IExceptionHandler - .NET 8+)
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
// Configure Authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"] ?? throw new InvalidOperationException("JWT SecretKey not configured")))
};
// Configure SignalR to use JWT from query string (for WebSocket upgrade)
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for SignalR hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
{
// Read the token from query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
// Configure Authorization Policies for RBAC
builder.Services.AddAuthorization(options =>
{
// Tenant Owner only
options.AddPolicy("RequireTenantOwner", policy =>
policy.RequireRole("TenantOwner"));
// Tenant Owner or Tenant Admin
options.AddPolicy("RequireTenantAdmin", policy =>
policy.RequireRole("TenantOwner", "TenantAdmin"));
// Tenant Owner, Tenant Admin, or Tenant Member (excludes Guest and AIAgent)
options.AddPolicy("RequireTenantMember", policy =>
policy.RequireRole("TenantOwner", "TenantAdmin", "TenantMember"));
// Human users only (excludes AIAgent)
options.AddPolicy("RequireHumanUser", policy =>
policy.RequireAssertion(context =>
!context.User.IsInRole("AIAgent")));
// AI Agent only (for MCP integration testing)
options.AddPolicy("RequireAIAgent", policy =>
policy.RequireRole("AIAgent"));
});
// Configure CORS for frontend (SignalR requires AllowCredentials)
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend", policy =>
{
policy.WithOrigins("http://localhost:3000", "https://localhost:3000")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials(); // Required for SignalR
});
});
// Configure SignalR
builder.Services.AddSignalR(options =>
{
// Enable detailed errors (development only)
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
// Client timeout settings
options.ClientTimeoutInterval = TimeSpan.FromSeconds(60);
options.HandshakeTimeout = TimeSpan.FromSeconds(15);
// Keep alive interval
options.KeepAliveInterval = TimeSpan.FromSeconds(15);
});
// Register Realtime Notification Service
builder.Services.AddScoped<IRealtimeNotificationService, RealtimeNotificationService>();
// Register Project Notification Service Adapter (for ProjectManagement module)
builder.Services.AddScoped<ColaFlow.Modules.ProjectManagement.Application.Services.IProjectNotificationService,
ProjectNotificationServiceAdapter>();
// Configure OpenAPI/Scalar
builder.Services.AddOpenApi();
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
// Performance logging (should be early to measure total request time)
app.UsePerformanceLogging();
// Global exception handler (should be first in pipeline)
app.UseExceptionHandler();
// Enable Response Compression (should be early in pipeline)
app.UseResponseCompression();
// Enable CORS
app.UseCors("AllowFrontend");
app.UseHttpsRedirection();
// Enable Response Caching (after HTTPS redirection)
app.UseResponseCaching();
// Authentication & Authorization
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
// Map SignalR Hubs (after UseAuthorization)
app.MapHub<ProjectHub>("/hubs/project");
app.MapHub<NotificationHub>("/hubs/notification");
app.Run();
// Make the implicit Program class public for integration tests
public partial class Program { }