In progress
This commit is contained in:
@@ -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<LoginResponseDto>;
|
||||
@@ -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<LoginCommand, LoginResponseDto>
|
||||
{
|
||||
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<LoginResponseDto> 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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace ColaFlow.Modules.Identity.Application.Commands.Login;
|
||||
|
||||
public class LoginCommandValidator : AbstractValidator<LoginCommand>
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -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<RegisterTenantResult>;
|
||||
|
||||
public record RegisterTenantResult(
|
||||
TenantDto Tenant,
|
||||
UserDto AdminUser,
|
||||
string AccessToken
|
||||
);
|
||||
@@ -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<RegisterTenantCommand, RegisterTenantResult>
|
||||
{
|
||||
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<RegisterTenantResult> 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<SubscriptionPlan>(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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace ColaFlow.Modules.Identity.Application.Commands.RegisterTenant;
|
||||
|
||||
public class RegisterTenantCommandValidator : AbstractValidator<RegisterTenantCommand>
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user