diff --git a/.github/workflows/ci-mcp-server.yml b/.github/workflows/ci-mcp-server.yml new file mode 100644 index 00000000..709d444e --- /dev/null +++ b/.github/workflows/ci-mcp-server.yml @@ -0,0 +1,50 @@ +name: GoodGo MCP Server CI + +on: + push: + paths: + - 'services/goodgo-mcp-server/**' + pull_request: + paths: + - 'services/goodgo-mcp-server/**' + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 8 + + - name: Install dependencies + working-directory: services/goodgo-mcp-server + run: pnpm install + + - name: Build + working-directory: services/goodgo-mcp-server + run: pnpm build + + - name: Run tests with coverage + working-directory: services/goodgo-mcp-server + run: pnpm test:coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: services/goodgo-mcp-server/coverage/cobertura-coverage.xml + flags: goodgo_mcp_server + fail_ci_if_error: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/ci-packages.yml b/.github/workflows/ci-packages.yml new file mode 100644 index 00000000..b4880771 --- /dev/null +++ b/.github/workflows/ci-packages.yml @@ -0,0 +1,134 @@ +name: Shared Packages CI + +on: + push: + paths: + - 'packages/**' + - 'tests/contract/**' + pull_request: + paths: + - 'packages/**' + - 'tests/contract/**' + +permissions: + contents: read + +jobs: + # --------------------------------------------------------------------------- + # EN: Unit tests for all shared TypeScript packages. + # VI: Unit tests cho tất cả shared TypeScript packages. + # --------------------------------------------------------------------------- + package-unit-tests: + name: Shared Package Unit Tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 8 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run @goodgo/types tests + run: pnpm --filter @goodgo/types test:coverage + + - name: Run @goodgo/http-client tests + run: pnpm --filter @goodgo/http-client test:coverage + + - name: Run @goodgo/auth-sdk tests + run: pnpm --filter @goodgo/auth-sdk test:coverage + + - name: Run @goodgo/logger tests + run: pnpm --filter @goodgo/logger test:coverage + + - name: Run @goodgo/tracing tests + run: pnpm --filter @goodgo/tracing test:coverage + + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + files: packages/*/coverage/cobertura-coverage.xml + flags: shared_packages + fail_ci_if_error: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + # --------------------------------------------------------------------------- + # EN: Pact consumer contract tests for 5 service boundaries. + # VI: Pact consumer contract tests cho 5 ranh giới service. + # --------------------------------------------------------------------------- + contract-tests: + name: Pact Contract Tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 8 + + - name: Install contract test dependencies + run: pnpm --filter @goodgo/contract-tests install + + - name: Run Pact consumer tests + run: pnpm --filter @goodgo/contract-tests test + + - name: Upload Pact files as artifact + uses: actions/upload-artifact@v4 + with: + name: pact-contracts + path: tests/contract/pacts/ + if-no-files-found: warn + + # --------------------------------------------------------------------------- + # EN: Blazor WASM component unit tests (AuthStateService + PosDataService). + # VI: Blazor WASM component unit tests (AuthStateService + PosDataService). + # --------------------------------------------------------------------------- + blazor-component-tests: + name: Blazor Component Tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Restore component test project + run: | + dotnet restore apps/web-client-tpos-net/tests/WebClientTpos.ComponentTests/WebClientTpos.ComponentTests.csproj + + - name: Run component tests + run: | + dotnet test apps/web-client-tpos-net/tests/WebClientTpos.ComponentTests/WebClientTpos.ComponentTests.csproj \ + --configuration Release --no-restore --verbosity normal \ + --collect:"XPlat Code Coverage" \ + --settings coverage.runsettings \ + --results-directory TestResults/component + + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + files: TestResults/component/**/coverage.cobertura.xml + flags: blazor_components + fail_ci_if_error: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index dfef4a5f..3d3d26ea 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -5,33 +5,106 @@ on: types: [opened, synchronize, reopened] jobs: - check: + # EN: Node.js frontend/packages checks + # VI: Kiem tra frontend/packages Node.js + node-check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - + - name: Setup PNPM uses: pnpm/action-setup@v2 with: version: 8 - + - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '25' cache: 'pnpm' - + - name: Install dependencies run: pnpm install --frozen-lockfile - + - name: Build shared packages run: pnpm --filter './packages/*' build - + - name: Lint all run: pnpm lint - + - name: Type check all run: pnpm typecheck - + - name: Build all run: pnpm build + + # EN: .NET microservices build check (matrix across all services) + # VI: Kiem tra build .NET microservices (matrix qua tat ca services) + dotnet-check: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + service: + - name: iam-service-net + project: IamService.API/IamService.API.csproj + - name: merchant-service-net + project: MerchantService.API/MerchantService.API.csproj + - name: order-service-net + project: OrderService.API/OrderService.API.csproj + - name: catalog-service-net + project: CatalogService.API/CatalogService.API.csproj + - name: inventory-service-net + project: InventoryService.API/InventoryService.API.csproj + - name: wallet-service-net + project: WalletService.API/WalletService.API.csproj + - name: fnb-engine-net + project: FnbEngine.API/FnbEngine.API.csproj + - name: booking-service-net + project: BookingService.API/BookingService.API.csproj + - name: membership-service-net + project: MembershipService.API/MembershipService.API.csproj + - name: chat-service-net + project: ChatService.API/ChatService.API.csproj + - name: social-service-net + project: SocialService.API/SocialService.API.csproj + - name: storage-service-net + project: StorageService.API/StorageService.API.csproj + - name: mining-service-net + project: MiningService.API/MiningService.API.csproj + - name: mission-service-net + project: MissionService.API/MissionService.API.csproj + - name: promotion-service-net + project: PromotionService.API/PromotionService.API.csproj + - name: ads-manager-service-net + project: AdsManagerService.API/AdsManagerService.API.csproj + - name: ads-serving-service-net + project: AdsServingService.API/AdsServingService.API.csproj + - name: ads-billing-service-net + project: AdsBillingService.API/AdsBillingService.API.csproj + - name: ads-tracking-service-net + project: AdsTrackingService.API/AdsTrackingService.API.csproj + - name: ads-analytics-service-net + project: AdsAnalyticsService.API/AdsAnalyticsService.API.csproj + - name: mkt-facebook-service-net + project: MktFacebookService.API/MktFacebookService.API.csproj + - name: mkt-whatsapp-service-net + project: MktWhatsappService.API/MktWhatsappService.API.csproj + - name: mkt-x-service-net + project: MktXService.API/MktXService.API.csproj + - name: mkt-zalo-service-net + project: MktZaloService.API/MktZaloService.API.csproj + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Restore ${{ matrix.service.name }} + run: dotnet restore services/${{ matrix.service.name }}/src/${{ matrix.service.project }} + + - name: Build ${{ matrix.service.name }} + run: dotnet build services/${{ matrix.service.name }}/src/${{ matrix.service.project }} --configuration Release --no-restore diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopFinance.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopFinance.razor index 43d2894d..db56721c 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopFinance.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopFinance.razor @@ -135,7 +135,7 @@ else protected override async Task OnInitializedAsync() { - _orders = await DataService.GetOrdersAsync(ShopId == Guid.Empty ? null : ShopId); + _orders = await DataService.GetOrdersAsync(ShopId == Guid.Empty ? null : ShopId, "all"); _wallets = await DataService.GetWalletsAsync(); _walletTxns = await DataService.GetWalletTransactionsAsync(); } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/tokens.generated.css b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/tokens.generated.css new file mode 100644 index 00000000..41abc0c3 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/tokens.generated.css @@ -0,0 +1,70 @@ +/** + * Do not edit directly, this file was auto-generated. + */ + +:root { + --border-radius-base: 6px; /** Default border radius */ + --border-radius-lg: 10px; /** Large border radius */ + --border-radius-xl: 14px; /** XL border radius */ + --border-radius-2xl: 20px; /** 2XL border radius */ + --color-primitive-neutral-0: #ffffff; /** Pure white */ + --color-primitive-neutral-50: #fafafa; /** Near white */ + --color-primitive-neutral-100: #ADADB0; /** Light gray */ + --color-primitive-neutral-200: #8B8B90; /** Medium gray */ + --color-primitive-neutral-300: #6B6B70; /** Muted gray */ + --color-primitive-neutral-400: #3A3A3E; /** Dark gray */ + --color-primitive-neutral-500: #2A2A2E; /** Darker gray */ + --color-primitive-neutral-600: #1F1F23; /** Surface border */ + --color-primitive-neutral-700: #1A1A1D; /** Elevated surface */ + --color-primitive-neutral-800: #111113; /** Base surface */ + --color-primitive-neutral-900: #0A0A0B; /** Page background */ + --color-primitive-neutral-950: #050506; /** Near black */ + --color-primitive-accent-400: #FF8A4C; /** Orange light */ + --color-primitive-accent-500: #FF5C00; /** Orange primary */ + --color-primitive-accent-600: #E05200; /** Orange dark */ + --color-primitive-success-500: #22C55E; /** Success green */ + --color-primitive-white: #ffffff; + --color-primitive-black: #000000; + --color-primitive-overlay: rgba(0, 0, 0, 0.6); + --color-semantic-bg-page: #0A0A0B; /** Page background */ + --color-semantic-bg-surface: #111113; /** Card / panel surface */ + --color-semantic-bg-elevated: #1A1A1D; /** Elevated elements */ + --color-semantic-bg-interactive: #2A2A2E; /** Interactive backgrounds */ + --color-semantic-bg-surface-hover: #1F1F23; /** Hover state */ + --color-semantic-bg-overlay: rgba(10, 10, 11, 0.9); /** Modal overlay */ + --color-semantic-text-primary: #ffffff; /** Primary text */ + --color-semantic-text-secondary: #ADADB0; /** Secondary text */ + --color-semantic-text-tertiary: #8B8B90; /** Tertiary / helper text */ + --color-semantic-text-disabled: #6B6B70; /** Disabled text */ + --color-semantic-text-muted: rgba(255, 255, 255, 0.8); /** Muted white text */ + --color-semantic-text-inverse: #0A0A0B; /** Inverse (dark on light) */ + --color-semantic-accent-primary: #FF5C00; /** Brand orange */ + --color-semantic-accent-light: #FF8A4C; /** Light orange */ + --color-semantic-accent-glow: rgba(255, 92, 0, 0.15); /** Subtle glow */ + --color-semantic-accent-glow-strong: rgba(255, 92, 0, 0.3); /** Strong glow */ + --color-semantic-border-subtle: #1F1F23; /** Subtle divider */ + --color-semantic-border-default: #2A2A2E; /** Default border */ + --color-semantic-border-strong: #3A3A3E; /** Strong border */ + --color-semantic-action-primary-bg: #FF5C00; /** CTA button bg */ + --color-semantic-action-primary-bg-hover: #E05200; /** CTA button hover */ + --color-semantic-action-primary-text: #ffffff; /** CTA button text */ + --color-semantic-action-secondary-bg: transparent; /** Ghost button bg */ + --color-semantic-action-secondary-bg-hover: rgba(255, 255, 255, 0.05); /** Ghost button hover */ + --color-semantic-action-secondary-text: #ffffff; /** Ghost button text */ + --color-semantic-action-secondary-border: #2A2A2E; /** Ghost button border */ + --color-semantic-status-success: #22C55E; /** Success state */ + --spacing-1: 0.25rem; /** 4px */ + --spacing-2: 0.5rem; /** 8px */ + --spacing-3: 0.75rem; /** 12px */ + --spacing-4: 1rem; /** 16px */ + --spacing-5: 1.25rem; /** 20px */ + --spacing-6: 1.5rem; /** 24px */ + --spacing-8: 2rem; /** 32px */ + --spacing-10: 2.5rem; /** 40px */ + --spacing-12: 3rem; /** 48px */ + --spacing-16: 4rem; /** 64px */ + --spacing-20: 5rem; /** 80px */ + --spacing-24: 6rem; /** 96px */ + --font-family-heading: 'Roboto', system-ui, sans-serif; /** Heading / display font */ + --font-family-body: 'Roboto', system-ui, sans-serif; /** Body text font */ +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/FinancialController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/FinancialController.cs index 0a423689..b799375b 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/FinancialController.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/FinancialController.cs @@ -22,15 +22,26 @@ public class FinancialController : ControllerBase } /// - /// EN: Extract userId from JWT Bearer token in the Authorization header. - /// VI: Trích xuất userId từ JWT Bearer token trong header Authorization. + /// EN: Extract userId from JWT — reads httpOnly session cookie first, then Authorization header (fallback). + /// VI: Trích xuất userId từ JWT — đọc httpOnly session cookie trước, rồi Authorization header (fallback). /// private Guid? GetUserIdFromToken() { - var authHeader = Request.Headers["Authorization"].FirstOrDefault(); - if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ")) return null; - - var token = authHeader["Bearer ".Length..]; + // EN: (1) Prefer httpOnly session cookie (BFF auth pattern). + // VI: (1) Ưu tiên httpOnly session cookie (BFF auth pattern). + string? token = null; + if (Request.Cookies.TryGetValue("bff_session", out var sessionToken) && !string.IsNullOrEmpty(sessionToken)) + { + token = sessionToken; + } + else + { + // EN: (2) Legacy fallback — Authorization header. + // VI: (2) Fallback cũ — Authorization header. + var authHeader = Request.Headers["Authorization"].FirstOrDefault(); + if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ")) return null; + token = authHeader["Bearer ".Length..]; + } var parts = token.Split('.'); if (parts.Length != 3) return null; @@ -73,9 +84,13 @@ public class FinancialController : ControllerBase if (!response.IsSuccessStatusCode) { - // EN: If 404, return empty array (user has no wallet yet). - // VI: Nếu 404, trả array rỗng (user chưa có ví). - if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + // EN: If 404 or auth failure (401/403), return empty array (user has no wallet yet, + // or WalletService token validation failed in local dev). + // VI: Nếu 404 hoặc lỗi auth (401/403), trả array rỗng (user chưa có ví, + // hoặc WalletService không validate được token khi chạy local dev). + if (response.StatusCode is System.Net.HttpStatusCode.NotFound + or System.Net.HttpStatusCode.Unauthorized + or System.Net.HttpStatusCode.Forbidden) return new ContentResult { StatusCode = 200, Content = "[]", ContentType = "application/json" }; return new ContentResult diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/OrderController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/OrderController.cs index b3e13ef3..1c13e053 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/OrderController.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/OrderController.cs @@ -35,17 +35,22 @@ public class OrderController : ControllerBase var qs = new List(); if (shopId.HasValue) qs.Add($"shopId={shopId}"); - // EN: Convert filter shorthand to fromDate/toDate for OrderService - // VI: Chuyển đổi filter shorthand thành fromDate/toDate cho OrderService - var now = DateTime.UtcNow; - var (fromDate, toDate) = filter?.ToLower() switch + // EN: Convert filter shorthand to fromDate/toDate for OrderService. + // "all" skips date filters entirely to return all historical orders. + // VI: Chuyển đổi filter shorthand thành fromDate/toDate cho OrderService. + // "all" bỏ qua date filter để trả về toàn bộ đơn hàng. + if (filter?.ToLower() != "all") { - "week" => (now.Date.AddDays(-(int)now.DayOfWeek), now), - "month" => (new DateTime(now.Year, now.Month, 1, 0, 0, 0, DateTimeKind.Utc), now), - _ => (now.Date, now) // "today" or default - }; - qs.Add($"fromDate={fromDate:O}"); - qs.Add($"toDate={toDate:O}"); + var now = DateTime.UtcNow; + var (fromDate, toDate) = filter?.ToLower() switch + { + "week" => (now.Date.AddDays(-(int)now.DayOfWeek), now), + "month" => (new DateTime(now.Year, now.Month, 1, 0, 0, 0, DateTimeKind.Utc), now), + _ => (now.Date, now) // "today" or default + }; + qs.Add($"fromDate={fromDate:O}"); + qs.Add($"toDate={toDate:O}"); + } var query = "?" + string.Join("&", qs); return _order.GetAsync($"/api/v1/orders{query}").ProxyAsync(); diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs index f7b88e48..6bfcd0cc 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs @@ -71,15 +71,26 @@ builder.Services.AddReverseProxy() // VI: Thêm hỗ trợ OpenAPI/Swagger builder.Services.AddOpenApi(); -// EN: Add CORS for Blazor WebAssembly client -// VI: Thêm CORS cho Blazor WebAssembly client +// EN: Add CORS for Blazor WebAssembly client — whitelist specific origins per environment. +// AllowAnyOrigin() is intentionally removed to prevent unauthorized cross-origin requests. +// VI: Thêm CORS cho Blazor WebAssembly client — whitelist origins theo môi trường. +// AllowAnyOrigin() đã bị xóa để ngăn các request cross-origin không được phép. +var allowedOrigins = builder.Configuration + .GetSection("Cors:AllowedOrigins") + .Get() + ?? (builder.Environment.IsDevelopment() + ? ["http://localhost:3000", "http://localhost:5000", "http://localhost:5173", "http://localhost:7001"] + : ["https://goodgo.vn", "https://admin.goodgo.vn", "https://pos.goodgo.vn", + "https://staging.goodgo.vn", "https://admin.staging.goodgo.vn", "https://pos.staging.goodgo.vn"]); + builder.Services.AddCors(options => { options.AddPolicy("BlazorClient", policy => { - policy.AllowAnyOrigin() + policy.WithOrigins(allowedOrigins) .AllowAnyMethod() - .AllowAnyHeader(); + .AllowAnyHeader() + .AllowCredentials(); }); }); diff --git a/apps/web-client-tpos-net/tests/WebClientTpos.ComponentTests/WebClientTpos.ComponentTests.csproj b/apps/web-client-tpos-net/tests/WebClientTpos.ComponentTests/WebClientTpos.ComponentTests.csproj new file mode 100644 index 00000000..b53fa3db --- /dev/null +++ b/apps/web-client-tpos-net/tests/WebClientTpos.ComponentTests/WebClientTpos.ComponentTests.csproj @@ -0,0 +1,45 @@ + + + + WebClientTpos.ComponentTests + WebClientTpos.ComponentTests + net10.0 + false + true + enable + enable + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/coverage.runsettings b/coverage.runsettings new file mode 100644 index 00000000..fc94e184 --- /dev/null +++ b/coverage.runsettings @@ -0,0 +1,33 @@ + + + + + + + + + cobertura + + + [*.UnitTests]*,[*.FunctionalTests]*,[*.Tests]* + + + [*.Domain]*,[*.API]*,[*.Infrastructure]* + + + 80 + line + total + + + + + diff --git a/deployments/local/.env b/deployments/local/.env index 70aebec3..a864e344 100644 --- a/deployments/local/.env +++ b/deployments/local/.env @@ -71,3 +71,7 @@ ADS_ANALYTICS_DATABASE_URL=Host=postgres;Port=5432;Database=ads_analytics_servic ADS_SERVING_DATABASE_URL=Host=postgres;Port=5432;Database=ads_serving_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable ADS_BILLING_DATABASE_URL=Host=postgres;Port=5432;Database=ads_billing_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable ADS_TRACKING_DATABASE_URL=Host=postgres;Port=5432;Database=ads_tracking_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable +MKT_FACEBOOK_DATABASE_URL=Host=postgres;Port=5432;Database=mkt_facebook_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable +MKT_WHATSAPP_DATABASE_URL=Host=postgres;Port=5432;Database=mkt_whatsapp_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable +MKT_X_DATABASE_URL=Host=postgres;Port=5432;Database=mkt_x_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable +MKT_ZALO_DATABASE_URL=Host=postgres;Port=5432;Database=mkt_zalo_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable diff --git a/deployments/local/.env.local b/deployments/local/.env.local index 70aebec3..a864e344 100644 --- a/deployments/local/.env.local +++ b/deployments/local/.env.local @@ -71,3 +71,7 @@ ADS_ANALYTICS_DATABASE_URL=Host=postgres;Port=5432;Database=ads_analytics_servic ADS_SERVING_DATABASE_URL=Host=postgres;Port=5432;Database=ads_serving_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable ADS_BILLING_DATABASE_URL=Host=postgres;Port=5432;Database=ads_billing_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable ADS_TRACKING_DATABASE_URL=Host=postgres;Port=5432;Database=ads_tracking_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable +MKT_FACEBOOK_DATABASE_URL=Host=postgres;Port=5432;Database=mkt_facebook_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable +MKT_WHATSAPP_DATABASE_URL=Host=postgres;Port=5432;Database=mkt_whatsapp_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable +MKT_X_DATABASE_URL=Host=postgres;Port=5432;Database=mkt_x_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable +MKT_ZALO_DATABASE_URL=Host=postgres;Port=5432;Database=mkt_zalo_service;Username=goodgo;Password=goodgo-local-2024;SSL Mode=Disable diff --git a/deployments/local/docker-compose.yml b/deployments/local/docker-compose.yml index 4366cbb1..896cbde2 100644 --- a/deployments/local/docker-compose.yml +++ b/deployments/local/docker-compose.yml @@ -762,6 +762,9 @@ services: - Jwt__Authority=http://iam-service-net:8080 - Jwt__Audience=goodgo-api - Jwt__RequireHttpsMetadata=false + # EN: Redis for caching and SignalR backplane + # VI: Redis cho caching và SignalR backplane + - Redis__ConnectionString=redis:6379,password=goodgo-redis-local ports: - "5017:8080" depends_on: @@ -769,6 +772,8 @@ services: condition: service_healthy catalog-service-net: condition: service_healthy + redis: + condition: service_healthy traefik: condition: service_started networks: @@ -1536,7 +1541,7 @@ services: # Promtail - Log Collector (ships container logs to Loki) promtail: - image: grafana/promtail:2.9.4 + image: grafana/promtail:3.3.0 container_name: promtail-local volumes: - /var/lib/docker/containers:/var/lib/docker/containers:ro diff --git a/deployments/local/env.local.example b/deployments/local/env.local.example index 75fd76a0..24c390a3 100644 --- a/deployments/local/env.local.example +++ b/deployments/local/env.local.example @@ -94,3 +94,7 @@ ADS_ANALYTICS_DATABASE_URL=Host=your-neon-host;Port=5432;Database=ads_analytics_ ADS_SERVING_DATABASE_URL=Host=your-neon-host;Port=5432;Database=ads_serving_service;Username=your-user;Password=your-password;SSL Mode=Require ADS_BILLING_DATABASE_URL=Host=your-neon-host;Port=5432;Database=ads_billing_service;Username=your-user;Password=your-password;SSL Mode=Require ADS_TRACKING_DATABASE_URL=Host=your-neon-host;Port=5432;Database=ads_tracking_service;Username=your-user;Password=your-password;SSL Mode=Require +MKT_FACEBOOK_DATABASE_URL=Host=your-neon-host;Port=5432;Database=mkt_facebook_service;Username=your-user;Password=your-password;SSL Mode=Require +MKT_WHATSAPP_DATABASE_URL=Host=your-neon-host;Port=5432;Database=mkt_whatsapp_service;Username=your-user;Password=your-password;SSL Mode=Require +MKT_X_DATABASE_URL=Host=your-neon-host;Port=5432;Database=mkt_x_service;Username=your-user;Password=your-password;SSL Mode=Require +MKT_ZALO_DATABASE_URL=Host=your-neon-host;Port=5432;Database=mkt_zalo_service;Username=your-user;Password=your-password;SSL Mode=Require diff --git a/deployments/staging/kubernetes/booking-service.yaml b/deployments/staging/kubernetes/booking-service.yaml new file mode 100644 index 00000000..525a72fe --- /dev/null +++ b/deployments/staging/kubernetes/booking-service.yaml @@ -0,0 +1,125 @@ +# EN: Booking Service - Booking & Reservation Management (Spa, Beauty, Karaoke scheduling) +# VI: Booking Service - Quan ly Dat lich & Dat cho (Spa, Beauty, Karaoke) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: booking-service + namespace: staging + labels: + app: booking-service + environment: staging + platform: goodgo + tier: backend +spec: + replicas: 2 + selector: + matchLabels: + app: booking-service + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + template: + metadata: + labels: + app: booking-service + environment: staging + spec: + containers: + - name: booking-service + image: goodgo/booking-service-net:staging + ports: + - containerPort: 8080 + protocol: TCP + envFrom: + - configMapRef: + name: goodgo-config + - secretRef: + name: goodgo-secrets + env: + # EN: Override service-specific database URL + # VI: Override URL database rieng cho service + - name: ConnectionStrings__DefaultConnection + valueFrom: + secretKeyRef: + name: goodgo-secrets + key: BOOKING_DATABASE_URL + - name: BookingService__ServiceName + value: "booking-service" + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: /health/live + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health/ready + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + startupProbe: + httpGet: + path: /health/live + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 12 +--- +apiVersion: v1 +kind: Service +metadata: + name: booking-service + namespace: staging + labels: + app: booking-service + environment: staging +spec: + selector: + app: booking-service + ports: + - name: http + protocol: TCP + port: 8080 + targetPort: 8080 + type: ClusterIP +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: booking-service-hpa + namespace: staging + labels: + app: booking-service +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: booking-service + minReplicas: 2 + maxReplicas: 4 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 75 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 diff --git a/deployments/staging/kubernetes/configmap.yaml b/deployments/staging/kubernetes/configmap.yaml index 6b288f15..1687144f 100644 --- a/deployments/staging/kubernetes/configmap.yaml +++ b/deployments/staging/kubernetes/configmap.yaml @@ -16,9 +16,15 @@ data: # EN: JWT Configuration (shared across all services) # VI: Cau hinh JWT (dung chung cho tat ca services) - Jwt__Authority: "http://iam-service:8080" + # EN: Use external HTTPS URL so RequireHttpsMetadata=true is valid. + # Services resolve OIDC discovery over public TLS endpoint via Traefik. + # VI: Dùng HTTPS external URL để RequireHttpsMetadata=true hoạt động đúng. + # Các service lấy OIDC discovery qua endpoint TLS công khai qua Traefik. + Jwt__Authority: "https://api.staging.goodgo.vn" Jwt__Audience: "goodgo-api" - Jwt__RequireHttpsMetadata: "false" + # EN: MUST be true in staging/prod — never allow HTTP metadata endpoints outside dev + # VI: PHẢI là true trong staging/prod — không cho phép HTTP metadata endpoint ngoài môi trường dev + Jwt__RequireHttpsMetadata: "true" # EN: Service Discovery URLs (K8s DNS: {service-name}.staging.svc.cluster.local) # VI: URL tim kiem service (K8s DNS: {service-name}.staging.svc.cluster.local) diff --git a/docs/audit/FIX-PLAN.md b/docs/audit/FIX-PLAN.md new file mode 100644 index 00000000..128092fc --- /dev/null +++ b/docs/audit/FIX-PLAN.md @@ -0,0 +1,206 @@ +# GoodGo POS System — Audit Fix Plan + +**Date:** 2026-03-23 +**Owner:** CEO Agent +**Source:** 14 agent audit reports (94 total findings) +**Status:** Active + +--- + +## Summary + +| Category | Critical | High | Medium | Low | Total | +|---|:---:|:---:|:---:|:---:|:---:| +| Security | 5 | 10 | 5 | 1 | **21** | +| Backend | 4 | 5 | 3 | 0 | **12** | +| Frontend | 5 | 9 | 5 | 4 | **23** | +| DevOps | 4 | 12 | 5 | 0 | **21** | +| Testing | 4 | 7 | 3 | 1 | **15** | +| Documentation | 0 | 2 | 0 | 0 | **2** | +| **Total** | **22** | **45** | **21** | **6** | **94** | + +--- + +## Wave 1 — P0 Blockers (Target: 24-48h) + +### Security Blockers (assign: Security Engineer) + +| ID | Finding | File | Fix | +|---|---|---|---| +| SEC-C-01 | DB credentials hardcoded in git (19 services) | All `appsettings.json` | Replace with env vars, add to `.gitignore` | +| SEC-C-02 | JWT token in MCP server `.env` committed | `services/goodgo-mcp-server/.env` | Revoke, remove from git, purge history | +| SEC-C-03 | `AddDeveloperSigningCredential()` in all envs | `iam-service-net/.../DependencyInjection.cs:142` | Wrap in `if (env.IsDevelopment())` | +| SEC-C-04 | Debug endpoints `[AllowAnonymous]` — privilege escalation | `merchant-service-net/.../StaffController.cs:249-390` | Delete or restrict to dev + SuperAdmin | +| SEC-C-05 | SQL injection via string interpolation | `merchant-service-net/.../StaffController.cs:307,367` | Use parameterized queries | + +### DevOps Blockers (assign: DevOps Engineer) + +| ID | Finding | File | Fix | +|---|---|---|---| +| DEVOPS-C-01 | K8s `:latest` image tag in production | All `production/kubernetes/*.yaml` | Use `IMAGE_TAG` placeholder + SHA | +| DEVOPS-C-02 | Alertmanager not configured — alerts silent | `prometheus/prometheus.yml:29` | Configure Alertmanager + receivers | +| DEVOPS-C-03 | CI pushes `:latest` to Docker Hub | `.github/workflows/docker-build.yml:99-103` | Remove `:latest`, use SHA only | +| DEVOPS-C-04 | 4 mkt-* services port 5000 conflict | `docker-compose.yml` | Assign ports 5021-5024 | + +--- + +## Wave 2 — P1 Urgent (Target: 1 week) + +### Security High (assign: Security Engineer) + +| ID | Finding | Fix | +|---|---|---| +| SEC-W-02 | No Content-Security-Policy header | Add CSP to Traefik `middlewares.yml` | +| SEC-W-03 | CORS `allowCredentials: true` with dev origins | Separate per-env CORS config | +| SEC-W-04 | `sslRedirect: false` in shared config | Set `true` in staging/prod | +| SEC-W-05 | `Jwt__RequireHttpsMetadata=false` in docker-compose | Verify K8s ConfigMaps don't have this | +| SEC-W-14 | BFF CORS wildcard `AllowAnyOrigin()` | Whitelist specific origins | +| SEC-W-15 | JWT validation skipped in dev (4 services) | Always validate signatures | + +### Backend Critical (assign: Senior Backend Engineer) + +| ID | Finding | Fix | +|---|---|---| +| BACK-C-01 | `AllowAnyOrigin()` on all 26 services | Restrict origins in production | +| BACK-C-02 | Idempotency missing in 23/26 services | Implement `IRequestManager` (wallet, booking first) | +| BACK-C-03 | Error response format inconsistent | Standardize to `{ success, error: { code, message } }` | +| BACK-C-04 | ProblemDetails mapping incomplete in template | Update template with full exception mapping | +| BACK-W-02 | TenantMiddleware SQL string interpolation | Parameterized queries in 5 services | + +### Frontend Critical (assign: Senior Frontend Engineer) + +| ID | Finding | Fix | +|---|---|---| +| SEC-W-11 | Client secret in WASM (extractable) | Move to BFF server-side | +| SEC-W-12 | Password grant deprecated | Migrate to PKCE flow | +| SEC-W-01 | JWT in localStorage (XSS risk) | Migrate to httpOnly cookies via BFF | +| FRONT-C-04 | No route guards for auth pages | Add `[Authorize]` + `AuthorizeView` | +| FRONT-C-05 | shopId not validated against permissions | Backend verification call | +| FRONT-W-01 | Token refresh not implemented | Add background refresh timer | +| FRONT-W-02 | Global HttpClient header mutation (race) | Per-request headers via `DelegatingHandler` | +| SEC-W-13 | No CDN SRI for Lucide icons | Add SRI hash, pin version | + +### DevOps High (assign: DevOps Engineer) + +| ID | Finding | Fix | +|---|---|---| +| DEVOPS-W-02 | 15+ services missing CI/CD pipelines | Generate CI workflows from template | +| DEVOPS-W-03 | `pr-checks.yml` no .NET build/test | Add matrix build for .NET | +| DEVOPS-W-10 | `RequireHttpsMetadata=false` in staging K8s | Set `true` in staging/prod | +| DEVOPS-W-11 | booking-service missing K8s manifest | Create staging manifest | +| DEVOPS-W-12 | 13 Traefik routes missing | Add routes for all missing services | + +### Testing Critical (assign: QA Engineer) + +| ID | Finding | Fix | +|---|---|---| +| TEST-C-01 | Only 1/26 services has CI test pipeline | Generate CI for 25 services | +| TEST-C-02 | MCP server zero tests | Add Vitest test suite | +| TEST-C-03 | No coverage thresholds enforced | Add `.runsettings` with 80% threshold | + +--- + +## Wave 3 — P2 High (Target: 2 weeks) + +### Architecture (assign: Architect) + +| ID | Finding | Fix | +|---|---|---| +| FRONT-I-01 | No shared UI component package | Extract shared Razor Class Library | +| FRONT-I-02 | ARIA/accessibility gaps | Add ARIA attributes to all components | +| FRONT-I-03 | No design-to-code token sync | Style Dictionary pipeline | +| FRONT-I-04 | `eval()` in OtpInput | Create JS module for focus | + +### Backend Architecture (assign: Senior Backend Engineer) + +| ID | Finding | Fix | +|---|---|---| +| BACK-I-01 | No OpenAPI specs in repo | Add `dotnet swagger tofile` to CI | +| BACK-I-02 | Missing Prometheus `/metrics` | Add OpenTelemetry + Prometheus exporter | +| BACK-W-01 | HttpContextAccessor in handlers | Inject contextual data from Controller | +| BACK-W-03 | Dapper no `commandTimeout` | Set explicit timeout on all queries | + +### Frontend Improvements (assign: Senior Frontend Engineer) + +| ID | Finding | Fix | +|---|---|---| +| FRONT-W-03 | ~20% POS pages incomplete backend integration | Implement 21 missing API integrations | +| FRONT-W-04 | Fragile multi-format deserialization | Standardize API response envelope | +| FRONT-W-06 | MudBlazor providers duplicated | Move to `App.razor` once | +| FRONT-W-07 | localStorage logic duplicated 5 files | Extract `LocalStorageService` | + +### DevOps Improvements (assign: DevOps Engineer) + +| ID | Finding | Fix | +|---|---|---| +| DEVOPS-W-01 | redis-exporter missing from compose | Add or remove scrape job | +| DEVOPS-W-04 | Redis single instance (SPOF) | Redis Sentinel or Cluster | +| DEVOPS-W-05 | No K8s NetworkPolicy | Add default-deny + whitelist | +| DEVOPS-M-01 | No image vulnerability scanning | Add Trivy to CI | + +### Testing Improvements (assign: QA Engineer) + +| ID | Finding | Fix | +|---|---|---| +| TEST-C-04 | No contract testing | Implement Pact.io for top 5 boundaries | +| TEST-W-01 | Shared packages zero tests | Add unit tests for 6 packages | +| TEST-W-04 | No performance/load testing | Add k6 load tests | +| TEST-W-05 | No frontend component tests | Add unit tests for key components | + +### Documentation (assign: Technical Writer) + +| ID | Finding | Fix | +|---|---|---| +| DOC-W-01 | Test credentials in ROADMAP.md | Remove credentials | +| DOC-W-02 | No ADR for Marketing dual-theme | Create ADR | + +--- + +## Wave 4 — P3 Medium (Target: 1 month) + +Lower priority items — tracked but deferred: +- FRONT-W-05: Lucide re-init on every render +- FRONT-W-08: Incomplete vi-VN translations +- FRONT-W-09: No IFormatProvider in JsonStringLocalizer +- FRONT-W-10: Event handler leak (no IAsyncDisposable) +- FRONT-W-11: Hardcoded Vietnamese in AuthInput +- FRONT-I-05 through FRONT-I-09: Component library expansion +- BACK-I-03: Outbox pattern (5d effort) +- BACK-I-04: Saga pattern (5d effort) +- DEVOPS-I-01 through DEVOPS-I-04: GitOps, PDB, Secrets Manager +- SEC-W-06 through SEC-W-10: Medium security items + +--- + +## Agent Assignment Matrix + +| Agent | Wave 1 | Wave 2 | Wave 3 | Total Items | +|---|:---:|:---:|:---:|:---:| +| **Security Engineer** | 5 | 6 | 0 | **11** | +| **Senior Backend Engineer** | 0 | 5 | 4 | **9** | +| **Senior Frontend Engineer** | 0 | 8 | 4 | **12** | +| **DevOps Engineer** | 4 | 5 | 4 | **13** | +| **QA Engineer** | 0 | 3 | 4 | **7** | +| **Architect** | 0 | 0 | 4 | **4** | +| **Technical Writer** | 0 | 0 | 2 | **2** | +| **CTO** | — | — | — | Review all | + +--- + +## QA Verification Plan + +After each wave completes: +1. Docker Compose rebuild: `docker-compose down && docker-compose up --build -d` +2. Health check all services: `curl http://localhost:{port}/health/live` +3. Run E2E tests: verify 38/41+ pass rate maintained +4. Security scan: verify hardcoded credentials removed +5. K8s dry-run: `kubectl apply --dry-run=server -f deployments/staging/kubernetes/` + +--- + +## Success Criteria + +- **Wave 1**: All 9 P0 blockers resolved, zero hardcoded credentials in git +- **Wave 2**: All 22 P1 items resolved, CI pipelines for all services +- **Wave 3**: Architecture improvements in place, test coverage >50% +- **Overall**: Project health score from 6.5/10 to 8.5/10 diff --git a/docs/audit/REPORT.md b/docs/audit/REPORT.md new file mode 100644 index 00000000..ef45639e --- /dev/null +++ b/docs/audit/REPORT.md @@ -0,0 +1,641 @@ +# Audit Report — POS System - AI + +**Date**: 2026-03-20 +**Compiled from**: 15 specialist audit reports (CEO, CTO, Architect, API Architect, Backend, Frontend, Database Architect, DevOps, Security, Product Manager, QA, UX/UI Designer, Founding Engineer, Research Analyst, Technical Writer) +**Platform**: GoodGo POS — 26 .NET 10 microservices, 5 frontend apps, MCP AI server +**Branch**: master (d0211e5) + +--- + +## Executive Summary + +GoodGo POS is a multi-vertical point-of-sale platform in **late-MVP / pre-production** phase. The architecture is exemplary — Clean Architecture + CQRS enforced across all 26 services, bilingual documentation (EN/VI), and a comprehensive observability stack. The core POS flow works end-to-end with 93% E2E test pass rate (38/41). The MCP AI server (12 tools) is a unique market differentiator no competitor offers. + +However, the platform has **severe security vulnerabilities** that must be addressed before any deployment: production database credentials are committed to git (CVSS 9.8), debug endpoints allow unauthenticated privilege escalation, JWT tokens are stored in localStorage, and the IdentityServer uses developer signing credentials in all environments. Test coverage sits at ~15% against a 70% target. Five services remain incomplete, and the entire marketing/CRM suite is demo-only with hardcoded fake data. + +**Overall Project Health Score: 5.5 / 10** + +| Category | Score | Key Factor | +|----------|:-----:|------------| +| Architecture & Patterns | 9/10 | Clean Architecture + CQRS exemplary across all services | +| Code Quality | 8/10 | Strong DDD, bilingual docs, consistent patterns | +| Security | 2/10 | Production credentials in git, debug endpoints, no CSP | +| Test Coverage | 3/10 | ~15% coverage, 1/26 services has CI tests | +| Infrastructure & DevOps | 6/10 | Good stack, K8s gaps, alerting non-functional | +| Frontend & UX | 5/10 | Solid foundation, critical a11y failures, 2,316 inline styles | +| Documentation | 7/10 | 102+ docs, but 96% OpenAPI specs missing | +| Production Readiness | 4/10 | POS core ready, platform incomplete | +| Mobile Readiness | 2/10 | iOS partial, Android template only | +| Product Completeness | 5/10 | Core POS works, marketing/analytics are stubs | + +--- + +## Critical Issues + +Issues that are **showstoppers** and must be fixed before any staging or production deployment. + +### SEC-1: Production Database Credentials Committed to Git (CVSS 9.8) +**Source**: Security Audit (CRIT-01) +**Severity**: BLOCKER +**Affected**: All 19 .NET microservices `appsettings.json` + +The Neon PostgreSQL production password (`npg_Ssfy6HKO0cXI`), Redis production password (`Velik@2026` with public IP `167.114.174.113`), SMTP credentials, and JWT signing secret are all hardcoded in `appsettings.json` files tracked by git. Anyone with repository read access can authenticate directly to the production database and Redis. All customer data, merchant data, orders, wallets, and PII are at risk. + +**Files**: `services/*/src/*/appsettings.json` (all 19 services) +**Fix**: Rotate ALL credentials immediately (within 24 hours). Replace with environment variable placeholders. Purge from git history with `git filter-repo` or BFG Repo Cleaner. Adopt Kubernetes External Secrets Operator or HashiCorp Vault. + +--- + +### SEC-2: Active JWT Bearer Token Committed in MCP Server .env +**Source**: Security Audit (CRIT-02) +**Severity**: BLOCKER +**File**: `services/goodgo-mcp-server/.env:3` + +A live, signed JWT bearer token is committed to git. Any party with repository access can replay this token to authenticate as the associated service account. The MCP server has 12 operational tools that can read/write F&B data. + +**Fix**: Revoke token immediately. Remove `.env` from git tracking. Purge from history. + +--- + +### SEC-3: Debug Endpoints Allow Unauthenticated Privilege Escalation +**Source**: CTO (C1), Backend (C1), CEO (referenced) +**Severity**: BLOCKER +**File**: `services/merchant-service-net/src/MerchantService.API/Controllers/StaffController.cs:249-390` + +Five `[AllowAnonymous]` debug endpoints in production code: +- `POST /api/v1/staff/debug/seed` — creates arbitrary staff data +- `POST /api/v1/staff/debug/update-userid` — overwrites any staff's userId via reflection +- `POST /api/v1/staff/debug/update-merchant` — overwrites any merchantId + +Any attacker can escalate privileges or tamper with merchant data. + +**Fix**: Delete these endpoints entirely or wrap with `if (env.IsDevelopment())` + `[Authorize(Roles = "SuperAdmin")]`. + +--- + +### SEC-4: SQL Injection in StaffController +**Source**: CTO (C2), Backend (C2) +**Severity**: BLOCKER +**File**: `services/merchant-service-net/src/MerchantService.API/Controllers/StaffController.cs:307, 367` + +String interpolation used directly in SQL queries. While current `Guid` types self-sanitize, any future refactor changing types to `string` creates an immediate injection vulnerability. + +```csharp +cmd.CommandText = $"UPDATE merchants SET user_id = '{userId}' WHERE id = '{merchantId}'"; +``` + +**Fix**: Use parameterized queries (`cmd.Parameters.AddWithValue()`). + +--- + +### SEC-5: IdentityServer Using Developer Signing Credential in All Environments +**Source**: Security Audit (CRIT-03) +**Severity**: CRITICAL +**File**: `services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs:142` + +`AddDeveloperSigningCredential()` is called unconditionally — no environment check. The signing key is regenerated on every restart, invalidating all active sessions. No certificate-based signing exists for production. + +**Fix**: Use cert-based signing for non-development environments. Store RSA key in Vault or K8s TLS Secret. + +--- + +### SEC-6: JWT Stored in localStorage (XSS Risk) +**Source**: Frontend (CRIT-01), Security (WARN-01) +**Severity**: CRITICAL +**File**: `apps/web-client-tpos-net/src/WebClientTpos.Client/Services/AuthService.cs:147-148` + +JWT tokens stored in `localStorage` are accessible to any JavaScript on the same origin. A single XSS vulnerability allows full token theft and account impersonation. + +**Fix**: Migrate to `httpOnly` cookies via the BFF server. Add CSP headers. + +--- + +### SEC-7: Client Secret Hardcoded in Browser-Delivered WASM Code +**Source**: Frontend (CRIT-02) +**Severity**: CRITICAL +**File**: `apps/web-client-tpos-net/src/WebClientTpos.Client/Services/AuthService.cs:39-40` + +```csharp +private const string ClientId = "password-client"; +private const string ClientSecret = "password-client-secret"; +``` + +Blazor WASM compiles to WebAssembly served to browsers. Constants are extractable via developer tools. + +**Fix**: Move OAuth2 token exchange to the BFF. The BFF holds secrets server-side. + +--- + +### SEC-8: Deprecated Password Grant (OAuth2) Used +**Source**: Frontend (CRIT-03) +**Severity**: CRITICAL +**File**: `apps/web-client-tpos-net/src/WebClientTpos.Client/Services/AuthService.cs:136` + +Resource Owner Password Credentials grant is deprecated in OAuth 2.1. It cannot support MFA properly and exposes credentials to the client application. + +**Fix**: Migrate to Authorization Code Flow with PKCE. + +--- + +### SEC-9: JWT Signature Validation Disabled in Development (4 Services) +**Source**: Backend (C3), Founding Engineer (C4) +**Severity**: CRITICAL +**Files**: `merchant-service-net`, `ads-serving-service-net`, `ads-tracking-service-net`, `ads-billing-service-net` — all `Program.cs` + +```csharp +ValidateIssuerSigningKey = builder.Environment.IsDevelopment() ? false : true, +``` + +If `ASPNETCORE_ENVIRONMENT` is misconfigured on a server, all JWT signatures are bypassed. Any self-crafted token is accepted. + +**Fix**: Use a shared deterministic development signing key via environment variable. Always validate signatures. + +--- + +### SEC-10: BFF CORS Wildcard + All 26 Services Use `AllowAnyOrigin()` +**Source**: CTO (C3), Backend (W1), Founding Engineer (C1) +**Severity**: CRITICAL +**Files**: All 26 services `Program.cs`, BFF `Program.cs:74-78` + +Every service allows any origin to make cross-origin requests. Enables CSRF attacks and violates same-origin policy. + +**Fix**: Configure per-environment origin whitelists. Production: only `https://goodgo.vn`, `https://admin.goodgo.vn`. + +--- + +### SEC-11: No Content-Security-Policy Header +**Source**: Security (WARN-02), Frontend (IMP-02) +**Severity**: CRITICAL +**File**: `infra/traefik/dynamic/middlewares.yml` + +The `secure-headers` middleware is missing CSP, Referrer-Policy, and Permissions-Policy headers. Without CSP, the browser has no defense against XSS exploitation. + +**Fix**: Add CSP with `default-src 'self'; script-src 'self' 'wasm-unsafe-eval';` and related policies. + +--- + +### INFRA-1: Production K8s Manifests Hardcode `:latest` Image Tag +**Source**: DevOps (CRIT-1), CEO (referenced) +**Severity**: CRITICAL +**Files**: `deployments/production/kubernetes/*.yaml` + +All production Kubernetes deployments use `goodgo/*:latest`. Not reproducible, no rollback capability. + +**Fix**: Use commit SHA tags. Inject via Kustomize/Helm or pipeline sed-replace. + +--- + +### INFRA-2: Alertmanager Not Configured — All Alert Rules Silent +**Source**: DevOps (CRIT-2) +**Severity**: CRITICAL +**File**: `infra/observability/prometheus/prometheus.yml:29` + +Prometheus `alerting.alertmanagers.targets` is empty (`[]`). All defined alert rules (ServiceDown, HighErrorRate, HighLatencyP95) fire into the void. Production incidents will have zero notification. + +**Fix**: Deploy Alertmanager with Slack/PagerDuty receivers. Update targets to `['alertmanager:9093']`. + +--- + +### INFRA-3: Health Check Endpoints Require Authentication (18/23 Services) +**Source**: Founding Engineer (C2) +**Severity**: CRITICAL +**Files**: 18 services missing `.AllowAnonymous()` on health endpoints + +Kubernetes kubelet cannot authenticate. Liveness/readiness probes return 401 Unauthorized, causing pod restart loops. + +**Fix**: Add `.AllowAnonymous()` to all `MapHealthChecks()` calls. + +--- + +### DATA-1: InventoryItem.CreatedAt Backing Field Not Mapped — Silent Data Loss +**Source**: Database Architect (C-1) +**Severity**: CRITICAL +**File**: `services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/InventoryItemEntityTypeConfiguration.cs:34` + +`builder.Ignore(i => i.CreatedAt)` discards the property but never maps the private backing field `_createdAt`. Every entity loaded from the database has `CreatedAt == DateTime.MinValue`. + +**Fix**: Map `_createdAt` backing field to `created_at` column. No migration needed. + +--- + +### DATA-2: No Optimistic Concurrency on Wallet Balances — Race Condition +**Source**: Database Architect (C-2) +**Severity**: CRITICAL +**File**: `services/wallet-service-net/src/WalletService.Infrastructure/EntityConfigurations/WalletItemEntityTypeConfiguration.cs` + +Two concurrent POS transactions can read the same balance and overwrite each other. Money disappears silently. + +**Fix**: Add `builder.UseXminAsConcurrencyToken()` (Npgsql built-in). No migration needed. + +--- + +### SVC-1: 5 Services Still In-Progress (22% Incomplete) +**Source**: CEO (C2), Research Analyst (CRIT-01) +**Severity**: CRITICAL + +| Service | Status | +|---------|--------| +| `promotion-service-net` | 0 commands, 0 queries — controllers reference non-existent logic | +| `mission-service-net` | Domain model done, 0 CQRS handlers | +| `inventory-service-net` | 1 command handler out of 12+ endpoints needed | +| `ads-analytics-service-net` | Minimal commands, incomplete aggregation | +| `ads-billing-service-net` | 3 of 8+ command handlers | + +**Impact**: Cannot deliver full feature coverage. Order fulfillment blocked (no stock validation). Voucher campaigns non-functional. + +**Fix**: 3 backend engineers, 2-3 weeks dedicated effort. + +--- + +### SVC-2: ads-serving-service-net is READ-ONLY +**Source**: CEO (C3), Research Analyst (WARN-02) +**Severity**: CRITICAL + +Zero command handlers. Auction logic is query-only. Cannot create or manage ad placements programmatically. Ads platform is non-functional for write operations. + +--- + +### PROD-1: Marketing Suite is 100% Demo — Zero Backend Integration +**Source**: Product Manager (CI-1) +**Severity**: CRITICAL +**Files**: 6 Marketing pages (`AiChatbot.razor`, `CustomerCrm.razor`, `LivechatConsole.razor`, `AiContentStudio.razor`, `ChatbotAutomation.razor`, `SocialHub.razor`) + +All 6 pages use hardcoded demo data arrays with fake customer records. This is marketed as a key differentiator vs. KiotViet/Sapo/iPOS but delivers zero actual functionality. Merchants who sign up for Growth/Pro plans expecting CRM and AI chatbot will experience immediate trust damage. + +**Fix**: Hide Marketing section with "Coming Soon" label, or implement real backend integration (2+ weeks). + +--- + +### PROD-2: Analytics & Reporting — 0% Wired to Real Data +**Source**: Product Manager (CI-4) +**Severity**: CRITICAL + +Revenue Dashboard, Staff Performance, and EOD Report pages exist with full UI but all KPI values (revenue, transactions, avg order value, growth %) are hardcoded demo values. Merchants making business decisions based on fake numbers will lose real money. + +**Fix**: Wire to order-service data. Add `GetDailyRevenueQuery` and `GetTopProductsQuery`. + +--- + +### API-1: Response Format Fragmentation (3 Incompatible Patterns) +**Source**: API Architect (CRIT-1), Frontend (WARN-08) +**Severity**: CRITICAL + +Three response patterns exist: IAM uses typed `ApiResponse`, Order/Catalog use anonymous objects, and Merchant returns raw entities. Frontend compensates with a "4-format smart deserializer" — a fragile workaround. SDK auto-generation is blocked. + +**Fix**: Extract `ApiResponse` to shared NuGet package. Enforce across all services. + +--- + +### TEST-1: Only 1 of 26 Services Has CI Test Pipeline +**Source**: QA (C1), CTO (W2) +**Severity**: CRITICAL + +Only `iam-service-net` runs tests in CI. Regressions in 25 other services are undetected until staging deployment. The `deploy-staging.yml` runs migrations but NOT tests before deployment. + +**Fix**: Generate CI pipelines for all 25 missing services using `ci-iam-service.yml` as template. + +--- + +### CROSS-1: Cross-Service Messaging Not Implemented +**Source**: CTO (C5) +**Severity**: CRITICAL + +All `IntegrationEvents/EventHandlers/` directories across 10+ services are empty. RabbitMQ is provisioned in docker-compose but no service publishes or consumes messages. System works via HTTP tight coupling, which will fail at scale. + +**Fix**: Implement `IEventBus` abstraction with RabbitMQ backend. Start with `OrderCreatedIntegrationEvent` to inventory deduction. + +--- + +## Warnings + +Issues that should be addressed within the next 2-4 weeks. + +### Security Warnings + +| ID | Issue | Source | Files | +|----|-------|--------|-------| +| W-SEC-1 | CORS `allowCredentials: true` with localhost origins in shared middleware config | Security (WARN-03) | `infra/traefik/dynamic/middlewares.yml:27` | +| W-SEC-2 | `sslRedirect: false` in shared middleware — applies to all environments | Security (WARN-04), CTO | `infra/traefik/dynamic/middlewares.yml:5` | +| W-SEC-3 | `Jwt__RequireHttpsMetadata=false` in staging K8s ConfigMap | DevOps (WARN-10), Security (WARN-05) | `deployments/staging/kubernetes/configmap.yaml:21` | +| W-SEC-4 | Test credentials hardcoded in ROADMAP.md (checked into git) | CTO (W8) | `ROADMAP.md` Section IX | +| W-SEC-5 | K8s staging secrets file contains literal placeholder strings | Security (WARN-09) | `deployments/staging/kubernetes/secrets.yaml` | +| W-SEC-6 | `AllowedHosts: "*"` in IAM service — DNS rebinding risk | Security (WARN-08) | `services/iam-service-net/appsettings.json:79` | +| W-SEC-7 | No CDN Subresource Integrity on Lucide script from unpkg.com | Frontend (CRIT-04) | `index.html:19` | +| W-SEC-8 | TOTP verification window allows 90-second replay (no used-code tracking) | Security (WARN-10) | `TotpTwoFactorService.cs:86` | +| W-SEC-9 | Unauthenticated ad tracking endpoints without rate limiting | Security (WARN-07) | Routes for `/api/v1/pixels`, `/api/v1/conversions` | +| W-SEC-10 | Traefik dashboard exposed without authentication in local dev | Security (WARN-06), DevOps (WARN-8) | `docker-compose.yml:121` | + +### Infrastructure Warnings + +| ID | Issue | Source | Files | +|----|-------|--------|-------| +| W-INF-1 | Docker Compose port conflicts — 4 mkt-* services all bind port 5000 | CEO (W1), CTO (C4) | `docker-compose.yml` | +| W-INF-2 | Missing K8s manifests — Staging: 9 missing, Production: 11 missing | CEO (W2), CTO (W3) | `deployments/*/kubernetes/` | +| W-INF-3 | 13 services missing Traefik gateway routes — bypass security middleware | CTO (W4), API Architect (CRIT-3) | `infra/traefik/dynamic/routes.yml` | +| W-INF-4 | 15+ services not in CI/CD deployment pipeline | DevOps (WARN-2) | `.github/workflows/deploy-*.yml` | +| W-INF-5 | `redis-exporter` scraped by Prometheus but missing from docker-compose | DevOps (WARN-1) | `prometheus.yml:132` | +| W-INF-6 | Redis deployed as single instance in K8s — SPOF for real-time features | DevOps (WARN-4) | `deployments/staging/kubernetes/redis.yaml` | +| W-INF-7 | No Kubernetes NetworkPolicy manifests — all pods can communicate freely | DevOps (WARN-5) | Missing from all K8s dirs | +| W-INF-8 | Distributed tracing (Jaeger) commented out — no cross-service request correlation | DevOps (WARN-7) | `docker-compose.yml:1230-1241` | +| W-INF-9 | `docker-build.yml` pushes `:latest` tag to Docker Hub from `main` branch | DevOps (CRIT-3) | `.github/workflows/docker-build.yml:99-103` | +| W-INF-10 | PR checks only validate Node.js — no .NET build/test in PR pipeline | DevOps (WARN-3) | `.github/workflows/pr-checks.yml` | +| W-INF-11 | MinIO uses default credentials `minioadmin/minioadmin123` | DevOps (WARN-6) | `docker-compose.yml:78-79` | + +### Frontend Warnings + +| ID | Issue | Source | Files | +|----|-------|--------|-------| +| W-FE-1 | Token refresh not implemented — silent 401 failures on token expiry | Frontend (WARN-01) | `AuthStateService.cs` | +| W-FE-2 | Global HttpClient header mutation — race condition under concurrent requests | Frontend (WARN-02) | `PosDataService.cs:40-47` | +| W-FE-3 | No route guards — admin layout renders before auth check | Frontend (WARN-06) | All layouts | +| W-FE-4 | `shopId` not validated against user permissions — URL manipulation possible | Frontend (WARN-07) | `AdminLayout.razor:246-286` | +| W-FE-5 | ~20% of POS pages have incomplete backend integration (21 TODOs) | Frontend (WARN-10) | Multiple POS pages | +| W-FE-6 | MudBlazor ThemeProvider declared in multiple layouts — duplicate instances | Frontend (WARN-04) | All layout files | +| W-FE-7 | localStorage logic duplicated across 5 files | Frontend (WARN-05) | `AuthService.cs`, layouts, `LanguageSwitcher.razor` | +| W-FE-8 | `eval()` used for DOM focus management in OTP input | Architect (W-2) | `OtpInput.razor` | +| W-FE-9 | Incomplete vi-VN translations vs en-US.json | Frontend (WARN-11) | `wwwroot/locales/vi-VN.json` | + +### UX/Accessibility Warnings + +| ID | Issue | Source | Files | +|----|-------|--------|-------| +| W-UX-1 | No `:focus-visible` styles — keyboard navigation invisible (WCAG 2.4.7) | UX/UI (Issue 1) | All CSS files | +| W-UX-2 | Clickable `
` elements instead of ` +``` + +--- + +### 3. No ARIA Labels on Interactive Icons — WCAG 2.1 §4.1.2 + +**Files:** `Components/Auth/AuthButton.razor`, `Components/Auth/AuthInput.razor` (line 37), all layout files +**Impact:** Icon-only buttons (close, toggle, back) have no accessible names — screen readers announce nothing or raw Unicode. + +**Example (AuthInput.razor ~line 37):** +```razor + + + + + +``` + +--- + +### 4. Error/Success Messages Missing `role="alert"` — WCAG 2.1 §4.1.3 + +**Files:** `Pages/Auth/LoginAdmin.razor` (lines 28–40), `Pages/Pos/Cafe/CafeDesktop.razor` +**Impact:** Dynamic status changes (login error, payment success, cart update) are not announced to screen readers. + +**Example (LoginAdmin.razor ~line 29):** +```razor + +
+ @_errorMessage +
+ + + +``` + +--- + +### 5. Hardcoded Vietnamese Strings in POS UI — Localization Failure + +**File:** `Pages/Pos/Cafe/CafeDesktop.razor` and all POS vertical pages +**Impact:** English-language users see Vietnamese text. App cannot support international merchants. + +**Examples found (CafeDesktop.razor):** +| Line | Hardcoded String | Should Be | +|------|-----------------|-----------| +| 24 | `"Đang tải..."` | `@L["Common_Loading"]` | +| 25 | `"Không thể tải dữ liệu"` | `@L["Common_LoadError"]` | +| 66 | `"Đơn hàng"` | `@L["Pos_OrderPanel_Title"]` | +| 67 | `"món"` | `@L["Pos_CartItem_Unit"]` | +| 89 | `"Nhập mã voucher..."` | `@L["Pos_Voucher_Placeholder"]` | +| 97 | `"Giảm giá"` | `@L["Pos_Discount_Label"]` | +| 107 | `"Tổng cộng"` | `@L["Pos_Total_Label"]` | +| 117 | `"Đang tạo đơn..."` | `@L["Pos_CreatingOrder"]` | +| 122 | `"Thanh toán"` | `@L["Pos_Checkout_Button"]` | + +Also found in layouts: +- `AdminLayout.razor` (line ~160): `"Có lỗi xảy ra"`, `"Vui lòng thử lại"`, `"Tải lại"` +- `StaffLayout.razor` (line 148): Same error boundary text +- `PosLayout.razor` (lines 29, 65, 115): `"GoodGo POS"`, `"Menu"`, `"Đơn hàng"` + +--- + +### 6. Hardcoded Color Values Instead of CSS Variables + +**Files:** `Pages/Pos/Cafe/CafeDesktop.razor`, `Pages/Auth/LoginAdmin.razor`, multiple admin pages +**Impact:** Theme overrides are impossible. Colors are not consistent with the design token system. + +**Examples:** +```razor + +style="background:rgba(139,92,246,0.1);color:#8B5CF6;" +style="background:rgba(239,68,68,0.12);color:#EF4444;" +style="background-color:rgba(139,92,246,0.125);" + + +class="status-purple" +class="status-danger" +``` + +**Hardcoded colors to replace:** +| Value | Should Be | +|-------|-----------| +| `#16A34A` | `var(--color-success)` | +| `#EF4444` | `var(--color-danger)` | +| `#8B5CF6` | `var(--color-purple)` | +| `rgba(139,92,246,0.1)` | `var(--color-purple-subtle)` | +| `rgba(239,68,68,0.12)` | `var(--color-danger-subtle)` | + +--- + +## Warnings + +Issues that create significant technical debt or UX inconsistency. + +### 7. 2,316 Inline Style Attributes + +Inline styles are scattered across all pages — especially POS desktop views, admin shop pages, and dashboard. This makes CSS maintenance impossible and prevents design system enforcement. + +**Top offending patterns:** +```razor +style="display:flex;flex-direction:column;gap:24px;" +style="padding:6px 12px;border-radius:8px;border:none;" +style="position:absolute;left:@{X}px;top:@{Y}px;" +``` + +**Strategy:** Create CSS utility classes and component classes. Move inline styles to scoped `.razor.css` files. + +--- + +### 8. Inconsistent Spacing Scale + +The design uses 10+ different spacing values without a defined scale: +- Gaps: 4, 6, 8, 10, 12, 14, 16, 20, 24, 28, 32, 48px +- Padding: 6, 8, 10, 12, 16, 20, 24px +- Border radius: 6, 8, 10, 12, 14, 20, 24px + +**Recommended scale:** +```css +/* Spacing: 4-point scale */ +--space-1: 4px; --space-2: 8px; --space-3: 12px; +--space-4: 16px; --space-5: 20px; --space-6: 24px; +--space-8: 32px; --space-12: 48px; + +/* Border radius: 3 values */ +--radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px; +``` + +--- + +### 9. Missing Contrast Ratio for Secondary Text — WCAG 2.1 §1.4.3 + +**File:** `wwwroot/css/admin.css` + +```css +/* Current — MAY fail WCAG AAA */ +--admin-text-secondary: #ADADB0; /* ratio ~5.08:1 on #1A1A1D — FAILS AAA (7:1) */ +--admin-text-tertiary: #8B8B90; /* ratio ~4.13:1 on #1A1A1D — FAILS AA (4.5:1) */ +``` + +`--admin-text-tertiary` likely **fails WCAG AA** for normal text. Needs contrast validation. + +--- + +### 10. Cart Items Use `
` Instead of Semantic List + +**File:** `Pages/Pos/Cafe/CafeDesktop.razor` +Cart items rendered as generic `
` elements. Should be `
    /
  • ` structure for screen readers to announce list count and items. + +--- + +### 11. No `.razor.css` Scoped Stylesheets Per Component + +All 8 reusable components (`AuthButton`, `AuthInput`, `OtpInput`, etc.) have zero scoped CSS files. Styles are defined globally in `auth.css`. This creates unintended style leakage and makes component refactoring dangerous. + +--- + +### 12. OTP Input Missing ARIA Group Label + +**File:** `Components/Auth/OtpInput.razor` (lines 12–19) +The 6-digit OTP input is a group of individual `` elements but lacks `role="group"` and `aria-label="One-time password"`. Screen readers will announce 6 separate unlabeled inputs. + +--- + +### 13. No Focus Trap in Modal Overlays + +**Files:** `Layout/PosLayout.razor`, admin dialog pages +When mobile overlays or modals open, focus is not trapped within them. Users pressing Tab will cycle through background content, violating WCAG 2.1 §2.1.2. + +--- + +## Improvements + +Recommendations that would improve UX quality and developer experience. + +### A. Create Component-Scoped CSS Files + +For each component in `Components/`, create a matching `.razor.css` file: +``` +Components/Auth/AuthButton.razor.css +Components/Auth/AuthInput.razor.css +Components/Pos/ResponsiveOrderPanel.razor.css +``` + +### B. Add Password Strength Indicator + +**File:** `Pages/Auth/Register.razor` +Registration form has no visual password strength feedback. Add a 4-step strength bar (weak → fair → good → strong) using the existing `PasswordStrengthCalculator` pattern from Swift app. + +### C. Standardize Icon Sizes to 3 Tiers + +Currently uses: 12, 14, 16, 18, 20, 24, 28, 32px. Reduce to: +```css +--icon-sm: 16px; /* Inline, label */ +--icon-md: 20px; /* Button, nav */ +--icon-lg: 24px; /* Header, feature */ +``` + +### D. Add `aria-expanded` to Expandable Sections + +**File:** `Pages/Admin/Shop/ShopRecipes.razor` +Accordion-style expand/collapse sections lack `aria-expanded` and `aria-controls`. Screen readers cannot determine collapsed state. + +### E. Add `aria-busy` to Async Loading States + +Async data-fetch loading spinners don't set `aria-busy="true"` on the container, so screen readers don't know content is loading. + +### F. Introduce Design Token Documentation + +Create a living style guide page at `/admin/design-system` (dev only) that shows all color tokens, spacing, typography, and component variants. Reference: MudBlazor Theme Manager pattern. + +### G. Validate Table Semantics + +Admin tables (staff management, inventory, orders) should verify `` on all headers. MudBlazor's `MudTable` typically handles this, but custom HTML tables may not. + +--- + +## Action Items + +Prioritized next steps. + +| # | Priority | Effort | Item | Files | +|---|----------|--------|------|-------| +| 1 | 🔴 Critical | Small | Add `:focus-visible` styles globally | `wwwroot/css/app.css` | +| 2 | 🔴 Critical | Large | Replace clickable `
    ` with `