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>
192 lines
6.1 KiB
C#
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 { }
|