diff --git a/.env.docker.dev b/.env.docker.dev index 60aba93..ce43a6f 100644 --- a/.env.docker.dev +++ b/.env.docker.dev @@ -36,8 +36,8 @@ CORS_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:3002,h # Stripe subscription checkout # STRIPE_API_KEY must be a Stripe secret/restricted key (sk_ or rk_). # STRIPE_WEBHOOK_SECRET must be a Stripe webhook signing secret (whsec_). -STRIPE_API_KEY=sk_test_51TvTsb9SpDRZn9yJyBAlUSXcTp9zpwQfhJYNKxfyYaZbqT6NN8W4pXu0zOUvpunrDPdtC0I6OZPzq0B5RRI1Ybub00OcYvj28K -STRIPE_WEBHOOK_SECRET=whsec_c5e0a6b2dd5e2f6ac804b428fe46f04e3af9b55c562f4124de20c52866f3c211 +STRIPE_API_KEY=placeholder +STRIPE_WEBHOOK_SECRET=placeholder # Email — Resend (primary) with SMTP fallback # Get your API key at https://resend.com/api-keys RESEND_API_KEY=re_PLACEHOLDER @@ -48,7 +48,7 @@ MAIL_HOST=smtp.gmail.com MAIL_PORT=587 MAIL_SCHEME=smtp MAIL_USERNAME=rentaldrivego@gmail.com -MAIL_PASSWORD=kfahihfzbcvkczew +MAIL_PASSWORD=placeholder MAIL_FROM_ADDRESS=rentaldrivego@gmail.com MAIL_FROM_NAME=RentalDriveGo MAIL_REPLY_TO_ADDRESS=rentaldrivego@gmail.com diff --git a/.env.docker.production.example b/.env.docker.production.example index c31cc39..0effdc8 100644 --- a/.env.docker.production.example +++ b/.env.docker.production.example @@ -80,7 +80,7 @@ MAIL_HOST=smtp.gmail.com MAIL_PORT=587 MAIL_SCHEME=smtp MAIL_USERNAME=rentaldrivego@gmail.com -MAIL_PASSWORD=kfahihfzbcvkczew +MAIL_PASSWORD=placeholder MAIL_FROM_ADDRESS=rentaldrivego@gmail.com MAIL_FROM_NAME=RentalDriveGo MAIL_REPLY_TO_ADDRESS=rentaldrivego@gmail.com @@ -89,8 +89,8 @@ MAIL_REPLY_TO_NAME=RentalDriveGo # Stripe subscription checkout # STRIPE_API_KEY must be a Stripe secret/restricted key (sk_ or rk_). # STRIPE_WEBHOOK_SECRET must be a Stripe webhook signing secret (whsec_). -STRIPE_API_KEY=sk_test_51TvTsb9SpDRZn9yJyBAlUSXcTp9zpwQfhJYNKxfyYaZbqT6NN8W4pXu0zOUvpunrDPdtC0I6OZPzq0B5RRI1Ybub00OcYvj28K -STRIPE_WEBHOOK_SECRET=whsec_c5e0a6b2dd5e2f6ac804b428fe46f04e3af9b55c562f4124de20c52866f3c211 +STRIPE_API_KEY=placeholder +STRIPE_WEBHOOK_SECRET=placeholder # ── Firebase push notifications (optional) ──────────────────────────────────── # FIREBASE_PROJECT_ID=your-firebase-project-id diff --git a/.env.example b/.env.example index ae679e8..b0b0427 100644 --- a/.env.example +++ b/.env.example @@ -49,6 +49,11 @@ PAYPAL_BASE_URL=https://api-m.paypal.com # Use https://api-m.sandbox.paypal.com for sandbox NEXT_PUBLIC_PAYPAL_CLIENT_ID=your-paypal-client-id +# ─── Stripe (subscription checkout) ──────────────────────────── +# Prefer a restricted API key (rk_) with the minimum Billing/Checkout permissions. +STRIPE_API_KEY=placeholder +STRIPE_WEBHOOK_SECRET=placeholder + # ─── Cloudinary (Vehicle + brand photos) ────────────────────── CLOUDINARY_CLOUD_NAME=your-cloud-name CLOUDINARY_API_KEY=your-api-key diff --git a/apps/api/src/modules/menu/menu.service.test.ts b/apps/api/src/modules/menu/menu.service.test.ts index fd1e279..d6a4fe9 100644 --- a/apps/api/src/modules/menu/menu.service.test.ts +++ b/apps/api/src/modules/menu/menu.service.test.ts @@ -199,7 +199,7 @@ describe('menu.service', () => { expect(result.items.find((item) => item.label === 'Reports')?.reasons.join(' ')).toContain('STARTER does not include this menu item') }) - it('returns the approved seven-item STARTER owner sidebar in order', async () => { + it('returns the approved STARTER owner sidebar in order', async () => { vi.mocked(prisma.employee.findUniqueOrThrow).mockResolvedValue({ id: 'employee_1', role: 'OWNER', @@ -367,6 +367,7 @@ describe('menu.service', () => { expect(result.items.map((menuItem) => menuItem.systemKey)).toEqual([ 'dashboard', 'reservations', + 'contracts', 'fleet', 'customers', 'reports', diff --git a/apps/api/src/modules/menu/menu.service.ts b/apps/api/src/modules/menu/menu.service.ts index af52fd5..d9289c9 100644 --- a/apps/api/src/modules/menu/menu.service.ts +++ b/apps/api/src/modules/menu/menu.service.ts @@ -73,11 +73,12 @@ const ROLE_RANK: Record = { OWNER: 3, MANAGER: 2, AGENT: 1 const BASELINE_EMPLOYEE_MENU_ITEMS = [ { systemKey: 'dashboard', label: 'Dashboard', routeOrUrl: '/', icon: 'LayoutDashboard', displayOrder: 10, minRole: 'AGENT' }, { systemKey: 'reservations', label: 'Reservations', routeOrUrl: '/reservations', icon: 'Calendar', displayOrder: 20, minRole: 'AGENT' }, - { systemKey: 'fleet', label: 'Fleet', routeOrUrl: '/fleet', icon: 'Car', displayOrder: 30, minRole: 'AGENT' }, - { systemKey: 'customers', label: 'Customers', routeOrUrl: '/customers', icon: 'Users', displayOrder: 40, minRole: 'AGENT' }, - { systemKey: 'reports', label: 'Reports', routeOrUrl: '/reports', icon: 'BarChart2', displayOrder: 50, minRole: 'MANAGER' }, - { systemKey: 'billing', label: 'Billing', routeOrUrl: '/billing', icon: 'CreditCard', displayOrder: 60, minRole: 'MANAGER' }, - { systemKey: 'settings', label: 'Settings', routeOrUrl: '/settings', icon: 'Settings', displayOrder: 70, minRole: 'OWNER' }, + { systemKey: 'contracts', label: 'Contracts', routeOrUrl: '/contracts', icon: 'FileText', displayOrder: 30, minRole: 'AGENT' }, + { systemKey: 'fleet', label: 'Fleet', routeOrUrl: '/fleet', icon: 'Car', displayOrder: 40, minRole: 'AGENT' }, + { systemKey: 'customers', label: 'Customers', routeOrUrl: '/customers', icon: 'Users', displayOrder: 50, minRole: 'AGENT' }, + { systemKey: 'reports', label: 'Reports', routeOrUrl: '/reports', icon: 'BarChart2', displayOrder: 60, minRole: 'MANAGER' }, + { systemKey: 'billing', label: 'Billing', routeOrUrl: '/billing', icon: 'CreditCard', displayOrder: 70, minRole: 'MANAGER' }, + { systemKey: 'settings', label: 'Settings', routeOrUrl: '/settings', icon: 'Settings', displayOrder: 80, minRole: 'OWNER' }, ] as const const MENU_RECOVERY_ROUTES = new Set(['/subscription', '/subscription/success', '/subscription/cancel']) diff --git a/production/.env.docker.production.example b/production/.env.docker.production.example index ecd54f5..5c6e46b 100644 --- a/production/.env.docker.production.example +++ b/production/.env.docker.production.example @@ -84,3 +84,8 @@ MAIL_FROM_ADDRESS=rentaldrivego@gmail.com MAIL_FROM_NAME=RentalDriveGo MAIL_REPLY_TO_ADDRESS=rentaldrivego@gmail.com MAIL_REPLY_TO_NAME=RentalDriveGo + +# Stripe subscription checkout +# Prefer a restricted API key (rk_) with the minimum Billing/Checkout permissions. +STRIPE_API_KEY=placeholder +STRIPE_WEBHOOK_SECRET=placeholder diff --git a/production/scripts/docker-prod-common.sh b/production/scripts/docker-prod-common.sh index fc2ee84..4d23596 100755 --- a/production/scripts/docker-prod-common.sh +++ b/production/scripts/docker-prod-common.sh @@ -187,7 +187,7 @@ EOF } validate_prod_env_file() { - local acme_email api_domain public_site_domain cors_origins postgres_password redis_password jwt_secret + local acme_email api_domain public_site_domain cors_origins postgres_password redis_password jwt_secret stripe_api_key stripe_webhook_secret acme_email="$(read_env_value ACME_EMAIL)" api_domain="$(read_env_value API_DOMAIN)" @@ -196,6 +196,8 @@ validate_prod_env_file() { postgres_password="$(read_env_value POSTGRES_PASSWORD)" redis_password="$(read_env_value REDIS_PASSWORD)" jwt_secret="$(read_env_value JWT_SECRET)" + stripe_api_key="$(read_env_value STRIPE_API_KEY)" + stripe_webhook_secret="$(read_env_value STRIPE_WEBHOOK_SECRET)" if [[ -z "${acme_email}" || "${acme_email}" != *@* || "${acme_email}" == *example.com* ]]; then echo "Invalid ACME_EMAIL in ${ENV_FILE}. Set ACME_EMAIL=${DEFAULT_ACME_EMAIL} for Let's Encrypt." >&2 @@ -226,6 +228,25 @@ validate_prod_env_file() { fi done + if [[ -z "${stripe_api_key}" || "${stripe_api_key}" == placeholder || "${stripe_api_key}" == replace-with-* || "${stripe_api_key}" == *changeme* || "${stripe_api_key}" == *change-me* ]]; then + echo "Invalid STRIPE_API_KEY in ${ENV_FILE}. Set a real Stripe secret or restricted API key for production billing." >&2 + exit 1 + fi + + if [[ "${stripe_api_key}" != sk_* && "${stripe_api_key}" != rk_* ]]; then + echo "Invalid STRIPE_API_KEY in ${ENV_FILE}. It must start with sk_ or rk_." >&2 + exit 1 + fi + + if [[ -z "${stripe_webhook_secret}" || "${stripe_webhook_secret}" == placeholder || "${stripe_webhook_secret}" == replace-with-* || "${stripe_webhook_secret}" == *changeme* || "${stripe_webhook_secret}" == *change-me* ]]; then + echo "Invalid STRIPE_WEBHOOK_SECRET in ${ENV_FILE}. Set the Stripe webhook signing secret for /api/v1/subscriptions/webhooks/stripe." >&2 + exit 1 + fi + + if [[ "${stripe_webhook_secret}" != whsec_* ]]; then + echo "Invalid STRIPE_WEBHOOK_SECRET in ${ENV_FILE}. It must start with whsec_." >&2 + exit 1 + fi } diff --git a/scripts/docker-prod-common.sh b/scripts/docker-prod-common.sh index 1d980fe..93f8093 100644 --- a/scripts/docker-prod-common.sh +++ b/scripts/docker-prod-common.sh @@ -181,7 +181,7 @@ EOF } validate_prod_env_file() { - local acme_email api_domain public_site_domain cors_origins portainer_domain postgres_password redis_password jwt_secret + local acme_email api_domain public_site_domain cors_origins portainer_domain postgres_password redis_password jwt_secret stripe_api_key stripe_webhook_secret acme_email="$(read_env_value ACME_EMAIL)" api_domain="$(read_env_value API_DOMAIN)" @@ -191,6 +191,8 @@ validate_prod_env_file() { postgres_password="$(read_env_value POSTGRES_PASSWORD)" redis_password="$(read_env_value REDIS_PASSWORD)" jwt_secret="$(read_env_value JWT_SECRET)" + stripe_api_key="$(read_env_value STRIPE_API_KEY)" + stripe_webhook_secret="$(read_env_value STRIPE_WEBHOOK_SECRET)" if [[ -z "${acme_email}" || "${acme_email}" != *@* || "${acme_email}" == *example.com* ]]; then echo "Invalid ACME_EMAIL in ${ENV_FILE}. Set it to a real contact email for Let's Encrypt, not example.com." >&2 @@ -221,6 +223,25 @@ validate_prod_env_file() { fi done + if [[ -z "${stripe_api_key}" || "${stripe_api_key}" == placeholder || "${stripe_api_key}" == replace-with-* || "${stripe_api_key}" == *changeme* || "${stripe_api_key}" == *change-me* ]]; then + echo "Invalid STRIPE_API_KEY in ${ENV_FILE}. Set a real Stripe secret or restricted API key for production billing." >&2 + exit 1 + fi + + if [[ "${stripe_api_key}" != sk_* && "${stripe_api_key}" != rk_* ]]; then + echo "Invalid STRIPE_API_KEY in ${ENV_FILE}. It must start with sk_ or rk_." >&2 + exit 1 + fi + + if [[ -z "${stripe_webhook_secret}" || "${stripe_webhook_secret}" == placeholder || "${stripe_webhook_secret}" == replace-with-* || "${stripe_webhook_secret}" == *changeme* || "${stripe_webhook_secret}" == *change-me* ]]; then + echo "Invalid STRIPE_WEBHOOK_SECRET in ${ENV_FILE}. Set the Stripe webhook signing secret for /api/v1/subscriptions/webhooks/stripe." >&2 + exit 1 + fi + + if [[ "${stripe_webhook_secret}" != whsec_* ]]; then + echo "Invalid STRIPE_WEBHOOK_SECRET in ${ENV_FILE}. It must start with whsec_." >&2 + exit 1 + fi if [[ -n "${portainer_domain}" && "${portainer_domain}" == *example.com* ]]; then echo "Invalid PORTAINER_DOMAIN in ${ENV_FILE}. Set a real hostname or disable the portainer router." >&2