Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -360,4 +360,6 @@ MigrationBackup/
.ionide/

# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd
# Hermes Agent workspace
.hermes/
40 changes: 40 additions & 0 deletions IMS.API/Attributes/ApiKeyAuthorizeAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using IMS.Infrastructure.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;

namespace IMS.API.Attributes;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ApiKeyAuthorizeAttribute : Attribute, IAsyncActionFilter
{
private const string HeaderName = "X-API-Key";

public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var dbContext = context.HttpContext.RequestServices.GetService<ApplicationDbContext>();
if (dbContext == null)
{
context.Result = new StatusCodeResult(StatusCodes.Status500InternalServerError);
return;
}

if (!context.HttpContext.Request.Headers.TryGetValue(HeaderName, out var keyValue) ||
string.IsNullOrWhiteSpace(keyValue))
{
context.Result = new UnauthorizedResult();
return;
}

var apiKey = await dbContext.ApiKeys
.FirstOrDefaultAsync(k => k.Key == keyValue.ToString() && k.IsActive);

if (apiKey is null || apiKey.ExpiresAt <= DateTime.UtcNow)
{
context.Result = new UnauthorizedResult();
return;
}

await next();
}
}
97 changes: 97 additions & 0 deletions IMS.API/Controllers/ApiKeysController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using IMS.API.Attributes;
using IMS.Core.Entities;
using IMS.Infrastructure.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace IMS.API.Controllers;

[ApiController]
[Route("api/apikeys")]
public class ApiKeysController : ControllerBase
{
private readonly ApplicationDbContext _db;
private readonly ILogger<ApiKeysController> _logger;

public ApiKeysController(ApplicationDbContext db, ILogger<ApiKeysController> logger)
{
_db = db;
_logger = logger;
}

[HttpPost("create")]
[ApiKeyAuthorize]
public async Task<IActionResult> Create([FromBody] CreateApiKeyRequest request)
{
if (!Request.Headers.TryGetValue("X-API-Key", out var headerKey))
{
return Unauthorized();
}

var adminKey = await _db.ApiKeys
.FirstOrDefaultAsync(k => k.Key == headerKey.ToString() && k.IsActive);

if (adminKey is null || adminKey.ExpiresAt <= DateTime.UtcNow)
{
return Unauthorized();
}

if (request.ExpiresAt.Kind != DateTimeKind.Utc)
{
request.ExpiresAt = request.ExpiresAt.ToUniversalTime();
}

var newKey = new ApiKey
{
Key = new string(Path.GetRandomFileName().Replace(".", "").Take(32).ToArray()),
Owner = string.IsNullOrWhiteSpace(request.Owner) ? null : request.Owner.Trim(),
ExpiresAt = request.ExpiresAt
};

_db.ApiKeys.Add(newKey);
await _db.SaveChangesAsync();

return Ok(new ApiKeyResponse
{
Id = newKey.Id,
Key = newKey.Key,
Owner = newKey.Owner,
ExpiresAt = newKey.ExpiresAt,
IsActive = newKey.IsActive
});
}

[HttpGet]
[ApiKeyAuthorize]
public async Task<IActionResult> GetAll()
{
var keys = await _db.ApiKeys
.OrderByDescending(k => k.CreatedAt)
.Select(k => new ApiKeyResponse
{
Id = k.Id,
Key = k.Key.Length > 6 ? k.Key.Substring(0, 6) + "..." : k.Key,
Owner = k.Owner,
ExpiresAt = k.ExpiresAt,
IsActive = k.IsActive
})
.ToListAsync();

return Ok(keys);
}
}

public class CreateApiKeyRequest
{
public string? Owner { get; set; }
public DateTime ExpiresAt { get; set; }
}

public class ApiKeyResponse
{
public int Id { get; set; }
public string Key { get; set; } = string.Empty;
public string? Owner { get; set; }
public DateTime ExpiresAt { get; set; }
public bool IsActive { get; set; }
}
16 changes: 16 additions & 0 deletions IMS.API/Controllers/TestController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using IMS.API.Attributes;
using Microsoft.AspNetCore.Mvc;

namespace IMS.API.Controllers;

[ApiController]
[Route("api/test")]
public class TestController : ControllerBase
{
[HttpGet("protected")]
[ApiKeyAuthorize]
public IActionResult Protected()
{
return Ok(new { Message = "You accessed a protected endpoint with a valid API key." });
}
}
13 changes: 7 additions & 6 deletions IMS.API/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using IMS.API.Attributes;
using IMS.API.Services;
using IMS.Core.Contracts;
using IMS.Core.Entities;
Expand Down Expand Up @@ -99,23 +100,23 @@
app.UseSwaggerUI();
}

app.UseHttpsRedirection();
if (!app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();
}
app.UseStaticFiles();
app.UseCors("AllowAll");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();



app.MapFallbackToFile("index.html");

using (var seedScope = app.Services.CreateScope())
{

var dbContext = seedScope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await RoleSeeder.SeedRolesAsync(dbContext, seedScope.ServiceProvider);

await DataSeeder.SeedDataAsync(seedScope.ServiceProvider);
}


app.Run();
6 changes: 3 additions & 3 deletions IMS.API/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
"AllowedHosts": "*",
"ConnectionStrings": {

"DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=IMS_DB;Trusted_Connection=True;TrustServerCertificate=True"
"DefaultConnection": "Server=db63575.public.databaseasp.net;Database=db63575;User Id=db63575;Password=__MONSTERASP_ENV__;Encrypt=False;MultipleActiveResultSets=True;TrustServerCertificate=True;"
},
"BaseUrl": "https://localhost:7086",
"BaseUrl": "http://imcapp.runasp.net",
"JwtOptions": {
"Issuer": "https://localhost:7086",
"Issuer": "http://imcapp.runasp.net",
"Audience": "InventoryManagementApi",
"SecretKey": "YourSuperSecretKey123456789012345678901234567890",
"ExpiryMinutes": 60
Expand Down
159 changes: 159 additions & 0 deletions IMS.API/wwwroot/css/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
body {
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica, Arial, sans-serif;
margin: 0;
padding: 18px;
background: #f7f8fa;
color: #1f2937;
}

a {
color: inherit;
text-decoration: none;
}

.hidden {
display: none !important;
}

.header {
background: #0f172a;
color: #fff;
padding: 18px 28px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}

.header .brand {
font-weight: 700;
font-size: 18px;
}

.header .user {
font-size: 14px;
opacity: 0.9;
}

.container {
max-width: 1100px;
margin: 28px auto;
padding: 0 18px;
}

.card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 14px;
padding: 18px;
margin-bottom: 16px;
}

.title {
font-size: 22px;
font-weight: 700;
margin-bottom: 14px;
}

.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 18px;
}

.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border: none;
padding: 10px 14px;
border-radius: 10px;
cursor: pointer;
font-weight: 600;
color: #fff;
background: #2563eb;
}

.btn.secondary {
background: #6b7280;
}
.btn.success {
background: #16a34a;
}
.btn.danger {
background: #dc2626;
}
.btn.ghost {
background: #e5e7eb;
color: #111827;
}

input, select, textarea {
width: 100%;
padding: 9px 10px;
border-radius: 10px;
border: 1px solid #d1d5db;
background: #fff;
font: inherit;
}

label {
display: block;
font-size: 13px;
font-weight: 700;
color: #374151;
margin: 10px 0 6px;
}

.form-grid > div {
margin-bottom: 14px;
padding: 0 6px;
}
}

.table {
width: 100%;
border-collapse: collapse;
margin-top: 12px;
background: #fff;
border-radius: 12px;
overflow: hidden;
border: 1px solid #e5e7eb;
}

.table th, .table td {
padding: 11px 13px;
text-align: left;
border-bottom: 1px solid #f3f4f6;
font-size: 14px;
}

.table th {
background: #f9fafb;
font-weight: 700;
color: #374151;
}

.login {
max-width: 380px;
margin: 80px auto;
padding: 22px;
}

.status {
margin-top: 10px;
font-size: 14px;
color: #dc2626;
}

.actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}

.actions-right {
margin-left: auto;
}
14 changes: 14 additions & 0 deletions IMS.API/wwwroot/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IMS Web</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<div id="app"></div>
<script src="/js/config.js"></script>
<script src="/js/app.js"></script>
</body>
</html>
Loading