feat: 添加Console API认证和OpenIddict集成

- 配置AuthService使用OpenIddict reference tokens
- 添加fengling-api客户端用于introspection验证
- 配置Console API通过OpenIddict验证reference tokens
- 实现Tenant/Users/Roles/OAuthClients CRUD API
- 添加GatewayController服务注册API
- 重构Repository和Service层支持多租户

BREAKING CHANGE: API认证现在使用OpenIddict reference tokens
This commit is contained in:
Sam 2026-02-08 19:01:25 +08:00
parent 61c3a27192
commit c8cb7c06bc
37 changed files with 1965 additions and 330 deletions

View File

@ -0,0 +1,361 @@
namespace Fengling.Console.Controllers;
/// <summary>
/// 网关管理控制器
/// 提供网关服务、路由、集群实例等配置管理功能
/// </summary>
[ApiController]
[Route("api/console/[controller]")]
[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
public class GatewayController(IGatewayService gatewayService, ILogger<GatewayController> logger)
: ControllerBase
{
/// <summary>
/// 获取网关统计数据
/// </summary>
/// <returns>网关的整体统计信息,包括请求量、响应时间等指标</returns>
/// <response code="200">成功返回网关统计数据</response>
/// <response code="500">服务器内部错误</response>
[HttpGet("statistics")]
[Produces("application/json")]
[ProducesResponseType(typeof(GatewayStatisticsDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<GatewayStatisticsDto>> 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 });
}
}
/// <summary>
/// 获取网关服务列表
/// </summary>
/// <param name="globalOnly">是否只返回全局服务默认为false</param>
/// <param name="tenantCode">租户编码,用于筛选特定租户的服务</param>
/// <returns>网关服务列表,包含服务名称、地址、健康状态等信息</returns>
/// <response code="200">成功返回网关服务列表</response>
/// <response code="500">服务器内部错误</response>
[HttpGet("services")]
[Produces("application/json")]
[ProducesResponseType(typeof(List<GatewayServiceDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<List<GatewayServiceDto>>> 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 });
}
}
/// <summary>
/// 获取单个网关服务详情
/// </summary>
/// <param name="serviceName">服务名称</param>
/// <param name="tenantCode">租户编码,用于筛选特定租户的服务</param>
/// <returns>网关服务的详细信息</returns>
/// <response code="200">成功返回服务详情</response>
/// <response code="404">服务不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpGet("services/{serviceName}")]
[Produces("application/json")]
[ProducesResponseType(typeof(GatewayServiceDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<GatewayServiceDto>> 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 });
}
}
/// <summary>
/// 注册新的网关服务
/// </summary>
/// <param name="dto">创建服务所需的配置信息</param>
/// <returns>创建的服务详情</returns>
/// <response code="201">成功创建服务</response>
/// <response code="400">请求参数无效或服务已存在</response>
/// <response code="500">服务器内部错误</response>
[HttpPost("services")]
[Produces("application/json")]
[ProducesResponseType(typeof(GatewayServiceDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<GatewayServiceDto>> 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 });
}
}
/// <summary>
/// 注销网关服务
/// </summary>
/// <param name="serviceName">要注销的服务名称</param>
/// <param name="tenantCode">租户编码,用于筛选特定租户的服务</param>
/// <returns>无内容响应</returns>
/// <response code="200">成功注销服务</response>
/// <response code="404">服务不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpDelete("services/{serviceName}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult> 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 });
}
}
/// <summary>
/// 获取网关路由列表
/// </summary>
/// <param name="globalOnly">是否只返回全局路由默认为false</param>
/// <returns>网关路由列表,包含路径匹配规则、转发目标等信息</returns>
/// <response code="200">成功返回网关路由列表</response>
/// <response code="500">服务器内部错误</response>
[HttpGet("routes")]
[Produces("application/json")]
[ProducesResponseType(typeof(List<GatewayRouteDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<List<GatewayRouteDto>>> 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 });
}
}
/// <summary>
/// 创建新的网关路由
/// </summary>
/// <param name="dto">创建路由所需的配置信息</param>
/// <returns>创建的路由详情</returns>
/// <response code="201">成功创建路由</response>
/// <response code="400">请求参数无效</response>
/// <response code="500">服务器内部错误</response>
[HttpPost("routes")]
[Produces("application/json")]
[ProducesResponseType(typeof(GatewayRouteDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<GatewayRouteDto>> 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 });
}
}
/// <summary>
/// 获取集群实例列表
/// </summary>
/// <param name="clusterId">集群ID</param>
/// <returns>指定集群下的所有服务实例列表</returns>
/// <response code="200">成功返回实例列表</response>
/// <response code="500">服务器内部错误</response>
[HttpGet("clusters/{clusterId}/instances")]
[Produces("application/json")]
[ProducesResponseType(typeof(List<GatewayInstanceDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<List<GatewayInstanceDto>>> 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 });
}
}
/// <summary>
/// 添加服务实例到集群
/// </summary>
/// <param name="dto">创建实例所需的配置信息</param>
/// <returns>创建的实例详情</returns>
/// <response code="201">成功添加实例</response>
/// <response code="400">请求参数无效</response>
/// <response code="500">服务器内部错误</response>
[HttpPost("instances")]
[Produces("application/json")]
[ProducesResponseType(typeof(GatewayInstanceDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<GatewayInstanceDto>> 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 });
}
}
/// <summary>
/// 移除服务实例
/// </summary>
/// <param name="instanceId">实例ID</param>
/// <returns>无内容响应</returns>
/// <response code="200">成功移除实例</response>
/// <response code="404">实例不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpDelete("instances/{instanceId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult> 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 });
}
}
/// <summary>
/// 更新实例权重
/// 用于负载均衡策略中调整实例的请求分发权重
/// </summary>
/// <param name="instanceId">实例ID</param>
/// <param name="dto">包含新权重值的请求体</param>
/// <returns>无内容响应</returns>
/// <response code="200">成功更新权重</response>
/// <response code="404">实例不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpPut("instances/{instanceId}/weight")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult> 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 });
}
}
/// <summary>
/// 重新加载网关配置
/// 触发网关重新加载所有配置,包括路由、服务、集群等配置
/// </summary>
/// <returns>无内容响应</returns>
/// <response code="200">成功重新加载配置</response>
/// <response code="500">服务器内部错误</response>
[HttpPost("reload")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult> 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 });
}
}
}

View File

@ -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;

View File

@ -1,12 +1,11 @@
using Fengling.Console.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Fengling.Console.Controllers; namespace Fengling.Console.Controllers;
/// <summary>
/// OAuth客户端管理控制器
/// </summary>
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/console/[controller]")]
[Authorize] [Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
public class OAuthClientsController : ControllerBase public class OAuthClientsController : ControllerBase
{ {
private readonly IOAuthClientService _service; private readonly IOAuthClientService _service;
@ -20,25 +19,31 @@ public class OAuthClientsController : ControllerBase
_logger = logger; _logger = logger;
} }
/// <summary>
/// 获取OAuth客户端列表
/// </summary>
/// <param name="query">分页查询参数支持按显示名称、客户端ID和状态筛选</param>
/// <returns>分页的OAuth客户端列表包含总数量、分页信息和客户端详情</returns>
/// <response code="200">成功返回OAuth客户端分页列表</response>
/// <response code="500">服务器内部错误</response>
[HttpGet] [HttpGet]
public async Task<ActionResult<object>> GetClients( [Produces("application/json")]
[FromQuery] int page = 1, [ProducesResponseType(typeof(OAuthClientListDto), StatusCodes.Status200OK)]
[FromQuery] int pageSize = 10, [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
[FromQuery] string? displayName = null, public async Task<ActionResult<OAuthClientListDto>> GetClients([FromQuery] OAuthClientQueryDto query)
[FromQuery] string? clientId = null,
[FromQuery] string? status = null)
{ {
try 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, Items = items.ToList(),
totalCount, TotalCount = totalCount,
page, Page = query.Page,
pageSize PageSize = query.PageSize
}); };
return Ok(result);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -47,14 +52,33 @@ public class OAuthClientsController : ControllerBase
} }
} }
/// <summary>
/// 获取OAuth客户端选项
/// </summary>
/// <returns>包含客户端类型、授权类型、授权范围等可选值的配置选项</returns>
/// <response code="200">成功返回客户端配置选项</response>
[HttpGet("options")] [HttpGet("options")]
[Produces("application/json")]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
public ActionResult<object> GetClientOptions() public ActionResult<object> GetClientOptions()
{ {
return Ok(_service.GetClientOptions()); return Ok(_service.GetClientOptions());
} }
/// <summary>
/// 获取单个OAuth客户端详情
/// </summary>
/// <param name="id">客户端唯一标识符</param>
/// <returns>OAuth客户端的完整配置信息</returns>
/// <response code="200">成功返回客户端详情</response>
/// <response code="404">客户端不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<ActionResult<object>> GetClient(string id) [Produces("application/json")]
[ProducesResponseType(typeof(OAuthClientDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<OAuthClientDto>> GetClient(string id)
{ {
try try
{ {
@ -73,8 +97,20 @@ public class OAuthClientsController : ControllerBase
} }
} }
/// <summary>
/// 创建新的OAuth客户端
/// </summary>
/// <param name="dto">创建客户端所需的配置信息</param>
/// <returns>创建的OAuth客户端详情</returns>
/// <response code="201">成功创建客户端</response>
/// <response code="400">请求参数无效或客户端ID已存在</response>
/// <response code="500">服务器内部错误</response>
[HttpPost] [HttpPost]
public async Task<ActionResult<object>> 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<ActionResult<OAuthClientDto>> CreateClient([FromBody] CreateClientDto dto)
{ {
try try
{ {
@ -92,7 +128,19 @@ public class OAuthClientsController : ControllerBase
} }
} }
/// <summary>
/// 为指定客户端生成新的密钥
/// </summary>
/// <param name="id">客户端唯一标识符</param>
/// <returns>包含新生成的客户端密钥信息</returns>
/// <response code="200">成功生成新密钥</response>
/// <response code="404">客户端不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpPost("{id}/generate-secret")] [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<ActionResult> GenerateSecret(string id) public async Task<ActionResult> GenerateSecret(string id)
{ {
try try
@ -111,7 +159,18 @@ public class OAuthClientsController : ControllerBase
} }
} }
/// <summary>
/// 删除指定的OAuth客户端
/// </summary>
/// <param name="id">客户端唯一标识符</param>
/// <returns>无内容响应</returns>
/// <response code="204">成功删除客户端</response>
/// <response code="404">客户端不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpDelete("{id}")] [HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> DeleteClient(string id) public async Task<IActionResult> DeleteClient(string id)
{ {
try try
@ -130,7 +189,19 @@ public class OAuthClientsController : ControllerBase
} }
} }
/// <summary>
/// 更新指定的OAuth客户端
/// </summary>
/// <param name="id">客户端唯一标识符</param>
/// <param name="dto">需要更新的客户端配置信息</param>
/// <returns>无内容响应</returns>
/// <response code="204">成功更新客户端</response>
/// <response code="404">客户端不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpPut("{id}")] [HttpPut("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> UpdateClient(string id, [FromBody] UpdateClientDto dto) public async Task<IActionResult> UpdateClient(string id, [FromBody] UpdateClientDto dto)
{ {
try try

View File

@ -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; namespace Fengling.Console.Controllers;
/// <summary>
/// 角色管理控制器
/// 提供角色的增删改查以及用户角色关联管理功能
/// </summary>
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/console/[controller]")]
[Authorize] [Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
public class RolesController : ControllerBase public class RolesController : ControllerBase
{ {
private readonly IRoleService _roleService; private readonly IRoleService _roleService;
@ -19,17 +18,30 @@ public class RolesController : ControllerBase
_logger = logger; _logger = logger;
} }
/// <summary>
/// 获取角色列表
/// </summary>
/// <param name="query">分页查询参数支持按名称和租户ID筛选</param>
/// <returns>分页的角色列表,包含角色基本信息和关联统计</returns>
/// <response code="200">成功返回角色分页列表</response>
/// <response code="500">服务器内部错误</response>
[HttpGet] [HttpGet]
public async Task<ActionResult<object>> GetRoles( [Produces("application/json")]
[FromQuery] int page = 1, [ProducesResponseType(typeof(PagedResultDto<RoleDto>), StatusCodes.Status200OK)]
[FromQuery] int pageSize = 10, [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
[FromQuery] string? name = null, public async Task<ActionResult<PagedResultDto<RoleDto>>> GetRoles([FromQuery] RoleQueryDto query)
[FromQuery] string? tenantId = null)
{ {
try try
{ {
var (items, totalCount) = await _roleService.GetRolesAsync(page, pageSize, name, tenantId); var (items, totalCount) = await _roleService.GetRolesAsync(query.Page, query.PageSize, query.Name, query.TenantId);
return Ok(new { items, totalCount, page, pageSize }); var result = new PagedResultDto<RoleDto>
{
Items = items.ToList(),
TotalCount = totalCount,
Page = query.Page,
PageSize = query.PageSize
};
return Ok(result);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -38,7 +50,19 @@ public class RolesController : ControllerBase
} }
} }
/// <summary>
/// 获取单个角色详情
/// </summary>
/// <param name="id">角色ID</param>
/// <returns>角色的详细信息,包括权限配置等</returns>
/// <response code="200">成功返回角色详情</response>
/// <response code="404">角色不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpGet("{id}")] [HttpGet("{id}")]
[Produces("application/json")]
[ProducesResponseType(typeof(RoleDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<RoleDto>> GetRole(long id) public async Task<ActionResult<RoleDto>> GetRole(long id)
{ {
try try
@ -57,7 +81,19 @@ public class RolesController : ControllerBase
} }
} }
/// <summary>
/// 获取指定角色的用户列表
/// </summary>
/// <param name="id">角色ID</param>
/// <returns>属于该角色的所有用户列表</returns>
/// <response code="200">成功返回用户列表</response>
/// <response code="404">角色不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpGet("{id}/users")] [HttpGet("{id}/users")]
[Produces("application/json")]
[ProducesResponseType(typeof(IEnumerable<UserDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<IEnumerable<UserDto>>> GetRoleUsers(long id) public async Task<ActionResult<IEnumerable<UserDto>>> GetRoleUsers(long id)
{ {
try try
@ -77,7 +113,19 @@ public class RolesController : ControllerBase
} }
} }
/// <summary>
/// 创建新角色
/// </summary>
/// <param name="dto">创建角色所需的配置信息</param>
/// <returns>创建的角色详情</returns>
/// <response code="201">成功创建角色</response>
/// <response code="400">请求参数无效或角色名称已存在</response>
/// <response code="500">服务器内部错误</response>
[HttpPost] [HttpPost]
[Produces("application/json")]
[ProducesResponseType(typeof(RoleDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<RoleDto>> CreateRole([FromBody] CreateRoleDto dto) public async Task<ActionResult<RoleDto>> CreateRole([FromBody] CreateRoleDto dto)
{ {
try try
@ -97,7 +145,21 @@ public class RolesController : ControllerBase
} }
} }
/// <summary>
/// 更新角色信息
/// </summary>
/// <param name="id">角色ID</param>
/// <param name="dto">需要更新的角色配置信息</param>
/// <returns>无内容响应</returns>
/// <response code="204">成功更新角色</response>
/// <response code="404">角色不存在</response>
/// <response code="400">请求参数无效</response>
/// <response code="500">服务器内部错误</response>
[HttpPut("{id}")] [HttpPut("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> UpdateRole(long id, [FromBody] UpdateRoleDto dto) public async Task<IActionResult> UpdateRole(long id, [FromBody] UpdateRoleDto dto)
{ {
try try
@ -122,7 +184,20 @@ public class RolesController : ControllerBase
} }
} }
/// <summary>
/// 删除角色
/// </summary>
/// <param name="id">角色ID</param>
/// <returns>无内容响应</returns>
/// <response code="204">成功删除角色</response>
/// <response code="404">角色不存在</response>
/// <response code="400">请求参数无效(如角色下有关联用户)</response>
/// <response code="500">服务器内部错误</response>
[HttpDelete("{id}")] [HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> DeleteRole(long id) public async Task<IActionResult> DeleteRole(long id)
{ {
try try
@ -147,7 +222,60 @@ public class RolesController : ControllerBase
} }
} }
/// <summary>
/// 将用户添加到角色
/// </summary>
/// <param name="id">角色ID</param>
/// <param name="userId">用户ID</param>
/// <returns>无内容响应</returns>
/// <response code="204">成功添加用户到角色</response>
/// <response code="404">角色或用户不存在</response>
/// <response code="400">请求参数无效或用户已在角色中</response>
/// <response code="500">服务器内部错误</response>
[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<IActionResult> 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 });
}
}
/// <summary>
/// 将用户从角色中移除
/// </summary>
/// <param name="id">角色ID</param>
/// <param name="userId">用户ID</param>
/// <returns>无内容响应</returns>
/// <response code="204">成功从角色中移除用户</response>
/// <response code="404">角色或用户不存在</response>
/// <response code="400">请求参数无效</response>
/// <response code="500">服务器内部错误</response>
[HttpDelete("{id}/users/{userId}")] [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<IActionResult> RemoveUserFromRole(long id, long userId) public async Task<IActionResult> RemoveUserFromRole(long id, long userId)
{ {
try try

View File

@ -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; namespace Fengling.Console.Controllers;
/// <summary>
/// 租户管理控制器
/// 提供租户的增删改查以及租户用户、角色、配置管理功能
/// </summary>
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/console/[controller]")]
[Authorize] [Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
public class TenantsController : ControllerBase public class TenantsController : ControllerBase
{ {
private readonly ITenantService _tenantService; private readonly ITenantService _tenantService;
@ -19,18 +18,31 @@ public class TenantsController : ControllerBase
_logger = logger; _logger = logger;
} }
/// <summary>
/// 获取租户列表
/// </summary>
/// <param name="query">分页查询参数,支持按名称、租户编码和状态筛选</param>
/// <returns>分页的租户列表,包含租户基本信息和状态</returns>
/// <response code="200">成功返回租户分页列表</response>
/// <response code="500">服务器内部错误</response>
[HttpGet] [HttpGet]
public async Task<ActionResult<object>> GetTenants( [Produces("application/json")]
[FromQuery] int page = 1, [ProducesResponseType(typeof(PagedResultDto<TenantDto>), StatusCodes.Status200OK)]
[FromQuery] int pageSize = 10, [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
[FromQuery] string? name = null, public async Task<ActionResult<PagedResultDto<TenantDto>>> GetTenants([FromQuery] TenantQueryDto query)
[FromQuery] string? tenantId = null,
[FromQuery] string? status = null)
{ {
try try
{ {
var (items, totalCount) = await _tenantService.GetTenantsAsync(page, pageSize, name, tenantId, status); var (items, totalCount) = await _tenantService.GetTenantsAsync(query.Page, query.PageSize, query.Name,
return Ok(new { items, totalCount, page, pageSize }); query.TenantId, query.Status);
var result = new PagedResultDto<TenantDto>
{
Items = items.ToList(),
TotalCount = totalCount,
Page = query.Page,
PageSize = query.PageSize
};
return Ok(result);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -39,7 +51,19 @@ public class TenantsController : ControllerBase
} }
} }
/// <summary>
/// 获取单个租户详情
/// </summary>
/// <param name="id">租户ID</param>
/// <returns>租户的详细信息,包括配置、限额等信息</returns>
/// <response code="200">成功返回租户详情</response>
/// <response code="404">租户不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpGet("{id}")] [HttpGet("{id}")]
[Produces("application/json")]
[ProducesResponseType(typeof(TenantDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<TenantDto>> GetTenant(long id) public async Task<ActionResult<TenantDto>> GetTenant(long id)
{ {
try try
@ -49,6 +73,7 @@ public class TenantsController : ControllerBase
{ {
return NotFound(); return NotFound();
} }
return Ok(tenant); return Ok(tenant);
} }
catch (Exception ex) catch (Exception ex)
@ -58,7 +83,19 @@ public class TenantsController : ControllerBase
} }
} }
/// <summary>
/// 获取指定租户的用户列表
/// </summary>
/// <param name="tenantId">租户ID</param>
/// <returns>属于该租户的所有用户列表</returns>
/// <response code="200">成功返回用户列表</response>
/// <response code="404">租户不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpGet("{tenantId}/users")] [HttpGet("{tenantId}/users")]
[Produces("application/json")]
[ProducesResponseType(typeof(IEnumerable<UserDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<IEnumerable<UserDto>>> GetTenantUsers(long tenantId) public async Task<ActionResult<IEnumerable<UserDto>>> GetTenantUsers(long tenantId)
{ {
try try
@ -78,7 +115,19 @@ public class TenantsController : ControllerBase
} }
} }
/// <summary>
/// 获取指定租户的角色列表
/// </summary>
/// <param name="tenantId">租户ID</param>
/// <returns>属于该租户的所有角色列表</returns>
/// <response code="200">成功返回角色列表</response>
/// <response code="404">租户不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpGet("{tenantId}/roles")] [HttpGet("{tenantId}/roles")]
[Produces("application/json")]
[ProducesResponseType(typeof(IEnumerable<object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<IEnumerable<object>>> GetTenantRoles(long tenantId) public async Task<ActionResult<IEnumerable<object>>> GetTenantRoles(long tenantId)
{ {
try try
@ -98,47 +147,81 @@ public class TenantsController : ControllerBase
} }
} }
[HttpGet("{tenantId}/settings")] /// <summary>
public async Task<ActionResult<TenantSettingsDto>> GetTenantSettings(string tenantId) /// 获取租户配置信息
/// </summary>
/// <param name="id">租户ID</param>
/// <returns>租户的详细配置信息,包括功能开关、配额限制等</returns>
/// <response code="200">成功返回租户配置</response>
/// <response code="404">租户不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpGet("{id}/settings")]
[Produces("application/json")]
[ProducesResponseType(typeof(TenantSettingsDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<TenantSettingsDto>> GetTenantSettings(long id)
{ {
try try
{ {
var settings = await _tenantService.GetTenantSettingsAsync(tenantId); var settings = await _tenantService.GetTenantSettingsAsync(id);
return Ok(settings); return Ok(settings);
} }
catch (KeyNotFoundException ex) catch (KeyNotFoundException ex)
{ {
_logger.LogWarning(ex, "Tenant not found: {TenantId}", tenantId); _logger.LogWarning(ex, "Tenant not found: {TenantId}", id);
return NotFound(); return NotFound();
} }
catch (Exception ex) 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 }); return StatusCode(500, new { message = ex.Message });
} }
} }
[HttpPut("{tenantId}/settings")] /// <summary>
public async Task<IActionResult> UpdateTenantSettings(string tenantId, [FromBody] TenantSettingsDto settings) /// 更新租户配置
/// </summary>
/// <param name="id">租户ID</param>
/// <param name="settings">需要更新的租户配置信息</param>
/// <returns>无内容响应</returns>
/// <response code="204">成功更新租户配置</response>
/// <response code="404">租户不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpPut("{id}/settings")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> UpdateTenantSettings(long id, [FromBody] TenantSettingsDto settings)
{ {
try try
{ {
await _tenantService.UpdateTenantSettingsAsync(tenantId, settings); await _tenantService.UpdateTenantSettingsAsync(id, settings);
return NoContent(); return NoContent();
} }
catch (KeyNotFoundException ex) catch (KeyNotFoundException ex)
{ {
_logger.LogWarning(ex, "Tenant not found: {TenantId}", tenantId); _logger.LogWarning(ex, "Tenant not found: {TenantId}", id);
return NotFound(); return NotFound();
} }
catch (Exception ex) 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 }); return StatusCode(500, new { message = ex.Message });
} }
} }
/// <summary>
/// 创建新租户
/// </summary>
/// <param name="dto">创建租户所需的配置信息</param>
/// <returns>创建的租户详情</returns>
/// <response code="201">成功创建租户</response>
/// <response code="500">服务器内部错误</response>
[HttpPost] [HttpPost]
[Produces("application/json")]
[ProducesResponseType(typeof(TenantDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<TenantDto>> CreateTenant([FromBody] CreateTenantDto dto) public async Task<ActionResult<TenantDto>> CreateTenant([FromBody] CreateTenantDto dto)
{ {
try try
@ -153,7 +236,19 @@ public class TenantsController : ControllerBase
} }
} }
/// <summary>
/// 更新租户信息
/// </summary>
/// <param name="id">租户ID</param>
/// <param name="dto">需要更新的租户配置信息</param>
/// <returns>无内容响应</returns>
/// <response code="204">成功更新租户</response>
/// <response code="404">租户不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpPut("{id}")] [HttpPut("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> UpdateTenant(long id, [FromBody] UpdateTenantDto dto) public async Task<IActionResult> UpdateTenant(long id, [FromBody] UpdateTenantDto dto)
{ {
try try
@ -173,7 +268,18 @@ public class TenantsController : ControllerBase
} }
} }
/// <summary>
/// 删除租户
/// </summary>
/// <param name="id">租户ID</param>
/// <returns>无内容响应</returns>
/// <response code="204">成功删除租户</response>
/// <response code="404">租户不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpDelete("{id}")] [HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> DeleteTenant(long id) public async Task<IActionResult> DeleteTenant(long id)
{ {
try try
@ -192,4 +298,4 @@ public class TenantsController : ControllerBase
return StatusCode(500, new { message = ex.Message }); return StatusCode(500, new { message = ex.Message });
} }
} }
} }

View File

@ -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; namespace Fengling.Console.Controllers;
/// <summary>
/// 用户管理控制器
/// 提供用户的增删改查以及密码重置等功能
/// </summary>
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/console/[controller]")]
[Authorize] [Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
public class UsersController : ControllerBase public class UsersController : ControllerBase
{ {
private readonly IUserService _userService; private readonly IUserService _userService;
@ -19,18 +20,30 @@ public class UsersController : ControllerBase
_logger = logger; _logger = logger;
} }
/// <summary>
/// 获取用户列表
/// </summary>
/// <param name="query">分页查询参数支持按用户名、邮箱和租户ID筛选</param>
/// <returns>分页的用户列表,包含用户基本信息和状态</returns>
/// <response code="200">成功返回用户分页列表</response>
/// <response code="500">服务器内部错误</response>
[HttpGet] [HttpGet]
public async Task<ActionResult<object>> GetUsers( [Produces("application/json")]
[FromQuery] int page = 1, [ProducesResponseType(typeof(PagedResultDto<UserDto>), StatusCodes.Status200OK)]
[FromQuery] int pageSize = 10, [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
[FromQuery] string? userName = null, public async Task<ActionResult<PagedResultDto<UserDto>>> GetUsers([FromQuery] UserQueryDto query)
[FromQuery] string? email = null,
[FromQuery] string? tenantId = null)
{ {
try try
{ {
var (items, totalCount) = await _userService.GetUsersAsync(page, pageSize, userName, email, tenantId); var (items, totalCount) = await _userService.GetUsersAsync(query.Page, query.PageSize, query.UserName, query.Email, query.TenantId);
return Ok(new { items, totalCount, page, pageSize }); var result = new PagedResultDto<UserDto>
{
Items = items.ToList(),
TotalCount = totalCount,
Page = query.Page,
PageSize = query.PageSize
};
return Ok(result);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -39,7 +52,19 @@ public class UsersController : ControllerBase
} }
} }
/// <summary>
/// 获取单个用户详情
/// </summary>
/// <param name="id">用户ID</param>
/// <returns>用户的详细信息,包括角色、租户等信息</returns>
/// <response code="200">成功返回用户详情</response>
/// <response code="404">用户不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpGet("{id}")] [HttpGet("{id}")]
[Produces("application/json")]
[ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<UserDto>> GetUser(long id) public async Task<ActionResult<UserDto>> GetUser(long id)
{ {
try try
@ -58,7 +83,19 @@ public class UsersController : ControllerBase
} }
} }
/// <summary>
/// 创建新用户
/// </summary>
/// <param name="dto">创建用户所需的配置信息</param>
/// <returns>创建的用户详情</returns>
/// <response code="201">成功创建用户</response>
/// <response code="400">请求参数无效或用户名/邮箱已存在</response>
/// <response code="500">服务器内部错误</response>
[HttpPost] [HttpPost]
[Produces("application/json")]
[ProducesResponseType(typeof(UserDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<UserDto>> CreateUser([FromBody] CreateUserDto dto) public async Task<ActionResult<UserDto>> CreateUser([FromBody] CreateUserDto dto)
{ {
try try
@ -78,7 +115,19 @@ public class UsersController : ControllerBase
} }
} }
/// <summary>
/// 更新用户信息
/// </summary>
/// <param name="id">用户ID</param>
/// <param name="dto">需要更新的用户配置信息</param>
/// <returns>无内容响应</returns>
/// <response code="204">成功更新用户</response>
/// <response code="404">用户不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpPut("{id}")] [HttpPut("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> UpdateUser(long id, [FromBody] UpdateUserDto dto) public async Task<IActionResult> UpdateUser(long id, [FromBody] UpdateUserDto dto)
{ {
try try
@ -98,7 +147,21 @@ public class UsersController : ControllerBase
} }
} }
/// <summary>
/// 重置用户密码
/// </summary>
/// <param name="id">用户ID</param>
/// <param name="dto">包含新密码的请求体</param>
/// <returns>无内容响应</returns>
/// <response code="204">成功重置密码</response>
/// <response code="404">用户不存在</response>
/// <response code="400">密码不符合复杂度要求</response>
/// <response code="500">服务器内部错误</response>
[HttpPut("{id}/password")] [HttpPut("{id}/password")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> ResetPassword(long id, [FromBody] ResetPasswordDto dto) public async Task<IActionResult> ResetPassword(long id, [FromBody] ResetPasswordDto dto)
{ {
try try
@ -123,7 +186,18 @@ public class UsersController : ControllerBase
} }
} }
/// <summary>
/// 删除用户
/// </summary>
/// <param name="id">用户ID</param>
/// <returns>无内容响应</returns>
/// <response code="204">成功删除用户</response>
/// <response code="404">用户不存在</response>
/// <response code="500">服务器内部错误</response>
[HttpDelete("{id}")] [HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> DeleteUser(long id) public async Task<IActionResult> DeleteUser(long id)
{ {
try try

View File

@ -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<ApplicationDbContext> options)
: IdentityDbContext<ApplicationUser, ApplicationRole, long>(options)
{
public DbSet<Tenant> Tenants { get; set; }
public DbSet<AccessLog> AccessLogs { get; set; }
public DbSet<AuditLog> AuditLogs { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<ApplicationUser>(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<ApplicationRole>(entity => { entity.Property(e => e.Description).HasMaxLength(200); });
builder.Entity<Tenant>(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<AccessLog>(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<AuditLog>(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);
});
}
}

View File

@ -6,12 +6,21 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DocumentationFile>bin\Debug\net10.0\Fengling.Console.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\net10.0\Fengling.Console.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
<PackageReference Include="OpenIddict.Abstractions" /> <PackageReference Include="OpenIddict.Abstractions" />
<PackageReference Include="OpenIddict.AspNetCore" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" /> <PackageReference Include="OpenIddict.EntityFrameworkCore" />
<PackageReference Include="OpenIddict.Server" /> <PackageReference Include="OpenIddict.Server" />
<PackageReference Include="OpenIddict.Server.AspNetCore" /> <PackageReference Include="OpenIddict.Server.AspNetCore" />
@ -19,7 +28,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Fengling.AuthService\Fengling.AuthService.csproj" /> <!-- <ProjectReference Include="..\Fengling.AuthService\Fengling.AuthService.csproj" />-->
<ProjectReference Include="..\YarpGateway\YarpGateway.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -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; }
}

89
Models/Dtos/GatewayDto.cs Normal file
View File

@ -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<GatewayServiceDto> RecentServices { get; set; } = new();
}
public class GatewayUpdateWeightDto
{
public int Weight { get; set; }
}

View File

@ -0,0 +1,69 @@
namespace Fengling.Console.Models.Dtos;
/// <summary>
/// OAuth客户端详细信息DTO
/// </summary>
public class OAuthClientDto
{
/// <summary>
/// 客户端唯一标识符
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// 客户端ID用于OAuth授权流程中的标识
/// </summary>
public string ClientId { get; set; } = string.Empty;
/// <summary>
/// 客户端显示名称
/// </summary>
public string DisplayName { get; set; } = string.Empty;
/// <summary>
/// 客户端描述信息
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 回调地址列表用于OAuth授权回调
/// </summary>
public string[] RedirectUris { get; set; } = Array.Empty<string>();
/// <summary>
/// 注销回调地址列表
/// </summary>
public string[] PostLogoutRedirectUris { get; set; } = Array.Empty<string>();
/// <summary>
/// 授权范围列表
/// </summary>
public string[] Scopes { get; set; } = Array.Empty<string>();
/// <summary>
/// 授权类型列表
/// </summary>
public string[] GrantTypes { get; set; } = Array.Empty<string>();
/// <summary>
/// 客户端类型public公开客户端或 confidential机密客户端
/// </summary>
public string? ClientType { get; set; }
/// <summary>
/// 授权同意类型implicit、explicit或system
/// </summary>
public string? ConsentType { get; set; }
/// <summary>
/// 客户端状态active、inactive或suspended
/// </summary>
public string Status { get; set; } = "active";
}
/// <summary>
/// OAuth客户端列表分页结果DTO
/// </summary>
public class OAuthClientListDto : PagedResultDto<OAuthClientDto>
{
}

View File

@ -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; }
}

View File

@ -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<T>
{
public List<T> 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;
}

View File

@ -0,0 +1,8 @@
namespace Fengling.Console.Models.Dtos;
public class RoleQueryDto : PaginationQueryDto
{
public string? Name { get; set; }
public string? TenantId { get; set; }
}

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -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;
}

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Identity;
namespace Fengling.Console.Models.Entities;
public class ApplicationRole : IdentityRole<long>
{
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<string>? Permissions { get; set; }
}

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Identity;
namespace Fengling.Console.Models.Entities;
public class ApplicationUser : IdentityUser<long>
{
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; }
}

View File

@ -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;
}

63
Models/Entities/Tenant.cs Normal file
View File

@ -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);
}

View File

@ -0,0 +1,3 @@
namespace Fengling.Console.Models.Entities;
public record TenantInfo(long Id, string TenantId, string Name);

View File

@ -1,6 +1,4 @@
using System.Reflection; using System.Reflection;
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Fengling.Console.Repositories; using Fengling.Console.Repositories;
using Fengling.Console.Services; using Fengling.Console.Services;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
@ -9,6 +7,10 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using System.Text; using System.Text;
using Fengling.Console.Datas;
using Fengling.Console.Models.Entities;
using OpenIddict.Validation.AspNetCore;
using YarpGateway.Data;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -19,6 +21,9 @@ builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")); options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"));
}); });
builder.Services.AddDbContext<GatewayDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("GatewayConnection")));
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>() builder.Services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>() .AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders(); .AddDefaultTokenProviders();
@ -39,27 +44,25 @@ builder.Services.AddOpenIddict()
.AddCore(options => .AddCore(options =>
{ {
options.UseEntityFrameworkCore().UseDbContext<ApplicationDbContext>(); options.UseEntityFrameworkCore().UseDbContext<ApplicationDbContext>();
}); })
.AddValidation(options =>
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 =>
{ {
options.TokenValidationParameters = new TokenValidationParameters options.SetIssuer("http://localhost:5132/");
{
ValidateIssuer = true, options.UseIntrospection()
ValidateAudience = true, .SetClientId("fengling-api")
ValidateLifetime = true, .SetClientSecret("fengling-api-secret");
ValidateIssuerSigningKey = true,
ValidIssuer = jwtIssuer, options.UseSystemNetHttp();
ValidAudience = jwtAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey ?? throw new InvalidOperationException("JWT Key is not configured"))) options.UseAspNetCore();
};
}); });
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
});
builder.Services.AddAuthorization(); builder.Services.AddAuthorization();
builder.Services.AddCors(options => builder.Services.AddCors(options =>

View File

@ -1,4 +1,4 @@
using Fengling.AuthService.Models; using Fengling.Console.Models.Entities;
namespace Fengling.Console.Repositories; namespace Fengling.Console.Repositories;

View File

@ -1,4 +1,4 @@
using Fengling.AuthService.Models; using Fengling.Console.Models.Entities;
namespace Fengling.Console.Repositories; namespace Fengling.Console.Repositories;

View File

@ -1,4 +1,4 @@
using Fengling.AuthService.Models; using Fengling.Console.Models.Entities;
namespace Fengling.Console.Repositories; namespace Fengling.Console.Repositories;

View File

@ -1,36 +1,30 @@
using Fengling.AuthService.Data; using Fengling.Console.Datas;
using Fengling.AuthService.Models; using Fengling.Console.Models.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Fengling.Console.Repositories; 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<ApplicationRole?> GetByIdAsync(long id) public async Task<ApplicationRole?> GetByIdAsync(long id)
{ {
return await _context.Roles.FindAsync(id); return await context.Roles.FindAsync(id);
} }
public async Task<ApplicationRole?> GetByNameAsync(string name) public async Task<ApplicationRole?> GetByNameAsync(string name)
{ {
return await _context.Roles.FirstOrDefaultAsync(r => r.Name == name); return await context.Roles.FirstOrDefaultAsync(r => r.Name == name);
} }
public async Task<IEnumerable<ApplicationRole>> GetAllAsync() public async Task<IEnumerable<ApplicationRole>> GetAllAsync()
{ {
return await _context.Roles.ToListAsync(); return await context.Roles.ToListAsync();
} }
public async Task<IEnumerable<ApplicationRole>> GetPagedAsync(int page, int pageSize, string? name = null, string? tenantId = null) public async Task<IEnumerable<ApplicationRole>> 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)) if (!string.IsNullOrEmpty(name))
{ {
@ -51,7 +45,7 @@ public class RoleRepository : IRoleRepository
public async Task<int> CountAsync(string? name = null, string? tenantId = null) public async Task<int> CountAsync(string? name = null, string? tenantId = null)
{ {
var query = _context.Roles.AsQueryable(); var query = context.Roles.AsQueryable();
if (!string.IsNullOrEmpty(name)) if (!string.IsNullOrEmpty(name))
{ {
@ -68,19 +62,19 @@ public class RoleRepository : IRoleRepository
public async Task AddAsync(ApplicationRole role) public async Task AddAsync(ApplicationRole role)
{ {
_context.Roles.Add(role); context.Roles.Add(role);
await _context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
public async Task UpdateAsync(ApplicationRole role) public async Task UpdateAsync(ApplicationRole role)
{ {
_context.Roles.Update(role); context.Roles.Update(role);
await _context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
public async Task DeleteAsync(ApplicationRole role) public async Task DeleteAsync(ApplicationRole role)
{ {
_context.Roles.Remove(role); context.Roles.Remove(role);
await _context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
} }

View File

@ -1,36 +1,30 @@
using Fengling.AuthService.Data; using Fengling.Console.Datas;
using Fengling.AuthService.Models; using Fengling.Console.Models.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Fengling.Console.Repositories; 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<Tenant?> GetByIdAsync(long id) public async Task<Tenant?> GetByIdAsync(long id)
{ {
return await _context.Tenants.FindAsync(id); return await context.Tenants.FindAsync(id);
} }
public async Task<Tenant?> GetByTenantIdAsync(string tenantId) public async Task<Tenant?> GetByTenantIdAsync(string tenantId)
{ {
return await _context.Tenants.FirstOrDefaultAsync(t => t.TenantId == tenantId); return await context.Tenants.FirstOrDefaultAsync(t => t.TenantId == tenantId);
} }
public async Task<IEnumerable<Tenant>> GetAllAsync() public async Task<IEnumerable<Tenant>> GetAllAsync()
{ {
return await _context.Tenants.ToListAsync(); return await context.Tenants.ToListAsync();
} }
public async Task<IEnumerable<Tenant>> GetPagedAsync(int page, int pageSize, string? name = null, string? tenantId = null, string? status = null) public async Task<IEnumerable<Tenant>> 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)) if (!string.IsNullOrEmpty(name))
{ {
@ -56,7 +50,7 @@ public class TenantRepository : ITenantRepository
public async Task<int> CountAsync(string? name = null, string? tenantId = null, string? status = null) public async Task<int> CountAsync(string? name = null, string? tenantId = null, string? status = null)
{ {
var query = _context.Tenants.AsQueryable(); var query = context.Tenants.AsQueryable();
if (!string.IsNullOrEmpty(name)) if (!string.IsNullOrEmpty(name))
{ {
@ -78,24 +72,24 @@ public class TenantRepository : ITenantRepository
public async Task AddAsync(Tenant tenant) public async Task AddAsync(Tenant tenant)
{ {
_context.Tenants.Add(tenant); context.Tenants.Add(tenant);
await _context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
public async Task UpdateAsync(Tenant tenant) public async Task UpdateAsync(Tenant tenant)
{ {
_context.Tenants.Update(tenant); context.Tenants.Update(tenant);
await _context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
public async Task DeleteAsync(Tenant tenant) public async Task DeleteAsync(Tenant tenant)
{ {
_context.Tenants.Remove(tenant); context.Tenants.Remove(tenant);
await _context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
public async Task<int> GetUserCountAsync(long tenantId) public async Task<int> 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);
} }
} }

View File

@ -1,5 +1,5 @@
using Fengling.AuthService.Data; using Fengling.Console.Datas;
using Fengling.AuthService.Models; using Fengling.Console.Models.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Fengling.Console.Repositories; namespace Fengling.Console.Repositories;

386
Services/GatewayService.cs Normal file
View File

@ -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<GatewayStatisticsDto> GetStatisticsAsync();
Task<List<GatewayServiceDto>> GetServicesAsync(bool globalOnly = false, string? tenantCode = null);
Task<GatewayServiceDto?> GetServiceAsync(string serviceName, string? tenantCode = null);
Task<GatewayServiceDto> RegisterServiceAsync(CreateGatewayServiceDto dto);
Task<bool> UnregisterServiceAsync(string serviceName, string? tenantCode = null);
Task<List<GatewayRouteDto>> GetRoutesAsync(bool globalOnly = false);
Task<GatewayRouteDto> CreateRouteAsync(CreateGatewayRouteDto dto);
Task<List<GatewayInstanceDto>> GetInstancesAsync(string clusterId);
Task<GatewayInstanceDto> AddInstanceAsync(CreateGatewayInstanceDto dto);
Task<bool> RemoveInstanceAsync(long instanceId);
Task<bool> UpdateInstanceWeightAsync(long instanceId, int weight);
Task ReloadGatewayAsync();
}
public class GatewayService : IGatewayService
{
private readonly IDbContextFactory<GatewayDbContext> _dbContextFactory;
private readonly ILogger<GatewayService> _logger;
public GatewayService(IDbContextFactory<GatewayDbContext> dbContextFactory, ILogger<GatewayService> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
public async Task<GatewayStatisticsDto> 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<List<GatewayServiceDto>> 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<GatewayServiceDto?> 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<GatewayServiceDto> 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<bool> 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<List<GatewayRouteDto>> 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<GatewayRouteDto> 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<List<GatewayInstanceDto>> 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<GatewayInstanceDto> 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<bool> 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<bool> 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
};
}
}

View File

@ -1,3 +1,4 @@
using Fengling.Console.Models.Dtos;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text.Json; using System.Text.Json;
@ -6,43 +7,15 @@ namespace Fengling.Console.Services;
public interface IOAuthClientService public interface IOAuthClientService
{ {
Task<(IEnumerable<object> Items, int TotalCount)> GetClientsAsync(int page, int pageSize, string? displayName = null, string? clientId = null, string? status = null); Task<(IEnumerable<OAuthClientDto> Items, int TotalCount)> GetClientsAsync(int page, int pageSize, string? displayName = null, string? clientId = null, string? status = null);
Task<object?> GetClientAsync(string id); Task<OAuthClientDto?> GetClientAsync(string id);
Task<object> CreateClientAsync(CreateClientDto dto); Task<OAuthClientDto> CreateClientAsync(CreateClientDto dto);
Task<object> GenerateSecretAsync(string id); Task<object> GenerateSecretAsync(string id);
Task<object> UpdateClientAsync(string id, UpdateClientDto dto); Task<object> UpdateClientAsync(string id, UpdateClientDto dto);
Task DeleteClientAsync(string id); Task DeleteClientAsync(string id);
object GetClientOptions(); 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 public class OAuthClientService : IOAuthClientService
{ {
private readonly IOpenIddictApplicationManager _applicationManager; private readonly IOpenIddictApplicationManager _applicationManager;
@ -62,10 +35,10 @@ public class OAuthClientService : IOAuthClientService
_logger = logger; _logger = logger;
} }
public async Task<(IEnumerable<object> Items, int TotalCount)> GetClientsAsync(int page, int pageSize, string? displayName = null, string? clientId = null, string? status = null) public async Task<(IEnumerable<OAuthClientDto> Items, int TotalCount)> GetClientsAsync(int page, int pageSize, string? displayName = null, string? clientId = null, string? status = null)
{ {
var applications = _applicationManager.ListAsync(); var applications = _applicationManager.ListAsync();
var clientList = new List<object>(); var clientList = new List<OAuthClientDto>();
await foreach (var application in applications) await foreach (var application in applications)
{ {
@ -83,28 +56,31 @@ public class OAuthClientService : IOAuthClientService
var permissions = await _applicationManager.GetPermissionsAsync(application); var permissions = await _applicationManager.GetPermissionsAsync(application);
var redirectUris = await _applicationManager.GetRedirectUrisAsync(application); var redirectUris = await _applicationManager.GetRedirectUrisAsync(application);
var postLogoutRedirectUris = await _applicationManager.GetPostLogoutRedirectUrisAsync(application); var postLogoutRedirectUris = await _applicationManager.GetPostLogoutRedirectUrisAsync(application);
var applicationId = await _applicationManager.GetIdAsync(application);
clientList.Add(new clientList.Add(new OAuthClientDto
{ {
id = application, Id = applicationId ?? clientIdValue ?? string.Empty,
clientId = clientIdValue, ClientId = clientIdValue ?? string.Empty,
displayName = displayNameValue, DisplayName = displayNameValue ?? string.Empty,
redirectUris = redirectUris.ToArray(), RedirectUris = redirectUris.ToArray(),
postLogoutRedirectUris = postLogoutRedirectUris.ToArray(), PostLogoutRedirectUris = postLogoutRedirectUris.ToArray(),
scopes = permissions Scopes = permissions
.Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope)) .Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope))
.Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length)), .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length))
grantTypes = permissions .ToArray(),
GrantTypes = permissions
.Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType)) .Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType))
.Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length)), .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length))
clientType = clientType?.ToString(), .ToArray(),
consentType = consentType?.ToString(), ClientType = clientType?.ToString(),
status = "active" ConsentType = consentType?.ToString(),
Status = "active"
}); });
} }
var sortedClients = clientList var sortedClients = clientList
.OrderByDescending(c => (c as dynamic).clientId) .OrderByDescending(c => c.ClientId)
.Skip((page - 1) * pageSize) .Skip((page - 1) * pageSize)
.Take(pageSize) .Take(pageSize)
.ToList(); .ToList();
@ -112,7 +88,7 @@ public class OAuthClientService : IOAuthClientService
return (sortedClients, clientList.Count); return (sortedClients, clientList.Count);
} }
public async Task<object?> GetClientAsync(string id) public async Task<OAuthClientDto?> GetClientAsync(string id)
{ {
var application = await _applicationManager.FindByIdAsync(id); var application = await _applicationManager.FindByIdAsync(id);
if (application == null) if (application == null)
@ -128,26 +104,28 @@ public class OAuthClientService : IOAuthClientService
var redirectUris = await _applicationManager.GetRedirectUrisAsync(application); var redirectUris = await _applicationManager.GetRedirectUrisAsync(application);
var postLogoutRedirectUris = await _applicationManager.GetPostLogoutRedirectUrisAsync(application); var postLogoutRedirectUris = await _applicationManager.GetPostLogoutRedirectUrisAsync(application);
return new return new OAuthClientDto
{ {
id = id, Id = id,
clientId = clientIdValue, ClientId = clientIdValue ?? string.Empty,
displayName = displayNameValue, DisplayName = displayNameValue ?? string.Empty,
redirectUris = redirectUris.ToArray(), RedirectUris = redirectUris.ToArray(),
postLogoutRedirectUris = postLogoutRedirectUris.ToArray(), PostLogoutRedirectUris = postLogoutRedirectUris.ToArray(),
scopes = permissions Scopes = permissions
.Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope)) .Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope))
.Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length)), .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length))
grantTypes = permissions .ToArray(),
GrantTypes = permissions
.Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType)) .Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType))
.Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length)), .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length))
clientType = clientType?.ToString(), .ToArray(),
consentType = consentType?.ToString(), ClientType = clientType?.ToString(),
status = "active" ConsentType = consentType?.ToString(),
Status = "active"
}; };
} }
public async Task<object> CreateClientAsync(CreateClientDto dto) public async Task<OAuthClientDto> CreateClientAsync(CreateClientDto dto)
{ {
var authServiceUrl = _configuration["AuthService:Url"] ?? "http://localhost:5132"; var authServiceUrl = _configuration["AuthService:Url"] ?? "http://localhost:5132";
var token = await GetAuthTokenAsync(); var token = await GetAuthTokenAsync();
@ -182,7 +160,7 @@ public class OAuthClientService : IOAuthClientService
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
var result = System.Text.Json.JsonSerializer.Deserialize<object>(content); var result = System.Text.Json.JsonSerializer.Deserialize<OAuthClientDto>(content);
_logger.LogInformation("Created OAuth client {ClientId}", dto.ClientId); _logger.LogInformation("Created OAuth client {ClientId}", dto.ClientId);

View File

@ -1,20 +1,23 @@
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Fengling.Console.Models.Dtos; using Fengling.Console.Models.Dtos;
using Fengling.Console.Repositories; using Fengling.Console.Repositories;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using System.Security.Claims; using System.Security.Claims;
using Fengling.Console.Datas;
using Fengling.Console.Models.Entities;
namespace Fengling.Console.Services; namespace Fengling.Console.Services;
public interface IRoleService public interface IRoleService
{ {
Task<(IEnumerable<RoleDto> Items, int TotalCount)> GetRolesAsync(int page, int pageSize, string? name = null, string? tenantId = null); Task<(IEnumerable<RoleDto> Items, int TotalCount)> GetRolesAsync(int page, int pageSize, string? name = null,
string? tenantId = null);
Task<RoleDto?> GetRoleAsync(long id); Task<RoleDto?> GetRoleAsync(long id);
Task<IEnumerable<UserDto>> GetRoleUsersAsync(long id); Task<IEnumerable<UserDto>> GetRoleUsersAsync(long id);
Task<RoleDto> CreateRoleAsync(CreateRoleDto dto); Task<RoleDto> CreateRoleAsync(CreateRoleDto dto);
Task<RoleDto> UpdateRoleAsync(long id, UpdateRoleDto dto); Task<RoleDto> UpdateRoleAsync(long id, UpdateRoleDto dto);
Task DeleteRoleAsync(long id); Task DeleteRoleAsync(long id);
Task AddUserToRoleAsync(long roleId, long userId);
Task RemoveUserFromRoleAsync(long roleId, long userId); Task RemoveUserFromRoleAsync(long roleId, long userId);
} }
@ -40,7 +43,8 @@ public class RoleService : IRoleService
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
} }
public async Task<(IEnumerable<RoleDto> Items, int TotalCount)> GetRolesAsync(int page, int pageSize, string? name = null, string? tenantId = null) public async Task<(IEnumerable<RoleDto> Items, int TotalCount)> GetRolesAsync(int page, int pageSize,
string? name = null, string? tenantId = null)
{ {
var roles = await _repository.GetPagedAsync(page, pageSize, name, tenantId); var roles = await _repository.GetPagedAsync(page, pageSize, name, tenantId);
var totalCount = await _repository.CountAsync(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))); 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 return new RoleDto
{ {
@ -173,7 +178,8 @@ public class RoleService : IRoleService
await _repository.UpdateAsync(role); 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!); var users = await _userManager.GetUsersInRoleAsync(role.Name!);
return new RoleDto return new RoleDto
@ -216,6 +222,29 @@ public class RoleService : IRoleService
await CreateAuditLog("role", "delete", "Role", role.Id, role.DisplayName, oldValue); 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) public async Task RemoveUserFromRoleAsync(long roleId, long userId)
{ {
var role = await _repository.GetByIdAsync(roleId); var role = await _repository.GetByIdAsync(roleId);
@ -239,10 +268,12 @@ public class RoleService : IRoleService
await CreateAuditLog("role", "update", "UserRole", null, $"{role.Name} - {user.UserName}"); 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 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 tenantId = httpContext?.User?.FindFirstValue("TenantId");
var log = new AuditLog var log = new AuditLog
@ -264,4 +295,4 @@ public class RoleService : IRoleService
_context.AuditLogs.Add(log); _context.AuditLogs.Add(log);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
} }
} }

View File

@ -1,9 +1,9 @@
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Fengling.Console.Models.Dtos; using Fengling.Console.Models.Dtos;
using Fengling.Console.Repositories; using Fengling.Console.Repositories;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using System.Security.Claims; using System.Security.Claims;
using Fengling.Console.Datas;
using Fengling.Console.Models.Entities;
namespace Fengling.Console.Services; namespace Fengling.Console.Services;
@ -13,47 +13,31 @@ public interface ITenantService
Task<TenantDto?> GetTenantAsync(long id); Task<TenantDto?> GetTenantAsync(long id);
Task<IEnumerable<UserDto>> GetTenantUsersAsync(long tenantId); Task<IEnumerable<UserDto>> GetTenantUsersAsync(long tenantId);
Task<IEnumerable<object>> GetTenantRolesAsync(long tenantId); Task<IEnumerable<object>> GetTenantRolesAsync(long tenantId);
Task<TenantSettingsDto> GetTenantSettingsAsync(string tenantId); Task<TenantSettingsDto> GetTenantSettingsAsync(long id);
Task UpdateTenantSettingsAsync(string tenantId, TenantSettingsDto settings); Task UpdateTenantSettingsAsync(long id, TenantSettingsDto settings);
Task<TenantDto> CreateTenantAsync(CreateTenantDto dto); Task<TenantDto> CreateTenantAsync(CreateTenantDto dto);
Task<TenantDto> UpdateTenantAsync(long id, UpdateTenantDto dto); Task<TenantDto> UpdateTenantAsync(long id, UpdateTenantDto dto);
Task DeleteTenantAsync(long id); Task DeleteTenantAsync(long id);
} }
public class TenantService : ITenantService public class TenantService(
ITenantRepository repository,
IUserRepository userRepository,
IRoleRepository roleRepository,
UserManager<ApplicationUser> userManager,
ApplicationDbContext context,
IHttpContextAccessor httpContextAccessor)
: ITenantService
{ {
private readonly ITenantRepository _repository;
private readonly IUserRepository _userRepository;
private readonly IRoleRepository _roleRepository;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ApplicationDbContext _context;
private readonly IHttpContextAccessor _httpContextAccessor;
public TenantService(
ITenantRepository repository,
IUserRepository userRepository,
IRoleRepository roleRepository,
UserManager<ApplicationUser> userManager,
ApplicationDbContext context,
IHttpContextAccessor httpContextAccessor)
{
_repository = repository;
_userRepository = userRepository;
_roleRepository = roleRepository;
_userManager = userManager;
_context = context;
_httpContextAccessor = httpContextAccessor;
}
public async Task<(IEnumerable<TenantDto> Items, int TotalCount)> GetTenantsAsync(int page, int pageSize, string? name = null, string? tenantId = null, string? status = null) public async Task<(IEnumerable<TenantDto> 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 tenants = await repository.GetPagedAsync(page, pageSize, name, tenantId, status);
var totalCount = await _repository.CountAsync(name, tenantId, status); var totalCount = await repository.CountAsync(name, tenantId, status);
var tenantDtos = new List<TenantDto>(); var tenantDtos = new List<TenantDto>();
foreach (var tenant in tenants) foreach (var tenant in tenants)
{ {
var userCount = await _repository.GetUserCountAsync(tenant.Id); var userCount = await repository.GetUserCountAsync(tenant.Id);
tenantDtos.Add(new TenantDto tenantDtos.Add(new TenantDto
{ {
Id = tenant.Id, Id = tenant.Id,
@ -76,7 +60,7 @@ public class TenantService : ITenantService
public async Task<TenantDto?> GetTenantAsync(long id) public async Task<TenantDto?> GetTenantAsync(long id)
{ {
var tenant = await _repository.GetByIdAsync(id); var tenant = await repository.GetByIdAsync(id);
if (tenant == null) return null; if (tenant == null) return null;
return new TenantDto return new TenantDto
@ -88,7 +72,7 @@ public class TenantService : ITenantService
ContactEmail = tenant.ContactEmail, ContactEmail = tenant.ContactEmail,
ContactPhone = tenant.ContactPhone, ContactPhone = tenant.ContactPhone,
MaxUsers = tenant.MaxUsers, MaxUsers = tenant.MaxUsers,
UserCount = await _repository.GetUserCountAsync(tenant.Id), UserCount = await repository.GetUserCountAsync(tenant.Id),
Status = tenant.Status, Status = tenant.Status,
ExpiresAt = tenant.ExpiresAt, ExpiresAt = tenant.ExpiresAt,
Description = tenant.Description, Description = tenant.Description,
@ -98,18 +82,18 @@ public class TenantService : ITenantService
public async Task<IEnumerable<UserDto>> GetTenantUsersAsync(long tenantId) public async Task<IEnumerable<UserDto>> GetTenantUsersAsync(long tenantId)
{ {
var tenant = await _repository.GetByIdAsync(tenantId); var tenant = await repository.GetByIdAsync(tenantId);
if (tenant == null) if (tenant == null)
{ {
throw new KeyNotFoundException($"Tenant with ID {tenantId} not found"); 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<UserDto>(); var userDtos = new List<UserDto>();
foreach (var user in users) foreach (var user in users)
{ {
var roles = await _userManager.GetRolesAsync(user); var roles = await userManager.GetRolesAsync(user);
userDtos.Add(new UserDto userDtos.Add(new UserDto
{ {
Id = user.Id, Id = user.Id,
@ -130,13 +114,13 @@ public class TenantService : ITenantService
public async Task<IEnumerable<object>> GetTenantRolesAsync(long tenantId) public async Task<IEnumerable<object>> GetTenantRolesAsync(long tenantId)
{ {
var tenant = await _repository.GetByIdAsync(tenantId); var tenant = await repository.GetByIdAsync(tenantId);
if (tenant == null) if (tenant == null)
{ {
throw new KeyNotFoundException($"Tenant with ID {tenantId} not found"); 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 return roles.Select(r => new
{ {
id = r.Id, id = r.Id,
@ -145,12 +129,12 @@ public class TenantService : ITenantService
}); });
} }
public async Task<TenantSettingsDto> GetTenantSettingsAsync(string tenantId) public async Task<TenantSettingsDto> GetTenantSettingsAsync(long id)
{ {
var tenant = await _repository.GetByTenantIdAsync(tenantId); var tenant = await repository.GetByIdAsync(id);
if (tenant == null) if (tenant == null)
{ {
throw new KeyNotFoundException($"Tenant with tenantId '{tenantId}' not found"); throw new KeyNotFoundException($"Tenant with ID {id} not found");
} }
return new TenantSettingsDto 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) 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)); 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 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)); 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<TenantDto> UpdateTenantAsync(long id, UpdateTenantDto dto) public async Task<TenantDto> UpdateTenantAsync(long id, UpdateTenantDto dto)
{ {
var tenant = await _repository.GetByIdAsync(id); var tenant = await repository.GetByIdAsync(id);
if (tenant == null) if (tenant == null)
{ {
throw new KeyNotFoundException($"Tenant with ID {id} not found"); throw new KeyNotFoundException($"Tenant with ID {id} not found");
@ -232,7 +216,7 @@ public class TenantService : ITenantService
tenant.ExpiresAt = dto.ExpiresAt; tenant.ExpiresAt = dto.ExpiresAt;
tenant.UpdatedAt = DateTime.UtcNow; 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)); 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, ContactEmail = tenant.ContactEmail,
ContactPhone = tenant.ContactPhone, ContactPhone = tenant.ContactPhone,
MaxUsers = tenant.MaxUsers, MaxUsers = tenant.MaxUsers,
UserCount = await _repository.GetUserCountAsync(tenant.Id), UserCount = await repository.GetUserCountAsync(tenant.Id),
Status = tenant.Status, Status = tenant.Status,
ExpiresAt = tenant.ExpiresAt, ExpiresAt = tenant.ExpiresAt,
Description = tenant.Description, Description = tenant.Description,
@ -255,7 +239,7 @@ public class TenantService : ITenantService
public async Task DeleteTenantAsync(long id) public async Task DeleteTenantAsync(long id)
{ {
var tenant = await _repository.GetByIdAsync(id); var tenant = await repository.GetByIdAsync(id);
if (tenant == null) if (tenant == null)
{ {
throw new KeyNotFoundException($"Tenant with ID {id} not found"); 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 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) foreach (var user in users)
{ {
user.IsDeleted = true; user.IsDeleted = true;
user.UpdatedTime = DateTime.UtcNow; user.UpdatedTime = DateTime.UtcNow;
await _context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
tenant.IsDeleted = true; tenant.IsDeleted = true;
await _repository.UpdateAsync(tenant); await repository.UpdateAsync(tenant);
await CreateAuditLog("tenant", "delete", "Tenant", tenant.Id, tenant.TenantId, oldValue); 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) 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 userName = httpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier) ?? httpContext?.User?.Identity?.Name ?? "system";
var tenantId = httpContext?.User?.FindFirstValue("TenantId"); var tenantId = httpContext?.User?.FindFirstValue("TenantId");
@ -299,7 +283,7 @@ public class TenantService : ITenantService
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow
}; };
_context.AuditLogs.Add(log); context.AuditLogs.Add(log);
await _context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
} }

View File

@ -1,9 +1,9 @@
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Fengling.Console.Models.Dtos; using Fengling.Console.Models.Dtos;
using Fengling.Console.Repositories; using Fengling.Console.Repositories;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using System.Security.Claims; using System.Security.Claims;
using Fengling.Console.Datas;
using Fengling.Console.Models.Entities;
namespace Fengling.Console.Services; namespace Fengling.Console.Services;
@ -17,40 +17,24 @@ public interface IUserService
Task DeleteUserAsync(long id); Task DeleteUserAsync(long id);
} }
public class UserService : IUserService public class UserService(
IUserRepository repository,
ITenantRepository tenantRepository,
UserManager<ApplicationUser> userManager,
RoleManager<ApplicationRole> roleManager,
ApplicationDbContext context,
IHttpContextAccessor httpContextAccessor)
: IUserService
{ {
private readonly IUserRepository _repository;
private readonly ITenantRepository _tenantRepository;
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<ApplicationRole> _roleManager;
private readonly ApplicationDbContext _context;
private readonly IHttpContextAccessor _httpContextAccessor;
public UserService(
IUserRepository repository,
ITenantRepository tenantRepository,
UserManager<ApplicationUser> userManager,
RoleManager<ApplicationRole> roleManager,
ApplicationDbContext context,
IHttpContextAccessor httpContextAccessor)
{
_repository = repository;
_tenantRepository = tenantRepository;
_userManager = userManager;
_roleManager = roleManager;
_context = context;
_httpContextAccessor = httpContextAccessor;
}
public async Task<(IEnumerable<UserDto> Items, int TotalCount)> GetUsersAsync(int page, int pageSize, string? userName = null, string? email = null, string? tenantId = null) public async Task<(IEnumerable<UserDto> 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 users = await repository.GetPagedAsync(page, pageSize, userName, email, tenantId);
var totalCount = await _repository.CountAsync(userName, email, tenantId); var totalCount = await repository.CountAsync(userName, email, tenantId);
var userDtos = new List<UserDto>(); var userDtos = new List<UserDto>();
foreach (var user in users) foreach (var user in users)
{ {
var roles = await _userManager.GetRolesAsync(user); var roles = await userManager.GetRolesAsync(user);
userDtos.Add(new UserDto userDtos.Add(new UserDto
{ {
Id = user.Id, Id = user.Id,
@ -72,10 +56,10 @@ public class UserService : IUserService
public async Task<UserDto?> GetUserAsync(long id) public async Task<UserDto?> GetUserAsync(long id)
{ {
var user = await _repository.GetByIdAsync(id); var user = await repository.GetByIdAsync(id);
if (user == null) return null; if (user == null) return null;
var roles = await _userManager.GetRolesAsync(user); var roles = await userManager.GetRolesAsync(user);
return new UserDto return new UserDto
{ {
Id = user.Id, Id = user.Id,
@ -99,7 +83,7 @@ public class UserService : IUserService
if (tenantId != 0) if (tenantId != 0)
{ {
tenant = await _tenantRepository.GetByIdAsync(tenantId); tenant = await tenantRepository.GetByIdAsync(tenantId);
if (tenant == null) if (tenant == null)
{ {
throw new InvalidOperationException("Invalid tenant ID"); throw new InvalidOperationException("Invalid tenant ID");
@ -117,7 +101,7 @@ public class UserService : IUserService
CreatedTime = DateTime.UtcNow CreatedTime = DateTime.UtcNow
}; };
var result = await _userManager.CreateAsync(user, dto.Password); var result = await userManager.CreateAsync(user, dto.Password);
if (!result.Succeeded) if (!result.Succeeded)
{ {
throw new InvalidOperationException(string.Join(", ", result.Errors.Select(e => e.Description))); 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) foreach (var roleId in dto.RoleIds)
{ {
var role = await _roleManager.FindByIdAsync(roleId.ToString()); var role = await roleManager.FindByIdAsync(roleId.ToString());
if (role != null) if (role != null)
{ {
await _userManager.AddToRoleAsync(user, role.Name!); await userManager.AddToRoleAsync(user, role.Name!);
} }
} }
} }
if (!dto.IsActive) if (!dto.IsActive)
{ {
await _userManager.SetLockoutEnabledAsync(user, true); await userManager.SetLockoutEnabledAsync(user, true);
await _userManager.SetLockoutEndDateAsync(user, DateTimeOffset.MaxValue); await userManager.SetLockoutEndDateAsync(user, DateTimeOffset.MaxValue);
} }
await CreateAuditLog("user", "create", "User", user.Id, user.UserName, null, System.Text.Json.JsonSerializer.Serialize(dto)); 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 return new UserDto
{ {
Id = user.Id, Id = user.Id,
@ -162,7 +146,7 @@ public class UserService : IUserService
public async Task<UserDto> UpdateUserAsync(long id, UpdateUserDto dto) public async Task<UserDto> UpdateUserAsync(long id, UpdateUserDto dto)
{ {
var user = await _repository.GetByIdAsync(id); var user = await repository.GetByIdAsync(id);
if (user == null) if (user == null)
{ {
throw new KeyNotFoundException($"User with ID {id} not found"); throw new KeyNotFoundException($"User with ID {id} not found");
@ -178,20 +162,20 @@ public class UserService : IUserService
if (dto.IsActive) if (dto.IsActive)
{ {
await _userManager.SetLockoutEnabledAsync(user, false); await userManager.SetLockoutEnabledAsync(user, false);
await _userManager.SetLockoutEndDateAsync(user, null); await userManager.SetLockoutEndDateAsync(user, null);
} }
else else
{ {
await _userManager.SetLockoutEnabledAsync(user, true); await userManager.SetLockoutEnabledAsync(user, true);
await _userManager.SetLockoutEndDateAsync(user, DateTimeOffset.MaxValue); 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)); 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 return new UserDto
{ {
Id = user.Id, Id = user.Id,
@ -210,14 +194,14 @@ public class UserService : IUserService
public async Task ResetPasswordAsync(long id, ResetPasswordDto dto) 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) if (user == null)
{ {
throw new KeyNotFoundException($"User with ID {id} not found"); throw new KeyNotFoundException($"User with ID {id} not found");
} }
var token = await _userManager.GeneratePasswordResetTokenAsync(user); var token = await userManager.GeneratePasswordResetTokenAsync(user);
var result = await _userManager.ResetPasswordAsync(user, token, dto.NewPassword); var result = await userManager.ResetPasswordAsync(user, token, dto.NewPassword);
if (!result.Succeeded) if (!result.Succeeded)
{ {
@ -229,7 +213,7 @@ public class UserService : IUserService
public async Task DeleteUserAsync(long id) public async Task DeleteUserAsync(long id)
{ {
var user = await _repository.GetByIdAsync(id); var user = await repository.GetByIdAsync(id);
if (user == null) if (user == null)
{ {
throw new KeyNotFoundException($"User with ID {id} not found"); 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); var oldValue = System.Text.Json.JsonSerializer.Serialize(user);
user.IsDeleted = true; user.IsDeleted = true;
user.UpdatedTime = DateTime.UtcNow; user.UpdatedTime = DateTime.UtcNow;
await _context.SaveChangesAsync(); await context.SaveChangesAsync();
await CreateAuditLog("user", "delete", "User", user.Id, user.UserName, oldValue); 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) 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 userName = httpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier) ?? httpContext?.User?.Identity?.Name ?? "system";
var tenantId = httpContext?.User?.FindFirstValue("TenantId"); var tenantId = httpContext?.User?.FindFirstValue("TenantId");
@ -265,7 +249,7 @@ public class UserService : IUserService
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow
}; };
_context.AuditLogs.Add(log); context.AuditLogs.Add(log);
await _context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
} }

View File

@ -2,7 +2,7 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Debug"
} }
} }
} }

View File

@ -5,5 +5,9 @@
"Microsoft.AspNetCore": "Warning" "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"
}
} }