diff --git a/.kilo/kilo.jsonc b/.kilo/kilo.jsonc new file mode 100644 index 0000000..0f6fc14 --- /dev/null +++ b/.kilo/kilo.jsonc @@ -0,0 +1,40 @@ +{ + "$schema": "https://app.kilo.ai/config.json", + "mcpServers": { + "memory": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-memory"] + }, + "filesystem": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "${workspaceFolder}" + ] + }, + "gitea": { + "command": "npx", + "args": ["-y", "gitea-mcp"], + "env": { + "GITEA_HOST": "https://192.168.3.80", + "GITEA_ACCESS_TOKEN": "d9eb80ffe28f6e4e3ad5d5a032ca9c5f93ea5414" + } + }, + "sequential-thinking": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-sequential-thinking"] + }, + "fetch": { + "command": "npx", + "args": ["-y", "fetch-mcp"] + }, + "git": { + "command": "npx", + "args": ["-y", "git-mcp"], + "env": { + "GIT_DEFAULT_PATH": "${workspaceFolder}" + } + } + } +} \ No newline at end of file diff --git a/SECURITY_HARDENING_APPLIED_REPORT.md b/SECURITY_HARDENING_APPLIED_REPORT.md deleted file mode 100644 index 89a56e9..0000000 --- a/SECURITY_HARDENING_APPLIED_REPORT.md +++ /dev/null @@ -1,160 +0,0 @@ -# Security Hardening Application Report - -Generated: 2026-06-09 -Project: RentalDriveGo / Car Management System -Input archive: `/mnt/data/car_management_system_plan_applied(1).zip` -Plan applied: `/mnt/data/SECURITY_HARDENING_IMPLEMENTATION_PLAN(1).md` - -## Executive result - -The uploaded project was inspected and the security hardening plan was applied as far as safely possible inside the source archive. The archive already contained a broad previous hardening pass. I did not blindly trust that state, because that is how software becomes an expensive apology letter. I re-audited the implementation against the plan and applied additional corrections where the code still contradicted the target security model. - -The final output includes a patched project archive, this report, a changed-file list, and an incremental diff for the additional changes made during this pass. - -## What was already present in the uploaded project - -The project already contained many of the plan-aligned building blocks: - -- Centralized JWT helpers for actor tokens. -- HttpOnly session cookie helpers for admin, employee, and renter sessions. -- Company authorization policy helpers. -- Admin 2FA and fresh-2FA enforcement middleware. -- Public booking access-token helpers. -- Webhook idempotency helpers. -- Hashed `CompanyApiKey` model and migration. -- `ReservationPublicAccess` model and migration. -- `WebhookEvent` model and migration. -- Upload validation helpers and public/private storage separation. -- Request ID and sanitized error response middleware. -- Production Docker/Traefik hardening, including Redis authentication and `x-middleware-subrequest` blocking. -- Static security scan script and CI security gates. - -That base work was useful, but it had a few security and test-consistency gaps. - -## Additional changes applied in this pass - -### 1. Removed legacy plaintext company API key storage - -The plan requires company API keys to be hash-only. The code had introduced `CompanyApiKey`, but the legacy `Company.apiKey` field still existed in the Prisma schema and middleware still allowed a fallback lookup against that plaintext field when `ALLOW_LEGACY_COMPANY_API_KEYS=true`. - -Changes applied: - -- Removed `Company.apiKey` from `packages/database/prisma/schema.prisma`. -- Removed `apiKey` from the local `Company` TypeScript interface in `packages/database/src/index.ts` and `packages/database/src/index.d.ts`. -- Removed the legacy plaintext fallback path from `apps/api/src/middleware/requireApiKey.ts`. -- Added migration `20260609233000_drop_legacy_company_api_key` to drop the old column and unique index. -- Rewrote `requireApiKey` tests around prefix lookup, hash comparison, revocation, and `lastUsedAt` updates. - -Security effect: raw company API keys are no longer accepted through the legacy company column and are no longer represented in the current Prisma schema. - -### 2. Centralized Socket.io token verification - -The main API process still verified Socket.io auth tokens directly with `jwt.verify(token, JWT_SECRET)` and did not enforce issuer, audience, actor type, or allowed algorithm. This contradicted the session-authentication phase of the plan. - -Changes applied: - -- Added `verifyAnyActorToken()` to `apps/api/src/security/tokens.ts`. -- Updated `apps/api/src/index.ts` to use centralized actor-token verification for Socket.io authentication. -- Removed the direct `jsonwebtoken` import from the main API entrypoint. - -Security effect: Socket.io no longer accepts tokens that bypass the centralized actor-token constraints. - -### 3. Hardened employee password-reset JWT verification - -Employee password-reset tokens were signed and verified directly with the JWT secret and no issuer, audience, or algorithm constraints. - -Changes applied: - -- Added explicit `HS256` signing for employee password-reset tokens. -- Added issuer `rentaldrivego-api`. -- Added audience `employee_password_reset`. -- Added matching verification constraints. - -Security effect: reset tokens now reject wrong issuer, wrong audience, or wrong algorithm instead of relying on a bare shared-secret verification. - -### 4. Repaired test fixtures and middleware tests - -Some tests still expected pre-hardening behavior. That is a bad smell: tests defending old weaknesses are basically tiny lobbyists for future incidents. - -Changes applied: - -- Updated API-key middleware tests to validate hashed-key behavior instead of plaintext `Company.apiKey` lookup. -- Updated auth middleware tests to match the centralized token verifier behavior. -- Updated integration test helper token generation to use `signActorToken()` so generated test tokens include issuer and audience. - -Security effect: future test runs are less likely to push developers back toward weaker auth behavior. - -## Verification performed in this sandbox - -| Check | Result | Notes | -|---|---:|---| -| Static security scan | PASS | `npm run security:static` completed successfully. | -| Critical production dependency audit | PASS | `npm audit --package-lock-only --omit=dev --audit-level=critical` exited successfully. | -| JSON syntax check | PASS | Root and app `package.json` files and lockfile parsed successfully. | -| Shell syntax check | PASS | Shell scripts and production entrypoint parsed with `bash -n`. | -| YAML parse check | PASS | `docker-compose.production.yml` and `.gitlab-ci.yml` parsed successfully. | -| Node script syntax | PASS | `scripts/security-static-check.mjs` passed `node --check`. | -| Full dependency install | NOT COMPLETED | `npm ci --ignore-scripts --prefer-offline` could not complete in this sandbox. | -| Full type-check/test/build | NOT RUN | Requires dependencies to be installed. Run in CI or a normal development environment. | -| Docker compose config/render | NOT RUN | Docker is unavailable in this sandbox. | -| Prisma generate/migrate | NOT RUN | Requires dependency installation and a normal Prisma/DB environment. | - -## Dependency audit note - -The critical audit gate passes. The audit still reports moderate findings involving `postcss` through Next.js and `uuid` through Firebase/cron-related dependency chains. The lockfile’s suggested fixes require forced or breaking upgrades, so I did not casually smash the dependency graph with a hammer and call the mess “security.” Those should be handled in a controlled dependency-upgrade ticket with full frontend and notification regression testing. - -## Files changed by this pass - -- `apps/api/src/index.ts` -- `apps/api/src/middleware/requireApiKey.ts` -- `apps/api/src/middleware/requireApiKey.test.ts` -- `apps/api/src/middleware/requireCompanyAuth.test.ts` -- `apps/api/src/middleware/requireRenterAuth.test.ts` -- `apps/api/src/modules/auth/auth.employee.service.ts` -- `apps/api/src/security/tokens.ts` -- `apps/api/src/tests/helpers/fixtures.ts` -- `packages/database/prisma/schema.prisma` -- `packages/database/prisma/migrations/20260609233000_drop_legacy_company_api_key/migration.sql` -- `packages/database/src/index.ts` -- `packages/database/src/index.d.ts` - -See also: - -- `security_hardening_incremental.diff` -- `security_hardening_changed_files.txt` - -## Phase status against the hardening plan - -| Phase | Status | Evidence / caveat | -|---|---:|---| -| Phase 0: Emergency stabilization | Partial / needs operator action | Static scan passes, placeholders are present, but real production secret rotation cannot be performed inside the archive. | -| Phase 1: Sessions and authentication | Substantially applied | HttpOnly session helpers and centralized actor JWT verification exist; Socket.io and reset-token gaps were corrected in this pass. | -| Phase 2: Authorization and tenant isolation | Substantially applied | Company policy middleware exists; tenant-safe patterns are present in many modules. Full proof requires test suite and code review across all repository methods. | -| Phase 3: Public booking privacy | Applied at source level | Public access token model/helper and safe public booking flow are present. Must be verified with integration tests. | -| Phase 4: Admin hardening | Applied at source level | Mandatory 2FA and fresh-2FA middleware are present. Enrollment and recovery-code workflows still need production validation. | -| Phase 5: API key hardening | Strengthened in this pass | Legacy plaintext company API key storage/fallback removed; hash-only `CompanyApiKey` path remains. | -| Phase 6: Payments and webhooks | Applied at source level | Raw-body webhook handling and idempotency helpers are present. Must be verified against provider test events. | -| Phase 7: Upload and storage hardening | Applied at source level | Magic-byte validation and public/private storage split are present. Must be verified with upload abuse tests. | -| Phase 8: Rate limiting, errors, browser security | Applied at source level | Request IDs, sanitized errors, rate limit middleware, and security headers are present. Must be verified in deployed environment. | -| Phase 9: Deployment hardening | Applied at config level | Redis auth, non-public DB tooling, private networks, and Traefik header blocking are present. Must be verified on the real host. | -| Phase 10: Observability, auditability, jobs | Partial | Logging/audit hooks exist, but queue migration and operational observability need dedicated validation. | -| CI/CD security gates | Applied at config level | Security scan and critical audit gates exist; full CI must run outside this sandbox. | - -## Required follow-up before launch - -1. Rotate all real production secrets and invalidate old sessions/API keys where appropriate. -2. Run `npm ci` in CI or a normal development environment. -3. Run `npm run db:generate` and apply the new migration after backup. -4. Run full type-check, unit tests, integration tests, e2e tests, and builds. -5. Run payment-provider webhook test events using real sandbox provider signatures. -6. Verify public/private storage behavior with real uploaded files. -7. Verify Redis and PostgreSQL are not externally reachable from the production host. -8. Run container image build and Trivy scan. -9. Confirm admin 2FA enrollment and fresh-2FA gates before enabling privileged admin actions in production. -10. Document any deferrals with owner, risk acceptance, compensating control, deadline, and ticket number. - -## Launch recommendation - -Do not launch publicly yet based only on the patched archive. The source now better matches the plan, and the critical static/audit checks pass, but the hard launch gate still depends on full CI, Prisma migration validation, deployment verification, and production secret rotation. - -The patched archive is suitable for the next CI/staging pass. Treat it as implementation-ready source, not production clearance. Production clearance comes from reproducible evidence, not vibes in a ZIP file. diff --git a/SECURITY_HARDENING_LEFTOVER_APPLIED_REPORT.md b/SECURITY_HARDENING_LEFTOVER_APPLIED_REPORT.md deleted file mode 100644 index e46c475..0000000 --- a/SECURITY_HARDENING_LEFTOVER_APPLIED_REPORT.md +++ /dev/null @@ -1,255 +0,0 @@ -# Security Hardening Leftover Application Report - -Project: RentalDriveGo / Car Management System -Input archive: `car_management_system_hardened_applied.zip` -Output archive: `car_management_system_leftover_applied.zip` -Date: 2026-06-09 - -## Executive Summary - -This pass applied the remaining source-level hardening gaps that were still practical to implement directly in the repository after the first hardening pass. The focus was on eliminating browser-readable authentication assumptions, enforcing app-layer blocking for the `x-middleware-subrequest` bypass class, improving actor-aware rate limiting, adding an admin 2FA recovery-code workflow, and bringing documentation/static checks into line with the hardened authentication model. - -This does not replace production operator work such as real secret rotation, live infrastructure verification, container scanning, applying migrations to a real database, or running the complete CI/test pipeline. Those items remain launch-gate evidence requirements. - -## Applied Changes - -### 1. Removed remaining browser-side employee token assumptions - -Changed files: - -- `apps/dashboard/src/lib/api.ts` -- `apps/dashboard/src/components/layout/TopBar.tsx` -- `apps/dashboard/src/components/layout/Sidebar.tsx` -- `apps/dashboard/src/app/(dashboard)/team/page.tsx` -- `apps/dashboard/src/app/sign-in/[[...sign-in]]/SignInPageClient.tsx` - -What changed: - -- Removed dashboard use of employee auth tokens from `localStorage`. -- Dashboard API calls now use `credentials: 'include'` and depend on HttpOnly cookies. -- Dashboard Socket.io connection now uses cookie credentials instead of script-provided auth tokens. -- Team page no longer decodes employee identity from a localStorage JWT. It resolves the current actor through `/auth/employee/me`. -- Admin 2FA sign-in form now accepts either a six-digit TOTP code or a recovery code value. - -Security effect: - -- Reduces script-readable authentication exposure. -- Aligns the dashboard with the intended HttpOnly session-cookie model. -- Prevents UI code from treating a readable JWT as the authority for employee identity. - -### 2. Added Socket.io HttpOnly-cookie session support - -Changed file: - -- `apps/api/src/index.ts` - -What changed: - -- Added Socket.io session-token extraction from HttpOnly cookies. -- Preserved explicit token verification for trusted non-browser/server contexts, while browser clients can now authenticate through cookies. -- Reused centralized actor-token verification. - -Security effect: - -- Real-time dashboard connections no longer require JavaScript-readable employee tokens. -- Socket authentication now follows the same actor-token validation path used elsewhere. - -### 3. Added app-layer `x-middleware-subrequest` blocking - -Changed files: - -- `apps/api/src/app.ts` -- `apps/dashboard/src/middleware.ts` -- `apps/carplace/src/middleware.ts` -- `apps/admin/src/middleware.ts` -- `apps/dashboard/src/middleware.test.ts` -- `apps/carplace/src/middleware.test.ts` - -What changed: - -- API now rejects requests containing `x-middleware-subrequest` before route handling. -- Dashboard, carplace, and admin Next middleware now reject the same header at the app layer. -- Added/updated middleware tests for the rejection path. - -Security effect: - -- Adds defense in depth beyond reverse-proxy filtering. -- Prevents the project from depending on a single infrastructure control for this bypass class. - -### 4. Hardened admin/browser fetch behavior - -Changed files include: - -- `apps/admin/src/lib/api.ts` -- `apps/admin/src/app/dashboard/admin-users/page.tsx` -- `apps/admin/src/app/dashboard/renters/page.tsx` -- `apps/admin/src/app/dashboard/companies/[id]/page.tsx` -- `apps/admin/src/app/dashboard/containers/page.tsx` -- `apps/admin/src/app/dashboard/pricing/page.tsx` -- `apps/admin/src/app/forgot-password/page.tsx` -- `apps/admin/src/app/reset-password/page.tsx` - -What changed: - -- Admin API wrapper uses `credentials: 'include'`. -- Manual admin fetch calls now include credentials where they directly call the admin API. -- Removed dead placeholder `getToken()` helpers that returned empty strings and created meaningless `Authorization: Bearer ` headers. - -Security effect: - -- Admin browser requests now consistently rely on the HttpOnly admin session cookie. -- Removes misleading bearer-token scaffolding from the admin UI. - -### 5. Improved actor-aware rate limiting - -Changed files: - -- `apps/api/src/middleware/rateLimiter.ts` -- `apps/api/src/app.ts` - -What changed: - -- API rate-limit keys now prefer a verified actor identity from session cookies or valid Bearer tokens. -- Actor-aware rate limiting falls back safely to existing request actor fields or anonymous IP keys. -- Admin authentication routes now receive the stricter authentication limiter before the broader admin limiter. - -Security effect: - -- Authenticated traffic is limited by actor identity instead of only coarse IP data. -- Login and admin-auth abuse get stricter protection. -- Multi-container correctness still depends on Redis availability/configuration in production, which must be verified during deployment. - -### 6. Added admin 2FA recovery-code backend workflow - -Changed files: - -- `packages/database/prisma/schema.prisma` -- `packages/database/prisma/migrations/20260610001500_add_admin_recovery_codes/migration.sql` -- `apps/api/src/modules/admin/admin.repo.ts` -- `apps/api/src/modules/admin/admin.service.ts` -- `apps/api/src/modules/admin/admin.schemas.ts` -- `apps/api/src/modules/admin/admin.routes.ts` - -What changed: - -- Added `AdminRecoveryCode` model. -- Recovery codes are stored as bcrypt hashes, not plaintext. -- TOTP enrollment now issues one-time recovery codes. -- Recovery-code regeneration is protected by authenticated admin access and fresh 2FA. -- Login can consume a valid unused recovery code when TOTP is enabled. -- Recovery-code issuance and use are audited. - -Security effect: - -- Adds a recovery path for mandatory admin 2FA without storing backup codes in plaintext. -- Preserves one-time-use semantics. -- Adds auditability for recovery-code lifecycle events. - -Limitation: - -- The backend returns recovery codes after enrollment/regeneration. A production-ready admin UI still needs to display them once with clear save instructions and must not persist them client-side. - -### 7. Strengthened static scanning for auth-token regressions - -Changed file: - -- `scripts/security-static-check.mjs` - -What changed: - -- Static scan now catches common regressions involving auth/session token names in `localStorage` and `document.cookie`. -- Scan specifically targets auth token/session names such as employee/admin/renter tokens and sessions. - -Security effect: - -- Makes it harder for future code changes to quietly reintroduce script-readable authentication tokens. - -### 8. Updated documentation to match the hardened model - -Changed files: - -- `apps/dashboard/README.md` -- `memory/project_auth_architecture.md` -- `docs/project-design/COOKIE_POLICY.md` -- `apps/api/src/swagger/openapi.ts` - -What changed: - -- Replaced stale localStorage/JWT handoff claims with HttpOnly session-cookie wording. -- Clarified that browser clients use HttpOnly sessions and that Bearer tokens are only for documented trusted server/mobile contexts. -- Updated cookie policy references from legacy `employee_token` wording to `employee_session`. - -Security effect: - -- Reduces the odds that a future developer follows stale documentation and reintroduces the old pattern. - -## Validation Performed - -The following checks were run successfully in the available environment: - -```bash -npm run security:static -node --check scripts/security-static-check.mjs -# JSON parse validation for package.json and package-lock.json files -# YAML parse validation for docker-compose.production.yml and .gitlab-ci.yml -bash -n docker/entrypoint.production.sh scripts/docker-prod-*.sh scripts/docker-registry-local-up.sh scripts/setup-clerk-keys.sh -# TypeScript/TSX syntax transpile check over 507 source files -npm audit --package-lock-only --omit=dev --audit-level=critical -``` - -Results: - -- Security static check: passed. -- Static-check script syntax: passed. -- Package JSON validation: passed. -- YAML validation: passed. -- Shell syntax validation: passed. -- TypeScript/TSX syntax transpile validation: passed. -- Critical production dependency audit: passed. - -Audit note: - -- `npm audit --audit-level=critical` exited successfully. -- Moderate advisories remain for transitive `postcss` and `uuid` paths. The available automatic fixes require breaking/force dependency changes, so they were not applied blindly in this source pass. - -## Not Fully Verified in This Environment - -The following items require a real development/CI/deployment environment: - -- `npm ci` from a clean checkout. -- Full workspace typecheck. -- Full unit, integration, security, and e2e test suites. -- Prisma client generation. -- Applying the new database migration to a real database. -- Database backup/restore verification. -- Docker image build and runtime validation. -- Container scanning with Trivy or equivalent. -- Live reverse-proxy validation for `x-middleware-subrequest` blocking. -- Redis-backed distributed rate-limit validation across multiple API containers. -- Provider webhook sandbox tests. -- Live admin 2FA recovery-code UX verification. - -## Remaining Launch-Gate Work - -These are still not things source edits can prove by themselves: - -1. Rotate all real production secrets. -2. Confirm no real secrets exist in repository history or image layers. -3. Apply and verify the new `admin_recovery_codes` migration. -4. Run the full CI gate: lint, typecheck, unit tests, integration tests, security tests, build, dependency audit, secret scan, and container scan. -5. Confirm Redis and PostgreSQL are private in production. -6. Confirm DB management tools are not publicly reachable. -7. Confirm production containers run non-root with reduced capabilities and resource limits. -8. Confirm webhook signature/idempotency behavior against real provider sandbox payloads. -9. Confirm private files are inaccessible through static routes in the deployed environment. -10. Confirm the admin UI displays recovery codes once and instructs admins to save them securely. - -## Changed Files - -See `security_hardening_leftover_changed_files.txt` for the complete file list and `security_hardening_leftover.diff` for the unified diff. - -## Final Assessment - -This pass closes a meaningful set of leftover source-level gaps from the security-hardening plan. The project is closer to the intended model: API-enforced security, HttpOnly browser sessions, stronger admin 2FA recovery, app-layer bypass blocking, and less stale documentation. - -However, this still should not be treated as production-ready until the remaining launch-gate evidence is collected from CI and the live deployment environment. Security that has not been tested in the actual runtime is mostly optimism with a lanyard. diff --git a/apps/api/src/modules/subscriptions/subscription.collections.service.ts b/apps/api/src/modules/subscriptions/subscription.collections.service.ts index c4ba67a..e14a594 100644 --- a/apps/api/src/modules/subscriptions/subscription.collections.service.ts +++ b/apps/api/src/modules/subscriptions/subscription.collections.service.ts @@ -269,7 +269,8 @@ async function createRequiredCallTask(caseData: any) { async function getRenewalPrice(plan: string, billingPeriod: string) { const configured = await prisma.pricingConfig.findUnique({ where: { plan_billingPeriod: { plan, billingPeriod } } }) - return configured?.amount ?? (PLAN_PRICES as any)[plan]?.[billingPeriod]?.MAD + const fallback = (PLAN_PRICES as any)[plan]?.[billingPeriod]?.MAD + return Number.isInteger(configured?.amount) && configured!.amount > 0 ? configured!.amount : fallback } export async function ensureRenewalCollectionsCases(now = new Date()) { diff --git a/apps/api/src/modules/subscriptions/subscription.manual.service.ts b/apps/api/src/modules/subscriptions/subscription.manual.service.ts index 07ef4c0..529748c 100644 --- a/apps/api/src/modules/subscriptions/subscription.manual.service.ts +++ b/apps/api/src/modules/subscriptions/subscription.manual.service.ts @@ -292,7 +292,7 @@ export async function ensurePrimaryBillingAccount(companyId: string, employeeId? async function resolvePrice(plan: string, billingPeriod: string) { const configured = await prisma.pricingConfig.findUnique({ where: { plan_billingPeriod: { plan, billingPeriod } } }) const fallback = (PLAN_PRICES as any)[plan]?.[billingPeriod]?.MAD - const amount = configured?.amount ?? fallback + const amount = Number.isInteger(configured?.amount) && configured!.amount > 0 ? configured!.amount : fallback if (!Number.isInteger(amount) || amount <= 0) throw new ValidationError('Invalid plan or billing period') return amount } diff --git a/apps/api/src/modules/subscriptions/subscription.service.edge.test.ts b/apps/api/src/modules/subscriptions/subscription.service.edge.test.ts index 02b422a..d4850c2 100644 --- a/apps/api/src/modules/subscriptions/subscription.service.edge.test.ts +++ b/apps/api/src/modules/subscriptions/subscription.service.edge.test.ts @@ -78,6 +78,22 @@ describe('subscription.service operational edges', () => { }) }) + it('keeps catalog defaults when persisted pricing overrides are invalid', async () => { + vi.mocked(prisma.pricingConfig.findMany).mockResolvedValue([ + { plan: 'STARTER', billingPeriod: 'ANNUAL', amount: 0 }, + { plan: 'GROWTH', billingPeriod: 'ANNUAL', amount: null }, + { plan: 'PRO', billingPeriod: 'ANNUAL', amount: -1 }, + { plan: 'ENTERPRISE', billingPeriod: 'ANNUAL', amount: 575040 }, + ] as never) + + await expect(service.getPlans()).resolves.toEqual(expect.objectContaining({ + STARTER: expect.objectContaining({ ANNUAL: { MAD: 143040 } }), + GROWTH: expect.objectContaining({ ANNUAL: { MAD: 287040 } }), + PRO: expect.objectContaining({ ANNUAL: { MAD: 383040 } }), + ENTERPRISE: expect.objectContaining({ ANNUAL: { MAD: 575040 } }), + })) + }) + it('starts trials only after enforcing one-trial-per-company policy', async () => { vi.mocked(repo.findByCompany).mockResolvedValue({ id: 'sub_old', trialUsed: true } as never) diff --git a/apps/api/src/modules/subscriptions/subscription.service.ts b/apps/api/src/modules/subscriptions/subscription.service.ts index d31b556..cdbc60d 100644 --- a/apps/api/src/modules/subscriptions/subscription.service.ts +++ b/apps/api/src/modules/subscriptions/subscription.service.ts @@ -18,8 +18,9 @@ export async function getPlans() { const configs = await prisma.pricingConfig.findMany() const result: Record>> = structuredClone(PLAN_PRICES) for (const c of configs) { + if (!Number.isInteger(c.amount) || c.amount <= 0) continue if (!result[c.plan]) result[c.plan] = {} - result[c.plan]![c.billingPeriod] = { MAD: c.amount } + result[c.plan]![c.billingPeriod] = { ...(result[c.plan]?.[c.billingPeriod] ?? {}), MAD: c.amount } } return result } diff --git a/apps/api/src/modules/subscriptions/subscription.upgrade.service.ts b/apps/api/src/modules/subscriptions/subscription.upgrade.service.ts index 6640d94..e6cedcc 100644 --- a/apps/api/src/modules/subscriptions/subscription.upgrade.service.ts +++ b/apps/api/src/modules/subscriptions/subscription.upgrade.service.ts @@ -61,7 +61,8 @@ async function getNextInvoiceSequence(tx: any) { async function resolvePrice(plan: PlanCode, billingPeriod: BillingPeriodCode) { const configured = await prisma.pricingConfig.findUnique({ where: { plan_billingPeriod: { plan, billingPeriod } } }) - const amount = configured?.amount ?? (PLAN_PRICES as any)[plan]?.[billingPeriod]?.MAD + const fallback = (PLAN_PRICES as any)[plan]?.[billingPeriod]?.MAD + const amount = Number.isInteger(configured?.amount) && configured!.amount > 0 ? configured!.amount : fallback if (!Number.isInteger(amount) || amount <= 0) throw new ValidationError('Invalid plan or billing period') return amount } diff --git a/apps/dashboard/src/app/(dashboard)/subscription/page.tsx b/apps/dashboard/src/app/(dashboard)/subscription/page.tsx index b002aa7..1ddaaef 100644 --- a/apps/dashboard/src/app/(dashboard)/subscription/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/subscription/page.tsx @@ -139,7 +139,13 @@ const PLAN_LABELS: Record = { ENTERPRISE: 'Enterprise', } -function mergePlanPrices(overrides?: Record>> | null) { +type PlanPriceOverrides = Record>> + +function isValidPriceAmount(amount: unknown): amount is number { + return typeof amount === 'number' && Number.isInteger(amount) && amount > 0 +} + +function mergePlanPrices(overrides?: PlanPriceOverrides | null) { const result: Record>> = { STARTER: { MONTHLY: { ...PLAN_PRICES.STARTER.MONTHLY }, @@ -162,7 +168,10 @@ function mergePlanPrices(overrides?: Record { const [prices, features] = await Promise.all([ - apiFetch>>>('/subscriptions/plans'), + apiFetch('/subscriptions/plans'), apiFetch('/subscriptions/features'), ]) setPlanPrices(mergePlanPrices(prices)) @@ -984,7 +993,7 @@ export default function SubscriptionPage() { {isActive && {copy.active}}

- {price ? formatCurrency(price, 'MAD') : '—'} + {isValidPriceAmount(price) ? formatCurrency(price, 'MAD') : '—'} /{billingPeriod === 'MONTHLY' ? copy.perMonthShort : copy.perYearShort}

    @@ -1069,7 +1078,7 @@ export default function SubscriptionPage() {

    {copy.total}

    - {payableAmount ? formatCurrency(payableAmount, 'MAD') : '—'} + {isValidPriceAmount(payableAmount) ? formatCurrency(payableAmount, 'MAD') : '—'} {!upgradeProration ? ( /{billingPeriod === 'MONTHLY' ? copy.perMonth : copy.perYear} ) : null} diff --git a/AGENTS.md b/docs/AGENTS.md similarity index 100% rename from AGENTS.md rename to docs/AGENTS.md diff --git a/DESIGN_MIGRATION_REPORT.md b/docs/DESIGN_MIGRATION_REPORT.md similarity index 100% rename from DESIGN_MIGRATION_REPORT.md rename to docs/DESIGN_MIGRATION_REPORT.md diff --git a/MANUAL_SUBSCRIPTION_PAYMENTS.md b/docs/MANUAL_SUBSCRIPTION_PAYMENTS.md similarity index 100% rename from MANUAL_SUBSCRIPTION_PAYMENTS.md rename to docs/MANUAL_SUBSCRIPTION_PAYMENTS.md diff --git a/NAVBAR_FOOTER_REFACTOR_REPORT.md b/docs/NAVBAR_FOOTER_REFACTOR_REPORT.md similarity index 100% rename from NAVBAR_FOOTER_REFACTOR_REPORT.md rename to docs/NAVBAR_FOOTER_REFACTOR_REPORT.md diff --git a/PHASE16_OPERATIONAL_UI_REPORT.md b/docs/PHASE16_OPERATIONAL_UI_REPORT.md similarity index 100% rename from PHASE16_OPERATIONAL_UI_REPORT.md rename to docs/PHASE16_OPERATIONAL_UI_REPORT.md diff --git a/PHASE16_OPERATIONAL_VALIDATION_REPORT.md b/docs/PHASE16_OPERATIONAL_VALIDATION_REPORT.md similarity index 100% rename from PHASE16_OPERATIONAL_VALIDATION_REPORT.md rename to docs/PHASE16_OPERATIONAL_VALIDATION_REPORT.md diff --git a/apps/SECURITY_HARDENING_APPLIED.md b/docs/SECURITY_HARDENING_APPLIED.md similarity index 100% rename from apps/SECURITY_HARDENING_APPLIED.md rename to docs/SECURITY_HARDENING_APPLIED.md