diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6a1402d..07d0ae9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -7,6 +7,8 @@ on: - 'BlueberryMart.Api/**' - 'Tests/**' - '.github/workflows/deploy.yml' + pull_request: + branches: [main] env: PROJECT_ID: project-76ca6efe-7878-4dc8-bff @@ -64,11 +66,12 @@ jobs: - name: Run integration tests run: dotnet test Tests/BlueberryMart.Api.Tests --verbosity normal - # ── Job 2: Build image and deploy (only if tests passed) ───────────────── + # ── Job 2: Build image and deploy (only if tests passed, push to main only) ── deploy: name: Build & Deploy runs-on: ubuntu-latest needs: test + if: github.event_name == 'push' permissions: contents: read diff --git a/.github/workflows/frontend-typecheck.yml b/.github/workflows/frontend-typecheck.yml index de52e11..a15e5cc 100644 --- a/.github/workflows/frontend-typecheck.yml +++ b/.github/workflows/frontend-typecheck.yml @@ -6,6 +6,8 @@ on: paths: - 'BlueberryMartApp/**' - '.github/workflows/frontend-typecheck.yml' + pull_request: + branches: [main] jobs: # Each check is its own job so it shows as a separate status line and runs in diff --git a/.github/workflows/portal-ci.yml b/.github/workflows/portal-ci.yml index aa866da..1386cd0 100644 --- a/.github/workflows/portal-ci.yml +++ b/.github/workflows/portal-ci.yml @@ -6,6 +6,8 @@ on: paths: - 'BlueberryMartPortal/**' - '.github/workflows/portal-ci.yml' + pull_request: + branches: [main] jobs: # Each check is its own job so it shows as a separate status line and runs in diff --git a/.gitignore b/.gitignore index 2a2a905..364a2c7 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ coverage/ # ── OS ──────────────────────────────────────────────────── .DS_Store Thumbs.db + +# ── Images ─────────────────────────────────────────────────── +BlueberryMart-screenshots \ No newline at end of file diff --git a/BlueberryMart.Api/Data/DbInitializer.cs b/BlueberryMart.Api/Data/DbInitializer.cs index 7c92798..113db1c 100644 --- a/BlueberryMart.Api/Data/DbInitializer.cs +++ b/BlueberryMart.Api/Data/DbInitializer.cs @@ -2,6 +2,7 @@ using BlueberryMart.Api.Security; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; namespace BlueberryMart.Api.Data; @@ -11,15 +12,25 @@ public static class DbInitializer /// cleanup below can remove it. In-store walk-in sales now use a null Order.UserId. private const string LegacyWalkInEmail = "walkin@system.blueberrymart.local"; - public static void Initialize(BlueberryMartDbContext context, IConfiguration config) + /// Demo login accounts previously seeded in every environment, including Production, + /// with passwords published in project docs. See . + private static readonly string[] LegacyDemoAccountEmails = + [ + "customer1@blueberrymart.com", + "customer2@blueberrymart.com", + "shareholder1@blueberrymart.com" + ]; + + public static void Initialize(BlueberryMartDbContext context, IConfiguration config, IHostEnvironment env) { // Apply any pending EF Core migrations (creates the schema on a fresh DB, // brings an existing DB up to date). Replaces the old EnsureCreated(). context.Database.Migrate(); - SeedDemoData(context); + SeedDemoData(context, env); EnsureAdmin(context, config); RemoveLegacyWalkInCustomer(context); + RotateLegacyDemoAccountPasswords(context, env); EnsureSettings(context); } @@ -83,7 +94,7 @@ private static void EnsureAdmin(BlueberryMartDbContext context, IConfiguration c context.SaveChanges(); } - private static void SeedDemoData(BlueberryMartDbContext context) + private static void SeedDemoData(BlueberryMartDbContext context, IHostEnvironment env) { if (context.Branches.Any() || context.Users.Any()) return; @@ -135,6 +146,11 @@ private static void SeedDemoData(BlueberryMartDbContext context) context.Inventory.AddRange(inventory); context.SaveChanges(); + // Demo login accounts are a local/dev/CI convenience only. Never seed them in Production — + // a known email+password (one of them Shareholder-level) would grant real access to a + // live system. Admins/testers create real accounts through normal registration instead. + if (env.IsProduction()) return; + var users = new List { new() @@ -176,5 +192,29 @@ private static void SeedDemoData(BlueberryMartDbContext context) context.SaveChanges(); } + /// + /// Earlier deploys seeded the demo accounts in into + /// Production too, with passwords that were published in project docs. Production no longer + /// seeds them (see ), but any copies an earlier deploy already + /// created still have the old, now-public password. On every Production startup, overwrite + /// their hash with a fresh random value that is never logged or stored anywhere — the + /// published passwords stop working immediately. No-op once the accounts are gone, and + /// a no-op everywhere but Production. + /// + private static void RotateLegacyDemoAccountPasswords(BlueberryMartDbContext context, IHostEnvironment env) + { + if (!env.IsProduction()) return; + + var accounts = context.Users.Where(u => LegacyDemoAccountEmails.Contains(u.Email)).ToList(); + if (accounts.Count == 0) return; + + foreach (var account in accounts) + { + account.PasswordHash = BCrypt(Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N")); + account.UpdatedAt = DateTime.UtcNow; + } + context.SaveChanges(); + } + private static string BCrypt(string plaintext) => PasswordHasher.Hash(plaintext); } diff --git a/BlueberryMart.Api/Program.cs b/BlueberryMart.Api/Program.cs index 8a73dd5..a76f497 100644 --- a/BlueberryMart.Api/Program.cs +++ b/BlueberryMart.Api/Program.cs @@ -317,7 +317,7 @@ await ctx.HttpContext.Response.WriteAsJsonAsync( { using var scope = app.Services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); - DbInitializer.Initialize(context, app.Configuration); + DbInitializer.Initialize(context, app.Configuration, app.Environment); } // One-off data seeding: `dotnet run --project BlueberryMart.Api -- seed [--orders N …]` diff --git a/Markdown files/ChatBot.md b/Markdown files/ChatBot.md index bad7e85..e7f8eb5 100644 --- a/Markdown files/ChatBot.md +++ b/Markdown files/ChatBot.md @@ -130,16 +130,22 @@ gcloud run services update blueberrymart-api --region us-central1 --project proj ## Verify (this is live and working) +Log in with a real shareholder account (Production no longer seeds demo login accounts — see +`SECURITY_POSTURE.md`), then hit `/api/chat` with that token: + ```bash PROD=https://blueberrymart-api-278293545480.us-central1.run.app TOKEN=$(curl -s -X POST $PROD/api/auth/login -H 'Content-Type: application/json' \ - -d '{"email":"shareholder1@blueberrymart.com","password":"shareholder1_password"}' \ + -d '{"email":"","password":""}' \ | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])") curl -s -X POST $PROD/api/chat -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"messages":[{"role":"user","content":"Is Brown Eggs in stock and how much?"}]}' ``` -Confirmed reply: *"Yes, Brown Eggs (12 pack) are in stock at Blueberry Mart Downtown, with 41 -packs available, and they cost Rs 180."* — grounded in the live catalog. +Confirmed reply (2026-06): *"Yes, Brown Eggs (12 pack) are in stock at Blueberry Mart Downtown, +with 41 packs available, and they cost Rs 180."* — grounded in the live catalog. + +For local/dev testing, the demo `shareholder1@blueberrymart.com` account is still seeded +automatically (Development/Testing only). --- diff --git a/Markdown files/SECURITY_POSTURE.md b/Markdown files/SECURITY_POSTURE.md index 7277a15..0e7c11c 100644 --- a/Markdown files/SECURITY_POSTURE.md +++ b/Markdown files/SECURITY_POSTURE.md @@ -38,6 +38,13 @@ test values. Batch 1 = `df6b2f2`: Dependabot, request-body cap, CORS; on top of **sandbox** key are both non-production test values (the eSewa one is eSewa's publicly documented sandbox HMAC key, used as a test vector). Not real secrets; left as-is. Just don't reuse that DB password anywhere real. +- **Fixed (2026-07-02):** `DbInitializer` was seeding demo login accounts (`customer1`, `customer2`, + `shareholder1`) into **Production**, and `shareholder1`'s password was published in + `ChatBot.md` — a public repo reader could log in with real Shareholder access (full inventory + + analytics). Fixed: demo accounts no longer seed when `IHostEnvironment.IsProduction()`; the + already-seeded Production copies have their password hash rotated to a fresh random value on + every Production startup (nobody knows it — equivalent to disabling those accounts). Doc scrubbed + of the plaintext password. Demo accounts still seed normally in Development/Testing/CI. - **Where we are:** Good. ### 4. Access control — *Can a user modify requests to gain access?* ✅ diff --git a/Tests/BlueberryMart.Api.Tests/Infrastructure/BlueberryMartApiFactory.cs b/Tests/BlueberryMart.Api.Tests/Infrastructure/BlueberryMartApiFactory.cs index b809a49..815581a 100644 --- a/Tests/BlueberryMart.Api.Tests/Infrastructure/BlueberryMartApiFactory.cs +++ b/Tests/BlueberryMart.Api.Tests/Infrastructure/BlueberryMartApiFactory.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; namespace BlueberryMart.Api.Tests.Infrastructure; @@ -70,7 +71,8 @@ public async Task InitializeAsync() await context.Database.EnsureDeletedAsync(); // DbInitializer.Initialize runs Migrate() to build the schema from migrations, then seeds var config = scope.ServiceProvider.GetRequiredService(); - DbInitializer.Initialize(context, config); + var env = scope.ServiceProvider.GetRequiredService(); + DbInitializer.Initialize(context, config, env); DowntownBranchId = (await context.Branches.FirstAsync(b => b.Name == "Blueberry Mart Downtown")).Id; SuburbsBranchId = (await context.Branches.FirstAsync(b => b.Name == "Blueberry Mart Suburbs")).Id;