Compare commits
2 commits
09347b9193
...
860000d0b3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
860000d0b3 | ||
|
|
b1d70f2603 |
6 changed files with 134 additions and 59 deletions
|
|
@ -1,43 +0,0 @@
|
|||
name: Publish Docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
env:
|
||||
REGISTRY: forge.lclr.dev
|
||||
IMAGE_NAME: thibaud-lclr/api-client
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: docker
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Docker CLI
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y docker.io
|
||||
|
||||
- name: Log in to Forgejo registry
|
||||
run: |
|
||||
printf '%s' "${{ secrets.FORGEJO_REGISTRY_TOKEN }}" \
|
||||
| docker login "${REGISTRY}" \
|
||||
--username "${{ secrets.FORGEJO_REGISTRY_USERNAME }}" \
|
||||
--password-stdin
|
||||
|
||||
- name: Build and push image
|
||||
run: |
|
||||
IMAGE_TAG="${REGISTRY}/${IMAGE_NAME}:${{ github.ref_name }}"
|
||||
docker build \
|
||||
--file prod.Dockerfile \
|
||||
--target aio \
|
||||
--label "org.opencontainers.image.source=https://${REGISTRY}/${IMAGE_NAME}" \
|
||||
--label "org.opencontainers.image.revision=${{ github.sha }}" \
|
||||
--label "org.opencontainers.image.version=${{ github.ref_name }}" \
|
||||
--tag "${IMAGE_TAG}" \
|
||||
.
|
||||
docker push "${IMAGE_TAG}"
|
||||
18
README.md
18
README.md
|
|
@ -104,6 +104,24 @@ La prod utilise uniquement `docker-compose.prod.yml`.
|
|||
|
||||
Ce compose ne contient pas de `build:`. Il tire les images du registry.
|
||||
|
||||
Sur Coolify, `docker-compose.prod.yml` est la source de vérité des variables
|
||||
d'environnement. Les variables sont déclarées dans les blocs `environment:` des
|
||||
services pour que Coolify les crée dans son UI au chargement du compose. Il ne
|
||||
faut pas compter sur un fichier `.env` du dépôt en prod : il est ignoré par Git.
|
||||
|
||||
Variables générées/préremplies pour Coolify :
|
||||
|
||||
- `SERVICE_PASSWORD_POSTGRES` : mot de passe PostgreSQL partagé entre la base et le backend
|
||||
- `SERVICE_BASE64_DATA_ENCRYPTION_KEY` : clé stable de 32 caractères pour `DATA_ENCRYPTION_KEY`
|
||||
- `SERVICE_URL_HOPPSCOTCH_APP` : URL publique de l'app
|
||||
- `SERVICE_URL_HOPPSCOTCH_ADMIN` : URL publique de l'admin
|
||||
- `SERVICE_URL_HOPPSCOTCH_BACKEND` : URL publique du backend
|
||||
- `SERVICE_FQDN_HOPPSCOTCH_BACKEND` : domaine du backend, utilisé pour `VITE_BACKEND_WS_URL`
|
||||
|
||||
Après l'import du compose dans Coolify, vérifier au minimum que les trois URLs
|
||||
publiques correspondent aux domaines assignés aux services app, admin et
|
||||
backend.
|
||||
|
||||
Démarrer avec le tag `latest` :
|
||||
|
||||
```sh
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ services:
|
|||
image: postgres:15
|
||||
user: postgres
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-testpass}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-hoppscotch}
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES:-testpass}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-hoppscotch}
|
||||
volumes:
|
||||
- hoppscotch-db:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
|
|
@ -22,19 +22,26 @@ services:
|
|||
container_name: hoppscotch-backend
|
||||
restart: unless-stopped
|
||||
image: ${API_CLIENT_REGISTRY:-forge.lclr.dev}/${API_CLIENT_NAMESPACE:-thibaud-lclr}/${API_CLIENT_IMAGE_PREFIX:-api-client}-backend:${API_CLIENT_TAG:-latest}
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD:-testpass}@hoppscotch-db:5432/${POSTGRES_DB:-hoppscotch}
|
||||
- DATABASE_URL=postgresql://postgres:${SERVICE_PASSWORD_POSTGRES:-testpass}@hoppscotch-db:5432/${POSTGRES_DB:-hoppscotch}
|
||||
- DATA_ENCRYPTION_KEY=${SERVICE_BASE64_DATA_ENCRYPTION_KEY:-0123456789abcdef0123456789abcdef}
|
||||
- VITE_BASE_URL=${SERVICE_URL_HOPPSCOTCH_APP:-http://localhost:3000}
|
||||
- VITE_SHORTCODE_BASE_URL=${SERVICE_URL_HOPPSCOTCH_APP:-http://localhost:3000}
|
||||
- VITE_ADMIN_URL=${SERVICE_URL_HOPPSCOTCH_ADMIN:-http://localhost:3100}
|
||||
- VITE_BACKEND_GQL_URL=${SERVICE_URL_HOPPSCOTCH_BACKEND:-http://localhost:3170}/graphql
|
||||
- VITE_BACKEND_WS_URL=wss://${SERVICE_FQDN_HOPPSCOTCH_BACKEND:-localhost:3170}/graphql
|
||||
- VITE_BACKEND_API_URL=${SERVICE_URL_HOPPSCOTCH_BACKEND:-http://localhost:3170}/v1
|
||||
- VITE_APP_TOS_LINK=${VITE_APP_TOS_LINK:-https://docs.hoppscotch.io/support/terms}
|
||||
- VITE_APP_PRIVACY_POLICY_LINK=${VITE_APP_PRIVACY_POLICY_LINK:-https://docs.hoppscotch.io/support/privacy}
|
||||
- VITE_PROXYSCOTCH_ACCESS_TOKEN=${VITE_PROXYSCOTCH_ACCESS_TOKEN:-}
|
||||
- ENABLE_SUBPATH_BASED_ACCESS=${ENABLE_SUBPATH_BASED_ACCESS:-false}
|
||||
- WHITELISTED_ORIGINS=${SERVICE_URL_HOPPSCOTCH_APP:-http://localhost:3000},${SERVICE_URL_HOPPSCOTCH_ADMIN:-http://localhost:3100}
|
||||
- TRUST_PROXY=${TRUST_PROXY:-true}
|
||||
depends_on:
|
||||
hoppscotch-db:
|
||||
condition: service_healthy
|
||||
command:
|
||||
[
|
||||
"sh",
|
||||
"-c",
|
||||
"pnpm exec prisma migrate deploy && node prod_run.mjs",
|
||||
]
|
||||
["sh", "-c", "pnpm exec prisma migrate deploy && node prod_run.mjs"]
|
||||
ports:
|
||||
- "3170:3170"
|
||||
|
||||
|
|
@ -42,8 +49,17 @@ services:
|
|||
container_name: hoppscotch-app
|
||||
restart: unless-stopped
|
||||
image: ${API_CLIENT_REGISTRY:-forge.lclr.dev}/${API_CLIENT_NAMESPACE:-thibaud-lclr}/${API_CLIENT_IMAGE_PREFIX:-api-client}-app:${API_CLIENT_TAG:-latest}
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- VITE_BASE_URL=${SERVICE_URL_HOPPSCOTCH_APP:-http://localhost:3000}
|
||||
- VITE_SHORTCODE_BASE_URL=${SERVICE_URL_HOPPSCOTCH_APP:-http://localhost:3000}
|
||||
- VITE_ADMIN_URL=${SERVICE_URL_HOPPSCOTCH_ADMIN:-http://localhost:3100}
|
||||
- VITE_BACKEND_GQL_URL=${SERVICE_URL_HOPPSCOTCH_BACKEND:-http://localhost:3170}/graphql
|
||||
- VITE_BACKEND_WS_URL=wss://${SERVICE_FQDN_HOPPSCOTCH_BACKEND:-localhost:3170}/graphql
|
||||
- VITE_BACKEND_API_URL=${SERVICE_URL_HOPPSCOTCH_BACKEND:-http://localhost:3170}/v1
|
||||
- VITE_APP_TOS_LINK=${VITE_APP_TOS_LINK:-https://docs.hoppscotch.io/support/terms}
|
||||
- VITE_APP_PRIVACY_POLICY_LINK=${VITE_APP_PRIVACY_POLICY_LINK:-https://docs.hoppscotch.io/support/privacy}
|
||||
- VITE_PROXYSCOTCH_ACCESS_TOKEN=${VITE_PROXYSCOTCH_ACCESS_TOKEN:-}
|
||||
- ENABLE_SUBPATH_BASED_ACCESS=${ENABLE_SUBPATH_BASED_ACCESS:-false}
|
||||
depends_on:
|
||||
hoppscotch-backend:
|
||||
condition: service_started
|
||||
|
|
@ -55,8 +71,16 @@ services:
|
|||
container_name: hoppscotch-sh-admin
|
||||
restart: unless-stopped
|
||||
image: ${API_CLIENT_REGISTRY:-forge.lclr.dev}/${API_CLIENT_NAMESPACE:-thibaud-lclr}/${API_CLIENT_IMAGE_PREFIX:-api-client}-sh-admin:${API_CLIENT_TAG:-latest}
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- VITE_BASE_URL=${SERVICE_URL_HOPPSCOTCH_APP:-http://localhost:3000}
|
||||
- VITE_SHORTCODE_BASE_URL=${SERVICE_URL_HOPPSCOTCH_APP:-http://localhost:3000}
|
||||
- VITE_ADMIN_URL=${SERVICE_URL_HOPPSCOTCH_ADMIN:-http://localhost:3100}
|
||||
- VITE_BACKEND_GQL_URL=${SERVICE_URL_HOPPSCOTCH_BACKEND:-http://localhost:3170}/graphql
|
||||
- VITE_BACKEND_API_URL=${SERVICE_URL_HOPPSCOTCH_BACKEND:-http://localhost:3170}/v1
|
||||
- VITE_APP_TOS_LINK=${VITE_APP_TOS_LINK:-https://docs.hoppscotch.io/support/terms}
|
||||
- VITE_APP_PRIVACY_POLICY_LINK=${VITE_APP_PRIVACY_POLICY_LINK:-https://docs.hoppscotch.io/support/privacy}
|
||||
- VITE_PROXYSCOTCH_ACCESS_TOKEN=${VITE_PROXYSCOTCH_ACCESS_TOKEN:-}
|
||||
- ENABLE_SUBPATH_BASED_ACCESS=${ENABLE_SUBPATH_BASED_ACCESS:-false}
|
||||
depends_on:
|
||||
hoppscotch-backend:
|
||||
condition: service_started
|
||||
|
|
|
|||
39
packages/hoppscotch-backend/src/cors-config.spec.ts
Normal file
39
packages/hoppscotch-backend/src/cors-config.spec.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { ConfigService } from '@nestjs/config';
|
||||
import { resolveProductionCorsOrigins } from './cors-config';
|
||||
|
||||
describe('resolveProductionCorsOrigins', () => {
|
||||
const configService = (values: Record<string, string | undefined>) =>
|
||||
({
|
||||
get: (key: string) => values[key],
|
||||
}) as ConfigService;
|
||||
|
||||
test('Should use WHITELISTED_ORIGINS when it is configured', () => {
|
||||
expect(
|
||||
resolveProductionCorsOrigins(
|
||||
configService({
|
||||
WHITELISTED_ORIGINS:
|
||||
'https://app.example.test, https://admin.example.test',
|
||||
VITE_BASE_URL: 'https://ignored.example.test',
|
||||
VITE_ADMIN_URL: 'https://ignored-admin.example.test',
|
||||
}),
|
||||
),
|
||||
).toEqual(['https://app.example.test', 'https://admin.example.test']);
|
||||
});
|
||||
|
||||
test('Should fall back to app and admin URLs when WHITELISTED_ORIGINS is missing', () => {
|
||||
expect(
|
||||
resolveProductionCorsOrigins(
|
||||
configService({
|
||||
VITE_BASE_URL: 'https://app.example.test',
|
||||
VITE_ADMIN_URL: 'https://admin.example.test',
|
||||
}),
|
||||
),
|
||||
).toEqual(['https://app.example.test', 'https://admin.example.test']);
|
||||
});
|
||||
|
||||
test('Should explain the missing production CORS configuration', () => {
|
||||
expect(() => resolveProductionCorsOrigins(configService({}))).toThrow(
|
||||
'Missing production CORS configuration. Set WHITELISTED_ORIGINS or configure VITE_BASE_URL/VITE_ADMIN_URL.',
|
||||
);
|
||||
});
|
||||
});
|
||||
36
packages/hoppscotch-backend/src/cors-config.ts
Normal file
36
packages/hoppscotch-backend/src/cors-config.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
const MISSING_PRODUCTION_CORS_CONFIG =
|
||||
'Missing production CORS configuration. Set WHITELISTED_ORIGINS or configure VITE_BASE_URL/VITE_ADMIN_URL.';
|
||||
|
||||
function parseOrigins(origins: string | undefined): string[] {
|
||||
return (
|
||||
origins
|
||||
?.split(',')
|
||||
.map((origin) => origin.trim())
|
||||
.filter(Boolean) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveProductionCorsOrigins(
|
||||
configService: ConfigService,
|
||||
): string[] {
|
||||
const whitelistedOrigins = parseOrigins(
|
||||
configService.get('WHITELISTED_ORIGINS'),
|
||||
);
|
||||
|
||||
if (whitelistedOrigins.length > 0) {
|
||||
return whitelistedOrigins;
|
||||
}
|
||||
|
||||
const derivedOrigins = [
|
||||
configService.get('VITE_BASE_URL'),
|
||||
configService.get('VITE_ADMIN_URL'),
|
||||
].filter(Boolean);
|
||||
|
||||
if (derivedOrigins.length === 0) {
|
||||
throw new Error(MISSING_PRODUCTION_CORS_CONFIG);
|
||||
}
|
||||
|
||||
return derivedOrigins;
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import { ConfigService } from '@nestjs/config';
|
|||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import { InfraTokenModule } from './infra-token/infra-token.module';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { resolveProductionCorsOrigins } from './cors-config';
|
||||
|
||||
function setupSwagger(
|
||||
app: NestExpressApplication,
|
||||
|
|
@ -58,7 +59,7 @@ async function bootstrap() {
|
|||
if (isProduction) {
|
||||
console.log('Enabling CORS with production settings');
|
||||
app.enableCors({
|
||||
origin: configService.get('WHITELISTED_ORIGINS').split(','),
|
||||
origin: resolveProductionCorsOrigins(configService),
|
||||
credentials: true,
|
||||
});
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in a new issue