Skip to content

Commit 042d35c

Browse files
Usermanagment Api Models, DbContext and Permission DTOs added along with Middlewares and serilog used as it is from the Centralized Logging - phase 2 api
1 parent d6038f3 commit 042d35c

20 files changed

+544
-1
lines changed

CentralizedLoggingMonitoring.sln

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio Version 17
4-
VisualStudioVersion = 17.14.36310.24 d17.14
4+
VisualStudioVersion = 17.14.36310.24
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CentralizedLoggingApi", "CentralizedLoggingApi\CentralizedLoggingApi.csproj", "{E4783310-16BC-401A-A1A7-1DB35C1311CC}"
77
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserManagementApi", "UserManagementApi\UserManagementApi.csproj", "{53EF3AAF-AE53-48F7-B081-50699EB501DA}"
9+
EndProject
810
Global
911
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1012
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +17,10 @@ Global
1517
{E4783310-16BC-401A-A1A7-1DB35C1311CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
1618
{E4783310-16BC-401A-A1A7-1DB35C1311CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
1719
{E4783310-16BC-401A-A1A7-1DB35C1311CC}.Release|Any CPU.Build.0 = Release|Any CPU
20+
{53EF3AAF-AE53-48F7-B081-50699EB501DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{53EF3AAF-AE53-48F7-B081-50699EB501DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{53EF3AAF-AE53-48F7-B081-50699EB501DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
23+
{53EF3AAF-AE53-48F7-B081-50699EB501DA}.Release|Any CPU.Build.0 = Release|Any CPU
1824
EndGlobalSection
1925
GlobalSection(SolutionProperties) = preSolution
2026
HideSolutionNode = FALSE
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
3+
namespace UserManagementApi.Controllers
4+
{
5+
[ApiController]
6+
[Route("[controller]")]
7+
public class WeatherForecastController : ControllerBase
8+
{
9+
private static readonly string[] Summaries = new[]
10+
{
11+
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
12+
};
13+
14+
private readonly ILogger<WeatherForecastController> _logger;
15+
16+
public WeatherForecastController(ILogger<WeatherForecastController> logger)
17+
{
18+
_logger = logger;
19+
}
20+
21+
[HttpGet(Name = "GetWeatherForecast")]
22+
public IEnumerable<WeatherForecast> Get()
23+
{
24+
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
25+
{
26+
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
27+
TemperatureC = Random.Shared.Next(-20, 55),
28+
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
29+
})
30+
.ToArray();
31+
}
32+
}
33+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace UserManagementApi.DTO
2+
{
3+
public record FunctionDto(int Id, string Code, string DisplayName);
4+
public record ModuleDto(int Id, string Name, string Area, string Controller, string Action, List<FunctionDto> Functions);
5+
public record CategoryDto(int Id, string Name, List<ModuleDto> Modules);
6+
public record UserPermissionsDto(int UserId, string UserName, List<CategoryDto> Categories);
7+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using UserManagementApi.Models;
3+
4+
namespace UserManagementApi.Data
5+
{
6+
public class AppDbContext : DbContext
7+
{
8+
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
9+
10+
public DbSet<Category> Categories => Set<Category>();
11+
public DbSet<Module> Modules => Set<Module>();
12+
public DbSet<Function> Functions => Set<Function>();
13+
public DbSet<Role> Roles => Set<Role>();
14+
public DbSet<AppUser> Users => Set<AppUser>();
15+
public DbSet<UserRole> UserRoles => Set<UserRole>();
16+
public DbSet<RoleFunction> RoleFunctions => Set<RoleFunction>();
17+
18+
protected override void OnModelCreating(ModelBuilder b)
19+
{
20+
base.OnModelCreating(b);
21+
22+
// Keys for join entities
23+
b.Entity<UserRole>().HasKey(x => new { x.UserId, x.RoleId });
24+
b.Entity<RoleFunction>().HasKey(x => new { x.RoleId, x.FunctionId });
25+
26+
// Relationships
27+
b.Entity<Module>()
28+
.HasOne(m => m.Category)
29+
.WithMany(c => c.Modules)
30+
.HasForeignKey(m => m.CategoryId)
31+
.OnDelete(DeleteBehavior.Cascade);
32+
33+
b.Entity<Function>()
34+
.HasOne(f => f.Module)
35+
.WithMany(m => m.Functions)
36+
.HasForeignKey(f => f.ModuleId)
37+
.OnDelete(DeleteBehavior.Cascade);
38+
39+
b.Entity<UserRole>()
40+
.HasOne(ur => ur.User)
41+
.WithMany(u => u.UserRoles)
42+
.HasForeignKey(ur => ur.UserId);
43+
44+
b.Entity<UserRole>()
45+
.HasOne(ur => ur.Role)
46+
.WithMany(r => r.UserRoles)
47+
.HasForeignKey(ur => ur.RoleId);
48+
49+
b.Entity<RoleFunction>()
50+
.HasOne(rf => rf.Role)
51+
.WithMany(r => r.RoleFunctions)
52+
.HasForeignKey(rf => rf.RoleId);
53+
54+
b.Entity<RoleFunction>()
55+
.HasOne(rf => rf.Function)
56+
.WithMany(f => f.RoleFunctions)
57+
.HasForeignKey(rf => rf.FunctionId);
58+
59+
// ---------- Seed Data ----------
60+
// Categories
61+
b.Entity<Category>().HasData(
62+
new Category { Id = 1, Name = "Administration" },
63+
new Category { Id = 2, Name = "Operations" }
64+
);
65+
66+
// Modules
67+
b.Entity<Module>().HasData(
68+
new Module { Id = 1, Name = "User Management", Area = "Admin", Controller = "Users", Action = "Index", CategoryId = 1 },
69+
new Module { Id = 2, Name = "Role Management", Area = "Admin", Controller = "Roles", Action = "Index", CategoryId = 1 },
70+
new Module { Id = 3, Name = "Payments", Area = "Ops", Controller = "Payments", Action = "Index", CategoryId = 2 }
71+
);
72+
73+
// Functions
74+
b.Entity<Function>().HasData(
75+
new Function { Id = 1, ModuleId = 1, Code = "Users.View", DisplayName = "View Users" },
76+
new Function { Id = 2, ModuleId = 1, Code = "Users.Edit", DisplayName = "Edit Users" },
77+
new Function { Id = 3, ModuleId = 2, Code = "Roles.View", DisplayName = "View Roles" },
78+
new Function { Id = 4, ModuleId = 2, Code = "Roles.Assign", DisplayName = "Assign Roles" },
79+
new Function { Id = 5, ModuleId = 3, Code = "Payments.View", DisplayName = "View Payments" }
80+
);
81+
82+
// Roles
83+
b.Entity<Role>().HasData(
84+
new Role { Id = 1, Name = "Admin" },
85+
new Role { Id = 2, Name = "Operator" }
86+
);
87+
88+
// Users
89+
b.Entity<AppUser>().HasData(
90+
new AppUser { Id = 1, UserName = "alice" },
91+
new AppUser { Id = 2, UserName = "bob" }
92+
);
93+
94+
// User ↔ Role
95+
b.Entity<UserRole>().HasData(
96+
new UserRole { UserId = 1, RoleId = 1 }, // alice → Admin
97+
new UserRole { UserId = 2, RoleId = 2 } // bob → Operator
98+
);
99+
100+
// Role ↔ Function
101+
b.Entity<RoleFunction>().HasData(
102+
// Admin gets everything
103+
new RoleFunction { RoleId = 1, FunctionId = 1 },
104+
new RoleFunction { RoleId = 1, FunctionId = 2 },
105+
new RoleFunction { RoleId = 1, FunctionId = 3 },
106+
new RoleFunction { RoleId = 1, FunctionId = 4 },
107+
new RoleFunction { RoleId = 1, FunctionId = 5 },
108+
109+
// Operator gets limited
110+
new RoleFunction { RoleId = 2, FunctionId = 1 }, // Users.View
111+
new RoleFunction { RoleId = 2, FunctionId = 5 } // Payments.View
112+
);
113+
}
114+
}
115+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System.Net.Mime;
2+
3+
namespace UserManagementApi.Middlewares
4+
{
5+
public class ExceptionHandlingMiddleware
6+
{
7+
private readonly RequestDelegate _next;
8+
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
9+
10+
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
11+
{
12+
_next = next;
13+
_logger = logger;
14+
}
15+
16+
public async Task Invoke(HttpContext context)
17+
{
18+
try
19+
{
20+
await _next(context); // continue pipeline
21+
}
22+
catch (OperationCanceledException oce) when (context.RequestAborted.IsCancellationRequested)
23+
{
24+
// Client aborted; don’t try to write a response body.
25+
_logger.LogWarning(oce, "Request aborted by client {Path} ({TraceId})",
26+
context.Request.Path, context.TraceIdentifier);
27+
// Let it bubble or just return; here we just return.
28+
}
29+
catch (Exception ex)
30+
{
31+
// Log using Serilog (through ILogger)
32+
_logger.LogError(ex, "Unhandled exception occurred while processing request {Path}", context.Request.Path);
33+
34+
// If the server already committed headers/body, we can't change it.
35+
if (context.Response.HasStarted)
36+
{
37+
_logger.LogWarning("The response has already started; cannot write error body. Rethrowing.");
38+
throw; // Let server infrastructure terminate the connection appropriately.
39+
}
40+
41+
// Clear headers/status that may have been set
42+
context.Response.Clear();
43+
context.Response.ContentType = "application/json";
44+
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
45+
46+
// If a previous component wrote to the (buffered) body, wipe it safely.
47+
try
48+
{
49+
if (context.Response.Body.CanSeek)
50+
{
51+
context.Response.Body.SetLength(0);
52+
context.Response.Body.Position = 0;
53+
}
54+
}
55+
catch
56+
{
57+
// If the body stream isn't seekable/writable (rare with your buffer), ignore.
58+
}
59+
// Let the server recalc Content-Length
60+
context.Response.Headers.ContentLength = null;
61+
62+
63+
var result = new
64+
{
65+
error = "An unexpected error occurred",
66+
details = ex.Message // optional, avoid exposing internals in production
67+
};
68+
69+
await context.Response.WriteAsJsonAsync(result);
70+
}
71+
}
72+
}
73+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
namespace UserManagementApi.Middlewares
2+
{
3+
public class RequestAudibilityMiddleware
4+
{
5+
private readonly RequestDelegate _next;
6+
private readonly ILogger<RequestAudibilityMiddleware> _logger;
7+
8+
public RequestAudibilityMiddleware(RequestDelegate next, ILogger<RequestAudibilityMiddleware> logger)
9+
{
10+
_next = next;
11+
_logger = logger;
12+
}
13+
14+
public async Task InvokeAsync(HttpContext context)
15+
{
16+
var requestId = context.TraceIdentifier;
17+
18+
// Log request info
19+
_logger.LogInformation("Incoming request {RequestId} {Method} {Path} from {RemoteIp}",
20+
requestId,
21+
context.Request.Method,
22+
context.Request.Path,
23+
context.Connection.RemoteIpAddress?.ToString());
24+
25+
26+
var originalBodyStream = context.Response.Body; // Save original response stream
27+
28+
await using var responseBody = new MemoryStream();
29+
context.Response.Body = responseBody;
30+
31+
try
32+
{
33+
await _next(context); // continue pipeline
34+
}
35+
finally
36+
{
37+
// Capture response
38+
context.Response.Body.Seek(0, SeekOrigin.Begin);
39+
var responseText = await new StreamReader(context.Response.Body).ReadToEndAsync();
40+
context.Response.Body.Seek(0, SeekOrigin.Begin);
41+
42+
_logger.LogInformation("Outgoing response {RequestId} with status {StatusCode}, length {Length}",
43+
requestId,
44+
context.Response.StatusCode,
45+
responseText?.Length);
46+
47+
// Copy back to original response body
48+
await responseBody.CopyToAsync(originalBodyStream);
49+
}
50+
}
51+
}
52+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace UserManagementApi.Models
4+
{
5+
public class AppUser
6+
{
7+
public int Id { get; set; }
8+
[MaxLength(120)] public string UserName { get; set; } = null!;
9+
public ICollection<UserRole> UserRoles { get; set; } = new List<UserRole>();
10+
}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace UserManagementApi.Models
4+
{
5+
public class Category
6+
{
7+
public int Id { get; set; }
8+
[MaxLength(100)] public string Name { get; set; } = null!;
9+
public ICollection<Module> Modules { get; set; } = new List<Module>();
10+
}
11+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace UserManagementApi.Models
4+
{
5+
public class Function
6+
{
7+
public int Id { get; set; }
8+
[MaxLength(120)] public string Code { get; set; } = null!;
9+
[MaxLength(200)] public string DisplayName { get; set; } = null!;
10+
11+
public int ModuleId { get; set; }
12+
public Module Module { get; set; } = null!;
13+
14+
public ICollection<RoleFunction> RoleFunctions { get; set; } = new List<RoleFunction>();
15+
}
16+
}

UserManagementApi/Models/Module.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace UserManagementApi.Models
4+
{
5+
public class Module
6+
{
7+
public int Id { get; set; }
8+
[MaxLength(100)] public string Name { get; set; } = null!;
9+
[MaxLength(100)] public string Area { get; set; } = null!;
10+
[MaxLength(100)] public string Controller { get; set; } = null!;
11+
[MaxLength(100)] public string Action { get; set; } = null!;
12+
13+
public int CategoryId { get; set; }
14+
public Category Category { get; set; } = null!;
15+
public ICollection<Function> Functions { get; set; } = new List<Function>();
16+
}
17+
}

0 commit comments

Comments
 (0)