diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cf4aac4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.github +.vs +bin +obj +data-protection-keys +*.user +*.suo +*.dbmdl +docker-compose*.yml +Dockerfile* +README.md diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index c62b908..382d37e 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -19,7 +19,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: Restore dependencies run: dotnet restore - name: Build diff --git a/.gitignore b/.gitignore index 9491a2f..60a6923 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,11 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +# Private deployment configuration and credentials +.env +.env.* +!.env.example +DEPLOYMENT_MANUAL.md +DEPLOYMENT_STEPS.md diff --git a/Controllers/AccountController.cs b/Controllers/AccountController.cs index 8347e14..5bfd32a 100644 --- a/Controllers/AccountController.cs +++ b/Controllers/AccountController.cs @@ -3,11 +3,7 @@ using Microsoft.AspNetCore.Mvc; using System.Security.Claims; using Microsoft.AspNetCore.Authorization; -using System.DirectoryServices.AccountManagement; using Scribe.Services; -using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.Blazor; -using Humanizer; -using System.Security; using Scribe.Models; namespace Scribe.Controllers @@ -15,13 +11,13 @@ namespace Scribe.Controllers [AllowAnonymous] public class AccountController : Controller { - private readonly string domain = "zlt.co.zw"; - private readonly string groupName = "Scribe Admins"; private readonly ILoggingService _loggingService; + private readonly IConfiguration _configuration; - public AccountController(ILoggingService loggingService) + public AccountController(ILoggingService loggingService, IConfiguration configuration) { _loggingService = loggingService; + _configuration = configuration; } [HttpGet] @@ -49,32 +45,24 @@ public async Task Login(string username, string password) { if (ValidateUser(username, password, out string validationMessage)) { - if (IsUserInGroup(username)) + var claims = new List { - var claims = new List - { - new Claim(ClaimTypes.Name, username), - new Claim(ClaimTypes.Role, "Scribe Admins") - }; - - var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); - - await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); - TempData["Success"] = "Welcome " + username; - var details = "User " + username + " logged in."; - await _loggingService.LogActionAsync(details, username); - - Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; - Response.Headers["Pragma"] = "no-cache"; - Response.Headers["Expires"] = "0"; - - return RedirectToAction("Index", "Home"); - } - else - { - ModelState.AddModelError("", "You do not have permission to access this system."); - TempData["Failure"] = "You do not have permission to access this system."; - } + new Claim(ClaimTypes.Name, username), + new Claim(ClaimTypes.Role, "Scribe Admins") + }; + + var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + + await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); + TempData["Success"] = "Welcome " + username; + var details = "User " + username + " logged in."; + await _loggingService.LogActionAsync(details, username); + + Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; + Response.Headers["Pragma"] = "no-cache"; + Response.Headers["Expires"] = "0"; + + return RedirectToAction("Index", "Home"); } else { @@ -117,45 +105,21 @@ public async Task Logout() private bool ValidateUser(string username, string password, out string validationMessage) { - using (var context = new PrincipalContext(ContextType.Domain, domain)) - { - if (context.ValidateCredentials(username, password)) - { - validationMessage = string.Empty; - return true; - } - else - { - using (var user = UserPrincipal.FindByIdentity(context, username)) - { - if (user != null && user.IsAccountLockedOut()) - { - validationMessage = "Your account is locked. Please try again later."; - TempData["Failure"] = "Your account is locked. Please try again later."; - } - else - { - validationMessage = "Invalid username or password."; - TempData["Failure"] = "Invalid username or password."; - } - } - return false; - } - } - } + var configuredUsername = _configuration["DemoAuthentication:Username"]; + var configuredPassword = _configuration["DemoAuthentication:Password"]; - private bool IsUserInGroup(string username) - { - using (var context = new PrincipalContext(ContextType.Domain, domain)) - using (var user = UserPrincipal.FindByIdentity(context, username)) - using (var group = GroupPrincipal.FindByIdentity(context, groupName)) + if (!string.IsNullOrWhiteSpace(configuredUsername) && + !string.IsNullOrWhiteSpace(configuredPassword) && + string.Equals(username?.Trim(), configuredUsername.Trim(), StringComparison.Ordinal) && + string.Equals(password, configuredPassword, StringComparison.Ordinal)) { - if (user != null && group != null) - { - return group.GetMembers().Any(member => member.SamAccountName.Equals(username, StringComparison.OrdinalIgnoreCase)); - } + validationMessage = string.Empty; + return true; } + + validationMessage = "Invalid username or password."; + TempData["Failure"] = validationMessage; return false; } } -} \ No newline at end of file +} diff --git a/Data/DemoDataSeeder.cs b/Data/DemoDataSeeder.cs new file mode 100644 index 0000000..c9f36c4 --- /dev/null +++ b/Data/DemoDataSeeder.cs @@ -0,0 +1,335 @@ +using Microsoft.EntityFrameworkCore; +using Scribe.Models; + +namespace Scribe.Data; + +public static class DemoDataSeeder +{ + private const string LaptopIcon = "ca2de226-17a3-4c19-9d66-4ffd5710932a_laptop.svg"; + private const string PrinterIcon = "9a819f22-86c4-4689-9615-f8b89758ad25_printer-svgrepo-com (1).svg"; + + public static async Task SeedAsync(ApplicationDbContext db) + { + var brands = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var seed in new[] + { + (Name: "Dell", Image: "dell.svg"), + (Name: "HP", Image: "hp.svg"), + (Name: "Lenovo", Image: "lenovo.svg"), + (Name: "Apple", Image: "apple.svg"), + (Name: "Cisco", Image: "cisco.svg"), + (Name: "Epson", Image: "epson.svg"), + (Name: "Samsung", Image: "samsung.svg"), + (Name: "ASUS", Image: "asus.svg"), + (Name: "Acer", Image: "acer.svg"), + (Name: "Intel", Image: "intel.svg") + }) + { + var brand = await db.Brands!.FirstOrDefaultAsync(x => x.Name == seed.Name); + if (brand == null) + { + brand = new Brand { Name = seed.Name, ImageName = seed.Image }; + db.Brands.Add(brand); + } + else if (string.IsNullOrWhiteSpace(brand.ImageName)) + { + brand.ImageName = seed.Image; + } + + brands[seed.Name] = brand; + } + + var categories = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var seed in new[] + { + (Name: "Laptops", Icon: LaptopIcon), + (Name: "Printers", Icon: PrinterIcon), + (Name: "Monitors", Icon: LaptopIcon), + (Name: "Smartphones", Icon: LaptopIcon), + (Name: "Networking", Icon: LaptopIcon), + (Name: "Accessories", Icon: PrinterIcon) + }) + { + var category = await db.Categories!.FirstOrDefaultAsync(x => x.Name == seed.Name); + if (category == null) + { + category = new Category { Name = seed.Name, Icon = seed.Icon }; + db.Categories.Add(category); + } + else if (string.IsNullOrWhiteSpace(category.Icon)) + { + category.Icon = seed.Icon; + } + + categories[seed.Name] = category; + } + + var locations = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var seed in new[] + { + (Name: "New Storage", Description: "IT Storage room"), + (Name: "Old Storage", Description: "IT Storage room"), + (Name: "User Station", Description: "User Office or Station"), + (Name: "Head Office", Description: "Main office floor"), + (Name: "Remote Office", Description: "Remote office location"), + (Name: "Server Room", Description: "Restricted infrastructure room") + }) + { + var location = await db.Locations!.FirstOrDefaultAsync(x => x.Name == seed.Name); + if (location == null) + { + location = new Location { Name = seed.Name, Description = seed.Description }; + db.Locations.Add(location); + } + + locations[seed.Name] = location; + } + + var departments = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var seedName in new[] { "IT", "No Department", "Finance", "HR", "Operations", "Sales" }) + { + var department = await db.Department!.FirstOrDefaultAsync(x => x.Name == seedName); + if (department == null) + { + department = new Department { Name = seedName }; + db.Department.Add(department); + } + + departments[seedName] = department; + } + + var conditions = await db.Condition!.ToDictionaryAsync(x => x.Name, StringComparer.OrdinalIgnoreCase); + var users = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var userName in new[] + { + "No User", + "Alice Johnson", + "Brian Moyo", + "Carol Smith", + "David Ncube", + "Evelyn Dube" + }) + { + var user = await db.ADUsers.FirstOrDefaultAsync(x => x.Name == userName); + if (user == null) + { + user = new ADUsers { Name = userName }; + db.ADUsers.Add(user); + } + + users[userName] = user; + } + + var groups = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var groupName in new[] { "IT Support", "Finance Team", "Operations Team" }) + { + var group = await db.Group.FirstOrDefaultAsync(x => x.Name == groupName); + if (group == null) + { + group = new Group { Name = groupName }; + db.Group.Add(group); + } + + groups[groupName] = group; + } + + var demoSystemUser = await db.SystemUsers.FirstOrDefaultAsync(x => x.SamAccountName == "demo"); + if (demoSystemUser == null) + { + db.SystemUsers.Add(new SystemUser + { + FirstName = "Demo", + LastName = "Administrator", + SamAccountName = "demo", + UserPrincipalName = "demo@demo.local", + DisplayName = "Demo Administrator" + }); + } + + await db.SaveChangesAsync(); + + var models = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var seed in new[] + { + (Name: "Dell Latitude 5440", Brand: "Dell", Category: "Laptops", Image: "demo-laptop.jpg"), + (Name: "HP EliteBook 840 G10", Brand: "HP", Category: "Laptops", Image: "demo-laptop.jpg"), + (Name: "Lenovo ThinkPad T14 Gen 4", Brand: "Lenovo", Category: "Laptops", Image: "demo-laptop.jpg"), + (Name: "Apple MacBook Air M2", Brand: "Apple", Category: "Laptops", Image: "demo-laptop.jpg"), + (Name: "Acer Aspire 5", Brand: "Acer", Category: "Laptops", Image: "demo-laptop.jpg"), + (Name: "Dell UltraSharp U2422H", Brand: "Dell", Category: "Monitors", Image: "demo-monitor.jpg"), + (Name: "HP LaserJet Pro M404dn", Brand: "HP", Category: "Printers", Image: "demo-printer.jpg"), + (Name: "Epson WorkForce Pro WF-4830", Brand: "Epson", Category: "Printers", Image: "demo-printer.jpg"), + (Name: "Samsung Galaxy S21", Brand: "Samsung", Category: "Smartphones", Image: "demo-smartphone.jpg"), + (Name: "Cisco RV340", Brand: "Cisco", Category: "Networking", Image: "demo-router.jpg"), + (Name: "ASUS RT-AX58U", Brand: "ASUS", Category: "Networking", Image: "demo-router.jpg") + }) + { + var model = await db.Models!.FirstOrDefaultAsync(x => x.Name == seed.Name); + if (model == null) + { + model = new Model + { + Name = seed.Name, + BrandId = brands[seed.Brand].Id, + CategoryId = categories[seed.Category].Id, + Image = seed.Image + }; + db.Models.Add(model); + } + else + { + model.BrandId ??= brands[seed.Brand].Id; + model.CategoryId ??= categories[seed.Category].Id; + model.Image ??= seed.Image; + } + + models[seed.Name] = model; + } + + await db.SaveChangesAsync(); + + var assets = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var seed in new[] + { + (Serial: "DEMO-DL5440-001", Model: "Dell Latitude 5440", Condition: "In Use", Department: "IT", Location: "User Station", User: "Alice Johnson", Group: (string?)null, Allocated: true, Description: "Primary laptop for the IT service desk", Mac: "02:00:00:54:40:01"), + (Serial: "DEMO-DL5440-002", Model: "Dell Latitude 5440", Condition: "New", Department: "IT", Location: "New Storage", User: (string?)null, Group: (string?)null, Allocated: false, Description: "New replacement laptop", Mac: "02:00:00:54:40:02"), + (Serial: "DEMO-HP840-001", Model: "HP EliteBook 840 G10", Condition: "In Use", Department: "Finance", Location: "Head Office", User: "Carol Smith", Group: (string?)null, Allocated: true, Description: "Finance analyst laptop", Mac: "02:00:00:84:00:01"), + (Serial: "DEMO-T14-001", Model: "Lenovo ThinkPad T14 Gen 4", Condition: "In Use", Department: "Operations", Location: "Remote Office", User: "Brian Moyo", Group: (string?)null, Allocated: true, Description: "Operations field laptop", Mac: "02:00:00:14:00:01"), + (Serial: "DEMO-MBA-001", Model: "Apple MacBook Air M2", Condition: "Awaiting User", Department: "Sales", Location: "New Storage", User: (string?)null, Group: (string?)null, Allocated: false, Description: "Sales onboarding laptop", Mac: "02:00:00:BA:00:01"), + (Serial: "DEMO-ACER-001", Model: "Acer Aspire 5", Condition: "Needs Repairs", Department: "HR", Location: "Old Storage", User: (string?)null, Group: (string?)null, Allocated: false, Description: "Laptop awaiting keyboard replacement", Mac: "02:00:00:AC:00:01"), + (Serial: "DEMO-DMON-001", Model: "Dell UltraSharp U2422H", Condition: "In Use", Department: "IT", Location: "User Station", User: "David Ncube", Group: (string?)null, Allocated: true, Description: "External monitor", Mac: (string?)null), + (Serial: "DEMO-HPPRN-001", Model: "HP LaserJet Pro M404dn", Condition: "Needs Repairs", Department: "Finance", Location: "Head Office", User: (string?)null, Group: "Finance Team", Allocated: true, Description: "Shared finance printer", Mac: (string?)null), + (Serial: "DEMO-EPPRN-001", Model: "Epson WorkForce Pro WF-4830", Condition: "New", Department: "Operations", Location: "New Storage", User: (string?)null, Group: (string?)null, Allocated: false, Description: "Spare multifunction printer", Mac: (string?)null), + (Serial: "DEMO-SAMS21-001", Model: "Samsung Galaxy S21", Condition: "In Use", Department: "Sales", Location: "User Station", User: "Evelyn Dube", Group: (string?)null, Allocated: true, Description: "Sales mobile device", Mac: (string?)null), + (Serial: "DEMO-CISCO-001", Model: "Cisco RV340", Condition: "In Use", Department: "IT", Location: "Server Room", User: (string?)null, Group: "IT Support", Allocated: true, Description: "Branch network router", Mac: (string?)null), + (Serial: "DEMO-ASUS-001", Model: "ASUS RT-AX58U", Condition: "Out Of Order", Department: "IT", Location: "Old Storage", User: (string?)null, Group: (string?)null, Allocated: false, Description: "Router with intermittent power failure", Mac: (string?)null) + }) + { + var asset = await db.SerialNumbers!.FirstOrDefaultAsync(x => x.Name == seed.Serial); + if (asset == null) + { + asset = new SerialNumber + { + Name = seed.Serial, + ModelId = models[seed.Model].Id, + ConditionId = conditions[seed.Condition].Id, + DepartmentId = departments[seed.Department].Id, + LocationId = locations[seed.Location].Id, + ADUsersId = seed.User == null ? null : users[seed.User].Id, + GroupId = seed.Group == null ? null : groups[seed.Group].Id, + Creation = DateTime.UtcNow.Date.AddMonths(-6), + Allocation = seed.Allocated ? DateTime.UtcNow.Date.AddMonths(-2) : null, + AllocatedBy = seed.Allocated ? "demo" : null, + CurrentlyAllocated = seed.Allocated, + Description = seed.Description, + MacAddress = seed.Mac + }; + db.SerialNumbers.Add(asset); + } + + assets[seed.Serial] = asset; + } + + await db.SaveChangesAsync(); + + foreach (var model in models.Values) + { + if (!await db.Warranties.AnyAsync(x => x.ModelId == model.Id)) + { + db.Warranties.Add(new Warranty + { + ModelId = model.Id, + PurchaseDate = DateTime.UtcNow.Date.AddYears(-1), + WarrantyDurationYears = 3 + }); + } + } + + foreach (var seed in new[] + { + (Serial: "DEMO-ACER-001", Description: "Keyboard replacement and inspection", Condition: "Needs Repairs", MonthsAgo: 1, NextMonths: 1), + (Serial: "DEMO-HPPRN-001", Description: "Toner and paper-feed inspection", Condition: "Needs Repairs", MonthsAgo: 2, NextMonths: 1), + (Serial: "DEMO-ASUS-001", Description: "Power adapter diagnostics", Condition: "Out Of Order", MonthsAgo: 3, NextMonths: 0) + }) + { + if (!await db.Maintenances.AnyAsync(x => x.SerialNumberId == assets[seed.Serial].Id && x.ServiceDescription == seed.Description)) + { + db.Maintenances.Add(new Maintenance + { + SerialNumberId = assets[seed.Serial].Id, + ServiceDescription = seed.Description, + ServiceDate = DateTime.UtcNow.Date.AddMonths(-seed.MonthsAgo), + NextServiceDate = DateTime.UtcNow.Date.AddMonths(seed.NextMonths), + ConditionId = conditions[seed.Condition].Id, + SystemUserId = "demo" + }); + } + } + + foreach (var seed in new[] + { + (Serial: "DEMO-DL5440-001", User: "Alice Johnson", Group: (string?)null), + (Serial: "DEMO-HP840-001", User: "Carol Smith", Group: (string?)null), + (Serial: "DEMO-T14-001", User: "Brian Moyo", Group: (string?)null), + (Serial: "DEMO-DMON-001", User: "David Ncube", Group: (string?)null), + (Serial: "DEMO-SAMS21-001", User: "Evelyn Dube", Group: (string?)null), + (Serial: "DEMO-HPPRN-001", User: (string?)null, Group: "Finance Team"), + (Serial: "DEMO-CISCO-001", User: (string?)null, Group: "IT Support") + }) + { + var asset = assets[seed.Serial]; + var userId = seed.User == null ? (int?)null : users[seed.User].Id; + var groupId = seed.Group == null ? (int?)null : groups[seed.Group].Id; + + if (!await db.AllocationHistory.AnyAsync(x => x.SerialNumberId == asset.Id && x.ADUsersId == userId && x.GroupId == groupId)) + { + db.AllocationHistory.Add(new AllocationHistory + { + SerialNumberId = asset.Id, + ADUsersId = userId, + GroupId = groupId, + AllocationDate = DateTime.UtcNow.Date.AddMonths(-2), + AllocatedBy = "demo" + }); + } + + if (userId.HasValue && !await db.IndividualAssignment.AnyAsync(x => x.SerialNumberId == asset.Id && x.ADUsersId == userId.Value)) + { + db.IndividualAssignment.Add(new IndividualAssignment + { + SerialNumberId = asset.Id, + ADUsersId = userId.Value + }); + } + + if (groupId.HasValue && !await db.SerialNumberGroup.AnyAsync(x => x.SerialNumberId == asset.Id && x.GroupId == groupId.Value)) + { + db.SerialNumberGroup.Add(new SerialNumberGroup + { + SerialNumberId = asset.Id, + GroupId = groupId + }); + } + } + + foreach (var seed in new[] + { + (User: "Alice Johnson", Group: "IT Support"), + (User: "David Ncube", Group: "IT Support"), + (User: "Carol Smith", Group: "Finance Team"), + (User: "Brian Moyo", Group: "Operations Team") + }) + { + if (!await db.UserGroup.AnyAsync(x => x.UserId == users[seed.User].Id && x.GroupId == groups[seed.Group].Id)) + { + db.UserGroup.Add(new UserGroup + { + UserId = users[seed.User].Id, + GroupId = groups[seed.Group].Id + }); + } + } + + await db.SaveChangesAsync(); + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9c6eaf5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# Build stage +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +COPY Scribe.csproj Scribe.sln ./ +RUN dotnet restore Scribe.csproj + +COPY . . +RUN dotnet publish Scribe.csproj -c Release -o /app/publish --no-restore + +# Runtime stage +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime +WORKDIR /app + +ENV ASPNETCORE_ENVIRONMENT=Production \ + ASPNETCORE_URLS=http://+:8080 + +COPY --from=build /app/publish . + +EXPOSE 8080 +ENTRYPOINT ["dotnet", "Scribe.dll"] diff --git a/IMAGE_SOURCES.md b/IMAGE_SOURCES.md new file mode 100644 index 0000000..d6c4ec3 --- /dev/null +++ b/IMAGE_SOURCES.md @@ -0,0 +1,36 @@ +# Demo image sources + +These assets are for the Scribe demonstration dataset. Keep the source and +license information with the project if the images are redistributed. + +## Brand logos + +The SVG logos were downloaded from the Simple Icons v16 CDN. Simple Icons is +released under CC0, but individual brand icons may have separate licensing or +trademark considerations. See the [Simple Icons disclaimer](https://github.com/simple-icons/simple-icons/blob/develop/DISCLAIMER.md). + +| File | Source | +| --- | --- | +| `wwwroot/media/brands/dell.svg` | https://cdn.jsdelivr.net/npm/simple-icons@v16/icons/dell.svg | +| `wwwroot/media/brands/hp.svg` | https://cdn.jsdelivr.net/npm/simple-icons@v16/icons/hp.svg | +| `wwwroot/media/brands/lenovo.svg` | https://cdn.jsdelivr.net/npm/simple-icons@v16/icons/lenovo.svg | +| `wwwroot/media/brands/apple.svg` | https://cdn.jsdelivr.net/npm/simple-icons@v16/icons/apple.svg | +| `wwwroot/media/brands/cisco.svg` | https://cdn.jsdelivr.net/npm/simple-icons@v16/icons/cisco.svg | +| `wwwroot/media/brands/epson.svg` | https://cdn.jsdelivr.net/npm/simple-icons@v16/icons/epson.svg | +| `wwwroot/media/brands/samsung.svg` | https://cdn.jsdelivr.net/npm/simple-icons@v16/icons/samsung.svg | +| `wwwroot/media/brands/asus.svg` | https://cdn.jsdelivr.net/npm/simple-icons@v16/icons/asus.svg | +| `wwwroot/media/brands/acer.svg` | https://cdn.jsdelivr.net/npm/simple-icons@v16/icons/acer.svg | +| `wwwroot/media/brands/intel.svg` | https://cdn.jsdelivr.net/npm/simple-icons@v16/icons/intel.svg | + +## Device photos + +The photos were obtained through Openverse and hosted on Flickr. Attribution +is required for the CC BY and CC BY-SA images. + +| File | Title / creator | License | Source | +| --- | --- | --- | --- | +| `wwwroot/media/models/demo-laptop.jpg` | “Netbook Inside” — pasukaru76 | CC0 | https://www.flickr.com/photos/38451115@N04/3535430725 | +| `wwwroot/media/models/demo-printer.jpg` | “Lexmark E120N Laser Printer” — Sir Adavis | CC BY | https://www.flickr.com/photos/9971770@N05/2148761728 | +| `wwwroot/media/models/demo-monitor.jpg` | “new computer / monitor setup” — hodgers | CC BY-SA | https://www.flickr.com/photos/35468147887@N01/3126952638 | +| `wwwroot/media/models/demo-smartphone.jpg` | “Samsung U700” — Asim Bijarani | CC BY | https://www.flickr.com/photos/8624431@N08/2463164981 | +| `wwwroot/media/models/demo-router.jpg` | “D-Link DI-524 undressed” — schoschie | CC BY | https://www.flickr.com/photos/87569910@N00/1448798334 | diff --git a/Migrations/20260821005826_InitialCreate.Designer.cs b/Migrations/20260821005826_InitialCreate.Designer.cs new file mode 100644 index 0000000..ec3e332 --- /dev/null +++ b/Migrations/20260821005826_InitialCreate.Designer.cs @@ -0,0 +1,756 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Scribe.Data; + +#nullable disable + +namespace Scribe.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260821005826_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Scribe.Models.ADUsers", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("ADUsers"); + + b.HasData( + new + { + Id = 1, + Name = "No User" + }); + }); + + modelBuilder.Entity("Scribe.Models.AllocationHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ADUsersId") + .HasColumnType("int"); + + b.Property("AllocatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("AllocationDate") + .HasColumnType("datetime2"); + + b.Property("DeallocatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeallocationDate") + .HasColumnType("datetime2"); + + b.Property("GroupId") + .HasColumnType("int"); + + b.Property("SerialNumberId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ADUsersId"); + + b.HasIndex("GroupId"); + + b.HasIndex("SerialNumberId"); + + b.ToTable("AllocationHistory"); + }); + + modelBuilder.Entity("Scribe.Models.Brand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ImageName") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Brands"); + }); + + modelBuilder.Entity("Scribe.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Icon") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Scribe.Models.Condition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ColorCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Condition"); + + b.HasData( + new + { + Id = 1, + ColorCode = "green", + Name = "New" + }, + new + { + Id = 2, + ColorCode = "yellow", + Name = "Needs Repairs" + }, + new + { + Id = 3, + ColorCode = "blue", + Name = "In Use" + }, + new + { + Id = 4, + ColorCode = "red", + Name = "Out Of Order" + }, + new + { + Id = 5, + ColorCode = "grey", + Name = "Awaiting User" + }); + }); + + modelBuilder.Entity("Scribe.Models.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Department"); + + b.HasData( + new + { + Id = 1, + Name = "IT" + }, + new + { + Id = 2, + Name = "No Department" + }); + }); + + modelBuilder.Entity("Scribe.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Group"); + }); + + modelBuilder.Entity("Scribe.Models.IndividualAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ADUsersId") + .HasColumnType("int"); + + b.Property("SerialNumberId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ADUsersId"); + + b.HasIndex("SerialNumberId"); + + b.ToTable("IndividualAssignment"); + }); + + modelBuilder.Entity("Scribe.Models.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Locations"); + + b.HasData( + new + { + Id = 1, + Description = "IT Storage room", + Name = "New Storage" + }, + new + { + Id = 2, + Description = "IT Storage room", + Name = "Old Storage" + }, + new + { + Id = 3, + Description = "User Office or Station", + Name = "User Station" + }); + }); + + modelBuilder.Entity("Scribe.Models.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("Details") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("User") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Log"); + }); + + modelBuilder.Entity("Scribe.Models.Maintenance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConditionId") + .HasColumnType("int"); + + b.Property("NextServiceDate") + .HasColumnType("datetime2"); + + b.Property("SerialNumberId") + .HasColumnType("int"); + + b.Property("ServiceDate") + .HasColumnType("datetime2"); + + b.Property("ServiceDescription") + .HasColumnType("nvarchar(max)"); + + b.Property("SystemUserId") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ConditionId"); + + b.HasIndex("SerialNumberId"); + + b.ToTable("Maintenances"); + }); + + modelBuilder.Entity("Scribe.Models.Model", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BrandId") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("Image") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BrandId"); + + b.HasIndex("CategoryId"); + + b.ToTable("Models"); + }); + + modelBuilder.Entity("Scribe.Models.SerialNumber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ADUsersId") + .HasColumnType("int"); + + b.Property("AllocatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Allocation") + .HasColumnType("datetime2"); + + b.Property("ConditionId") + .HasColumnType("int"); + + b.Property("Creation") + .HasColumnType("datetime2"); + + b.Property("CurrentlyAllocated") + .HasColumnType("bit"); + + b.Property("DeallocatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DepartmentId") + .HasColumnType("int"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("GroupId") + .HasColumnType("int"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("MacAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("ModelId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SerialNumberId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ADUsersId"); + + b.HasIndex("ConditionId"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("GroupId"); + + b.HasIndex("LocationId"); + + b.HasIndex("ModelId"); + + b.HasIndex("SerialNumberId"); + + b.ToTable("SerialNumbers"); + }); + + modelBuilder.Entity("Scribe.Models.SerialNumberGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ADUsersId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("int"); + + b.Property("SerialNumberId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ADUsersId"); + + b.HasIndex("GroupId"); + + b.HasIndex("SerialNumberId"); + + b.ToTable("SerialNumberGroup"); + }); + + modelBuilder.Entity("Scribe.Models.SystemUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.Property("SamAccountName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserPrincipalName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("SystemUsers"); + }); + + modelBuilder.Entity("Scribe.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("GroupId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("UserId"); + + b.ToTable("UserGroup"); + }); + + modelBuilder.Entity("Scribe.Models.Warranty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ModelId") + .HasColumnType("int"); + + b.Property("PurchaseDate") + .HasColumnType("datetime2"); + + b.Property("WarrantyDurationYears") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ModelId"); + + b.ToTable("Warranties"); + }); + + modelBuilder.Entity("Scribe.Models.AllocationHistory", b => + { + b.HasOne("Scribe.Models.ADUsers", "ADUsers") + .WithMany() + .HasForeignKey("ADUsersId"); + + b.HasOne("Scribe.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId"); + + b.HasOne("Scribe.Models.SerialNumber", "SerialNumber") + .WithMany() + .HasForeignKey("SerialNumberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ADUsers"); + + b.Navigation("Group"); + + b.Navigation("SerialNumber"); + }); + + modelBuilder.Entity("Scribe.Models.IndividualAssignment", b => + { + b.HasOne("Scribe.Models.ADUsers", "ADUsers") + .WithMany() + .HasForeignKey("ADUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Scribe.Models.SerialNumber", "SerialNumber") + .WithMany() + .HasForeignKey("SerialNumberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ADUsers"); + + b.Navigation("SerialNumber"); + }); + + modelBuilder.Entity("Scribe.Models.Maintenance", b => + { + b.HasOne("Scribe.Models.Condition", "Condition") + .WithMany() + .HasForeignKey("ConditionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Scribe.Models.SerialNumber", "SerialNumber") + .WithMany() + .HasForeignKey("SerialNumberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Condition"); + + b.Navigation("SerialNumber"); + }); + + modelBuilder.Entity("Scribe.Models.Model", b => + { + b.HasOne("Scribe.Models.Brand", "Brand") + .WithMany() + .HasForeignKey("BrandId"); + + b.HasOne("Scribe.Models.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.Navigation("Brand"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Scribe.Models.SerialNumber", b => + { + b.HasOne("Scribe.Models.ADUsers", "ADUsers") + .WithMany("SerialNumbers") + .HasForeignKey("ADUsersId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Scribe.Models.Condition", "Condition") + .WithMany() + .HasForeignKey("ConditionId"); + + b.HasOne("Scribe.Models.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Scribe.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId"); + + b.HasOne("Scribe.Models.Location", "Location") + .WithMany() + .HasForeignKey("LocationId"); + + b.HasOne("Scribe.Models.Model", "Model") + .WithMany("SerialNumbers") + .HasForeignKey("ModelId"); + + b.HasOne("Scribe.Models.SerialNumber", null) + .WithMany("SerialNumbers") + .HasForeignKey("SerialNumberId"); + + b.Navigation("ADUsers"); + + b.Navigation("Condition"); + + b.Navigation("Department"); + + b.Navigation("Group"); + + b.Navigation("Location"); + + b.Navigation("Model"); + }); + + modelBuilder.Entity("Scribe.Models.SerialNumberGroup", b => + { + b.HasOne("Scribe.Models.ADUsers", "ADUsers") + .WithMany() + .HasForeignKey("ADUsersId"); + + b.HasOne("Scribe.Models.Group", "Group") + .WithMany("SerialNumberGroups") + .HasForeignKey("GroupId"); + + b.HasOne("Scribe.Models.SerialNumber", "SerialNumber") + .WithMany() + .HasForeignKey("SerialNumberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ADUsers"); + + b.Navigation("Group"); + + b.Navigation("SerialNumber"); + }); + + modelBuilder.Entity("Scribe.Models.UserGroup", b => + { + b.HasOne("Scribe.Models.Group", "Group") + .WithMany("UserGroups") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Scribe.Models.ADUsers", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Scribe.Models.Warranty", b => + { + b.HasOne("Scribe.Models.Model", "Model") + .WithMany() + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Model"); + }); + + modelBuilder.Entity("Scribe.Models.ADUsers", b => + { + b.Navigation("SerialNumbers"); + }); + + modelBuilder.Entity("Scribe.Models.Group", b => + { + b.Navigation("SerialNumberGroups"); + + b.Navigation("UserGroups"); + }); + + modelBuilder.Entity("Scribe.Models.Model", b => + { + b.Navigation("SerialNumbers"); + }); + + modelBuilder.Entity("Scribe.Models.SerialNumber", b => + { + b.Navigation("SerialNumbers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20260821005826_InitialCreate.cs b/Migrations/20260821005826_InitialCreate.cs new file mode 100644 index 0000000..b3b3f5a --- /dev/null +++ b/Migrations/20260821005826_InitialCreate.cs @@ -0,0 +1,603 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Scribe.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ADUsers", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ADUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Brands", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false), + ImageName = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Brands", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Icon = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Condition", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false), + ColorCode = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Condition", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Department", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Department", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Group", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Group", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Locations", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Description = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Locations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Log", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Details = table.Column(type: "nvarchar(max)", nullable: false), + User = table.Column(type: "nvarchar(max)", nullable: false), + Date = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Log", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "SystemUsers", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + FirstName = table.Column(type: "nvarchar(max)", nullable: true), + LastName = table.Column(type: "nvarchar(max)", nullable: true), + SamAccountName = table.Column(type: "nvarchar(max)", nullable: true), + UserPrincipalName = table.Column(type: "nvarchar(max)", nullable: true), + DisplayName = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_SystemUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Models", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false), + BrandId = table.Column(type: "int", nullable: true), + CategoryId = table.Column(type: "int", nullable: true), + Image = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Models", x => x.Id); + table.ForeignKey( + name: "FK_Models_Brands_BrandId", + column: x => x.BrandId, + principalTable: "Brands", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Models_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "UserGroup", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "int", nullable: false), + GroupId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserGroup", x => x.Id); + table.ForeignKey( + name: "FK_UserGroup_ADUsers_UserId", + column: x => x.UserId, + principalTable: "ADUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_UserGroup_Group_GroupId", + column: x => x.GroupId, + principalTable: "Group", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "SerialNumbers", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false), + ModelId = table.Column(type: "int", nullable: true), + ADUsersId = table.Column(type: "int", nullable: true), + ConditionId = table.Column(type: "int", nullable: true), + DepartmentId = table.Column(type: "int", nullable: true), + LocationId = table.Column(type: "int", nullable: true), + Creation = table.Column(type: "datetime2", nullable: true), + Allocation = table.Column(type: "datetime2", nullable: true), + AllocatedBy = table.Column(type: "nvarchar(max)", nullable: true), + DeallocatedBy = table.Column(type: "nvarchar(max)", nullable: true), + GroupId = table.Column(type: "int", nullable: true), + CurrentlyAllocated = table.Column(type: "bit", nullable: false), + Description = table.Column(type: "nvarchar(max)", nullable: true), + MacAddress = table.Column(type: "nvarchar(max)", nullable: true), + SerialNumberId = table.Column(type: "int", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_SerialNumbers", x => x.Id); + table.ForeignKey( + name: "FK_SerialNumbers_ADUsers_ADUsersId", + column: x => x.ADUsersId, + principalTable: "ADUsers", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_SerialNumbers_Condition_ConditionId", + column: x => x.ConditionId, + principalTable: "Condition", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_SerialNumbers_Department_DepartmentId", + column: x => x.DepartmentId, + principalTable: "Department", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_SerialNumbers_Group_GroupId", + column: x => x.GroupId, + principalTable: "Group", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_SerialNumbers_Locations_LocationId", + column: x => x.LocationId, + principalTable: "Locations", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_SerialNumbers_Models_ModelId", + column: x => x.ModelId, + principalTable: "Models", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_SerialNumbers_SerialNumbers_SerialNumberId", + column: x => x.SerialNumberId, + principalTable: "SerialNumbers", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Warranties", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ModelId = table.Column(type: "int", nullable: false), + PurchaseDate = table.Column(type: "datetime2", nullable: true), + WarrantyDurationYears = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Warranties", x => x.Id); + table.ForeignKey( + name: "FK_Warranties_Models_ModelId", + column: x => x.ModelId, + principalTable: "Models", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AllocationHistory", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + SerialNumberId = table.Column(type: "int", nullable: false), + ADUsersId = table.Column(type: "int", nullable: true), + AllocationDate = table.Column(type: "datetime2", nullable: false), + DeallocationDate = table.Column(type: "datetime2", nullable: true), + AllocatedBy = table.Column(type: "nvarchar(max)", nullable: true), + DeallocatedBy = table.Column(type: "nvarchar(max)", nullable: true), + GroupId = table.Column(type: "int", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AllocationHistory", x => x.Id); + table.ForeignKey( + name: "FK_AllocationHistory_ADUsers_ADUsersId", + column: x => x.ADUsersId, + principalTable: "ADUsers", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_AllocationHistory_Group_GroupId", + column: x => x.GroupId, + principalTable: "Group", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_AllocationHistory_SerialNumbers_SerialNumberId", + column: x => x.SerialNumberId, + principalTable: "SerialNumbers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "IndividualAssignment", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ADUsersId = table.Column(type: "int", nullable: false), + SerialNumberId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_IndividualAssignment", x => x.Id); + table.ForeignKey( + name: "FK_IndividualAssignment_ADUsers_ADUsersId", + column: x => x.ADUsersId, + principalTable: "ADUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_IndividualAssignment_SerialNumbers_SerialNumberId", + column: x => x.SerialNumberId, + principalTable: "SerialNumbers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Maintenances", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + SerialNumberId = table.Column(type: "int", nullable: false), + ServiceDescription = table.Column(type: "nvarchar(max)", nullable: true), + ServiceDate = table.Column(type: "datetime2", nullable: false), + NextServiceDate = table.Column(type: "datetime2", nullable: false), + ConditionId = table.Column(type: "int", nullable: false), + SystemUserId = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Maintenances", x => x.Id); + table.ForeignKey( + name: "FK_Maintenances_Condition_ConditionId", + column: x => x.ConditionId, + principalTable: "Condition", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Maintenances_SerialNumbers_SerialNumberId", + column: x => x.SerialNumberId, + principalTable: "SerialNumbers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "SerialNumberGroup", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + SerialNumberId = table.Column(type: "int", nullable: false), + GroupId = table.Column(type: "int", nullable: true), + ADUsersId = table.Column(type: "int", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_SerialNumberGroup", x => x.Id); + table.ForeignKey( + name: "FK_SerialNumberGroup_ADUsers_ADUsersId", + column: x => x.ADUsersId, + principalTable: "ADUsers", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_SerialNumberGroup_Group_GroupId", + column: x => x.GroupId, + principalTable: "Group", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_SerialNumberGroup_SerialNumbers_SerialNumberId", + column: x => x.SerialNumberId, + principalTable: "SerialNumbers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "ADUsers", + columns: new[] { "Id", "Name" }, + values: new object[] { 1, "No User" }); + + migrationBuilder.InsertData( + table: "Condition", + columns: new[] { "Id", "ColorCode", "Name" }, + values: new object[,] + { + { 1, "green", "New" }, + { 2, "yellow", "Needs Repairs" }, + { 3, "blue", "In Use" }, + { 4, "red", "Out Of Order" }, + { 5, "grey", "Awaiting User" } + }); + + migrationBuilder.InsertData( + table: "Department", + columns: new[] { "Id", "Name" }, + values: new object[,] + { + { 1, "IT" }, + { 2, "No Department" } + }); + + migrationBuilder.InsertData( + table: "Locations", + columns: new[] { "Id", "Description", "Name" }, + values: new object[,] + { + { 1, "IT Storage room", "New Storage" }, + { 2, "IT Storage room", "Old Storage" }, + { 3, "User Office or Station", "User Station" } + }); + + migrationBuilder.CreateIndex( + name: "IX_AllocationHistory_ADUsersId", + table: "AllocationHistory", + column: "ADUsersId"); + + migrationBuilder.CreateIndex( + name: "IX_AllocationHistory_GroupId", + table: "AllocationHistory", + column: "GroupId"); + + migrationBuilder.CreateIndex( + name: "IX_AllocationHistory_SerialNumberId", + table: "AllocationHistory", + column: "SerialNumberId"); + + migrationBuilder.CreateIndex( + name: "IX_IndividualAssignment_ADUsersId", + table: "IndividualAssignment", + column: "ADUsersId"); + + migrationBuilder.CreateIndex( + name: "IX_IndividualAssignment_SerialNumberId", + table: "IndividualAssignment", + column: "SerialNumberId"); + + migrationBuilder.CreateIndex( + name: "IX_Maintenances_ConditionId", + table: "Maintenances", + column: "ConditionId"); + + migrationBuilder.CreateIndex( + name: "IX_Maintenances_SerialNumberId", + table: "Maintenances", + column: "SerialNumberId"); + + migrationBuilder.CreateIndex( + name: "IX_Models_BrandId", + table: "Models", + column: "BrandId"); + + migrationBuilder.CreateIndex( + name: "IX_Models_CategoryId", + table: "Models", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_SerialNumberGroup_ADUsersId", + table: "SerialNumberGroup", + column: "ADUsersId"); + + migrationBuilder.CreateIndex( + name: "IX_SerialNumberGroup_GroupId", + table: "SerialNumberGroup", + column: "GroupId"); + + migrationBuilder.CreateIndex( + name: "IX_SerialNumberGroup_SerialNumberId", + table: "SerialNumberGroup", + column: "SerialNumberId"); + + migrationBuilder.CreateIndex( + name: "IX_SerialNumbers_ADUsersId", + table: "SerialNumbers", + column: "ADUsersId"); + + migrationBuilder.CreateIndex( + name: "IX_SerialNumbers_ConditionId", + table: "SerialNumbers", + column: "ConditionId"); + + migrationBuilder.CreateIndex( + name: "IX_SerialNumbers_DepartmentId", + table: "SerialNumbers", + column: "DepartmentId"); + + migrationBuilder.CreateIndex( + name: "IX_SerialNumbers_GroupId", + table: "SerialNumbers", + column: "GroupId"); + + migrationBuilder.CreateIndex( + name: "IX_SerialNumbers_LocationId", + table: "SerialNumbers", + column: "LocationId"); + + migrationBuilder.CreateIndex( + name: "IX_SerialNumbers_ModelId", + table: "SerialNumbers", + column: "ModelId"); + + migrationBuilder.CreateIndex( + name: "IX_SerialNumbers_SerialNumberId", + table: "SerialNumbers", + column: "SerialNumberId"); + + migrationBuilder.CreateIndex( + name: "IX_UserGroup_GroupId", + table: "UserGroup", + column: "GroupId"); + + migrationBuilder.CreateIndex( + name: "IX_UserGroup_UserId", + table: "UserGroup", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Warranties_ModelId", + table: "Warranties", + column: "ModelId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AllocationHistory"); + + migrationBuilder.DropTable( + name: "IndividualAssignment"); + + migrationBuilder.DropTable( + name: "Log"); + + migrationBuilder.DropTable( + name: "Maintenances"); + + migrationBuilder.DropTable( + name: "SerialNumberGroup"); + + migrationBuilder.DropTable( + name: "SystemUsers"); + + migrationBuilder.DropTable( + name: "UserGroup"); + + migrationBuilder.DropTable( + name: "Warranties"); + + migrationBuilder.DropTable( + name: "SerialNumbers"); + + migrationBuilder.DropTable( + name: "ADUsers"); + + migrationBuilder.DropTable( + name: "Condition"); + + migrationBuilder.DropTable( + name: "Department"); + + migrationBuilder.DropTable( + name: "Group"); + + migrationBuilder.DropTable( + name: "Locations"); + + migrationBuilder.DropTable( + name: "Models"); + + migrationBuilder.DropTable( + name: "Brands"); + + migrationBuilder.DropTable( + name: "Categories"); + } + } +} diff --git a/Program.cs b/Program.cs index b4f7586..5024845 100644 --- a/Program.cs +++ b/Program.cs @@ -3,9 +3,9 @@ using Scribe.Data; using Scribe.Infrastructure; using Scribe.Services; -using System.Security.Claims; -using System.DirectoryServices.AccountManagement; using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.HttpOverrides; +using System.Net; var builder = WebApplication.CreateBuilder(args); @@ -15,36 +15,24 @@ options.UseSqlServer(connectionString)); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); -// Use only Cookie Authentication +// Cookie authentication is used for VPS/demo deployments. It does not depend on +// Windows Authentication, Active Directory, or an AD group lookup. builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) - .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme); + .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => + { + options.LoginPath = "/Account/Login"; + options.AccessDeniedPath = "/Account/AccessDenied"; + options.Cookie.SecurePolicy = CookieSecurePolicy.Always; + }); builder.Services.AddAuthorization(options => { - options.AddPolicy("GroupPolicy", policy => - policy.RequireAssertion(context => - { - var user = context.User; - var domain = "zlt.co.zw"; - var groupName = "Scribe Admins"; - - using (var principalContext = new PrincipalContext(ContextType.Domain, domain)) - using (var group = GroupPrincipal.FindByIdentity(principalContext, groupName)) - { - if (group != null) - { - foreach (var member in group.GetMembers()) - { - if (member.SamAccountName.Equals(user.Identity.Name, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - } - } - return false; - })); - options.FallbackPolicy = options.GetPolicy("GroupPolicy"); + // Keep the policy name used by existing controllers while removing the + // Active Directory group lookup from its implementation. + options.AddPolicy("GroupPolicy", policy => policy.RequireAuthenticatedUser()); + options.FallbackPolicy = new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .Build(); }); // Registering Services @@ -65,12 +53,42 @@ }); // Configure Data Protection +var keyDirectory = builder.Configuration["DataProtection:KeyDirectory"] + ?? Path.Combine(builder.Environment.ContentRootPath, "data-protection-keys"); builder.Services.AddDataProtection() - .PersistKeysToFileSystem(new DirectoryInfo(@"C:\keys")) + .PersistKeysToFileSystem(new DirectoryInfo(keyDirectory)) .SetApplicationName("Scribe"); +builder.Services.Configure(options => +{ + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor + | ForwardedHeaders.XForwardedProto + | ForwardedHeaders.XForwardedHost; + options.KnownProxies.Clear(); + options.KnownNetworks.Clear(); + options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(IPAddress.Parse("100.64.0.0"), 10)); + options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(IPAddress.Parse("192.168.0.0"), 16)); +}); + var app = builder.Build(); +app.UseForwardedHeaders(); + +// The Compose demo stack can initialize its database automatically. Leave this +// disabled by default for existing deployments where migrations are managed +// separately. +if (builder.Configuration.GetValue("Database:ApplyMigrationsOnStartup")) +{ + using var scope = app.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.Migrate(); + + if (builder.Configuration.GetValue("Database:SeedDemoData")) + { + await DemoDataSeeder.SeedAsync(db); + } +} + // Configure the HTTP request pipeline. if (app.Environment.IsProduction()) { @@ -82,7 +100,10 @@ app.UseHsts(); } -app.UseHttpsRedirection(); +if (builder.Configuration.GetValue("Http:UseHttpsRedirection", true)) +{ + app.UseHttpsRedirection(); +} app.UseStaticFiles(); app.UseRouting(); @@ -108,4 +129,4 @@ app.MapRazorPages(); -app.Run(); \ No newline at end of file +app.Run(); diff --git a/README.md b/README.md index f216324..9c32d9a 100644 --- a/README.md +++ b/README.md @@ -1,117 +1,89 @@ -# Scribe - Inventory Management System +# Scribe -readme_content = """# 📦 Scribe - Inventory Management System +[![Build](https://github.com/mudabs/Scribe/actions/workflows/dotnet.yml/badge.svg)](https://github.com/mudabs/Scribe/actions/workflows/dotnet.yml) -Transform Content Creation, Empower Knowledge Sharing Seamlessly +Scribe is an ASP.NET Core MVC application for managing an organization’s inventory and equipment lifecycle. -![GitHub stars](https://img.shields.io/github/stars/scribe) ![GitHub forks](https://img.shields.io/github/forks/scribe) ![GitHub issues](https://img.shields.io/github/issues/scribe) +## Features -![ASP.NET Core](https://img.shields.io/badge/ASP.NET%20Core-5C2D91?style=for-the-badge&logo=dot-net&logoColor=white) -![MVC](https://img.shields.io/badge/MVC-007ACC?style=for-the-badge&logo=mvc&logoColor=white) -![NuGet](https://img.shields.io/badge/NuGet-004880?style=for-the-badge&logo=nuget&logoColor=white) +- Inventory catalog for brands, categories, models, serial numbers, conditions, and locations +- Asset allocation, deallocation, assignment history, and user/group ownership +- Maintenance and service-history tracking +- Dashboard and inventory reporting views +- CSV, XLS, and XLSX serial-number imports +- Active Directory integration for Windows deployments +- Local cookie authentication and seeded demonstration data for container/VPS deployments ---- +## Technology -## 📚 Table of Contents - -- [Overview](#overview) -- [Features](#features) -- [Architecture](#architecture) -- [Getting Started](#getting-started) - - [Prerequisites](#prerequisites) - - [Installation](#installation) - - [Usage](#usage) - - [Testing](#testing) -- [Technologies Used](#technologies-used) -- [Contributing](#contributing) -- [License](#license) - ---- - -## 🧭 Overview - -**Scribe** is a robust and scalable ASP.NET Core MVC application designed to manage inventory, assets, users, and maintenance workflows. It is ideal for organizations looking to streamline their internal operations with a centralized, secure, and extensible platform. - ---- - -## 🚀 Features +- .NET 9 and ASP.NET Core MVC +- Entity Framework Core 9 +- SQL Server +- Razor Views, Bootstrap, and jQuery +- ExcelDataReader for spreadsheet imports -- 🔧 **Asset Management**: Track devices, tools, and consumables with lifecycle states, conditions, and location mapping. -- 👥 **User & Role Management**: Integrates with Active Directory for seamless user provisioning and access control. -- 📊 **Reporting & Analytics**: Export data to Excel/PDF and visualize trends with built-in dashboards. -- 🧩 **Modular UI Components**: Reusable Razor components for CRUD operations, modals, and validation. -- ⚙️ **Centralized Configuration**: Manage environment variables, connection strings, and feature toggles. -- 🛠️ **Maintenance Scheduling**: Log maintenance tasks, assign technicians, and track service history. +## Run with Docker ---- +Docker and Docker Compose are the simplest way to run the demonstration stack. The stack starts Scribe and a persistent SQL Server container, applies EF Core migrations on startup, and exposes the app at http://localhost:8080. -## 🏗️ Architecture + docker compose up --build -d -- **Backend**: ASP.NET Core MVC with Entity Framework Core -- **Frontend**: Razor Views, Bootstrap, jQuery -- **Database**: SQL Server (or any EF Core-compatible DB) -- **Authentication**: Active Directory / Identity -- **Dependency Injection**: Built-in .NET Core DI container +The default demonstration credentials are demo / ChangeMe123!. Set DEMO_USERNAME, DEMO_PASSWORD, and SA_PASSWORD before starting the stack to replace them. ---- +Demo data seeding is enabled by default and adds brands, categories, models, devices, users, groups, warranties, maintenance records, and local image assets. Set SEED_DEMO_DATA=false to disable seeding. The seeder is additive and safe to run against an existing database. -## 🛠️ Getting Started +Stop the stack with: -### Prerequisites + docker compose down -- [.NET SDK 6.0+](https://dotnet.microsoft.com/) -- [SQL Server](https://www.microsoft.com/en-us/sql-server) -- [Visual Studio 2022+](https://visualstudio.microsoft.com/) -- NuGet Package Manager +The named database volume is preserved. To remove it as well, use docker compose down -v. -### Installation +## Run from source -1. **Clone the repository** - git clone https://github.com/scribe/scribe - cd scribe +Prerequisites: -2. **Restore dependencies** - dotnet restore +- .NET SDK 9.0 +- SQL Server, or Docker for the database container +- EF Core CLI tools if you need to manage migrations -3. **Update database** - dotnet ef database update +Restore and build: -4. **Run the application** - dotnet run + dotnet restore + dotnet build -### Usage +Set the connection string through the ConnectionStrings__DefaultConnection environment variable; do not put database credentials in appsettings.json. For a local run, set it before applying migrations: -- Access the app at `https://localhost:5001` -- Login with your Active Directory credentials or seeded admin account -- Navigate through the dashboard to manage assets, users, and reports + $env:ConnectionStrings__DefaultConnection = "Server=localhost;Database=ScribeDB;User Id=sa;Password=YOUR_DATABASE_PASSWORD;MultipleActiveResultSets=true;TrustServerCertificate=True" -### Testing + dotnet ef database update + dotnet run -- Unit tests are located in the `Scribe.Tests` project -- Run tests using: - dotnet test +## Authentication ---- +The Windows deployment path supports Active Directory. The Docker/VPS demonstration path uses local cookie authentication and does not require Windows Authentication or an Active Directory group lookup. -## 🧰 Technologies Used +For the deployed VPS instance, the username is demo. Its password is stored in the protected /opt/apps/scribe/.env file and is intentionally not committed to the repository. -- ASP.NET Core MVC -- Entity Framework Core -- SQL Server -- Active Directory -- Bootstrap 5 -- jQuery -- NuGet +## Deployment ---- +The deployed demonstration instance is available at https://scribe.munashemudabura.com. HTTPS terminates at the public IONOS gateway and is forwarded privately over Tailscale to the Scribe container on vps01; SQL Server is not publicly exposed. -## 🤝 Contributing +The IONOS site configuration used for this deployment is in the [deployment configuration](deploy/ionos/scribe.munashemudabura.com.conf). -Contributions are welcome! Please fork the repository and submit a pull request with your changes. +## Verification ---- +The repository currently contains the application project but no dedicated automated test project. The CI workflow restores dependencies, builds the solution with .NET 9, and runs dotnet test so tests can be added later without changing the workflow. -## 📄 License + dotnet build + dotnet test --no-build -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +## Repository layout +- Controllers/ — MVC controllers +- Data/ — EF Core context and runtime demo seeding +- Migrations/ — EF Core database migrations +- Models/ — domain models +- Services/ and Infrastructure/ — application services and view components +- Views/ — Razor views +- wwwroot/ — static assets and seeded brand/model images +- deploy/ — deployment configuration diff --git a/Scribe.csproj b/Scribe.csproj index 535b4a5..0f37925 100644 --- a/Scribe.csproj +++ b/Scribe.csproj @@ -27,8 +27,6 @@ - - diff --git a/appsettings.json b/appsettings.json index edefdaa..c88b43a 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,16 +1,13 @@ { - "ConnectionStrings": { - //Live DB - "DefaultConnection": "Server=ZW-ETS-01;Database=ScribeDB;User Id=sa;Password=Admin.123;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True" - - - //"DefaultConnection": "Server=ZW-ETS-01;Database=ScribeDB2;User Id=sa;Password=Admin.123;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True" - }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, + "DemoAuthentication": { + "Username": "demo", + "Password": "ChangeMe123!" + }, "AllowedHosts": "*" -} \ No newline at end of file +} diff --git a/deploy/ionos/scribe.munashemudabura.com.conf b/deploy/ionos/scribe.munashemudabura.com.conf new file mode 100644 index 0000000..8aa4c76 --- /dev/null +++ b/deploy/ionos/scribe.munashemudabura.com.conf @@ -0,0 +1,37 @@ +server { + listen 80; + listen [::]:80; + server_name scribe.munashemudabura.com; + + location /.well-known/acme-challenge/ { + root /var/www/scribe-acme; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name scribe.munashemudabura.com; + + client_max_body_size 25m; + + ssl_certificate /etc/letsencrypt/live/scribe.munashemudabura.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/scribe.munashemudabura.com/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + location / { + proxy_pass http://100.110.81.2:8082; + proxy_redirect http://scribe.munashemudabura.com/ https://scribe.munashemudabura.com/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..104c6a4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "8080:8080" + environment: + ASPNETCORE_ENVIRONMENT: Production + ASPNETCORE_URLS: http://+:8080 + Http__UseHttpsRedirection: "false" + ConnectionStrings__DefaultConnection: "Server=db,1433;Database=ScribeDB;User Id=sa;Password=${SA_PASSWORD:?Set SA_PASSWORD in .env or your environment};MultipleActiveResultSets=true;TrustServerCertificate=True" + Database__ApplyMigrationsOnStartup: "true" + Database__SeedDemoData: "${SEED_DEMO_DATA:-true}" + DemoAuthentication__Username: "${DEMO_USERNAME:-demo}" + DemoAuthentication__Password: "${DEMO_PASSWORD:-ChangeMe123!}" + DataProtection__KeyDirectory: "/app/data-protection-keys" + volumes: + - app_keys:/app/data-protection-keys + depends_on: + db: + condition: service_healthy + restart: unless-stopped + + db: + image: mcr.microsoft.com/mssql/server:2022-latest + environment: + ACCEPT_EULA: "Y" + MSSQL_SA_PASSWORD: "${SA_PASSWORD:?Set SA_PASSWORD in .env or your environment}" + ports: + - "1433:1433" + volumes: + - sqlserver_data:/var/opt/mssql + healthcheck: + test: ["CMD-SHELL", "(/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P \"$${MSSQL_SA_PASSWORD}\" -C -Q \"SELECT 1\" || /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P \"$${MSSQL_SA_PASSWORD}\" -Q \"SELECT 1\") >/dev/null 2>&1"] + interval: 10s + timeout: 5s + retries: 20 + start_period: 30s + restart: unless-stopped + +volumes: + app_keys: + sqlserver_data: diff --git a/wwwroot/media/brands/acer.svg b/wwwroot/media/brands/acer.svg new file mode 100644 index 0000000..ca9b2bb --- /dev/null +++ b/wwwroot/media/brands/acer.svg @@ -0,0 +1 @@ +Acer \ No newline at end of file diff --git a/wwwroot/media/brands/apple.svg b/wwwroot/media/brands/apple.svg new file mode 100644 index 0000000..de4b0d1 --- /dev/null +++ b/wwwroot/media/brands/apple.svg @@ -0,0 +1 @@ +Apple \ No newline at end of file diff --git a/wwwroot/media/brands/asus.svg b/wwwroot/media/brands/asus.svg new file mode 100644 index 0000000..18ef199 --- /dev/null +++ b/wwwroot/media/brands/asus.svg @@ -0,0 +1 @@ +ASUS \ No newline at end of file diff --git a/wwwroot/media/brands/cisco.svg b/wwwroot/media/brands/cisco.svg new file mode 100644 index 0000000..bb41aaf --- /dev/null +++ b/wwwroot/media/brands/cisco.svg @@ -0,0 +1 @@ +Cisco \ No newline at end of file diff --git a/wwwroot/media/brands/dell.svg b/wwwroot/media/brands/dell.svg new file mode 100644 index 0000000..d272bbf --- /dev/null +++ b/wwwroot/media/brands/dell.svg @@ -0,0 +1 @@ +Dell \ No newline at end of file diff --git a/wwwroot/media/brands/epson.svg b/wwwroot/media/brands/epson.svg new file mode 100644 index 0000000..e400ea5 --- /dev/null +++ b/wwwroot/media/brands/epson.svg @@ -0,0 +1 @@ +Epson \ No newline at end of file diff --git a/wwwroot/media/brands/hp.svg b/wwwroot/media/brands/hp.svg new file mode 100644 index 0000000..04b5f0e --- /dev/null +++ b/wwwroot/media/brands/hp.svg @@ -0,0 +1 @@ +HP \ No newline at end of file diff --git a/wwwroot/media/brands/intel.svg b/wwwroot/media/brands/intel.svg new file mode 100644 index 0000000..47b79bf --- /dev/null +++ b/wwwroot/media/brands/intel.svg @@ -0,0 +1 @@ +Intel \ No newline at end of file diff --git a/wwwroot/media/brands/lenovo.svg b/wwwroot/media/brands/lenovo.svg new file mode 100644 index 0000000..c03719a --- /dev/null +++ b/wwwroot/media/brands/lenovo.svg @@ -0,0 +1 @@ +Lenovo \ No newline at end of file diff --git a/wwwroot/media/brands/samsung.svg b/wwwroot/media/brands/samsung.svg new file mode 100644 index 0000000..627cbf2 --- /dev/null +++ b/wwwroot/media/brands/samsung.svg @@ -0,0 +1 @@ +Samsung \ No newline at end of file diff --git a/wwwroot/media/icons/categories/default.svg b/wwwroot/media/icons/categories/default.svg new file mode 100644 index 0000000..78a117f --- /dev/null +++ b/wwwroot/media/icons/categories/default.svg @@ -0,0 +1 @@ + No icon \ No newline at end of file diff --git a/wwwroot/media/models/demo-laptop.jpg b/wwwroot/media/models/demo-laptop.jpg new file mode 100644 index 0000000..e006c69 Binary files /dev/null and b/wwwroot/media/models/demo-laptop.jpg differ diff --git a/wwwroot/media/models/demo-monitor.jpg b/wwwroot/media/models/demo-monitor.jpg new file mode 100644 index 0000000..05f31db Binary files /dev/null and b/wwwroot/media/models/demo-monitor.jpg differ diff --git a/wwwroot/media/models/demo-printer.jpg b/wwwroot/media/models/demo-printer.jpg new file mode 100644 index 0000000..7a6ba5b Binary files /dev/null and b/wwwroot/media/models/demo-printer.jpg differ diff --git a/wwwroot/media/models/demo-router.jpg b/wwwroot/media/models/demo-router.jpg new file mode 100644 index 0000000..d57aeba Binary files /dev/null and b/wwwroot/media/models/demo-router.jpg differ diff --git a/wwwroot/media/models/demo-smartphone.jpg b/wwwroot/media/models/demo-smartphone.jpg new file mode 100644 index 0000000..98d270f Binary files /dev/null and b/wwwroot/media/models/demo-smartphone.jpg differ