initialize admin panel and necessry pages
This commit is contained in:
126
src/GreenHome.Api/Controllers/AdminController.cs
Normal file
126
src/GreenHome.Api/Controllers/AdminController.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using GreenHome.Api.Filters;
|
||||
using GreenHome.Application;
|
||||
using GreenHome.Application.Dtos.Admin;
|
||||
using GreenHome.Domain;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace GreenHome.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin")]
|
||||
[ServiceFilter(typeof(AdminAuthFilter))]
|
||||
public sealed class AdminController : ControllerBase
|
||||
{
|
||||
private readonly IAdminService _adminService;
|
||||
|
||||
public AdminController(IAdminService adminService)
|
||||
{
|
||||
_adminService = adminService;
|
||||
}
|
||||
|
||||
// ==================== Dashboard ====================
|
||||
|
||||
[HttpGet("dashboard")]
|
||||
public async Task<ActionResult<AdminDashboardDto>> Dashboard(CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _adminService.GetDashboardAsync(cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// ==================== Users ====================
|
||||
|
||||
[HttpGet("users")]
|
||||
public async Task<ActionResult<PagedResult<AdminUserListItemDto>>> Users(
|
||||
[FromQuery] string? search,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filter = new AdminUserFilter
|
||||
{
|
||||
Search = search,
|
||||
Page = Math.Max(1, page),
|
||||
PageSize = Math.Clamp(pageSize, 1, 100)
|
||||
};
|
||||
|
||||
var result = await _adminService.GetUsersAsync(filter, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("users/{id:int}")]
|
||||
public async Task<ActionResult<AdminUserDetailDto>> GetUser(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _adminService.GetUserAsync(id, cancellationToken);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpPut("users/{id:int}/role")]
|
||||
public async Task<IActionResult> SetUserRole(int id, [FromBody] ChangeUserRoleRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var currentAdminId = ((UserDto)HttpContext.Items["AdminUser"]!).Id;
|
||||
var outcome = await _adminService.SetUserRoleAsync(id, request.Role, currentAdminId, cancellationToken);
|
||||
return outcome switch
|
||||
{
|
||||
UserMutationOutcome.Ok => Ok(new { success = true }),
|
||||
UserMutationOutcome.NotFound => NotFound(),
|
||||
UserMutationOutcome.ConflictSelf => Conflict(new { message = "نمیتوانید نقش خود را تغییر دهید" }),
|
||||
UserMutationOutcome.ConflictLastAdmin => Conflict(new { message = "آخرین مدیر را نمیتوان تغییر نقش داد" }),
|
||||
_ => NotFound()
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPut("users/{id:int}/status")]
|
||||
public async Task<IActionResult> SetUserStatus(int id, [FromBody] ChangeUserStatusRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var currentAdminId = ((UserDto)HttpContext.Items["AdminUser"]!).Id;
|
||||
var outcome = await _adminService.SetUserStatusAsync(id, request.IsEnabled, currentAdminId, cancellationToken);
|
||||
return outcome switch
|
||||
{
|
||||
UserMutationOutcome.Ok => Ok(new { success = true }),
|
||||
UserMutationOutcome.NotFound => NotFound(),
|
||||
UserMutationOutcome.ConflictSelf => Conflict(new { message = "نمیتوانید اکانت خود را غیرفعال کنید" }),
|
||||
UserMutationOutcome.ConflictLastAdmin => Conflict(new { message = "آخرین مدیر فعال را نمیتوان غیرفعال کرد" }),
|
||||
_ => NotFound()
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== Devices ====================
|
||||
|
||||
[HttpGet("devices")]
|
||||
public async Task<ActionResult<PagedResult<AdminDeviceListItemDto>>> Devices(
|
||||
[FromQuery] string? search,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filter = new AdminDeviceFilter
|
||||
{
|
||||
Search = search,
|
||||
Page = Math.Max(1, page),
|
||||
PageSize = Math.Clamp(pageSize, 1, 100)
|
||||
};
|
||||
|
||||
var result = await _adminService.GetDevicesAsync(filter, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("devices/{id:int}")]
|
||||
public async Task<ActionResult<AdminDeviceDetailDto>> GetDevice(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _adminService.GetDeviceAsync(id, cancellationToken);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
// ==================== Usage ====================
|
||||
|
||||
[HttpGet("usage")]
|
||||
public async Task<ActionResult<IReadOnlyList<UsageDayDto>>> Usage(
|
||||
[FromQuery] DateTime? from,
|
||||
[FromQuery] DateTime? to,
|
||||
[FromQuery] int? userId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _adminService.GetUsageAsync(from, to, userId, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
60
src/GreenHome.Api/Filters/AdminAuthFilter.cs
Normal file
60
src/GreenHome.Api/Filters/AdminAuthFilter.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using GreenHome.Application;
|
||||
using GreenHome.Domain;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace GreenHome.Api.Filters;
|
||||
|
||||
/// <summary>
|
||||
/// Guards all api/admin/* endpoints:
|
||||
/// - missing/invalid/expired token -> 401
|
||||
/// - valid token but Role != Admin -> 403
|
||||
/// Role is always read from the server-side user lookup (IAuthService.GetUserByTokenAsync
|
||||
/// does a DB lookup), never from client-provided claims.
|
||||
/// </summary>
|
||||
public sealed class AdminAuthFilter : IAsyncActionFilter
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
|
||||
public AdminAuthFilter(IAuthService authService)
|
||||
{
|
||||
_authService = authService;
|
||||
}
|
||||
|
||||
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||
{
|
||||
var header = context.HttpContext.Request.Headers.Authorization.ToString();
|
||||
var token = header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
|
||||
? header["Bearer ".Length..].Trim()
|
||||
: null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
context.Result = new UnauthorizedObjectResult(new { message = "Token ارسال نشده است" });
|
||||
return;
|
||||
}
|
||||
|
||||
var user = await _authService.GetUserByTokenAsync(token, context.HttpContext.RequestAborted);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
// invalid/expired token OR disabled user
|
||||
context.Result = new UnauthorizedObjectResult(new { message = "Token نامعتبر است" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.Role != UserRole.Admin)
|
||||
{
|
||||
context.Result = new ObjectResult(new { message = "دسترسی شما به این بخش مجاز نیست" })
|
||||
{
|
||||
StatusCode = StatusCodes.Status403Forbidden
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// expose the authenticated admin to the controller without a second DB round-trip
|
||||
context.HttpContext.Items["AdminUser"] = user;
|
||||
|
||||
await next();
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,9 @@ void ConfigureCoreServices(WebApplicationBuilder builder)
|
||||
builder.Services.AddAutoMapper(typeof(GreenHome.Application.MappingProfile));
|
||||
builder.Services.AddValidatorsFromAssemblyContaining<GreenHome.Application.DeviceDtoValidator>();
|
||||
|
||||
// Admin panel auth filter (used via [ServiceFilter] on AdminController)
|
||||
builder.Services.AddScoped<GreenHome.Api.Filters.AdminAuthFilter>();
|
||||
|
||||
builder.Services.AddInfrastructure(builder.Configuration);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,5 +22,9 @@
|
||||
"TokenSecret": "CHANGE_ME_USE_A_STRONG_RANDOM_SECRET_AT_LEAST_32_CHARS"
|
||||
},
|
||||
|
||||
"Admin": {
|
||||
"OnlineWindowMinutes": 15
|
||||
},
|
||||
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Reference in New Issue
Block a user