Commit MWS backend source tree

The ASP.NET Core solution (mws.api/mws.application/mws.domain/
mws.infrastructure) existed only as untracked working files. Adding
it to version control, plus a .gitignore for build output and local
tooling directories, so the CQRS/mediator refactor plan has a real
git history to branch and diff against.
This commit is contained in:
2026-08-13 23:10:22 +07:00
parent 96944ad2c3
commit f72aaa2329
94 changed files with 7758 additions and 0 deletions
@@ -0,0 +1,10 @@
using Mws.Application.Auth;
namespace Mws.Infrastructure.Authentication;
public class BcryptPasswordHasher : IPasswordHasher
{
public string Hash(string password) => BCrypt.Net.BCrypt.HashPassword(password, BCrypt.Net.BCrypt.GenerateSalt());
public bool Verify(string password, string hash) => BCrypt.Net.BCrypt.Verify(password, hash);
}
@@ -0,0 +1,9 @@
namespace Mws.Infrastructure.Authentication;
public class JwtOptions
{
public string Secret { get; set; } = string.Empty;
public string Issuer { get; set; } = "Mws";
public string Audience { get; set; } = "Mws.Clients";
public TimeSpan Expiration { get; set; } = TimeSpan.FromHours(12);
}
@@ -0,0 +1,32 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using Mws.Application.Auth;
namespace Mws.Infrastructure.Authentication;
public class JwtTokenService(JwtOptions options) : ITokenService
{
public string CreateToken(Guid userId, string username)
{
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, userId.ToString()),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new(ClaimTypes.Name, username),
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(options.Secret));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: options.Issuer,
audience: options.Audience,
claims: claims,
expires: DateTime.UtcNow.Add(options.Expiration),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
+53
View File
@@ -0,0 +1,53 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Mws.Application.Accounts;
using Mws.Application.Auth;
using Mws.Application.Common;
using Mws.Application.Documents;
using Mws.Application.Permissions;
using Mws.Application.Projects;
using Mws.Application.Roles;
using Mws.Application.Tasks;
using Mws.Infrastructure.Authentication;
using Mws.Infrastructure.Persistence;
namespace Mws.Infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("Default")
?? "Host=localhost;Port=5432;Database=mws;Username=mws;Password=mws";
services.AddDbContext<AppDbContext>(options => options.UseNpgsql(connectionString));
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddAutoMapper(cfg => { }, typeof(MappingProfile).Assembly);
services.AddScoped<IPasswordHasher, BcryptPasswordHasher>();
var jwtSection = configuration.GetSection("Jwt");
var jwtOptions = jwtSection.Get<JwtOptions>() ?? new JwtOptions();
if (string.IsNullOrWhiteSpace(jwtOptions.Secret) || jwtOptions.Secret.Length < 32)
{
jwtOptions.Secret = Environment.GetEnvironmentVariable("MWS_JWT_SECRET")
?? "mws-development-secret-key-change-me-in-production-123456";
}
services.Configure<JwtOptions>(jwtSection.Bind);
services.AddSingleton(jwtOptions);
services.AddScoped<ITokenService, JwtTokenService>();
services.AddScoped<IAuthService, AuthService>();
services.AddScoped<IAccountService, AccountService>();
services.AddScoped<IPermissionService, PermissionService>();
services.AddScoped<IRoleService, RoleService>();
services.AddScoped<IProjectService, ProjectService>();
services.AddScoped<IProjectMemberService, ProjectMemberService>();
services.AddScoped<IDocumentService, DocumentService>();
services.AddScoped<ITaskService, TaskService>();
return services;
}
}
@@ -0,0 +1,264 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Mws.Infrastructure.Persistence;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Mws.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260809054503_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Content")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<Guid?>("ParentId")
.HasColumnType("uuid");
b.Property<Guid>("ProjectId")
.HasColumnType("uuid");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("ParentId");
b.HasIndex("ProjectId", "ParentId");
b.ToTable("documents", (string)null);
});
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("projects", (string)null);
});
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
{
b.Property<Guid>("ProjectId")
.HasColumnType("uuid");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("Role")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("ProjectId", "UserId");
b.HasIndex("UserId");
b.ToTable("project_members", (string)null);
});
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("AssigneeId")
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<DateTime?>("DueDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Priority")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<Guid>("ProjectId")
.HasColumnType("uuid");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("AssigneeId");
b.HasIndex("ProjectId");
b.HasIndex("Status");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("Mws.Domain.Users.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<string>("PasswordHash")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("users", (string)null);
});
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
{
b.HasOne("Mws.Domain.Documents.Document", "Parent")
.WithMany("Children")
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Parent");
});
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
{
b.HasOne("Mws.Domain.Projects.Project", "Project")
.WithMany("Members")
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Mws.Domain.Users.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
b.Navigation("User");
});
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
{
b.HasOne("Mws.Domain.Users.User", "Assignee")
.WithMany()
.HasForeignKey("AssigneeId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Assignee");
});
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
{
b.Navigation("Children");
});
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
{
b.Navigation("Members");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,180 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Mws.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "documents",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
ProjectId = table.Column<Guid>(type: "uuid", nullable: false),
ParentId = table.Column<Guid>(type: "uuid", nullable: true),
Title = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: false),
Content = table.Column<string>(type: "text", nullable: true),
Type = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedBy = table.Column<Guid>(type: "uuid", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_documents", x => x.Id);
table.ForeignKey(
name: "FK_documents_documents_ParentId",
column: x => x.ParentId,
principalTable: "documents",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "projects",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
Description = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_projects", x => x.Id);
});
migrationBuilder.CreateTable(
name: "users",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Username = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
PasswordHash = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
DisplayName = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_users", x => x.Id);
});
migrationBuilder.CreateTable(
name: "project_members",
columns: table => new
{
ProjectId = table.Column<Guid>(type: "uuid", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
Role = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_project_members", x => new { x.ProjectId, x.UserId });
table.ForeignKey(
name: "FK_project_members_projects_ProjectId",
column: x => x.ProjectId,
principalTable: "projects",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_project_members_users_UserId",
column: x => x.UserId,
principalTable: "users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "tasks",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
ProjectId = table.Column<Guid>(type: "uuid", nullable: false),
Title = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: false),
Description = table.Column<string>(type: "text", nullable: true),
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
Priority = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
AssigneeId = table.Column<Guid>(type: "uuid", nullable: true),
DueDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_tasks", x => x.Id);
table.ForeignKey(
name: "FK_tasks_users_AssigneeId",
column: x => x.AssigneeId,
principalTable: "users",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateIndex(
name: "IX_documents_ParentId",
table: "documents",
column: "ParentId");
migrationBuilder.CreateIndex(
name: "IX_documents_ProjectId_ParentId",
table: "documents",
columns: new[] { "ProjectId", "ParentId" });
migrationBuilder.CreateIndex(
name: "IX_project_members_UserId",
table: "project_members",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_tasks_AssigneeId",
table: "tasks",
column: "AssigneeId");
migrationBuilder.CreateIndex(
name: "IX_tasks_ProjectId",
table: "tasks",
column: "ProjectId");
migrationBuilder.CreateIndex(
name: "IX_tasks_Status",
table: "tasks",
column: "Status");
migrationBuilder.CreateIndex(
name: "IX_users_Username",
table: "users",
column: "Username",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "documents");
migrationBuilder.DropTable(
name: "project_members");
migrationBuilder.DropTable(
name: "tasks");
migrationBuilder.DropTable(
name: "projects");
migrationBuilder.DropTable(
name: "users");
}
}
}
@@ -0,0 +1,353 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Mws.Infrastructure.Persistence;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Mws.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260811133605_AddRolesAndPermissions")]
partial class AddRolesAndPermissions
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Content")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<Guid?>("ParentId")
.HasColumnType("uuid");
b.Property<Guid>("ProjectId")
.HasColumnType("uuid");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("ParentId");
b.HasIndex("ProjectId", "ParentId");
b.ToTable("documents", (string)null);
});
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("projects", (string)null);
});
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
{
b.Property<Guid>("ProjectId")
.HasColumnType("uuid");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("Role")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("ProjectId", "UserId");
b.HasIndex("UserId");
b.ToTable("project_members", (string)null);
});
modelBuilder.Entity("Mws.Domain.Roles.Role", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsSystem")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("roles", (string)null);
});
modelBuilder.Entity("Mws.Domain.Roles.RolePermission", b =>
{
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.Property<string>("Screen")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<bool>("CanCreate")
.HasColumnType("boolean");
b.Property<bool>("CanDelete")
.HasColumnType("boolean");
b.Property<bool>("CanEdit")
.HasColumnType("boolean");
b.Property<bool>("CanView")
.HasColumnType("boolean");
b.HasKey("RoleId", "Screen");
b.ToTable("role_permissions", (string)null);
});
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("AssigneeId")
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<DateTime?>("DueDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Priority")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<Guid>("ProjectId")
.HasColumnType("uuid");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("AssigneeId");
b.HasIndex("ProjectId");
b.HasIndex("Status");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("Mws.Domain.Users.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<bool>("IsActive")
.HasColumnType("boolean");
b.Property<string>("PasswordHash")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.HasIndex("Username")
.IsUnique();
b.ToTable("users", (string)null);
});
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
{
b.HasOne("Mws.Domain.Documents.Document", "Parent")
.WithMany("Children")
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Parent");
});
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
{
b.HasOne("Mws.Domain.Projects.Project", "Project")
.WithMany("Members")
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Mws.Domain.Users.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
b.Navigation("User");
});
modelBuilder.Entity("Mws.Domain.Roles.RolePermission", b =>
{
b.HasOne("Mws.Domain.Roles.Role", "Role")
.WithMany("Permissions")
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
});
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
{
b.HasOne("Mws.Domain.Users.User", "Assignee")
.WithMany()
.HasForeignKey("AssigneeId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Assignee");
});
modelBuilder.Entity("Mws.Domain.Users.User", b =>
{
b.HasOne("Mws.Domain.Roles.Role", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Role");
});
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
{
b.Navigation("Children");
});
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
{
b.Navigation("Members");
});
modelBuilder.Entity("Mws.Domain.Roles.Role", b =>
{
b.Navigation("Permissions");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,111 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Mws.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddRolesAndPermissions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsActive",
table: "users",
type: "boolean",
nullable: false,
defaultValue: true);
migrationBuilder.AddColumn<Guid>(
name: "RoleId",
table: "users",
type: "uuid",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.CreateTable(
name: "roles",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
IsSystem = table.Column<bool>(type: "boolean", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_roles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "role_permissions",
columns: table => new
{
RoleId = table.Column<Guid>(type: "uuid", nullable: false),
Screen = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
CanView = table.Column<bool>(type: "boolean", nullable: false),
CanCreate = table.Column<bool>(type: "boolean", nullable: false),
CanEdit = table.Column<bool>(type: "boolean", nullable: false),
CanDelete = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_role_permissions", x => new { x.RoleId, x.Screen });
table.ForeignKey(
name: "FK_role_permissions_roles_RoleId",
column: x => x.RoleId,
principalTable: "roles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_users_RoleId",
table: "users",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "IX_roles_Name",
table: "roles",
column: "Name",
unique: true);
migrationBuilder.AddForeignKey(
name: "FK_users_roles_RoleId",
table: "users",
column: "RoleId",
principalTable: "roles",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_users_roles_RoleId",
table: "users");
migrationBuilder.DropTable(
name: "role_permissions");
migrationBuilder.DropTable(
name: "roles");
migrationBuilder.DropIndex(
name: "IX_users_RoleId",
table: "users");
migrationBuilder.DropColumn(
name: "IsActive",
table: "users");
migrationBuilder.DropColumn(
name: "RoleId",
table: "users");
}
}
}
@@ -0,0 +1,365 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Mws.Infrastructure.Persistence;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Mws.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260812124332_AddMemberDocumentPermissions")]
partial class AddMemberDocumentPermissions
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Content")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<Guid?>("ParentId")
.HasColumnType("uuid");
b.Property<Guid>("ProjectId")
.HasColumnType("uuid");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("ParentId");
b.HasIndex("ProjectId", "ParentId");
b.ToTable("documents", (string)null);
});
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("projects", (string)null);
});
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
{
b.Property<Guid>("ProjectId")
.HasColumnType("uuid");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<bool>("CanCreateDocuments")
.HasColumnType("boolean");
b.Property<bool>("CanDeleteDocuments")
.HasColumnType("boolean");
b.Property<bool>("CanEditDocuments")
.HasColumnType("boolean");
b.Property<bool>("CanViewDocuments")
.HasColumnType("boolean");
b.Property<string>("Role")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("ProjectId", "UserId");
b.HasIndex("UserId");
b.ToTable("project_members", (string)null);
});
modelBuilder.Entity("Mws.Domain.Roles.Role", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsSystem")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("roles", (string)null);
});
modelBuilder.Entity("Mws.Domain.Roles.RolePermission", b =>
{
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.Property<string>("Screen")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<bool>("CanCreate")
.HasColumnType("boolean");
b.Property<bool>("CanDelete")
.HasColumnType("boolean");
b.Property<bool>("CanEdit")
.HasColumnType("boolean");
b.Property<bool>("CanView")
.HasColumnType("boolean");
b.HasKey("RoleId", "Screen");
b.ToTable("role_permissions", (string)null);
});
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("AssigneeId")
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<DateTime?>("DueDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Priority")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<Guid>("ProjectId")
.HasColumnType("uuid");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("AssigneeId");
b.HasIndex("ProjectId");
b.HasIndex("Status");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("Mws.Domain.Users.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<bool>("IsActive")
.HasColumnType("boolean");
b.Property<string>("PasswordHash")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.HasIndex("Username")
.IsUnique();
b.ToTable("users", (string)null);
});
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
{
b.HasOne("Mws.Domain.Documents.Document", "Parent")
.WithMany("Children")
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Parent");
});
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
{
b.HasOne("Mws.Domain.Projects.Project", "Project")
.WithMany("Members")
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Mws.Domain.Users.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
b.Navigation("User");
});
modelBuilder.Entity("Mws.Domain.Roles.RolePermission", b =>
{
b.HasOne("Mws.Domain.Roles.Role", "Role")
.WithMany("Permissions")
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
});
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
{
b.HasOne("Mws.Domain.Users.User", "Assignee")
.WithMany()
.HasForeignKey("AssigneeId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Assignee");
});
modelBuilder.Entity("Mws.Domain.Users.User", b =>
{
b.HasOne("Mws.Domain.Roles.Role", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Role");
});
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
{
b.Navigation("Children");
});
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
{
b.Navigation("Members");
});
modelBuilder.Entity("Mws.Domain.Roles.Role", b =>
{
b.Navigation("Permissions");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,65 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Mws.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddMemberDocumentPermissions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "CanCreateDocuments",
table: "project_members",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "CanDeleteDocuments",
table: "project_members",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "CanEditDocuments",
table: "project_members",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "CanViewDocuments",
table: "project_members",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.Sql("UPDATE \"project_members\" SET \"CanViewDocuments\" = true, \"CanCreateDocuments\" = true, \"CanEditDocuments\" = true, \"CanDeleteDocuments\" = true WHERE \"Role\" = 'Owner';");
migrationBuilder.Sql("UPDATE \"project_members\" SET \"CanViewDocuments\" = true WHERE \"Role\" = 'Member';");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CanCreateDocuments",
table: "project_members");
migrationBuilder.DropColumn(
name: "CanDeleteDocuments",
table: "project_members");
migrationBuilder.DropColumn(
name: "CanEditDocuments",
table: "project_members");
migrationBuilder.DropColumn(
name: "CanViewDocuments",
table: "project_members");
}
}
}
@@ -0,0 +1,391 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Mws.Infrastructure.Persistence;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Mws.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260812130925_SplitProjectMemberPermissions")]
partial class SplitProjectMemberPermissions
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Content")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<Guid?>("ParentId")
.HasColumnType("uuid");
b.Property<Guid>("ProjectId")
.HasColumnType("uuid");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("ParentId");
b.HasIndex("ProjectId", "ParentId");
b.ToTable("documents", (string)null);
});
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("projects", (string)null);
});
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
{
b.Property<Guid>("ProjectId")
.HasColumnType("uuid");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("Role")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("ProjectId", "UserId");
b.HasIndex("UserId");
b.ToTable("project_members", (string)null);
});
modelBuilder.Entity("Mws.Domain.Projects.ProjectMemberPermission", b =>
{
b.Property<Guid>("ProjectId")
.HasColumnType("uuid");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("Screen")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<bool>("CanCreate")
.HasColumnType("boolean");
b.Property<bool>("CanDelete")
.HasColumnType("boolean");
b.Property<bool>("CanEdit")
.HasColumnType("boolean");
b.Property<bool>("CanView")
.HasColumnType("boolean");
b.HasKey("ProjectId", "UserId", "Screen");
b.ToTable("project_member_permissions", (string)null);
});
modelBuilder.Entity("Mws.Domain.Roles.Role", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsSystem")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("roles", (string)null);
});
modelBuilder.Entity("Mws.Domain.Roles.RolePermission", b =>
{
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.Property<string>("Screen")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<bool>("CanCreate")
.HasColumnType("boolean");
b.Property<bool>("CanDelete")
.HasColumnType("boolean");
b.Property<bool>("CanEdit")
.HasColumnType("boolean");
b.Property<bool>("CanView")
.HasColumnType("boolean");
b.HasKey("RoleId", "Screen");
b.ToTable("role_permissions", (string)null);
});
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("AssigneeId")
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<DateTime?>("DueDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Priority")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<Guid>("ProjectId")
.HasColumnType("uuid");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("AssigneeId");
b.HasIndex("ProjectId");
b.HasIndex("Status");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("Mws.Domain.Users.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<bool>("IsActive")
.HasColumnType("boolean");
b.Property<string>("PasswordHash")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.HasIndex("Username")
.IsUnique();
b.ToTable("users", (string)null);
});
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
{
b.HasOne("Mws.Domain.Documents.Document", "Parent")
.WithMany("Children")
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Parent");
});
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
{
b.HasOne("Mws.Domain.Projects.Project", "Project")
.WithMany("Members")
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Mws.Domain.Users.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
b.Navigation("User");
});
modelBuilder.Entity("Mws.Domain.Projects.ProjectMemberPermission", b =>
{
b.HasOne("Mws.Domain.Projects.ProjectMember", null)
.WithMany()
.HasForeignKey("ProjectId", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Mws.Domain.Roles.RolePermission", b =>
{
b.HasOne("Mws.Domain.Roles.Role", "Role")
.WithMany("Permissions")
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
});
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
{
b.HasOne("Mws.Domain.Users.User", "Assignee")
.WithMany()
.HasForeignKey("AssigneeId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Assignee");
});
modelBuilder.Entity("Mws.Domain.Users.User", b =>
{
b.HasOne("Mws.Domain.Roles.Role", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Role");
});
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
{
b.Navigation("Children");
});
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
{
b.Navigation("Members");
});
modelBuilder.Entity("Mws.Domain.Roles.Role", b =>
{
b.Navigation("Permissions");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,105 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Mws.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class SplitProjectMemberPermissions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "project_member_permissions",
columns: table => new
{
ProjectId = table.Column<Guid>(type: "uuid", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
Screen = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
CanView = table.Column<bool>(type: "boolean", nullable: false),
CanCreate = table.Column<bool>(type: "boolean", nullable: false),
CanEdit = table.Column<bool>(type: "boolean", nullable: false),
CanDelete = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_project_member_permissions", x => new { x.ProjectId, x.UserId, x.Screen });
table.ForeignKey(
name: "FK_project_member_permissions_project_members_ProjectId_UserId",
columns: x => new { x.ProjectId, x.UserId },
principalTable: "project_members",
principalColumns: new[] { "ProjectId", "UserId" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.Sql("""
INSERT INTO "project_member_permissions" ("ProjectId", "UserId", "Screen", "CanView", "CanCreate", "CanEdit", "CanDelete")
SELECT "ProjectId", "UserId", 'documents', "CanViewDocuments", "CanCreateDocuments", "CanEditDocuments", "CanDeleteDocuments"
FROM "project_members";
""");
migrationBuilder.DropColumn(
name: "CanCreateDocuments",
table: "project_members");
migrationBuilder.DropColumn(
name: "CanDeleteDocuments",
table: "project_members");
migrationBuilder.DropColumn(
name: "CanEditDocuments",
table: "project_members");
migrationBuilder.DropColumn(
name: "CanViewDocuments",
table: "project_members");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "CanCreateDocuments",
table: "project_members",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "CanDeleteDocuments",
table: "project_members",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "CanEditDocuments",
table: "project_members",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "CanViewDocuments",
table: "project_members",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.Sql("""
UPDATE "project_members" m
SET "CanViewDocuments" = p."CanView",
"CanCreateDocuments" = p."CanCreate",
"CanEditDocuments" = p."CanEdit",
"CanDeleteDocuments" = p."CanDelete"
FROM "project_member_permissions" p
WHERE p."ProjectId" = m."ProjectId" AND p."UserId" = m."UserId" AND p."Screen" = 'documents';
""");
migrationBuilder.DropTable(
name: "project_member_permissions");
}
}
}
@@ -0,0 +1,388 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Mws.Infrastructure.Persistence;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Mws.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Content")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<Guid?>("ParentId")
.HasColumnType("uuid");
b.Property<Guid>("ProjectId")
.HasColumnType("uuid");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("ParentId");
b.HasIndex("ProjectId", "ParentId");
b.ToTable("documents", (string)null);
});
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("projects", (string)null);
});
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
{
b.Property<Guid>("ProjectId")
.HasColumnType("uuid");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("Role")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("ProjectId", "UserId");
b.HasIndex("UserId");
b.ToTable("project_members", (string)null);
});
modelBuilder.Entity("Mws.Domain.Projects.ProjectMemberPermission", b =>
{
b.Property<Guid>("ProjectId")
.HasColumnType("uuid");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("Screen")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<bool>("CanCreate")
.HasColumnType("boolean");
b.Property<bool>("CanDelete")
.HasColumnType("boolean");
b.Property<bool>("CanEdit")
.HasColumnType("boolean");
b.Property<bool>("CanView")
.HasColumnType("boolean");
b.HasKey("ProjectId", "UserId", "Screen");
b.ToTable("project_member_permissions", (string)null);
});
modelBuilder.Entity("Mws.Domain.Roles.Role", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsSystem")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("roles", (string)null);
});
modelBuilder.Entity("Mws.Domain.Roles.RolePermission", b =>
{
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.Property<string>("Screen")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<bool>("CanCreate")
.HasColumnType("boolean");
b.Property<bool>("CanDelete")
.HasColumnType("boolean");
b.Property<bool>("CanEdit")
.HasColumnType("boolean");
b.Property<bool>("CanView")
.HasColumnType("boolean");
b.HasKey("RoleId", "Screen");
b.ToTable("role_permissions", (string)null);
});
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("AssigneeId")
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<DateTime?>("DueDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Priority")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<Guid>("ProjectId")
.HasColumnType("uuid");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("AssigneeId");
b.HasIndex("ProjectId");
b.HasIndex("Status");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("Mws.Domain.Users.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<bool>("IsActive")
.HasColumnType("boolean");
b.Property<string>("PasswordHash")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.HasIndex("Username")
.IsUnique();
b.ToTable("users", (string)null);
});
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
{
b.HasOne("Mws.Domain.Documents.Document", "Parent")
.WithMany("Children")
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Parent");
});
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
{
b.HasOne("Mws.Domain.Projects.Project", "Project")
.WithMany("Members")
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Mws.Domain.Users.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
b.Navigation("User");
});
modelBuilder.Entity("Mws.Domain.Projects.ProjectMemberPermission", b =>
{
b.HasOne("Mws.Domain.Projects.ProjectMember", null)
.WithMany()
.HasForeignKey("ProjectId", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Mws.Domain.Roles.RolePermission", b =>
{
b.HasOne("Mws.Domain.Roles.Role", "Role")
.WithMany("Permissions")
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
});
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
{
b.HasOne("Mws.Domain.Users.User", "Assignee")
.WithMany()
.HasForeignKey("AssigneeId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Assignee");
});
modelBuilder.Entity("Mws.Domain.Users.User", b =>
{
b.HasOne("Mws.Domain.Roles.Role", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Role");
});
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
{
b.Navigation("Children");
});
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
{
b.Navigation("Members");
});
modelBuilder.Entity("Mws.Domain.Roles.Role", b =>
{
b.Navigation("Permissions");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore;
using Mws.Domain.Documents;
using Mws.Domain.Projects;
using Mws.Domain.Roles;
using Mws.Domain.Tasks;
using Mws.Domain.Users;
namespace Mws.Infrastructure.Persistence;
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<User> Users => Set<User>();
public DbSet<Project> Projects => Set<Project>();
public DbSet<ProjectMember> ProjectMembers => Set<ProjectMember>();
public DbSet<ProjectMemberPermission> ProjectMemberPermissions => Set<ProjectMemberPermission>();
public DbSet<Document> Documents => Set<Document>();
public DbSet<TaskItem> Tasks => Set<TaskItem>();
public DbSet<Role> Roles => Set<Role>();
public DbSet<RolePermission> RolePermissions => Set<RolePermission>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}
@@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Mws.Domain.Documents;
namespace Mws.Infrastructure.Persistence.Configuration;
public class DocumentConfiguration : IEntityTypeConfiguration<Document>
{
public void Configure(EntityTypeBuilder<Document> e)
{
e.ToTable("documents");
e.HasKey(d => d.Id);
e.Property(d => d.Title).HasMaxLength(300).IsRequired();
e.Property(d => d.Type).HasConversion<string>().HasMaxLength(20);
e.Property(d => d.Content).HasColumnType("text");
e.HasIndex(d => new { d.ProjectId, d.ParentId });
e.HasIndex(d => d.ParentId);
e.HasOne(d => d.Parent)
.WithMany(d => d.Children)
.HasForeignKey(d => d.ParentId)
.OnDelete(DeleteBehavior.Restrict);
}
}
@@ -0,0 +1,17 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Mws.Domain.Projects;
namespace Mws.Infrastructure.Persistence.Configuration;
public class ProjectConfiguration : IEntityTypeConfiguration<Project>
{
public void Configure(EntityTypeBuilder<Project> e)
{
e.ToTable("projects");
e.HasKey(p => p.Id);
e.Property(p => p.Name).HasMaxLength(200).IsRequired();
e.Property(p => p.Description).HasMaxLength(2000);
e.Property(p => p.Status).HasConversion<string>().HasMaxLength(20);
}
}
@@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Mws.Domain.Projects;
namespace Mws.Infrastructure.Persistence.Configuration;
public class ProjectMemberConfiguration : IEntityTypeConfiguration<ProjectMember>
{
public void Configure(EntityTypeBuilder<ProjectMember> e)
{
e.ToTable("project_members");
e.HasKey(m => new { m.ProjectId, m.UserId });
e.Property(m => m.Role).HasConversion<string>().HasMaxLength(20);
e.HasOne(m => m.Project)
.WithMany(p => p.Members)
.HasForeignKey(m => m.ProjectId)
.OnDelete(DeleteBehavior.Cascade);
e.HasOne(m => m.User)
.WithMany()
.HasForeignKey(m => m.UserId)
.OnDelete(DeleteBehavior.Cascade);
}
}
@@ -0,0 +1,20 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Mws.Domain.Projects;
namespace Mws.Infrastructure.Persistence.Configuration;
public class ProjectMemberPermissionConfiguration : IEntityTypeConfiguration<ProjectMemberPermission>
{
public void Configure(EntityTypeBuilder<ProjectMemberPermission> e)
{
e.ToTable("project_member_permissions");
e.HasKey(p => new { p.ProjectId, p.UserId, p.Screen });
e.Property(p => p.Screen).HasMaxLength(50).IsRequired();
e.HasOne<ProjectMember>()
.WithMany()
.HasForeignKey(p => new { p.ProjectId, p.UserId })
.OnDelete(DeleteBehavior.Cascade);
}
}
@@ -0,0 +1,16 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Mws.Domain.Roles;
namespace Mws.Infrastructure.Persistence.Configuration;
public class RoleConfiguration : IEntityTypeConfiguration<Role>
{
public void Configure(EntityTypeBuilder<Role> e)
{
e.ToTable("roles");
e.HasKey(r => r.Id);
e.Property(r => r.Name).HasMaxLength(100).IsRequired();
e.HasIndex(r => r.Name).IsUnique();
}
}
@@ -0,0 +1,20 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Mws.Domain.Roles;
namespace Mws.Infrastructure.Persistence.Configuration;
public class RolePermissionConfiguration : IEntityTypeConfiguration<RolePermission>
{
public void Configure(EntityTypeBuilder<RolePermission> e)
{
e.ToTable("role_permissions");
e.HasKey(p => new { p.RoleId, p.Screen });
e.Property(p => p.Screen).HasMaxLength(50).IsRequired();
e.HasOne(p => p.Role)
.WithMany(r => r.Permissions)
.HasForeignKey(p => p.RoleId)
.OnDelete(DeleteBehavior.Cascade);
}
}
@@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Mws.Domain.Tasks;
namespace Mws.Infrastructure.Persistence.Configuration;
public class TaskConfiguration : IEntityTypeConfiguration<TaskItem>
{
public void Configure(EntityTypeBuilder<TaskItem> e)
{
e.ToTable("tasks");
e.HasKey(t => t.Id);
e.Property(t => t.Title).HasMaxLength(300).IsRequired();
e.Property(t => t.Description).HasColumnType("text");
e.Property(t => t.Status).HasConversion<string>().HasMaxLength(20);
e.Property(t => t.Priority).HasConversion<string>().HasMaxLength(20);
e.HasIndex(t => t.ProjectId);
e.HasIndex(t => t.AssigneeId);
e.HasIndex(t => t.Status);
e.HasOne(t => t.Assignee)
.WithMany()
.HasForeignKey(t => t.AssigneeId)
.OnDelete(DeleteBehavior.SetNull);
}
}
@@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Mws.Domain.Users;
namespace Mws.Infrastructure.Persistence.Configuration;
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> e)
{
e.ToTable("users");
e.HasKey(u => u.Id);
e.Property(u => u.Username).HasMaxLength(100).IsRequired();
e.HasIndex(u => u.Username).IsUnique();
e.Property(u => u.PasswordHash).HasMaxLength(200).IsRequired();
e.Property(u => u.DisplayName).HasMaxLength(150).IsRequired();
e.HasOne(u => u.Role)
.WithMany()
.HasForeignKey(u => u.RoleId)
.OnDelete(DeleteBehavior.Restrict);
}
}
+178
View File
@@ -0,0 +1,178 @@
using Microsoft.EntityFrameworkCore;
using Mws.Application.Auth;
using Mws.Application.Permissions;
using Mws.Domain.Documents;
using Mws.Domain.Projects;
using Mws.Domain.Roles;
using Mws.Domain.Tasks;
using Mws.Domain.Users;
using TaskPriority = Mws.Domain.Tasks.TaskPriority;
using TaskStatus = Mws.Domain.Tasks.TaskStatus;
namespace Mws.Infrastructure.Persistence;
public static class DbSeeder
{
public static async Task SeedAsync(AppDbContext db, IPasswordHasher passwordHasher)
{
if (await db.Users.AnyAsync())
{
return;
}
var now = DateTime.UtcNow;
var adminRole = new Role { Id = Guid.NewGuid(), Name = "Admin", IsSystem = true, CreatedAt = now, UpdatedAt = now };
var memberRole = new Role { Id = Guid.NewGuid(), Name = "Member", IsSystem = true, CreatedAt = now, UpdatedAt = now };
adminRole.Permissions = ScreenCatalog.Screens.Select(s => new RolePermission
{
RoleId = adminRole.Id, Screen = s.Key, CanView = true, CanCreate = true, CanEdit = true, CanDelete = true,
}).ToList();
memberRole.Permissions = ScreenCatalog.Screens
.Where(s => s.Key is not ("accounts" or "roles"))
.Select(s => new RolePermission
{
RoleId = memberRole.Id, Screen = s.Key, CanView = true, CanCreate = true, CanEdit = true, CanDelete = false,
}).ToList();
db.Roles.AddRange(adminRole, memberRole);
await db.SaveChangesAsync();
var admin = new User
{
Id = Guid.NewGuid(),
Username = "admin",
PasswordHash = passwordHasher.Hash("password"),
DisplayName = "Admin",
RoleId = adminRole.Id,
CreatedAt = now,
UpdatedAt = now,
};
var alice = new User
{
Id = Guid.NewGuid(),
Username = "alice",
PasswordHash = passwordHasher.Hash("password"),
DisplayName = "Alice",
RoleId = memberRole.Id,
CreatedAt = now,
UpdatedAt = now,
};
var bob = new User
{
Id = Guid.NewGuid(),
Username = "bob",
PasswordHash = passwordHasher.Hash("password"),
DisplayName = "Bob",
RoleId = memberRole.Id,
CreatedAt = now,
UpdatedAt = now,
};
db.Users.AddRange(admin, alice, bob);
await db.SaveChangesAsync();
var project = new Project
{
Id = Guid.NewGuid(),
Name = "MWS",
Description = "My Workspace - internal tool for the team.",
Status = ProjectStatus.Active,
CreatedAt = now,
UpdatedAt = now,
};
db.Projects.Add(project);
db.ProjectMembers.AddRange(
new ProjectMember { ProjectId = project.Id, UserId = admin.Id, Role = MemberRole.Owner },
new ProjectMember { ProjectId = project.Id, UserId = alice.Id, Role = MemberRole.Member },
new ProjectMember { ProjectId = project.Id, UserId = bob.Id, Role = MemberRole.Member });
db.ProjectMemberPermissions.AddRange(
new ProjectMemberPermission { ProjectId = project.Id, UserId = alice.Id, Screen = ProjectPermissionScreens.Documents, CanView = true },
new ProjectMemberPermission { ProjectId = project.Id, UserId = bob.Id, Screen = ProjectPermissionScreens.Documents, CanView = true });
await db.SaveChangesAsync();
var requirements = new Document
{
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = null,
Title = "Requirements", Type = DocumentType.Folder,
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
};
var design = new Document
{
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = null,
Title = "Technical Design", Type = DocumentType.Folder,
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
};
var notes = new Document
{
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = null,
Title = "Notes", Type = DocumentType.Folder,
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
};
db.Documents.AddRange(requirements, design, notes);
db.Documents.Add(new Document
{
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = requirements.Id,
Title = "Authentication", Type = DocumentType.Document,
Content = "<h2>Authentication</h2><p>Phase 1 uses simple username/password with JWT.</p>",
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
});
db.Documents.Add(new Document
{
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = requirements.Id,
Title = "User Management", Type = DocumentType.Document,
Content = "<h2>User Management</h2><p>Users are created via seeding in Phase 1.</p>",
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
});
db.Documents.Add(new Document
{
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = design.Id,
Title = "API Design", Type = DocumentType.Document,
Content = "<h2>API Design</h2><p>REST API with ASP.NET Core Web API.</p>",
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
});
await db.SaveChangesAsync();
db.Tasks.AddRange(
new TaskItem
{
Id = Guid.NewGuid(), ProjectId = project.Id, Title = "Set up solution",
Status = TaskStatus.Done, Priority = TaskPriority.High, AssigneeId = admin.Id,
CreatedBy = admin.Id, CreatedAt = now, UpdatedAt = now,
},
new TaskItem
{
Id = Guid.NewGuid(), ProjectId = project.Id, Title = "Implement authentication",
Description = "Login endpoint with BCrypt and JWT.",
Status = TaskStatus.InProgress, Priority = TaskPriority.High, AssigneeId = alice.Id,
DueDate = now.AddDays(3),
CreatedBy = admin.Id, CreatedAt = now, UpdatedAt = now,
},
new TaskItem
{
Id = Guid.NewGuid(), ProjectId = project.Id, Title = "Task board drag & drop",
Status = TaskStatus.Todo, Priority = TaskPriority.Medium, AssigneeId = bob.Id,
DueDate = now.AddDays(7),
CreatedBy = admin.Id, CreatedAt = now, UpdatedAt = now,
},
new TaskItem
{
Id = Guid.NewGuid(), ProjectId = project.Id, Title = "Polish documents UI",
Status = TaskStatus.Todo, Priority = TaskPriority.Low,
CreatedBy = admin.Id, CreatedAt = now, UpdatedAt = now,
});
await db.SaveChangesAsync();
}
}
@@ -0,0 +1,41 @@
using Microsoft.EntityFrameworkCore;
using Mws.Application.Common.Repositories;
using Mws.Domain.Documents;
namespace Mws.Infrastructure.Persistence.Repositories;
public class DocumentRepository(AppDbContext db) : RepositoryBase<Document>(db), IDocumentRepository
{
public Task<Document?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
Set.FirstOrDefaultAsync(d => d.Id == id, ct);
public Task<List<Document>> GetTreeForProjectAsync(Guid projectId, CancellationToken ct = default) =>
Set.Where(d => d.ProjectId == projectId).OrderBy(d => d.Title).ToListAsync(ct);
public Task<Document?> GetInProjectAsync(Guid parentId, Guid projectId, CancellationToken ct = default) =>
Set.FirstOrDefaultAsync(d => d.Id == parentId && d.ProjectId == projectId, ct);
public Task<Guid?> GetParentIdAsync(Guid documentId, CancellationToken ct = default) =>
Set.Where(d => d.Id == documentId).Select(d => d.ParentId).SingleAsync(ct);
public Task<List<Document>> GetChildrenAsync(Guid parentId, CancellationToken ct = default) =>
Set.Where(d => d.ParentId == parentId).ToListAsync(ct);
public Task<List<Document>> SearchAsync(List<Guid> projectIds, string? term, int take, CancellationToken ct = default)
{
var query = Set.Where(d => projectIds.Contains(d.ProjectId));
if (!string.IsNullOrWhiteSpace(term))
{
var lower = term.Trim().ToLower();
query = query.Where(d => d.Title.ToLower().Contains(lower));
}
return query.OrderByDescending(d => d.UpdatedAt).Take(take).ToListAsync(ct);
}
public Task<int> CountByTypeForProjectAsync(Guid projectId, DocumentType type, CancellationToken ct = default) =>
Set.CountAsync(d => d.ProjectId == projectId && d.Type == type, ct);
public Task<List<Document>> GetRecentForProjectAsync(Guid projectId, DocumentType type, int take, CancellationToken ct = default) =>
Set.Where(d => d.ProjectId == projectId && d.Type == type).OrderByDescending(d => d.UpdatedAt).Take(take).ToListAsync(ct);
}
@@ -0,0 +1,88 @@
using Microsoft.EntityFrameworkCore;
using Mws.Application.Common.Repositories;
using Mws.Domain.Projects;
namespace Mws.Infrastructure.Persistence.Repositories;
public class ProjectRepository(AppDbContext db) : RepositoryBase<Project>(db), IProjectRepository
{
public Task<Project?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
Set.FirstOrDefaultAsync(p => p.Id == id, ct);
public Task<Project?> GetForUserAsync(Guid userId, Guid projectId, CancellationToken ct = default) =>
Set.FirstOrDefaultAsync(p => p.Id == projectId && p.Members.Any(m => m.UserId == userId), ct);
public Task<List<Project>> GetForUserAsync(Guid userId, CancellationToken ct = default) =>
Set.Where(p => p.Members.Any(m => m.UserId == userId))
.OrderByDescending(p => p.UpdatedAt)
.ToListAsync(ct);
public Task<List<Project>> SearchForUserAsync(Guid userId, string? term, CancellationToken ct = default)
{
var query = Set.Where(p => p.Members.Any(m => m.UserId == userId));
if (!string.IsNullOrWhiteSpace(term))
{
var lower = term.Trim().ToLower();
query = query.Where(p => p.Name.ToLower().Contains(lower));
}
return query.OrderByDescending(p => p.UpdatedAt).ToListAsync(ct);
}
public Task<int> CountMembersAsync(Guid projectId, CancellationToken ct = default) =>
Db.ProjectMembers.CountAsync(m => m.ProjectId == projectId, ct);
public Task<bool> IsMemberAsync(Guid projectId, Guid userId, CancellationToken ct = default) =>
Db.ProjectMembers.AnyAsync(m => m.ProjectId == projectId && m.UserId == userId, ct);
public Task<MemberRole?> GetMemberRoleAsync(Guid projectId, Guid userId, CancellationToken ct = default) =>
Db.ProjectMembers
.Where(m => m.ProjectId == projectId && m.UserId == userId)
.Select(m => (MemberRole?)m.Role)
.SingleOrDefaultAsync(ct);
public Task<ProjectMember?> GetMemberAsync(Guid projectId, Guid userId, CancellationToken ct = default) =>
Db.ProjectMembers.FirstOrDefaultAsync(m => m.ProjectId == projectId && m.UserId == userId, ct);
public Task<ProjectMember?> GetMemberWithUserAsync(Guid projectId, Guid userId, CancellationToken ct = default) =>
Db.ProjectMembers.Include(m => m.User).FirstOrDefaultAsync(m => m.ProjectId == projectId && m.UserId == userId, ct);
public Task<List<ProjectMember>> GetMembersWithUserAsync(Guid projectId, CancellationToken ct = default) =>
Db.ProjectMembers
.Where(m => m.ProjectId == projectId)
.Include(m => m.User)
.OrderByDescending(m => m.Role)
.ThenBy(m => m.User.DisplayName)
.ToListAsync(ct);
public Task<int> CountOwnersAsync(Guid projectId, CancellationToken ct = default) =>
Db.ProjectMembers.CountAsync(m => m.ProjectId == projectId && m.Role == MemberRole.Owner, ct);
public Task<List<Guid>> GetProjectIdsForUserAsync(Guid userId, CancellationToken ct = default) =>
Set.Where(p => p.Members.Any(m => m.UserId == userId)).Select(p => p.Id).ToListAsync(ct);
public Task<List<Guid>> GetOwnedProjectIdsAsync(Guid userId, CancellationToken ct = default) =>
Db.ProjectMembers.Where(m => m.UserId == userId && m.Role == MemberRole.Owner).Select(m => m.ProjectId).ToListAsync(ct);
public Task<List<Guid>> GetDocumentViewableProjectIdsAsync(Guid userId, CancellationToken ct = default) =>
Db.ProjectMembers
.Where(m => m.UserId == userId &&
(m.Role == MemberRole.Owner ||
Db.ProjectMemberPermissions.Any(p => p.ProjectId == m.ProjectId && p.UserId == userId && p.Screen == ProjectPermissionScreens.Documents && p.CanView)))
.Select(m => m.ProjectId)
.ToListAsync(ct);
public void AddMember(ProjectMember member) => Db.ProjectMembers.Add(member);
public void RemoveMember(ProjectMember member) => Db.ProjectMembers.Remove(member);
public Task<ProjectMemberPermission?> GetMemberPermissionAsync(Guid projectId, Guid userId, string screen, CancellationToken ct = default) =>
Db.ProjectMemberPermissions.FirstOrDefaultAsync(p => p.ProjectId == projectId && p.UserId == userId && p.Screen == screen, ct);
public Task<Dictionary<Guid, ProjectMemberPermission>> GetMemberPermissionsAsync(Guid projectId, string screen, CancellationToken ct = default) =>
Db.ProjectMemberPermissions
.Where(p => p.ProjectId == projectId && p.Screen == screen)
.ToDictionaryAsync(p => p.UserId, ct);
public void AddMemberPermission(ProjectMemberPermission permission) => Db.ProjectMemberPermissions.Add(permission);
}
@@ -0,0 +1,13 @@
using Microsoft.EntityFrameworkCore;
using Mws.Application.Common.Repositories;
namespace Mws.Infrastructure.Persistence.Repositories;
public abstract class RepositoryBase<T>(AppDbContext db) : IRepository<T> where T : class
{
protected readonly AppDbContext Db = db;
protected DbSet<T> Set => Db.Set<T>();
public void Add(T entity) => Set.Add(entity);
public void Remove(T entity) => Set.Remove(entity);
}
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore;
using Mws.Application.Common.Repositories;
using Mws.Domain.Roles;
namespace Mws.Infrastructure.Persistence.Repositories;
public class RoleRepository(AppDbContext db) : RepositoryBase<Role>(db), IRoleRepository
{
public Task<List<Role>> GetAllWithPermissionsAsync(CancellationToken ct = default) =>
Set.Include(r => r.Permissions).OrderBy(r => r.Name).ToListAsync(ct);
public Task<Role?> GetByIdWithPermissionsAsync(Guid id, CancellationToken ct = default) =>
Set.Include(r => r.Permissions).FirstOrDefaultAsync(r => r.Id == id, ct);
public Task<Role?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
Set.FirstOrDefaultAsync(r => r.Id == id, ct);
public Task<bool> ExistsByNameAsync(string name, Guid? excludeId, CancellationToken ct = default) =>
Set.AnyAsync(r => r.Name == name && (excludeId == null || r.Id != excludeId), ct);
public Task<RolePermission?> GetPermissionAsync(Guid roleId, string screen, CancellationToken ct = default) =>
Db.RolePermissions.FirstOrDefaultAsync(p => p.RoleId == roleId && p.Screen == screen, ct);
public Task<Dictionary<string, RolePermission>> GetPermissionsAsync(Guid roleId, CancellationToken ct = default) =>
Db.RolePermissions.Where(p => p.RoleId == roleId).ToDictionaryAsync(p => p.Screen, ct);
public void RemovePermissions(IEnumerable<RolePermission> permissions) => Db.RolePermissions.RemoveRange(permissions);
}
@@ -0,0 +1,60 @@
using Microsoft.EntityFrameworkCore;
using Mws.Application.Common.Repositories;
using Mws.Domain.Tasks;
using TaskPriority = Mws.Domain.Tasks.TaskPriority;
using TaskStatus = Mws.Domain.Tasks.TaskStatus;
namespace Mws.Infrastructure.Persistence.Repositories;
public class TaskRepository(AppDbContext db) : RepositoryBase<TaskItem>(db), ITaskRepository
{
public Task<TaskItem?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
Set.FirstOrDefaultAsync(t => t.Id == id, ct);
public Task<TaskItem?> GetWithAssigneeAsync(Guid id, CancellationToken ct = default) =>
Set.Include(t => t.Assignee).FirstOrDefaultAsync(t => t.Id == id, ct);
public Task<List<TaskItem>> GetForProjectAsync(
Guid projectId, TaskStatus? status, TaskPriority? priority, Guid? assigneeId, CancellationToken ct = default)
{
var query = Set.Where(t => t.ProjectId == projectId);
if (status.HasValue)
{
query = query.Where(t => t.Status == status);
}
if (priority.HasValue)
{
query = query.Where(t => t.Priority == priority);
}
if (assigneeId.HasValue)
{
query = query.Where(t => t.AssigneeId == assigneeId);
}
return query.Include(t => t.Assignee).OrderByDescending(t => t.UpdatedAt).ToListAsync(ct);
}
public Task<List<TaskItem>> SearchWithAssigneeAsync(List<Guid> projectIds, string? term, int take, CancellationToken ct = default)
{
var query = Set.Where(t => projectIds.Contains(t.ProjectId));
if (!string.IsNullOrWhiteSpace(term))
{
var lower = term.Trim().ToLower();
query = query.Where(t => t.Title.ToLower().Contains(lower));
}
return query.Include(t => t.Assignee).OrderByDescending(t => t.UpdatedAt).Take(take).ToListAsync(ct);
}
public async Task<Dictionary<TaskStatus, int>> CountByStatusForProjectAsync(Guid projectId, CancellationToken ct = default)
{
return await Set.Where(t => t.ProjectId == projectId)
.GroupBy(t => t.Status)
.Select(g => new { Status = g.Key, Count = g.Count() })
.ToDictionaryAsync(a => a.Status, a => a.Count, ct);
}
public Task<List<TaskItem>> GetRecentForProjectAsync(Guid projectId, int take, CancellationToken ct = default) =>
Set.Where(t => t.ProjectId == projectId).OrderByDescending(t => t.UpdatedAt).Take(take).ToListAsync(ct);
}
@@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore;
using Mws.Application.Common.Repositories;
using Mws.Domain.Users;
namespace Mws.Infrastructure.Persistence.Repositories;
public class UserRepository(AppDbContext db) : RepositoryBase<User>(db), IUserRepository
{
public Task<User?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
Set.FirstOrDefaultAsync(u => u.Id == id, ct);
public Task<User?> GetByIdWithRoleAsync(Guid id, CancellationToken ct = default) =>
Set.Include(u => u.Role).FirstOrDefaultAsync(u => u.Id == id, ct);
public Task<User?> GetByUsernameWithRoleAsync(string username, CancellationToken ct = default) =>
Set.Include(u => u.Role).SingleOrDefaultAsync(u => u.Username == username, ct);
public Task<bool> ExistsByUsernameAsync(string username, CancellationToken ct = default) =>
Set.AnyAsync(u => u.Username == username, ct);
public Task<bool> ExistsByRoleIdAsync(Guid roleId, CancellationToken ct = default) =>
Set.AnyAsync(u => u.RoleId == roleId, ct);
public Task<Guid> GetRoleIdAsync(Guid userId, CancellationToken ct = default) =>
Set.Where(u => u.Id == userId).Select(u => u.RoleId).SingleOrDefaultAsync(ct);
public Task<List<User>> SearchWithRoleAsync(string? term, int? take, CancellationToken ct = default)
{
var query = Set.Include(u => u.Role).AsQueryable();
if (!string.IsNullOrWhiteSpace(term))
{
var lower = term.Trim().ToLower();
query = query.Where(u => u.Username.ToLower().Contains(lower) || u.DisplayName.ToLower().Contains(lower));
}
query = query.OrderBy(u => u.DisplayName);
if (take is { } n)
{
query = query.Take(n);
}
return query.ToListAsync(ct);
}
}
@@ -0,0 +1,28 @@
using Mws.Application.Common;
using Mws.Application.Common.Repositories;
using Mws.Infrastructure.Persistence.Repositories;
namespace Mws.Infrastructure.Persistence;
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _db;
public UnitOfWork(AppDbContext db)
{
_db = db;
Users = new UserRepository(db);
Projects = new ProjectRepository(db);
Tasks = new TaskRepository(db);
Documents = new DocumentRepository(db);
Roles = new RoleRepository(db);
}
public IUserRepository Users { get; }
public IProjectRepository Projects { get; }
public ITaskRepository Tasks { get; }
public IDocumentRepository Documents { get; }
public IRoleRepository Roles { get; }
public Task<int> SaveChangesAsync(CancellationToken ct = default) => _db.SaveChangesAsync(ct);
}
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\mws.application\mws.application.csproj" />
<ProjectReference Include="..\mws.domain\mws.domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.11" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
</ItemGroup>
</Project>