emloyee and user fixings
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
namespace ERPCore.Infra.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Best-effort "Browser on OS" label from a raw User-Agent string, for display in the
|
||||
/// login-device history — not a full parsing library, just enough to be readable
|
||||
/// (docs call this out: exact device model detection is out of scope).
|
||||
/// </summary>
|
||||
public static class UserAgentLabeler
|
||||
{
|
||||
public static string Label(string? userAgent)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(userAgent)) return "Unknown device";
|
||||
|
||||
var os = DetectOs(userAgent);
|
||||
var browser = DetectBrowser(userAgent);
|
||||
return $"{browser} on {os}";
|
||||
}
|
||||
|
||||
private static string DetectOs(string ua)
|
||||
{
|
||||
if (ua.Contains("Windows", StringComparison.OrdinalIgnoreCase)) return "Windows";
|
||||
if (ua.Contains("Mac OS X", StringComparison.OrdinalIgnoreCase) || ua.Contains("Macintosh", StringComparison.OrdinalIgnoreCase)) return "macOS";
|
||||
if (ua.Contains("Android", StringComparison.OrdinalIgnoreCase)) return "Android";
|
||||
if (ua.Contains("iPhone", StringComparison.OrdinalIgnoreCase) || ua.Contains("iPad", StringComparison.OrdinalIgnoreCase)) return "iOS";
|
||||
if (ua.Contains("Linux", StringComparison.OrdinalIgnoreCase)) return "Linux";
|
||||
return "an unknown OS";
|
||||
}
|
||||
|
||||
private static string DetectBrowser(string ua)
|
||||
{
|
||||
// Order matters: Edge/OPR/Chrome all also contain "Safari"/"Chrome" tokens.
|
||||
if (ua.Contains("Edg/", StringComparison.OrdinalIgnoreCase)) return "Edge";
|
||||
if (ua.Contains("OPR/", StringComparison.OrdinalIgnoreCase) || ua.Contains("Opera", StringComparison.OrdinalIgnoreCase)) return "Opera";
|
||||
if (ua.Contains("Firefox", StringComparison.OrdinalIgnoreCase)) return "Firefox";
|
||||
if (ua.Contains("CriOS", StringComparison.OrdinalIgnoreCase) || ua.Contains("Chrome", StringComparison.OrdinalIgnoreCase)) return "Chrome";
|
||||
if (ua.Contains("Safari", StringComparison.OrdinalIgnoreCase)) return "Safari";
|
||||
return "an unknown browser";
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ public static class AuditScribe
|
||||
private static readonly HashSet<Type> Excluded =
|
||||
[
|
||||
typeof(AuditLog), typeof(JournalEntryStub), typeof(NumberSequence),
|
||||
typeof(StockLedger), typeof(StockLayer),
|
||||
typeof(StockLedger), typeof(StockLayer), typeof(LoginActivity),
|
||||
];
|
||||
|
||||
private static readonly JsonSerializerOptions Json = new()
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class LoginActivityConfiguration : IEntityTypeConfiguration<LoginActivity>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<LoginActivity> builder)
|
||||
{
|
||||
builder.ToTable("login_activities");
|
||||
builder.HasKey(l => l.LoginActivityId);
|
||||
|
||||
builder.Property(l => l.IpAddress).HasMaxLength(64);
|
||||
builder.Property(l => l.UserAgent).HasMaxLength(500);
|
||||
builder.Property(l => l.DeviceLabel).HasMaxLength(200);
|
||||
builder.Property(l => l.LoginAt).IsRequired();
|
||||
|
||||
builder.HasOne(l => l.User).WithMany().HasForeignKey(l => l.UserId).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasIndex(l => new { l.UserId, l.LoginAt });
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,8 @@ public sealed class NavItemConfiguration : IEntityTypeConfiguration<NavItem>
|
||||
builder.HasData(
|
||||
new NavItem { NavItemId = 1, Code = "dashboard", Label = "Dashboard", Href = "/dashboard", SortOrder = 1 },
|
||||
new NavItem { NavItemId = 2, Code = "products", Label = "Products", Href = "/dashboard/products", SortOrder = 2 },
|
||||
new NavItem { NavItemId = 3, Code = "vendors", Label = "Vendors", Href = "/dashboard/vendors", SortOrder = 3 },
|
||||
// NavItemId 3 ("vendors") retired 2026-09-09 — Vendors moved under Procurement as a
|
||||
// sub-item (see SubNavItemConfiguration/PermissionConfiguration), matching the sidebar.
|
||||
new NavItem { NavItemId = 4, Code = "procurement", Label = "Procurement", Href = "/dashboard/procurement", SortOrder = 4 },
|
||||
new NavItem { NavItemId = 5, Code = "receiving", Label = "Receiving", Href = "/dashboard/receiving/grn", SortOrder = 5 },
|
||||
new NavItem { NavItemId = 6, Code = "stock", Label = "Stock", Href = "/dashboard/stock", SortOrder = 6 },
|
||||
|
||||
@@ -26,7 +26,9 @@ public sealed class PermissionConfiguration : IEntityTypeConfiguration<Permissio
|
||||
builder.HasData(
|
||||
new Permission { PermissionId = 1, Code = "NAV:dashboard", NavItemId = 1 },
|
||||
new Permission { PermissionId = 2, Code = "NAV:products", NavItemId = 2 },
|
||||
new Permission { PermissionId = 3, Code = "NAV:vendors", NavItemId = 3 },
|
||||
// Moved under Procurement as a sub-item (2026-09-09) — same PermissionId (3), just
|
||||
// repointed to the new SubNavItem, so any role already granted this permission keeps it.
|
||||
new Permission { PermissionId = 3, Code = "NAV:procurement.vendors", SubNavItemId = 30 },
|
||||
new Permission { PermissionId = 4, Code = "NAV:procurement", NavItemId = 4 },
|
||||
new Permission { PermissionId = 5, Code = "NAV:receiving", NavItemId = 5 },
|
||||
new Permission { PermissionId = 6, Code = "NAV:stock", NavItemId = 6 },
|
||||
@@ -67,7 +69,21 @@ public sealed class PermissionConfiguration : IEntityTypeConfiguration<Permissio
|
||||
new Permission { PermissionId = 37, Code = "NAV:production.runs", SubNavItemId = 25 },
|
||||
new Permission { PermissionId = 38, Code = "NAV:production.produced", SubNavItemId = 26 },
|
||||
new Permission { PermissionId = 39, Code = "NAV:procurement.purchase-invoices", SubNavItemId = 28 },
|
||||
new Permission { PermissionId = 40, Code = "NAV:settings.branches", SubNavItemId = 29 }
|
||||
new Permission { PermissionId = 40, Code = "NAV:settings.branches", SubNavItemId = 29 },
|
||||
new Permission { PermissionId = 41, Code = "NAV:stock.ledger", SubNavItemId = 31 },
|
||||
new Permission { PermissionId = 42, Code = "NAV:stock.enquiry", SubNavItemId = 32 },
|
||||
new Permission { PermissionId = 43, Code = "NAV:stock.valuation", SubNavItemId = 33 },
|
||||
new Permission { PermissionId = 44, Code = "NAV:stock.transfers", SubNavItemId = 34 },
|
||||
new Permission { PermissionId = 45, Code = "NAV:stock.adjustments", SubNavItemId = 35 },
|
||||
new Permission { PermissionId = 46, Code = "NAV:stock.counts", SubNavItemId = 36 },
|
||||
new Permission { PermissionId = 47, Code = "NAV:stock.reorder-alerts", SubNavItemId = 37 },
|
||||
new Permission { PermissionId = 48, Code = "NAV:stock.wastage", SubNavItemId = 38 },
|
||||
// Sales (NavItemId 13) children — SubNavItemId 23 had no permission row before now
|
||||
// (mirrors SubNavItemConfiguration's rename of that row to sales.bundle-templates).
|
||||
new Permission { PermissionId = 49, Code = "NAV:sales.bundle-templates", SubNavItemId = 23 },
|
||||
new Permission { PermissionId = 50, Code = "NAV:sales.invoices", SubNavItemId = 40 },
|
||||
new Permission { PermissionId = 51, Code = "NAV:sales.free-issues", SubNavItemId = 41 },
|
||||
new Permission { PermissionId = 52, Code = "NAV:sales.day-end", SubNavItemId = 42 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,19 @@ public sealed class SubNavItemConfiguration : IEntityTypeConfiguration<SubNavIte
|
||||
new SubNavItem { SubNavItemId = 7, NavItemId = 9, Code = "settings.roles", Label = "Roles", Href = "/dashboard/settings/roles", SortOrder = 1 },
|
||||
new SubNavItem { SubNavItemId = 8, NavItemId = 9, Code = "settings.users", Label = "Users", Href = "/dashboard/settings/users", SortOrder = 2 },
|
||||
new SubNavItem { SubNavItemId = 24, NavItemId = 9, Code = "settings.notification-config", Label = "Notification Config", Href = "/dashboard/settings/notification-config", SortOrder = 3 },
|
||||
new SubNavItem { SubNavItemId = 23, NavItemId = 13, Code = "sales.bundle-sales", Label = "Bundle Sales", Href = "/dashboard/sales/bundles", SortOrder = 1 },
|
||||
// Sales (NavItemId 13) children — mirror the sidebar order (AppSidebar.tsx). SubNavItemId
|
||||
// 23 is renamed in place (was "sales.bundle-sales" / Bundle Sales) rather than
|
||||
// delete+recreate, so any role already granted this permission keeps it; the rest are new.
|
||||
new SubNavItem { SubNavItemId = 40, NavItemId = 13, Code = "sales.invoices", Label = "Sales", Href = "/dashboard/sales/invoices", SortOrder = 1 },
|
||||
new SubNavItem { SubNavItemId = 23, NavItemId = 13, Code = "sales.bundle-templates", Label = "Bundle Templates", Href = "/dashboard/sales/bundles/templates", SortOrder = 2 },
|
||||
new SubNavItem { SubNavItemId = 41, NavItemId = 13, Code = "sales.free-issues", Label = "Free Issues", Href = "/dashboard/sales/free-issues", SortOrder = 3 },
|
||||
new SubNavItem { SubNavItemId = 42, NavItemId = 13, Code = "sales.day-end", Label = "Day End", Href = "/dashboard/sales/day-end", SortOrder = 4 },
|
||||
// Procurement (NavItemId 4) children — mirror the hub page order.
|
||||
// IDs 17-20 (not 9-12): 9-12 were already claimed by the Ledgers sub-items below;
|
||||
// these procurement rows were never actually migrated into the database before now.
|
||||
// Vendors (SubNavItemId 30) reuses PermissionId 3, which used to point at the retired
|
||||
// top-level "vendors" NavItem — so any role already granted this permission keeps it.
|
||||
new SubNavItem { SubNavItemId = 30, NavItemId = 4, Code = "procurement.vendors", Label = "Vendors", Href = "/dashboard/vendors", SortOrder = 0 },
|
||||
new SubNavItem { SubNavItemId = 17, NavItemId = 4, Code = "procurement.requisitions", Label = "Requisitions", Href = "/dashboard/procurement/requisitions", SortOrder = 1 },
|
||||
new SubNavItem { SubNavItemId = 18, NavItemId = 4, Code = "procurement.rfqs", Label = "RFQs", Href = "/dashboard/procurement/rfqs", SortOrder = 2 },
|
||||
new SubNavItem { SubNavItemId = 19, NavItemId = 4, Code = "procurement.purchase-orders", Label = "Purchase Orders", Href = "/dashboard/procurement/purchase-orders", SortOrder = 3 },
|
||||
@@ -64,7 +73,18 @@ public sealed class SubNavItemConfiguration : IEntityTypeConfiguration<SubNavIte
|
||||
// FixProductionTemplatesSubNavItemIdConflict for the data repair.
|
||||
new SubNavItem { SubNavItemId = 27, NavItemId = 14, Code = "production.templates", Label = "Templates", Href = "/dashboard/production/templates", SortOrder = 1 },
|
||||
new SubNavItem { SubNavItemId = 25, NavItemId = 14, Code = "production.runs", Label = "Runs", Href = "/dashboard/production/runs", SortOrder = 2 },
|
||||
new SubNavItem { SubNavItemId = 26, NavItemId = 14, Code = "production.produced", Label = "Produced", Href = "/dashboard/production/produced", SortOrder = 3 }
|
||||
new SubNavItem { SubNavItemId = 26, NavItemId = 14, Code = "production.produced", Label = "Produced", Href = "/dashboard/production/produced", SortOrder = 3 },
|
||||
// Stock (NavItemId 6) children — mirror the sidebar order (AppSidebar.tsx). These rows
|
||||
// never existed in the database before now, so "stock" is still bypassed frontend-side
|
||||
// in AppSidebar.tsx until roles are actually granted these permissions.
|
||||
new SubNavItem { SubNavItemId = 31, NavItemId = 6, Code = "stock.ledger", Label = "Stock Ledger", Href = "/dashboard/stock/ledger", SortOrder = 1 },
|
||||
new SubNavItem { SubNavItemId = 32, NavItemId = 6, Code = "stock.enquiry", Label = "Stock Enquiry", Href = "/dashboard/stock/enquiry", SortOrder = 2 },
|
||||
new SubNavItem { SubNavItemId = 33, NavItemId = 6, Code = "stock.valuation", Label = "Valuation", Href = "/dashboard/stock/valuation", SortOrder = 3 },
|
||||
new SubNavItem { SubNavItemId = 34, NavItemId = 6, Code = "stock.transfers", Label = "Transfers", Href = "/dashboard/stock/transfers", SortOrder = 4 },
|
||||
new SubNavItem { SubNavItemId = 35, NavItemId = 6, Code = "stock.adjustments", Label = "Adjustments", Href = "/dashboard/stock/adjustments", SortOrder = 5 },
|
||||
new SubNavItem { SubNavItemId = 36, NavItemId = 6, Code = "stock.counts", Label = "Counts", Href = "/dashboard/stock/counts", SortOrder = 6 },
|
||||
new SubNavItem { SubNavItemId = 37, NavItemId = 6, Code = "stock.reorder-alerts", Label = "Reorder Alerts", Href = "/dashboard/stock/reorder-alerts", SortOrder = 7 },
|
||||
new SubNavItem { SubNavItemId = 38, NavItemId = 6, Code = "stock.wastage", Label = "Wastage", Href = "/dashboard/stock/wastage", SortOrder = 8 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ public class ErpDbContext : DbContext
|
||||
// --- Cross-cutting: audit trail + GL-ready journal (docs/10 Part C.7) ---
|
||||
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
|
||||
public DbSet<JournalEntryStub> JournalEntryStubs => Set<JournalEntryStub>();
|
||||
public DbSet<LoginActivity> LoginActivities => Set<LoginActivity>();
|
||||
|
||||
// --- HRM: org masters (docs/12-BACKEND-HRM.md Part C.1) ---
|
||||
public DbSet<Branch> Branches => Set<Branch>();
|
||||
|
||||
Reference in New Issue
Block a user