diff --git a/Controllers/GatewayController.cs b/Controllers/GatewayController.cs new file mode 100644 index 0000000..7c58dcb --- /dev/null +++ b/Controllers/GatewayController.cs @@ -0,0 +1,361 @@ +namespace Fengling.Console.Controllers; + +/// +/// 网关管理控制器 +/// 提供网关服务、路由、集群实例等配置管理功能 +/// +[ApiController] +[Route("api/console/[controller]")] +[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)] +public class GatewayController(IGatewayService gatewayService, ILogger logger) + : ControllerBase +{ + /// + /// 获取网关统计数据 + /// + /// 网关的整体统计信息,包括请求量、响应时间等指标 + /// 成功返回网关统计数据 + /// 服务器内部错误 + [HttpGet("statistics")] + [Produces("application/json")] + [ProducesResponseType(typeof(GatewayStatisticsDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task> GetStatistics() + { + try + { + var statistics = await gatewayService.GetStatisticsAsync(); + return Ok(statistics); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting gateway statistics"); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// 获取网关服务列表 + /// + /// 是否只返回全局服务,默认为false + /// 租户编码,用于筛选特定租户的服务 + /// 网关服务列表,包含服务名称、地址、健康状态等信息 + /// 成功返回网关服务列表 + /// 服务器内部错误 + [HttpGet("services")] + [Produces("application/json")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task>> GetServices( + [FromQuery] bool globalOnly = false, + [FromQuery] string? tenantCode = null) + { + try + { + var services = await gatewayService.GetServicesAsync(globalOnly, tenantCode); + return Ok(services); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting gateway services"); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// 获取单个网关服务详情 + /// + /// 服务名称 + /// 租户编码,用于筛选特定租户的服务 + /// 网关服务的详细信息 + /// 成功返回服务详情 + /// 服务不存在 + /// 服务器内部错误 + [HttpGet("services/{serviceName}")] + [Produces("application/json")] + [ProducesResponseType(typeof(GatewayServiceDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task> GetService( + string serviceName, + [FromQuery] string? tenantCode = null) + { + try + { + var service = await gatewayService.GetServiceAsync(serviceName, tenantCode); + if (service == null) + { + return NotFound(new { message = $"Service {serviceName} not found" }); + } + return Ok(service); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting service {ServiceName}", serviceName); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// 注册新的网关服务 + /// + /// 创建服务所需的配置信息 + /// 创建的服务详情 + /// 成功创建服务 + /// 请求参数无效或服务已存在 + /// 服务器内部错误 + [HttpPost("services")] + [Produces("application/json")] + [ProducesResponseType(typeof(GatewayServiceDto), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task> RegisterService([FromBody] CreateGatewayServiceDto dto) + { + try + { + var service = await gatewayService.RegisterServiceAsync(dto); + return CreatedAtAction(nameof(GetService), new { serviceName = service.ServicePrefix }, service); + } + catch (InvalidOperationException ex) + { + logger.LogWarning(ex, "Validation error registering service"); + return BadRequest(new { message = ex.Message }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error registering service"); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// 注销网关服务 + /// + /// 要注销的服务名称 + /// 租户编码,用于筛选特定租户的服务 + /// 无内容响应 + /// 成功注销服务 + /// 服务不存在 + /// 服务器内部错误 + [HttpDelete("services/{serviceName}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task UnregisterService( + string serviceName, + [FromQuery] string? tenantCode = null) + { + try + { + var result = await gatewayService.UnregisterServiceAsync(serviceName, tenantCode); + if (!result) + { + return NotFound(new { message = $"Service {serviceName} not found" }); + } + return Ok(new { message = "Service unregistered successfully" }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error unregistering service {ServiceName}", serviceName); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// 获取网关路由列表 + /// + /// 是否只返回全局路由,默认为false + /// 网关路由列表,包含路径匹配规则、转发目标等信息 + /// 成功返回网关路由列表 + /// 服务器内部错误 + [HttpGet("routes")] + [Produces("application/json")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task>> GetRoutes([FromQuery] bool globalOnly = false) + { + try + { + var routes = await gatewayService.GetRoutesAsync(globalOnly); + return Ok(routes); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting gateway routes"); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// 创建新的网关路由 + /// + /// 创建路由所需的配置信息 + /// 创建的路由详情 + /// 成功创建路由 + /// 请求参数无效 + /// 服务器内部错误 + [HttpPost("routes")] + [Produces("application/json")] + [ProducesResponseType(typeof(GatewayRouteDto), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task> CreateRoute([FromBody] CreateGatewayRouteDto dto) + { + try + { + var route = await gatewayService.CreateRouteAsync(dto); + return Ok(route); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { message = ex.Message }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error creating gateway route"); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// 获取集群实例列表 + /// + /// 集群ID + /// 指定集群下的所有服务实例列表 + /// 成功返回实例列表 + /// 服务器内部错误 + [HttpGet("clusters/{clusterId}/instances")] + [Produces("application/json")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task>> GetInstances(string clusterId) + { + try + { + var instances = await gatewayService.GetInstancesAsync(clusterId); + return Ok(instances); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting instances for cluster {ClusterId}", clusterId); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// 添加服务实例到集群 + /// + /// 创建实例所需的配置信息 + /// 创建的实例详情 + /// 成功添加实例 + /// 请求参数无效 + /// 服务器内部错误 + [HttpPost("instances")] + [Produces("application/json")] + [ProducesResponseType(typeof(GatewayInstanceDto), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task> AddInstance([FromBody] CreateGatewayInstanceDto dto) + { + try + { + var instance = await gatewayService.AddInstanceAsync(dto); + return CreatedAtAction(nameof(GetInstances), new { clusterId = dto.ClusterId }, instance); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { message = ex.Message }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error adding instance to cluster {ClusterId}", dto.ClusterId); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// 移除服务实例 + /// + /// 实例ID + /// 无内容响应 + /// 成功移除实例 + /// 实例不存在 + /// 服务器内部错误 + [HttpDelete("instances/{instanceId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task RemoveInstance(long instanceId) + { + try + { + var result = await gatewayService.RemoveInstanceAsync(instanceId); + if (!result) + { + return NotFound(new { message = $"Instance {instanceId} not found" }); + } + return Ok(new { message = "Instance removed successfully" }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error removing instance {InstanceId}", instanceId); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// 更新实例权重 + /// 用于负载均衡策略中调整实例的请求分发权重 + /// + /// 实例ID + /// 包含新权重值的请求体 + /// 无内容响应 + /// 成功更新权重 + /// 实例不存在 + /// 服务器内部错误 + [HttpPut("instances/{instanceId}/weight")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task UpdateWeight(long instanceId, [FromBody] GatewayUpdateWeightDto dto) + { + try + { + var result = await gatewayService.UpdateInstanceWeightAsync(instanceId, dto.Weight); + if (!result) + { + return NotFound(new { message = $"Instance {instanceId} not found" }); + } + return Ok(new { message = "Weight updated successfully" }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error updating weight for instance {InstanceId}", instanceId); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// 重新加载网关配置 + /// 触发网关重新加载所有配置,包括路由、服务、集群等配置 + /// + /// 无内容响应 + /// 成功重新加载配置 + /// 服务器内部错误 + [HttpPost("reload")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task ReloadGateway() + { + try + { + await gatewayService.ReloadGatewayAsync(); + return Ok(new { message = "Gateway configuration reloaded successfully" }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error reloading gateway configuration"); + return StatusCode(500, new { message = ex.Message }); + } + } +} diff --git a/Controllers/GlobalUsing.cs b/Controllers/GlobalUsing.cs new file mode 100644 index 0000000..737ce24 --- /dev/null +++ b/Controllers/GlobalUsing.cs @@ -0,0 +1,5 @@ +global using OpenIddict.Validation.AspNetCore; +global using Fengling.Console.Models.Dtos; +global using Fengling.Console.Services; +global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Mvc; \ No newline at end of file diff --git a/Controllers/OAuthClientsController.cs b/Controllers/OAuthClientsController.cs index 09a7fc3..1a33f4a 100644 --- a/Controllers/OAuthClientsController.cs +++ b/Controllers/OAuthClientsController.cs @@ -1,12 +1,11 @@ -using Fengling.Console.Services; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - namespace Fengling.Console.Controllers; +/// +/// OAuth客户端管理控制器 +/// [ApiController] -[Route("api/[controller]")] -[Authorize] +[Route("api/console/[controller]")] +[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)] public class OAuthClientsController : ControllerBase { private readonly IOAuthClientService _service; @@ -20,25 +19,31 @@ public class OAuthClientsController : ControllerBase _logger = logger; } + /// + /// 获取OAuth客户端列表 + /// + /// 分页查询参数,支持按显示名称、客户端ID和状态筛选 + /// 分页的OAuth客户端列表,包含总数量、分页信息和客户端详情 + /// 成功返回OAuth客户端分页列表 + /// 服务器内部错误 [HttpGet] - public async Task> GetClients( - [FromQuery] int page = 1, - [FromQuery] int pageSize = 10, - [FromQuery] string? displayName = null, - [FromQuery] string? clientId = null, - [FromQuery] string? status = null) + [Produces("application/json")] + [ProducesResponseType(typeof(OAuthClientListDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task> GetClients([FromQuery] OAuthClientQueryDto query) { try { - var (items, totalCount) = await _service.GetClientsAsync(page, pageSize, displayName, clientId, status); + var (items, totalCount) = await _service.GetClientsAsync(query.Page, query.PageSize, query.DisplayName, query.ClientId, query.Status); - return Ok(new + var result = new OAuthClientListDto { - items, - totalCount, - page, - pageSize - }); + Items = items.ToList(), + TotalCount = totalCount, + Page = query.Page, + PageSize = query.PageSize + }; + return Ok(result); } catch (Exception ex) { @@ -47,14 +52,33 @@ public class OAuthClientsController : ControllerBase } } + /// + /// 获取OAuth客户端选项 + /// + /// 包含客户端类型、授权类型、授权范围等可选值的配置选项 + /// 成功返回客户端配置选项 [HttpGet("options")] + [Produces("application/json")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] public ActionResult GetClientOptions() { return Ok(_service.GetClientOptions()); } + /// + /// 获取单个OAuth客户端详情 + /// + /// 客户端唯一标识符 + /// OAuth客户端的完整配置信息 + /// 成功返回客户端详情 + /// 客户端不存在 + /// 服务器内部错误 [HttpGet("{id}")] - public async Task> GetClient(string id) + [Produces("application/json")] + [ProducesResponseType(typeof(OAuthClientDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task> GetClient(string id) { try { @@ -73,8 +97,20 @@ public class OAuthClientsController : ControllerBase } } + /// + /// 创建新的OAuth客户端 + /// + /// 创建客户端所需的配置信息 + /// 创建的OAuth客户端详情 + /// 成功创建客户端 + /// 请求参数无效或客户端ID已存在 + /// 服务器内部错误 [HttpPost] - public async Task> CreateClient([FromBody] CreateClientDto dto) + [Produces("application/json")] + [ProducesResponseType(typeof(OAuthClientDto), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task> CreateClient([FromBody] CreateClientDto dto) { try { @@ -92,7 +128,19 @@ public class OAuthClientsController : ControllerBase } } + /// + /// 为指定客户端生成新的密钥 + /// + /// 客户端唯一标识符 + /// 包含新生成的客户端密钥信息 + /// 成功生成新密钥 + /// 客户端不存在 + /// 服务器内部错误 [HttpPost("{id}/generate-secret")] + [Produces("application/json")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task GenerateSecret(string id) { try @@ -111,7 +159,18 @@ public class OAuthClientsController : ControllerBase } } + /// + /// 删除指定的OAuth客户端 + /// + /// 客户端唯一标识符 + /// 无内容响应 + /// 成功删除客户端 + /// 客户端不存在 + /// 服务器内部错误 [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task DeleteClient(string id) { try @@ -130,7 +189,19 @@ public class OAuthClientsController : ControllerBase } } + /// + /// 更新指定的OAuth客户端 + /// + /// 客户端唯一标识符 + /// 需要更新的客户端配置信息 + /// 无内容响应 + /// 成功更新客户端 + /// 客户端不存在 + /// 服务器内部错误 [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task UpdateClient(string id, [FromBody] UpdateClientDto dto) { try diff --git a/Controllers/RolesController.cs b/Controllers/RolesController.cs index f9db496..e72768d 100644 --- a/Controllers/RolesController.cs +++ b/Controllers/RolesController.cs @@ -1,13 +1,12 @@ -using Fengling.Console.Models.Dtos; -using Fengling.Console.Services; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - namespace Fengling.Console.Controllers; +/// +/// 角色管理控制器 +/// 提供角色的增删改查以及用户角色关联管理功能 +/// [ApiController] -[Route("api/[controller]")] -[Authorize] +[Route("api/console/[controller]")] +[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)] public class RolesController : ControllerBase { private readonly IRoleService _roleService; @@ -19,17 +18,30 @@ public class RolesController : ControllerBase _logger = logger; } + /// + /// 获取角色列表 + /// + /// 分页查询参数,支持按名称和租户ID筛选 + /// 分页的角色列表,包含角色基本信息和关联统计 + /// 成功返回角色分页列表 + /// 服务器内部错误 [HttpGet] - public async Task> GetRoles( - [FromQuery] int page = 1, - [FromQuery] int pageSize = 10, - [FromQuery] string? name = null, - [FromQuery] string? tenantId = null) + [Produces("application/json")] + [ProducesResponseType(typeof(PagedResultDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task>> GetRoles([FromQuery] RoleQueryDto query) { try { - var (items, totalCount) = await _roleService.GetRolesAsync(page, pageSize, name, tenantId); - return Ok(new { items, totalCount, page, pageSize }); + var (items, totalCount) = await _roleService.GetRolesAsync(query.Page, query.PageSize, query.Name, query.TenantId); + var result = new PagedResultDto + { + Items = items.ToList(), + TotalCount = totalCount, + Page = query.Page, + PageSize = query.PageSize + }; + return Ok(result); } catch (Exception ex) { @@ -38,7 +50,19 @@ public class RolesController : ControllerBase } } + /// + /// 获取单个角色详情 + /// + /// 角色ID + /// 角色的详细信息,包括权限配置等 + /// 成功返回角色详情 + /// 角色不存在 + /// 服务器内部错误 [HttpGet("{id}")] + [Produces("application/json")] + [ProducesResponseType(typeof(RoleDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task> GetRole(long id) { try @@ -57,7 +81,19 @@ public class RolesController : ControllerBase } } + /// + /// 获取指定角色的用户列表 + /// + /// 角色ID + /// 属于该角色的所有用户列表 + /// 成功返回用户列表 + /// 角色不存在 + /// 服务器内部错误 [HttpGet("{id}/users")] + [Produces("application/json")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task>> GetRoleUsers(long id) { try @@ -77,7 +113,19 @@ public class RolesController : ControllerBase } } + /// + /// 创建新角色 + /// + /// 创建角色所需的配置信息 + /// 创建的角色详情 + /// 成功创建角色 + /// 请求参数无效或角色名称已存在 + /// 服务器内部错误 [HttpPost] + [Produces("application/json")] + [ProducesResponseType(typeof(RoleDto), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task> CreateRole([FromBody] CreateRoleDto dto) { try @@ -97,7 +145,21 @@ public class RolesController : ControllerBase } } + /// + /// 更新角色信息 + /// + /// 角色ID + /// 需要更新的角色配置信息 + /// 无内容响应 + /// 成功更新角色 + /// 角色不存在 + /// 请求参数无效 + /// 服务器内部错误 [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task UpdateRole(long id, [FromBody] UpdateRoleDto dto) { try @@ -122,7 +184,20 @@ public class RolesController : ControllerBase } } + /// + /// 删除角色 + /// + /// 角色ID + /// 无内容响应 + /// 成功删除角色 + /// 角色不存在 + /// 请求参数无效(如角色下有关联用户) + /// 服务器内部错误 [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task DeleteRole(long id) { try @@ -147,7 +222,60 @@ public class RolesController : ControllerBase } } + /// + /// 将用户添加到角色 + /// + /// 角色ID + /// 用户ID + /// 无内容响应 + /// 成功添加用户到角色 + /// 角色或用户不存在 + /// 请求参数无效或用户已在角色中 + /// 服务器内部错误 + [HttpPost("{id}/users/{userId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task AddUserToRole(long id, long userId) + { + try + { + await _roleService.AddUserToRoleAsync(id, userId); + return NoContent(); + } + catch (KeyNotFoundException ex) + { + _logger.LogWarning(ex, "Role or user not found: RoleId={RoleId}, UserId={UserId}", id, userId); + return NotFound(); + } + catch (InvalidOperationException ex) + { + _logger.LogWarning(ex, "Validation error adding user {UserId} to role {RoleId}", userId, id); + return BadRequest(new { message = ex.Message }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error adding user {UserId} to role {RoleId}", userId, id); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// 将用户从角色中移除 + /// + /// 角色ID + /// 用户ID + /// 无内容响应 + /// 成功从角色中移除用户 + /// 角色或用户不存在 + /// 请求参数无效 + /// 服务器内部错误 [HttpDelete("{id}/users/{userId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task RemoveUserFromRole(long id, long userId) { try diff --git a/Controllers/TenantsController.cs b/Controllers/TenantsController.cs index 8e232e4..bfe309c 100644 --- a/Controllers/TenantsController.cs +++ b/Controllers/TenantsController.cs @@ -1,13 +1,12 @@ -using Fengling.Console.Models.Dtos; -using Fengling.Console.Services; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - namespace Fengling.Console.Controllers; +/// +/// 租户管理控制器 +/// 提供租户的增删改查以及租户用户、角色、配置管理功能 +/// [ApiController] -[Route("api/[controller]")] -[Authorize] +[Route("api/console/[controller]")] +[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)] public class TenantsController : ControllerBase { private readonly ITenantService _tenantService; @@ -19,18 +18,31 @@ public class TenantsController : ControllerBase _logger = logger; } + /// + /// 获取租户列表 + /// + /// 分页查询参数,支持按名称、租户编码和状态筛选 + /// 分页的租户列表,包含租户基本信息和状态 + /// 成功返回租户分页列表 + /// 服务器内部错误 [HttpGet] - public async Task> GetTenants( - [FromQuery] int page = 1, - [FromQuery] int pageSize = 10, - [FromQuery] string? name = null, - [FromQuery] string? tenantId = null, - [FromQuery] string? status = null) + [Produces("application/json")] + [ProducesResponseType(typeof(PagedResultDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task>> GetTenants([FromQuery] TenantQueryDto query) { try { - var (items, totalCount) = await _tenantService.GetTenantsAsync(page, pageSize, name, tenantId, status); - return Ok(new { items, totalCount, page, pageSize }); + var (items, totalCount) = await _tenantService.GetTenantsAsync(query.Page, query.PageSize, query.Name, + query.TenantId, query.Status); + var result = new PagedResultDto + { + Items = items.ToList(), + TotalCount = totalCount, + Page = query.Page, + PageSize = query.PageSize + }; + return Ok(result); } catch (Exception ex) { @@ -39,7 +51,19 @@ public class TenantsController : ControllerBase } } + /// + /// 获取单个租户详情 + /// + /// 租户ID + /// 租户的详细信息,包括配置、限额等信息 + /// 成功返回租户详情 + /// 租户不存在 + /// 服务器内部错误 [HttpGet("{id}")] + [Produces("application/json")] + [ProducesResponseType(typeof(TenantDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task> GetTenant(long id) { try @@ -49,6 +73,7 @@ public class TenantsController : ControllerBase { return NotFound(); } + return Ok(tenant); } catch (Exception ex) @@ -58,7 +83,19 @@ public class TenantsController : ControllerBase } } + /// + /// 获取指定租户的用户列表 + /// + /// 租户ID + /// 属于该租户的所有用户列表 + /// 成功返回用户列表 + /// 租户不存在 + /// 服务器内部错误 [HttpGet("{tenantId}/users")] + [Produces("application/json")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task>> GetTenantUsers(long tenantId) { try @@ -78,7 +115,19 @@ public class TenantsController : ControllerBase } } + /// + /// 获取指定租户的角色列表 + /// + /// 租户ID + /// 属于该租户的所有角色列表 + /// 成功返回角色列表 + /// 租户不存在 + /// 服务器内部错误 [HttpGet("{tenantId}/roles")] + [Produces("application/json")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task>> GetTenantRoles(long tenantId) { try @@ -98,47 +147,81 @@ public class TenantsController : ControllerBase } } - [HttpGet("{tenantId}/settings")] - public async Task> GetTenantSettings(string tenantId) + /// + /// 获取租户配置信息 + /// + /// 租户ID + /// 租户的详细配置信息,包括功能开关、配额限制等 + /// 成功返回租户配置 + /// 租户不存在 + /// 服务器内部错误 + [HttpGet("{id}/settings")] + [Produces("application/json")] + [ProducesResponseType(typeof(TenantSettingsDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task> GetTenantSettings(long id) { try { - var settings = await _tenantService.GetTenantSettingsAsync(tenantId); + var settings = await _tenantService.GetTenantSettingsAsync(id); return Ok(settings); } catch (KeyNotFoundException ex) { - _logger.LogWarning(ex, "Tenant not found: {TenantId}", tenantId); + _logger.LogWarning(ex, "Tenant not found: {TenantId}", id); return NotFound(); } catch (Exception ex) { - _logger.LogError(ex, "Error getting settings for tenant {TenantId}", tenantId); + _logger.LogError(ex, "Error getting settings for tenant {TenantId}", id); return StatusCode(500, new { message = ex.Message }); } } - [HttpPut("{tenantId}/settings")] - public async Task UpdateTenantSettings(string tenantId, [FromBody] TenantSettingsDto settings) + /// + /// 更新租户配置 + /// + /// 租户ID + /// 需要更新的租户配置信息 + /// 无内容响应 + /// 成功更新租户配置 + /// 租户不存在 + /// 服务器内部错误 + [HttpPut("{id}/settings")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task UpdateTenantSettings(long id, [FromBody] TenantSettingsDto settings) { try { - await _tenantService.UpdateTenantSettingsAsync(tenantId, settings); + await _tenantService.UpdateTenantSettingsAsync(id, settings); return NoContent(); } catch (KeyNotFoundException ex) { - _logger.LogWarning(ex, "Tenant not found: {TenantId}", tenantId); + _logger.LogWarning(ex, "Tenant not found: {TenantId}", id); return NotFound(); } catch (Exception ex) { - _logger.LogError(ex, "Error updating settings for tenant {TenantId}", tenantId); + _logger.LogError(ex, "Error updating settings for tenant {TenantId}", id); return StatusCode(500, new { message = ex.Message }); } } + /// + /// 创建新租户 + /// + /// 创建租户所需的配置信息 + /// 创建的租户详情 + /// 成功创建租户 + /// 服务器内部错误 [HttpPost] + [Produces("application/json")] + [ProducesResponseType(typeof(TenantDto), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task> CreateTenant([FromBody] CreateTenantDto dto) { try @@ -153,7 +236,19 @@ public class TenantsController : ControllerBase } } + /// + /// 更新租户信息 + /// + /// 租户ID + /// 需要更新的租户配置信息 + /// 无内容响应 + /// 成功更新租户 + /// 租户不存在 + /// 服务器内部错误 [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task UpdateTenant(long id, [FromBody] UpdateTenantDto dto) { try @@ -173,7 +268,18 @@ public class TenantsController : ControllerBase } } + /// + /// 删除租户 + /// + /// 租户ID + /// 无内容响应 + /// 成功删除租户 + /// 租户不存在 + /// 服务器内部错误 [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task DeleteTenant(long id) { try @@ -192,4 +298,4 @@ public class TenantsController : ControllerBase return StatusCode(500, new { message = ex.Message }); } } -} +} \ No newline at end of file diff --git a/Controllers/UsersController.cs b/Controllers/UsersController.cs index 4147cca..4023d64 100644 --- a/Controllers/UsersController.cs +++ b/Controllers/UsersController.cs @@ -1,13 +1,14 @@ -using Fengling.Console.Models.Dtos; -using Fengling.Console.Services; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; + namespace Fengling.Console.Controllers; +/// +/// 用户管理控制器 +/// 提供用户的增删改查以及密码重置等功能 +/// [ApiController] -[Route("api/[controller]")] -[Authorize] +[Route("api/console/[controller]")] +[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)] public class UsersController : ControllerBase { private readonly IUserService _userService; @@ -19,18 +20,30 @@ public class UsersController : ControllerBase _logger = logger; } + /// + /// 获取用户列表 + /// + /// 分页查询参数,支持按用户名、邮箱和租户ID筛选 + /// 分页的用户列表,包含用户基本信息和状态 + /// 成功返回用户分页列表 + /// 服务器内部错误 [HttpGet] - public async Task> GetUsers( - [FromQuery] int page = 1, - [FromQuery] int pageSize = 10, - [FromQuery] string? userName = null, - [FromQuery] string? email = null, - [FromQuery] string? tenantId = null) + [Produces("application/json")] + [ProducesResponseType(typeof(PagedResultDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task>> GetUsers([FromQuery] UserQueryDto query) { try { - var (items, totalCount) = await _userService.GetUsersAsync(page, pageSize, userName, email, tenantId); - return Ok(new { items, totalCount, page, pageSize }); + var (items, totalCount) = await _userService.GetUsersAsync(query.Page, query.PageSize, query.UserName, query.Email, query.TenantId); + var result = new PagedResultDto + { + Items = items.ToList(), + TotalCount = totalCount, + Page = query.Page, + PageSize = query.PageSize + }; + return Ok(result); } catch (Exception ex) { @@ -39,7 +52,19 @@ public class UsersController : ControllerBase } } + /// + /// 获取单个用户详情 + /// + /// 用户ID + /// 用户的详细信息,包括角色、租户等信息 + /// 成功返回用户详情 + /// 用户不存在 + /// 服务器内部错误 [HttpGet("{id}")] + [Produces("application/json")] + [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task> GetUser(long id) { try @@ -58,7 +83,19 @@ public class UsersController : ControllerBase } } + /// + /// 创建新用户 + /// + /// 创建用户所需的配置信息 + /// 创建的用户详情 + /// 成功创建用户 + /// 请求参数无效或用户名/邮箱已存在 + /// 服务器内部错误 [HttpPost] + [Produces("application/json")] + [ProducesResponseType(typeof(UserDto), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task> CreateUser([FromBody] CreateUserDto dto) { try @@ -78,7 +115,19 @@ public class UsersController : ControllerBase } } + /// + /// 更新用户信息 + /// + /// 用户ID + /// 需要更新的用户配置信息 + /// 无内容响应 + /// 成功更新用户 + /// 用户不存在 + /// 服务器内部错误 [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task UpdateUser(long id, [FromBody] UpdateUserDto dto) { try @@ -98,7 +147,21 @@ public class UsersController : ControllerBase } } + /// + /// 重置用户密码 + /// + /// 用户ID + /// 包含新密码的请求体 + /// 无内容响应 + /// 成功重置密码 + /// 用户不存在 + /// 密码不符合复杂度要求 + /// 服务器内部错误 [HttpPut("{id}/password")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task ResetPassword(long id, [FromBody] ResetPasswordDto dto) { try @@ -123,7 +186,18 @@ public class UsersController : ControllerBase } } + /// + /// 删除用户 + /// + /// 用户ID + /// 无内容响应 + /// 成功删除用户 + /// 用户不存在 + /// 服务器内部错误 [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] public async Task DeleteUser(long id) { try diff --git a/Datas/ApplicationDbContext.cs b/Datas/ApplicationDbContext.cs new file mode 100644 index 0000000..e80e682 --- /dev/null +++ b/Datas/ApplicationDbContext.cs @@ -0,0 +1,85 @@ +using Fengling.Console.Models.Entities; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace Fengling.Console.Datas; + +public class ApplicationDbContext(DbContextOptions options) + : IdentityDbContext(options) +{ + public DbSet Tenants { get; set; } + public DbSet AccessLogs { get; set; } + public DbSet AuditLogs { get; set; } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.Entity(entity => + { + entity.Property(e => e.RealName).HasMaxLength(100); + entity.Property(e => e.Phone).HasMaxLength(20); + entity.HasIndex(e => e.Phone).IsUnique(); + + entity.OwnsOne(e => e.TenantInfo, navigationBuilder => + { + navigationBuilder.Property(e => e.Id).HasColumnName("TenantId"); + navigationBuilder.Property(e => e.TenantId).HasColumnName("TenantCode"); + navigationBuilder.Property(e => e.Name).HasColumnName("TenantName"); + navigationBuilder.WithOwner(); + }); + }); + + builder.Entity(entity => { entity.Property(e => e.Description).HasMaxLength(200); }); + + builder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.HasIndex(e => e.TenantId).IsUnique(); + entity.Property(e => e.TenantId).HasMaxLength(50); + entity.Property(e => e.Name).HasMaxLength(100); + entity.Property(e => e.ContactName).HasMaxLength(50); + entity.Property(e => e.ContactEmail).HasMaxLength(100); + entity.Property(e => e.ContactPhone).HasMaxLength(20); + entity.Property(e => e.Status).HasMaxLength(20); + entity.Property(e => e.Description).HasMaxLength(500); + }); + + builder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.HasIndex(e => e.CreatedAt); + entity.HasIndex(e => e.UserName); + entity.HasIndex(e => e.TenantId); + entity.HasIndex(e => e.Action); + entity.HasIndex(e => e.Status); + entity.Property(e => e.UserName).HasMaxLength(50); + entity.Property(e => e.TenantId).HasMaxLength(50); + entity.Property(e => e.Action).HasMaxLength(20); + entity.Property(e => e.Resource).HasMaxLength(200); + entity.Property(e => e.Method).HasMaxLength(10); + entity.Property(e => e.IpAddress).HasMaxLength(50); + entity.Property(e => e.UserAgent).HasMaxLength(500); + entity.Property(e => e.Status).HasMaxLength(20); + }); + + builder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.HasIndex(e => e.CreatedAt); + entity.HasIndex(e => e.Operator); + entity.HasIndex(e => e.TenantId); + entity.HasIndex(e => e.Operation); + entity.HasIndex(e => e.Action); + entity.Property(e => e.Operator).HasMaxLength(50); + entity.Property(e => e.TenantId).HasMaxLength(50); + entity.Property(e => e.Operation).HasMaxLength(20); + entity.Property(e => e.Action).HasMaxLength(20); + entity.Property(e => e.TargetType).HasMaxLength(50); + entity.Property(e => e.TargetName).HasMaxLength(100); + entity.Property(e => e.IpAddress).HasMaxLength(50); + entity.Property(e => e.Description).HasMaxLength(500); + entity.Property(e => e.Status).HasMaxLength(20); + }); + } +} \ No newline at end of file diff --git a/Fengling.Console.csproj b/Fengling.Console.csproj index 3ef007b..7bb7328 100644 --- a/Fengling.Console.csproj +++ b/Fengling.Console.csproj @@ -6,12 +6,21 @@ enable + + bin\Debug\net10.0\Fengling.Console.xml + + + + bin\Release\net10.0\Fengling.Console.xml + + + @@ -19,7 +28,8 @@ - + + diff --git a/Models/Dtos/CreateClientDto.cs b/Models/Dtos/CreateClientDto.cs new file mode 100644 index 0000000..884cb7e --- /dev/null +++ b/Models/Dtos/CreateClientDto.cs @@ -0,0 +1,16 @@ +namespace Fengling.Console.Models.Dtos; + +public record CreateClientDto +{ + public string ClientId { get; init; } = string.Empty; + public string? ClientSecret { get; init; } + public string DisplayName { get; init; } = string.Empty; + public string[]? RedirectUris { get; init; } + public string[]? PostLogoutRedirectUris { get; init; } + public string[]? Scopes { get; init; } + public string[]? GrantTypes { get; init; } + public string? ClientType { get; init; } + public string? ConsentType { get; init; } + public string? Status { get; init; } + public string? Description { get; init; } +} diff --git a/Models/Dtos/GatewayDto.cs b/Models/Dtos/GatewayDto.cs new file mode 100644 index 0000000..8049c03 --- /dev/null +++ b/Models/Dtos/GatewayDto.cs @@ -0,0 +1,89 @@ +namespace Fengling.Console.Models.Dtos; + +public class GatewayServiceDto +{ + public long Id { get; set; } + public string ServicePrefix { get; set; } = ""; + public string ServiceName { get; set; } = ""; + public string Version { get; set; } = "v1"; + public string ClusterId { get; set; } = ""; + public string PathPattern { get; set; } = ""; + public string ServiceAddress { get; set; } = ""; + public string DestinationId { get; set; } = ""; + public int Weight { get; set; } = 1; + public int InstanceCount { get; set; } + public bool IsGlobal { get; set; } + public string? TenantCode { get; set; } + public int Status { get; set; } = 1; + public DateTime CreatedAt { get; set; } +} + +public class CreateGatewayServiceDto +{ + public string ServicePrefix { get; set; } = ""; + public string ServiceName { get; set; } = ""; + public string Version { get; set; } = "v1"; + public string ServiceAddress { get; set; } = ""; + public string DestinationId { get; set; } = ""; + public int Weight { get; set; } = 1; + public bool IsGlobal { get; set; } = true; + public string? TenantCode { get; set; } +} + +public class GatewayRouteDto +{ + public long Id { get; set; } + public string ServiceName { get; set; } = ""; + public string ClusterId { get; set; } = ""; + public string PathPattern { get; set; } = ""; + public int Priority { get; set; } + public bool IsGlobal { get; set; } + public string? TenantCode { get; set; } + public int Status { get; set; } + public int InstanceCount { get; set; } +} + +public class CreateGatewayRouteDto +{ + public string ServiceName { get; set; } = ""; + public string ClusterId { get; set; } = ""; + public string PathPattern { get; set; } = ""; + public int Priority { get; set; } = 10; + public bool IsGlobal { get; set; } = true; + public string? TenantCode { get; set; } +} + +public class GatewayInstanceDto +{ + public long Id { get; set; } + public string ClusterId { get; set; } = ""; + public string DestinationId { get; set; } = ""; + public string Address { get; set; } = ""; + public int Weight { get; set; } = 1; + public int Health { get; set; } = 1; + public int Status { get; set; } = 1; + public DateTime CreatedAt { get; set; } +} + +public class CreateGatewayInstanceDto +{ + public string ClusterId { get; set; } = ""; + public string DestinationId { get; set; } = ""; + public string Address { get; set; } = ""; + public int Weight { get; set; } = 1; +} + +public class GatewayStatisticsDto +{ + public int TotalServices { get; set; } + public int GlobalRoutes { get; set; } + public int TenantRoutes { get; set; } + public int TotalInstances { get; set; } + public int HealthyInstances { get; set; } + public List RecentServices { get; set; } = new(); +} + +public class GatewayUpdateWeightDto +{ + public int Weight { get; set; } +} diff --git a/Models/Dtos/OAuthClientDto.cs b/Models/Dtos/OAuthClientDto.cs new file mode 100644 index 0000000..22944d2 --- /dev/null +++ b/Models/Dtos/OAuthClientDto.cs @@ -0,0 +1,69 @@ +namespace Fengling.Console.Models.Dtos; + +/// +/// OAuth客户端详细信息DTO +/// +public class OAuthClientDto +{ + /// + /// 客户端唯一标识符 + /// + public string Id { get; set; } = string.Empty; + + /// + /// 客户端ID,用于OAuth授权流程中的标识 + /// + public string ClientId { get; set; } = string.Empty; + + /// + /// 客户端显示名称 + /// + public string DisplayName { get; set; } = string.Empty; + + /// + /// 客户端描述信息 + /// + public string? Description { get; set; } + + /// + /// 回调地址列表,用于OAuth授权回调 + /// + public string[] RedirectUris { get; set; } = Array.Empty(); + + /// + /// 注销回调地址列表 + /// + public string[] PostLogoutRedirectUris { get; set; } = Array.Empty(); + + /// + /// 授权范围列表 + /// + public string[] Scopes { get; set; } = Array.Empty(); + + /// + /// 授权类型列表 + /// + public string[] GrantTypes { get; set; } = Array.Empty(); + + /// + /// 客户端类型:public(公开客户端)或 confidential(机密客户端) + /// + public string? ClientType { get; set; } + + /// + /// 授权同意类型:implicit、explicit或system + /// + public string? ConsentType { get; set; } + + /// + /// 客户端状态:active、inactive或suspended + /// + public string Status { get; set; } = "active"; +} + +/// +/// OAuth客户端列表分页结果DTO +/// +public class OAuthClientListDto : PagedResultDto +{ +} diff --git a/Models/Dtos/OAuthClientQueryDto.cs b/Models/Dtos/OAuthClientQueryDto.cs new file mode 100644 index 0000000..1720069 --- /dev/null +++ b/Models/Dtos/OAuthClientQueryDto.cs @@ -0,0 +1,10 @@ +namespace Fengling.Console.Models.Dtos; + +public class OAuthClientQueryDto : PaginationQueryDto +{ + public string? DisplayName { get; set; } + + public string? ClientId { get; set; } + + public string? Status { get; set; } +} diff --git a/Models/Dtos/PaginationDto.cs b/Models/Dtos/PaginationDto.cs new file mode 100644 index 0000000..a08374d --- /dev/null +++ b/Models/Dtos/PaginationDto.cs @@ -0,0 +1,29 @@ +namespace Fengling.Console.Models.Dtos; + +public class PaginationQueryDto +{ + public int Page { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string? SortBy { get; set; } + + public string? SortOrder { get; set; } +} + +public class PagedResultDto +{ + public List Items { get; set; } = new(); + + public int TotalCount { get; set; } + + public int Page { get; set; } + + public int PageSize { get; set; } + + public int TotalPages => PageSize > 0 ? (int)Math.Ceiling((double)TotalCount / PageSize) : 0; + + public bool HasNextPage => Page < TotalPages; + + public bool HasPreviousPage => Page > 1; +} diff --git a/Models/Dtos/RoleQueryDto.cs b/Models/Dtos/RoleQueryDto.cs new file mode 100644 index 0000000..c6e6a85 --- /dev/null +++ b/Models/Dtos/RoleQueryDto.cs @@ -0,0 +1,8 @@ +namespace Fengling.Console.Models.Dtos; + +public class RoleQueryDto : PaginationQueryDto +{ + public string? Name { get; set; } + + public string? TenantId { get; set; } +} diff --git a/Models/Dtos/TenantQueryDto.cs b/Models/Dtos/TenantQueryDto.cs new file mode 100644 index 0000000..adb41d3 --- /dev/null +++ b/Models/Dtos/TenantQueryDto.cs @@ -0,0 +1,10 @@ +namespace Fengling.Console.Models.Dtos; + +public class TenantQueryDto : PaginationQueryDto +{ + public string? Name { get; set; } + + public string? TenantId { get; set; } + + public string? Status { get; set; } +} diff --git a/Models/Dtos/UpdateClientDto.cs b/Models/Dtos/UpdateClientDto.cs new file mode 100644 index 0000000..38eb13f --- /dev/null +++ b/Models/Dtos/UpdateClientDto.cs @@ -0,0 +1,14 @@ +namespace Fengling.Console.Models.Dtos; + +public record UpdateClientDto +{ + public string? DisplayName { get; init; } + public string[]? RedirectUris { get; init; } + public string[]? PostLogoutRedirectUris { get; init; } + public string[]? Scopes { get; init; } + public string[]? GrantTypes { get; init; } + public string? ClientType { get; init; } + public string? ConsentType { get; init; } + public string? Status { get; init; } + public string? Description { get; init; } +} diff --git a/Models/Dtos/UserQueryDto.cs b/Models/Dtos/UserQueryDto.cs new file mode 100644 index 0000000..79379fd --- /dev/null +++ b/Models/Dtos/UserQueryDto.cs @@ -0,0 +1,10 @@ +namespace Fengling.Console.Models.Dtos; + +public class UserQueryDto : PaginationQueryDto +{ + public string? UserName { get; set; } + + public string? Email { get; set; } + + public string? TenantId { get; set; } +} diff --git a/Models/Entities/AccessLog.cs b/Models/Entities/AccessLog.cs new file mode 100644 index 0000000..afd0a9b --- /dev/null +++ b/Models/Entities/AccessLog.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.DataAnnotations; + +namespace Fengling.Console.Models.Entities; + +public class AccessLog +{ + [Key] + public long Id { get; set; } + + [MaxLength(50)] + public string? UserName { get; set; } + + [MaxLength(50)] + public string? TenantId { get; set; } + + [MaxLength(20)] + public string Action { get; set; } = string.Empty; + + [MaxLength(200)] + public string? Resource { get; set; } + + [MaxLength(10)] + public string? Method { get; set; } + + [MaxLength(50)] + public string? IpAddress { get; set; } + + [MaxLength(500)] + public string? UserAgent { get; set; } + + [MaxLength(20)] + public string Status { get; set; } = "success"; + + public int Duration { get; set; } + + public string? RequestData { get; set; } + + public string? ResponseData { get; set; } + + public string? ErrorMessage { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/Models/Entities/ApplicationRole.cs b/Models/Entities/ApplicationRole.cs new file mode 100644 index 0000000..4a33b87 --- /dev/null +++ b/Models/Entities/ApplicationRole.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Identity; + +namespace Fengling.Console.Models.Entities; + +public class ApplicationRole : IdentityRole +{ + public string? Description { get; set; } + public DateTime CreatedTime { get; set; } = DateTime.UtcNow; + public long? TenantId { get; set; } + public bool IsSystem { get; set; } + public string? DisplayName { get; set; } + public List? Permissions { get; set; } +} diff --git a/Models/Entities/ApplicationUser.cs b/Models/Entities/ApplicationUser.cs new file mode 100644 index 0000000..b310b37 --- /dev/null +++ b/Models/Entities/ApplicationUser.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Identity; + +namespace Fengling.Console.Models.Entities; + +public class ApplicationUser : IdentityUser +{ + public string? RealName { get; set; } + public string? Phone { get; set; } + public TenantInfo TenantInfo { get; set; } = null!; + public DateTime CreatedTime { get; set; } = DateTime.UtcNow; + public DateTime? UpdatedTime { get; set; } + public bool IsDeleted { get; set; } +} diff --git a/Models/Entities/AuditLog.cs b/Models/Entities/AuditLog.cs new file mode 100644 index 0000000..4ea18df --- /dev/null +++ b/Models/Entities/AuditLog.cs @@ -0,0 +1,47 @@ +using System.ComponentModel.DataAnnotations; + +namespace Fengling.Console.Models.Entities; + +public class AuditLog +{ + [Key] + public long Id { get; set; } + + [MaxLength(50)] + [Required] + public string Operator { get; set; } = string.Empty; + + [MaxLength(50)] + public string? TenantId { get; set; } + + [MaxLength(20)] + public string Operation { get; set; } = string.Empty; + + [MaxLength(20)] + public string Action { get; set; } = string.Empty; + + [MaxLength(50)] + public string? TargetType { get; set; } + + public long? TargetId { get; set; } + + [MaxLength(100)] + public string? TargetName { get; set; } + + [MaxLength(50)] + public string IpAddress { get; set; } = string.Empty; + + [MaxLength(500)] + public string? Description { get; set; } + + public string? OldValue { get; set; } + + public string? NewValue { get; set; } + + public string? ErrorMessage { get; set; } + + [MaxLength(20)] + public string Status { get; set; } = "success"; + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/Models/Entities/Tenant.cs b/Models/Entities/Tenant.cs new file mode 100644 index 0000000..fa9da08 --- /dev/null +++ b/Models/Entities/Tenant.cs @@ -0,0 +1,63 @@ +using System.ComponentModel.DataAnnotations; + +namespace Fengling.Console.Models.Entities; + +public class Tenant +{ + private long _id; + private string _tenantId; + private string _name; + + [Key] + public long Id + { + get => _id; + set => _id = value; + } + + [MaxLength(50)] + [Required] + public string TenantId + { + get => _tenantId; + set => _tenantId = value; + } + + [MaxLength(100)] + [Required] + public string Name + { + get => _name; + set => _name = value; + } + + [MaxLength(50)] + [Required] + public string ContactName { get; set; } = string.Empty; + + [MaxLength(100)] + [Required] + [EmailAddress] + public string ContactEmail { get; set; } = string.Empty; + + [MaxLength(20)] + public string? ContactPhone { get; set; } + + public int? MaxUsers { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [MaxLength(500)] + public string? Description { get; set; } + + [MaxLength(20)] + public string Status { get; set; } = "active"; + + public DateTime? ExpiresAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public bool IsDeleted { get; set; } + + public TenantInfo Info => new(Id, TenantId, Name); +} diff --git a/Models/Entities/TenantInfo.cs b/Models/Entities/TenantInfo.cs new file mode 100644 index 0000000..b08be7f --- /dev/null +++ b/Models/Entities/TenantInfo.cs @@ -0,0 +1,3 @@ +namespace Fengling.Console.Models.Entities; + +public record TenantInfo(long Id, string TenantId, string Name); diff --git a/Program.cs b/Program.cs index 9713dc7..2080052 100644 --- a/Program.cs +++ b/Program.cs @@ -1,6 +1,4 @@ using System.Reflection; -using Fengling.AuthService.Data; -using Fengling.AuthService.Models; using Fengling.Console.Repositories; using Fengling.Console.Services; using OpenIddict.Abstractions; @@ -9,6 +7,10 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; using Microsoft.IdentityModel.Tokens; using System.Text; +using Fengling.Console.Datas; +using Fengling.Console.Models.Entities; +using OpenIddict.Validation.AspNetCore; +using YarpGateway.Data; var builder = WebApplication.CreateBuilder(args); @@ -19,6 +21,9 @@ builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")); }); +builder.Services.AddDbContext(options => + options.UseNpgsql(builder.Configuration.GetConnectionString("GatewayConnection"))); + builder.Services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); @@ -39,27 +44,25 @@ builder.Services.AddOpenIddict() .AddCore(options => { options.UseEntityFrameworkCore().UseDbContext(); - }); - -var jwtKey = builder.Configuration["Jwt:Key"]; -var jwtIssuer = builder.Configuration["Jwt:Issuer"]; -var jwtAudience = builder.Configuration["Jwt:Audience"]; - -builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(options => + }) + .AddValidation(options => { - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidateAudience = true, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - ValidIssuer = jwtIssuer, - ValidAudience = jwtAudience, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey ?? throw new InvalidOperationException("JWT Key is not configured"))) - }; + options.SetIssuer("http://localhost:5132/"); + + options.UseIntrospection() + .SetClientId("fengling-api") + .SetClientSecret("fengling-api-secret"); + + options.UseSystemNetHttp(); + + options.UseAspNetCore(); }); +builder.Services.AddAuthentication(options => +{ + options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; +}); + builder.Services.AddAuthorization(); builder.Services.AddCors(options => diff --git a/Repositories/IRoleRepository.cs b/Repositories/IRoleRepository.cs index 2674ae5..1f6ec45 100644 --- a/Repositories/IRoleRepository.cs +++ b/Repositories/IRoleRepository.cs @@ -1,4 +1,4 @@ -using Fengling.AuthService.Models; +using Fengling.Console.Models.Entities; namespace Fengling.Console.Repositories; diff --git a/Repositories/ITenantRepository.cs b/Repositories/ITenantRepository.cs index 12ec6d0..800877b 100644 --- a/Repositories/ITenantRepository.cs +++ b/Repositories/ITenantRepository.cs @@ -1,4 +1,4 @@ -using Fengling.AuthService.Models; +using Fengling.Console.Models.Entities; namespace Fengling.Console.Repositories; diff --git a/Repositories/IUserRepository.cs b/Repositories/IUserRepository.cs index 009f5f5..59fd279 100644 --- a/Repositories/IUserRepository.cs +++ b/Repositories/IUserRepository.cs @@ -1,4 +1,4 @@ -using Fengling.AuthService.Models; +using Fengling.Console.Models.Entities; namespace Fengling.Console.Repositories; diff --git a/Repositories/RoleRepository.cs b/Repositories/RoleRepository.cs index 73a2a8c..8e09129 100644 --- a/Repositories/RoleRepository.cs +++ b/Repositories/RoleRepository.cs @@ -1,36 +1,30 @@ -using Fengling.AuthService.Data; -using Fengling.AuthService.Models; +using Fengling.Console.Datas; +using Fengling.Console.Models.Entities; using Microsoft.EntityFrameworkCore; namespace Fengling.Console.Repositories; -public class RoleRepository : IRoleRepository +public class RoleRepository(ApplicationDbContext context) : IRoleRepository { - private readonly ApplicationDbContext _context; - - public RoleRepository(ApplicationDbContext context) - { - _context = context; - } - public async Task GetByIdAsync(long id) { - return await _context.Roles.FindAsync(id); + return await context.Roles.FindAsync(id); } public async Task GetByNameAsync(string name) { - return await _context.Roles.FirstOrDefaultAsync(r => r.Name == name); + return await context.Roles.FirstOrDefaultAsync(r => r.Name == name); } public async Task> GetAllAsync() { - return await _context.Roles.ToListAsync(); + return await context.Roles.ToListAsync(); } - public async Task> GetPagedAsync(int page, int pageSize, string? name = null, string? tenantId = null) + public async Task> GetPagedAsync(int page, int pageSize, string? name = null, + string? tenantId = null) { - var query = _context.Roles.AsQueryable(); + var query = context.Roles.AsQueryable(); if (!string.IsNullOrEmpty(name)) { @@ -51,7 +45,7 @@ public class RoleRepository : IRoleRepository public async Task CountAsync(string? name = null, string? tenantId = null) { - var query = _context.Roles.AsQueryable(); + var query = context.Roles.AsQueryable(); if (!string.IsNullOrEmpty(name)) { @@ -68,19 +62,19 @@ public class RoleRepository : IRoleRepository public async Task AddAsync(ApplicationRole role) { - _context.Roles.Add(role); - await _context.SaveChangesAsync(); + context.Roles.Add(role); + await context.SaveChangesAsync(); } public async Task UpdateAsync(ApplicationRole role) { - _context.Roles.Update(role); - await _context.SaveChangesAsync(); + context.Roles.Update(role); + await context.SaveChangesAsync(); } public async Task DeleteAsync(ApplicationRole role) { - _context.Roles.Remove(role); - await _context.SaveChangesAsync(); + context.Roles.Remove(role); + await context.SaveChangesAsync(); } -} +} \ No newline at end of file diff --git a/Repositories/TenantRepository.cs b/Repositories/TenantRepository.cs index 30b7ce6..f7806fe 100644 --- a/Repositories/TenantRepository.cs +++ b/Repositories/TenantRepository.cs @@ -1,36 +1,30 @@ -using Fengling.AuthService.Data; -using Fengling.AuthService.Models; +using Fengling.Console.Datas; +using Fengling.Console.Models.Entities; using Microsoft.EntityFrameworkCore; namespace Fengling.Console.Repositories; -public class TenantRepository : ITenantRepository +public class TenantRepository(ApplicationDbContext context) : ITenantRepository { - private readonly ApplicationDbContext _context; - - public TenantRepository(ApplicationDbContext context) - { - _context = context; - } - public async Task GetByIdAsync(long id) { - return await _context.Tenants.FindAsync(id); + return await context.Tenants.FindAsync(id); } public async Task GetByTenantIdAsync(string tenantId) { - return await _context.Tenants.FirstOrDefaultAsync(t => t.TenantId == tenantId); + return await context.Tenants.FirstOrDefaultAsync(t => t.TenantId == tenantId); } public async Task> GetAllAsync() { - return await _context.Tenants.ToListAsync(); + return await context.Tenants.ToListAsync(); } - public async Task> GetPagedAsync(int page, int pageSize, string? name = null, string? tenantId = null, string? status = null) + public async Task> GetPagedAsync(int page, int pageSize, string? name = null, + string? tenantId = null, string? status = null) { - var query = _context.Tenants.AsQueryable(); + var query = context.Tenants.AsQueryable(); if (!string.IsNullOrEmpty(name)) { @@ -56,7 +50,7 @@ public class TenantRepository : ITenantRepository public async Task CountAsync(string? name = null, string? tenantId = null, string? status = null) { - var query = _context.Tenants.AsQueryable(); + var query = context.Tenants.AsQueryable(); if (!string.IsNullOrEmpty(name)) { @@ -78,24 +72,24 @@ public class TenantRepository : ITenantRepository public async Task AddAsync(Tenant tenant) { - _context.Tenants.Add(tenant); - await _context.SaveChangesAsync(); + context.Tenants.Add(tenant); + await context.SaveChangesAsync(); } public async Task UpdateAsync(Tenant tenant) { - _context.Tenants.Update(tenant); - await _context.SaveChangesAsync(); + context.Tenants.Update(tenant); + await context.SaveChangesAsync(); } public async Task DeleteAsync(Tenant tenant) { - _context.Tenants.Remove(tenant); - await _context.SaveChangesAsync(); + context.Tenants.Remove(tenant); + await context.SaveChangesAsync(); } public async Task GetUserCountAsync(long tenantId) { - return await _context.Users.CountAsync(u => u.TenantInfo.Id == tenantId && !u.IsDeleted); + return await context.Users.CountAsync(u => u.TenantInfo.Id == tenantId && !u.IsDeleted); } -} +} \ No newline at end of file diff --git a/Repositories/UserRepository.cs b/Repositories/UserRepository.cs index 9677e64..fdad108 100644 --- a/Repositories/UserRepository.cs +++ b/Repositories/UserRepository.cs @@ -1,5 +1,5 @@ -using Fengling.AuthService.Data; -using Fengling.AuthService.Models; +using Fengling.Console.Datas; +using Fengling.Console.Models.Entities; using Microsoft.EntityFrameworkCore; namespace Fengling.Console.Repositories; diff --git a/Services/GatewayService.cs b/Services/GatewayService.cs new file mode 100644 index 0000000..acf71c9 --- /dev/null +++ b/Services/GatewayService.cs @@ -0,0 +1,386 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using YarpGateway.Data; +using YarpGateway.Models; +using Fengling.Console.Models.Dtos; + +namespace Fengling.Console.Services; + +public interface IGatewayService +{ + Task GetStatisticsAsync(); + Task> GetServicesAsync(bool globalOnly = false, string? tenantCode = null); + Task GetServiceAsync(string serviceName, string? tenantCode = null); + Task RegisterServiceAsync(CreateGatewayServiceDto dto); + Task UnregisterServiceAsync(string serviceName, string? tenantCode = null); + Task> GetRoutesAsync(bool globalOnly = false); + Task CreateRouteAsync(CreateGatewayRouteDto dto); + Task> GetInstancesAsync(string clusterId); + Task AddInstanceAsync(CreateGatewayInstanceDto dto); + Task RemoveInstanceAsync(long instanceId); + Task UpdateInstanceWeightAsync(long instanceId, int weight); + Task ReloadGatewayAsync(); +} + +public class GatewayService : IGatewayService +{ + private readonly IDbContextFactory _dbContextFactory; + private readonly ILogger _logger; + + public GatewayService(IDbContextFactory dbContextFactory, ILogger logger) + { + _dbContextFactory = dbContextFactory; + _logger = logger; + } + + public async Task GetStatisticsAsync() + { + await using var db = await _dbContextFactory.CreateDbContextAsync(); + + var routes = await db.TenantRoutes.Where(r => !r.IsDeleted).ToListAsync(); + var instances = await db.ServiceInstances.Where(i => !i.IsDeleted).ToListAsync(); + + return new GatewayStatisticsDto + { + TotalServices = routes.Select(r => r.ServiceName).Distinct().Count(), + GlobalRoutes = routes.Count(r => r.IsGlobal), + TenantRoutes = routes.Count(r => !r.IsGlobal), + TotalInstances = instances.Count, + HealthyInstances = instances.Count(i => i.Health == 1), + RecentServices = routes + .OrderByDescending(r => r.CreatedTime) + .Take(5) + .Select(MapToServiceDto) + .ToList() + }; + } + + public async Task> GetServicesAsync(bool globalOnly = false, string? tenantCode = null) + { + await using var db = await _dbContextFactory.CreateDbContextAsync(); + + var query = db.TenantRoutes.Where(r => !r.IsDeleted); + + if (globalOnly) + query = query.Where(r => r.IsGlobal); + else if (!string.IsNullOrEmpty(tenantCode)) + query = query.Where(r => r.TenantCode == tenantCode); + + var routes = await query.OrderByDescending(r => r.CreatedTime).ToListAsync(); + var clusters = routes.Select(r => r.ClusterId).Distinct().ToList(); + + var instances = await db.ServiceInstances + .Where(i => clusters.Contains(i.ClusterId) && !i.IsDeleted) + .GroupBy(i => i.ClusterId) + .ToDictionaryAsync(g => g.Key, g => g.Count()); + + return routes.Select(r => MapToServiceDto(r, instances.GetValueOrDefault(r.ClusterId, 0))).ToList(); + } + + public async Task GetServiceAsync(string serviceName, string? tenantCode = null) + { + await using var db = await _dbContextFactory.CreateDbContextAsync(); + + var route = await db.TenantRoutes + .FirstOrDefaultAsync(r => + r.ServiceName == serviceName && + r.IsDeleted == false && + (r.IsGlobal || r.TenantCode == tenantCode)); + + if (route == null) return null; + + var instances = await db.ServiceInstances + .CountAsync(i => i.ClusterId == route.ClusterId && !i.IsDeleted); + + return MapToServiceDto(route, instances); + } + + public async Task RegisterServiceAsync(CreateGatewayServiceDto dto) + { + await using var db = await _dbContextFactory.CreateDbContextAsync(); + + var clusterId = $"{dto.ServicePrefix}-service"; + var pathPattern = $"/{dto.ServicePrefix}/{dto.Version}/{{**path}}"; + var destinationId = string.IsNullOrEmpty(dto.DestinationId) + ? $"{dto.ServicePrefix}-1" + : dto.DestinationId; + + // Check if route already exists + var existingRoute = await db.TenantRoutes + .FirstOrDefaultAsync(r => + r.ServiceName == dto.ServicePrefix && + r.IsGlobal == dto.IsGlobal && + (dto.IsGlobal || r.TenantCode == dto.TenantCode)); + + if (existingRoute != null) + { + throw new InvalidOperationException($"Service {dto.ServicePrefix} already registered"); + } + + // Add instance + var instanceId = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + var instance = new GwServiceInstance + { + Id = instanceId, + ClusterId = clusterId, + DestinationId = destinationId, + Address = dto.ServiceAddress, + Weight = dto.Weight, + Health = 1, + Status = 1, + CreatedTime = DateTime.UtcNow + }; + await db.ServiceInstances.AddAsync(instance); + + // Add route + var routeId = instanceId + 1; + var route = new GwTenantRoute + { + Id = routeId, + TenantCode = dto.IsGlobal ? "" : dto.TenantCode ?? "", + ServiceName = dto.ServicePrefix, + ClusterId = clusterId, + PathPattern = pathPattern, + Priority = dto.IsGlobal ? 0 : 10, + Status = 1, + IsGlobal = dto.IsGlobal, + CreatedTime = DateTime.UtcNow + }; + await db.TenantRoutes.AddAsync(route); + + await db.SaveChangesAsync(); + + _logger.LogInformation("Registered service {Service} at {Address}", dto.ServicePrefix, dto.ServiceAddress); + + return MapToServiceDto(route, 1); + } + + public async Task UnregisterServiceAsync(string serviceName, string? tenantCode = null) + { + await using var db = await _dbContextFactory.CreateDbContextAsync(); + + var route = await db.TenantRoutes + .FirstOrDefaultAsync(r => + r.ServiceName == serviceName && + r.IsDeleted == false && + (r.IsGlobal || r.TenantCode == tenantCode)); + + if (route == null) return false; + + // Soft delete route + route.IsDeleted = true; + route.UpdatedTime = DateTime.UtcNow; + + // Soft delete instances + var instances = await db.ServiceInstances + .Where(i => i.ClusterId == route.ClusterId && !i.IsDeleted) + .ToListAsync(); + + foreach (var instance in instances) + { + instance.IsDeleted = true; + instance.UpdatedTime = DateTime.UtcNow; + } + + await db.SaveChangesAsync(); + + _logger.LogInformation("Unregistered service {Service}", serviceName); + + return true; + } + + public async Task> GetRoutesAsync(bool globalOnly = false) + { + await using var db = await _dbContextFactory.CreateDbContextAsync(); + + var query = db.TenantRoutes.Where(r => !r.IsDeleted); + + if (globalOnly) + query = query.Where(r => r.IsGlobal); + + var routes = await query.OrderByDescending(r => r.Priority).ToListAsync(); + var clusters = routes.Select(r => r.ClusterId).Distinct().ToList(); + + var instances = await db.ServiceInstances + .Where(i => clusters.Contains(i.ClusterId) && !i.IsDeleted) + .GroupBy(i => i.ClusterId) + .ToDictionaryAsync(g => g.Key, g => g.Count()); + + return routes.Select(r => new GatewayRouteDto + { + Id = r.Id, + ServiceName = r.ServiceName, + ClusterId = r.ClusterId, + PathPattern = r.PathPattern, + Priority = r.Priority, + IsGlobal = r.IsGlobal, + TenantCode = r.TenantCode, + Status = r.Status, + InstanceCount = instances.GetValueOrDefault(r.ClusterId, 0) + }).ToList(); + } + + public async Task CreateRouteAsync(CreateGatewayRouteDto dto) + { + await using var db = await _dbContextFactory.CreateDbContextAsync(); + + var existing = await db.TenantRoutes + .FirstOrDefaultAsync(r => + r.ServiceName == dto.ServiceName && + r.IsGlobal == dto.IsGlobal && + (dto.IsGlobal || r.TenantCode == dto.TenantCode)); + + if (existing != null) + { + throw new InvalidOperationException($"Route for {dto.ServiceName} already exists"); + } + + var route = new GwTenantRoute + { + Id = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + TenantCode = dto.IsGlobal ? "" : dto.TenantCode ?? "", + ServiceName = dto.ServiceName, + ClusterId = dto.ClusterId, + PathPattern = dto.PathPattern, + Priority = dto.Priority, + Status = 1, + IsGlobal = dto.IsGlobal, + CreatedTime = DateTime.UtcNow + }; + + await db.TenantRoutes.AddAsync(route); + await db.SaveChangesAsync(); + + return new GatewayRouteDto + { + Id = route.Id, + ServiceName = route.ServiceName, + ClusterId = route.ClusterId, + PathPattern = route.PathPattern, + Priority = route.Priority, + IsGlobal = route.IsGlobal, + TenantCode = route.TenantCode, + Status = route.Status, + InstanceCount = 0 + }; + } + + public async Task> GetInstancesAsync(string clusterId) + { + await using var db = await _dbContextFactory.CreateDbContextAsync(); + + var instances = await db.ServiceInstances + .Where(i => i.ClusterId == clusterId && !i.IsDeleted) + .OrderByDescending(i => i.Weight) + .ToListAsync(); + + return instances.Select(i => new GatewayInstanceDto + { + Id = i.Id, + ClusterId = i.ClusterId, + DestinationId = i.DestinationId, + Address = i.Address, + Weight = i.Weight, + Health = i.Health, + Status = i.Status, + CreatedAt = i.CreatedTime + }).ToList(); + } + + public async Task AddInstanceAsync(CreateGatewayInstanceDto dto) + { + await using var db = await _dbContextFactory.CreateDbContextAsync(); + + var existing = await db.ServiceInstances + .FirstOrDefaultAsync(i => + i.ClusterId == dto.ClusterId && + i.DestinationId == dto.DestinationId && + !i.IsDeleted); + + if (existing != null) + { + throw new InvalidOperationException($"Instance {dto.DestinationId} already exists in cluster {dto.ClusterId}"); + } + + var instance = new GwServiceInstance + { + Id = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + ClusterId = dto.ClusterId, + DestinationId = dto.DestinationId, + Address = dto.Address, + Weight = dto.Weight, + Health = 1, + Status = 1, + CreatedTime = DateTime.UtcNow + }; + + await db.ServiceInstances.AddAsync(instance); + await db.SaveChangesAsync(); + + return new GatewayInstanceDto + { + Id = instance.Id, + ClusterId = instance.ClusterId, + DestinationId = instance.DestinationId, + Address = instance.Address, + Weight = instance.Weight, + Health = instance.Health, + Status = instance.Status, + CreatedAt = instance.CreatedTime + }; + } + + public async Task RemoveInstanceAsync(long instanceId) + { + await using var db = await _dbContextFactory.CreateDbContextAsync(); + + var instance = await db.ServiceInstances.FindAsync(instanceId); + if (instance == null) return false; + + instance.IsDeleted = true; + instance.UpdatedTime = DateTime.UtcNow; + + await db.SaveChangesAsync(); + return true; + } + + public async Task UpdateInstanceWeightAsync(long instanceId, int weight) + { + await using var db = await _dbContextFactory.CreateDbContextAsync(); + + var instance = await db.ServiceInstances.FindAsync(instanceId); + if (instance == null) return false; + + instance.Weight = weight; + instance.UpdatedTime = DateTime.UtcNow; + + await db.SaveChangesAsync(); + return true; + } + + public async Task ReloadGatewayAsync() + { + _logger.LogInformation("Gateway configuration reloaded"); + await Task.CompletedTask; + } + + private static GatewayServiceDto MapToServiceDto(GwTenantRoute route, int instanceCount = 0) + { + return new GatewayServiceDto + { + Id = route.Id, + ServicePrefix = route.ServiceName, + ServiceName = route.ServiceName, + ClusterId = route.ClusterId, + PathPattern = route.PathPattern, + ServiceAddress = "", + DestinationId = "", + Weight = 1, + InstanceCount = instanceCount, + IsGlobal = route.IsGlobal, + TenantCode = route.TenantCode, + Status = route.Status, + CreatedAt = route.CreatedTime + }; + } +} diff --git a/Services/OAuthClientService.cs b/Services/OAuthClientService.cs index 39ed620..25fbf4c 100644 --- a/Services/OAuthClientService.cs +++ b/Services/OAuthClientService.cs @@ -1,3 +1,4 @@ +using Fengling.Console.Models.Dtos; using OpenIddict.Abstractions; using System.Security.Cryptography; using System.Text.Json; @@ -6,43 +7,15 @@ namespace Fengling.Console.Services; public interface IOAuthClientService { - Task<(IEnumerable Items, int TotalCount)> GetClientsAsync(int page, int pageSize, string? displayName = null, string? clientId = null, string? status = null); - Task GetClientAsync(string id); - Task CreateClientAsync(CreateClientDto dto); + Task<(IEnumerable Items, int TotalCount)> GetClientsAsync(int page, int pageSize, string? displayName = null, string? clientId = null, string? status = null); + Task GetClientAsync(string id); + Task CreateClientAsync(CreateClientDto dto); Task GenerateSecretAsync(string id); Task UpdateClientAsync(string id, UpdateClientDto dto); Task DeleteClientAsync(string id); object GetClientOptions(); } -public record CreateClientDto -{ - public string ClientId { get; init; } = string.Empty; - public string? ClientSecret { get; init; } - public string DisplayName { get; init; } = string.Empty; - public string[]? RedirectUris { get; init; } - public string[]? PostLogoutRedirectUris { get; init; } - public string[]? Scopes { get; init; } - public string[]? GrantTypes { get; init; } - public string? ClientType { get; init; } - public string? ConsentType { get; init; } - public string? Status { get; init; } - public string? Description { get; init; } -} - -public record UpdateClientDto -{ - public string? DisplayName { get; init; } - public string[]? RedirectUris { get; init; } - public string[]? PostLogoutRedirectUris { get; init; } - public string[]? Scopes { get; init; } - public string[]? GrantTypes { get; init; } - public string? ClientType { get; init; } - public string? ConsentType { get; init; } - public string? Status { get; init; } - public string? Description { get; init; } -} - public class OAuthClientService : IOAuthClientService { private readonly IOpenIddictApplicationManager _applicationManager; @@ -62,10 +35,10 @@ public class OAuthClientService : IOAuthClientService _logger = logger; } - public async Task<(IEnumerable Items, int TotalCount)> GetClientsAsync(int page, int pageSize, string? displayName = null, string? clientId = null, string? status = null) + public async Task<(IEnumerable Items, int TotalCount)> GetClientsAsync(int page, int pageSize, string? displayName = null, string? clientId = null, string? status = null) { var applications = _applicationManager.ListAsync(); - var clientList = new List(); + var clientList = new List(); await foreach (var application in applications) { @@ -83,28 +56,31 @@ public class OAuthClientService : IOAuthClientService var permissions = await _applicationManager.GetPermissionsAsync(application); var redirectUris = await _applicationManager.GetRedirectUrisAsync(application); var postLogoutRedirectUris = await _applicationManager.GetPostLogoutRedirectUrisAsync(application); + var applicationId = await _applicationManager.GetIdAsync(application); - clientList.Add(new + clientList.Add(new OAuthClientDto { - id = application, - clientId = clientIdValue, - displayName = displayNameValue, - redirectUris = redirectUris.ToArray(), - postLogoutRedirectUris = postLogoutRedirectUris.ToArray(), - scopes = permissions + Id = applicationId ?? clientIdValue ?? string.Empty, + ClientId = clientIdValue ?? string.Empty, + DisplayName = displayNameValue ?? string.Empty, + RedirectUris = redirectUris.ToArray(), + PostLogoutRedirectUris = postLogoutRedirectUris.ToArray(), + Scopes = permissions .Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope)) - .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length)), - grantTypes = permissions + .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length)) + .ToArray(), + GrantTypes = permissions .Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType)) - .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length)), - clientType = clientType?.ToString(), - consentType = consentType?.ToString(), - status = "active" + .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length)) + .ToArray(), + ClientType = clientType?.ToString(), + ConsentType = consentType?.ToString(), + Status = "active" }); } var sortedClients = clientList - .OrderByDescending(c => (c as dynamic).clientId) + .OrderByDescending(c => c.ClientId) .Skip((page - 1) * pageSize) .Take(pageSize) .ToList(); @@ -112,7 +88,7 @@ public class OAuthClientService : IOAuthClientService return (sortedClients, clientList.Count); } - public async Task GetClientAsync(string id) + public async Task GetClientAsync(string id) { var application = await _applicationManager.FindByIdAsync(id); if (application == null) @@ -128,26 +104,28 @@ public class OAuthClientService : IOAuthClientService var redirectUris = await _applicationManager.GetRedirectUrisAsync(application); var postLogoutRedirectUris = await _applicationManager.GetPostLogoutRedirectUrisAsync(application); - return new + return new OAuthClientDto { - id = id, - clientId = clientIdValue, - displayName = displayNameValue, - redirectUris = redirectUris.ToArray(), - postLogoutRedirectUris = postLogoutRedirectUris.ToArray(), - scopes = permissions + Id = id, + ClientId = clientIdValue ?? string.Empty, + DisplayName = displayNameValue ?? string.Empty, + RedirectUris = redirectUris.ToArray(), + PostLogoutRedirectUris = postLogoutRedirectUris.ToArray(), + Scopes = permissions .Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope)) - .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length)), - grantTypes = permissions + .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length)) + .ToArray(), + GrantTypes = permissions .Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType)) - .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length)), - clientType = clientType?.ToString(), - consentType = consentType?.ToString(), - status = "active" + .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length)) + .ToArray(), + ClientType = clientType?.ToString(), + ConsentType = consentType?.ToString(), + Status = "active" }; } - public async Task CreateClientAsync(CreateClientDto dto) + public async Task CreateClientAsync(CreateClientDto dto) { var authServiceUrl = _configuration["AuthService:Url"] ?? "http://localhost:5132"; var token = await GetAuthTokenAsync(); @@ -182,7 +160,7 @@ public class OAuthClientService : IOAuthClientService response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync(); - var result = System.Text.Json.JsonSerializer.Deserialize(content); + var result = System.Text.Json.JsonSerializer.Deserialize(content); _logger.LogInformation("Created OAuth client {ClientId}", dto.ClientId); diff --git a/Services/RoleService.cs b/Services/RoleService.cs index eade92b..fe4412b 100644 --- a/Services/RoleService.cs +++ b/Services/RoleService.cs @@ -1,20 +1,23 @@ -using Fengling.AuthService.Data; -using Fengling.AuthService.Models; using Fengling.Console.Models.Dtos; using Fengling.Console.Repositories; using Microsoft.AspNetCore.Identity; using System.Security.Claims; +using Fengling.Console.Datas; +using Fengling.Console.Models.Entities; namespace Fengling.Console.Services; public interface IRoleService { - Task<(IEnumerable Items, int TotalCount)> GetRolesAsync(int page, int pageSize, string? name = null, string? tenantId = null); + Task<(IEnumerable Items, int TotalCount)> GetRolesAsync(int page, int pageSize, string? name = null, + string? tenantId = null); + Task GetRoleAsync(long id); Task> GetRoleUsersAsync(long id); Task CreateRoleAsync(CreateRoleDto dto); Task UpdateRoleAsync(long id, UpdateRoleDto dto); Task DeleteRoleAsync(long id); + Task AddUserToRoleAsync(long roleId, long userId); Task RemoveUserFromRoleAsync(long roleId, long userId); } @@ -40,7 +43,8 @@ public class RoleService : IRoleService _httpContextAccessor = httpContextAccessor; } - public async Task<(IEnumerable Items, int TotalCount)> GetRolesAsync(int page, int pageSize, string? name = null, string? tenantId = null) + public async Task<(IEnumerable Items, int TotalCount)> GetRolesAsync(int page, int pageSize, + string? name = null, string? tenantId = null) { var roles = await _repository.GetPagedAsync(page, pageSize, name, tenantId); var totalCount = await _repository.CountAsync(name, tenantId); @@ -136,7 +140,8 @@ public class RoleService : IRoleService throw new InvalidOperationException(string.Join(", ", result.Errors.Select(e => e.Description))); } - await CreateAuditLog("role", "create", "Role", role.Id, role.DisplayName, null, System.Text.Json.JsonSerializer.Serialize(dto)); + await CreateAuditLog("role", "create", "Role", role.Id, role.DisplayName, null, + System.Text.Json.JsonSerializer.Serialize(dto)); return new RoleDto { @@ -173,7 +178,8 @@ public class RoleService : IRoleService await _repository.UpdateAsync(role); - await CreateAuditLog("role", "update", "Role", role.Id, role.DisplayName, oldValue, System.Text.Json.JsonSerializer.Serialize(role)); + await CreateAuditLog("role", "update", "Role", role.Id, role.DisplayName, oldValue, + System.Text.Json.JsonSerializer.Serialize(role)); var users = await _userManager.GetUsersInRoleAsync(role.Name!); return new RoleDto @@ -216,6 +222,29 @@ public class RoleService : IRoleService await CreateAuditLog("role", "delete", "Role", role.Id, role.DisplayName, oldValue); } + public async Task AddUserToRoleAsync(long roleId, long userId) + { + var role = await _repository.GetByIdAsync(roleId); + if (role == null) + { + throw new KeyNotFoundException($"Role with ID {roleId} not found"); + } + + var user = await _userManager.FindByIdAsync(userId.ToString()); + if (user == null) + { + throw new KeyNotFoundException($"User with ID {userId} not found"); + } + + var result = await _userManager.AddToRoleAsync(user, role.Name!); + if (!result.Succeeded) + { + throw new InvalidOperationException(string.Join(", ", result.Errors.Select(e => e.Description))); + } + + await CreateAuditLog("role", "update", "UserRole", null, $"{role.Name} - {user.UserName}"); + } + public async Task RemoveUserFromRoleAsync(long roleId, long userId) { var role = await _repository.GetByIdAsync(roleId); @@ -239,10 +268,12 @@ public class RoleService : IRoleService await CreateAuditLog("role", "update", "UserRole", null, $"{role.Name} - {user.UserName}"); } - private async Task CreateAuditLog(string operation, string action, string targetType, long? targetId, string? targetName, string? oldValue = null, string? newValue = null) + private async Task CreateAuditLog(string operation, string action, string targetType, long? targetId, + string? targetName, string? oldValue = null, string? newValue = null) { var httpContext = _httpContextAccessor.HttpContext; - var userName = httpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier) ?? httpContext?.User?.Identity?.Name ?? "system"; + var userName = httpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier) ?? + httpContext?.User?.Identity?.Name ?? "system"; var tenantId = httpContext?.User?.FindFirstValue("TenantId"); var log = new AuditLog @@ -264,4 +295,4 @@ public class RoleService : IRoleService _context.AuditLogs.Add(log); await _context.SaveChangesAsync(); } -} +} \ No newline at end of file diff --git a/Services/TenantService.cs b/Services/TenantService.cs index aa92927..0fe14e0 100644 --- a/Services/TenantService.cs +++ b/Services/TenantService.cs @@ -1,9 +1,9 @@ -using Fengling.AuthService.Data; -using Fengling.AuthService.Models; using Fengling.Console.Models.Dtos; using Fengling.Console.Repositories; using Microsoft.AspNetCore.Identity; using System.Security.Claims; +using Fengling.Console.Datas; +using Fengling.Console.Models.Entities; namespace Fengling.Console.Services; @@ -13,47 +13,31 @@ public interface ITenantService Task GetTenantAsync(long id); Task> GetTenantUsersAsync(long tenantId); Task> GetTenantRolesAsync(long tenantId); - Task GetTenantSettingsAsync(string tenantId); - Task UpdateTenantSettingsAsync(string tenantId, TenantSettingsDto settings); + Task GetTenantSettingsAsync(long id); + Task UpdateTenantSettingsAsync(long id, TenantSettingsDto settings); Task CreateTenantAsync(CreateTenantDto dto); Task UpdateTenantAsync(long id, UpdateTenantDto dto); Task DeleteTenantAsync(long id); } -public class TenantService : ITenantService +public class TenantService( + ITenantRepository repository, + IUserRepository userRepository, + IRoleRepository roleRepository, + UserManager userManager, + ApplicationDbContext context, + IHttpContextAccessor httpContextAccessor) + : ITenantService { - private readonly ITenantRepository _repository; - private readonly IUserRepository _userRepository; - private readonly IRoleRepository _roleRepository; - private readonly UserManager _userManager; - private readonly ApplicationDbContext _context; - private readonly IHttpContextAccessor _httpContextAccessor; - - public TenantService( - ITenantRepository repository, - IUserRepository userRepository, - IRoleRepository roleRepository, - UserManager userManager, - ApplicationDbContext context, - IHttpContextAccessor httpContextAccessor) - { - _repository = repository; - _userRepository = userRepository; - _roleRepository = roleRepository; - _userManager = userManager; - _context = context; - _httpContextAccessor = httpContextAccessor; - } - public async Task<(IEnumerable Items, int TotalCount)> GetTenantsAsync(int page, int pageSize, string? name = null, string? tenantId = null, string? status = null) { - var tenants = await _repository.GetPagedAsync(page, pageSize, name, tenantId, status); - var totalCount = await _repository.CountAsync(name, tenantId, status); + var tenants = await repository.GetPagedAsync(page, pageSize, name, tenantId, status); + var totalCount = await repository.CountAsync(name, tenantId, status); var tenantDtos = new List(); foreach (var tenant in tenants) { - var userCount = await _repository.GetUserCountAsync(tenant.Id); + var userCount = await repository.GetUserCountAsync(tenant.Id); tenantDtos.Add(new TenantDto { Id = tenant.Id, @@ -76,7 +60,7 @@ public class TenantService : ITenantService public async Task GetTenantAsync(long id) { - var tenant = await _repository.GetByIdAsync(id); + var tenant = await repository.GetByIdAsync(id); if (tenant == null) return null; return new TenantDto @@ -88,7 +72,7 @@ public class TenantService : ITenantService ContactEmail = tenant.ContactEmail, ContactPhone = tenant.ContactPhone, MaxUsers = tenant.MaxUsers, - UserCount = await _repository.GetUserCountAsync(tenant.Id), + UserCount = await repository.GetUserCountAsync(tenant.Id), Status = tenant.Status, ExpiresAt = tenant.ExpiresAt, Description = tenant.Description, @@ -98,18 +82,18 @@ public class TenantService : ITenantService public async Task> GetTenantUsersAsync(long tenantId) { - var tenant = await _repository.GetByIdAsync(tenantId); + var tenant = await repository.GetByIdAsync(tenantId); if (tenant == null) { throw new KeyNotFoundException($"Tenant with ID {tenantId} not found"); } - var users = await _userRepository.GetPagedAsync(1, int.MaxValue, null, null, tenantId.ToString()); + var users = await userRepository.GetPagedAsync(1, int.MaxValue, null, null, tenantId.ToString()); var userDtos = new List(); foreach (var user in users) { - var roles = await _userManager.GetRolesAsync(user); + var roles = await userManager.GetRolesAsync(user); userDtos.Add(new UserDto { Id = user.Id, @@ -130,13 +114,13 @@ public class TenantService : ITenantService public async Task> GetTenantRolesAsync(long tenantId) { - var tenant = await _repository.GetByIdAsync(tenantId); + var tenant = await repository.GetByIdAsync(tenantId); if (tenant == null) { throw new KeyNotFoundException($"Tenant with ID {tenantId} not found"); } - var roles = await _roleRepository.GetPagedAsync(1, int.MaxValue, null, tenantId.ToString()); + var roles = await roleRepository.GetPagedAsync(1, int.MaxValue, null, tenantId.ToString()); return roles.Select(r => new { id = r.Id, @@ -145,12 +129,12 @@ public class TenantService : ITenantService }); } - public async Task GetTenantSettingsAsync(string tenantId) + public async Task GetTenantSettingsAsync(long id) { - var tenant = await _repository.GetByTenantIdAsync(tenantId); + var tenant = await repository.GetByIdAsync(id); if (tenant == null) { - throw new KeyNotFoundException($"Tenant with tenantId '{tenantId}' not found"); + throw new KeyNotFoundException($"Tenant with ID {id} not found"); } return new TenantSettingsDto @@ -164,12 +148,12 @@ public class TenantService : ITenantService }; } - public async Task UpdateTenantSettingsAsync(string tenantId, TenantSettingsDto settings) + public async Task UpdateTenantSettingsAsync(long id, TenantSettingsDto settings) { - var tenant = await _repository.GetByTenantIdAsync(tenantId); + var tenant = await repository.GetByIdAsync(id); if (tenant == null) { - throw new KeyNotFoundException($"Tenant with tenantId '{tenantId}' not found"); + throw new KeyNotFoundException($"Tenant with ID {id} not found"); } await CreateAuditLog("tenant", "update", "TenantSettings", tenant.Id, tenant.TenantId, null, System.Text.Json.JsonSerializer.Serialize(settings)); @@ -191,7 +175,7 @@ public class TenantService : ITenantService CreatedAt = DateTime.UtcNow }; - await _repository.AddAsync(tenant); + await repository.AddAsync(tenant); await CreateAuditLog("tenant", "create", "Tenant", tenant.Id, tenant.TenantId, null, System.Text.Json.JsonSerializer.Serialize(dto)); @@ -214,7 +198,7 @@ public class TenantService : ITenantService public async Task UpdateTenantAsync(long id, UpdateTenantDto dto) { - var tenant = await _repository.GetByIdAsync(id); + var tenant = await repository.GetByIdAsync(id); if (tenant == null) { throw new KeyNotFoundException($"Tenant with ID {id} not found"); @@ -232,7 +216,7 @@ public class TenantService : ITenantService tenant.ExpiresAt = dto.ExpiresAt; tenant.UpdatedAt = DateTime.UtcNow; - await _repository.UpdateAsync(tenant); + await repository.UpdateAsync(tenant); await CreateAuditLog("tenant", "update", "Tenant", tenant.Id, tenant.TenantId, oldValue, System.Text.Json.JsonSerializer.Serialize(tenant)); @@ -245,7 +229,7 @@ public class TenantService : ITenantService ContactEmail = tenant.ContactEmail, ContactPhone = tenant.ContactPhone, MaxUsers = tenant.MaxUsers, - UserCount = await _repository.GetUserCountAsync(tenant.Id), + UserCount = await repository.GetUserCountAsync(tenant.Id), Status = tenant.Status, ExpiresAt = tenant.ExpiresAt, Description = tenant.Description, @@ -255,7 +239,7 @@ public class TenantService : ITenantService public async Task DeleteTenantAsync(long id) { - var tenant = await _repository.GetByIdAsync(id); + var tenant = await repository.GetByIdAsync(id); if (tenant == null) { throw new KeyNotFoundException($"Tenant with ID {id} not found"); @@ -263,23 +247,23 @@ public class TenantService : ITenantService var oldValue = System.Text.Json.JsonSerializer.Serialize(tenant); - var users = await _userRepository.GetPagedAsync(1, int.MaxValue, null, null, id.ToString()); + var users = await userRepository.GetPagedAsync(1, int.MaxValue, null, null, id.ToString()); foreach (var user in users) { user.IsDeleted = true; user.UpdatedTime = DateTime.UtcNow; - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(); } tenant.IsDeleted = true; - await _repository.UpdateAsync(tenant); + await repository.UpdateAsync(tenant); await CreateAuditLog("tenant", "delete", "Tenant", tenant.Id, tenant.TenantId, oldValue); } private async Task CreateAuditLog(string operation, string action, string targetType, long? targetId, string? targetName, string? oldValue = null, string? newValue = null) { - var httpContext = _httpContextAccessor.HttpContext; + var httpContext = httpContextAccessor.HttpContext; var userName = httpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier) ?? httpContext?.User?.Identity?.Name ?? "system"; var tenantId = httpContext?.User?.FindFirstValue("TenantId"); @@ -299,7 +283,7 @@ public class TenantService : ITenantService CreatedAt = DateTime.UtcNow }; - _context.AuditLogs.Add(log); - await _context.SaveChangesAsync(); + context.AuditLogs.Add(log); + await context.SaveChangesAsync(); } } diff --git a/Services/UserService.cs b/Services/UserService.cs index e07fc85..0f17713 100644 --- a/Services/UserService.cs +++ b/Services/UserService.cs @@ -1,9 +1,9 @@ -using Fengling.AuthService.Data; -using Fengling.AuthService.Models; using Fengling.Console.Models.Dtos; using Fengling.Console.Repositories; using Microsoft.AspNetCore.Identity; using System.Security.Claims; +using Fengling.Console.Datas; +using Fengling.Console.Models.Entities; namespace Fengling.Console.Services; @@ -17,40 +17,24 @@ public interface IUserService Task DeleteUserAsync(long id); } -public class UserService : IUserService +public class UserService( + IUserRepository repository, + ITenantRepository tenantRepository, + UserManager userManager, + RoleManager roleManager, + ApplicationDbContext context, + IHttpContextAccessor httpContextAccessor) + : IUserService { - private readonly IUserRepository _repository; - private readonly ITenantRepository _tenantRepository; - private readonly UserManager _userManager; - private readonly RoleManager _roleManager; - private readonly ApplicationDbContext _context; - private readonly IHttpContextAccessor _httpContextAccessor; - - public UserService( - IUserRepository repository, - ITenantRepository tenantRepository, - UserManager userManager, - RoleManager roleManager, - ApplicationDbContext context, - IHttpContextAccessor httpContextAccessor) - { - _repository = repository; - _tenantRepository = tenantRepository; - _userManager = userManager; - _roleManager = roleManager; - _context = context; - _httpContextAccessor = httpContextAccessor; - } - public async Task<(IEnumerable Items, int TotalCount)> GetUsersAsync(int page, int pageSize, string? userName = null, string? email = null, string? tenantId = null) { - var users = await _repository.GetPagedAsync(page, pageSize, userName, email, tenantId); - var totalCount = await _repository.CountAsync(userName, email, tenantId); + var users = await repository.GetPagedAsync(page, pageSize, userName, email, tenantId); + var totalCount = await repository.CountAsync(userName, email, tenantId); var userDtos = new List(); foreach (var user in users) { - var roles = await _userManager.GetRolesAsync(user); + var roles = await userManager.GetRolesAsync(user); userDtos.Add(new UserDto { Id = user.Id, @@ -72,10 +56,10 @@ public class UserService : IUserService public async Task GetUserAsync(long id) { - var user = await _repository.GetByIdAsync(id); + var user = await repository.GetByIdAsync(id); if (user == null) return null; - var roles = await _userManager.GetRolesAsync(user); + var roles = await userManager.GetRolesAsync(user); return new UserDto { Id = user.Id, @@ -99,7 +83,7 @@ public class UserService : IUserService if (tenantId != 0) { - tenant = await _tenantRepository.GetByIdAsync(tenantId); + tenant = await tenantRepository.GetByIdAsync(tenantId); if (tenant == null) { throw new InvalidOperationException("Invalid tenant ID"); @@ -117,7 +101,7 @@ public class UserService : IUserService CreatedTime = DateTime.UtcNow }; - var result = await _userManager.CreateAsync(user, dto.Password); + var result = await userManager.CreateAsync(user, dto.Password); if (!result.Succeeded) { throw new InvalidOperationException(string.Join(", ", result.Errors.Select(e => e.Description))); @@ -127,23 +111,23 @@ public class UserService : IUserService { foreach (var roleId in dto.RoleIds) { - var role = await _roleManager.FindByIdAsync(roleId.ToString()); + var role = await roleManager.FindByIdAsync(roleId.ToString()); if (role != null) { - await _userManager.AddToRoleAsync(user, role.Name!); + await userManager.AddToRoleAsync(user, role.Name!); } } } if (!dto.IsActive) { - await _userManager.SetLockoutEnabledAsync(user, true); - await _userManager.SetLockoutEndDateAsync(user, DateTimeOffset.MaxValue); + await userManager.SetLockoutEnabledAsync(user, true); + await userManager.SetLockoutEndDateAsync(user, DateTimeOffset.MaxValue); } await CreateAuditLog("user", "create", "User", user.Id, user.UserName, null, System.Text.Json.JsonSerializer.Serialize(dto)); - var roles = await _userManager.GetRolesAsync(user); + var roles = await userManager.GetRolesAsync(user); return new UserDto { Id = user.Id, @@ -162,7 +146,7 @@ public class UserService : IUserService public async Task UpdateUserAsync(long id, UpdateUserDto dto) { - var user = await _repository.GetByIdAsync(id); + var user = await repository.GetByIdAsync(id); if (user == null) { throw new KeyNotFoundException($"User with ID {id} not found"); @@ -178,20 +162,20 @@ public class UserService : IUserService if (dto.IsActive) { - await _userManager.SetLockoutEnabledAsync(user, false); - await _userManager.SetLockoutEndDateAsync(user, null); + await userManager.SetLockoutEnabledAsync(user, false); + await userManager.SetLockoutEndDateAsync(user, null); } else { - await _userManager.SetLockoutEnabledAsync(user, true); - await _userManager.SetLockoutEndDateAsync(user, DateTimeOffset.MaxValue); + await userManager.SetLockoutEnabledAsync(user, true); + await userManager.SetLockoutEndDateAsync(user, DateTimeOffset.MaxValue); } - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(); await CreateAuditLog("user", "update", "User", user.Id, user.UserName, oldValue, System.Text.Json.JsonSerializer.Serialize(user)); - var roles = await _userManager.GetRolesAsync(user); + var roles = await userManager.GetRolesAsync(user); return new UserDto { Id = user.Id, @@ -210,14 +194,14 @@ public class UserService : IUserService public async Task ResetPasswordAsync(long id, ResetPasswordDto dto) { - var user = await _userManager.FindByIdAsync(id.ToString()); + var user = await userManager.FindByIdAsync(id.ToString()); if (user == null) { throw new KeyNotFoundException($"User with ID {id} not found"); } - var token = await _userManager.GeneratePasswordResetTokenAsync(user); - var result = await _userManager.ResetPasswordAsync(user, token, dto.NewPassword); + var token = await userManager.GeneratePasswordResetTokenAsync(user); + var result = await userManager.ResetPasswordAsync(user, token, dto.NewPassword); if (!result.Succeeded) { @@ -229,7 +213,7 @@ public class UserService : IUserService public async Task DeleteUserAsync(long id) { - var user = await _repository.GetByIdAsync(id); + var user = await repository.GetByIdAsync(id); if (user == null) { throw new KeyNotFoundException($"User with ID {id} not found"); @@ -238,14 +222,14 @@ public class UserService : IUserService var oldValue = System.Text.Json.JsonSerializer.Serialize(user); user.IsDeleted = true; user.UpdatedTime = DateTime.UtcNow; - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(); await CreateAuditLog("user", "delete", "User", user.Id, user.UserName, oldValue); } private async Task CreateAuditLog(string operation, string action, string targetType, long? targetId, string? targetName, string? oldValue = null, string? newValue = null) { - var httpContext = _httpContextAccessor.HttpContext; + var httpContext = httpContextAccessor.HttpContext; var userName = httpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier) ?? httpContext?.User?.Identity?.Name ?? "system"; var tenantId = httpContext?.User?.FindFirstValue("TenantId"); @@ -265,7 +249,7 @@ public class UserService : IUserService CreatedAt = DateTime.UtcNow }; - _context.AuditLogs.Add(log); - await _context.SaveChangesAsync(); + context.AuditLogs.Add(log); + await context.SaveChangesAsync(); } } diff --git a/appsettings.Development.json b/appsettings.Development.json index 0c208ae..bee822d 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -2,7 +2,7 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft.AspNetCore": "Debug" } } } diff --git a/appsettings.json b/appsettings.json index 10f68b8..eb3c667 100644 --- a/appsettings.json +++ b/appsettings.json @@ -5,5 +5,9 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Host=192.168.100.10;Port=5432;Database=fengling_auth;Username=movingsam;Password=sl52788542", + "GatewayConnection" : "Host=192.168.100.10;Port=5432;Database=fengling_gateway;Username=movingsam;Password=sl52788542" + } }