Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ on:
- 'BlueberryMart.Api/**'
- 'Tests/**'
- '.github/workflows/deploy.yml'
pull_request:
branches: [main]

env:
PROJECT_ID: project-76ca6efe-7878-4dc8-bff
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/frontend-typecheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/portal-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ coverage/
# ── OS ────────────────────────────────────────────────────
.DS_Store
Thumbs.db

# ── Images ───────────────────────────────────────────────────
BlueberryMart-screenshots
46 changes: 43 additions & 3 deletions BlueberryMart.Api/Data/DbInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using BlueberryMart.Api.Security;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

namespace BlueberryMart.Api.Data;

Expand All @@ -11,15 +12,25 @@ public static class DbInitializer
/// cleanup below can remove it. In-store walk-in sales now use a null <c>Order.UserId</c>.</summary>
private const string LegacyWalkInEmail = "walkin@system.blueberrymart.local";

public static void Initialize(BlueberryMartDbContext context, IConfiguration config)
/// <summary>Demo login accounts previously seeded in every environment, including Production,
/// with passwords published in project docs. See <see cref="RotateLegacyDemoAccountPasswords"/>.</summary>
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);
}

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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<User>
{
new()
Expand Down Expand Up @@ -176,5 +192,29 @@ private static void SeedDemoData(BlueberryMartDbContext context)
context.SaveChanges();
}

/// <summary>
/// Earlier deploys seeded the demo accounts in <see cref="LegacyDemoAccountEmails"/> into
/// Production too, with passwords that were published in project docs. Production no longer
/// seeds them (see <see cref="SeedDemoData"/>), 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.
/// </summary>
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);
}
2 changes: 1 addition & 1 deletion BlueberryMart.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ await ctx.HttpContext.Response.WriteAsJsonAsync(
{
using var scope = app.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<BlueberryMartDbContext>();
DbInitializer.Initialize(context, app.Configuration);
DbInitializer.Initialize(context, app.Configuration, app.Environment);
}

// One-off data seeding: `dotnet run --project BlueberryMart.Api -- seed [--orders N …]`
Expand Down
12 changes: 9 additions & 3 deletions Markdown files/ChatBot.md
Original file line number Diff line number Diff line change
Expand Up @@ -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":"<your-shareholder-email>","password":"<your-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).

---

Expand Down
7 changes: 7 additions & 0 deletions Markdown files/SECURITY_POSTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?* ✅
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<IConfiguration>();
DbInitializer.Initialize(context, config);
var env = scope.ServiceProvider.GetRequiredService<IHostEnvironment>();
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;
Expand Down
Loading