diff --git a/api/.env.docker b/api/.env.docker index 235972f4d..96783afef 100644 --- a/api/.env.docker +++ b/api/.env.docker @@ -9,6 +9,11 @@ USE_HTTP_LOGGER=true # Defaults to "false" for self-hosted deployments (no limits). ENTITLEMENT_ENABLED=false +# Set to "true" to enable the per-plan message send/receive quota (e.g. 200 +# messages/month on the free plan) and the "Upgrade to send more messages" +# paywall. Defaults to "false" for self-hosted deployments (no limits). +BILLING_ENABLED=false + EVENTS_QUEUE_TYPE=emulator EVENTS_QUEUE_NAME=events-local EVENTS_QUEUE_ENDPOINT=http://localhost:8000/v1/events diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index 8d9e778d4..6b2920de1 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -999,6 +999,7 @@ func (container *Container) BillingService() (service *services.BillingService) return services.NewBillingService( container.Logger(), container.Tracer(), + os.Getenv("BILLING_ENABLED") == "true", container.InMemoryCache(), container.Mailer(), container.UserEmailFactory(), diff --git a/api/pkg/services/billing_service.go b/api/pkg/services/billing_service.go index e94002d1d..41f05e036 100644 --- a/api/pkg/services/billing_service.go +++ b/api/pkg/services/billing_service.go @@ -20,6 +20,7 @@ type BillingService struct { service logger telemetry.Logger tracer telemetry.Tracer + enabled bool cache cache.Cache emailFactory emails.UserEmailFactory mailer emails.Mailer @@ -28,9 +29,11 @@ type BillingService struct { } // NewBillingService creates a new BillingService +// The enabled flag should come from the BILLING_ENABLED environment variable. func NewBillingService( logger telemetry.Logger, tracer telemetry.Tracer, + enabled bool, cache cache.Cache, mailer emails.Mailer, emailFactory emails.UserEmailFactory, @@ -40,6 +43,7 @@ func NewBillingService( return &BillingService{ logger: logger.WithService(fmt.Sprintf("%T", s)), tracer: tracer, + enabled: enabled, cache: cache, emailFactory: emailFactory, mailer: mailer, @@ -53,6 +57,10 @@ func (service *BillingService) IsEntitledWithCount(ctx context.Context, userID e ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger) defer span.End() + if !service.enabled { + return nil + } + user, err := service.userRepository.Load(ctx, userID) if err != nil { ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user with ID [%s], entitlement successful", userID))) diff --git a/docker-compose.yml b/docker-compose.yml index ef63287d2..aff543bc4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,15 @@ services: postgres: - image: postgres:alpine + image: postgres:16-alpine environment: POSTGRES_DB: httpsms - POSTGRES_PASSWORD: dbpassword - POSTGRES_USER: dbusername + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_USER: ${POSTGRES_USER} volumes: - postgres:/var/lib/postgresql/data - ports: - - "5435:5432" restart: on-failure healthcheck: - test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d httpsms"] interval: 30s timeout: 60s retries: 5 @@ -22,31 +20,79 @@ services: command: redis-server volumes: - redis:/var/lib/redis - ports: - - "6379:6379" restart: on-failure api: build: context: ./api - ports: - - "8000:8000" depends_on: postgres: condition: service_healthy redis: condition: service_started - env_file: - - ./api/.env + restart: on-failure + environment: + SERVICE_FQDN_API_8000: + ENV: local + GCP_PROJECT_ID: ${GCP_PROJECT_ID} + USE_HTTP_LOGGER: "true" + ENTITLEMENT_ENABLED: "false" + EVENTS_QUEUE_TYPE: emulator + EVENTS_QUEUE_NAME: events-local + EVENTS_QUEUE_ENDPOINT: http://localhost:8000/v1/events + EVENTS_QUEUE_USER_API_KEY: ${EVENTS_QUEUE_USER_API_KEY} + EVENTS_QUEUE_USER_ID: ${EVENTS_QUEUE_USER_ID} + FIREBASE_CREDENTIALS: ${FIREBASE_CREDENTIALS} + SMTP_FROM_NAME: httpSMS + SMTP_FROM_EMAIL: ${SMTP_FROM_EMAIL} + SMTP_USERNAME: ${SMTP_USERNAME} + SMTP_PASSWORD: ${SMTP_PASSWORD} + SMTP_HOST: ${SMTP_HOST} + SMTP_PORT: ${SMTP_PORT} + APP_URL: ${SERVICE_URL_WEB} + APP_NAME: httpSMS + APP_PORT: 8000 + SWAGGER_HOST: ${SERVICE_FQDN_API} + DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/httpsms + DATABASE_URL_DEDICATED: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/httpsms + REDIS_URL: redis://@redis:6379 + GCS_BUCKET_NAME: ${GCS_BUCKET_NAME} + AXIOM_TOKEN: ${AXIOM_TOKEN} + AXIOM_DATASET_EVENTS: ${AXIOM_DATASET_EVENTS} + AXIOM_DATASET_METRICS: ${AXIOM_DATASET_METRICS} + PUSHER_APP_ID: ${PUSHER_APP_ID} + PUSHER_KEY: ${PUSHER_KEY} + PUSHER_SECRET: ${PUSHER_SECRET} + PUSHER_CLUSTER: ${PUSHER_CLUSTER} + CLOUDFLARE_TURNSTILE_SECRET_KEY: ${CLOUDFLARE_TURNSTILE_SECRET_KEY} web: build: context: ./web - ports: - - "3000:3000" + args: + API_BASE_URL: ${SERVICE_URL_API} + APP_URL: ${SERVICE_URL_WEB} + APP_NAME: httpSMS + APP_GITHUB_URL: https://github.com/NdoleStudio/httpsms + APP_DOCUMENTATION_URL: https://docs.httpsms.com + APP_DOWNLOAD_URL: https://github.com/NdoleStudio/httpsms/releases/latest/download/HttpSms.apk + APP_ENV: production + CLOUDFLARE_TURNSTILE_SITE_KEY: ${CLOUDFLARE_TURNSTILE_SITE_KEY} + PUSHER_KEY: ${PUSHER_KEY} + PUSHER_CLUSTER: ${PUSHER_CLUSTER} + FIREBASE_API_KEY: ${FIREBASE_API_KEY} + FIREBASE_AUTH_DOMAIN: ${FIREBASE_AUTH_DOMAIN} + FIREBASE_PROJECT_ID: ${FIREBASE_PROJECT_ID} + FIREBASE_STORAGE_BUCKET: ${FIREBASE_STORAGE_BUCKET} + FIREBASE_MESSAGING_SENDER_ID: ${FIREBASE_MESSAGING_SENDER_ID} + FIREBASE_APP_ID: ${FIREBASE_APP_ID} + FIREBASE_MEASUREMENT_ID: ${FIREBASE_MEASUREMENT_ID} depends_on: api: condition: service_started + restart: on-failure + environment: + SERVICE_FQDN_WEB_3000: volumes: redis: diff --git a/web/Dockerfile b/web/Dockerfile index f283fd32a..5bb4455bf 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -3,7 +3,7 @@ FROM node:22-alpine AS build WORKDIR /app -COPY package.json pnpm-lock.yaml ./ +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ # Install pnpm RUN npm install -g pnpm @@ -11,6 +11,45 @@ RUN npm install -g pnpm RUN pnpm install COPY . . + +# These are read by nuxt.config.ts at generate-time and baked into the static +# output (the app ships as a client-rendered SPA), so they must be available +# as build args rather than runtime container env vars. +ARG API_BASE_URL +ARG APP_URL +ARG APP_NAME +ARG APP_GITHUB_URL +ARG APP_DOCUMENTATION_URL +ARG APP_DOWNLOAD_URL +ARG APP_ENV +ARG CLOUDFLARE_TURNSTILE_SITE_KEY +ARG PUSHER_KEY +ARG PUSHER_CLUSTER +ARG FIREBASE_API_KEY +ARG FIREBASE_AUTH_DOMAIN +ARG FIREBASE_PROJECT_ID +ARG FIREBASE_STORAGE_BUCKET +ARG FIREBASE_MESSAGING_SENDER_ID +ARG FIREBASE_APP_ID +ARG FIREBASE_MEASUREMENT_ID +ENV API_BASE_URL=$API_BASE_URL \ + APP_URL=$APP_URL \ + APP_NAME=$APP_NAME \ + APP_GITHUB_URL=$APP_GITHUB_URL \ + APP_DOCUMENTATION_URL=$APP_DOCUMENTATION_URL \ + APP_DOWNLOAD_URL=$APP_DOWNLOAD_URL \ + APP_ENV=$APP_ENV \ + CLOUDFLARE_TURNSTILE_SITE_KEY=$CLOUDFLARE_TURNSTILE_SITE_KEY \ + PUSHER_KEY=$PUSHER_KEY \ + PUSHER_CLUSTER=$PUSHER_CLUSTER \ + FIREBASE_API_KEY=$FIREBASE_API_KEY \ + FIREBASE_AUTH_DOMAIN=$FIREBASE_AUTH_DOMAIN \ + FIREBASE_PROJECT_ID=$FIREBASE_PROJECT_ID \ + FIREBASE_STORAGE_BUCKET=$FIREBASE_STORAGE_BUCKET \ + FIREBASE_MESSAGING_SENDER_ID=$FIREBASE_MESSAGING_SENDER_ID \ + FIREBASE_APP_ID=$FIREBASE_APP_ID \ + FIREBASE_MEASUREMENT_ID=$FIREBASE_MEASUREMENT_ID + RUN pnpm run generate # production stage