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;