diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index c7dd53f..faca770 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -1,8 +1,7 @@
{
"permissions": {
"allow": [
- "Bash(Stop-Process -Force)",
- "Bash(Select-Object -First 3)"
+ "Bash(powershell:*)"
],
"deny": [],
"ask": []
diff --git a/colaflow-api/ColaFlow.sln b/colaflow-api/ColaFlow.sln
index 0cf0d28..e7c6b64 100644
--- a/colaflow-api/ColaFlow.sln
+++ b/colaflow-api/ColaFlow.sln
@@ -39,6 +39,22 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.ArchitectureTests", "tests\ColaFlow.ArchitectureTests\ColaFlow.ArchitectureTests.csproj", "{A059FDA9-5454-49A8-A025-0FC5130574EE}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Identity", "Identity", "{0EC62A1A-9858-3A60-8A0C-FC9AACFA2EC7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.Modules.Identity.Domain", "src\Modules\Identity\ColaFlow.Modules.Identity.Domain\ColaFlow.Modules.Identity.Domain.csproj", "{1647C962-6F4B-4AF4-8608-11B784B8C59E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.Modules.Identity.Application", "src\Modules\Identity\ColaFlow.Modules.Identity.Application\ColaFlow.Modules.Identity.Application.csproj", "{97193F5A-5F0D-48C2-8A94-9768B624437D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.Modules.Identity.Infrastructure", "src\Modules\Identity\ColaFlow.Modules.Identity.Infrastructure\ColaFlow.Modules.Identity.Infrastructure.csproj", "{775AF575-9989-4D7C-BD3B-18262CD9283F}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{D7DC9B74-6BC4-2470-2038-1E57C2DCB73B}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Identity", "Identity", "{ACB2D19B-6984-27D8-539C-F209B7C78BA5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.Modules.Identity.Domain.Tests", "tests\Modules\Identity\ColaFlow.Modules.Identity.Domain.Tests\ColaFlow.Modules.Identity.Domain.Tests.csproj", "{18EA8D3B-8570-4D51-B410-580F0782A61C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColaFlow.Modules.Identity.Infrastructure.Tests", "tests\Modules\Identity\ColaFlow.Modules.Identity.Infrastructure.Tests\ColaFlow.Modules.Identity.Infrastructure.Tests.csproj", "{6401A1D7-2E1E-4FE1-B2F6-3DC82C2948DA}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -205,6 +221,66 @@ Global
{A059FDA9-5454-49A8-A025-0FC5130574EE}.Release|x64.Build.0 = Release|Any CPU
{A059FDA9-5454-49A8-A025-0FC5130574EE}.Release|x86.ActiveCfg = Release|Any CPU
{A059FDA9-5454-49A8-A025-0FC5130574EE}.Release|x86.Build.0 = Release|Any CPU
+ {1647C962-6F4B-4AF4-8608-11B784B8C59E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1647C962-6F4B-4AF4-8608-11B784B8C59E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1647C962-6F4B-4AF4-8608-11B784B8C59E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1647C962-6F4B-4AF4-8608-11B784B8C59E}.Debug|x64.Build.0 = Debug|Any CPU
+ {1647C962-6F4B-4AF4-8608-11B784B8C59E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1647C962-6F4B-4AF4-8608-11B784B8C59E}.Debug|x86.Build.0 = Debug|Any CPU
+ {1647C962-6F4B-4AF4-8608-11B784B8C59E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1647C962-6F4B-4AF4-8608-11B784B8C59E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1647C962-6F4B-4AF4-8608-11B784B8C59E}.Release|x64.ActiveCfg = Release|Any CPU
+ {1647C962-6F4B-4AF4-8608-11B784B8C59E}.Release|x64.Build.0 = Release|Any CPU
+ {1647C962-6F4B-4AF4-8608-11B784B8C59E}.Release|x86.ActiveCfg = Release|Any CPU
+ {1647C962-6F4B-4AF4-8608-11B784B8C59E}.Release|x86.Build.0 = Release|Any CPU
+ {97193F5A-5F0D-48C2-8A94-9768B624437D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {97193F5A-5F0D-48C2-8A94-9768B624437D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {97193F5A-5F0D-48C2-8A94-9768B624437D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {97193F5A-5F0D-48C2-8A94-9768B624437D}.Debug|x64.Build.0 = Debug|Any CPU
+ {97193F5A-5F0D-48C2-8A94-9768B624437D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {97193F5A-5F0D-48C2-8A94-9768B624437D}.Debug|x86.Build.0 = Debug|Any CPU
+ {97193F5A-5F0D-48C2-8A94-9768B624437D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {97193F5A-5F0D-48C2-8A94-9768B624437D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {97193F5A-5F0D-48C2-8A94-9768B624437D}.Release|x64.ActiveCfg = Release|Any CPU
+ {97193F5A-5F0D-48C2-8A94-9768B624437D}.Release|x64.Build.0 = Release|Any CPU
+ {97193F5A-5F0D-48C2-8A94-9768B624437D}.Release|x86.ActiveCfg = Release|Any CPU
+ {97193F5A-5F0D-48C2-8A94-9768B624437D}.Release|x86.Build.0 = Release|Any CPU
+ {775AF575-9989-4D7C-BD3B-18262CD9283F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {775AF575-9989-4D7C-BD3B-18262CD9283F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {775AF575-9989-4D7C-BD3B-18262CD9283F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {775AF575-9989-4D7C-BD3B-18262CD9283F}.Debug|x64.Build.0 = Debug|Any CPU
+ {775AF575-9989-4D7C-BD3B-18262CD9283F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {775AF575-9989-4D7C-BD3B-18262CD9283F}.Debug|x86.Build.0 = Debug|Any CPU
+ {775AF575-9989-4D7C-BD3B-18262CD9283F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {775AF575-9989-4D7C-BD3B-18262CD9283F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {775AF575-9989-4D7C-BD3B-18262CD9283F}.Release|x64.ActiveCfg = Release|Any CPU
+ {775AF575-9989-4D7C-BD3B-18262CD9283F}.Release|x64.Build.0 = Release|Any CPU
+ {775AF575-9989-4D7C-BD3B-18262CD9283F}.Release|x86.ActiveCfg = Release|Any CPU
+ {775AF575-9989-4D7C-BD3B-18262CD9283F}.Release|x86.Build.0 = Release|Any CPU
+ {18EA8D3B-8570-4D51-B410-580F0782A61C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {18EA8D3B-8570-4D51-B410-580F0782A61C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {18EA8D3B-8570-4D51-B410-580F0782A61C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {18EA8D3B-8570-4D51-B410-580F0782A61C}.Debug|x64.Build.0 = Debug|Any CPU
+ {18EA8D3B-8570-4D51-B410-580F0782A61C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {18EA8D3B-8570-4D51-B410-580F0782A61C}.Debug|x86.Build.0 = Debug|Any CPU
+ {18EA8D3B-8570-4D51-B410-580F0782A61C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {18EA8D3B-8570-4D51-B410-580F0782A61C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {18EA8D3B-8570-4D51-B410-580F0782A61C}.Release|x64.ActiveCfg = Release|Any CPU
+ {18EA8D3B-8570-4D51-B410-580F0782A61C}.Release|x64.Build.0 = Release|Any CPU
+ {18EA8D3B-8570-4D51-B410-580F0782A61C}.Release|x86.ActiveCfg = Release|Any CPU
+ {18EA8D3B-8570-4D51-B410-580F0782A61C}.Release|x86.Build.0 = Release|Any CPU
+ {6401A1D7-2E1E-4FE1-B2F6-3DC82C2948DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6401A1D7-2E1E-4FE1-B2F6-3DC82C2948DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6401A1D7-2E1E-4FE1-B2F6-3DC82C2948DA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6401A1D7-2E1E-4FE1-B2F6-3DC82C2948DA}.Debug|x64.Build.0 = Debug|Any CPU
+ {6401A1D7-2E1E-4FE1-B2F6-3DC82C2948DA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6401A1D7-2E1E-4FE1-B2F6-3DC82C2948DA}.Debug|x86.Build.0 = Debug|Any CPU
+ {6401A1D7-2E1E-4FE1-B2F6-3DC82C2948DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6401A1D7-2E1E-4FE1-B2F6-3DC82C2948DA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6401A1D7-2E1E-4FE1-B2F6-3DC82C2948DA}.Release|x64.ActiveCfg = Release|Any CPU
+ {6401A1D7-2E1E-4FE1-B2F6-3DC82C2948DA}.Release|x64.Build.0 = Release|Any CPU
+ {6401A1D7-2E1E-4FE1-B2F6-3DC82C2948DA}.Release|x86.ActiveCfg = Release|Any CPU
+ {6401A1D7-2E1E-4FE1-B2F6-3DC82C2948DA}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -226,5 +302,13 @@ Global
{2AC4CB72-078B-44D7-A3E6-B1651F1B8C29} = {CA0D0B73-F1EC-F12F-54BA-8DF761F62CA4}
{EF0BCA60-10E6-48AF-807D-416D262B85E3} = {CA0D0B73-F1EC-F12F-54BA-8DF761F62CA4}
{A059FDA9-5454-49A8-A025-0FC5130574EE} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
+ {0EC62A1A-9858-3A60-8A0C-FC9AACFA2EC7} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558}
+ {1647C962-6F4B-4AF4-8608-11B784B8C59E} = {0EC62A1A-9858-3A60-8A0C-FC9AACFA2EC7}
+ {97193F5A-5F0D-48C2-8A94-9768B624437D} = {0EC62A1A-9858-3A60-8A0C-FC9AACFA2EC7}
+ {775AF575-9989-4D7C-BD3B-18262CD9283F} = {0EC62A1A-9858-3A60-8A0C-FC9AACFA2EC7}
+ {D7DC9B74-6BC4-2470-2038-1E57C2DCB73B} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
+ {ACB2D19B-6984-27D8-539C-F209B7C78BA5} = {D7DC9B74-6BC4-2470-2038-1E57C2DCB73B}
+ {18EA8D3B-8570-4D51-B410-580F0782A61C} = {ACB2D19B-6984-27D8-539C-F209B7C78BA5}
+ {6401A1D7-2E1E-4FE1-B2F6-3DC82C2948DA} = {ACB2D19B-6984-27D8-539C-F209B7C78BA5}
EndGlobalSection
EndGlobal
diff --git a/colaflow-api/src/ColaFlow.API/ColaFlow.API.csproj b/colaflow-api/src/ColaFlow.API/ColaFlow.API.csproj
index fe81186..ef2475c 100644
--- a/colaflow-api/src/ColaFlow.API/ColaFlow.API.csproj
+++ b/colaflow-api/src/ColaFlow.API/ColaFlow.API.csproj
@@ -20,11 +20,13 @@
+
+
-
+
diff --git a/colaflow-api/src/ColaFlow.API/Controllers/AuthController.cs b/colaflow-api/src/ColaFlow.API/Controllers/AuthController.cs
new file mode 100644
index 0000000..03ed66b
--- /dev/null
+++ b/colaflow-api/src/ColaFlow.API/Controllers/AuthController.cs
@@ -0,0 +1,38 @@
+using ColaFlow.Modules.Identity.Application.Commands.Login;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ColaFlow.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class AuthController : ControllerBase
+{
+ private readonly IMediator _mediator;
+
+ public AuthController(IMediator mediator)
+ {
+ _mediator = mediator;
+ }
+
+ ///
+ /// Login with email and password
+ ///
+ [HttpPost("login")]
+ public async Task Login([FromBody] LoginCommand command)
+ {
+ var result = await _mediator.Send(command);
+ return Ok(result);
+ }
+
+ ///
+ /// Get current user (requires authentication)
+ ///
+ [HttpGet("me")]
+ // [Authorize] // TODO: Add after JWT middleware is configured
+ public async Task GetCurrentUser()
+ {
+ // TODO: Implement after JWT middleware
+ return Ok(new { message = "Current user endpoint - to be implemented" });
+ }
+}
diff --git a/colaflow-api/src/ColaFlow.API/Controllers/TenantsController.cs b/colaflow-api/src/ColaFlow.API/Controllers/TenantsController.cs
new file mode 100644
index 0000000..56a57cb
--- /dev/null
+++ b/colaflow-api/src/ColaFlow.API/Controllers/TenantsController.cs
@@ -0,0 +1,55 @@
+using ColaFlow.Modules.Identity.Application.Commands.RegisterTenant;
+using ColaFlow.Modules.Identity.Application.Queries.GetTenantBySlug;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ColaFlow.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class TenantsController : ControllerBase
+{
+ private readonly IMediator _mediator;
+
+ public TenantsController(IMediator mediator)
+ {
+ _mediator = mediator;
+ }
+
+ ///
+ /// Register a new tenant (company signup)
+ ///
+ [HttpPost("register")]
+ public async Task Register([FromBody] RegisterTenantCommand command)
+ {
+ var result = await _mediator.Send(command);
+ return Ok(result);
+ }
+
+ ///
+ /// Get tenant by slug (for login page tenant resolution)
+ ///
+ [HttpGet("{slug}")]
+ public async Task GetBySlug(string slug)
+ {
+ var query = new GetTenantBySlugQuery(slug);
+ var result = await _mediator.Send(query);
+
+ if (result == null)
+ return NotFound(new { message = "Tenant not found" });
+
+ return Ok(result);
+ }
+
+ ///
+ /// Check if tenant slug is available
+ ///
+ [HttpGet("check-slug/{slug}")]
+ public async Task CheckSlug(string slug)
+ {
+ var query = new GetTenantBySlugQuery(slug);
+ var result = await _mediator.Send(query);
+
+ return Ok(new { available = result == null });
+ }
+}
diff --git a/colaflow-api/src/ColaFlow.API/Program.cs b/colaflow-api/src/ColaFlow.API/Program.cs
index 7a9aaf0..64357c3 100644
--- a/colaflow-api/src/ColaFlow.API/Program.cs
+++ b/colaflow-api/src/ColaFlow.API/Program.cs
@@ -1,5 +1,7 @@
using ColaFlow.API.Extensions;
using ColaFlow.API.Handlers;
+using ColaFlow.Modules.Identity.Application;
+using ColaFlow.Modules.Identity.Infrastructure;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
@@ -7,6 +9,10 @@ var builder = WebApplication.CreateBuilder(args);
// Register ProjectManagement Module
builder.Services.AddProjectManagementModule(builder.Configuration);
+// Register Identity Module
+builder.Services.AddIdentityApplication();
+builder.Services.AddIdentityInfrastructure(builder.Configuration);
+
// Add controllers
builder.Services.AddControllers();
diff --git a/colaflow-api/src/ColaFlow.API/appsettings.Development.json b/colaflow-api/src/ColaFlow.API/appsettings.Development.json
index 533fa21..98f588e 100644
--- a/colaflow-api/src/ColaFlow.API/appsettings.Development.json
+++ b/colaflow-api/src/ColaFlow.API/appsettings.Development.json
@@ -1,6 +1,7 @@
{
"ConnectionStrings": {
- "PMDatabase": "Host=localhost;Port=5432;Database=colaflow_pm;Username=colaflow;Password=colaflow_dev_password"
+ "PMDatabase": "Host=localhost;Port=5432;Database=colaflow_pm;Username=colaflow;Password=colaflow_dev_password",
+ "DefaultConnection": "Host=localhost;Port=5432;Database=colaflow_identity;Username=colaflow;Password=colaflow_dev_password"
},
"MediatR": {
"LicenseKey": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxNzkzNTc3NjAwIiwiaWF0IjoiMTc2MjEyNTU2MiIsImFjY291bnRfaWQiOiIwMTlhNDZkZGZiZjk3YTk4Yjg1ZTVmOTllNWRhZjIwNyIsImN1c3RvbWVyX2lkIjoiY3RtXzAxazkzZHdnOG0weDByanp3Mm5rM2dxeDExIiwic3ViX2lkIjoiLSIsImVkaXRpb24iOiIwIiwidHlwZSI6IjIifQ.V45vUlze27pQG3Vs9dvagyUTSp-a74ymB6I0TIGD_NwFt1mMMPsuVXOKH1qK7A7V5qDQBvYyryzJy8xRE1rRKq2MJKgyfYjvzuGkpBbKbM6JRQPYknb5tjF-Rf3LAeWp73FiqbPZOPt5saCsoKqUHej-4zcKg5GA4y-PpGaGAONKyqwK9G2rvc1BUHfEnHKRMr0pprA5W1Yx-Lry85KOckUsI043HGOdfbubnGdAZs74FKvrV2qVir6K6VsZjWwX8IFnl1CzxjICa5MxyHOAVpXRnRtMt6fpsA1fMstFuRjq_2sbqGfsTv6LyCzLPnXdmU5DnWZHUcjy0xlAT_f0aw"
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/ColaFlow.Modules.Identity.Application.csproj b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/ColaFlow.Modules.Identity.Application.csproj
new file mode 100644
index 0000000..a1d81f6
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/ColaFlow.Modules.Identity.Application.csproj
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/Login/LoginCommand.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/Login/LoginCommand.cs
new file mode 100644
index 0000000..c501e7f
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/Login/LoginCommand.cs
@@ -0,0 +1,10 @@
+using ColaFlow.Modules.Identity.Application.Dtos;
+using MediatR;
+
+namespace ColaFlow.Modules.Identity.Application.Commands.Login;
+
+public record LoginCommand(
+ string TenantSlug,
+ string Email,
+ string Password
+) : IRequest;
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/Login/LoginCommandHandler.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/Login/LoginCommandHandler.cs
new file mode 100644
index 0000000..0caf114
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/Login/LoginCommandHandler.cs
@@ -0,0 +1,84 @@
+using ColaFlow.Modules.Identity.Application.Dtos;
+using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
+using ColaFlow.Modules.Identity.Domain.Aggregates.Users;
+using ColaFlow.Modules.Identity.Domain.Repositories;
+using MediatR;
+
+namespace ColaFlow.Modules.Identity.Application.Commands.Login;
+
+public class LoginCommandHandler : IRequestHandler
+{
+ private readonly ITenantRepository _tenantRepository;
+ private readonly IUserRepository _userRepository;
+ // Note: In production, inject IPasswordHasher and IJwtService
+
+ public LoginCommandHandler(
+ ITenantRepository tenantRepository,
+ IUserRepository userRepository)
+ {
+ _tenantRepository = tenantRepository;
+ _userRepository = userRepository;
+ }
+
+ public async Task Handle(LoginCommand request, CancellationToken cancellationToken)
+ {
+ // 1. Find tenant
+ var slug = TenantSlug.Create(request.TenantSlug);
+ var tenant = await _tenantRepository.GetBySlugAsync(slug, cancellationToken);
+ if (tenant == null)
+ {
+ throw new UnauthorizedAccessException("Invalid credentials");
+ }
+
+ // 2. Find user
+ var email = Email.Create(request.Email);
+ var user = await _userRepository.GetByEmailAsync(TenantId.Create(tenant.Id), email, cancellationToken);
+ if (user == null)
+ {
+ throw new UnauthorizedAccessException("Invalid credentials");
+ }
+
+ // 3. Verify password (simplified - TODO: use IPasswordHasher)
+ // if (!PasswordHasher.Verify(request.Password, user.PasswordHash))
+ // {
+ // throw new UnauthorizedAccessException("Invalid credentials");
+ // }
+
+ // 4. Generate JWT token (simplified - TODO: use IJwtService)
+ var accessToken = "dummy-token";
+
+ // 5. Update last login time
+ user.RecordLogin();
+ await _userRepository.UpdateAsync(user, cancellationToken);
+
+ // 6. Return result
+ return new LoginResponseDto
+ {
+ User = new UserDto
+ {
+ Id = user.Id,
+ TenantId = tenant.Id,
+ Email = user.Email.Value,
+ FullName = user.FullName.Value,
+ Status = user.Status.ToString(),
+ AuthProvider = user.AuthProvider.ToString(),
+ IsEmailVerified = user.EmailVerifiedAt.HasValue,
+ LastLoginAt = user.LastLoginAt,
+ CreatedAt = user.CreatedAt
+ },
+ Tenant = new TenantDto
+ {
+ Id = tenant.Id,
+ Name = tenant.Name.Value,
+ Slug = tenant.Slug.Value,
+ Status = tenant.Status.ToString(),
+ Plan = tenant.Plan.ToString(),
+ SsoEnabled = tenant.SsoConfig != null,
+ SsoProvider = tenant.SsoConfig?.Provider.ToString(),
+ CreatedAt = tenant.CreatedAt,
+ UpdatedAt = tenant.UpdatedAt ?? tenant.CreatedAt
+ },
+ AccessToken = accessToken
+ };
+ }
+}
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/Login/LoginCommandValidator.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/Login/LoginCommandValidator.cs
new file mode 100644
index 0000000..730b944
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/Login/LoginCommandValidator.cs
@@ -0,0 +1,19 @@
+using FluentValidation;
+
+namespace ColaFlow.Modules.Identity.Application.Commands.Login;
+
+public class LoginCommandValidator : AbstractValidator
+{
+ public LoginCommandValidator()
+ {
+ RuleFor(x => x.TenantSlug)
+ .NotEmpty().WithMessage("Tenant slug is required");
+
+ RuleFor(x => x.Email)
+ .NotEmpty().WithMessage("Email is required")
+ .EmailAddress().WithMessage("Invalid email format");
+
+ RuleFor(x => x.Password)
+ .NotEmpty().WithMessage("Password is required");
+ }
+}
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/RegisterTenant/RegisterTenantCommand.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/RegisterTenant/RegisterTenantCommand.cs
new file mode 100644
index 0000000..4af839c
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/RegisterTenant/RegisterTenantCommand.cs
@@ -0,0 +1,19 @@
+using ColaFlow.Modules.Identity.Application.Dtos;
+using MediatR;
+
+namespace ColaFlow.Modules.Identity.Application.Commands.RegisterTenant;
+
+public record RegisterTenantCommand(
+ string TenantName,
+ string TenantSlug,
+ string SubscriptionPlan,
+ string AdminEmail,
+ string AdminPassword,
+ string AdminFullName
+) : IRequest;
+
+public record RegisterTenantResult(
+ TenantDto Tenant,
+ UserDto AdminUser,
+ string AccessToken
+);
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/RegisterTenant/RegisterTenantCommandHandler.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/RegisterTenant/RegisterTenantCommandHandler.cs
new file mode 100644
index 0000000..fa5105a
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/RegisterTenant/RegisterTenantCommandHandler.cs
@@ -0,0 +1,83 @@
+using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
+using ColaFlow.Modules.Identity.Domain.Aggregates.Users;
+using ColaFlow.Modules.Identity.Domain.Repositories;
+using MediatR;
+
+namespace ColaFlow.Modules.Identity.Application.Commands.RegisterTenant;
+
+public class RegisterTenantCommandHandler : IRequestHandler
+{
+ private readonly ITenantRepository _tenantRepository;
+ private readonly IUserRepository _userRepository;
+ // Note: In production, inject IJwtService and IPasswordHasher
+
+ public RegisterTenantCommandHandler(
+ ITenantRepository tenantRepository,
+ IUserRepository userRepository)
+ {
+ _tenantRepository = tenantRepository;
+ _userRepository = userRepository;
+ }
+
+ public async Task Handle(
+ RegisterTenantCommand request,
+ CancellationToken cancellationToken)
+ {
+ // 1. Validate slug uniqueness
+ var slug = TenantSlug.Create(request.TenantSlug);
+ var slugExists = await _tenantRepository.ExistsBySlugAsync(slug, cancellationToken);
+ if (slugExists)
+ {
+ throw new InvalidOperationException($"Tenant slug '{request.TenantSlug}' is already taken");
+ }
+
+ // 2. Create tenant
+ var plan = Enum.Parse(request.SubscriptionPlan);
+ var tenant = Tenant.Create(
+ TenantName.Create(request.TenantName),
+ slug,
+ plan);
+
+ await _tenantRepository.AddAsync(tenant, cancellationToken);
+
+ // 3. Create admin user
+ // Note: In production, hash password first using IPasswordHasher
+ var adminUser = User.CreateLocal(
+ TenantId.Create(tenant.Id),
+ Email.Create(request.AdminEmail),
+ request.AdminPassword, // TODO: Hash password
+ FullName.Create(request.AdminFullName));
+
+ await _userRepository.AddAsync(adminUser, cancellationToken);
+
+ // 4. Generate JWT token (simplified - TODO: use IJwtService)
+ var accessToken = "dummy-token";
+
+ // 5. Return result
+ return new RegisterTenantResult(
+ new Dtos.TenantDto
+ {
+ Id = tenant.Id,
+ Name = tenant.Name.Value,
+ Slug = tenant.Slug.Value,
+ Status = tenant.Status.ToString(),
+ Plan = tenant.Plan.ToString(),
+ SsoEnabled = tenant.SsoConfig != null,
+ SsoProvider = tenant.SsoConfig?.Provider.ToString(),
+ CreatedAt = tenant.CreatedAt,
+ UpdatedAt = tenant.UpdatedAt ?? tenant.CreatedAt
+ },
+ new Dtos.UserDto
+ {
+ Id = adminUser.Id,
+ TenantId = tenant.Id,
+ Email = adminUser.Email.Value,
+ FullName = adminUser.FullName.Value,
+ Status = adminUser.Status.ToString(),
+ AuthProvider = adminUser.AuthProvider.ToString(),
+ IsEmailVerified = adminUser.EmailVerifiedAt.HasValue,
+ CreatedAt = adminUser.CreatedAt
+ },
+ accessToken);
+ }
+}
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/RegisterTenant/RegisterTenantCommandValidator.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/RegisterTenant/RegisterTenantCommandValidator.cs
new file mode 100644
index 0000000..480b168
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/RegisterTenant/RegisterTenantCommandValidator.cs
@@ -0,0 +1,43 @@
+using FluentValidation;
+
+namespace ColaFlow.Modules.Identity.Application.Commands.RegisterTenant;
+
+public class RegisterTenantCommandValidator : AbstractValidator
+{
+ public RegisterTenantCommandValidator()
+ {
+ RuleFor(x => x.TenantName)
+ .NotEmpty().WithMessage("Tenant name is required")
+ .MinimumLength(2).WithMessage("Tenant name must be at least 2 characters")
+ .MaximumLength(100).WithMessage("Tenant name cannot exceed 100 characters");
+
+ RuleFor(x => x.TenantSlug)
+ .NotEmpty().WithMessage("Tenant slug is required")
+ .MinimumLength(3).WithMessage("Tenant slug must be at least 3 characters")
+ .MaximumLength(50).WithMessage("Tenant slug cannot exceed 50 characters")
+ .Matches("^[a-z0-9]+(?:-[a-z0-9]+)*$")
+ .WithMessage("Tenant slug can only contain lowercase letters, numbers, and hyphens");
+
+ RuleFor(x => x.SubscriptionPlan)
+ .NotEmpty().WithMessage("Subscription plan is required")
+ .Must(plan => new[] { "Free", "Starter", "Professional", "Enterprise" }.Contains(plan))
+ .WithMessage("Invalid subscription plan");
+
+ RuleFor(x => x.AdminEmail)
+ .NotEmpty().WithMessage("Admin email is required")
+ .EmailAddress().WithMessage("Invalid email format");
+
+ RuleFor(x => x.AdminPassword)
+ .NotEmpty().WithMessage("Admin password is required")
+ .MinimumLength(8).WithMessage("Password must be at least 8 characters")
+ .Matches("[A-Z]").WithMessage("Password must contain at least one uppercase letter")
+ .Matches("[a-z]").WithMessage("Password must contain at least one lowercase letter")
+ .Matches("[0-9]").WithMessage("Password must contain at least one digit")
+ .Matches("[^a-zA-Z0-9]").WithMessage("Password must contain at least one special character");
+
+ RuleFor(x => x.AdminFullName)
+ .NotEmpty().WithMessage("Admin full name is required")
+ .MinimumLength(2).WithMessage("Full name must be at least 2 characters")
+ .MaximumLength(100).WithMessage("Full name cannot exceed 100 characters");
+ }
+}
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/DependencyInjection.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/DependencyInjection.cs
new file mode 100644
index 0000000..9056d7f
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/DependencyInjection.cs
@@ -0,0 +1,21 @@
+using FluentValidation;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace ColaFlow.Modules.Identity.Application;
+
+public static class DependencyInjection
+{
+ public static IServiceCollection AddIdentityApplication(this IServiceCollection services)
+ {
+ // MediatR
+ services.AddMediatR(config =>
+ {
+ config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly);
+ });
+
+ // FluentValidation
+ services.AddValidatorsFromAssembly(typeof(DependencyInjection).Assembly);
+
+ return services;
+ }
+}
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Dtos/LoginResponseDto.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Dtos/LoginResponseDto.cs
new file mode 100644
index 0000000..2a66d05
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Dtos/LoginResponseDto.cs
@@ -0,0 +1,8 @@
+namespace ColaFlow.Modules.Identity.Application.Dtos;
+
+public class LoginResponseDto
+{
+ public UserDto User { get; set; } = null!;
+ public TenantDto Tenant { get; set; } = null!;
+ public string AccessToken { get; set; } = string.Empty;
+}
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Dtos/TenantDto.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Dtos/TenantDto.cs
new file mode 100644
index 0000000..e56f006
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Dtos/TenantDto.cs
@@ -0,0 +1,14 @@
+namespace ColaFlow.Modules.Identity.Application.Dtos;
+
+public class TenantDto
+{
+ public Guid Id { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public string Slug { get; set; } = string.Empty;
+ public string Status { get; set; } = string.Empty;
+ public string Plan { get; set; } = string.Empty;
+ public bool SsoEnabled { get; set; }
+ public string? SsoProvider { get; set; }
+ public DateTime CreatedAt { get; set; }
+ public DateTime UpdatedAt { get; set; }
+}
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Dtos/UserDto.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Dtos/UserDto.cs
new file mode 100644
index 0000000..29ca33e
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Dtos/UserDto.cs
@@ -0,0 +1,16 @@
+namespace ColaFlow.Modules.Identity.Application.Dtos;
+
+public class UserDto
+{
+ public Guid Id { get; set; }
+ public Guid TenantId { get; set; }
+ public string Email { get; set; } = string.Empty;
+ public string FullName { get; set; } = string.Empty;
+ public string Status { get; set; } = string.Empty;
+ public string AuthProvider { get; set; } = string.Empty;
+ public string? AvatarUrl { get; set; }
+ public string? JobTitle { get; set; }
+ public bool IsEmailVerified { get; set; }
+ public DateTime? LastLoginAt { get; set; }
+ public DateTime CreatedAt { get; set; }
+}
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Queries/GetTenantBySlug/GetTenantBySlugQuery.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Queries/GetTenantBySlug/GetTenantBySlugQuery.cs
new file mode 100644
index 0000000..1c32c9c
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Queries/GetTenantBySlug/GetTenantBySlugQuery.cs
@@ -0,0 +1,6 @@
+using ColaFlow.Modules.Identity.Application.Dtos;
+using MediatR;
+
+namespace ColaFlow.Modules.Identity.Application.Queries.GetTenantBySlug;
+
+public record GetTenantBySlugQuery(string Slug) : IRequest;
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Queries/GetTenantBySlug/GetTenantBySlugQueryHandler.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Queries/GetTenantBySlug/GetTenantBySlugQueryHandler.cs
new file mode 100644
index 0000000..ace95bd
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Queries/GetTenantBySlug/GetTenantBySlugQueryHandler.cs
@@ -0,0 +1,38 @@
+using ColaFlow.Modules.Identity.Application.Dtos;
+using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
+using ColaFlow.Modules.Identity.Domain.Repositories;
+using MediatR;
+
+namespace ColaFlow.Modules.Identity.Application.Queries.GetTenantBySlug;
+
+public class GetTenantBySlugQueryHandler : IRequestHandler
+{
+ private readonly ITenantRepository _tenantRepository;
+
+ public GetTenantBySlugQueryHandler(ITenantRepository tenantRepository)
+ {
+ _tenantRepository = tenantRepository;
+ }
+
+ public async Task Handle(GetTenantBySlugQuery request, CancellationToken cancellationToken)
+ {
+ var slug = TenantSlug.Create(request.Slug);
+ var tenant = await _tenantRepository.GetBySlugAsync(slug, cancellationToken);
+
+ if (tenant == null)
+ return null;
+
+ return new TenantDto
+ {
+ Id = tenant.Id,
+ Name = tenant.Name.Value,
+ Slug = tenant.Slug.Value,
+ Status = tenant.Status.ToString(),
+ Plan = tenant.Plan.ToString(),
+ SsoEnabled = tenant.SsoConfig != null,
+ SsoProvider = tenant.SsoConfig?.Provider.ToString(),
+ CreatedAt = tenant.CreatedAt,
+ UpdatedAt = tenant.UpdatedAt ?? tenant.CreatedAt
+ };
+ }
+}
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/SsoConfiguredEvent.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/SsoConfiguredEvent.cs
new file mode 100644
index 0000000..9bea05e
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/SsoConfiguredEvent.cs
@@ -0,0 +1,5 @@
+using ColaFlow.Shared.Kernel.Events;
+
+namespace ColaFlow.Modules.Identity.Domain.Aggregates.Tenants.Events;
+
+public sealed record SsoConfiguredEvent(Guid TenantId, SsoProvider Provider) : DomainEvent;
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/SsoDisabledEvent.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/SsoDisabledEvent.cs
new file mode 100644
index 0000000..c658aba
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/SsoDisabledEvent.cs
@@ -0,0 +1,5 @@
+using ColaFlow.Shared.Kernel.Events;
+
+namespace ColaFlow.Modules.Identity.Domain.Aggregates.Tenants.Events;
+
+public sealed record SsoDisabledEvent(Guid TenantId) : DomainEvent;
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/TenantActivatedEvent.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/TenantActivatedEvent.cs
new file mode 100644
index 0000000..3a19bd0
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/TenantActivatedEvent.cs
@@ -0,0 +1,5 @@
+using ColaFlow.Shared.Kernel.Events;
+
+namespace ColaFlow.Modules.Identity.Domain.Aggregates.Tenants.Events;
+
+public sealed record TenantActivatedEvent(Guid TenantId) : DomainEvent;
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/TenantCancelledEvent.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/TenantCancelledEvent.cs
new file mode 100644
index 0000000..fe4ebea
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/TenantCancelledEvent.cs
@@ -0,0 +1,5 @@
+using ColaFlow.Shared.Kernel.Events;
+
+namespace ColaFlow.Modules.Identity.Domain.Aggregates.Tenants.Events;
+
+public sealed record TenantCancelledEvent(Guid TenantId) : DomainEvent;
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/TenantCreatedEvent.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/TenantCreatedEvent.cs
new file mode 100644
index 0000000..53cae8b
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/TenantCreatedEvent.cs
@@ -0,0 +1,5 @@
+using ColaFlow.Shared.Kernel.Events;
+
+namespace ColaFlow.Modules.Identity.Domain.Aggregates.Tenants.Events;
+
+public sealed record TenantCreatedEvent(Guid TenantId, string Slug) : DomainEvent;
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/TenantPlanUpgradedEvent.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/TenantPlanUpgradedEvent.cs
new file mode 100644
index 0000000..1a48f31
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/TenantPlanUpgradedEvent.cs
@@ -0,0 +1,5 @@
+using ColaFlow.Shared.Kernel.Events;
+
+namespace ColaFlow.Modules.Identity.Domain.Aggregates.Tenants.Events;
+
+public sealed record TenantPlanUpgradedEvent(Guid TenantId, SubscriptionPlan NewPlan) : DomainEvent;
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/TenantSuspendedEvent.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/TenantSuspendedEvent.cs
new file mode 100644
index 0000000..63b30f1
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/Events/TenantSuspendedEvent.cs
@@ -0,0 +1,5 @@
+using ColaFlow.Shared.Kernel.Events;
+
+namespace ColaFlow.Modules.Identity.Domain.Aggregates.Tenants.Events;
+
+public sealed record TenantSuspendedEvent(Guid TenantId, string Reason) : DomainEvent;
diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/SsoConfiguration.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/SsoConfiguration.cs
new file mode 100644
index 0000000..3c16604
--- /dev/null
+++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Tenants/SsoConfiguration.cs
@@ -0,0 +1,93 @@
+using ColaFlow.Shared.Kernel.Common;
+
+namespace ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
+
+public sealed class SsoConfiguration : ValueObject
+{
+ public SsoProvider Provider { get; }
+ public string Authority { get; }
+ public string ClientId { get; }
+ public string ClientSecret { get; } // Encrypted in database
+ public string? MetadataUrl { get; }
+
+ // SAML-specific
+ public string? EntityId { get; }
+ public string? SignOnUrl { get; }
+ public string? Certificate { get; }
+
+ private SsoConfiguration(
+ SsoProvider provider,
+ string authority,
+ string clientId,
+ string clientSecret,
+ string? metadataUrl = null,
+ string? entityId = null,
+ string? signOnUrl = null,
+ string? certificate = null)
+ {
+ Provider = provider;
+ Authority = authority;
+ ClientId = clientId;
+ ClientSecret = clientSecret;
+ MetadataUrl = metadataUrl;
+ EntityId = entityId;
+ SignOnUrl = signOnUrl;
+ Certificate = certificate;
+ }
+
+ public static SsoConfiguration CreateOidc(
+ SsoProvider provider,
+ string authority,
+ string clientId,
+ string clientSecret,
+ string? metadataUrl = null)
+ {
+ if (provider == SsoProvider.GenericSaml)
+ throw new ArgumentException("Use CreateSaml for SAML configuration");
+
+ if (string.IsNullOrWhiteSpace(authority))
+ throw new ArgumentException("Authority is required", nameof(authority));
+
+ if (string.IsNullOrWhiteSpace(clientId))
+ throw new ArgumentException("Client ID is required", nameof(clientId));
+
+ if (string.IsNullOrWhiteSpace(clientSecret))
+ throw new ArgumentException("Client secret is required", nameof(clientSecret));
+
+ return new SsoConfiguration(provider, authority, clientId, clientSecret, metadataUrl);
+ }
+
+ public static SsoConfiguration CreateSaml(
+ string entityId,
+ string signOnUrl,
+ string certificate,
+ string? metadataUrl = null)
+ {
+ if (string.IsNullOrWhiteSpace(entityId))
+ throw new ArgumentException("Entity ID is required", nameof(entityId));
+
+ if (string.IsNullOrWhiteSpace(signOnUrl))
+ throw new ArgumentException("Sign-on URL is required", nameof(signOnUrl));
+
+ if (string.IsNullOrWhiteSpace(certificate))
+ throw new ArgumentException("Certificate is required", nameof(certificate));
+
+ return new SsoConfiguration(
+ SsoProvider.GenericSaml,
+ signOnUrl,
+ entityId,
+ string.Empty, // No client secret for SAML
+ metadataUrl,
+ entityId,
+ signOnUrl,
+ certificate);
+ }
+
+ protected override IEnumerable