Initial commit
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
using System.Text.Json;
|
||||
using License.Api.Data;
|
||||
using License.Api.DTOs;
|
||||
using License.Api.Models;
|
||||
using License.Api.Services;
|
||||
using License.Api.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace License.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(Policy = "SuperAdmin")]
|
||||
[Route("api/admin/agents")]
|
||||
public class AdminAgentsController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly ConfigService _config;
|
||||
|
||||
public AdminAgentsController(AppDbContext db, ConfigService config)
|
||||
{
|
||||
_db = db;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] AgentCreateRequest request)
|
||||
{
|
||||
var agentSystemEnabled = await _config.GetBoolAsync("feature.agent_system", true);
|
||||
if (!agentSystemEnabled)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var agent = new Agent
|
||||
{
|
||||
AdminId = request.AdminId,
|
||||
AgentCode = request.AgentCode,
|
||||
CompanyName = request.CompanyName,
|
||||
ContactPerson = request.ContactPerson,
|
||||
ContactPhone = request.ContactPhone,
|
||||
ContactEmail = request.ContactEmail,
|
||||
PasswordHash = PasswordHasher.Hash(request.Password),
|
||||
Balance = request.InitialBalance,
|
||||
Discount = request.Discount,
|
||||
CreditLimit = request.CreditLimit,
|
||||
MaxProjects = request.AllowedProjects?.Count ?? 0,
|
||||
AllowedProjects = request.AllowedProjects == null ? null : JsonSerializer.Serialize(request.AllowedProjects),
|
||||
Status = "active",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_db.Agents.Add(agent);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
var data = new AgentDetailResponse
|
||||
{
|
||||
Id = agent.Id,
|
||||
AgentCode = agent.AgentCode,
|
||||
CompanyName = agent.CompanyName,
|
||||
ContactPerson = agent.ContactPerson,
|
||||
ContactPhone = agent.ContactPhone,
|
||||
ContactEmail = agent.ContactEmail,
|
||||
Balance = agent.Balance,
|
||||
Discount = agent.Discount,
|
||||
CreditLimit = agent.CreditLimit,
|
||||
Status = agent.Status,
|
||||
CreatedAt = agent.CreatedAt
|
||||
};
|
||||
|
||||
return Ok(ApiResponse<AgentDetailResponse>.Ok(data));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List([FromQuery] string? status, [FromQuery] int page = 1, [FromQuery] int pageSize = 20)
|
||||
{
|
||||
var agentSystemEnabled = await _config.GetBoolAsync("feature.agent_system", true);
|
||||
if (!agentSystemEnabled)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
page = Math.Max(1, page);
|
||||
pageSize = Math.Clamp(pageSize, 1, 100);
|
||||
|
||||
var query = _db.Agents.AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(status))
|
||||
query = query.Where(a => a.Status == status);
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var items = await query.OrderByDescending(a => a.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(a => new AgentListItem
|
||||
{
|
||||
Id = a.Id,
|
||||
AgentCode = a.AgentCode,
|
||||
CompanyName = a.CompanyName,
|
||||
ContactPerson = a.ContactPerson,
|
||||
ContactPhone = a.ContactPhone,
|
||||
Balance = a.Balance,
|
||||
Discount = a.Discount,
|
||||
Status = a.Status,
|
||||
CreatedAt = a.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var result = new PagedResult<AgentListItem>
|
||||
{
|
||||
Items = items,
|
||||
Pagination = new PaginationInfo
|
||||
{
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
Total = total,
|
||||
TotalPages = (int)Math.Ceiling(total / (double)pageSize)
|
||||
}
|
||||
};
|
||||
|
||||
return Ok(ApiResponse<PagedResult<AgentListItem>>.Ok(result));
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}")]
|
||||
public async Task<IActionResult> Get(int id)
|
||||
{
|
||||
var agentSystemEnabled = await _config.GetBoolAsync("feature.agent_system", true);
|
||||
if (!agentSystemEnabled)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var agent = await _db.Agents.FindAsync(id);
|
||||
if (agent == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
|
||||
var transactions = await _db.AgentTransactions
|
||||
.Where(t => t.AgentId == id)
|
||||
.OrderByDescending(t => t.CreatedAt)
|
||||
.Take(50)
|
||||
.ToListAsync();
|
||||
|
||||
var stats = await _db.CardKeys
|
||||
.Where(c => c.AgentId == id && c.DeletedAt == null)
|
||||
.GroupBy(c => c.AgentId)
|
||||
.Select(g => new
|
||||
{
|
||||
totalCards = g.Count(),
|
||||
activeCards = g.Count(x => x.Status == "active"),
|
||||
totalRevenue = g.Sum(x => x.SoldPrice ?? 0)
|
||||
})
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
return Ok(ApiResponse<object>.Ok(new
|
||||
{
|
||||
agent = new AgentDetailResponse
|
||||
{
|
||||
Id = agent.Id,
|
||||
AgentCode = agent.AgentCode,
|
||||
CompanyName = agent.CompanyName,
|
||||
ContactPerson = agent.ContactPerson,
|
||||
ContactPhone = agent.ContactPhone,
|
||||
ContactEmail = agent.ContactEmail,
|
||||
Balance = agent.Balance,
|
||||
Discount = agent.Discount,
|
||||
CreditLimit = agent.CreditLimit,
|
||||
Status = agent.Status,
|
||||
CreatedAt = agent.CreatedAt
|
||||
},
|
||||
stats,
|
||||
transactions
|
||||
}));
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}")]
|
||||
public async Task<IActionResult> Update(int id, [FromBody] AgentUpdateRequest request)
|
||||
{
|
||||
var agentSystemEnabled = await _config.GetBoolAsync("feature.agent_system", true);
|
||||
if (!agentSystemEnabled)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var agent = await _db.Agents.FindAsync(id);
|
||||
if (agent == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.CompanyName))
|
||||
agent.CompanyName = request.CompanyName;
|
||||
if (!string.IsNullOrWhiteSpace(request.ContactPerson))
|
||||
agent.ContactPerson = request.ContactPerson;
|
||||
if (!string.IsNullOrWhiteSpace(request.ContactPhone))
|
||||
agent.ContactPhone = request.ContactPhone;
|
||||
if (!string.IsNullOrWhiteSpace(request.ContactEmail))
|
||||
agent.ContactEmail = request.ContactEmail;
|
||||
if (request.Discount.HasValue)
|
||||
agent.Discount = request.Discount.Value;
|
||||
if (request.CreditLimit.HasValue)
|
||||
agent.CreditLimit = request.CreditLimit.Value;
|
||||
if (request.AllowedProjects != null)
|
||||
{
|
||||
agent.AllowedProjects = JsonSerializer.Serialize(request.AllowedProjects);
|
||||
agent.MaxProjects = request.AllowedProjects.Count;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(request.Status))
|
||||
agent.Status = request.Status;
|
||||
|
||||
agent.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/disable")]
|
||||
public async Task<IActionResult> Disable(int id)
|
||||
{
|
||||
var agentSystemEnabled = await _config.GetBoolAsync("feature.agent_system", true);
|
||||
if (!agentSystemEnabled)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var agent = await _db.Agents.FindAsync(id);
|
||||
if (agent == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
|
||||
agent.Status = "disabled";
|
||||
agent.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/enable")]
|
||||
public async Task<IActionResult> Enable(int id)
|
||||
{
|
||||
var agentSystemEnabled = await _config.GetBoolAsync("feature.agent_system", true);
|
||||
if (!agentSystemEnabled)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var agent = await _db.Agents.FindAsync(id);
|
||||
if (agent == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
|
||||
agent.Status = "active";
|
||||
agent.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var agentSystemEnabled = await _config.GetBoolAsync("feature.agent_system", true);
|
||||
if (!agentSystemEnabled)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var agent = await _db.Agents.FindAsync(id);
|
||||
if (agent == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
|
||||
_db.Agents.Remove(agent);
|
||||
await _db.SaveChangesAsync();
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/recharge")]
|
||||
public async Task<IActionResult> Recharge(int id, [FromBody] AgentBalanceRequest request)
|
||||
{
|
||||
var agentSystemEnabled = await _config.GetBoolAsync("feature.agent_system", true);
|
||||
if (!agentSystemEnabled)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
return await AdjustBalance(id, request.Amount, "recharge", request.Remark);
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/deduct")]
|
||||
public async Task<IActionResult> Deduct(int id, [FromBody] AgentBalanceRequest request)
|
||||
{
|
||||
var agentSystemEnabled = await _config.GetBoolAsync("feature.agent_system", true);
|
||||
if (!agentSystemEnabled)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
return await AdjustBalance(id, -Math.Abs(request.Amount), "consume", request.Remark);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/transactions")]
|
||||
public async Task<IActionResult> Transactions(int id)
|
||||
{
|
||||
var agentSystemEnabled = await _config.GetBoolAsync("feature.agent_system", true);
|
||||
if (!agentSystemEnabled)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var items = await _db.AgentTransactions
|
||||
.Where(t => t.AgentId == id)
|
||||
.OrderByDescending(t => t.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(ApiResponse<List<AgentTransaction>>.Ok(items));
|
||||
}
|
||||
|
||||
private async Task<IActionResult> AdjustBalance(int id, decimal amount, string type, string? remark)
|
||||
{
|
||||
if (!User.TryGetUserId(out var adminId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
await using var tx = await _db.Database.BeginTransactionAsync();
|
||||
var agent = await _db.Agents
|
||||
.FromSqlRaw("SELECT * FROM \"Agents\" WHERE \"Id\" = {0} FOR UPDATE", id)
|
||||
.FirstOrDefaultAsync();
|
||||
if (agent == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
|
||||
var balanceBefore = agent.Balance;
|
||||
var balanceAfter = balanceBefore + amount;
|
||||
if (balanceAfter < -agent.CreditLimit)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
agent.Balance = balanceAfter;
|
||||
agent.UpdatedAt = DateTime.UtcNow;
|
||||
_db.AgentTransactions.Add(new AgentTransaction
|
||||
{
|
||||
AgentId = agent.Id,
|
||||
Type = type,
|
||||
Amount = amount,
|
||||
BalanceBefore = balanceBefore,
|
||||
BalanceAfter = balanceAfter,
|
||||
Remark = remark,
|
||||
CreatedBy = adminId,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
await tx.CommitAsync();
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using License.Api.Data;
|
||||
using License.Api.DTOs;
|
||||
using License.Api.Security;
|
||||
using License.Api.Services;
|
||||
using License.Api.Utils;
|
||||
using License.Api.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace License.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin")]
|
||||
public class AdminAuthController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly JwtTokenService _jwt;
|
||||
|
||||
public AdminAuthController(AppDbContext db, JwtTokenService jwt)
|
||||
{
|
||||
_db = db;
|
||||
_jwt = jwt;
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> Login([FromBody] AdminLoginRequest request)
|
||||
{
|
||||
var admin = await _db.Admins.FirstOrDefaultAsync(a => a.Username == request.Username);
|
||||
if (admin == null || !PasswordHasher.Verify(request.Password, admin.PasswordHash))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
if (admin.Status != "active")
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
admin.LastLoginAt = DateTime.UtcNow;
|
||||
admin.LastLoginIp = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
await LogAccessAsync($"admin:{admin.Username}", "admin_login");
|
||||
|
||||
var token = _jwt.CreateAdminToken(admin);
|
||||
var permissions = ResolvePermissions(admin);
|
||||
var data = new
|
||||
{
|
||||
token,
|
||||
user = new
|
||||
{
|
||||
id = admin.Id,
|
||||
username = admin.Username,
|
||||
role = admin.Role,
|
||||
permissions
|
||||
}
|
||||
};
|
||||
|
||||
return Ok(ApiResponse<object>.Ok(data));
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Admin")]
|
||||
[HttpPost("logout")]
|
||||
public IActionResult Logout()
|
||||
{
|
||||
var username = User.FindFirst("username")?.Value ?? "admin";
|
||||
_ = LogAccessAsync($"admin:{username}", "admin_logout");
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Admin")]
|
||||
[HttpGet("profile")]
|
||||
public async Task<IActionResult> Profile()
|
||||
{
|
||||
if (!User.TryGetUserId(out var adminId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
var admin = await _db.Admins.FindAsync(adminId);
|
||||
if (admin == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
|
||||
return Ok(ApiResponse<object>.Ok(new
|
||||
{
|
||||
id = admin.Id,
|
||||
username = admin.Username,
|
||||
role = admin.Role,
|
||||
email = admin.Email,
|
||||
permissions = ResolvePermissions(admin)
|
||||
}));
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Admin")]
|
||||
[HttpPut("profile")]
|
||||
public async Task<IActionResult> UpdateProfile([FromBody] AdminUpdateRequest request)
|
||||
{
|
||||
if (!User.TryGetUserId(out var adminId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
var admin = await _db.Admins.FindAsync(adminId);
|
||||
if (admin == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Email))
|
||||
admin.Email = request.Email;
|
||||
|
||||
admin.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Admin")]
|
||||
[HttpPost("change-password")]
|
||||
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordRequest request)
|
||||
{
|
||||
if (!User.TryGetUserId(out var adminId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
var admin = await _db.Admins.FindAsync(adminId);
|
||||
if (admin == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
|
||||
if (!PasswordHasher.Verify(request.OldPassword, admin.PasswordHash))
|
||||
return BadRequest(ApiResponse.Fail(400, "bad_request"));
|
||||
|
||||
admin.PasswordHash = PasswordHasher.Hash(request.NewPassword);
|
||||
admin.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
private async Task LogAccessAsync(string? deviceId, string action)
|
||||
{
|
||||
_db.AccessLogs.Add(new AccessLog
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
Action = action,
|
||||
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
UserAgent = HttpContext.Request.Headers.UserAgent.ToString(),
|
||||
ResponseCode = 200,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static List<string> ResolvePermissions(Admin admin)
|
||||
{
|
||||
if (string.Equals(admin.Role, "super_admin", StringComparison.OrdinalIgnoreCase))
|
||||
return new List<string> { "*" };
|
||||
|
||||
var (hasAll, allowed) = AdminAccessService.ParsePermissions(admin.Permissions);
|
||||
if (hasAll)
|
||||
return new List<string> { "*" };
|
||||
return allowed.OrderBy(p => p).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,594 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using ClosedXML.Excel;
|
||||
using License.Api.Data;
|
||||
using License.Api.DTOs;
|
||||
using License.Api.Models;
|
||||
using License.Api.Services;
|
||||
using License.Api.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace License.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(Policy = "Admin")]
|
||||
[Route("api/admin/cards")]
|
||||
public class AdminCardsController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly CardService _cards;
|
||||
private readonly IdempotencyService _idempotency;
|
||||
private readonly ConfigService _config;
|
||||
private readonly AdminAccessService _adminAccess;
|
||||
|
||||
public AdminCardsController(AppDbContext db, CardService cards, IdempotencyService idempotency, ConfigService config, AdminAccessService adminAccess)
|
||||
{
|
||||
_db = db;
|
||||
_cards = cards;
|
||||
_idempotency = idempotency;
|
||||
_config = config;
|
||||
_adminAccess = adminAccess;
|
||||
}
|
||||
|
||||
[HttpPost("generate")]
|
||||
public async Task<IActionResult> Generate([FromBody] CardGenerateRequest request)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
if (request.Quantity <= 0 || request.Quantity > 10000)
|
||||
return BadRequest(ApiResponse.Fail(400, "bad_request"));
|
||||
|
||||
var project = await _db.Projects.FirstOrDefaultAsync(p => p.ProjectId == request.ProjectId);
|
||||
if (project == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(project.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
if (!project.IsEnabled)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(1011, "project_disabled"));
|
||||
|
||||
var requestHash = IdempotencyService.ComputeRequestHash(JsonSerializer.Serialize(request));
|
||||
var idempotencyKey = Request.Headers["X-Idempotency-Key"].ToString();
|
||||
if (!string.IsNullOrWhiteSpace(idempotencyKey))
|
||||
{
|
||||
var existing = await _idempotency.GetAsync(idempotencyKey);
|
||||
if (existing != null)
|
||||
{
|
||||
if (!string.Equals(existing.RequestHash, requestHash, StringComparison.OrdinalIgnoreCase))
|
||||
return Conflict(ApiResponse.Fail(400, "bad_request"));
|
||||
|
||||
var cached = JsonSerializer.Deserialize<ApiResponse<CardGenerateResponse>>(existing.ResponseBody ?? "{}")
|
||||
?? ApiResponse<CardGenerateResponse>.Fail(500, "internal_error");
|
||||
return StatusCode(existing.ResponseCode ?? 200, cached);
|
||||
}
|
||||
}
|
||||
|
||||
if (!User.TryGetUserId(out var adminId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
var data = await _cards.GenerateAsync(request, adminId);
|
||||
var response = ApiResponse<CardGenerateResponse>.Ok(data);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(idempotencyKey))
|
||||
{
|
||||
var body = JsonSerializer.Serialize(response);
|
||||
await _idempotency.StoreAsync(idempotencyKey, Request.Path, requestHash, 200, body);
|
||||
}
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List([FromQuery] string? projectId, [FromQuery] string? status, [FromQuery] int page = 1, [FromQuery] int pageSize = 20)
|
||||
{
|
||||
page = Math.Max(1, page);
|
||||
pageSize = Math.Clamp(pageSize, 1, 100);
|
||||
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var query = _db.CardKeys.Where(c => c.DeletedAt == null).AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(projectId))
|
||||
{
|
||||
if (!scope.CanAccessProject(projectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
query = query.Where(c => c.ProjectId == projectId);
|
||||
}
|
||||
else if (!scope.IsSuperAdmin && !scope.HasAllProjects)
|
||||
{
|
||||
if (scope.AllowedProjects.Count == 0)
|
||||
{
|
||||
var empty = new PagedResult<CardKey>
|
||||
{
|
||||
Items = new List<CardKey>(),
|
||||
Pagination = new PaginationInfo
|
||||
{
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
Total = 0,
|
||||
TotalPages = 0
|
||||
}
|
||||
};
|
||||
return Ok(ApiResponse<PagedResult<CardKey>>.Ok(empty));
|
||||
}
|
||||
var allowed = scope.AllowedProjects.ToList();
|
||||
query = query.Where(c => c.ProjectId != null && allowed.Contains(c.ProjectId));
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(status))
|
||||
query = query.Where(c => c.Status == status);
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var items = await query.OrderByDescending(c => c.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
var result = new PagedResult<CardKey>
|
||||
{
|
||||
Items = items,
|
||||
Pagination = new PaginationInfo
|
||||
{
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
Total = total,
|
||||
TotalPages = (int)Math.Ceiling(total / (double)pageSize)
|
||||
}
|
||||
};
|
||||
|
||||
return Ok(ApiResponse<PagedResult<CardKey>>.Ok(result));
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}")]
|
||||
public async Task<IActionResult> Get(int id)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var card = await _db.CardKeys
|
||||
.Include(c => c.Devices)
|
||||
.Include(c => c.Logs)
|
||||
.AsSplitQuery()
|
||||
.FirstOrDefaultAsync(c => c.Id == id && c.DeletedAt == null);
|
||||
|
||||
if (card == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(card.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
return Ok(ApiResponse<CardKey>.Ok(card));
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/logs")]
|
||||
public async Task<IActionResult> Logs(int id)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var card = await _db.CardKeys.FirstOrDefaultAsync(c => c.Id == id && c.DeletedAt == null);
|
||||
if (card == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(card.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var logs = await _db.CardKeyLogs
|
||||
.Where(l => l.CardKeyId == id)
|
||||
.OrderByDescending(l => l.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(ApiResponse<List<CardKeyLog>>.Ok(logs));
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}")]
|
||||
public async Task<IActionResult> UpdateNote(int id, [FromBody] CardNoteUpdateRequest request)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var card = await _db.CardKeys.FirstOrDefaultAsync(c => c.Id == id && c.DeletedAt == null);
|
||||
if (card == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(card.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
card.Note = request.Note;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/ban")]
|
||||
public async Task<IActionResult> Ban(int id, [FromBody] CardBanRequest request)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var card = await _db.CardKeys.FirstOrDefaultAsync(c => c.Id == id && c.DeletedAt == null);
|
||||
if (card == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(card.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
if (!User.TryGetUserId(out var adminId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
await _cards.BanAsync(card, request.Reason, adminId, "admin");
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/unban")]
|
||||
public async Task<IActionResult> Unban(int id)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var card = await _db.CardKeys.FirstOrDefaultAsync(c => c.Id == id && c.DeletedAt == null);
|
||||
if (card == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(card.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
if (!User.TryGetUserId(out var adminId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
await _cards.UnbanAsync(card, adminId, "admin");
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/extend")]
|
||||
public async Task<IActionResult> Extend(int id, [FromBody] CardExtendRequest request)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var renewalEnabled = await _config.GetBoolAsync("feature.card_renewal", true);
|
||||
if (!renewalEnabled)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var card = await _db.CardKeys.FirstOrDefaultAsync(c => c.Id == id && c.DeletedAt == null);
|
||||
if (card == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(card.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
if (!User.TryGetUserId(out var adminId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
await _cards.ExtendAsync(card, request.Days, adminId, "admin");
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/reset-device")]
|
||||
public async Task<IActionResult> ResetDevice(int id)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var card = await _db.CardKeys.FirstOrDefaultAsync(c => c.Id == id && c.DeletedAt == null);
|
||||
if (card == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(card.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
if (!User.TryGetUserId(out var adminId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
await _cards.ResetDeviceAsync(card, adminId, "admin");
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var card = await _db.CardKeys.FirstOrDefaultAsync(c => c.Id == id && c.DeletedAt == null);
|
||||
if (card == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(card.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
card.DeletedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpPost("ban-batch")]
|
||||
public async Task<IActionResult> BanBatch([FromBody] CardBatchRequest request)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var cards = await _db.CardKeys.Where(c => request.Ids.Contains(c.Id) && c.DeletedAt == null).ToListAsync();
|
||||
if (!scope.IsSuperAdmin && !scope.HasAllProjects)
|
||||
{
|
||||
if (cards.Any(c => !scope.CanAccessProject(c.ProjectId)))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
}
|
||||
if (!User.TryGetUserId(out var adminId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
foreach (var card in cards)
|
||||
await _cards.BanAsync(card, request.Reason, adminId, "admin");
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpPost("unban-batch")]
|
||||
public async Task<IActionResult> UnbanBatch([FromBody] CardBatchRequest request)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var cards = await _db.CardKeys.Where(c => request.Ids.Contains(c.Id) && c.DeletedAt == null).ToListAsync();
|
||||
if (!scope.IsSuperAdmin && !scope.HasAllProjects)
|
||||
{
|
||||
if (cards.Any(c => !scope.CanAccessProject(c.ProjectId)))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
}
|
||||
if (!User.TryGetUserId(out var adminId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
foreach (var card in cards)
|
||||
await _cards.UnbanAsync(card, adminId, "admin");
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpDelete("batch")]
|
||||
public async Task<IActionResult> DeleteBatch([FromBody] CardBatchRequest request)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var cards = await _db.CardKeys.Where(c => request.Ids.Contains(c.Id) && c.DeletedAt == null).ToListAsync();
|
||||
if (!scope.IsSuperAdmin && !scope.HasAllProjects)
|
||||
{
|
||||
if (cards.Any(c => !scope.CanAccessProject(c.ProjectId)))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
}
|
||||
foreach (var card in cards)
|
||||
card.DeletedAt = DateTime.UtcNow;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpGet("export")]
|
||||
public async Task<IActionResult> Export([FromQuery] string? projectId, [FromQuery] string? format)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var query = _db.CardKeys.Where(c => c.DeletedAt == null);
|
||||
if (!string.IsNullOrWhiteSpace(projectId))
|
||||
{
|
||||
if (!scope.CanAccessProject(projectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
query = query.Where(c => c.ProjectId == projectId);
|
||||
}
|
||||
else if (!scope.IsSuperAdmin && !scope.HasAllProjects)
|
||||
{
|
||||
if (scope.AllowedProjects.Count == 0)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
var allowed = scope.AllowedProjects.ToList();
|
||||
query = query.Where(c => c.ProjectId != null && allowed.Contains(c.ProjectId));
|
||||
}
|
||||
|
||||
var items = await query.OrderByDescending(c => c.CreatedAt).ToListAsync();
|
||||
var ext = string.IsNullOrWhiteSpace(format) ? "csv" : format.ToLowerInvariant();
|
||||
if (ext is "excel" or "xlsx")
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var sheet = workbook.Worksheets.Add("CardKeys");
|
||||
sheet.Cell(1, 1).Value = "keyCode";
|
||||
sheet.Cell(1, 2).Value = "cardType";
|
||||
sheet.Cell(1, 3).Value = "status";
|
||||
sheet.Cell(1, 4).Value = "expireTime";
|
||||
sheet.Cell(1, 5).Value = "note";
|
||||
var row = 2;
|
||||
foreach (var card in items)
|
||||
{
|
||||
sheet.Cell(row, 1).Value = card.KeyCode;
|
||||
sheet.Cell(row, 2).Value = card.CardType;
|
||||
sheet.Cell(row, 3).Value = card.Status;
|
||||
sheet.Cell(row, 4).Value = card.ExpireTime?.ToString("O") ?? string.Empty;
|
||||
sheet.Cell(row, 5).Value = card.Note ?? string.Empty;
|
||||
row++;
|
||||
}
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
workbook.SaveAs(ms);
|
||||
var bytes = ms.ToArray();
|
||||
return File(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "cardkeys.xlsx");
|
||||
}
|
||||
else
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("keyCode,cardType,status,expireTime,note");
|
||||
foreach (var card in items)
|
||||
{
|
||||
sb.AppendLine($"{card.KeyCode},{card.CardType},{card.Status},{card.ExpireTime:O},{card.Note}");
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(sb.ToString());
|
||||
var contentType = ext == "txt" ? "text/plain" : "text/csv";
|
||||
var fileName = ext == "txt" ? "cardkeys.txt" : "cardkeys.csv";
|
||||
return File(bytes, contentType, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("import")]
|
||||
public async Task<IActionResult> Import([FromForm] IFormFile file, [FromForm] string projectId)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var project = await _db.Projects.FirstOrDefaultAsync(p => p.ProjectId == projectId);
|
||||
if (project == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(project.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
if (!project.IsEnabled)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(1011, "project_disabled"));
|
||||
|
||||
var pricing = await _db.ProjectPricing
|
||||
.Where(p => p.ProjectId == projectId && p.IsEnabled)
|
||||
.Select(p => new { p.CardType, p.DurationDays })
|
||||
.ToListAsync();
|
||||
var durationByType = pricing
|
||||
.GroupBy(p => p.CardType.Trim().ToLowerInvariant())
|
||||
.ToDictionary(g => g.Key, g => g.Select(x => x.DurationDays).Distinct().ToList());
|
||||
|
||||
(string cardType, int durationDays) ResolveCardMeta(string keyCode, string? rawCardType)
|
||||
{
|
||||
var normalizedType = string.IsNullOrWhiteSpace(rawCardType)
|
||||
? null
|
||||
: rawCardType.Trim().ToLowerInvariant();
|
||||
|
||||
var decodedDuration = 0;
|
||||
var decodedType = (string?)null;
|
||||
if (CardKeyGenerator.TryDecode(keyCode, out var keyType, out var keyDuration))
|
||||
{
|
||||
decodedDuration = keyDuration;
|
||||
decodedType = CardDefaults.ResolveCardType(keyType);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(normalizedType) && !string.IsNullOrWhiteSpace(decodedType))
|
||||
normalizedType = decodedType;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(normalizedType))
|
||||
normalizedType = "unknown";
|
||||
|
||||
if (decodedDuration > 0)
|
||||
return (normalizedType, decodedDuration);
|
||||
|
||||
if (durationByType.TryGetValue(normalizedType, out var durations) && durations.Count == 1)
|
||||
return (normalizedType, durations[0]);
|
||||
|
||||
return (normalizedType, CardDefaults.ResolveDurationDays(normalizedType));
|
||||
}
|
||||
|
||||
var successes = 0;
|
||||
var failures = new List<object>();
|
||||
var extension = Path.GetExtension(file.FileName);
|
||||
if (string.Equals(extension, ".xlsx", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
using var workbook = new XLWorkbook(file.OpenReadStream());
|
||||
var sheet = workbook.Worksheets.First();
|
||||
var rowIndex = 1;
|
||||
foreach (var row in sheet.RowsUsed())
|
||||
{
|
||||
var keyCell = row.Cell(1).GetString();
|
||||
if (rowIndex == 1 && keyCell.Equals("keyCode", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
rowIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(keyCell))
|
||||
{
|
||||
rowIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await _db.CardKeys.AnyAsync(c => c.KeyCode == keyCell))
|
||||
{
|
||||
failures.Add(new { row = rowIndex, keyCode = keyCell, reason = "exists" });
|
||||
rowIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var cardType = row.Cell(2).GetString();
|
||||
var status = row.Cell(3).GetString();
|
||||
var note = row.Cell(5).GetString();
|
||||
var meta = ResolveCardMeta(keyCell, cardType);
|
||||
|
||||
var card = new CardKey
|
||||
{
|
||||
ProjectId = projectId,
|
||||
KeyCode = keyCell.Trim(),
|
||||
CardType = meta.cardType,
|
||||
DurationDays = meta.durationDays,
|
||||
Status = string.IsNullOrWhiteSpace(status) ? "unused" : status.Trim(),
|
||||
Note = string.IsNullOrWhiteSpace(note) ? null : note.Trim(),
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_db.CardKeys.Add(card);
|
||||
successes++;
|
||||
rowIndex++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using var reader = new StreamReader(file.OpenReadStream());
|
||||
var lineNum = 0;
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var line = await reader.ReadLineAsync();
|
||||
lineNum++;
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
if (lineNum == 1 && line.StartsWith("keyCode", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
var parts = line.Split(',');
|
||||
if (parts.Length == 0)
|
||||
continue;
|
||||
|
||||
var keyCode = parts[0].Trim();
|
||||
if (string.IsNullOrWhiteSpace(keyCode))
|
||||
continue;
|
||||
|
||||
if (await _db.CardKeys.AnyAsync(c => c.KeyCode == keyCode))
|
||||
{
|
||||
failures.Add(new { row = lineNum, keyCode, reason = "exists" });
|
||||
continue;
|
||||
}
|
||||
|
||||
var cardType = parts.Length > 1 ? parts[1].Trim() : string.Empty;
|
||||
var meta = ResolveCardMeta(keyCode, cardType);
|
||||
var card = new CardKey
|
||||
{
|
||||
ProjectId = projectId,
|
||||
KeyCode = keyCode,
|
||||
CardType = meta.cardType,
|
||||
DurationDays = meta.durationDays,
|
||||
Status = parts.Length > 2 ? parts[2].Trim() : "unused",
|
||||
Note = parts.Length > 4 ? parts[4].Trim() : null,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_db.CardKeys.Add(card);
|
||||
successes++;
|
||||
}
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse<object>.Ok(new
|
||||
{
|
||||
total = successes + failures.Count,
|
||||
success = successes,
|
||||
failed = failures.Count,
|
||||
errors = failures
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using License.Api.Data;
|
||||
using License.Api.DTOs;
|
||||
using License.Api.Models;
|
||||
using License.Api.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace License.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(Policy = "Admin")]
|
||||
[Route("api/admin/devices")]
|
||||
public class AdminDevicesController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly AdminAccessService _adminAccess;
|
||||
|
||||
public AdminDevicesController(AppDbContext db, AdminAccessService adminAccess)
|
||||
{
|
||||
_db = db;
|
||||
_adminAccess = adminAccess;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List([FromQuery] string? projectId, [FromQuery] bool? isActive)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var query = _db.Devices.Where(d => d.DeletedAt == null).AsQueryable();
|
||||
if (isActive.HasValue)
|
||||
query = query.Where(d => d.IsActive == isActive.Value);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(projectId))
|
||||
{
|
||||
if (!scope.CanAccessProject(projectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
var cardIds = await _db.CardKeys.Where(c => c.ProjectId == projectId).Select(c => c.Id).ToListAsync();
|
||||
query = query.Where(d => cardIds.Contains(d.CardKeyId));
|
||||
}
|
||||
else if (!scope.IsSuperAdmin && !scope.HasAllProjects)
|
||||
{
|
||||
if (scope.AllowedProjects.Count == 0)
|
||||
return Ok(ApiResponse<List<Device>>.Ok(new List<Device>()));
|
||||
var allowed = scope.AllowedProjects.ToList();
|
||||
var cardIds = await _db.CardKeys
|
||||
.Where(c => c.ProjectId != null && allowed.Contains(c.ProjectId))
|
||||
.Select(c => c.Id)
|
||||
.ToListAsync();
|
||||
query = query.Where(d => cardIds.Contains(d.CardKeyId));
|
||||
}
|
||||
|
||||
var items = await query.OrderByDescending(d => d.LastHeartbeat).Take(200).ToListAsync();
|
||||
return Ok(ApiResponse<List<Device>>.Ok(items));
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
public async Task<IActionResult> Unbind(int id)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var device = await _db.Devices.FindAsync(id);
|
||||
if (device == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
|
||||
if (!scope.IsSuperAdmin && !scope.HasAllProjects)
|
||||
{
|
||||
var projectId = await _db.CardKeys.Where(c => c.Id == device.CardKeyId)
|
||||
.Select(c => c.ProjectId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (!scope.CanAccessProject(projectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
}
|
||||
|
||||
device.DeletedAt = DateTime.UtcNow;
|
||||
device.IsActive = false;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/kick")]
|
||||
public async Task<IActionResult> Kick(int id)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var device = await _db.Devices.FindAsync(id);
|
||||
if (device == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
|
||||
if (!scope.IsSuperAdmin && !scope.HasAllProjects)
|
||||
{
|
||||
var projectId = await _db.CardKeys.Where(c => c.Id == device.CardKeyId)
|
||||
.Select(c => c.ProjectId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (!scope.CanAccessProject(projectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
}
|
||||
|
||||
device.IsActive = false;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using License.Api.Data;
|
||||
using License.Api.DTOs;
|
||||
using License.Api.Models;
|
||||
using License.Api.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace License.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(Policy = "Admin")]
|
||||
[Route("api/admin/logs")]
|
||||
public class AdminLogsController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly AdminAccessService _adminAccess;
|
||||
|
||||
public AdminLogsController(AppDbContext db, AdminAccessService adminAccess)
|
||||
{
|
||||
_db = db;
|
||||
_adminAccess = adminAccess;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List([FromQuery] string? action, [FromQuery] int page = 1, [FromQuery] int pageSize = 20)
|
||||
{
|
||||
page = Math.Max(1, page);
|
||||
pageSize = Math.Clamp(pageSize, 1, 100);
|
||||
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var query = _db.AccessLogs.AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(action))
|
||||
query = query.Where(l => l.Action == action);
|
||||
if (!scope.IsSuperAdmin && !scope.HasAllProjects)
|
||||
{
|
||||
if (scope.AllowedProjects.Count == 0)
|
||||
{
|
||||
var empty = new PagedResult<AccessLog>
|
||||
{
|
||||
Items = new List<AccessLog>(),
|
||||
Pagination = new PaginationInfo
|
||||
{
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
Total = 0,
|
||||
TotalPages = 0
|
||||
}
|
||||
};
|
||||
return Ok(ApiResponse<PagedResult<AccessLog>>.Ok(empty));
|
||||
}
|
||||
var allowed = scope.AllowedProjects.ToList();
|
||||
query = query.Where(l => l.ProjectId != null && allowed.Contains(l.ProjectId));
|
||||
}
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var items = await query.OrderByDescending(l => l.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
var result = new PagedResult<AccessLog>
|
||||
{
|
||||
Items = items,
|
||||
Pagination = new PaginationInfo
|
||||
{
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
Total = total,
|
||||
TotalPages = (int)Math.Ceiling(total / (double)pageSize)
|
||||
}
|
||||
};
|
||||
|
||||
return Ok(ApiResponse<PagedResult<AccessLog>>.Ok(result));
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}")]
|
||||
public async Task<IActionResult> Get(int id)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var log = await _db.AccessLogs.FindAsync(id);
|
||||
if (log == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.IsSuperAdmin && !scope.HasAllProjects && !scope.CanAccessProject(log.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
return Ok(ApiResponse<AccessLog>.Ok(log));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
using License.Api.Data;
|
||||
using License.Api.DTOs;
|
||||
using License.Api.Models;
|
||||
using License.Api.Services;
|
||||
using License.Api.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace License.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(Policy = "Admin")]
|
||||
[Route("api/admin/projects")]
|
||||
public class AdminProjectsController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly SoftwareService _software;
|
||||
private readonly AdminAccessService _adminAccess;
|
||||
|
||||
public AdminProjectsController(AppDbContext db, SoftwareService software, AdminAccessService adminAccess)
|
||||
{
|
||||
_db = db;
|
||||
_software = software;
|
||||
_adminAccess = adminAccess;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] ProjectCreateRequest request)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var project = new Project
|
||||
{
|
||||
ProjectId = RandomIdGenerator.GenerateProjectId(),
|
||||
ProjectKey = RandomIdGenerator.GenerateKey(32),
|
||||
ProjectSecret = RandomIdGenerator.GenerateSecret(48),
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
MaxDevices = request.MaxDevices,
|
||||
AutoUpdate = request.AutoUpdate,
|
||||
IconUrl = request.IconUrl,
|
||||
CreatedBy = scope.Admin.Id,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_db.Projects.Add(project);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
if (!scope.IsSuperAdmin && !scope.HasAllProjects)
|
||||
{
|
||||
scope.AddProject(project.ProjectId);
|
||||
scope.Admin.Permissions = scope.SerializePermissions();
|
||||
scope.Admin.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var data = new
|
||||
{
|
||||
id = project.Id,
|
||||
projectId = project.ProjectId,
|
||||
projectKey = project.ProjectKey,
|
||||
projectSecret = project.ProjectSecret
|
||||
};
|
||||
|
||||
return Ok(ApiResponse<object>.Ok(data));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List([FromQuery] int page = 1, [FromQuery] int pageSize = 20)
|
||||
{
|
||||
page = Math.Max(1, page);
|
||||
pageSize = Math.Clamp(pageSize, 1, 100);
|
||||
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var query = _db.Projects.AsQueryable();
|
||||
if (!scope.IsSuperAdmin && !scope.HasAllProjects)
|
||||
{
|
||||
if (scope.AllowedProjects.Count == 0)
|
||||
{
|
||||
var empty = new PagedResult<ProjectListItem>
|
||||
{
|
||||
Items = new List<ProjectListItem>(),
|
||||
Pagination = new PaginationInfo
|
||||
{
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
Total = 0,
|
||||
TotalPages = 0
|
||||
}
|
||||
};
|
||||
return Ok(ApiResponse<PagedResult<ProjectListItem>>.Ok(empty));
|
||||
}
|
||||
var allowed = scope.AllowedProjects.ToList();
|
||||
query = query.Where(p => allowed.Contains(p.ProjectId));
|
||||
}
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var items = await query.OrderByDescending(p => p.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(p => new ProjectListItem
|
||||
{
|
||||
Id = p.Id,
|
||||
ProjectId = p.ProjectId,
|
||||
Name = p.Name,
|
||||
Description = p.Description,
|
||||
IconUrl = p.IconUrl,
|
||||
MaxDevices = p.MaxDevices,
|
||||
AutoUpdate = p.AutoUpdate,
|
||||
IsEnabled = p.IsEnabled,
|
||||
CreatedAt = p.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var result = new PagedResult<ProjectListItem>
|
||||
{
|
||||
Items = items,
|
||||
Pagination = new PaginationInfo
|
||||
{
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
Total = total,
|
||||
TotalPages = (int)Math.Ceiling(total / (double)pageSize)
|
||||
}
|
||||
};
|
||||
|
||||
return Ok(ApiResponse<PagedResult<ProjectListItem>>.Ok(result));
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}")]
|
||||
public async Task<IActionResult> Get(int id)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var project = await _db.Projects.FindAsync(id);
|
||||
if (project == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(project.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var data = new ProjectDetailResponse
|
||||
{
|
||||
Id = project.Id,
|
||||
ProjectId = project.ProjectId,
|
||||
ProjectKey = project.ProjectKey,
|
||||
Name = project.Name,
|
||||
Description = project.Description,
|
||||
IconUrl = project.IconUrl,
|
||||
MaxDevices = project.MaxDevices,
|
||||
AutoUpdate = project.AutoUpdate,
|
||||
IsEnabled = project.IsEnabled,
|
||||
CreatedAt = project.CreatedAt,
|
||||
UpdatedAt = project.UpdatedAt
|
||||
};
|
||||
|
||||
return Ok(ApiResponse<ProjectDetailResponse>.Ok(data));
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}")]
|
||||
public async Task<IActionResult> Update(int id, [FromBody] ProjectUpdateRequest request)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var project = await _db.Projects.FindAsync(id);
|
||||
if (project == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(project.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Name))
|
||||
project.Name = request.Name;
|
||||
if (!string.IsNullOrWhiteSpace(request.Description))
|
||||
project.Description = request.Description;
|
||||
if (request.MaxDevices.HasValue)
|
||||
project.MaxDevices = request.MaxDevices.Value;
|
||||
if (request.AutoUpdate.HasValue)
|
||||
project.AutoUpdate = request.AutoUpdate.Value;
|
||||
if (request.IsEnabled.HasValue)
|
||||
project.IsEnabled = request.IsEnabled.Value;
|
||||
if (!string.IsNullOrWhiteSpace(request.IconUrl))
|
||||
project.IconUrl = request.IconUrl;
|
||||
|
||||
project.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var project = await _db.Projects.FindAsync(id);
|
||||
if (project == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(project.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
project.IsEnabled = false;
|
||||
project.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/stats")]
|
||||
public async Task<IActionResult> Stats(int id)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var project = await _db.Projects.FindAsync(id);
|
||||
if (project == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(project.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var stats = await _db.Statistics
|
||||
.Where(s => s.ProjectId == project.ProjectId)
|
||||
.OrderByDescending(s => s.Date)
|
||||
.Take(30)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(ApiResponse<object>.Ok(stats));
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/docs")]
|
||||
public async Task<IActionResult> GetDocs(int id)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var project = await _db.Projects.FindAsync(id);
|
||||
if (project == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(project.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
return Ok(ApiResponse<object>.Ok(new { content = project.DocsContent ?? string.Empty }));
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}/docs")]
|
||||
public async Task<IActionResult> UpdateDocs(int id, [FromBody] ProjectDocUpdateRequest request)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var project = await _db.Projects.FindAsync(id);
|
||||
if (project == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(project.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
project.DocsContent = request.Content ?? string.Empty;
|
||||
project.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/pricing")]
|
||||
public async Task<IActionResult> GetPricing(int id)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var project = await _db.Projects.FindAsync(id);
|
||||
if (project == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(project.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var items = await _db.ProjectPricing.Where(p => p.ProjectId == project.ProjectId).ToListAsync();
|
||||
return Ok(ApiResponse<List<ProjectPricing>>.Ok(items));
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/pricing")]
|
||||
public async Task<IActionResult> CreatePricing(int id, [FromBody] ProjectPricingCreateRequest request)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var project = await _db.Projects.FindAsync(id);
|
||||
if (project == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(project.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var pricing = new ProjectPricing
|
||||
{
|
||||
ProjectId = project.ProjectId,
|
||||
CardType = request.CardType,
|
||||
DurationDays = request.DurationDays,
|
||||
OriginalPrice = request.OriginalPrice,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_db.ProjectPricing.Add(pricing);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse<ProjectPricing>.Ok(pricing));
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}/pricing/{priceId:int}")]
|
||||
public async Task<IActionResult> UpdatePricing(int id, int priceId, [FromBody] ProjectPricingUpdateRequest request)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var pricing = await _db.ProjectPricing.FindAsync(priceId);
|
||||
if (pricing == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(pricing.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
if (request.OriginalPrice.HasValue)
|
||||
pricing.OriginalPrice = request.OriginalPrice.Value;
|
||||
if (request.IsEnabled.HasValue)
|
||||
pricing.IsEnabled = request.IsEnabled.Value;
|
||||
|
||||
pricing.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}/pricing/{priceId:int}")]
|
||||
public async Task<IActionResult> DeletePricing(int id, int priceId)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var pricing = await _db.ProjectPricing.FindAsync(priceId);
|
||||
if (pricing == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(pricing.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
_db.ProjectPricing.Remove(pricing);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/versions")]
|
||||
public async Task<IActionResult> Versions(int id)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var project = await _db.Projects.FindAsync(id);
|
||||
if (project == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(project.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var items = await _db.SoftwareVersions
|
||||
.Where(v => v.ProjectId == project.ProjectId)
|
||||
.OrderByDescending(v => v.PublishedAt)
|
||||
.Select(v => new SoftwareVersionListItem
|
||||
{
|
||||
Id = v.Id,
|
||||
Version = v.Version,
|
||||
FileSize = v.FileSize,
|
||||
FileHash = v.FileHash,
|
||||
IsStable = v.IsStable,
|
||||
IsForceUpdate = v.IsForceUpdate,
|
||||
Changelog = v.Changelog,
|
||||
PublishedAt = v.PublishedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(ApiResponse<List<SoftwareVersionListItem>>.Ok(items));
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/versions")]
|
||||
[RequestSizeLimit(1024L * 1024L * 500L)]
|
||||
public async Task<IActionResult> UploadVersion(int id, [FromForm] string version, [FromForm] IFormFile file, [FromForm] string? changelog, [FromForm] bool isForceUpdate = false, [FromForm] bool isStable = true)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var project = await _db.Projects.FindAsync(id);
|
||||
if (project == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(project.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
if (!User.TryGetUserId(out var adminId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
var entity = await _software.CreateVersionAsync(project.ProjectId, version, file, changelog, isForceUpdate, isStable, adminId);
|
||||
|
||||
var data = new
|
||||
{
|
||||
versionId = entity.Id,
|
||||
version = entity.Version,
|
||||
fileUrl = entity.FileUrl,
|
||||
fileHash = entity.FileHash,
|
||||
encryptionKey = entity.EncryptionKey
|
||||
};
|
||||
|
||||
return Ok(ApiResponse<object>.Ok(data));
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}/versions/{versionId:int}")]
|
||||
public async Task<IActionResult> UpdateVersion(int id, int versionId, [FromBody] SoftwareVersionUpdateRequest update)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var version = await _db.SoftwareVersions.FindAsync(versionId);
|
||||
if (version == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(version.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
version.IsForceUpdate = update.IsForceUpdate;
|
||||
version.IsStable = update.IsStable;
|
||||
version.Changelog = update.Changelog;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}/versions/{versionId:int}")]
|
||||
public async Task<IActionResult> DeleteVersion(int id, int versionId)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var version = await _db.SoftwareVersions.FindAsync(versionId);
|
||||
if (version == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!scope.CanAccessProject(version.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
_db.SoftwareVersions.Remove(version);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
using License.Api.Data;
|
||||
using License.Api.DTOs;
|
||||
using License.Api.Models;
|
||||
using License.Api.Services;
|
||||
using License.Api.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace License.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(Policy = "SuperAdmin")]
|
||||
[Route("api/admin")]
|
||||
public class AdminSettingsController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly ConfigService _configService;
|
||||
|
||||
public AdminSettingsController(AppDbContext db, ConfigService configService)
|
||||
{
|
||||
_db = db;
|
||||
_configService = configService;
|
||||
}
|
||||
|
||||
[HttpGet("settings")]
|
||||
public async Task<IActionResult> GetSettings()
|
||||
{
|
||||
var configs = await _db.SystemConfigs.OrderBy(c => c.Category).ToListAsync();
|
||||
return Ok(ApiResponse<List<SystemConfig>>.Ok(configs));
|
||||
}
|
||||
|
||||
[HttpPut("settings")]
|
||||
public async Task<IActionResult> UpdateSettings([FromBody] List<SystemConfig> configs)
|
||||
{
|
||||
foreach (var config in configs)
|
||||
{
|
||||
var existing = await _db.SystemConfigs.FirstOrDefaultAsync(c => c.ConfigKey == config.ConfigKey);
|
||||
if (existing == null)
|
||||
{
|
||||
config.UpdatedAt = DateTime.UtcNow;
|
||||
_db.SystemConfigs.Add(config);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.ConfigValue = config.ConfigValue;
|
||||
existing.ValueType = config.ValueType;
|
||||
existing.Category = config.Category;
|
||||
existing.DisplayName = config.DisplayName;
|
||||
existing.Description = config.Description;
|
||||
existing.Options = config.Options;
|
||||
existing.IsPublic = config.IsPublic;
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
_configService.Invalidate(config.ConfigKey);
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpGet("admins")]
|
||||
public async Task<IActionResult> Admins()
|
||||
{
|
||||
var items = await _db.Admins
|
||||
.OrderBy(a => a.Id)
|
||||
.Select(a => new
|
||||
{
|
||||
id = a.Id,
|
||||
username = a.Username,
|
||||
email = a.Email,
|
||||
role = a.Role,
|
||||
permissions = a.Permissions,
|
||||
status = a.Status,
|
||||
lastLoginAt = a.LastLoginAt,
|
||||
createdAt = a.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var data = items.Select(a =>
|
||||
{
|
||||
var permissions = ResolvePermissions(a.role, a.permissions);
|
||||
return new
|
||||
{
|
||||
a.id,
|
||||
a.username,
|
||||
a.email,
|
||||
a.role,
|
||||
permissions,
|
||||
a.status,
|
||||
a.lastLoginAt,
|
||||
a.createdAt
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return Ok(ApiResponse<object>.Ok(data));
|
||||
}
|
||||
|
||||
[HttpPost("admins")]
|
||||
public async Task<IActionResult> CreateAdmin([FromBody] AdminCreateRequest request)
|
||||
{
|
||||
var admin = new Admin
|
||||
{
|
||||
Username = request.Username,
|
||||
PasswordHash = PasswordHasher.Hash(request.Password),
|
||||
Email = request.Email,
|
||||
Role = request.Role,
|
||||
Permissions = request.Permissions,
|
||||
Status = "active",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_db.Admins.Add(admin);
|
||||
await _db.SaveChangesAsync();
|
||||
return Ok(ApiResponse<object>.Ok(new
|
||||
{
|
||||
id = admin.Id,
|
||||
username = admin.Username,
|
||||
email = admin.Email,
|
||||
role = admin.Role,
|
||||
permissions = ResolvePermissions(admin.Role, admin.Permissions),
|
||||
status = admin.Status
|
||||
}));
|
||||
}
|
||||
|
||||
[HttpPut("admins/{id:int}")]
|
||||
public async Task<IActionResult> UpdateAdmin(int id, [FromBody] AdminUpdateRequest request)
|
||||
{
|
||||
var admin = await _db.Admins.FindAsync(id);
|
||||
if (admin == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Email))
|
||||
admin.Email = request.Email;
|
||||
if (!string.IsNullOrWhiteSpace(request.Role))
|
||||
admin.Role = request.Role;
|
||||
if (!string.IsNullOrWhiteSpace(request.Permissions))
|
||||
admin.Permissions = request.Permissions;
|
||||
if (!string.IsNullOrWhiteSpace(request.Status))
|
||||
admin.Status = request.Status;
|
||||
|
||||
admin.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[HttpDelete("admins/{id:int}")]
|
||||
public async Task<IActionResult> DeleteAdmin(int id)
|
||||
{
|
||||
var admin = await _db.Admins.FindAsync(id);
|
||||
if (admin == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
|
||||
_db.Admins.Remove(admin);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
private static List<string> ResolvePermissions(string role, string? raw)
|
||||
{
|
||||
if (string.Equals(role, "super_admin", StringComparison.OrdinalIgnoreCase))
|
||||
return new List<string> { "*" };
|
||||
|
||||
var (hasAll, allowed) = AdminAccessService.ParsePermissions(raw);
|
||||
if (hasAll)
|
||||
return new List<string> { "*" };
|
||||
return allowed.OrderBy(p => p).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using License.Api.DTOs;
|
||||
using License.Api.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace License.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(Policy = "Admin")]
|
||||
[Route("api/admin/stats")]
|
||||
public class AdminStatsController : ControllerBase
|
||||
{
|
||||
private readonly StatsService _stats;
|
||||
private readonly AdminAccessService _adminAccess;
|
||||
|
||||
public AdminStatsController(StatsService stats, AdminAccessService adminAccess)
|
||||
{
|
||||
_stats = stats;
|
||||
_adminAccess = adminAccess;
|
||||
}
|
||||
|
||||
[HttpGet("dashboard")]
|
||||
public async Task<IActionResult> Dashboard()
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var filter = !scope.IsSuperAdmin && !scope.HasAllProjects ? scope.AllowedProjects.ToList() : null;
|
||||
var data = await _stats.GetDashboardAsync(filter);
|
||||
return Ok(ApiResponse<object>.Ok(data));
|
||||
}
|
||||
|
||||
[HttpGet("projects")]
|
||||
public async Task<IActionResult> Projects()
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var filter = !scope.IsSuperAdmin && !scope.HasAllProjects ? scope.AllowedProjects.ToList() : null;
|
||||
var items = await _stats.GetProjectStatsAsync(filter);
|
||||
return Ok(ApiResponse<List<ProjectStatsItem>>.Ok(items));
|
||||
}
|
||||
|
||||
[HttpGet("agents")]
|
||||
public async Task<IActionResult> Agents()
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
if (!scope.IsSuperAdmin)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var items = await _stats.GetAgentStatsAsync();
|
||||
return Ok(ApiResponse<List<AgentStatsItem>>.Ok(items));
|
||||
}
|
||||
|
||||
[HttpGet("logs")]
|
||||
public async Task<IActionResult> Logs([FromQuery] int days = 7)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var filter = !scope.IsSuperAdmin && !scope.HasAllProjects ? scope.AllowedProjects.ToList() : null;
|
||||
var items = await _stats.GetLogStatsAsync(days, filter);
|
||||
return Ok(ApiResponse<List<LogStatsItem>>.Ok(items));
|
||||
}
|
||||
|
||||
[HttpGet("export")]
|
||||
public async Task<IActionResult> Export([FromQuery] int days = 30)
|
||||
{
|
||||
var scope = await _adminAccess.GetScopeAsync(User);
|
||||
if (scope == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var filter = !scope.IsSuperAdmin && !scope.HasAllProjects ? scope.AllowedProjects.ToList() : null;
|
||||
var csv = await _stats.ExportStatsCsvAsync(days, filter);
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(csv);
|
||||
return File(bytes, "text/csv", "stats.csv");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using System.Text.Json;
|
||||
using License.Api.Data;
|
||||
using License.Api.DTOs;
|
||||
using License.Api.Models;
|
||||
using License.Api.Security;
|
||||
using License.Api.Services;
|
||||
using License.Api.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace License.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/agent")]
|
||||
public class AgentAuthController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly JwtTokenService _jwt;
|
||||
private readonly ConfigService _config;
|
||||
|
||||
public AgentAuthController(AppDbContext db, JwtTokenService jwt, ConfigService config)
|
||||
{
|
||||
_db = db;
|
||||
_jwt = jwt;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> Login([FromBody] AgentLoginRequest request)
|
||||
{
|
||||
var agentSystemEnabled = await _config.GetBoolAsync("feature.agent_system", true);
|
||||
if (!agentSystemEnabled)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var agent = await _db.Agents.FirstOrDefaultAsync(a => a.AgentCode == request.AgentCode);
|
||||
if (agent == null || !PasswordHasher.Verify(request.Password, agent.PasswordHash))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
if (agent.Status != "active")
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
agent.LastLoginAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
await LogAccessAsync($"agent:{agent.AgentCode}", "agent_login");
|
||||
|
||||
var allowed = string.IsNullOrWhiteSpace(agent.AllowedProjects)
|
||||
? new List<string>()
|
||||
: JsonSerializer.Deserialize<List<string>>(agent.AllowedProjects) ?? new List<string>();
|
||||
|
||||
var projects = await _db.Projects
|
||||
.Where(p => allowed.Count == 0 || allowed.Contains(p.ProjectId))
|
||||
.Select(p => new { projectId = p.ProjectId, projectName = p.Name })
|
||||
.ToListAsync();
|
||||
|
||||
var token = _jwt.CreateAgentToken(agent);
|
||||
var data = new
|
||||
{
|
||||
token,
|
||||
agent = new
|
||||
{
|
||||
id = agent.Id,
|
||||
agentCode = agent.AgentCode,
|
||||
companyName = agent.CompanyName,
|
||||
balance = agent.Balance,
|
||||
discount = agent.Discount
|
||||
},
|
||||
allowedProjects = projects
|
||||
};
|
||||
|
||||
return Ok(ApiResponse<object>.Ok(data));
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Agent")]
|
||||
[HttpPost("logout")]
|
||||
public IActionResult Logout()
|
||||
{
|
||||
var agentCode = User.FindFirst("agentCode")?.Value ?? "agent";
|
||||
_ = LogAccessAsync($"agent:{agentCode}", "agent_logout");
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Agent")]
|
||||
[HttpGet("profile")]
|
||||
public async Task<IActionResult> Profile()
|
||||
{
|
||||
if (!User.TryGetUserId(out var agentId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
var agent = await _db.Agents.FindAsync(agentId);
|
||||
if (agent == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
|
||||
return Ok(ApiResponse<object>.Ok(new
|
||||
{
|
||||
id = agent.Id,
|
||||
agentCode = agent.AgentCode,
|
||||
companyName = agent.CompanyName,
|
||||
contactPerson = agent.ContactPerson,
|
||||
contactPhone = agent.ContactPhone,
|
||||
contactEmail = agent.ContactEmail,
|
||||
balance = agent.Balance,
|
||||
discount = agent.Discount,
|
||||
creditLimit = agent.CreditLimit,
|
||||
status = agent.Status
|
||||
}));
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Agent")]
|
||||
[HttpPut("profile")]
|
||||
public async Task<IActionResult> UpdateProfile([FromBody] AgentUpdateRequest request)
|
||||
{
|
||||
if (!User.TryGetUserId(out var agentId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
var agent = await _db.Agents.FindAsync(agentId);
|
||||
if (agent == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.CompanyName))
|
||||
agent.CompanyName = request.CompanyName;
|
||||
if (!string.IsNullOrWhiteSpace(request.ContactPerson))
|
||||
agent.ContactPerson = request.ContactPerson;
|
||||
if (!string.IsNullOrWhiteSpace(request.ContactPhone))
|
||||
agent.ContactPhone = request.ContactPhone;
|
||||
if (!string.IsNullOrWhiteSpace(request.ContactEmail))
|
||||
agent.ContactEmail = request.ContactEmail;
|
||||
|
||||
agent.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Agent")]
|
||||
[HttpPost("change-password")]
|
||||
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordRequest request)
|
||||
{
|
||||
if (!User.TryGetUserId(out var agentId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
var agent = await _db.Agents.FindAsync(agentId);
|
||||
if (agent == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
|
||||
if (!PasswordHasher.Verify(request.OldPassword, agent.PasswordHash))
|
||||
return BadRequest(ApiResponse.Fail(400, "bad_request"));
|
||||
|
||||
agent.PasswordHash = PasswordHasher.Hash(request.NewPassword);
|
||||
agent.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(ApiResponse.Ok());
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Agent")]
|
||||
[HttpGet("transactions")]
|
||||
public async Task<IActionResult> Transactions()
|
||||
{
|
||||
if (!User.TryGetUserId(out var agentId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
var items = await _db.AgentTransactions
|
||||
.Where(t => t.AgentId == agentId)
|
||||
.OrderByDescending(t => t.CreatedAt)
|
||||
.Take(200)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(ApiResponse<List<AgentTransaction>>.Ok(items));
|
||||
}
|
||||
|
||||
private async Task LogAccessAsync(string? deviceId, string action)
|
||||
{
|
||||
_db.AccessLogs.Add(new AccessLog
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
Action = action,
|
||||
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
UserAgent = HttpContext.Request.Headers.UserAgent.ToString(),
|
||||
ResponseCode = 200,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
using System.Text.Json;
|
||||
using License.Api.Data;
|
||||
using License.Api.DTOs;
|
||||
using License.Api.Models;
|
||||
using License.Api.Services;
|
||||
using License.Api.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace License.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(Policy = "Agent")]
|
||||
[Route("api/agent/cards")]
|
||||
public class AgentCardsController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly CardService _cards;
|
||||
private readonly IdempotencyService _idempotency;
|
||||
private readonly ConfigService _config;
|
||||
|
||||
public AgentCardsController(AppDbContext db, CardService cards, IdempotencyService idempotency, ConfigService config)
|
||||
{
|
||||
_db = db;
|
||||
_cards = cards;
|
||||
_idempotency = idempotency;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
[HttpPost("generate")]
|
||||
public async Task<IActionResult> Generate([FromBody] CardGenerateRequest request)
|
||||
{
|
||||
var agentSystemEnabled = await _config.GetBoolAsync("feature.agent_system", true);
|
||||
if (!agentSystemEnabled)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
if (request.Quantity <= 0 || request.Quantity > 10000)
|
||||
return BadRequest(ApiResponse.Fail(400, "bad_request"));
|
||||
|
||||
var project = await _db.Projects.FirstOrDefaultAsync(p => p.ProjectId == request.ProjectId);
|
||||
if (project == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!project.IsEnabled)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(1011, "project_disabled"));
|
||||
|
||||
if (!User.TryGetUserId(out var agentId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
var agent = await _db.Agents.FindAsync(agentId);
|
||||
if (agent == null || agent.Status != "active")
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var allowedProjects = string.IsNullOrWhiteSpace(agent.AllowedProjects)
|
||||
? new List<string>()
|
||||
: JsonSerializer.Deserialize<List<string>>(agent.AllowedProjects) ?? new List<string>();
|
||||
|
||||
if (allowedProjects.Count > 0 && !allowedProjects.Contains(project.ProjectId))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var pricing = await _db.ProjectPricing.FirstOrDefaultAsync(p =>
|
||||
p.ProjectId == project.ProjectId &&
|
||||
p.CardType == request.CardType &&
|
||||
p.DurationDays == request.DurationDays &&
|
||||
p.IsEnabled);
|
||||
if (pricing == null)
|
||||
return BadRequest(ApiResponse.Fail(400, "bad_request"));
|
||||
|
||||
var unitPrice = pricing.OriginalPrice * (agent.Discount / 100m);
|
||||
var totalCost = unitPrice * request.Quantity;
|
||||
|
||||
var requestHash = IdempotencyService.ComputeRequestHash(JsonSerializer.Serialize(request));
|
||||
var idempotencyKey = Request.Headers["X-Idempotency-Key"].ToString();
|
||||
if (!string.IsNullOrWhiteSpace(idempotencyKey))
|
||||
{
|
||||
var existing = await _idempotency.GetAsync(idempotencyKey);
|
||||
if (existing != null)
|
||||
{
|
||||
if (!string.Equals(existing.RequestHash, requestHash, StringComparison.OrdinalIgnoreCase))
|
||||
return Conflict(ApiResponse.Fail(400, "bad_request"));
|
||||
|
||||
var cached = JsonSerializer.Deserialize<ApiResponse<AgentCardGenerateResponse>>(existing.ResponseBody ?? "{}")
|
||||
?? ApiResponse<AgentCardGenerateResponse>.Fail(500, "internal_error");
|
||||
return StatusCode(existing.ResponseCode ?? 200, cached);
|
||||
}
|
||||
}
|
||||
|
||||
await using var tx = await _db.Database.BeginTransactionAsync();
|
||||
var lockedAgent = await _db.Agents
|
||||
.FromSqlRaw("SELECT * FROM \"Agents\" WHERE \"Id\" = {0} FOR UPDATE", agentId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (lockedAgent == null)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
var balanceBefore = lockedAgent.Balance;
|
||||
var balanceAfter = balanceBefore - totalCost;
|
||||
if (balanceAfter < -lockedAgent.CreditLimit)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
lockedAgent.Balance = balanceAfter;
|
||||
lockedAgent.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
_db.AgentTransactions.Add(new AgentTransaction
|
||||
{
|
||||
AgentId = lockedAgent.Id,
|
||||
Type = "consume",
|
||||
Amount = -totalCost,
|
||||
BalanceBefore = balanceBefore,
|
||||
BalanceAfter = balanceAfter,
|
||||
Remark = $"generate_cards:{request.CardType}:{request.DurationDays}x{request.Quantity}",
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
var generated = await _cards.GenerateAsync(request, agentId, lockedAgent.Id, unitPrice, "agent");
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
await tx.CommitAsync();
|
||||
|
||||
var response = new AgentCardGenerateResponse
|
||||
{
|
||||
BatchId = generated.BatchId,
|
||||
Keys = generated.Keys,
|
||||
Count = generated.Count,
|
||||
UnitPrice = unitPrice,
|
||||
TotalPrice = totalCost,
|
||||
BalanceAfter = balanceAfter
|
||||
};
|
||||
|
||||
var apiResponse = ApiResponse<AgentCardGenerateResponse>.Ok(response);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(idempotencyKey))
|
||||
{
|
||||
var body = JsonSerializer.Serialize(apiResponse);
|
||||
await _idempotency.StoreAsync(idempotencyKey, Request.Path, requestHash, 200, body);
|
||||
}
|
||||
|
||||
return Ok(apiResponse);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List([FromQuery] string? projectId, [FromQuery] string? status, [FromQuery] int page = 1, [FromQuery] int pageSize = 20)
|
||||
{
|
||||
var agentSystemEnabled = await _config.GetBoolAsync("feature.agent_system", true);
|
||||
if (!agentSystemEnabled)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "forbidden"));
|
||||
|
||||
page = Math.Max(1, page);
|
||||
pageSize = Math.Clamp(pageSize, 1, 100);
|
||||
|
||||
if (!User.TryGetUserId(out var agentId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var query = _db.CardKeys.Where(c => c.AgentId == agentId && c.DeletedAt == null).AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(projectId))
|
||||
query = query.Where(c => c.ProjectId == projectId);
|
||||
if (!string.IsNullOrWhiteSpace(status))
|
||||
query = query.Where(c => c.Status == status);
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var items = await query.OrderByDescending(c => c.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(c => new AgentCardListItem
|
||||
{
|
||||
Id = c.Id,
|
||||
KeyCode = c.KeyCode,
|
||||
CardType = c.CardType,
|
||||
Status = c.Status,
|
||||
ActivateTime = c.ActivateTime,
|
||||
ExpireTime = c.ExpireTime,
|
||||
Note = c.Note,
|
||||
CreatedAt = c.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var result = new PagedResult<AgentCardListItem>
|
||||
{
|
||||
Items = items,
|
||||
Pagination = new PaginationInfo
|
||||
{
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
Total = total,
|
||||
TotalPages = (int)Math.Ceiling(total / (double)pageSize)
|
||||
}
|
||||
};
|
||||
|
||||
return Ok(ApiResponse<PagedResult<AgentCardListItem>>.Ok(result));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using License.Api.DTOs;
|
||||
using License.Api.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace License.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/auth")]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
private readonly AuthService _auth;
|
||||
|
||||
public AuthController(AuthService auth)
|
||||
{
|
||||
_auth = auth;
|
||||
}
|
||||
|
||||
[HttpPost("verify")]
|
||||
public async Task<IActionResult> Verify([FromBody] AuthVerifyRequest request)
|
||||
{
|
||||
var (response, status) = await _auth.VerifyAsync(request, HttpContext);
|
||||
return StatusCode(status, response);
|
||||
}
|
||||
|
||||
[HttpPost("heartbeat")]
|
||||
public async Task<IActionResult> Heartbeat([FromBody] AuthHeartbeatRequest request)
|
||||
{
|
||||
var (response, status) = await _auth.HeartbeatAsync(request, HttpContext);
|
||||
return StatusCode(status, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using License.Api.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace License.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("health")]
|
||||
public class HealthController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IConnectionMultiplexer? _redis;
|
||||
|
||||
public HealthController(AppDbContext db, IConnectionMultiplexer? redis = null)
|
||||
{
|
||||
_db = db;
|
||||
_redis = redis;
|
||||
}
|
||||
|
||||
[HttpGet("live")]
|
||||
public IActionResult Live()
|
||||
{
|
||||
return Ok(new { status = "ok" });
|
||||
}
|
||||
|
||||
[HttpGet("ready")]
|
||||
public async Task<IActionResult> Ready()
|
||||
{
|
||||
var checks = new Dictionary<string, string>();
|
||||
try
|
||||
{
|
||||
await _db.Database.ExecuteSqlRawAsync("SELECT 1");
|
||||
checks["database"] = "ok";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
checks["database"] = $"error: {ex.Message}";
|
||||
}
|
||||
|
||||
if (_redis != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var db = _redis.GetDatabase();
|
||||
await db.PingAsync();
|
||||
checks["redis"] = "ok";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
checks["redis"] = $"error: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
var status = checks.Values.All(v => v == "ok") ? "ok" : "error";
|
||||
var code = status == "ok" ? 200 : 503;
|
||||
|
||||
return StatusCode(code, new { status, checks });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using License.Api.Data;
|
||||
using License.Api.DTOs;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace License.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/config")]
|
||||
public class PublicConfigController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public PublicConfigController(AppDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[HttpGet("public")]
|
||||
public async Task<IActionResult> GetPublicConfigs()
|
||||
{
|
||||
var configs = await _db.SystemConfigs
|
||||
.AsNoTracking()
|
||||
.Where(c => c.IsPublic)
|
||||
.OrderBy(c => c.Category)
|
||||
.ThenBy(c => c.ConfigKey)
|
||||
.Select(c => new
|
||||
{
|
||||
key = c.ConfigKey,
|
||||
value = c.ConfigValue,
|
||||
valueType = c.ValueType,
|
||||
category = c.Category,
|
||||
displayName = c.DisplayName,
|
||||
description = c.Description,
|
||||
options = c.Options
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(ApiResponse<object>.Ok(configs));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using License.Api.Data;
|
||||
using License.Api.DTOs;
|
||||
using License.Api.Models;
|
||||
using License.Api.Options;
|
||||
using License.Api.Security;
|
||||
using License.Api.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace License.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/software")]
|
||||
public class SoftwareController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly SoftwareService _software;
|
||||
private readonly JwtTokenService _jwt;
|
||||
private readonly ILogger<SoftwareController> _logger;
|
||||
private readonly ConfigService _config;
|
||||
private readonly StorageOptions _storage;
|
||||
|
||||
public SoftwareController(AppDbContext db, SoftwareService software, JwtTokenService jwt, ILogger<SoftwareController> logger, ConfigService config, IOptions<StorageOptions> storageOptions)
|
||||
{
|
||||
_db = db;
|
||||
_software = software;
|
||||
_jwt = jwt;
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_storage = storageOptions.Value;
|
||||
}
|
||||
|
||||
[HttpPost("check-update")]
|
||||
public async Task<IActionResult> CheckUpdate([FromBody] SoftwareCheckUpdateRequest request)
|
||||
{
|
||||
var project = await _db.Projects.FirstOrDefaultAsync(p => p.ProjectId == request.ProjectId);
|
||||
if (project == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
if (!project.IsEnabled)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(1011, "project_disabled"));
|
||||
|
||||
var autoUpdateEnabled = (await _config.GetBoolAsync("feature.auto_update", true)) && project.AutoUpdate;
|
||||
if (!autoUpdateEnabled)
|
||||
{
|
||||
var disabled = new SoftwareCheckUpdateResponse { HasUpdate = false };
|
||||
return Ok(ApiResponse<SoftwareCheckUpdateResponse>.Ok(disabled));
|
||||
}
|
||||
|
||||
var data = await _software.CheckUpdateAsync(request);
|
||||
var forceUpdateEnabled = await _config.GetBoolAsync("feature.force_update", false);
|
||||
if (forceUpdateEnabled)
|
||||
data.ForceUpdate = true;
|
||||
return Ok(ApiResponse<SoftwareCheckUpdateResponse>.Ok(data));
|
||||
}
|
||||
|
||||
[HttpGet("download")]
|
||||
public async Task<IActionResult> Download([FromQuery] string? version, [FromQuery] string? token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var principal = _jwt.ValidateToken(token);
|
||||
if (principal == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var type = principal.Claims.FirstOrDefault(c => c.Type == "type")?.Value;
|
||||
if (!string.Equals(type, "card", StringComparison.OrdinalIgnoreCase))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var projectId = principal.Claims.FirstOrDefault(c => c.Type == "projectId")?.Value;
|
||||
if (string.IsNullOrWhiteSpace(projectId))
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
var cardIdStr = principal.Claims.FirstOrDefault(c => c.Type == System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames.Sub)?.Value;
|
||||
int.TryParse(cardIdStr, out var cardId);
|
||||
|
||||
if (cardId > 0)
|
||||
{
|
||||
var card = await _db.CardKeys.FirstOrDefaultAsync(c => c.Id == cardId && c.DeletedAt == null);
|
||||
if (card == null)
|
||||
return Unauthorized(ApiResponse.Fail(401, "unauthorized"));
|
||||
|
||||
if (card.Status == "banned")
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(1003, "card_banned"));
|
||||
|
||||
if (card.ExpireTime.HasValue && card.ExpireTime <= DateTime.UtcNow)
|
||||
{
|
||||
card.Status = "expired";
|
||||
await _db.SaveChangesAsync();
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(1002, "card_expired"));
|
||||
}
|
||||
}
|
||||
|
||||
var software = await _software.GetVersionAsync(projectId, version);
|
||||
if (software == null)
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
|
||||
try
|
||||
{
|
||||
var filePath = software.FileUrl;
|
||||
if (!System.IO.File.Exists(filePath))
|
||||
return NotFound(ApiResponse.Fail(404, "not_found"));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(software.FileHash))
|
||||
Response.Headers["X-File-Hash"] = software.FileHash;
|
||||
|
||||
if (software.FileSize.HasValue)
|
||||
Response.Headers["X-File-Size"] = software.FileSize.Value.ToString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(software.EncryptionKey))
|
||||
{
|
||||
if (_storage.RequireHttpsForDownloadKey && !IsSecureRequest(Request))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail(403, "https_required"));
|
||||
|
||||
Response.Headers["X-Encryption-Method"] = "AES-256-GCM";
|
||||
Response.Headers["X-Encryption-Key"] = software.EncryptionKey;
|
||||
using var fs = System.IO.File.OpenRead(filePath);
|
||||
var nonce = new byte[12];
|
||||
var read = await fs.ReadAsync(nonce, 0, nonce.Length);
|
||||
if (read == nonce.Length)
|
||||
Response.Headers["X-Encryption-Nonce"] = Convert.ToBase64String(nonce);
|
||||
}
|
||||
|
||||
_db.AccessLogs.Add(new AccessLog
|
||||
{
|
||||
ProjectId = projectId,
|
||||
CardKeyId = cardId > 0 ? cardId : null,
|
||||
Action = "download",
|
||||
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
UserAgent = HttpContext.Request.Headers.UserAgent.ToString(),
|
||||
ResponseCode = 200,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return PhysicalFile(filePath, "application/octet-stream", enableRangeProcessing: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to download file");
|
||||
return StatusCode(500, ApiResponse.Fail(500, "internal_error"));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSecureRequest(HttpRequest request)
|
||||
{
|
||||
if (request.IsHttps)
|
||||
return true;
|
||||
|
||||
var forwardedProto = request.Headers["X-Forwarded-Proto"].ToString();
|
||||
return string.Equals(forwardedProto, "https", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user