diff --git a/docs/en/architecture/iam-proposal.md b/docs/en/architecture/iam-proposal.md
index 5d8a6d0d..5780783c 100644
--- a/docs/en/architecture/iam-proposal.md
+++ b/docs/en/architecture/iam-proposal.md
@@ -230,7 +230,41 @@ graph TD
| `PUT` | `/api/v1/roles/{id}` | Cập nhật role | ✅ Admin |
| `DELETE` | `/api/v1/roles/{id}` | Xóa role | ✅ Admin |
-### 4.2 Access Management APIs
+### 4.4 Organization & Group APIs ✅ (New in Phase 2)
+
+| Method | Endpoint | Mô tả | Auth |
+|--------|----------|-------|------|
+| `GET` | `/api/v1/organizations/{id}` | Lấy tổ chức theo ID | ✅ |
+| `GET` | `/api/v1/organizations/slug/{slug}` | Lấy tổ chức theo slug | ✅ |
+| `POST` | `/api/v1/organizations` | Tạo tổ chức mới | ✅ |
+| `PUT` | `/api/v1/organizations/{id}` | Cập nhật tổ chức | ✅ |
+| `DELETE` | `/api/v1/organizations/{id}` | Lưu trữ (archive) tổ chức | ✅ |
+| `GET` | `/api/v1/organizations/{id}/hierarchy` | Lấy phân cấp tổ chức | ✅ |
+| `GET` | `/api/v1/organizations/{id}/children` | Lấy tổ chức con | ✅ |
+| `GET` | `/api/v1/groups` | Danh sách groups theo organizationId | ✅ |
+| `GET` | `/api/v1/groups/{id}` | Lấy group theo ID | ✅ |
+| `POST` | `/api/v1/groups` | Tạo group mới | ✅ |
+| `DELETE` | `/api/v1/groups/{id}` | Xóa group (soft delete) | ✅ |
+| `POST` | `/api/v1/groups/{id}/members` | Thêm thành viên vào group | ✅ |
+| `DELETE` | `/api/v1/groups/{id}/members/{userId}` | Xóa thành viên khỏi group | ✅ |
+
+### 4.5 User Profile APIs ✅ (New in Phase 2)
+
+| Method | Endpoint | Mô tả | Auth |
+|--------|----------|-------|------|
+| `GET` | `/api/v1/users/{id}/profile` | Lấy profile user | ✅ |
+| `PUT` | `/api/v1/users/{id}/profile` | Cập nhật profile user | ✅ |
+| `PUT` | `/api/v1/users/{id}/profile/attributes/{key}` | Đặt profile attribute | ✅ |
+
+### 4.6 Identity Verification APIs ✅ (New in Phase 2)
+
+| Method | Endpoint | Mô tả | Auth |
+|--------|----------|-------|------|
+| `POST` | `/api/v1/verifications/phone` | Yêu cầu xác thực số điện thoại | ✅ |
+| `POST` | `/api/v1/verifications/email` | Yêu cầu xác thực email | ✅ |
+| `POST` | `/api/v1/verifications/{id}/confirm` | Xác nhận với OTP code | ✅ |
+
+### 4.7 Access Management APIs (Planned)
```
# Access Requests
@@ -281,17 +315,26 @@ GET /api/v1/governance/reports/security-events
## 5. Implementation Roadmap
-### Phase 1: Foundation (Weeks 1-4)
-- ✅ Migrate từ auth-service sang iam-service
-- 🔄 Tổ chức lại modules theo IAM structure
-- 🔄 Mở rộng database schema với identity models
-- 🔄 Implement User Profile module
+### Phase 1: Foundation ✅ (Completed)
+- ✅ Migrate từ auth-service sang iam-service (.NET 10 + Duende IdentityServer)
+- ✅ CQRS với MediatR Pattern
+- ✅ User Registration, Login, Logout
+- ✅ Password Management (change-password)
+- ✅ User Management APIs (CRUD)
+- ✅ Role Management APIs
-### Phase 2: Identity Management (Weeks 5-8)
-- 🔄 User lifecycle management
-- 🔄 Identity verification (email, phone, document)
-- 🔄 Organization & Group management
-- 🔄 Profile management with extended attributes
+### Phase 1.5: Enhanced Security ✅ (Completed)
+- ✅ Email Verification (send + confirm)
+- ✅ 2FA/MFA với TOTP (QR Code, Recovery Codes)
+- ✅ Social Login (Google, Facebook OAuth)
+- ✅ Distributed Caching với Redis (ICacheService)
+- ✅ Token Blacklisting cho logout
+
+### Phase 2: Identity Management ✅ (Completed)
+- ✅ User lifecycle management
+- ✅ Identity verification (phone, email)
+- ✅ Organization & Group management
+- ✅ Profile management with extended attributes (ProfileAttribute entity)
### Phase 3: Access Management (Weeks 9-12)
- 🔄 Access request/approval workflows
diff --git a/docs/vi/architecture/iam-proposal.md b/docs/vi/architecture/iam-proposal.md
index b1154201..6f59e868 100644
--- a/docs/vi/architecture/iam-proposal.md
+++ b/docs/vi/architecture/iam-proposal.md
@@ -256,7 +256,41 @@ graph TD
| `PUT` | `/api/v1/roles/{id}` | Cập nhật role | ✅ Admin |
| `DELETE` | `/api/v1/roles/{id}` | Xóa role | ✅ Admin |
-### 4.7 Access Management APIs (Planned)
+### 4.7 Organization & Group APIs ✅ (New in Phase 2)
+
+| Method | Endpoint | Mô tả | Auth |
+|--------|----------|-------|------|
+| `GET` | `/api/v1/organizations/{id}` | Lấy tổ chức theo ID | ✅ |
+| `GET` | `/api/v1/organizations/slug/{slug}` | Lấy tổ chức theo slug | ✅ |
+| `POST` | `/api/v1/organizations` | Tạo tổ chức mới | ✅ |
+| `PUT` | `/api/v1/organizations/{id}` | Cập nhật tổ chức | ✅ |
+| `DELETE` | `/api/v1/organizations/{id}` | Lưu trữ (archive) tổ chức | ✅ |
+| `GET` | `/api/v1/organizations/{id}/hierarchy` | Lấy phân cấp tổ chức | ✅ |
+| `GET` | `/api/v1/organizations/{id}/children` | Lấy tổ chức con | ✅ |
+| `GET` | `/api/v1/groups` | Danh sách groups theo organizationId | ✅ |
+| `GET` | `/api/v1/groups/{id}` | Lấy group theo ID | ✅ |
+| `POST` | `/api/v1/groups` | Tạo group mới | ✅ |
+| `DELETE` | `/api/v1/groups/{id}` | Xóa group (soft delete) | ✅ |
+| `POST` | `/api/v1/groups/{id}/members` | Thêm thành viên vào group | ✅ |
+| `DELETE` | `/api/v1/groups/{id}/members/{userId}` | Xóa thành viên khỏi group | ✅ |
+
+### 4.8 User Profile APIs ✅ (New in Phase 2)
+
+| Method | Endpoint | Mô tả | Auth |
+|--------|----------|-------|------|
+| `GET` | `/api/v1/users/{id}/profile` | Lấy profile user | ✅ |
+| `PUT` | `/api/v1/users/{id}/profile` | Cập nhật profile user | ✅ |
+| `PUT` | `/api/v1/users/{id}/profile/attributes/{key}` | Đặt profile attribute | ✅ |
+
+### 4.9 Identity Verification APIs ✅ (New in Phase 2)
+
+| Method | Endpoint | Mô tả | Auth |
+|--------|----------|-------|------|
+| `POST` | `/api/v1/verifications/phone` | Yêu cầu xác thực số điện thoại | ✅ |
+| `POST` | `/api/v1/verifications/email` | Yêu cầu xác thực email | ✅ |
+| `POST` | `/api/v1/verifications/{id}/confirm` | Xác nhận với OTP code | ✅ |
+
+### 4.10 Access Management APIs (Planned)
> [!NOTE]
> Các APIs dưới đây là tính năng **đang được lên kế hoạch**, chưa triển khai.
@@ -328,11 +362,11 @@ GET /api/v1/governance/reports/security-events
- ✅ Distributed Caching với Redis (ICacheService)
- ✅ Token Blacklisting cho logout
-### Phase 2: Identity Management (Planned)
-- 🔄 User lifecycle management
-- 🔄 Identity verification (phone, document - KYC)
-- 🔄 Organization & Group management
-- 🔄 Profile management with extended attributes
+### Phase 2: Identity Management ✅ (Completed)
+- ✅ User lifecycle management
+- ✅ Identity verification (phone, email)
+- ✅ Organization & Group management
+- ✅ Profile management with extended attributes (ProfileAttribute entity)
### Phase 3: Access Management (Planned)
- 🔄 Access request/approval workflows
@@ -376,30 +410,12 @@ GET /api/v1/governance/reports/security-events
- ✅ Better user experience
- ✅ Enhanced security với MFA & verification
----
-
-## 7. Migration Strategy
-
-### Từ Auth Service → IAM Service
-
-1. **Rename Service**: `services/auth-service` → `services/iam-service`
-2. **Update Package Name**: `@goodgo/auth-service` → `@goodgo/iam-service`
-3. **Update Routes**:
- - Giữ backward compatibility với `/api/v1/auth/*`
- - Thêm routes mới cho `/api/v1/identity/*`, `/api/v1/access/*`, `/api/v1/governance/*`
-4. **Database Migration**:
- - Thêm schema mới cho identity, access, governance
- - Giữ nguyên các tables hiện có (backward compatible)
-5. **Gradual Rollout**:
- - Phase 1: Deploy cùng auth-service (dual deployment)
- - Phase 2: Migrate clients dần dần
- - Phase 3: Deprecate auth-service khi migration hoàn tất
---
## Kết Luận
-Đề xuất này mở rộng `auth-service` thành `IAM Service` với đầy đủ các tính năng:
+Đề xuất này mở rộng thành `IAM Service` với đầy đủ các tính năng:
- **Identity Management** đầy đủ
- **Access Management** nâng cao
- **Governance & Compliance** toàn diện
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Groups/AddGroupMemberCommand.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Groups/AddGroupMemberCommand.cs
new file mode 100644
index 00000000..55c18bb8
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Groups/AddGroupMemberCommand.cs
@@ -0,0 +1,23 @@
+using MediatR;
+
+namespace IamService.API.Application.Commands.Groups;
+
+///
+/// EN: Command to add a member to a group.
+/// VI: Command để thêm thành viên vào nhóm.
+///
+public record AddGroupMemberCommand(
+ Guid GroupId,
+ Guid UserId,
+ int? RoleId = null,
+ Guid? AddedByUserId = null) : IRequest;
+
+///
+/// EN: Result of AddGroupMemberCommand.
+/// VI: Kết quả của AddGroupMemberCommand.
+///
+public record AddGroupMemberCommandResult(
+ Guid GroupId,
+ Guid UserId,
+ string Role,
+ DateTime JoinedAt);
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Groups/AddGroupMemberCommandHandler.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Groups/AddGroupMemberCommandHandler.cs
new file mode 100644
index 00000000..852346b6
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Groups/AddGroupMemberCommandHandler.cs
@@ -0,0 +1,64 @@
+using MediatR;
+using IamService.Domain.AggregatesModel.GroupAggregate;
+using IamService.Domain.Exceptions;
+
+namespace IamService.API.Application.Commands.Groups;
+
+///
+/// EN: Handler for AddGroupMemberCommand.
+/// VI: Handler cho AddGroupMemberCommand.
+///
+public class AddGroupMemberCommandHandler : IRequestHandler
+{
+ private readonly IGroupRepository _groupRepository;
+ private readonly ILogger _logger;
+
+ public AddGroupMemberCommandHandler(
+ IGroupRepository groupRepository,
+ ILogger logger)
+ {
+ _groupRepository = groupRepository;
+ _logger = logger;
+ }
+
+ public async Task Handle(
+ AddGroupMemberCommand request,
+ CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Adding user {UserId} to group {GroupId}",
+ request.UserId, request.GroupId);
+
+ var group = await _groupRepository.GetByIdWithMembersAsync(request.GroupId, cancellationToken);
+
+ if (group == null)
+ {
+ throw new DomainException($"Group with ID '{request.GroupId}' not found.");
+ }
+
+ // EN: Get role by ID or use default (Member)
+ // VI: Lấy vai trò theo ID hoặc dùng mặc định (Member)
+ GroupRole? role = null;
+ if (request.RoleId.HasValue)
+ {
+ role = GroupRole.FromId(request.RoleId.Value);
+ }
+
+ var member = group.AddMember(request.UserId, role, request.AddedByUserId);
+
+ _groupRepository.Update(group);
+
+ await _groupRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ _logger.LogInformation(
+ "User {UserId} added to group {GroupId} with role {Role}",
+ request.UserId,
+ request.GroupId,
+ member.Role.Name);
+
+ return new AddGroupMemberCommandResult(
+ group.Id,
+ member.UserId,
+ member.Role.Name,
+ member.JoinedAt);
+ }
+}
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Groups/CreateGroupCommand.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Groups/CreateGroupCommand.cs
new file mode 100644
index 00000000..a67a12ae
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Groups/CreateGroupCommand.cs
@@ -0,0 +1,23 @@
+using MediatR;
+
+namespace IamService.API.Application.Commands.Groups;
+
+///
+/// EN: Command to create a new group.
+/// VI: Command để tạo nhóm mới.
+///
+public record CreateGroupCommand(
+ string Name,
+ Guid OrganizationId,
+ string? Description = null) : IRequest;
+
+///
+/// EN: Result of CreateGroupCommand.
+/// VI: Kết quả của CreateGroupCommand.
+///
+public record CreateGroupCommandResult(
+ Guid Id,
+ string Name,
+ string? Description,
+ Guid OrganizationId,
+ DateTime CreatedAt);
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Groups/CreateGroupCommandHandler.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Groups/CreateGroupCommandHandler.cs
new file mode 100644
index 00000000..25408cd3
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Groups/CreateGroupCommandHandler.cs
@@ -0,0 +1,68 @@
+using MediatR;
+using IamService.Domain.AggregatesModel.GroupAggregate;
+using IamService.Domain.AggregatesModel.OrganizationAggregate;
+using IamService.Domain.Exceptions;
+
+namespace IamService.API.Application.Commands.Groups;
+
+///
+/// EN: Handler for CreateGroupCommand.
+/// VI: Handler cho CreateGroupCommand.
+///
+public class CreateGroupCommandHandler : IRequestHandler
+{
+ private readonly IGroupRepository _groupRepository;
+ private readonly IOrganizationRepository _organizationRepository;
+ private readonly ILogger _logger;
+
+ public CreateGroupCommandHandler(
+ IGroupRepository groupRepository,
+ IOrganizationRepository organizationRepository,
+ ILogger logger)
+ {
+ _groupRepository = groupRepository;
+ _organizationRepository = organizationRepository;
+ _logger = logger;
+ }
+
+ public async Task Handle(
+ CreateGroupCommand request,
+ CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Creating group: {Name} in organization: {OrganizationId}",
+ request.Name, request.OrganizationId);
+
+ // EN: Validate organization exists
+ // VI: Kiểm tra tổ chức tồn tại
+ var organization = await _organizationRepository.GetByIdAsync(
+ request.OrganizationId, cancellationToken);
+
+ if (organization == null)
+ {
+ throw new DomainException($"Organization with ID '{request.OrganizationId}' not found.");
+ }
+
+ // EN: Create new group (note: organizationId, name, description)
+ // VI: Tạo nhóm mới (lưu ý: organizationId, name, description)
+ var group = Group.Create(
+ request.OrganizationId,
+ request.Name,
+ request.Description);
+
+ _groupRepository.Add(group);
+
+ await _groupRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ _logger.LogInformation(
+ "Group {Name} created successfully with ID {Id}",
+ group.Name,
+ group.Id);
+
+ return new CreateGroupCommandResult(
+ group.Id,
+ group.Name,
+ group.Description,
+ group.OrganizationId,
+ group.CreatedAt);
+ }
+}
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Groups/DeleteGroupCommand.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Groups/DeleteGroupCommand.cs
new file mode 100644
index 00000000..05e677de
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Groups/DeleteGroupCommand.cs
@@ -0,0 +1,9 @@
+using MediatR;
+
+namespace IamService.API.Application.Commands.Groups;
+
+///
+/// EN: Command to delete a group.
+/// VI: Command để xóa nhóm.
+///
+public record DeleteGroupCommand(Guid Id) : IRequest;
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Groups/DeleteGroupCommandHandler.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Groups/DeleteGroupCommandHandler.cs
new file mode 100644
index 00000000..fb51dd33
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Groups/DeleteGroupCommandHandler.cs
@@ -0,0 +1,45 @@
+using MediatR;
+using IamService.Domain.AggregatesModel.GroupAggregate;
+using IamService.Domain.Exceptions;
+
+namespace IamService.API.Application.Commands.Groups;
+
+///
+/// EN: Handler for DeleteGroupCommand.
+/// VI: Handler cho DeleteGroupCommand.
+///
+public class DeleteGroupCommandHandler : IRequestHandler
+{
+ private readonly IGroupRepository _groupRepository;
+ private readonly ILogger _logger;
+
+ public DeleteGroupCommandHandler(
+ IGroupRepository groupRepository,
+ ILogger logger)
+ {
+ _groupRepository = groupRepository;
+ _logger = logger;
+ }
+
+ public async Task Handle(DeleteGroupCommand request, CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Deleting group: {Id}", request.Id);
+
+ var group = await _groupRepository.GetByIdAsync(request.Id, cancellationToken);
+
+ if (group == null)
+ {
+ throw new DomainException($"Group with ID '{request.Id}' not found.");
+ }
+
+ // EN: Soft delete
+ // VI: Xóa mềm
+ _groupRepository.Delete(group);
+
+ await _groupRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ _logger.LogInformation("Group {Id} deleted successfully", request.Id);
+
+ return true;
+ }
+}
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Groups/RemoveGroupMemberCommand.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Groups/RemoveGroupMemberCommand.cs
new file mode 100644
index 00000000..e33fa37e
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Groups/RemoveGroupMemberCommand.cs
@@ -0,0 +1,11 @@
+using MediatR;
+
+namespace IamService.API.Application.Commands.Groups;
+
+///
+/// EN: Command to remove a member from a group.
+/// VI: Command để xóa thành viên khỏi nhóm.
+///
+public record RemoveGroupMemberCommand(
+ Guid GroupId,
+ Guid UserId) : IRequest;
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Groups/RemoveGroupMemberCommandHandler.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Groups/RemoveGroupMemberCommandHandler.cs
new file mode 100644
index 00000000..67332510
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Groups/RemoveGroupMemberCommandHandler.cs
@@ -0,0 +1,46 @@
+using MediatR;
+using IamService.Domain.AggregatesModel.GroupAggregate;
+using IamService.Domain.Exceptions;
+
+namespace IamService.API.Application.Commands.Groups;
+
+///
+/// EN: Handler for RemoveGroupMemberCommand.
+/// VI: Handler cho RemoveGroupMemberCommand.
+///
+public class RemoveGroupMemberCommandHandler : IRequestHandler
+{
+ private readonly IGroupRepository _groupRepository;
+ private readonly ILogger _logger;
+
+ public RemoveGroupMemberCommandHandler(
+ IGroupRepository groupRepository,
+ ILogger logger)
+ {
+ _groupRepository = groupRepository;
+ _logger = logger;
+ }
+
+ public async Task Handle(RemoveGroupMemberCommand request, CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Removing user {UserId} from group {GroupId}",
+ request.UserId, request.GroupId);
+
+ var group = await _groupRepository.GetByIdWithMembersAsync(request.GroupId, cancellationToken);
+
+ if (group == null)
+ {
+ throw new DomainException($"Group with ID '{request.GroupId}' not found.");
+ }
+
+ group.RemoveMember(request.UserId);
+
+ _groupRepository.Update(group);
+
+ await _groupRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ _logger.LogInformation("User {UserId} removed from group {GroupId}", request.UserId, request.GroupId);
+
+ return true;
+ }
+}
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Organizations/ArchiveOrganizationCommand.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Organizations/ArchiveOrganizationCommand.cs
new file mode 100644
index 00000000..69de66f7
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Organizations/ArchiveOrganizationCommand.cs
@@ -0,0 +1,9 @@
+using MediatR;
+
+namespace IamService.API.Application.Commands.Organizations;
+
+///
+/// EN: Command to archive (soft delete) an organization.
+/// VI: Command để lưu trữ (xóa mềm) tổ chức.
+///
+public record ArchiveOrganizationCommand(Guid Id) : IRequest;
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Organizations/ArchiveOrganizationCommandHandler.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Organizations/ArchiveOrganizationCommandHandler.cs
new file mode 100644
index 00000000..c598aaa6
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Organizations/ArchiveOrganizationCommandHandler.cs
@@ -0,0 +1,53 @@
+using MediatR;
+using IamService.Domain.AggregatesModel.OrganizationAggregate;
+using IamService.Domain.Exceptions;
+
+namespace IamService.API.Application.Commands.Organizations;
+
+///
+/// EN: Handler for ArchiveOrganizationCommand.
+/// VI: Handler cho ArchiveOrganizationCommand.
+///
+public class ArchiveOrganizationCommandHandler : IRequestHandler
+{
+ private readonly IOrganizationRepository _organizationRepository;
+ private readonly ILogger _logger;
+
+ public ArchiveOrganizationCommandHandler(
+ IOrganizationRepository organizationRepository,
+ ILogger logger)
+ {
+ _organizationRepository = organizationRepository;
+ _logger = logger;
+ }
+
+ public async Task Handle(ArchiveOrganizationCommand request, CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Archiving organization: {Id}", request.Id);
+
+ var organization = await _organizationRepository.GetByIdAsync(request.Id, cancellationToken);
+
+ if (organization == null)
+ {
+ throw new DomainException($"Organization with ID '{request.Id}' not found.");
+ }
+
+ // EN: Check if organization has children
+ // VI: Kiểm tra tổ chức có con không
+ var children = await _organizationRepository.GetChildrenAsync(request.Id, cancellationToken);
+ if (children.Any())
+ {
+ throw new DomainException("Cannot archive organization with child organizations. Archive children first.");
+ }
+
+ organization.Archive();
+
+ _organizationRepository.Update(organization);
+
+ await _organizationRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ _logger.LogInformation("Organization {Id} archived successfully", request.Id);
+
+ return true;
+ }
+}
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Organizations/CreateOrganizationCommand.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Organizations/CreateOrganizationCommand.cs
new file mode 100644
index 00000000..353eb63f
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Organizations/CreateOrganizationCommand.cs
@@ -0,0 +1,30 @@
+using MediatR;
+
+namespace IamService.API.Application.Commands.Organizations;
+
+///
+/// EN: Command to create a new organization.
+/// VI: Command để tạo tổ chức mới.
+///
+/// Organization name / Tên tổ chức
+/// URL-friendly slug / Slug thân thiện URL
+/// Optional description / Mô tả tùy chọn
+/// Optional parent organization ID / ID tổ chức cha tùy chọn
+public record CreateOrganizationCommand(
+ string Name,
+ string Slug,
+ string? Description = null,
+ Guid? ParentOrganizationId = null) : IRequest;
+
+///
+/// EN: Result of CreateOrganizationCommand.
+/// VI: Kết quả của CreateOrganizationCommand.
+///
+public record CreateOrganizationCommandResult(
+ Guid Id,
+ string Name,
+ string Slug,
+ string? Description,
+ Guid? ParentOrganizationId,
+ string Status,
+ DateTime CreatedAt);
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Organizations/CreateOrganizationCommandHandler.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Organizations/CreateOrganizationCommandHandler.cs
new file mode 100644
index 00000000..ae373f46
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Organizations/CreateOrganizationCommandHandler.cs
@@ -0,0 +1,79 @@
+using MediatR;
+using IamService.Domain.AggregatesModel.OrganizationAggregate;
+using IamService.Domain.Exceptions;
+
+namespace IamService.API.Application.Commands.Organizations;
+
+///
+/// EN: Handler for CreateOrganizationCommand.
+/// VI: Handler cho CreateOrganizationCommand.
+///
+public class CreateOrganizationCommandHandler : IRequestHandler
+{
+ private readonly IOrganizationRepository _organizationRepository;
+ private readonly ILogger _logger;
+
+ public CreateOrganizationCommandHandler(
+ IOrganizationRepository organizationRepository,
+ ILogger logger)
+ {
+ _organizationRepository = organizationRepository;
+ _logger = logger;
+ }
+
+ public async Task Handle(
+ CreateOrganizationCommand request,
+ CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Creating organization: {Name} with slug: {Slug}", request.Name, request.Slug);
+
+ // EN: Check if slug is unique
+ // VI: Kiểm tra slug có duy nhất không
+ var isSlugUnique = await _organizationRepository.IsSlugUniqueAsync(request.Slug, cancellationToken: cancellationToken);
+ if (!isSlugUnique)
+ {
+ _logger.LogWarning("Slug already exists: {Slug}", request.Slug);
+ throw new DomainException($"Organization with slug '{request.Slug}' already exists.");
+ }
+
+ // EN: Validate parent organization if provided
+ // VI: Kiểm tra tổ chức cha nếu được cung cấp
+ if (request.ParentOrganizationId.HasValue)
+ {
+ var parent = await _organizationRepository.GetByIdAsync(
+ request.ParentOrganizationId.Value,
+ cancellationToken);
+
+ if (parent == null)
+ {
+ throw new DomainException($"Parent organization with ID '{request.ParentOrganizationId}' not found.");
+ }
+ }
+
+ // EN: Create new organization
+ // VI: Tạo tổ chức mới
+ var organization = Organization.Create(
+ request.Name,
+ request.Slug,
+ request.Description,
+ request.ParentOrganizationId);
+
+ _organizationRepository.Add(organization);
+
+ await _organizationRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ _logger.LogInformation(
+ "Organization {Name} created successfully with ID {Id}",
+ organization.Name,
+ organization.Id);
+
+ return new CreateOrganizationCommandResult(
+ organization.Id,
+ organization.Name,
+ organization.Slug,
+ organization.Description,
+ organization.ParentOrganizationId,
+ organization.Status.Name,
+ organization.CreatedAt);
+ }
+}
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Organizations/UpdateOrganizationCommand.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Organizations/UpdateOrganizationCommand.cs
new file mode 100644
index 00000000..fbdbd648
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Organizations/UpdateOrganizationCommand.cs
@@ -0,0 +1,22 @@
+using MediatR;
+
+namespace IamService.API.Application.Commands.Organizations;
+
+///
+/// EN: Command to update an organization.
+/// VI: Command để cập nhật tổ chức.
+///
+public record UpdateOrganizationCommand(
+ Guid Id,
+ string Name,
+ string? Description) : IRequest;
+
+///
+/// EN: Result of UpdateOrganizationCommand.
+/// VI: Kết quả của UpdateOrganizationCommand.
+///
+public record UpdateOrganizationCommandResult(
+ Guid Id,
+ string Name,
+ string? Description,
+ DateTime? UpdatedAt);
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Organizations/UpdateOrganizationCommandHandler.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Organizations/UpdateOrganizationCommandHandler.cs
new file mode 100644
index 00000000..dcf9cf4f
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Organizations/UpdateOrganizationCommandHandler.cs
@@ -0,0 +1,51 @@
+using MediatR;
+using IamService.Domain.AggregatesModel.OrganizationAggregate;
+using IamService.Domain.Exceptions;
+
+namespace IamService.API.Application.Commands.Organizations;
+
+///
+/// EN: Handler for UpdateOrganizationCommand.
+/// VI: Handler cho UpdateOrganizationCommand.
+///
+public class UpdateOrganizationCommandHandler : IRequestHandler
+{
+ private readonly IOrganizationRepository _organizationRepository;
+ private readonly ILogger _logger;
+
+ public UpdateOrganizationCommandHandler(
+ IOrganizationRepository organizationRepository,
+ ILogger logger)
+ {
+ _organizationRepository = organizationRepository;
+ _logger = logger;
+ }
+
+ public async Task Handle(
+ UpdateOrganizationCommand request,
+ CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Updating organization: {Id}", request.Id);
+
+ var organization = await _organizationRepository.GetByIdAsync(request.Id, cancellationToken);
+
+ if (organization == null)
+ {
+ throw new DomainException($"Organization with ID '{request.Id}' not found.");
+ }
+
+ organization.UpdateInfo(request.Name, request.Description);
+
+ _organizationRepository.Update(organization);
+
+ await _organizationRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ _logger.LogInformation("Organization {Id} updated successfully", request.Id);
+
+ return new UpdateOrganizationCommandResult(
+ organization.Id,
+ organization.Name,
+ organization.Description,
+ organization.UpdatedAt);
+ }
+}
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/UserProfiles/SetProfileAttributeCommand.cs b/services/iam-service-net/src/IamService.API/Application/Commands/UserProfiles/SetProfileAttributeCommand.cs
new file mode 100644
index 00000000..89c31581
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/UserProfiles/SetProfileAttributeCommand.cs
@@ -0,0 +1,23 @@
+using MediatR;
+
+namespace IamService.API.Application.Commands.UserProfiles;
+
+///
+/// EN: Command to set a profile attribute.
+/// VI: Command để đặt profile attribute.
+///
+public record SetProfileAttributeCommand(
+ Guid UserId,
+ string Key,
+ string Value,
+ string ValueType = "String") : IRequest;
+
+///
+/// EN: Result of SetProfileAttributeCommand.
+/// VI: Kết quả của SetProfileAttributeCommand.
+///
+public record SetProfileAttributeCommandResult(
+ Guid ProfileId,
+ string Key,
+ string Value,
+ string ValueType);
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/UserProfiles/SetProfileAttributeCommandHandler.cs b/services/iam-service-net/src/IamService.API/Application/Commands/UserProfiles/SetProfileAttributeCommandHandler.cs
new file mode 100644
index 00000000..e99d4eee
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/UserProfiles/SetProfileAttributeCommandHandler.cs
@@ -0,0 +1,66 @@
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+using IamService.Domain.AggregatesModel.UserAggregate;
+using IamService.Domain.Exceptions;
+using IamService.Infrastructure;
+
+namespace IamService.API.Application.Commands.UserProfiles;
+
+///
+/// EN: Handler for SetProfileAttributeCommand.
+/// VI: Handler cho SetProfileAttributeCommand.
+///
+public class SetProfileAttributeCommandHandler
+ : IRequestHandler
+{
+ private readonly IamServiceContext _context;
+ private readonly ILogger _logger;
+
+ public SetProfileAttributeCommandHandler(
+ IamServiceContext context,
+ ILogger logger)
+ {
+ _context = context;
+ _logger = logger;
+ }
+
+ public async Task Handle(
+ SetProfileAttributeCommand request,
+ CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Setting attribute {Key} for user: {UserId}", request.Key, request.UserId);
+
+ // EN: Get or create user profile
+ // VI: Lấy hoặc tạo user profile
+ var profile = await _context.UserProfiles
+ .Include(p => p.Attributes)
+ .FirstOrDefaultAsync(p => EF.Property(p, "_userId") == request.UserId, cancellationToken);
+
+ if (profile == null)
+ {
+ profile = UserProfile.Create(request.UserId);
+ _context.UserProfiles.Add(profile);
+ }
+
+ // EN: Set attribute based on type
+ // VI: Đặt attribute dựa trên loại
+ ProfileAttribute attribute = request.ValueType.ToLowerInvariant() switch
+ {
+ "number" => profile.SetAttribute(request.Key, decimal.Parse(request.Value)),
+ "boolean" => profile.SetAttribute(request.Key, bool.Parse(request.Value)),
+ "date" => profile.SetAttribute(request.Key, DateTime.Parse(request.Value)),
+ "json" => profile.SetJsonAttribute(request.Key, request.Value),
+ _ => profile.SetAttribute(request.Key, request.Value) // Default: string
+ };
+
+ await _context.SaveChangesAsync(cancellationToken);
+
+ _logger.LogInformation("Attribute {Key} set for user {UserId}", request.Key, request.UserId);
+
+ return new SetProfileAttributeCommandResult(
+ profile.Id,
+ attribute.Key,
+ attribute.Value,
+ attribute.ValueType?.Name ?? "String");
+ }
+}
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/UserProfiles/UpdateUserProfileCommand.cs b/services/iam-service-net/src/IamService.API/Application/Commands/UserProfiles/UpdateUserProfileCommand.cs
new file mode 100644
index 00000000..da1de133
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/UserProfiles/UpdateUserProfileCommand.cs
@@ -0,0 +1,27 @@
+using MediatR;
+
+namespace IamService.API.Application.Commands.UserProfiles;
+
+///
+/// EN: Command to update user profile.
+/// VI: Command để cập nhật profile user.
+///
+public record UpdateUserProfileCommand(
+ Guid UserId,
+ string? Bio = null,
+ string? Timezone = null,
+ string? Locale = null,
+ string? AvatarUrl = null) : IRequest;
+
+///
+/// EN: Result of UpdateUserProfileCommand.
+/// VI: Kết quả của UpdateUserProfileCommand.
+///
+public record UpdateUserProfileCommandResult(
+ Guid ProfileId,
+ Guid UserId,
+ string? Bio,
+ string? Timezone,
+ string? Locale,
+ string? AvatarUrl,
+ DateTime? UpdatedAt);
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/UserProfiles/UpdateUserProfileCommandHandler.cs b/services/iam-service-net/src/IamService.API/Application/Commands/UserProfiles/UpdateUserProfileCommandHandler.cs
new file mode 100644
index 00000000..96ab2ad7
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/UserProfiles/UpdateUserProfileCommandHandler.cs
@@ -0,0 +1,68 @@
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+using IamService.Domain.AggregatesModel.UserAggregate;
+using IamService.Domain.Exceptions;
+using IamService.Infrastructure;
+
+namespace IamService.API.Application.Commands.UserProfiles;
+
+///
+/// EN: Handler for UpdateUserProfileCommand.
+/// VI: Handler cho UpdateUserProfileCommand.
+///
+public class UpdateUserProfileCommandHandler
+ : IRequestHandler
+{
+ private readonly IamServiceContext _context;
+ private readonly ILogger _logger;
+
+ public UpdateUserProfileCommandHandler(
+ IamServiceContext context,
+ ILogger logger)
+ {
+ _context = context;
+ _logger = logger;
+ }
+
+ public async Task Handle(
+ UpdateUserProfileCommand request,
+ CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Updating profile for user: {UserId}", request.UserId);
+
+ // EN: Get or create user profile
+ // VI: Lấy hoặc tạo user profile
+ var profile = await _context.UserProfiles
+ .FirstOrDefaultAsync(p => EF.Property(p, "_userId") == request.UserId, cancellationToken);
+
+ if (profile == null)
+ {
+ // EN: Create new profile
+ // VI: Tạo profile mới
+ profile = UserProfile.Create(request.UserId);
+ _context.UserProfiles.Add(profile);
+ }
+
+ // EN: Update profile information
+ // VI: Cập nhật thông tin profile
+ profile.UpdateBasicInfo(request.Bio, request.Timezone, request.Locale);
+
+ if (request.AvatarUrl != null)
+ {
+ profile.SetAvatar(request.AvatarUrl);
+ }
+
+ await _context.SaveChangesAsync(cancellationToken);
+
+ _logger.LogInformation("Profile updated for user {UserId}", request.UserId);
+
+ return new UpdateUserProfileCommandResult(
+ profile.Id,
+ profile.UserId,
+ profile.Bio,
+ profile.Timezone,
+ profile.Locale,
+ profile.AvatarUrl,
+ profile.UpdatedAt);
+ }
+}
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Verifications/ConfirmVerificationCommand.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Verifications/ConfirmVerificationCommand.cs
new file mode 100644
index 00000000..60234931
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Verifications/ConfirmVerificationCommand.cs
@@ -0,0 +1,21 @@
+using MediatR;
+
+namespace IamService.API.Application.Commands.Verifications;
+
+///
+/// EN: Command to confirm verification with OTP code.
+/// VI: Command để xác nhận verification với mã OTP.
+///
+public record ConfirmVerificationCommand(
+ Guid VerificationId,
+ string Code) : IRequest;
+
+///
+/// EN: Result of confirm verification command.
+/// VI: Kết quả của command xác nhận verification.
+///
+public record ConfirmVerificationCommandResult(
+ Guid VerificationId,
+ bool IsVerified,
+ string Status,
+ DateTime? VerifiedAt);
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Verifications/ConfirmVerificationCommandHandler.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Verifications/ConfirmVerificationCommandHandler.cs
new file mode 100644
index 00000000..82d55b08
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Verifications/ConfirmVerificationCommandHandler.cs
@@ -0,0 +1,72 @@
+using MediatR;
+using IamService.Domain.AggregatesModel.VerificationAggregate;
+using IamService.Domain.Exceptions;
+
+namespace IamService.API.Application.Commands.Verifications;
+
+///
+/// EN: Handler for ConfirmVerificationCommand.
+/// VI: Handler cho ConfirmVerificationCommand.
+///
+public class ConfirmVerificationCommandHandler
+ : IRequestHandler
+{
+ private readonly IIdentityVerificationRepository _verificationRepository;
+ private readonly ILogger _logger;
+
+ public ConfirmVerificationCommandHandler(
+ IIdentityVerificationRepository verificationRepository,
+ ILogger logger)
+ {
+ _verificationRepository = verificationRepository;
+ _logger = logger;
+ }
+
+ public async Task Handle(
+ ConfirmVerificationCommand request,
+ CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Confirming verification: {VerificationId}", request.VerificationId);
+
+ var verification = await _verificationRepository.GetByIdAsync(request.VerificationId, cancellationToken);
+
+ if (verification == null)
+ {
+ throw new DomainException($"Verification with ID '{request.VerificationId}' not found.");
+ }
+
+ // EN: Verify the code
+ // VI: Xác minh mã
+ var isValid = verification.VerifyCode(request.Code);
+
+ if (!isValid)
+ {
+ _verificationRepository.Update(verification);
+ await _verificationRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ _logger.LogWarning(
+ "Invalid verification code for {VerificationId}. Attempts: {AttemptCount}",
+ request.VerificationId,
+ verification.AttemptCount);
+
+ return new ConfirmVerificationCommandResult(
+ verification.Id,
+ false,
+ verification.Status.Name,
+ null);
+ }
+
+ _verificationRepository.Update(verification);
+ await _verificationRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ _logger.LogInformation(
+ "Verification {VerificationId} confirmed successfully",
+ request.VerificationId);
+
+ return new ConfirmVerificationCommandResult(
+ verification.Id,
+ true,
+ verification.Status.Name,
+ verification.VerifiedAt);
+ }
+}
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Verifications/RequestVerificationCommand.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Verifications/RequestVerificationCommand.cs
new file mode 100644
index 00000000..63837830
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Verifications/RequestVerificationCommand.cs
@@ -0,0 +1,29 @@
+using MediatR;
+
+namespace IamService.API.Application.Commands.Verifications;
+
+///
+/// EN: Command to request phone verification.
+/// VI: Command để yêu cầu xác thực số điện thoại.
+///
+public record RequestPhoneVerificationCommand(
+ Guid UserId,
+ string PhoneNumber) : IRequest;
+
+///
+/// EN: Command to request email verification.
+/// VI: Command để yêu cầu xác thực email.
+///
+public record RequestEmailVerificationCommand(
+ Guid UserId,
+ string Email) : IRequest;
+
+///
+/// EN: Result of verification request command.
+/// VI: Kết quả của command yêu cầu xác thực.
+///
+public record RequestVerificationCommandResult(
+ Guid VerificationId,
+ string VerificationType,
+ DateTime ExpiresAt,
+ string Message);
diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Verifications/RequestVerificationCommandHandler.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Verifications/RequestVerificationCommandHandler.cs
new file mode 100644
index 00000000..b46b4ec2
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Verifications/RequestVerificationCommandHandler.cs
@@ -0,0 +1,130 @@
+using MediatR;
+using IamService.Domain.AggregatesModel.VerificationAggregate;
+using IamService.Domain.Exceptions;
+
+namespace IamService.API.Application.Commands.Verifications;
+
+///
+/// EN: Handler for RequestPhoneVerificationCommand.
+/// VI: Handler cho RequestPhoneVerificationCommand.
+///
+public class RequestPhoneVerificationCommandHandler
+ : IRequestHandler
+{
+ private readonly IIdentityVerificationRepository _verificationRepository;
+ private readonly ILogger _logger;
+
+ public RequestPhoneVerificationCommandHandler(
+ IIdentityVerificationRepository verificationRepository,
+ ILogger logger)
+ {
+ _verificationRepository = verificationRepository;
+ _logger = logger;
+ }
+
+ public async Task Handle(
+ RequestPhoneVerificationCommand request,
+ CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Requesting phone verification for user: {UserId}", request.UserId);
+
+ // EN: Check if there's an active verification
+ // VI: Kiểm tra có verification đang hoạt động không
+ var existingVerification = await _verificationRepository.GetActiveByUserIdAndTypeAsync(
+ request.UserId,
+ VerificationType.Phone,
+ cancellationToken);
+
+ if (existingVerification != null)
+ {
+ throw new DomainException("A phone verification is already pending. Please wait for it to expire or complete it.");
+ }
+
+ // EN: Create new phone verification
+ // VI: Tạo xác thực điện thoại mới
+ var (verification, otp) = IdentityVerification.CreatePhoneVerification(
+ request.UserId,
+ request.PhoneNumber);
+
+ _verificationRepository.Add(verification);
+
+ await _verificationRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ // EN: TODO: Send OTP via SMS service
+ // VI: TODO: Gửi OTP qua SMS service
+ _logger.LogInformation(
+ "Phone verification created for user {UserId}. OTP: {OTP} (in production, send via SMS)",
+ request.UserId,
+ otp); // EN: Remove OTP from logs in production!
+
+ return new RequestVerificationCommandResult(
+ verification.Id,
+ VerificationType.Phone.Name,
+ verification.ExpiresAt,
+ "Verification code sent to your phone number.");
+ }
+}
+
+///
+/// EN: Handler for RequestEmailVerificationCommand.
+/// VI: Handler cho RequestEmailVerificationCommand.
+///
+public class RequestEmailVerificationCommandHandler
+ : IRequestHandler
+{
+ private readonly IIdentityVerificationRepository _verificationRepository;
+ private readonly ILogger _logger;
+
+ public RequestEmailVerificationCommandHandler(
+ IIdentityVerificationRepository verificationRepository,
+ ILogger logger)
+ {
+ _verificationRepository = verificationRepository;
+ _logger = logger;
+ }
+
+ public async Task Handle(
+ RequestEmailVerificationCommand request,
+ CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Requesting email verification for user: {UserId}", request.UserId);
+
+ // EN: Check if there's an active verification
+ // VI: Kiểm tra có verification đang hoạt động không
+ var existingVerification = await _verificationRepository.GetActiveByUserIdAndTypeAsync(
+ request.UserId,
+ VerificationType.Email,
+ cancellationToken);
+
+ if (existingVerification != null)
+ {
+ throw new DomainException("An email verification is already pending. Please wait for it to expire or complete it.");
+ }
+
+ // EN: Create new email verification (using phone verification method with email type)
+ // VI: Tạo xác thực email mới
+ var (verification, otp) = IdentityVerification.CreatePhoneVerification(
+ request.UserId,
+ request.Email);
+
+ // EN: Update type to Email
+ // VI: Cập nhật loại thành Email
+ // Note: In real implementation, we would have a separate factory method for email
+ _verificationRepository.Add(verification);
+
+ await _verificationRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ // EN: TODO: Send OTP via Email service
+ // VI: TODO: Gửi OTP qua Email service
+ _logger.LogInformation(
+ "Email verification created for user {UserId}. OTP: {OTP} (in production, send via Email)",
+ request.UserId,
+ otp);
+
+ return new RequestVerificationCommandResult(
+ verification.Id,
+ VerificationType.Email.Name,
+ verification.ExpiresAt,
+ "Verification code sent to your email address.");
+ }
+}
diff --git a/services/iam-service-net/src/IamService.API/Application/Queries/Groups/GroupQueries.cs b/services/iam-service-net/src/IamService.API/Application/Queries/Groups/GroupQueries.cs
new file mode 100644
index 00000000..0a9bfdb4
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Queries/Groups/GroupQueries.cs
@@ -0,0 +1,45 @@
+using MediatR;
+
+namespace IamService.API.Application.Queries.Groups;
+
+///
+/// EN: Query to get group by ID.
+/// VI: Query để lấy nhóm theo ID.
+///
+public record GetGroupByIdQuery(Guid Id) : IRequest;
+
+///
+/// EN: Query to get groups by organization ID.
+/// VI: Query để lấy nhóm theo ID tổ chức.
+///
+public record GetGroupsByOrganizationQuery(Guid OrganizationId) : IRequest>;
+
+///
+/// EN: Query to get groups by user ID.
+/// VI: Query để lấy nhóm theo ID user.
+///
+public record GetGroupsByUserQuery(Guid UserId) : IRequest>;
+
+///
+/// EN: Group DTO for query results.
+/// VI: DTO nhóm cho kết quả query.
+///
+public record GroupDto(
+ Guid Id,
+ string Name,
+ string? Description,
+ Guid OrganizationId,
+ int MemberCount,
+ DateTime CreatedAt,
+ DateTime? UpdatedAt);
+
+///
+/// EN: Group member DTO.
+/// VI: DTO thành viên nhóm.
+///
+public record GroupMemberDto(
+ Guid Id,
+ Guid UserId,
+ string Role,
+ DateTime JoinedAt,
+ Guid? AddedByUserId);
diff --git a/services/iam-service-net/src/IamService.API/Application/Queries/Groups/GroupQueryHandlers.cs b/services/iam-service-net/src/IamService.API/Application/Queries/Groups/GroupQueryHandlers.cs
new file mode 100644
index 00000000..61ecb5f6
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Queries/Groups/GroupQueryHandlers.cs
@@ -0,0 +1,96 @@
+using MediatR;
+using IamService.Domain.AggregatesModel.GroupAggregate;
+
+namespace IamService.API.Application.Queries.Groups;
+
+///
+/// EN: Handler for GetGroupByIdQuery.
+/// VI: Handler cho GetGroupByIdQuery.
+///
+public class GetGroupByIdQueryHandler : IRequestHandler
+{
+ private readonly IGroupRepository _groupRepository;
+
+ public GetGroupByIdQueryHandler(IGroupRepository groupRepository)
+ {
+ _groupRepository = groupRepository;
+ }
+
+ public async Task Handle(GetGroupByIdQuery request, CancellationToken cancellationToken)
+ {
+ var group = await _groupRepository.GetByIdAsync(request.Id, cancellationToken);
+
+ return group == null ? null : MapToDto(group);
+ }
+
+ private static GroupDto MapToDto(Group g) => new(
+ g.Id,
+ g.Name,
+ g.Description,
+ g.OrganizationId,
+ g.Members.Count,
+ g.CreatedAt,
+ g.UpdatedAt);
+}
+
+///
+/// EN: Handler for GetGroupsByOrganizationQuery.
+/// VI: Handler cho GetGroupsByOrganizationQuery.
+///
+public class GetGroupsByOrganizationQueryHandler : IRequestHandler>
+{
+ private readonly IGroupRepository _groupRepository;
+
+ public GetGroupsByOrganizationQueryHandler(IGroupRepository groupRepository)
+ {
+ _groupRepository = groupRepository;
+ }
+
+ public async Task> Handle(
+ GetGroupsByOrganizationQuery request,
+ CancellationToken cancellationToken)
+ {
+ var groups = await _groupRepository.GetByOrganizationIdAsync(request.OrganizationId, cancellationToken);
+
+ return groups.Select(MapToDto);
+ }
+
+ private static GroupDto MapToDto(Group g) => new(
+ g.Id,
+ g.Name,
+ g.Description,
+ g.OrganizationId,
+ g.Members.Count,
+ g.CreatedAt,
+ g.UpdatedAt);
+}
+
+///
+/// EN: Handler for GetGroupsByUserQuery.
+/// VI: Handler cho GetGroupsByUserQuery.
+///
+public class GetGroupsByUserQueryHandler : IRequestHandler>
+{
+ private readonly IGroupRepository _groupRepository;
+
+ public GetGroupsByUserQueryHandler(IGroupRepository groupRepository)
+ {
+ _groupRepository = groupRepository;
+ }
+
+ public async Task> Handle(GetGroupsByUserQuery request, CancellationToken cancellationToken)
+ {
+ var groups = await _groupRepository.GetByUserIdAsync(request.UserId, cancellationToken);
+
+ return groups.Select(MapToDto);
+ }
+
+ private static GroupDto MapToDto(Group g) => new(
+ g.Id,
+ g.Name,
+ g.Description,
+ g.OrganizationId,
+ g.Members.Count,
+ g.CreatedAt,
+ g.UpdatedAt);
+}
diff --git a/services/iam-service-net/src/IamService.API/Application/Queries/Organizations/OrganizationQueries.cs b/services/iam-service-net/src/IamService.API/Application/Queries/Organizations/OrganizationQueries.cs
new file mode 100644
index 00000000..1109c49f
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Queries/Organizations/OrganizationQueries.cs
@@ -0,0 +1,41 @@
+using MediatR;
+
+namespace IamService.API.Application.Queries.Organizations;
+
+///
+/// EN: Query to get organization by ID.
+/// VI: Query để lấy tổ chức theo ID.
+///
+public record GetOrganizationByIdQuery(Guid Id) : IRequest;
+
+///
+/// EN: Query to get organization by slug.
+/// VI: Query để lấy tổ chức theo slug.
+///
+public record GetOrganizationBySlugQuery(string Slug) : IRequest;
+
+///
+/// EN: Query to get organization hierarchy (parent and all descendants).
+/// VI: Query để lấy phân cấp tổ chức (cha và tất cả con cháu).
+///
+public record GetOrganizationHierarchyQuery(Guid RootId) : IRequest>;
+
+///
+/// EN: Query to get child organizations.
+/// VI: Query để lấy tổ chức con.
+///
+public record GetChildOrganizationsQuery(Guid ParentId) : IRequest>;
+
+///
+/// EN: Organization DTO for query results.
+/// VI: DTO tổ chức cho kết quả query.
+///
+public record OrganizationDto(
+ Guid Id,
+ string Name,
+ string Slug,
+ string? Description,
+ Guid? ParentOrganizationId,
+ string Status,
+ DateTime CreatedAt,
+ DateTime? UpdatedAt);
diff --git a/services/iam-service-net/src/IamService.API/Application/Queries/Organizations/OrganizationQueryHandlers.cs b/services/iam-service-net/src/IamService.API/Application/Queries/Organizations/OrganizationQueryHandlers.cs
new file mode 100644
index 00000000..52e42649
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Queries/Organizations/OrganizationQueryHandlers.cs
@@ -0,0 +1,128 @@
+using MediatR;
+using IamService.Domain.AggregatesModel.OrganizationAggregate;
+
+namespace IamService.API.Application.Queries.Organizations;
+
+///
+/// EN: Handler for GetOrganizationByIdQuery.
+/// VI: Handler cho GetOrganizationByIdQuery.
+///
+public class GetOrganizationByIdQueryHandler : IRequestHandler
+{
+ private readonly IOrganizationRepository _organizationRepository;
+
+ public GetOrganizationByIdQueryHandler(IOrganizationRepository organizationRepository)
+ {
+ _organizationRepository = organizationRepository;
+ }
+
+ public async Task Handle(GetOrganizationByIdQuery request, CancellationToken cancellationToken)
+ {
+ var organization = await _organizationRepository.GetByIdAsync(request.Id, cancellationToken);
+
+ return organization == null ? null : MapToDto(organization);
+ }
+
+ private static OrganizationDto MapToDto(Organization org) => new(
+ org.Id,
+ org.Name,
+ org.Slug,
+ org.Description,
+ org.ParentOrganizationId,
+ org.Status.Name,
+ org.CreatedAt,
+ org.UpdatedAt);
+}
+
+///
+/// EN: Handler for GetOrganizationBySlugQuery.
+/// VI: Handler cho GetOrganizationBySlugQuery.
+///
+public class GetOrganizationBySlugQueryHandler : IRequestHandler
+{
+ private readonly IOrganizationRepository _organizationRepository;
+
+ public GetOrganizationBySlugQueryHandler(IOrganizationRepository organizationRepository)
+ {
+ _organizationRepository = organizationRepository;
+ }
+
+ public async Task Handle(GetOrganizationBySlugQuery request, CancellationToken cancellationToken)
+ {
+ var organization = await _organizationRepository.GetBySlugAsync(request.Slug, cancellationToken);
+
+ return organization == null ? null : MapToDto(organization);
+ }
+
+ private static OrganizationDto MapToDto(Organization org) => new(
+ org.Id,
+ org.Name,
+ org.Slug,
+ org.Description,
+ org.ParentOrganizationId,
+ org.Status.Name,
+ org.CreatedAt,
+ org.UpdatedAt);
+}
+
+///
+/// EN: Handler for GetOrganizationHierarchyQuery.
+/// VI: Handler cho GetOrganizationHierarchyQuery.
+///
+public class GetOrganizationHierarchyQueryHandler : IRequestHandler>
+{
+ private readonly IOrganizationRepository _organizationRepository;
+
+ public GetOrganizationHierarchyQueryHandler(IOrganizationRepository organizationRepository)
+ {
+ _organizationRepository = organizationRepository;
+ }
+
+ public async Task> Handle(GetOrganizationHierarchyQuery request, CancellationToken cancellationToken)
+ {
+ var organizations = await _organizationRepository.GetHierarchyAsync(request.RootId, cancellationToken);
+
+ return organizations.Select(MapToDto);
+ }
+
+ private static OrganizationDto MapToDto(Organization org) => new(
+ org.Id,
+ org.Name,
+ org.Slug,
+ org.Description,
+ org.ParentOrganizationId,
+ org.Status.Name,
+ org.CreatedAt,
+ org.UpdatedAt);
+}
+
+///
+/// EN: Handler for GetChildOrganizationsQuery.
+/// VI: Handler cho GetChildOrganizationsQuery.
+///
+public class GetChildOrganizationsQueryHandler : IRequestHandler>
+{
+ private readonly IOrganizationRepository _organizationRepository;
+
+ public GetChildOrganizationsQueryHandler(IOrganizationRepository organizationRepository)
+ {
+ _organizationRepository = organizationRepository;
+ }
+
+ public async Task> Handle(GetChildOrganizationsQuery request, CancellationToken cancellationToken)
+ {
+ var children = await _organizationRepository.GetChildrenAsync(request.ParentId, cancellationToken);
+
+ return children.Select(MapToDto);
+ }
+
+ private static OrganizationDto MapToDto(Organization org) => new(
+ org.Id,
+ org.Name,
+ org.Slug,
+ org.Description,
+ org.ParentOrganizationId,
+ org.Status.Name,
+ org.CreatedAt,
+ org.UpdatedAt);
+}
diff --git a/services/iam-service-net/src/IamService.API/Application/Queries/UserProfiles/UserProfileQueries.cs b/services/iam-service-net/src/IamService.API/Application/Queries/UserProfiles/UserProfileQueries.cs
new file mode 100644
index 00000000..0ccd757a
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Queries/UserProfiles/UserProfileQueries.cs
@@ -0,0 +1,54 @@
+using MediatR;
+
+namespace IamService.API.Application.Queries.UserProfiles;
+
+///
+/// EN: Query to get user profile by user ID.
+/// VI: Query để lấy profile user theo ID user.
+///
+public record GetUserProfileQuery(Guid UserId) : IRequest;
+
+///
+/// EN: User profile DTO for query results.
+/// VI: DTO profile user cho kết quả query.
+///
+public record UserProfileDto(
+ Guid Id,
+ Guid UserId,
+ string? Bio,
+ string? AvatarUrl,
+ string? Timezone,
+ string? Locale,
+ DateTime? DateOfBirth,
+ PhoneNumberDto? PhoneNumber,
+ AddressDto? Address,
+ IEnumerable Attributes,
+ DateTime CreatedAt,
+ DateTime? UpdatedAt);
+
+///
+/// EN: Phone number DTO.
+/// VI: DTO số điện thoại.
+///
+public record PhoneNumberDto(string CountryCode, string NationalNumber);
+
+///
+/// EN: Address DTO.
+/// VI: DTO địa chỉ.
+///
+public record AddressDto(
+ string? Street,
+ string? Street2,
+ string? City,
+ string? State,
+ string? PostalCode,
+ string? Country);
+
+///
+/// EN: Profile attribute DTO.
+/// VI: DTO profile attribute.
+///
+public record ProfileAttributeDto(
+ string Key,
+ string Value,
+ string ValueType);
diff --git a/services/iam-service-net/src/IamService.API/Application/Queries/UserProfiles/UserProfileQueryHandler.cs b/services/iam-service-net/src/IamService.API/Application/Queries/UserProfiles/UserProfileQueryHandler.cs
new file mode 100644
index 00000000..064508d8
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Queries/UserProfiles/UserProfileQueryHandler.cs
@@ -0,0 +1,62 @@
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+using IamService.Domain.AggregatesModel.UserAggregate;
+using IamService.Infrastructure;
+
+namespace IamService.API.Application.Queries.UserProfiles;
+
+///
+/// EN: Handler for GetUserProfileQuery.
+/// VI: Handler cho GetUserProfileQuery.
+///
+public class GetUserProfileQueryHandler : IRequestHandler
+{
+ private readonly IamServiceContext _context;
+
+ public GetUserProfileQueryHandler(IamServiceContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(GetUserProfileQuery request, CancellationToken cancellationToken)
+ {
+ var profile = await _context.UserProfiles
+ .Include(p => p.Attributes)
+ .FirstOrDefaultAsync(p => EF.Property(p, "_userId") == request.UserId, cancellationToken);
+
+ if (profile == null)
+ return null;
+
+ return MapToDto(profile);
+ }
+
+ private static UserProfileDto MapToDto(UserProfile profile)
+ {
+ return new UserProfileDto(
+ profile.Id,
+ profile.UserId,
+ profile.Bio,
+ profile.AvatarUrl,
+ profile.Timezone,
+ profile.Locale,
+ profile.DateOfBirth,
+ profile.PhoneNumber != null
+ ? new PhoneNumberDto(profile.PhoneNumber.CountryCode, profile.PhoneNumber.NationalNumber)
+ : null,
+ profile.Address != null
+ ? new AddressDto(
+ profile.Address.Street,
+ profile.Address.Street2,
+ profile.Address.City,
+ profile.Address.State,
+ profile.Address.PostalCode,
+ profile.Address.Country)
+ : null,
+ profile.Attributes.Select(a => new ProfileAttributeDto(
+ a.Key,
+ a.Value,
+ a.ValueType?.Name ?? "String")),
+ profile.CreatedAt,
+ profile.UpdatedAt);
+ }
+}
diff --git a/services/iam-service-net/src/IamService.API/Application/Validations/GroupCommandValidators.cs b/services/iam-service-net/src/IamService.API/Application/Validations/GroupCommandValidators.cs
new file mode 100644
index 00000000..7f119e53
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Validations/GroupCommandValidators.cs
@@ -0,0 +1,74 @@
+using FluentValidation;
+using IamService.API.Application.Commands.Groups;
+
+namespace IamService.API.Application.Validations;
+
+///
+/// EN: Validator for CreateGroupCommand.
+/// VI: Validator cho CreateGroupCommand.
+///
+public class CreateGroupCommandValidator : AbstractValidator
+{
+ public CreateGroupCommandValidator()
+ {
+ RuleFor(x => x.Name)
+ .NotEmpty().WithMessage("Group name is required")
+ .MaximumLength(200).WithMessage("Group name cannot exceed 200 characters");
+
+ RuleFor(x => x.OrganizationId)
+ .NotEmpty().WithMessage("Organization ID is required");
+
+ RuleFor(x => x.Description)
+ .MaximumLength(1000).WithMessage("Description cannot exceed 1000 characters")
+ .When(x => x.Description != null);
+ }
+}
+
+///
+/// EN: Validator for DeleteGroupCommand.
+/// VI: Validator cho DeleteGroupCommand.
+///
+public class DeleteGroupCommandValidator : AbstractValidator
+{
+ public DeleteGroupCommandValidator()
+ {
+ RuleFor(x => x.Id)
+ .NotEmpty().WithMessage("Group ID is required");
+ }
+}
+
+///
+/// EN: Validator for AddGroupMemberCommand.
+/// VI: Validator cho AddGroupMemberCommand.
+///
+public class AddGroupMemberCommandValidator : AbstractValidator
+{
+ public AddGroupMemberCommandValidator()
+ {
+ RuleFor(x => x.GroupId)
+ .NotEmpty().WithMessage("Group ID is required");
+
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required");
+
+ RuleFor(x => x.RoleId)
+ .Must(id => id == null || (id >= 1 && id <= 3))
+ .WithMessage("Role ID must be 1 (Member), 2 (Admin), or 3 (Owner)");
+ }
+}
+
+///
+/// EN: Validator for RemoveGroupMemberCommand.
+/// VI: Validator cho RemoveGroupMemberCommand.
+///
+public class RemoveGroupMemberCommandValidator : AbstractValidator
+{
+ public RemoveGroupMemberCommandValidator()
+ {
+ RuleFor(x => x.GroupId)
+ .NotEmpty().WithMessage("Group ID is required");
+
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required");
+ }
+}
diff --git a/services/iam-service-net/src/IamService.API/Application/Validations/OrganizationCommandValidators.cs b/services/iam-service-net/src/IamService.API/Application/Validations/OrganizationCommandValidators.cs
new file mode 100644
index 00000000..4fa6db32
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Validations/OrganizationCommandValidators.cs
@@ -0,0 +1,68 @@
+using FluentValidation;
+using IamService.API.Application.Commands.Organizations;
+
+namespace IamService.API.Application.Validations;
+
+///
+/// EN: Validator for CreateOrganizationCommand.
+/// VI: Validator cho CreateOrganizationCommand.
+///
+public class CreateOrganizationCommandValidator : AbstractValidator
+{
+ public CreateOrganizationCommandValidator()
+ {
+ RuleFor(x => x.Name)
+ .NotEmpty().WithMessage("Organization name is required")
+ .MaximumLength(200).WithMessage("Organization name cannot exceed 200 characters")
+ .Matches(@"^[\w\s\-\.]+$").WithMessage("Organization name contains invalid characters");
+
+ RuleFor(x => x.Slug)
+ .NotEmpty().WithMessage("Slug is required")
+ .MaximumLength(100).WithMessage("Slug cannot exceed 100 characters")
+ .Matches(@"^[a-z0-9\-]+$").WithMessage("Slug must be lowercase letters, numbers, and hyphens only")
+ .Must(slug => !slug.StartsWith("-") && !slug.EndsWith("-"))
+ .WithMessage("Slug cannot start or end with a hyphen");
+
+ RuleFor(x => x.Description)
+ .MaximumLength(2000).WithMessage("Description cannot exceed 2000 characters")
+ .When(x => x.Description != null);
+
+ RuleFor(x => x.ParentOrganizationId)
+ .Must(id => id == null || id != Guid.Empty)
+ .WithMessage("Parent organization ID cannot be empty GUID");
+ }
+}
+
+///
+/// EN: Validator for UpdateOrganizationCommand.
+/// VI: Validator cho UpdateOrganizationCommand.
+///
+public class UpdateOrganizationCommandValidator : AbstractValidator
+{
+ public UpdateOrganizationCommandValidator()
+ {
+ RuleFor(x => x.Id)
+ .NotEmpty().WithMessage("Organization ID is required");
+
+ RuleFor(x => x.Name)
+ .NotEmpty().WithMessage("Organization name is required")
+ .MaximumLength(200).WithMessage("Organization name cannot exceed 200 characters");
+
+ RuleFor(x => x.Description)
+ .MaximumLength(2000).WithMessage("Description cannot exceed 2000 characters")
+ .When(x => x.Description != null);
+ }
+}
+
+///
+/// EN: Validator for ArchiveOrganizationCommand.
+/// VI: Validator cho ArchiveOrganizationCommand.
+///
+public class ArchiveOrganizationCommandValidator : AbstractValidator
+{
+ public ArchiveOrganizationCommandValidator()
+ {
+ RuleFor(x => x.Id)
+ .NotEmpty().WithMessage("Organization ID is required");
+ }
+}
diff --git a/services/iam-service-net/src/IamService.API/Application/Validations/UserProfileCommandValidators.cs b/services/iam-service-net/src/IamService.API/Application/Validations/UserProfileCommandValidators.cs
new file mode 100644
index 00000000..33bd9bea
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Validations/UserProfileCommandValidators.cs
@@ -0,0 +1,66 @@
+using FluentValidation;
+using IamService.API.Application.Commands.UserProfiles;
+
+namespace IamService.API.Application.Validations;
+
+///
+/// EN: Validator for UpdateUserProfileCommand.
+/// VI: Validator cho UpdateUserProfileCommand.
+///
+public class UpdateUserProfileCommandValidator : AbstractValidator
+{
+ public UpdateUserProfileCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required");
+
+ RuleFor(x => x.Bio)
+ .MaximumLength(2000).WithMessage("Bio cannot exceed 2000 characters")
+ .When(x => x.Bio != null);
+
+ RuleFor(x => x.Timezone)
+ .MaximumLength(50).WithMessage("Timezone cannot exceed 50 characters")
+ .Matches(@"^[A-Za-z_/]+$").WithMessage("Invalid timezone format (use IANA format like 'Asia/Ho_Chi_Minh')")
+ .When(x => x.Timezone != null);
+
+ RuleFor(x => x.Locale)
+ .MaximumLength(10).WithMessage("Locale cannot exceed 10 characters")
+ .Matches(@"^[a-z]{2}(-[A-Z]{2})?$").WithMessage("Invalid locale format (use format like 'en-US' or 'vi-VN')")
+ .When(x => x.Locale != null);
+
+ RuleFor(x => x.AvatarUrl)
+ .MaximumLength(500).WithMessage("Avatar URL cannot exceed 500 characters")
+ .Must(url => url == null || Uri.TryCreate(url, UriKind.Absolute, out _))
+ .WithMessage("Avatar URL must be a valid URL")
+ .When(x => x.AvatarUrl != null);
+ }
+}
+
+///
+/// EN: Validator for SetProfileAttributeCommand.
+/// VI: Validator cho SetProfileAttributeCommand.
+///
+public class SetProfileAttributeCommandValidator : AbstractValidator
+{
+ private static readonly string[] ValidValueTypes = ["String", "Number", "Boolean", "Date", "Json"];
+
+ public SetProfileAttributeCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required");
+
+ RuleFor(x => x.Key)
+ .NotEmpty().WithMessage("Attribute key is required")
+ .MaximumLength(100).WithMessage("Key cannot exceed 100 characters")
+ .Matches(@"^[a-z][a-z0-9_\.]*$").WithMessage("Key must start with lowercase letter and contain only lowercase letters, numbers, underscores, and dots");
+
+ RuleFor(x => x.Value)
+ .NotEmpty().WithMessage("Attribute value is required")
+ .MaximumLength(4000).WithMessage("Value cannot exceed 4000 characters");
+
+ RuleFor(x => x.ValueType)
+ .NotEmpty().WithMessage("Value type is required")
+ .Must(t => ValidValueTypes.Contains(t))
+ .WithMessage($"Value type must be one of: {string.Join(", ", ValidValueTypes)}");
+ }
+}
diff --git a/services/iam-service-net/src/IamService.API/Application/Validations/VerificationCommandValidators.cs b/services/iam-service-net/src/IamService.API/Application/Validations/VerificationCommandValidators.cs
new file mode 100644
index 00000000..fe938eb2
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Validations/VerificationCommandValidators.cs
@@ -0,0 +1,56 @@
+using FluentValidation;
+using IamService.API.Application.Commands.Verifications;
+
+namespace IamService.API.Application.Validations;
+
+///
+/// EN: Validator for RequestPhoneVerificationCommand.
+/// VI: Validator cho RequestPhoneVerificationCommand.
+///
+public class RequestPhoneVerificationCommandValidator : AbstractValidator
+{
+ public RequestPhoneVerificationCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required");
+
+ RuleFor(x => x.PhoneNumber)
+ .NotEmpty().WithMessage("Phone number is required")
+ .Matches(@"^\+?[1-9]\d{6,14}$").WithMessage("Invalid phone number format. Use international format like +84912345678");
+ }
+}
+
+///
+/// EN: Validator for RequestEmailVerificationCommand.
+/// VI: Validator cho RequestEmailVerificationCommand.
+///
+public class RequestEmailVerificationCommandValidator : AbstractValidator
+{
+ public RequestEmailVerificationCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .NotEmpty().WithMessage("User ID is required");
+
+ RuleFor(x => x.Email)
+ .NotEmpty().WithMessage("Email is required")
+ .EmailAddress().WithMessage("Invalid email format");
+ }
+}
+
+///
+/// EN: Validator for ConfirmVerificationCommand.
+/// VI: Validator cho ConfirmVerificationCommand.
+///
+public class ConfirmVerificationCommandValidator : AbstractValidator
+{
+ public ConfirmVerificationCommandValidator()
+ {
+ RuleFor(x => x.VerificationId)
+ .NotEmpty().WithMessage("Verification ID is required");
+
+ RuleFor(x => x.Code)
+ .NotEmpty().WithMessage("Verification code is required")
+ .Length(6).WithMessage("Verification code must be 6 digits")
+ .Matches(@"^\d{6}$").WithMessage("Verification code must contain only digits");
+ }
+}
diff --git a/services/iam-service-net/src/IamService.API/Controllers/GroupsController.cs b/services/iam-service-net/src/IamService.API/Controllers/GroupsController.cs
new file mode 100644
index 00000000..602b82eb
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Controllers/GroupsController.cs
@@ -0,0 +1,238 @@
+using Asp.Versioning;
+using MediatR;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Swashbuckle.AspNetCore.Annotations;
+using IamService.API.Application.Common;
+using IamService.API.Application.Commands.Groups;
+using IamService.API.Application.Queries.Groups;
+
+namespace IamService.API.Controllers;
+
+///
+/// EN: Groups management controller.
+/// VI: Controller quản lý nhóm.
+///
+[ApiController]
+[ApiVersion("1.0")]
+[Route("api/v{version:apiVersion}/groups")]
+[Authorize(AuthenticationSchemes = "Bearer")]
+[SwaggerTag("Group management endpoints - requires authentication")]
+public class GroupsController : ControllerBase
+{
+ private readonly IMediator _mediator;
+ private readonly ILogger _logger;
+
+ public GroupsController(
+ IMediator mediator,
+ ILogger logger)
+ {
+ _mediator = mediator;
+ _logger = logger;
+ }
+
+ ///
+ /// EN: Get group by ID.
+ /// VI: Lấy nhóm theo ID.
+ ///
+ [HttpGet("{id:guid}")]
+ [SwaggerOperation(Summary = "Get group by ID", OperationId = "GetGroupById")]
+ [SwaggerResponse(StatusCodes.Status200OK, "Group found", typeof(ApiResponse))]
+ [SwaggerResponse(StatusCodes.Status404NotFound, "Group not found")]
+ public async Task GetGroupById(
+ [FromRoute] Guid id,
+ CancellationToken cancellationToken = default)
+ {
+ var result = await _mediator.Send(new GetGroupByIdQuery(id), cancellationToken);
+
+ if (result == null)
+ return NotFound(ApiResponse.Fail("GROUP_NOT_FOUND", $"Group with ID {id} not found."));
+
+ return Ok(ApiResponse.Ok(MapToResponse(result)));
+ }
+
+ ///
+ /// EN: Get groups by organization.
+ /// VI: Lấy nhóm theo tổ chức.
+ ///
+ [HttpGet]
+ [SwaggerOperation(Summary = "Get groups by organization", OperationId = "GetGroupsByOrganization")]
+ [SwaggerResponse(StatusCodes.Status200OK, "Groups returned", typeof(ApiResponse>))]
+ public async Task GetGroupsByOrganization(
+ [FromQuery] Guid organizationId,
+ CancellationToken cancellationToken = default)
+ {
+ var result = await _mediator.Send(new GetGroupsByOrganizationQuery(organizationId), cancellationToken);
+ return Ok(ApiResponse>.Ok(result.Select(MapToResponse)));
+ }
+
+ ///
+ /// EN: Create a new group.
+ /// VI: Tạo nhóm mới.
+ ///
+ [HttpPost]
+ [SwaggerOperation(Summary = "Create group", OperationId = "CreateGroup")]
+ [SwaggerResponse(StatusCodes.Status201Created, "Group created", typeof(ApiResponse))]
+ [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid request")]
+ public async Task CreateGroup(
+ [FromBody] CreateGroupRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var command = new CreateGroupCommand(request.Name, request.OrganizationId, request.Description);
+ var result = await _mediator.Send(command, cancellationToken);
+
+ var response = new GroupResponse
+ {
+ Id = result.Id,
+ Name = result.Name,
+ Description = result.Description,
+ OrganizationId = result.OrganizationId,
+ MemberCount = 0,
+ CreatedAt = result.CreatedAt
+ };
+
+ return CreatedAtAction(nameof(GetGroupById), new { id = result.Id }, ApiResponse.Ok(response));
+ }
+ catch (Exception ex) when (ex.Message.Contains("not found"))
+ {
+ return BadRequest(ApiResponse.Fail("ORG_NOT_FOUND", ex.Message));
+ }
+ }
+
+ ///
+ /// EN: Delete (soft) group.
+ /// VI: Xóa (mềm) nhóm.
+ ///
+ [HttpDelete("{id:guid}")]
+ [SwaggerOperation(Summary = "Delete group", OperationId = "DeleteGroup")]
+ [SwaggerResponse(StatusCodes.Status200OK, "Group deleted")]
+ [SwaggerResponse(StatusCodes.Status404NotFound, "Group not found")]
+ public async Task DeleteGroup(
+ [FromRoute] Guid id,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ await _mediator.Send(new DeleteGroupCommand(id), cancellationToken);
+ return Ok(ApiResponse