diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..beed25a2 --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +web: bin/scalingo_run_web +postdeploy: python manage.py migrate diff --git a/bin/scalingo_postcompile b/bin/scalingo_postcompile new file mode 100644 index 00000000..425f8c02 --- /dev/null +++ b/bin/scalingo_postcompile @@ -0,0 +1,11 @@ +#!/bin/bash + +set -o errexit # always exit on error +set -o pipefail # don't ignore exit codes when piping output + +echo "-----> Running post-compile script" + +# Remove all the files we don't need +rm -rf src docker env.d .cursor .github compose.yaml README.md .cache + +chmod +x bin/scalingo_run_web diff --git a/bin/scalingo_postfrontend b/bin/scalingo_postfrontend new file mode 100644 index 00000000..d30d3b3b --- /dev/null +++ b/bin/scalingo_postfrontend @@ -0,0 +1,15 @@ +#!/bin/bash + +set -o errexit # always exit on error +set -o pipefail # don't ignore exit codes when piping output + +echo "-----> Running post-frontend script" + +# Move the frontend build to the nginx root and clean up +mkdir -p build/ +mv src/frontend/apps/drive/out build/frontend-out + +mv src/backend/* ./ +mv src/nginx/* ./ + +echo "3.13" > .python-version diff --git a/bin/scalingo_run_web b/bin/scalingo_run_web new file mode 100644 index 00000000..144d4df9 --- /dev/null +++ b/bin/scalingo_run_web @@ -0,0 +1,15 @@ +#!/bin/bash + +# Start the Django backend +gunicorn -b :8000 drive.wsgi:application --log-file - & + +# Start the Nginx server +bin/run & + +# if the current shell is killed, also terminate all its children +trap "pkill SIGTERM -P $$" SIGTERM + +# wait for a single child to finish, +wait -n +# then kill all the other tasks +pkill -P $$ diff --git a/docker/files/production/etc/nginx/conf.d/default.conf b/docker/files/production/etc/nginx/conf.d/default.conf index 8ee996ee..64c7b7f6 100644 --- a/docker/files/production/etc/nginx/conf.d/default.conf +++ b/docker/files/production/etc/nginx/conf.d/default.conf @@ -65,7 +65,7 @@ server { } location /media-auth { - proxy_pass http://docs_backend/api/v1.0/documents/media-auth/; + proxy_pass http://docs_backend/api/v1.0/items/media-auth/; proxy_set_header X-Forwarded-Proto https; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; diff --git a/src/backend/drive/settings.py b/src/backend/drive/settings.py index 82f4d31f..1d4ed20b 100755 --- a/src/backend/drive/settings.py +++ b/src/backend/drive/settings.py @@ -16,6 +16,7 @@ from socket import gethostbyname, gethostname from django.utils.translation import gettext_lazy as _ +import dj_database_url import posthog import sentry_sdk from configurations import Configuration, values @@ -23,7 +24,7 @@ from sentry_sdk.integrations.django import DjangoIntegration # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -DATA_DIR = os.path.join("/", "data") +DATA_DIR = os.environ.get("DATA_DIR", os.path.join("/", "data")) def get_release(): @@ -74,7 +75,9 @@ class Base(Configuration): # Database DATABASES = { - "default": { + "default": dj_database_url.config() + if os.environ.get("DATABASE_URL") + else { "ENGINE": values.Value( "django.db.backends.postgresql_psycopg2", environ_name="DB_ENGINE", diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index bf035c4f..11c44458 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -27,6 +27,7 @@ requires-python = ">=3.12" dependencies = [ "boto3==1.36.7", "Brotli==1.1.0", + "dj-database-url==2.3.0", "celery[redis]==5.5.0", "django==5.1.9", "django-configurations==2.5.1", diff --git a/src/frontend/apps/drive/package.json b/src/frontend/apps/drive/package.json index d955819e..a7daae00 100644 --- a/src/frontend/apps/drive/package.json +++ b/src/frontend/apps/drive/package.json @@ -4,11 +4,14 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build", + "build": "next build --no-lint", "start": "next start", "lint": "next lint", "build-theme": "cunningham -g css,scss -o src/styles && mv src/styles/cunningham-tokens.scss src/styles/cunningham-tokens-sass.scss" }, + "engines": { + "node": ">=22.0.0 <23.0.0" + }, "dependencies": { "@gouvfr-lasuite/ui-kit": "0.5.0", "@openfun/cunningham-react": "3.0.0", diff --git a/src/frontend/apps/drive/src/features/explorer/components/Explorer.tsx b/src/frontend/apps/drive/src/features/explorer/components/Explorer.tsx index 11b0950a..edc07b13 100644 --- a/src/frontend/apps/drive/src/features/explorer/components/Explorer.tsx +++ b/src/frontend/apps/drive/src/features/explorer/components/Explorer.tsx @@ -10,8 +10,8 @@ export interface ExplorerProps { childrenItems?: Item[]; gridActionsCell?: (params: ExplorerGridActionsCellProps) => React.ReactNode; disableItemDragAndDrop?: boolean; - gridHeader?: JSX.Element; - selectionBarActions?: JSX.Element; + gridHeader?: React.ReactNode; + selectionBarActions?: React.ReactNode; filters?: ItemFilters; onFiltersChange?: (filters: ItemFilters) => void; // Override the default onNavigate from ExplorerContext diff --git a/src/frontend/package.json b/src/frontend/package.json index 67776fed..8c6ebc60 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -7,11 +7,12 @@ "packages/*" ], "scripts": { - "lint": "yarn workspaces run lint" + "dev": "turbo run dev", + "build": "cd apps/drive && yarn build" }, "resolutions": {}, "devDependencies": { "turbo": "2.0.10" }, "packageManager": "yarn@1.22.22" -} +} \ No newline at end of file diff --git a/src/nginx/servers.conf.erb b/src/nginx/servers.conf.erb new file mode 100644 index 00000000..1ae1fac2 --- /dev/null +++ b/src/nginx/servers.conf.erb @@ -0,0 +1,87 @@ +# ERB templated nginx configuration +# see https://doc.scalingo.com/platform/deployment/buildpacks/nginx + +upstream backend_server { + server localhost:8000 fail_timeout=0; +} + +server { + + listen <%= ENV["PORT"] %>; + server_name _; + + root /app/build/frontend-out; + + error_page 404 /404.html; + + # Django rest framework + location ^~ /api/ { + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_redirect off; + proxy_pass http://backend_server; + } + + # Django admin + location ^~ /admin/ { + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_redirect off; + proxy_pass http://backend_server; + } + + # Proxy auth for media + location /media/ { + # Auth request configuration + auth_request /media-auth; + auth_request_set $authHeader $upstream_http_authorization; + auth_request_set $authDate $upstream_http_x_amz_date; + auth_request_set $authContentSha256 $upstream_http_x_amz_content_sha256; + + # Pass specific headers from the auth response + proxy_set_header Authorization $authHeader; + proxy_set_header X-Amz-Date $authDate; + proxy_set_header X-Amz-Content-SHA256 $authContentSha256; + + # Get resource from Object Storage + proxy_pass <%= ENV["AWS_S3_BUCKET_INTERNAL_URL"] %>; + proxy_set_header Host <%= ENV["AWS_S3_BUCKET_INTERNAL_HOST"] %>; + add_header Content-Security-Policy "default-src 'none'" always; + } + + location /media-auth { + proxy_pass http://backend_server/api/v1.0/items/media-auth/; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Original-URL $request_uri; + + # Prevent the body from being passed + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-Method $request_method; + } + + location ~ "^/explorer/items/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/?$" { + try_files $uri /explorer/items/[id].html; + } + + location ~ "^/sdk/explorer/items/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/?$" { + try_files $uri /sdk/explorer/items/[id].html; + } + + location = /404.html { + internal; + } + + # Frontend export + location / { + try_files $uri index.html $uri/ =404; + } + +}