Files
pos-system/docs/audit/frontend.md

14 KiB
Raw Blame History

Frontend Audit Report — POS System

Auditor: Senior Frontend Microservice Engineer Date: 2026-03-20 Scope: Blazor WASM apps (web-client-tpos-net, web-client-base-net), MudBlazor usage, component architecture, state management Codebase: /apps/web-client-tpos-net (195 Razor files, 248 C# files)


Executive Summary

The POS System frontend (web-client-tpos-net) has a solid architectural foundation with well-structured multi-layout design, multi-vertical POS support, and clean component separation. However, 3 critical security vulnerabilities were identified (JWT in localStorage, hardcoded OAuth2 client secret, password grant flow), combined with near-zero test coverage (~1 test file, 36 lines) and ~20% of POS pages having incomplete backend integration. These issues must be resolved before production deployment.


Critical Issues

CRIT-01 — JWT Stored in localStorage (XSS Risk)

File: apps/web-client-tpos-net/src/WebClientTpos.Client/Services/AuthService.cs lines 147148 Severity: CRITICAL

await _js.InvokeVoidAsync("localStorage.setItem", TokenKey(role), token.AccessToken);

JWT tokens are stored in localStorage, making them accessible to JavaScript. Any XSS exploit will steal all active tokens and allow full account impersonation.

Fix: Use httpOnly cookies via the BFF (WebClientTpos.Server). Add a /api/auth/token endpoint on the BFF that sets the token as a cookie rather than returning it in the response body.


CRIT-02 — Client Secret Hardcoded in Client-Side Code

File: apps/web-client-tpos-net/src/WebClientTpos.Client/Services/AuthService.cs lines 3940 Severity: CRITICAL

private const string ClientId = "password-client";
private const string ClientSecret = "password-client-secret";

Blazor WASM compiles to WebAssembly served to the browser. Any constant is extractable via browser developer tools or disassembly. If password-client-secret is a real secret used in Duende IdentityServer, it is already compromised.

Fix: Move all OAuth2 token exchange calls to the BFF. The BFF holds the client secret server-side and proxies to IAM on behalf of the browser.


CRIT-03 — Deprecated Password Grant (OAuth2)

File: apps/web-client-tpos-net/src/WebClientTpos.Client/Services/AuthService.cs line 136 Severity: CRITICAL

new KeyValuePair<string, string>("grant_type", "password"),

The Resource Owner Password Credentials grant is deprecated in OAuth 2.1. It bypasses consent screens and exposes credentials to the client application. It cannot support MFA flows properly.

Fix: Migrate to Authorization Code Flow with PKCE. Use Duende IdentityServer's built-in OIDC endpoints.


CRIT-04 — No CDN Subresource Integrity (SRI)

File: apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/index.html line 19 Severity: HIGH

Lucide icons loaded from unpkg.com CDN without an integrity attribute. A CDN compromise would allow arbitrary JavaScript execution in all POS sessions.

Fix:

<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"
        integrity="sha384-[HASH]"
        crossorigin="anonymous"></script>

Use a pinned version and generate the SRI hash with openssl dgst -sha384.


Warnings

WARN-01 — Token Refresh Not Implemented

File: apps/web-client-tpos-net/src/WebClientTpos.Client/Services/AuthStateService.cs Severity: HIGH

AuthStateService stores the access token but never checks expiry or calls the refresh token endpoint. When tokens expire, API calls silently fail with 401, and users are not redirected to login.

Fix: Add a TokenExpiry property and a background timer that refreshes the token 60 seconds before expiry using the refresh_token from OAuthTokenResponse.


WARN-02 — Global HttpClient Header Mutation (Race Condition)

File: apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs lines 4047 Severity: HIGH

_http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", _authState.Token);

Mutating DefaultRequestHeaders on a shared HttpClient instance is not thread-safe. Under concurrent requests, one request may attach or overwrite the header mid-flight.

Fix: Use HttpRequestMessage with per-request headers, or register a DelegatingHandler that injects the Bearer token automatically without touching global headers.


WARN-03 — Lucide Re-Init on Every Render

File: apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/AdminLayout.razor line 196 Severity: MEDIUM

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    try { await JS.InvokeVoidAsync("lucide.createIcons"); } catch { }

createIcons() traverses the entire DOM on every render cycle. index.html already sets up a MutationObserver for this purpose (lines 2546). The explicit call is redundant and adds unnecessary overhead.

Fix: Remove the lucide.createIcons() call from OnAfterRenderAsync entirely. The MutationObserver handles dynamic icon insertion.


WARN-04 — MudBlazor ThemeProvider Declared in Multiple Layouts

File: apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/AdminLayout.razor lines 1720 Severity: MEDIUM

<MudThemeProvider>, <MudPopoverProvider>, <MudDialogProvider>, and <MudSnackbarProvider> are declared in each layout independently. This causes multiple instances in the component tree and can create CSS specificity conflicts and duplicate dialog/snackbar stacks.

Fix: Move all four providers to App.razor once, outside the <Router>. Remove from individual layouts.


WARN-05 — localStorage Logic Duplicated Across 5 Files

Severity: MEDIUM

Token read/write logic is repeated in:

  1. AuthService.cs lines 147148
  2. AuthService.cs lines 230236
  3. AdminLayout.razor lines 215220
  4. StaffLayout.razor (similar pattern)
  5. LanguageSwitcher.razor

Fix: Extract a LocalStorageService with typed GetAsync<T> / SetAsync<T> / RemoveAsync methods. Register as singleton.


WARN-06 — No Route Guards for Authenticated Pages

File: apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/AdminLayout.razor Severity: HIGH

Session restore happens in OnAfterRenderAsync, meaning the admin layout renders briefly before auth is checked. No <AuthorizeView> or route guard redirects unauthenticated users before render.

Fix: Add a RedirectToLogin component or use Blazor's [Authorize] attribute with a proper AuthenticationStateProvider backed by AuthStateService.


WARN-07 — shopId Not Validated Against User Permissions

File: apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/AdminLayout.razor lines 246286 Severity: HIGH

Shop context is detected by parsing the URL path (e.g., /admin/shop/{shopId}/cafe/). There is no check that the authenticated user has access to the given shopId. A user could navigate directly to another merchant's shop URL.

Fix: After detecting shopId from URL, call the backend to verify the current user owns/manages that shop. Redirect to /admin with an error if unauthorized.


WARN-08 — Fragile Multi-Format API Response Deserialization

File: apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs lines 122185 Severity: MEDIUM

The deserializer tries 5 different response shapes in order. At line 152155, it falls back to selecting the first array property found in the JSON object. If a response contains multiple array properties (e.g., { "items": [...], "errors": [...] }), it selects whichever comes first in the serialized JSON, which is non-deterministic.

Fix: Standardize all API responses to a single envelope: { "success": bool, "data": T, "error": { "code": "", "message": "" } }. Remove the multi-format fallback logic.


WARN-09 — Hardcoded String in AuthInput Component

File: apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Auth/AuthInput.razor line 56 Severity: LOW

public string ForgotPasswordText { get; set; } = "Quên mật khẩu?";

Default value hardcoded in Vietnamese. English-culture users will see Vietnamese text.

Fix: Use @inject IStringLocalizer<AuthInput> L and default to L["ForgotPassword"].


WARN-10 — ~20% of POS Pages Have Incomplete Backend Integration

Severity: HIGH

21 TODO comments indicate unimplemented API integration:

File Missing Integration
MemberCard.razor Member visit history API
PartialPayment.razor Order Service API
TipEntry.razor Order Service API
GiftCardPayment.razor Order Service API
PaymentPending.razor Order Service API
CashDrawer.razor Merchant Service staff/shift
QuickSale.razor Merchant Service staff/shift
PendingOrders.razor Merchant Service staff/shift
ClockInOut.razor Merchant Service staff/shift
ShiftManagement.razor Merchant Service staff/shift
OrderCancel.razor DDD Value Object mapping fix
PriceCheck.razor DDD Value Object mapping fix
OrderEdit.razor DDD Value Object mapping fix
VoidRefund.razor DDD Value Object mapping fix
StockIn.razor DDD Value Object mapping fix
StockTransfer.razor DDD Value Object mapping fix
StockOut.razor DDD Value Object mapping fix
TreatmentPlan.razor Backend CRUD API
ConsentForm.razor Backend persistence API

WARN-11 — Incomplete vi-VN Translations

File: apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/locales/vi-VN.json Severity: LOW

Some keys present in en-US.json are missing in vi-VN.json. When a key is missing, JsonStringLocalizer returns the key name as the display text.

Fix: Run a diff between the two JSON files in CI. Fail the build on missing translation keys.


WARN-12 — No IFormatProvider in JsonStringLocalizer

File: apps/web-client-tpos-net/src/WebClientTpos.Client/Localization/JsonStringLocalizer.cs Severity: LOW

string.Format(format, arguments) is called without a culture-aware IFormatProvider. Numbers and dates will be formatted using the thread's current culture, not the user's selected locale.

Fix:

var value = string.Format(CultureInfo.CurrentUICulture, format ?? name, arguments);

WARN-13 — No IAsyncDisposable on Layout Components

Severity: LOW

AdminLayout.razor subscribes to AuthStateService.OnChange but does not implement IAsyncDisposable to unsubscribe. In long-lived SPA sessions with layout reloads (e.g., via forceLoad navigation), this will leak event handlers.

Fix:

@implements IDisposable
// ...
public void Dispose() => _authState.OnChange -= StateHasChanged;

Improvements

IMP-01 — Implement Authorization Code Flow + PKCE

Replace the password grant with a proper OIDC flow. Duende IdentityServer supports this natively. This enables MFA, social login, and OAuth2 compliance.

IMP-02 — Add Content Security Policy

Add CSP header in BFF or via Traefik middleware:

Content-Security-Policy: default-src 'self'; script-src 'self' unpkg.com; style-src 'self' 'unsafe-inline'

IMP-03 — Add @key to List Renders

All @foreach that render lists of components should use @key to prevent unnecessary DOM diffing:

@foreach (var item in _items)
{
    <ItemComponent @key="item.Id" Item="item" />
}

IMP-04 — Lazy Load POS Vertical Pages

Each POS vertical (Karaoke, Restaurant, Spa, Cafe, Retail) should be lazy-loaded. Blazor WASM supports lazy assembly loading via LazyAssemblyLoader. This will reduce initial download size.

IMP-05 — Extract AdminLayout Shop Context to Service

AdminLayout.razor is 328 lines with complex shop context detection inline. Extract shop context detection to a ShopContextService (scoped) that parses the URL and validates permissions.

IMP-06 — Add @rendermode InteractiveWebAssembly Explicitly

With .NET 8+, pages should declare render mode explicitly to avoid confusion with static rendering.

IMP-07 — Standardize DTO Naming Convention

DTOs use mixed Dto vs no-suffix naming:

  • MerchantRegisterDto (has Dto suffix) vs AuthTokenResponse (no suffix)

Standardize to always use Dto suffix for data transfer objects.

IMP-08 — Add BFF Health Check Routes

WebClientTpos.Server/Program.cs has no health check endpoints. The BFF should expose /health/live and /health/ready for K8s probes.

IMP-09 — Pin Lucide Version

Currently using lucide@latest. Pin to a specific version (e.g., lucide@0.363.0) to prevent breaking changes from CDN updates.


Action Items

Prioritized by severity:

Priority Item File(s) Effort
P0 CRIT-02: Verify ClientSecret not exposed in prod AuthService.cs:40 1h
P0 CRIT-01: Migrate JWT to httpOnly cookie via BFF AuthService.cs, BFF 2d
P0 CRIT-03: Replace password grant with PKCE flow AuthService.cs 3d
P0 WARN-06: Add route guards / AuthorizeView All layouts 1d
P0 WARN-07: Validate shopId against user permissions AdminLayout.razor:246 4h
P1 CRIT-04: Add SRI hash to Lucide CDN index.html:19 30m
P1 WARN-01: Implement token refresh AuthStateService.cs 1d
P1 WARN-02: Fix global HttpClient header mutation PosDataService.cs:40 4h
P1 WARN-03: Remove redundant lucide.createIcons call AdminLayout.razor:196 30m
P1 WARN-04: Move MudBlazor providers to App.razor All layouts 1h
P1 WARN-10: Implement missing API integrations (21 TODOs) Multiple POS pages 5d
P2 WARN-05: Extract LocalStorageService 5 files 4h
P2 WARN-08: Standardize API response envelope PosDataService.cs:122 2d
P2 WARN-13: Add IDisposable to layouts Layout files 1h
P2 IMP-01: Write 50+ unit tests for services /tests/ 3d
P2 IMP-04: Lazy load POS vertical assemblies Program.cs 1d
P3 WARN-09: Localize ForgotPasswordText AuthInput.razor:56 30m
P3 WARN-11: CI check for missing translation keys CI pipeline 2h
P3 WARN-12: Add IFormatProvider to localizer JsonStringLocalizer.cs 1h
P3 IMP-09: Pin Lucide version index.html:19 15m

Generated by Senior Frontend Microservice Engineer audit — TEC-229