Consolidate audit-fixes branch: webhooks, integrations, and deploy hardening
Batch commit of the pending working tree on security/audit-fixes-2026-07. Major areas: - Outbound webhooks / Zapier: schema + signed delivery with retries, public v1 API (REST-hook subscribe/unsubscribe), settings UI, cron drain. - Deploy hardening: email via SMTP2GO (Resend fully removed), verified DB TLS (DATABASE_SSL=require + DATABASE_CA), storage fails loud in production when Spaces is unconfigured instead of silently using ephemeral disk. - Integrations & features (concurrent work): accounting (QuickBooks/Xero), e-signature (DocuSign/Dropbox Sign), PayPal, geocoding/maps, onboarding, expanded legal pages. - DB migrations 0006–0009. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
969d5d4c8a
commit
c9968531e4
+172
@@ -0,0 +1,172 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# DigitalOcean App Platform spec — Property Management Network
|
||||
#
|
||||
# Deploy: doctl apps create --spec .do/app.yaml
|
||||
# Update: doctl apps update <APP_ID> --spec .do/app.yaml
|
||||
#
|
||||
# SOURCE: image-based from DigitalOcean Container Registry (DOCR). The app's git
|
||||
# lives on self-hosted Gitea, which App Platform cannot pull, so we build the
|
||||
# Docker image ourselves and push it to DOCR. See DIGITALOCEAN.md for the full
|
||||
# build/push/deploy walkthrough.
|
||||
#
|
||||
# SECRETS: values marked `type: SECRET` are placeholders — set the real values in
|
||||
# the App Platform dashboard (App → Settings → Environment Variables) or via
|
||||
# `doctl`. Never commit real secrets to this file.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
name: property-management-network
|
||||
region: nyc
|
||||
|
||||
services:
|
||||
- name: web
|
||||
# Pre-built image pushed to DOCR (repository must exist in your registry).
|
||||
image:
|
||||
registry_type: DOCR
|
||||
repository: property-management-network
|
||||
tag: latest
|
||||
deploy_on_push:
|
||||
enabled: true
|
||||
instance_count: 1
|
||||
instance_size_slug: apps-s-1vcpu-1gb
|
||||
http_port: 3000
|
||||
health_check:
|
||||
http_path: /api/health
|
||||
initial_delay_seconds: 20
|
||||
period_seconds: 30
|
||||
timeout_seconds: 5
|
||||
success_threshold: 1
|
||||
failure_threshold: 3
|
||||
envs:
|
||||
# ── App URLs ──────────────────────────────────────────────────────────
|
||||
# ${APP_URL} resolves to the app's public URL at runtime. NOTE: the client
|
||||
# bundle bakes NEXT_PUBLIC_APP_URL at *image build* time (see Dockerfile /
|
||||
# DIGITALOCEAN.md), so build the image with the same URL you serve on.
|
||||
- key: NEXT_PUBLIC_APP_URL
|
||||
scope: RUN_TIME
|
||||
value: ${APP_URL}
|
||||
- key: BETTER_AUTH_URL
|
||||
scope: RUN_TIME
|
||||
value: ${APP_URL}
|
||||
- key: NEXT_PUBLIC_APP_NAME
|
||||
scope: RUN_TIME
|
||||
value: Property Management Network
|
||||
|
||||
# ── Database (managed Postgres — use the PRIVATE host; see DIGITALOCEAN.md) ──
|
||||
- key: DATABASE_URL
|
||||
scope: RUN_TIME
|
||||
type: SECRET
|
||||
value: REPLACE_IN_DASHBOARD
|
||||
# Verified TLS (encrypted + certificate-checked). DO Managed Postgres uses
|
||||
# a CA that isn't in the system trust store, so paste the cluster's CA cert
|
||||
# into DATABASE_CA: DO control panel → Database → Connection Details →
|
||||
# "Download CA certificate", then paste its PEM contents as the DATABASE_CA
|
||||
# secret in the App Platform dashboard. Without a valid CA the app will
|
||||
# refuse to connect (fail loud) rather than run unverified.
|
||||
# Emergency fallback ONLY (not for production): DATABASE_SSL=no-verify is
|
||||
# encrypted but does NOT verify the server certificate.
|
||||
- key: DATABASE_SSL
|
||||
scope: RUN_TIME
|
||||
value: require
|
||||
- key: DATABASE_CA
|
||||
scope: RUN_TIME
|
||||
type: SECRET
|
||||
value: REPLACE_IN_DASHBOARD
|
||||
# Schema is migrated out-of-band (as doadmin), NOT on boot — the app user
|
||||
# intentionally lacks DDL rights. Keep this false; run migrations manually.
|
||||
- key: RUN_MIGRATIONS_ON_START
|
||||
scope: RUN_TIME
|
||||
value: "false"
|
||||
|
||||
# ── Better Auth ───────────────────────────────────────────────────────
|
||||
- key: BETTER_AUTH_SECRET
|
||||
scope: RUN_TIME
|
||||
type: SECRET
|
||||
value: REPLACE_IN_DASHBOARD
|
||||
- key: GOOGLE_CLIENT_ID
|
||||
scope: RUN_TIME
|
||||
type: SECRET
|
||||
value: REPLACE_IN_DASHBOARD
|
||||
- key: GOOGLE_CLIENT_SECRET
|
||||
scope: RUN_TIME
|
||||
type: SECRET
|
||||
value: REPLACE_IN_DASHBOARD
|
||||
|
||||
# ── Stripe ────────────────────────────────────────────────────────────
|
||||
- key: STRIPE_SECRET_KEY
|
||||
scope: RUN_TIME
|
||||
type: SECRET
|
||||
value: REPLACE_IN_DASHBOARD
|
||||
- key: STRIPE_WEBHOOK_SECRET
|
||||
scope: RUN_TIME
|
||||
type: SECRET
|
||||
value: REPLACE_IN_DASHBOARD
|
||||
# No STRIPE_*_PRICE_ID vars — prices are resolved by lookup key and
|
||||
# auto-created on first checkout (lib/stripe/prices.ts). Going live only
|
||||
# needs the two live secrets above + the live publishable key below.
|
||||
|
||||
# ── OpenAI ────────────────────────────────────────────────────────────
|
||||
- key: OPENAI_API_KEY
|
||||
scope: RUN_TIME
|
||||
type: SECRET
|
||||
value: REPLACE_IN_DASHBOARD
|
||||
|
||||
# ── Email (SMTP — SMTP2GO) ────────────────────────────────────────────
|
||||
# The app sends mail via SMTP only (nodemailer). Email is silently skipped
|
||||
# unless SMTP_HOST + SMTP_USER + SMTP_PASS are all set — password resets,
|
||||
# email verification, rent/overdue/lease reminders, team invites, and
|
||||
# payment links all depend on this. EMAIL_FROM is a bare address; the app
|
||||
# wraps it as "Property Management Network <…>".
|
||||
- key: SMTP_HOST
|
||||
scope: RUN_TIME
|
||||
value: mail.smtp2go.com
|
||||
- key: SMTP_PORT
|
||||
scope: RUN_TIME
|
||||
value: "2525"
|
||||
- key: SMTP_USER
|
||||
scope: RUN_TIME
|
||||
type: SECRET
|
||||
value: REPLACE_IN_DASHBOARD
|
||||
- key: SMTP_PASS
|
||||
scope: RUN_TIME
|
||||
type: SECRET
|
||||
value: REPLACE_IN_DASHBOARD
|
||||
- key: EMAIL_FROM
|
||||
scope: RUN_TIME
|
||||
value: postmaster@propertymanagement.network
|
||||
|
||||
# ── Cloudflare Turnstile (site key is public; baked into the client bundle
|
||||
# at image build time — keep it in sync when you build) ──
|
||||
- key: NEXT_PUBLIC_TURNSTILE_SITE_KEY
|
||||
scope: RUN_TIME
|
||||
value: 0x4AAAAAADuDQverznfv1a60
|
||||
- key: TURNSTILE_SECRET_KEY
|
||||
scope: RUN_TIME
|
||||
type: SECRET
|
||||
value: REPLACE_IN_DASHBOARD
|
||||
|
||||
# ── Object storage (DigitalOcean Spaces + CDN) ────────────────────────
|
||||
- key: SPACES_KEY
|
||||
scope: RUN_TIME
|
||||
type: SECRET
|
||||
value: REPLACE_IN_DASHBOARD
|
||||
- key: SPACES_SECRET
|
||||
scope: RUN_TIME
|
||||
type: SECRET
|
||||
value: REPLACE_IN_DASHBOARD
|
||||
- key: SPACES_REGION
|
||||
scope: RUN_TIME
|
||||
value: nyc3
|
||||
- key: SPACES_BUCKET
|
||||
scope: RUN_TIME
|
||||
value: property-management-network
|
||||
- key: SPACES_ENDPOINT
|
||||
scope: RUN_TIME
|
||||
value: https://nyc3.digitaloceanspaces.com
|
||||
- key: SPACES_CDN_ENDPOINT
|
||||
scope: RUN_TIME
|
||||
value: https://nyc3.cdn.digitaloceanspaces.com
|
||||
|
||||
# ── Cron (Bearer token the DO Function sends to /api/cron/*) ──
|
||||
- key: CRON_SECRET
|
||||
scope: RUN_TIME
|
||||
type: SECRET
|
||||
value: REPLACE_IN_DASHBOARD
|
||||
Reference in New Issue
Block a user