feat(api): Enhance authentication and user management endpoints
- Updated API documentation to include new user management features such as password change and logout functionalities. - Added detailed descriptions and examples for OAuth2 token endpoint, supporting password, refresh token, and client credentials grants. - Introduced new endpoints for user management, including retrieving, updating, and deleting users. - Enhanced Swagger annotations for better clarity and usability of the API documentation. - Implemented response models for password change and logout operations to standardize API responses.
This commit is contained in:
@@ -55,27 +55,42 @@ dotnet run --project src/IamService.API
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Authentication
|
||||
### Authentication (`/api/v1/auth`)
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| POST | `/api/v1/auth/register` | Register new user |
|
||||
| POST | `/connect/token` | OAuth2 token endpoint |
|
||||
| Method | Endpoint | Description | Auth |
|
||||
|--------|----------|-------------|------|
|
||||
| `POST` | `/api/v1/auth/register` | Register new user | ❌ |
|
||||
| `POST` | `/connect/token` | OAuth2 token endpoint (login, refresh) | ❌ |
|
||||
| `POST` | `/api/v1/auth/change-password` | Change password | ✅ |
|
||||
| `POST` | `/api/v1/auth/logout` | Logout (revoke tokens) | ✅ |
|
||||
|
||||
### Token Request (Password Grant)
|
||||
### User Management (`/api/v1/users`)
|
||||
|
||||
| Method | Endpoint | Description | Auth |
|
||||
|--------|----------|-------------|------|
|
||||
| `GET` | `/api/v1/users` | List users (paginated) | ✅ |
|
||||
| `GET` | `/api/v1/users/me` | Get current user | ✅ |
|
||||
| `GET` | `/api/v1/users/{id}` | Get user by ID | ✅ |
|
||||
| `PUT` | `/api/v1/users/{id}` | Update user | ✅ |
|
||||
| `DELETE` | `/api/v1/users/{id}` | Delete user (soft delete) | ✅ |
|
||||
|
||||
### Token Request Examples
|
||||
|
||||
**Password Grant (Login):**
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5001/connect/token \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "grant_type=password&username=user@example.com&password=Password123!"
|
||||
-d "grant_type=password&username=user@example.com&password=Password123!&scope=openid profile email roles api"
|
||||
```
|
||||
|
||||
### Users (Protected)
|
||||
**Refresh Token:**
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/v1/users` | List users (paginated) |
|
||||
| GET | `/api/v1/users/me` | Get current user |
|
||||
```bash
|
||||
curl -X POST http://localhost:5001/connect/token \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "grant_type=refresh_token&refresh_token=YOUR_REFRESH_TOKEN"
|
||||
```
|
||||
|
||||
### Health Checks
|
||||
|
||||
@@ -85,6 +100,12 @@ curl -X POST http://localhost:5001/connect/token \
|
||||
| `/health/live` | Liveness probe |
|
||||
| `/health/ready` | Readiness probe |
|
||||
|
||||
## Swagger UI
|
||||
|
||||
After running the service, access Swagger UI at:
|
||||
- **Local**: http://localhost:5001/swagger
|
||||
- **Docker**: http://localhost/api/v1/iam/swagger
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
@@ -115,6 +136,7 @@ iam-service-net/
|
||||
|----------|-------------|---------|
|
||||
| `ASPNETCORE_ENVIRONMENT` | Environment | Development |
|
||||
| `DATABASE_URL` | PostgreSQL connection | - |
|
||||
| `JWT_SECRET` | JWT signing secret (32+ chars) | - |
|
||||
| `REDIS_URL` | Redis connection | - |
|
||||
|
||||
### Password Policy
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
# Template Microservice .NET 10
|
||||
# IAM Service .NET 10
|
||||
|
||||
> Template microservice .NET 10 cấp doanh nghiệp theo các pattern DDD, CQRS và Clean Architecture.
|
||||
> **Service IAM (Identity and Access Management) .NET 10 với OAuth2/OIDC sử dụng OpenIddict.**
|
||||
|
||||
## Tổng Quan
|
||||
|
||||
Template này cung cấp cấu trúc sẵn sàng production cho microservices .NET dựa trên kiến trúc tham chiếu eShopOnContainers với:
|
||||
IAM Service cung cấp các chức năng quản lý danh tính và truy cập:
|
||||
|
||||
- **Domain-Driven Design (DDD)** - Aggregates, Entities, Value Objects, Domain Events
|
||||
- **CQRS Pattern** - Tách biệt Commands (ghi) và Queries (đọc) với MediatR
|
||||
- **Clean Architecture** - Phân tầng Domain, Infrastructure, API
|
||||
- **EF Core 10** - PostgreSQL với connection resilience
|
||||
- **FluentValidation** - Validation request
|
||||
- **API Versioning** - Versioning theo URL segment
|
||||
- **Health Checks** - Probes sẵn sàng cho Kubernetes
|
||||
- **Structured Logging** - Serilog với console và Seq
|
||||
- **OAuth2/OIDC** - Authentication với OpenIddict
|
||||
- **User Management** - CRUD operations cho users
|
||||
- **Password Management** - Đổi mật khẩu
|
||||
- **Token Management** - Issue, refresh, revoke tokens
|
||||
- **CQRS Pattern** - MediatR cho Commands/Queries
|
||||
- **Clean Architecture** - Domain, Infrastructure, API layers
|
||||
|
||||
## Yêu Cầu
|
||||
|
||||
@@ -21,112 +19,48 @@ Template này cung cấp cấu trúc sẵn sàng production cho microservices .N
|
||||
|---------|-----------|
|
||||
| .NET SDK | 10.0.101+ |
|
||||
| Docker | 24.0+ |
|
||||
| PostgreSQL | 15+ (hoặc dùng Docker) |
|
||||
|
||||
```bash
|
||||
# Kiểm tra phiên bản .NET
|
||||
dotnet --version
|
||||
# Kết quả nên là: 10.0.xxx
|
||||
```
|
||||
| PostgreSQL | 15+ |
|
||||
|
||||
## Bắt Đầu Nhanh
|
||||
|
||||
### 1. Tạo Service Mới
|
||||
### Chạy với Docker
|
||||
|
||||
```bash
|
||||
# Sao chép template sang service mới
|
||||
cp -r services/_template_dot_net services/your-service-name
|
||||
|
||||
# Di chuyển đến thư mục service
|
||||
cd services/your-service-name
|
||||
|
||||
# Đổi tên tất cả "IamService" thành "YourService"
|
||||
find . -type f -name "*.cs" -exec sed -i '' 's/IamService/YourService/g' {} +
|
||||
find . -type f -name "*.csproj" -exec sed -i '' 's/IamService/YourService/g' {} +
|
||||
```
|
||||
|
||||
### 2. Cấu Hình Môi Trường
|
||||
|
||||
```bash
|
||||
# Sao chép template môi trường
|
||||
cp .env.example .env
|
||||
|
||||
# Chỉnh sửa với cấu hình của bạn
|
||||
nano .env
|
||||
```
|
||||
|
||||
### 3. Chạy với Docker
|
||||
|
||||
```bash
|
||||
# Khởi động tất cả services (API + PostgreSQL + Redis)
|
||||
cd deployments/local
|
||||
docker-compose up -d
|
||||
|
||||
# Xem logs
|
||||
docker-compose logs -f iamservice-api
|
||||
```
|
||||
|
||||
### 4. Chạy Local
|
||||
### Chạy Local
|
||||
|
||||
```bash
|
||||
# Khôi phục dependencies
|
||||
cd services/iam-service-net
|
||||
dotnet restore
|
||||
|
||||
# Build tất cả projects
|
||||
dotnet build
|
||||
|
||||
# Chạy API
|
||||
dotnet run --project src/IamService.API
|
||||
```
|
||||
|
||||
## Cấu Trúc Dự Án
|
||||
## API Endpoints
|
||||
|
||||
```
|
||||
_template_dot_net/
|
||||
├── src/
|
||||
│ ├── IamService.API/ # Lớp Presentation (Controllers, CQRS)
|
||||
│ │ ├── Controllers/ # Các API endpoints
|
||||
│ │ ├── Application/ # Triển khai CQRS
|
||||
│ │ │ ├── Commands/ # Thao tác ghi (MediatR)
|
||||
│ │ │ ├── Queries/ # Thao tác đọc
|
||||
│ │ │ ├── Behaviors/ # MediatR pipeline behaviors
|
||||
│ │ │ └── Validations/ # FluentValidation validators
|
||||
│ │ ├── Middleware/ # Custom middleware
|
||||
│ │ └── Program.cs # Điểm vào ứng dụng
|
||||
│ │
|
||||
│ ├── IamService.Domain/ # Lớp Domain (Business logic thuần túy)
|
||||
│ │ ├── AggregatesModel/ # Aggregate roots và entities
|
||||
│ │ ├── Events/ # Domain events
|
||||
│ │ ├── Exceptions/ # Domain exceptions
|
||||
│ │ └── SeedWork/ # Base classes (Entity, ValueObject, etc.)
|
||||
│ │
|
||||
│ └── IamService.Infrastructure/ # Lớp Infrastructure (Truy cập dữ liệu)
|
||||
│ ├── EntityConfigurations/ # Cấu hình EF Core Fluent API
|
||||
│ ├── Repositories/ # Triển khai repositories
|
||||
│ ├── Idempotency/ # Xử lý idempotency request
|
||||
│ └── IamServiceContext.cs # DbContext với Unit of Work
|
||||
│
|
||||
├── tests/
|
||||
│ ├── IamService.UnitTests/ # Unit tests (Domain, Application)
|
||||
│ └── IamService.FunctionalTests/ # Integration tests (API endpoints)
|
||||
│
|
||||
├── Dockerfile # Multi-stage Docker build
|
||||
├── docker-compose.yml # Thiết lập phát triển local
|
||||
├── global.json # Pin phiên bản .NET SDK
|
||||
└── Directory.Build.props # Thuộc tính MSBuild chung
|
||||
```
|
||||
### Authentication (`/api/v1/auth`)
|
||||
|
||||
## Các Endpoint API
|
||||
| Method | Endpoint | Mô Tả | Auth |
|
||||
|--------|----------|-------|------|
|
||||
| `POST` | `/api/v1/auth/register` | Đăng ký user mới | ❌ |
|
||||
| `POST` | `/connect/token` | OAuth2 Token (login, refresh) | ❌ |
|
||||
| `POST` | `/api/v1/auth/change-password` | Đổi mật khẩu | ✅ |
|
||||
| `POST` | `/api/v1/auth/logout` | Đăng xuất (revoke tokens) | ✅ |
|
||||
|
||||
| Method | Endpoint | Mô Tả |
|
||||
|--------|----------|-------|
|
||||
| `GET` | `/api/v1/samples` | Lấy tất cả samples |
|
||||
| `GET` | `/api/v1/samples/{id}` | Lấy sample theo ID |
|
||||
| `POST` | `/api/v1/samples` | Tạo sample mới |
|
||||
| `PUT` | `/api/v1/samples/{id}` | Cập nhật sample |
|
||||
| `DELETE` | `/api/v1/samples/{id}` | Xóa sample |
|
||||
| `PATCH` | `/api/v1/samples/{id}/status` | Thay đổi trạng thái |
|
||||
### User Management (`/api/v1/users`)
|
||||
|
||||
### Health Endpoints
|
||||
| Method | Endpoint | Mô Tả | Auth |
|
||||
|--------|----------|-------|------|
|
||||
| `GET` | `/api/v1/users` | Lấy danh sách users (phân trang) | ✅ |
|
||||
| `GET` | `/api/v1/users/me` | Lấy thông tin user hiện tại | ✅ |
|
||||
| `GET` | `/api/v1/users/{id}` | Lấy user theo ID | ✅ |
|
||||
| `PUT` | `/api/v1/users/{id}` | Cập nhật thông tin user | ✅ |
|
||||
| `DELETE` | `/api/v1/users/{id}` | Xóa user (soft delete) | ✅ |
|
||||
|
||||
### Health Checks
|
||||
|
||||
| Endpoint | Mục Đích |
|
||||
|----------|----------|
|
||||
@@ -134,97 +68,44 @@ _template_dot_net/
|
||||
| `/health/live` | Kiểm tra sống |
|
||||
| `/health/ready` | Kiểm tra sẵn sàng |
|
||||
|
||||
## Pattern CQRS
|
||||
## OAuth2 Token Endpoint
|
||||
|
||||
### Commands (Thao Tác Ghi)
|
||||
|
||||
```csharp
|
||||
// Định nghĩa command
|
||||
public record CreateSampleCommand(string Name, string? Description)
|
||||
: IRequest<CreateSampleCommandResult>;
|
||||
|
||||
// Xử lý command
|
||||
public class CreateSampleCommandHandler : IRequestHandler<CreateSampleCommand, CreateSampleCommandResult>
|
||||
{
|
||||
public async Task<CreateSampleCommandResult> Handle(CreateSampleCommand request, CancellationToken ct)
|
||||
{
|
||||
var sample = new Sample(request.Name, request.Description);
|
||||
_repository.Add(sample);
|
||||
await _repository.UnitOfWork.SaveEntitiesAsync(ct);
|
||||
return new CreateSampleCommandResult(sample.Id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Queries (Thao Tác Đọc)
|
||||
|
||||
```csharp
|
||||
// Định nghĩa query
|
||||
public record GetSampleQuery(Guid SampleId) : IRequest<SampleViewModel?>;
|
||||
```
|
||||
|
||||
## Domain Model
|
||||
|
||||
### Aggregate Root
|
||||
|
||||
```csharp
|
||||
public class Sample : Entity, IAggregateRoot
|
||||
{
|
||||
public string Name => _name;
|
||||
public SampleStatus Status => _status;
|
||||
|
||||
public Sample(string name, string? description) {
|
||||
// Validation business logic
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new SampleDomainException("Tên sample không được để trống");
|
||||
|
||||
// Domain event
|
||||
AddDomainEvent(new SampleCreatedDomainEvent(this));
|
||||
}
|
||||
|
||||
public void Activate() {
|
||||
if (_status != SampleStatus.Draft)
|
||||
throw new SampleDomainException("Chỉ sample draft mới có thể kích hoạt");
|
||||
// Chuyển đổi trạng thái
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Kiểm Thử
|
||||
### Password Grant (Login)
|
||||
|
||||
```bash
|
||||
# Chạy tất cả tests
|
||||
dotnet test
|
||||
|
||||
# Chạy với coverage
|
||||
dotnet test /p:CollectCoverage=true /p:CoverageReportFormat=cobertura
|
||||
|
||||
# Chạy project test cụ thể
|
||||
dotnet test tests/IamService.UnitTests
|
||||
curl -X POST http://localhost:5001/connect/token \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "grant_type=password&username=user@example.com&password=YourPassword&scope=openid profile email roles api"
|
||||
```
|
||||
|
||||
### Refresh Token
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5001/connect/token \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "grant_type=refresh_token&refresh_token=YOUR_REFRESH_TOKEN"
|
||||
```
|
||||
|
||||
## Swagger UI
|
||||
|
||||
Sau khi chạy service, truy cập Swagger UI tại:
|
||||
- **Local**: http://localhost:5001/swagger
|
||||
- **Docker**: http://localhost/api/v1/iam/swagger
|
||||
|
||||
## Cấu Hình
|
||||
|
||||
### Biến Môi Trường
|
||||
|
||||
| Biến | Mô Tả | Mặc định |
|
||||
|------|-------|----------|
|
||||
| `ASPNETCORE_ENVIRONMENT` | Tên môi trường | `Development` |
|
||||
| `DATABASE_URL` | Connection string PostgreSQL | - |
|
||||
| `REDIS_URL` | Connection string Redis | - |
|
||||
| `JWT_SECRET` | Secret ký JWT (tối thiểu 32 ký tự) | - |
|
||||
| `ASPNETCORE_ENVIRONMENT` | Môi trường | `Development` |
|
||||
| `DATABASE_URL` | PostgreSQL connection | - |
|
||||
| `JWT_SECRET` | Secret ký JWT (32+ ký tự) | - |
|
||||
|
||||
### appsettings.json
|
||||
## Kiểm Thử
|
||||
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Database=iamservice;Username=postgres;Password=postgres"
|
||||
},
|
||||
"Serilog": {
|
||||
"MinimumLevel": "Information"
|
||||
}
|
||||
}
|
||||
```bash
|
||||
dotnet test
|
||||
```
|
||||
|
||||
## Triển Khai
|
||||
@@ -232,34 +113,12 @@ dotnet test tests/IamService.UnitTests
|
||||
### Docker Build
|
||||
|
||||
```bash
|
||||
# Build Docker image
|
||||
docker build -t iamservice:latest .
|
||||
|
||||
# Chạy container
|
||||
docker run -p 5000:8080 --env-file .env iamservice:latest
|
||||
docker build -t goodgo/iam-service:latest .
|
||||
docker run -p 5001:8080 --env-file .env goodgo/iam-service:latest
|
||||
```
|
||||
|
||||
### Kubernetes
|
||||
|
||||
Xem [ARCHITECTURE.md](./ARCHITECTURE.md) để biết manifests triển khai Kubernetes.
|
||||
|
||||
## Có Gì Mới Trong .NET 10
|
||||
|
||||
- Tính năng ngôn ngữ **C# 14**
|
||||
- Hỗ trợ **Native AOT** được cải thiện
|
||||
- Hiệu suất **async/await** tốt hơn
|
||||
- **JSON serialization** được nâng cao
|
||||
- Cải thiện hiệu suất toàn diện
|
||||
- Hỗ trợ **LTS** 3 năm (đến tháng 11/2028)
|
||||
|
||||
## Tài Nguyên
|
||||
|
||||
- [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers) - Kiến trúc tham chiếu
|
||||
- [Tài liệu .NET 10](https://docs.microsoft.com/en-us/dotnet/core/whats-new/dotnet-10)
|
||||
- [DDD với .NET](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/)
|
||||
- [MediatR](https://github.com/jbogard/MediatR) - Thư viện CQRS
|
||||
- [FluentValidation](https://docs.fluentvalidation.net/) - Thư viện validation
|
||||
|
||||
## Giấy Phép
|
||||
|
||||
Độc quyền - GoodGo Platform
|
||||
- [OpenIddict Documentation](https://documentation.openiddict.com/)
|
||||
- [ASP.NET Core Identity](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity)
|
||||
- [OAuth2 Specification](https://oauth.net/2/)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using MediatR;
|
||||
|
||||
namespace IamService.API.Application.Commands.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to change user password.
|
||||
/// VI: Command để đổi mật khẩu user.
|
||||
/// </summary>
|
||||
/// <param name="UserId">User ID / ID user</param>
|
||||
/// <param name="CurrentPassword">Current password / Mật khẩu hiện tại</param>
|
||||
/// <param name="NewPassword">New password / Mật khẩu mới</param>
|
||||
public record ChangePasswordCommand(
|
||||
Guid UserId,
|
||||
string CurrentPassword,
|
||||
string NewPassword) : IRequest<ChangePasswordCommandResult>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result of ChangePasswordCommand.
|
||||
/// VI: Kết quả của ChangePasswordCommand.
|
||||
/// </summary>
|
||||
/// <param name="Success">Whether the operation was successful / Thao tác có thành công không</param>
|
||||
/// <param name="Message">Result message / Thông điệp kết quả</param>
|
||||
public record ChangePasswordCommandResult(bool Success, string Message);
|
||||
@@ -0,0 +1,52 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using IamService.Domain.AggregatesModel.UserAggregate;
|
||||
using IamService.Domain.Exceptions;
|
||||
|
||||
namespace IamService.API.Application.Commands.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for ChangePasswordCommand.
|
||||
/// VI: Handler cho ChangePasswordCommand.
|
||||
/// </summary>
|
||||
public class ChangePasswordCommandHandler : IRequestHandler<ChangePasswordCommand, ChangePasswordCommandResult>
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly ILogger<ChangePasswordCommandHandler> _logger;
|
||||
|
||||
public ChangePasswordCommandHandler(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ILogger<ChangePasswordCommandHandler> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ChangePasswordCommandResult> Handle(ChangePasswordCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Changing password for user: {UserId}", request.UserId);
|
||||
|
||||
var user = await _userManager.FindByIdAsync(request.UserId.ToString());
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("User not found: {UserId}", request.UserId);
|
||||
throw new DomainException($"User with ID {request.UserId} not found.");
|
||||
}
|
||||
|
||||
// EN: Validate current password and change to new password
|
||||
// VI: Xác thực mật khẩu hiện tại và đổi sang mật khẩu mới
|
||||
var result = await _userManager.ChangePasswordAsync(user, request.CurrentPassword, request.NewPassword);
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
var errors = string.Join(", ", result.Errors.Select(e => e.Description));
|
||||
_logger.LogWarning("Failed to change password for user {UserId}: {Errors}", request.UserId, errors);
|
||||
return new ChangePasswordCommandResult(false, $"Failed to change password: {errors}");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Password changed successfully for user {UserId}", request.UserId);
|
||||
|
||||
return new ChangePasswordCommandResult(true, "Password changed successfully.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using MediatR;
|
||||
|
||||
namespace IamService.API.Application.Commands.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to logout user (revoke tokens).
|
||||
/// VI: Command để logout user (thu hồi tokens).
|
||||
/// </summary>
|
||||
/// <param name="UserId">User ID / ID user</param>
|
||||
public record LogoutCommand(Guid UserId) : IRequest<LogoutCommandResult>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result of LogoutCommand.
|
||||
/// VI: Kết quả của LogoutCommand.
|
||||
/// </summary>
|
||||
/// <param name="Success">Whether the operation was successful / Thao tác có thành công không</param>
|
||||
/// <param name="Message">Result message / Thông điệp kết quả</param>
|
||||
public record LogoutCommandResult(bool Success, string Message);
|
||||
@@ -0,0 +1,64 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using OpenIddict.Abstractions;
|
||||
using IamService.Domain.AggregatesModel.UserAggregate;
|
||||
|
||||
namespace IamService.API.Application.Commands.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for LogoutCommand - revokes all tokens for the user.
|
||||
/// VI: Handler cho LogoutCommand - thu hồi tất cả tokens của user.
|
||||
/// </summary>
|
||||
public class LogoutCommandHandler : IRequestHandler<LogoutCommand, LogoutCommandResult>
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly IOpenIddictTokenManager _tokenManager;
|
||||
private readonly ILogger<LogoutCommandHandler> _logger;
|
||||
|
||||
public LogoutCommandHandler(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
IOpenIddictTokenManager tokenManager,
|
||||
ILogger<LogoutCommandHandler> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_tokenManager = tokenManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<LogoutCommandResult> Handle(LogoutCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Logging out user: {UserId}", request.UserId);
|
||||
|
||||
var user = await _userManager.FindByIdAsync(request.UserId.ToString());
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("User not found during logout: {UserId}", request.UserId);
|
||||
// EN: Still return success - user doesn't exist, so effectively logged out
|
||||
// VI: Vẫn trả về success - user không tồn tại, nên coi như đã logout
|
||||
return new LogoutCommandResult(true, "User logged out.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// EN: Revoke all tokens for this user
|
||||
// VI: Thu hồi tất cả tokens của user này
|
||||
var tokens = _tokenManager.FindBySubjectAsync(request.UserId.ToString(), cancellationToken);
|
||||
|
||||
await foreach (var token in tokens)
|
||||
{
|
||||
await _tokenManager.TryRevokeAsync(token, cancellationToken);
|
||||
}
|
||||
|
||||
_logger.LogInformation("All tokens revoked for user {UserId}", request.UserId);
|
||||
return new LogoutCommandResult(true, "User logged out successfully. All tokens revoked.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to revoke tokens for user {UserId}", request.UserId);
|
||||
// EN: Still consider it a logout even if token revocation fails
|
||||
// VI: Vẫn coi như logout ngay cả khi thu hồi token thất bại
|
||||
return new LogoutCommandResult(true, "User logged out. Token revocation may be pending.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using MediatR;
|
||||
|
||||
namespace IamService.API.Application.Commands.Users;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to delete a user.
|
||||
/// VI: Command để xóa user.
|
||||
/// </summary>
|
||||
/// <param name="UserId">User ID to delete / ID user cần xóa</param>
|
||||
public record DeleteUserCommand(Guid UserId) : IRequest<DeleteUserCommandResult>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result of DeleteUserCommand.
|
||||
/// VI: Kết quả của DeleteUserCommand.
|
||||
/// </summary>
|
||||
/// <param name="Success">Whether deletion was successful / Xóa có thành công không</param>
|
||||
/// <param name="Message">Result message / Thông điệp kết quả</param>
|
||||
public record DeleteUserCommandResult(bool Success, string Message);
|
||||
@@ -0,0 +1,54 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using IamService.Domain.AggregatesModel.UserAggregate;
|
||||
using IamService.Domain.Exceptions;
|
||||
|
||||
namespace IamService.API.Application.Commands.Users;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for DeleteUserCommand.
|
||||
/// VI: Handler cho DeleteUserCommand.
|
||||
/// </summary>
|
||||
public class DeleteUserCommandHandler : IRequestHandler<DeleteUserCommand, DeleteUserCommandResult>
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly ILogger<DeleteUserCommandHandler> _logger;
|
||||
|
||||
public DeleteUserCommandHandler(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ILogger<DeleteUserCommandHandler> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<DeleteUserCommandResult> Handle(DeleteUserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Deleting user: {UserId}", request.UserId);
|
||||
|
||||
var user = await _userManager.FindByIdAsync(request.UserId.ToString());
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("User not found: {UserId}", request.UserId);
|
||||
throw new DomainException($"User with ID {request.UserId} not found.");
|
||||
}
|
||||
|
||||
// EN: Soft delete - deactivate the user instead of hard delete
|
||||
// VI: Soft delete - deactivate user thay vì xóa hoàn toàn
|
||||
user.Disable();
|
||||
|
||||
var updateResult = await _userManager.UpdateAsync(user);
|
||||
|
||||
if (!updateResult.Succeeded)
|
||||
{
|
||||
var errors = string.Join(", ", updateResult.Errors.Select(e => e.Description));
|
||||
_logger.LogError("Failed to delete user {UserId}: {Errors}", request.UserId, errors);
|
||||
throw new DomainException($"Failed to delete user: {errors}");
|
||||
}
|
||||
|
||||
_logger.LogInformation("User {UserId} deleted (deactivated) successfully", request.UserId);
|
||||
|
||||
return new DeleteUserCommandResult(true, $"User {request.UserId} has been deactivated successfully.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using MediatR;
|
||||
|
||||
namespace IamService.API.Application.Commands.Users;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to update user information.
|
||||
/// VI: Command để cập nhật thông tin user.
|
||||
/// </summary>
|
||||
/// <param name="UserId">User ID to update / ID user cần cập nhật</param>
|
||||
/// <param name="FirstName">New first name / Tên mới</param>
|
||||
/// <param name="LastName">New last name / Họ mới</param>
|
||||
public record UpdateUserCommand(
|
||||
Guid UserId,
|
||||
string? FirstName,
|
||||
string? LastName) : IRequest<UpdateUserCommandResult>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result of UpdateUserCommand.
|
||||
/// VI: Kết quả của UpdateUserCommand.
|
||||
/// </summary>
|
||||
public record UpdateUserCommandResult(
|
||||
Guid UserId,
|
||||
string Email,
|
||||
string FirstName,
|
||||
string LastName,
|
||||
string FullName);
|
||||
@@ -0,0 +1,74 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using IamService.Domain.AggregatesModel.UserAggregate;
|
||||
using IamService.Domain.Exceptions;
|
||||
|
||||
namespace IamService.API.Application.Commands.Users;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for UpdateUserCommand.
|
||||
/// VI: Handler cho UpdateUserCommand.
|
||||
/// </summary>
|
||||
public class UpdateUserCommandHandler : IRequestHandler<UpdateUserCommand, UpdateUserCommandResult>
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly ILogger<UpdateUserCommandHandler> _logger;
|
||||
|
||||
public UpdateUserCommandHandler(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ILogger<UpdateUserCommandHandler> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<UpdateUserCommandResult> Handle(UpdateUserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Updating user: {UserId}", request.UserId);
|
||||
|
||||
var user = await _userManager.FindByIdAsync(request.UserId.ToString());
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("User not found: {UserId}", request.UserId);
|
||||
throw new DomainException($"User with ID {request.UserId} not found.");
|
||||
}
|
||||
|
||||
// EN: Update user properties
|
||||
// VI: Cập nhật thuộc tính user
|
||||
if (!string.IsNullOrWhiteSpace(request.FirstName))
|
||||
{
|
||||
user.UpdateProfile(request.FirstName, user.LastName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.LastName))
|
||||
{
|
||||
user.UpdateProfile(user.FirstName, request.LastName);
|
||||
}
|
||||
|
||||
// EN: If both are provided, update together
|
||||
// VI: Nếu cả hai được cung cấp, cập nhật cùng lúc
|
||||
if (!string.IsNullOrWhiteSpace(request.FirstName) && !string.IsNullOrWhiteSpace(request.LastName))
|
||||
{
|
||||
user.UpdateProfile(request.FirstName, request.LastName);
|
||||
}
|
||||
|
||||
var result = await _userManager.UpdateAsync(user);
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
var errors = string.Join(", ", result.Errors.Select(e => e.Description));
|
||||
_logger.LogError("Failed to update user {UserId}: {Errors}", request.UserId, errors);
|
||||
throw new DomainException($"Failed to update user: {errors}");
|
||||
}
|
||||
|
||||
_logger.LogInformation("User {UserId} updated successfully", request.UserId);
|
||||
|
||||
return new UpdateUserCommandResult(
|
||||
user.Id,
|
||||
user.Email ?? string.Empty,
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.FullName);
|
||||
}
|
||||
}
|
||||
@@ -125,3 +125,51 @@ public class PaginationInfo
|
||||
/// <example>10</example>
|
||||
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: OAuth2 Token Response (RFC 6749).
|
||||
/// VI: OAuth2 Token Response (RFC 6749).
|
||||
/// </summary>
|
||||
public class TokenResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// EN: The access token issued by the authorization server.
|
||||
/// VI: Access token được cấp bởi authorization server.
|
||||
/// </summary>
|
||||
/// <example>eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9...</example>
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// EN: The type of the token issued (always "Bearer").
|
||||
/// VI: Loại token được cấp (luôn là "Bearer").
|
||||
/// </summary>
|
||||
/// <example>Bearer</example>
|
||||
public string TokenType { get; set; } = "Bearer";
|
||||
|
||||
/// <summary>
|
||||
/// EN: The lifetime in seconds of the access token.
|
||||
/// VI: Thời gian sống (giây) của access token.
|
||||
/// </summary>
|
||||
/// <example>3600</example>
|
||||
public int ExpiresIn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// EN: The refresh token (if requested).
|
||||
/// VI: Refresh token (nếu được yêu cầu).
|
||||
/// </summary>
|
||||
/// <example>CfDJ8NrU3...</example>
|
||||
public string? RefreshToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// EN: The scope of the access token.
|
||||
/// VI: Scope của access token.
|
||||
/// </summary>
|
||||
/// <example>openid profile email roles api</example>
|
||||
public string? Scope { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// EN: The ID token (if openid scope requested).
|
||||
/// VI: ID token (nếu openid scope được yêu cầu).
|
||||
/// </summary>
|
||||
public string? IdToken { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using MediatR;
|
||||
using IamService.Domain.AggregatesModel.UserAggregate;
|
||||
|
||||
namespace IamService.API.Application.Queries.Users;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query to get user by ID.
|
||||
/// VI: Query để lấy user theo ID.
|
||||
/// </summary>
|
||||
/// <param name="UserId">User ID / ID của user</param>
|
||||
public record GetUserByIdQuery(Guid UserId) : IRequest<GetUserByIdQueryResult?>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query result containing user data.
|
||||
/// VI: Kết quả query chứa dữ liệu user.
|
||||
/// </summary>
|
||||
public record GetUserByIdQueryResult(
|
||||
Guid Id,
|
||||
string Email,
|
||||
string FirstName,
|
||||
string LastName,
|
||||
string FullName,
|
||||
UserStatus Status,
|
||||
DateTime CreatedAt,
|
||||
DateTime? LastLoginAt);
|
||||
@@ -0,0 +1,46 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using IamService.Domain.AggregatesModel.UserAggregate;
|
||||
|
||||
namespace IamService.API.Application.Queries.Users;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetUserByIdQuery.
|
||||
/// VI: Handler cho GetUserByIdQuery.
|
||||
/// </summary>
|
||||
public class GetUserByIdQueryHandler : IRequestHandler<GetUserByIdQuery, GetUserByIdQueryResult?>
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly ILogger<GetUserByIdQueryHandler> _logger;
|
||||
|
||||
public GetUserByIdQueryHandler(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ILogger<GetUserByIdQueryHandler> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<GetUserByIdQueryResult?> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Getting user by ID: {UserId}", request.UserId);
|
||||
|
||||
var user = await _userManager.FindByIdAsync(request.UserId.ToString());
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("User not found: {UserId}", request.UserId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new GetUserByIdQueryResult(
|
||||
user.Id,
|
||||
user.Email ?? string.Empty,
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.FullName,
|
||||
user.Status,
|
||||
user.CreatedAt,
|
||||
user.LastLoginAt);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OpenIddict.Abstractions;
|
||||
using OpenIddict.Server.AspNetCore;
|
||||
using OpenIddict.Validation.AspNetCore;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using IamService.API.Application.Commands.Auth;
|
||||
using IamService.API.Application.Common;
|
||||
@@ -70,12 +71,51 @@ public class AuthController : ControllerBase
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// EN: OAuth2 Token endpoint (handled by OpenIddict).
|
||||
/// VI: OAuth2 Token endpoint (được xử lý bởi OpenIddict).
|
||||
/// EN: OAuth2 Token endpoint - supports password, refresh_token, and client_credentials grants.
|
||||
/// VI: OAuth2 Token endpoint - hỗ trợ password, refresh_token, và client_credentials grants.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// **Password Grant (Login):**
|
||||
/// ```
|
||||
/// POST /connect/token
|
||||
/// Content-Type: application/x-www-form-urlencoded
|
||||
///
|
||||
/// grant_type=password&username=user@example.com&password=YourPassword&scope=openid profile email roles api
|
||||
/// ```
|
||||
///
|
||||
/// **Refresh Token Grant:**
|
||||
/// ```
|
||||
/// POST /connect/token
|
||||
/// Content-Type: application/x-www-form-urlencoded
|
||||
///
|
||||
/// grant_type=refresh_token&refresh_token=YOUR_REFRESH_TOKEN
|
||||
/// ```
|
||||
///
|
||||
/// **Client Credentials Grant:**
|
||||
/// ```
|
||||
/// POST /connect/token
|
||||
/// Content-Type: application/x-www-form-urlencoded
|
||||
///
|
||||
/// grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&scope=api
|
||||
/// ```
|
||||
/// </remarks>
|
||||
/// <returns>OAuth2 token response with access_token, refresh_token, expires_in</returns>
|
||||
[HttpPost("~/connect/token")]
|
||||
[Consumes("application/x-www-form-urlencoded")]
|
||||
[Produces("application/json")]
|
||||
[SwaggerOperation(
|
||||
Summary = "OAuth2 Token Endpoint",
|
||||
Description = "Exchanges credentials for access tokens. Supports password, refresh_token, and client_credentials grant types.",
|
||||
OperationId = "GetToken",
|
||||
Tags = new[] { "Authentication" })]
|
||||
[SwaggerResponse(StatusCodes.Status200OK, "Token issued successfully", typeof(TokenResponse))]
|
||||
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid request (missing parameters)")]
|
||||
[SwaggerResponse(StatusCodes.Status401Unauthorized, "Invalid credentials or token")]
|
||||
[SwaggerResponse(StatusCodes.Status403Forbidden, "Account locked or unsupported grant type")]
|
||||
[ProducesResponseType(typeof(TokenResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> Exchange()
|
||||
{
|
||||
var request = HttpContext.GetOpenIddictServerRequest()
|
||||
@@ -244,6 +284,76 @@ public class AuthController : ControllerBase
|
||||
SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Change user password.
|
||||
/// VI: Đổi mật khẩu user.
|
||||
/// </summary>
|
||||
/// <param name="request">Change password request data</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Result of password change operation</returns>
|
||||
[HttpPost("change-password")]
|
||||
[Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
|
||||
[SwaggerOperation(
|
||||
Summary = "Change password",
|
||||
Description = "Changes the password for the currently authenticated user. Requires current password verification.",
|
||||
OperationId = "ChangePassword")]
|
||||
[SwaggerResponse(StatusCodes.Status200OK, "Password changed successfully", typeof(ChangePasswordResponse))]
|
||||
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid request (current password incorrect)")]
|
||||
[SwaggerResponse(StatusCodes.Status401Unauthorized, "Authentication required")]
|
||||
[ProducesResponseType(typeof(ChangePasswordResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> ChangePassword(
|
||||
[FromBody, SwaggerRequestBody("Password change data", Required = true)] ChangePasswordRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userIdClaim = User.FindFirst("sub")?.Value;
|
||||
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var command = new ChangePasswordCommand(userId, request.CurrentPassword, request.NewPassword);
|
||||
var result = await _mediator.Send(command, cancellationToken);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
return BadRequest(new ChangePasswordResponse { Success = false, Message = result.Message });
|
||||
}
|
||||
|
||||
return Ok(new ChangePasswordResponse { Success = true, Message = result.Message });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Logout user and revoke tokens.
|
||||
/// VI: Logout user và thu hồi tokens.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Result of logout operation</returns>
|
||||
[HttpPost("logout")]
|
||||
[Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
|
||||
[SwaggerOperation(
|
||||
Summary = "Logout",
|
||||
Description = "Logs out the current user and revokes all associated tokens.",
|
||||
OperationId = "Logout")]
|
||||
[SwaggerResponse(StatusCodes.Status200OK, "User logged out successfully", typeof(LogoutResponse))]
|
||||
[SwaggerResponse(StatusCodes.Status401Unauthorized, "Authentication required")]
|
||||
[ProducesResponseType(typeof(LogoutResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> Logout(CancellationToken cancellationToken)
|
||||
{
|
||||
var userIdClaim = User.FindFirst("sub")?.Value;
|
||||
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var command = new LogoutCommand(userId);
|
||||
var result = await _mediator.Send(command, cancellationToken);
|
||||
|
||||
return Ok(new LogoutResponse { Success = result.Success, Message = result.Message });
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetDestinations(Claim claim)
|
||||
{
|
||||
switch (claim.Type)
|
||||
@@ -266,3 +376,62 @@ public class AuthController : ControllerBase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request body for changing password.
|
||||
/// VI: Request body để đổi mật khẩu.
|
||||
/// </summary>
|
||||
public class ChangePasswordRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// EN: Current password.
|
||||
/// VI: Mật khẩu hiện tại.
|
||||
/// </summary>
|
||||
/// <example>OldPassword123!</example>
|
||||
public string CurrentPassword { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// EN: New password.
|
||||
/// VI: Mật khẩu mới.
|
||||
/// </summary>
|
||||
/// <example>NewPassword456!</example>
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Response for change password operation.
|
||||
/// VI: Response cho thao tác đổi mật khẩu.
|
||||
/// </summary>
|
||||
public class ChangePasswordResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// EN: Whether the operation was successful.
|
||||
/// VI: Thao tác có thành công không.
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result message.
|
||||
/// VI: Thông điệp kết quả.
|
||||
/// </summary>
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Response for logout operation.
|
||||
/// VI: Response cho thao tác logout.
|
||||
/// </summary>
|
||||
public class LogoutResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// EN: Whether the operation was successful.
|
||||
/// VI: Thao tác có thành công không.
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result message.
|
||||
/// VI: Thông điệp kết quả.
|
||||
/// </summary>
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using OpenIddict.Validation.AspNetCore;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using IamService.API.Application.Common;
|
||||
using IamService.API.Application.Commands.Users;
|
||||
using IamService.API.Application.Queries.Users;
|
||||
|
||||
namespace IamService.API.Controllers;
|
||||
@@ -79,6 +80,135 @@ public class UsersController : ControllerBase
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get user by ID.
|
||||
/// VI: Lấy user theo ID.
|
||||
/// </summary>
|
||||
/// <param name="id">User ID</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>User information</returns>
|
||||
[HttpGet("{id:guid}")]
|
||||
[SwaggerOperation(
|
||||
Summary = "Get user by ID",
|
||||
Description = "Retrieves a specific user by their unique identifier.",
|
||||
OperationId = "GetUserById")]
|
||||
[SwaggerResponse(StatusCodes.Status200OK, "Successfully retrieved user", typeof(ApiResponse<UserDto>))]
|
||||
[SwaggerResponse(StatusCodes.Status401Unauthorized, "Authentication required")]
|
||||
[SwaggerResponse(StatusCodes.Status404NotFound, "User not found")]
|
||||
[ProducesResponseType(typeof(ApiResponse<UserDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetUserById(
|
||||
[FromRoute, SwaggerParameter("User ID", Required = true)] Guid id,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = new GetUserByIdQuery(id);
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound(ApiResponse<UserDto>.Fail("USER_NOT_FOUND", $"User with ID {id} not found."));
|
||||
}
|
||||
|
||||
return Ok(ApiResponse<UserDto>.Ok(new UserDto
|
||||
{
|
||||
Id = result.Id,
|
||||
Email = result.Email,
|
||||
FirstName = result.FirstName,
|
||||
LastName = result.LastName,
|
||||
FullName = result.FullName,
|
||||
Status = result.Status.Name,
|
||||
CreatedAt = result.CreatedAt,
|
||||
LastLoginAt = result.LastLoginAt
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Update user information.
|
||||
/// VI: Cập nhật thông tin user.
|
||||
/// </summary>
|
||||
/// <param name="id">User ID to update</param>
|
||||
/// <param name="request">Update data</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Updated user information</returns>
|
||||
[HttpPut("{id:guid}")]
|
||||
[SwaggerOperation(
|
||||
Summary = "Update user",
|
||||
Description = "Updates a user's information (first name, last name).",
|
||||
OperationId = "UpdateUser")]
|
||||
[SwaggerResponse(StatusCodes.Status200OK, "User updated successfully", typeof(ApiResponse<UserDto>))]
|
||||
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid request data")]
|
||||
[SwaggerResponse(StatusCodes.Status401Unauthorized, "Authentication required")]
|
||||
[SwaggerResponse(StatusCodes.Status404NotFound, "User not found")]
|
||||
[ProducesResponseType(typeof(ApiResponse<UserDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> UpdateUser(
|
||||
[FromRoute, SwaggerParameter("User ID to update", Required = true)] Guid id,
|
||||
[FromBody, SwaggerRequestBody("User update data", Required = true)] UpdateUserRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var command = new UpdateUserCommand(id, request.FirstName, request.LastName);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _mediator.Send(command, cancellationToken);
|
||||
|
||||
return Ok(ApiResponse<UserDto>.Ok(new UserDto
|
||||
{
|
||||
Id = result.UserId,
|
||||
Email = result.Email,
|
||||
FirstName = result.FirstName,
|
||||
LastName = result.LastName,
|
||||
FullName = result.FullName
|
||||
}));
|
||||
}
|
||||
catch (Exception ex) when (ex.Message.Contains("not found"))
|
||||
{
|
||||
return NotFound(ApiResponse<UserDto>.Fail("USER_NOT_FOUND", ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Delete (deactivate) a user.
|
||||
/// VI: Xóa (vô hiệu hóa) user.
|
||||
/// </summary>
|
||||
/// <param name="id">User ID to delete</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Deletion result</returns>
|
||||
[HttpDelete("{id:guid}")]
|
||||
[SwaggerOperation(
|
||||
Summary = "Delete user",
|
||||
Description = "Soft deletes (deactivates) a user. The user data is retained but marked as inactive.",
|
||||
OperationId = "DeleteUser")]
|
||||
[SwaggerResponse(StatusCodes.Status200OK, "User deleted successfully")]
|
||||
[SwaggerResponse(StatusCodes.Status401Unauthorized, "Authentication required")]
|
||||
[SwaggerResponse(StatusCodes.Status404NotFound, "User not found")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DeleteUserResult>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> DeleteUser(
|
||||
[FromRoute, SwaggerParameter("User ID to delete", Required = true)] Guid id,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var command = new DeleteUserCommand(id);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _mediator.Send(command, cancellationToken);
|
||||
return Ok(ApiResponse<DeleteUserResult>.Ok(new DeleteUserResult
|
||||
{
|
||||
Success = result.Success,
|
||||
Message = result.Message
|
||||
}));
|
||||
}
|
||||
catch (Exception ex) when (ex.Message.Contains("not found"))
|
||||
{
|
||||
return NotFound(ApiResponse<DeleteUserResult>.Fail("USER_NOT_FOUND", ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get current user info.
|
||||
/// VI: Lấy thông tin user hiện tại.
|
||||
@@ -110,3 +240,42 @@ public class UsersController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request body for updating user.
|
||||
/// VI: Request body để cập nhật user.
|
||||
/// </summary>
|
||||
public class UpdateUserRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// EN: New first name.
|
||||
/// VI: Tên mới.
|
||||
/// </summary>
|
||||
/// <example>John</example>
|
||||
public string? FirstName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// EN: New last name.
|
||||
/// VI: Họ mới.
|
||||
/// </summary>
|
||||
/// <example>Doe</example>
|
||||
public string? LastName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result of delete user operation.
|
||||
/// VI: Kết quả của thao tác xóa user.
|
||||
/// </summary>
|
||||
public class DeleteUserResult
|
||||
{
|
||||
/// <summary>
|
||||
/// EN: Whether the operation was successful.
|
||||
/// VI: Thao tác có thành công không.
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result message.
|
||||
/// VI: Thông điệp kết quả.
|
||||
/// </summary>
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user