diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Common/Interfaces/ITenantContext.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Common/Interfaces/ITenantContext.cs
new file mode 100644
index 0000000..98fdc62
--- /dev/null
+++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Common/Interfaces/ITenantContext.cs
@@ -0,0 +1,14 @@
+namespace ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
+
+///
+/// Provides access to the current tenant context
+///
+public interface ITenantContext
+{
+ ///
+ /// Gets the current tenant ID
+ ///
+ /// The current tenant ID
+ /// Thrown when tenant context is not available
+ Guid GetCurrentTenantId();
+}
diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Aggregates/ProjectAggregate/Epic.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Aggregates/ProjectAggregate/Epic.cs
index 14347eb..b0f4f90 100644
--- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Aggregates/ProjectAggregate/Epic.cs
+++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Aggregates/ProjectAggregate/Epic.cs
@@ -11,6 +11,7 @@ namespace ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
public class Epic : Entity
{
public new EpicId Id { get; private set; }
+ public TenantId TenantId { get; private set; }
public string Name { get; private set; }
public string Description { get; private set; }
public ProjectId ProjectId { get; private set; }
@@ -28,6 +29,7 @@ public class Epic : Entity
private Epic()
{
Id = null!;
+ TenantId = null!;
Name = null!;
Description = null!;
ProjectId = null!;
@@ -36,7 +38,7 @@ public class Epic : Entity
CreatedBy = null!;
}
- public static Epic Create(string name, string description, ProjectId projectId, UserId createdBy)
+ public static Epic Create(TenantId tenantId, string name, string description, ProjectId projectId, UserId createdBy)
{
if (string.IsNullOrWhiteSpace(name))
throw new DomainException("Epic name cannot be empty");
@@ -47,6 +49,7 @@ public class Epic : Entity
return new Epic
{
Id = EpicId.Create(),
+ TenantId = tenantId,
Name = name,
Description = description ?? string.Empty,
ProjectId = projectId,
@@ -59,7 +62,7 @@ public class Epic : Entity
public Story CreateStory(string title, string description, TaskPriority priority, UserId createdBy)
{
- var story = Story.Create(title, description, this.Id, priority, createdBy);
+ var story = Story.Create(this.TenantId, title, description, this.Id, priority, createdBy);
_stories.Add(story);
return story;
}
diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Aggregates/ProjectAggregate/Project.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Aggregates/ProjectAggregate/Project.cs
index bd54c50..98cfebe 100644
--- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Aggregates/ProjectAggregate/Project.cs
+++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Aggregates/ProjectAggregate/Project.cs
@@ -87,7 +87,7 @@ public class Project : AggregateRoot
if (Status == ProjectStatus.Archived)
throw new DomainException("Cannot create epic in an archived project");
- var epic = Epic.Create(name, description, this.Id, createdBy);
+ var epic = Epic.Create(this.TenantId, name, description, this.Id, createdBy);
_epics.Add(epic);
AddDomainEvent(new EpicCreatedEvent(epic.Id, epic.Name, this.Id));
diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Aggregates/ProjectAggregate/Story.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Aggregates/ProjectAggregate/Story.cs
index 11c73eb..c8f9859 100644
--- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Aggregates/ProjectAggregate/Story.cs
+++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Aggregates/ProjectAggregate/Story.cs
@@ -11,6 +11,7 @@ namespace ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
public class Story : Entity
{
public new StoryId Id { get; private set; }
+ public TenantId TenantId { get; private set; }
public string Title { get; private set; }
public string Description { get; private set; }
public EpicId EpicId { get; private set; }
@@ -31,6 +32,7 @@ public class Story : Entity
private Story()
{
Id = null!;
+ TenantId = null!;
Title = null!;
Description = null!;
EpicId = null!;
@@ -39,7 +41,7 @@ public class Story : Entity
CreatedBy = null!;
}
- public static Story Create(string title, string description, EpicId epicId, TaskPriority priority, UserId createdBy)
+ public static Story Create(TenantId tenantId, string title, string description, EpicId epicId, TaskPriority priority, UserId createdBy)
{
if (string.IsNullOrWhiteSpace(title))
throw new DomainException("Story title cannot be empty");
@@ -50,6 +52,7 @@ public class Story : Entity
return new Story
{
Id = StoryId.Create(),
+ TenantId = tenantId,
Title = title,
Description = description ?? string.Empty,
EpicId = epicId,
@@ -62,7 +65,7 @@ public class Story : Entity
public WorkTask CreateTask(string title, string description, TaskPriority priority, UserId createdBy)
{
- var task = WorkTask.Create(title, description, this.Id, priority, createdBy);
+ var task = WorkTask.Create(this.TenantId, title, description, this.Id, priority, createdBy);
_tasks.Add(task);
return task;
}
diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Aggregates/ProjectAggregate/WorkTask.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Aggregates/ProjectAggregate/WorkTask.cs
index 0a9a9e5..7b0a492 100644
--- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Aggregates/ProjectAggregate/WorkTask.cs
+++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Aggregates/ProjectAggregate/WorkTask.cs
@@ -12,6 +12,7 @@ namespace ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
public class WorkTask : Entity
{
public new TaskId Id { get; private set; }
+ public TenantId TenantId { get; private set; }
public string Title { get; private set; }
public string Description { get; private set; }
public StoryId StoryId { get; private set; }
@@ -29,6 +30,7 @@ public class WorkTask : Entity
private WorkTask()
{
Id = null!;
+ TenantId = null!;
Title = null!;
Description = null!;
StoryId = null!;
@@ -37,7 +39,7 @@ public class WorkTask : Entity
CreatedBy = null!;
}
- public static WorkTask Create(string title, string description, StoryId storyId, TaskPriority priority, UserId createdBy)
+ public static WorkTask Create(TenantId tenantId, string title, string description, StoryId storyId, TaskPriority priority, UserId createdBy)
{
if (string.IsNullOrWhiteSpace(title))
throw new DomainException("Task title cannot be empty");
@@ -48,6 +50,7 @@ public class WorkTask : Entity
return new WorkTask
{
Id = TaskId.Create(),
+ TenantId = tenantId,
Title = title,
Description = description ?? string.Empty,
StoryId = storyId,
diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/20251104153716_AddTenantIdToEpicStoryTask.Designer.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/20251104153716_AddTenantIdToEpicStoryTask.Designer.cs
new file mode 100644
index 0000000..a3924e9
--- /dev/null
+++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/20251104153716_AddTenantIdToEpicStoryTask.Designer.cs
@@ -0,0 +1,325 @@
+//
+using System;
+using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations
+{
+ [DbContext(typeof(PMDbContext))]
+ [Migration("20251104153716_AddTenantIdToEpicStoryTask")]
+ partial class AddTenantIdToEpicStoryTask
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("project_management")
+ .HasAnnotation("ProductVersion", "9.0.10")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("Priority")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("ProjectId")
+ .HasColumnType("uuid");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("ProjectId");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_epics_tenant_id");
+
+ b.ToTable("Epics", "project_management");
+ });
+
+ modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("OwnerId")
+ .HasColumnType("uuid");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("OwnerId");
+
+ b.HasIndex("TenantId");
+
+ b.ToTable("Projects", "project_management");
+ });
+
+ modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("ActualHours")
+ .HasColumnType("numeric");
+
+ b.Property("AssigneeId")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(4000)
+ .HasColumnType("character varying(4000)");
+
+ b.Property("EpicId")
+ .HasColumnType("uuid");
+
+ b.Property("EstimatedHours")
+ .HasColumnType("numeric");
+
+ b.Property("Priority")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AssigneeId");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("EpicId");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_stories_tenant_id");
+
+ b.ToTable("Stories", "project_management");
+ });
+
+ modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.WorkTask", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("ActualHours")
+ .HasColumnType("numeric");
+
+ b.Property("AssigneeId")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(4000)
+ .HasColumnType("character varying(4000)");
+
+ b.Property("EstimatedHours")
+ .HasColumnType("numeric");
+
+ b.Property("Priority")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("StoryId")
+ .HasColumnType("uuid");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AssigneeId");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("StoryId");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_tasks_tenant_id");
+
+ b.ToTable("Tasks", "project_management");
+ });
+
+ modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", b =>
+ {
+ b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", null)
+ .WithMany("Epics")
+ .HasForeignKey("ProjectId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", b =>
+ {
+ b.OwnsOne("ColaFlow.Modules.ProjectManagement.Domain.ValueObjects.ProjectKey", "Key", b1 =>
+ {
+ b1.Property("ProjectId")
+ .HasColumnType("uuid");
+
+ b1.Property("Value")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasColumnName("Key");
+
+ b1.HasKey("ProjectId");
+
+ b1.HasIndex("Value")
+ .IsUnique();
+
+ b1.ToTable("Projects", "project_management");
+
+ b1.WithOwner()
+ .HasForeignKey("ProjectId");
+ });
+
+ b.Navigation("Key")
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", b =>
+ {
+ b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", null)
+ .WithMany("Stories")
+ .HasForeignKey("EpicId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.WorkTask", b =>
+ {
+ b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", null)
+ .WithMany("Tasks")
+ .HasForeignKey("StoryId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", b =>
+ {
+ b.Navigation("Stories");
+ });
+
+ modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", b =>
+ {
+ b.Navigation("Epics");
+ });
+
+ modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", b =>
+ {
+ b.Navigation("Tasks");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/20251104153716_AddTenantIdToEpicStoryTask.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/20251104153716_AddTenantIdToEpicStoryTask.cs
new file mode 100644
index 0000000..4bd7312
--- /dev/null
+++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/20251104153716_AddTenantIdToEpicStoryTask.cs
@@ -0,0 +1,91 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations
+{
+ ///
+ public partial class AddTenantIdToEpicStoryTask : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "tenant_id",
+ schema: "project_management",
+ table: "Tasks",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ migrationBuilder.AddColumn(
+ name: "tenant_id",
+ schema: "project_management",
+ table: "Stories",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ migrationBuilder.AddColumn(
+ name: "tenant_id",
+ schema: "project_management",
+ table: "Epics",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ migrationBuilder.CreateIndex(
+ name: "ix_tasks_tenant_id",
+ schema: "project_management",
+ table: "Tasks",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_stories_tenant_id",
+ schema: "project_management",
+ table: "Stories",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_epics_tenant_id",
+ schema: "project_management",
+ table: "Epics",
+ column: "tenant_id");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "ix_tasks_tenant_id",
+ schema: "project_management",
+ table: "Tasks");
+
+ migrationBuilder.DropIndex(
+ name: "ix_stories_tenant_id",
+ schema: "project_management",
+ table: "Stories");
+
+ migrationBuilder.DropIndex(
+ name: "ix_epics_tenant_id",
+ schema: "project_management",
+ table: "Epics");
+
+ migrationBuilder.DropColumn(
+ name: "tenant_id",
+ schema: "project_management",
+ table: "Tasks");
+
+ migrationBuilder.DropColumn(
+ name: "tenant_id",
+ schema: "project_management",
+ table: "Stories");
+
+ migrationBuilder.DropColumn(
+ name: "tenant_id",
+ schema: "project_management",
+ table: "Epics");
+ }
+ }
+}
diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/PMDbContextModelSnapshot.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/PMDbContextModelSnapshot.cs
index 137a045..28bbdfa 100644
--- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/PMDbContextModelSnapshot.cs
+++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/PMDbContextModelSnapshot.cs
@@ -57,6 +57,10 @@ namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations
.HasMaxLength(50)
.HasColumnType("character varying(50)");
+ b.Property("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
b.Property("UpdatedAt")
.HasColumnType("timestamp with time zone");
@@ -66,6 +70,9 @@ namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations
b.HasIndex("ProjectId");
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_epics_tenant_id");
+
b.ToTable("Epics", "project_management");
});
@@ -150,6 +157,10 @@ namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations
.HasMaxLength(50)
.HasColumnType("character varying(50)");
+ b.Property("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
b.Property("Title")
.IsRequired()
.HasMaxLength(200)
@@ -166,6 +177,9 @@ namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations
b.HasIndex("EpicId");
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_stories_tenant_id");
+
b.ToTable("Stories", "project_management");
});
@@ -207,6 +221,10 @@ namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations
b.Property("StoryId")
.HasColumnType("uuid");
+ b.Property("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
b.Property("Title")
.IsRequired()
.HasMaxLength(200)
@@ -223,6 +241,9 @@ namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations
b.HasIndex("StoryId");
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_tasks_tenant_id");
+
b.ToTable("Tasks", "project_management");
});
diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Configurations/EpicConfiguration.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Configurations/EpicConfiguration.cs
index 4e08604..80ce8cb 100644
--- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Configurations/EpicConfiguration.cs
+++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Configurations/EpicConfiguration.cs
@@ -26,6 +26,14 @@ public class EpicConfiguration : IEntityTypeConfiguration
.IsRequired()
.ValueGeneratedNever();
+ // TenantId (required for multi-tenancy)
+ builder.Property(e => e.TenantId)
+ .HasConversion(
+ id => id.Value,
+ value => TenantId.From(value))
+ .IsRequired()
+ .HasColumnName("tenant_id");
+
// ProjectId (foreign key)
builder.Property(e => e.ProjectId)
.HasConversion(
@@ -78,6 +86,8 @@ public class EpicConfiguration : IEntityTypeConfiguration
.OnDelete(DeleteBehavior.Cascade);
// Indexes
+ builder.HasIndex(e => e.TenantId)
+ .HasDatabaseName("ix_epics_tenant_id");
builder.HasIndex(e => e.ProjectId);
builder.HasIndex(e => e.CreatedAt);
}
diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Configurations/StoryConfiguration.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Configurations/StoryConfiguration.cs
index 3cf3b82..45f9939 100644
--- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Configurations/StoryConfiguration.cs
+++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Configurations/StoryConfiguration.cs
@@ -26,6 +26,14 @@ public class StoryConfiguration : IEntityTypeConfiguration
.IsRequired()
.ValueGeneratedNever();
+ // TenantId (required for multi-tenancy)
+ builder.Property(s => s.TenantId)
+ .HasConversion(
+ id => id.Value,
+ value => TenantId.From(value))
+ .IsRequired()
+ .HasColumnName("tenant_id");
+
// EpicId (foreign key)
builder.Property(s => s.EpicId)
.HasConversion(
@@ -88,6 +96,8 @@ public class StoryConfiguration : IEntityTypeConfiguration
.OnDelete(DeleteBehavior.Cascade);
// Indexes
+ builder.HasIndex(s => s.TenantId)
+ .HasDatabaseName("ix_stories_tenant_id");
builder.HasIndex(s => s.EpicId);
builder.HasIndex(s => s.AssigneeId);
builder.HasIndex(s => s.CreatedAt);
diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Configurations/WorkTaskConfiguration.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Configurations/WorkTaskConfiguration.cs
index 7ee92b0..4455a28 100644
--- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Configurations/WorkTaskConfiguration.cs
+++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Configurations/WorkTaskConfiguration.cs
@@ -26,6 +26,14 @@ public class WorkTaskConfiguration : IEntityTypeConfiguration
.IsRequired()
.ValueGeneratedNever();
+ // TenantId (required for multi-tenancy)
+ builder.Property(t => t.TenantId)
+ .HasConversion(
+ id => id.Value,
+ value => TenantId.From(value))
+ .IsRequired()
+ .HasColumnName("tenant_id");
+
// StoryId (foreign key)
builder.Property(t => t.StoryId)
.HasConversion(
@@ -81,6 +89,8 @@ public class WorkTaskConfiguration : IEntityTypeConfiguration
builder.Property(t => t.UpdatedAt);
// Indexes
+ builder.HasIndex(t => t.TenantId)
+ .HasDatabaseName("ix_tasks_tenant_id");
builder.HasIndex(t => t.StoryId);
builder.HasIndex(t => t.AssigneeId);
builder.HasIndex(t => t.CreatedAt);
diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/PMDbContext.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/PMDbContext.cs
index 3566f09..62ce26d 100644
--- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/PMDbContext.cs
+++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/PMDbContext.cs
@@ -34,9 +34,18 @@ public class PMDbContext : DbContext
// Apply all entity configurations from this assembly
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
- // Multi-tenant Global Query Filter for Project
+ // Multi-tenant Global Query Filters
modelBuilder.Entity().HasQueryFilter(p =>
p.TenantId == GetCurrentTenantId());
+
+ modelBuilder.Entity().HasQueryFilter(e =>
+ e.TenantId == GetCurrentTenantId());
+
+ modelBuilder.Entity().HasQueryFilter(s =>
+ s.TenantId == GetCurrentTenantId());
+
+ modelBuilder.Entity().HasQueryFilter(t =>
+ t.TenantId == GetCurrentTenantId());
}
private TenantId GetCurrentTenantId()
diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Services/TenantContext.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Services/TenantContext.cs
new file mode 100644
index 0000000..74d45a0
--- /dev/null
+++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Services/TenantContext.cs
@@ -0,0 +1,33 @@
+using Microsoft.AspNetCore.Http;
+using System.Security.Claims;
+using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
+
+namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Services;
+
+///
+/// Implementation of ITenantContext that retrieves tenant ID from JWT claims
+///
+public sealed class TenantContext : ITenantContext
+{
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
+ public TenantContext(IHttpContextAccessor httpContextAccessor)
+ {
+ _httpContextAccessor = httpContextAccessor;
+ }
+
+ public Guid GetCurrentTenantId()
+ {
+ var httpContext = _httpContextAccessor.HttpContext;
+ if (httpContext == null)
+ throw new InvalidOperationException("HTTP context is not available");
+
+ var user = httpContext.User;
+ var tenantClaim = user.FindFirst("tenant_id") ?? user.FindFirst("tenantId");
+
+ if (tenantClaim == null || !Guid.TryParse(tenantClaim.Value, out var tenantId))
+ throw new UnauthorizedAccessException("Tenant ID not found in claims");
+
+ return tenantId;
+ }
+}
diff --git a/colaflow-api/src/Modules/ProjectManagement/ProjectManagementModule.cs b/colaflow-api/src/Modules/ProjectManagement/ProjectManagementModule.cs
index 038fbbb..e0108fc 100644
--- a/colaflow-api/src/Modules/ProjectManagement/ProjectManagementModule.cs
+++ b/colaflow-api/src/Modules/ProjectManagement/ProjectManagementModule.cs
@@ -7,9 +7,11 @@ using FluentValidation;
using MediatR;
using ColaFlow.Modules.ProjectManagement.Application.Behaviors;
using ColaFlow.Modules.ProjectManagement.Application.Commands.CreateProject;
+using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
using ColaFlow.Modules.ProjectManagement.Infrastructure.Repositories;
+using ColaFlow.Modules.ProjectManagement.Infrastructure.Services;
namespace ColaFlow.Modules.ProjectManagement;
@@ -32,6 +34,9 @@ public class ProjectManagementModule : IModule
services.AddScoped();
services.AddScoped();
+ // Register tenant context service
+ services.AddScoped();
+
// Note: IProjectNotificationService is registered in the API layer (Program.cs)
// as it depends on IRealtimeNotificationService which is API-specific