From c1d0c9cb769a8493b8e6b8620069fb6172ea3219 Mon Sep 17 00:00:00 2001 From: Nathan Levesque Date: Fri, 21 Jun 2024 17:03:17 -0400 Subject: [PATCH] Removed nginx serving of frontend locally --- .env.example | 44 ------------- .github/workflows/ci.yml | 4 +- .github/workflows/production.yml | 2 +- .github/workflows/release-candidate.yml | 2 +- .gitignore | 1 - README.md | 21 ++++-- app.json | 18 +----- channels/models.py | 6 +- channels/serializers_test.py | 7 +- config/nginx.conf | 42 ------------ config/nginx.conf.erb | 38 +---------- config/static-app.conf | 10 +++ docker-compose-e2e-tests.yml | 12 +++- docker-compose.yml | 64 +++++++++++-------- env/.gitignore | 1 + env/backend.env | 32 ++++++++++ env/backend.local.example.env | 22 +++++++ .env.ci => env/ci.env | 3 +- env/frontend.env | 3 + env/frontend.local.example.env | 0 env/shared.env | 5 ++ env/shared.local.example.env | 5 ++ fixtures/common.py | 10 --- frontends/api/src/clients.ts | 2 +- frontends/api/src/generated/v0/api.ts | 12 ---- frontends/api/src/generated/v1/api.ts | 12 ---- frontends/api/src/test-utils/urls.ts | 2 +- frontends/mit-open/jest.config.ts | 2 +- frontends/mit-open/src/common/urls.test.ts | 12 ++-- frontends/mit-open/src/common/urls.ts | 4 +- frontends/mit-open/webpack.config.js | 14 ++-- learning_resources/etl/constants.py | 2 +- learning_resources/etl/podcast.py | 9 +-- learning_resources/etl/podcast_test.py | 8 +-- learning_resources/serializers_test.py | 22 +++---- .../indexing_api_test.py | 2 - learning_resources_search/tasks.py | 4 +- main/settings.py | 20 +++--- main/settings_test.py | 3 +- main/urls.py | 2 + main/utils.py | 14 ++++ main/utils_test.py | 12 ++++ nginx/20-compile-nginx-conf-erb.sh | 18 ++++++ nginx/Dockerfile | 22 +++++++ openapi/specs/v0.yaml | 8 --- openapi/specs/v1.yaml | 8 --- package.json | 4 +- profiles/models.py | 3 +- profiles/serializers.py | 26 -------- profiles/urls.py | 6 -- profiles/utils.py | 7 +- profiles/views.py | 27 +------- profiles/views_test.py | 43 +------------ 53 files changed, 276 insertions(+), 406 deletions(-) delete mode 100644 .env.example delete mode 100644 config/nginx.conf create mode 100644 config/static-app.conf create mode 100644 env/.gitignore create mode 100644 env/backend.env create mode 100644 env/backend.local.example.env rename .env.ci => env/ci.env (90%) create mode 100644 env/frontend.env create mode 100644 env/frontend.local.example.env create mode 100644 env/shared.env create mode 100644 env/shared.local.example.env create mode 100755 nginx/20-compile-nginx-conf-erb.sh create mode 100644 nginx/Dockerfile diff --git a/.env.example b/.env.example deleted file mode 100644 index 7423fece5b..0000000000 --- a/.env.example +++ /dev/null @@ -1,44 +0,0 @@ -COMPOSE_PROFILES=backend,frontend - -CELERY_TASK_ALWAYS_EAGER=True -DJANGO_LOG_LEVEL=INFO -LOG_LEVEL=info -MITOPEN_BASE_URL=http://od.odl.local:8063 -MITOPEN_COOKIE_NAME=discussions -MITOPEN_COOKIE_DOMAIN=odl.local -MITOPEN_JWT_SECRET= -MITOPEN_USE_S3=False -MITOPEN_AXIOS_WITH_CREDENTIALS=False -MITOPEN_AXIOS_BASE_PATH="" -INDEXING_API_USERNAME=mitodl -MAILGUN_SENDER_DOMAIN= -MAILGUN_KEY= -MAILGUN_RECIPIENT_OVERRIDE= -STATUS_TOKEN= -OPENSEARCH_INDEX=discussions_local -OPENSEARCH_INDEXING_CHUNK_SIZE=100 -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_STORAGE_BUCKET_NAME= -SOCIAL_AUTH_MICROMASTERS_LOGIN_URL= -YOUTUBE_DEVELOPER_KEY= -TIKA_SERVER_ENDPOINT=http://tika:9998/ -TIKA_CLIENT_ONLY=True -WEBPACK_ANALYZE=False - -SOCIAL_AUTH_OL_OIDC_OIDC_ENDPOINT= -SOCIAL_AUTH_OL_OIDC_KEY= -SOCIAL_AUTH_OL_OIDC_SECRET= -AUTHORIZATION_URL= -ACCESS_TOKEN_URL= -USERINFO_URL= -KEYCLOAK_BASE_URL= -KEYCLOAK_REALM_NAME= - -POSTHOG_PROJECT_ID= -POSTHOG_PROJECT_API_KEY= -POSTHOG_PERSONAL_API_KEY= -POSTHOG_HOST=https://app.posthog.com -POSTHOG_TIMEOUT_MS=1500 - -MITOPEN_SUPPORT_EMAIL=mitopen-support@mit.edu diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af12b53bc5..9db50ba1c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,7 +85,7 @@ jobs: CELERY_BROKER_URL: redis://localhost:6379/4 CELERY_RESULT_BACKEND: redis://localhost:6379/4 TIKA_CLIENT_ONLY: "True" - MITOPEN_BASE_URL: http://localhost:8063/ + MITOPEN_APP_BASE_URL: http://localhost:8062/ MAILGUN_KEY: fake_mailgun_key MAILGUN_SENDER_DOMAIN: other.fake.site OPENSEARCH_INDEX: testindex @@ -123,7 +123,7 @@ jobs: - name: Webpack build run: yarn run build env: - MITOPEN_AXIOS_BASE_PATH: https://api.mitopen-test.odl.mit.edu + MITOPEN_API_BASE_URL: https://api.mitopen-test.odl.mit.edu - name: Lints run: yarn run lint-check diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml index 94dbecd4e8..fbc1ca38c5 100644 --- a/.github/workflows/production.yml +++ b/.github/workflows/production.yml @@ -41,7 +41,7 @@ jobs: POSTHOG_PROJECT_ID: ${{ secrets.POSTHOG_PROJECT_ID_PROD }} POSTHOG_PROJECT_API_KEY: ${{ secrets.POSTHOG_PROJECT_API_KEY_PROD }} MITOPEN_AXIOS_WITH_CREDENTIALS: true - MITOPEN_AXIOS_BASE_PATH: https://api.mitopen.odl.mit.edu + MITOPEN_API_BASE_URL: https://api.mitopen.odl.mit.edu MITOPEN_SUPPORT_EMAIL: mitopen-support@mit.edu - uses: akhileshns/heroku-deploy@581dd286c962b6972d427fcf8980f60755c15520 diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 2bf3f66cb2..4d0454255f 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -41,7 +41,7 @@ jobs: POSTHOG_PROJECT_ID: ${{ secrets.POSTHOG_PROJECT_ID_RC }} POSTHOG_PROJECT_API_KEY: ${{ secrets.POSTHOG_PROJECT_API_KEY_RC }} MITOPEN_AXIOS_WITH_CREDENTIALS: true - MITOPEN_AXIOS_BASE_PATH: https://api.mitopen-rc.odl.mit.edu + MITOPEN_API_BASE_URL: https://api.mitopen-rc.odl.mit.edu MITOPEN_SUPPORT_EMAIL: odl-mitopen-rc-support@mit.edu - uses: akhileshns/heroku-deploy@581dd286c962b6972d427fcf8980f60755c15520 diff --git a/.gitignore b/.gitignore index d82f14b80e..05e6d520b2 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ __pycache__/ # Distribution / packaging .Python -env/ build/ build-exports/ develop-eggs/ diff --git a/README.md b/README.md index 8740c88be8..b03fb7bbd7 100644 --- a/README.md +++ b/README.md @@ -17,18 +17,25 @@ MIT Open follows the same [initial setup steps outlined in the common OL web app Run through those steps **including the addition of `/etc/hosts` aliases and the optional step for running the `createsuperuser` command**. -### Configure required `.env` settings +### Configuration + +Configuration can be put in the following filess which are gitignored: + +``` +mit-open/ + ├── env/ + │ ├── shared.local.env (provided to both frontend and backend containers) + │ ├── frontend.local.env (provided only to frontend containers) + │ └── backend.local.env (provided only to frontend containers) + └── .env (legacy file) +``` The following settings must be configured before running the app: - `COMPOSE_PROFILES` Controls which docker containers run. To run them all, use `COMPOSE_PROFILES=backend,frontend`. See [Frontend Development](./frontends/README.md) for more. - -- `INDEXING_API_USERNAME` - - At least to start out, this should be set to the username of the superuser - you created above. + This can be set either in a top-level `.env` that `docker compose` [automatically ingests](https://docs.docker.com/compose/environment-variables/envvars/#compose_env_files) or through any other method of setting an environment variable in your shell (e.g. `direnv`). - `MAILGUN_KEY` and `MAILGUN_SENDER_DOMAIN` @@ -216,7 +223,7 @@ Once these are set (and you've restarted the app), you should see events flowing A Javascript bundle of exported frontend components can be generated for use in external websites that have CORS allowance into a given instance of `mit-open`. There are a few settings you might want to change in order to get the expected results. - `MITOPEN_AXIOS_WITH_CREDENTIALS` - This sets `withCredentials: true` when initializing the Axios API, which tells the end user's browser to send along any browser level cookies for the current domain when making CORS requests -- `MITOPEN_AXIOS_BASE_PATH` - This sets the base path used for API requests, which will need to be set to a fully qualified url pointing to an instance of `mit-open` (i.e. https://mitopen.odl.mit.edu) in order for requests from the external site to reach the proper destination +- `MITOPEN_API_BASE_URL` - This sets the base url used for API requests, which will need to be set to a fully qualified url pointing to an instance of `mit-open` (i.e. https://mitopen.odl.mit.edu) in order for requests from the external site to reach the proper destination - `CORS_ALLOWED_ORIGINS`, `CSRF_TRUSTED_ORIGINS` - On the instance of `mit-open` that the externally hosted components will access via the API, the domains of any sites that need CORS access need to be here as a list of strings To build the bundle of exported components, run: diff --git a/app.json b/app.json index a129046b63..d4e43ecf3d 100644 --- a/app.json +++ b/app.json @@ -4,9 +4,6 @@ { "url": "https://github.com/heroku/heroku-buildpack-apt" }, - { - "url": "https://github.com/heroku/heroku-buildpack-nodejs" - }, { "url": "https://github.com/moneymeets/python-poetry-buildpack" }, @@ -172,9 +169,6 @@ "description": "Prefix path for cached images generated by imagekit", "required": false }, - "INDEXING_API_USERNAME": { - "description": "Username used for indexing" - }, "INDEXING_ERROR_RETRIES": { "description": "Number of times to retry an indexing operation on failure", "required": false @@ -298,20 +292,12 @@ "description": "List of pluggy plugins to use for authentication", "required": false }, - "MITOPEN_AXIOS_WITH_CREDENTIALS": { - "description": "When building the Axios API, set defaults.withCredentials to this value", - "required": false - }, - "MITOPEN_AXIOS_BASE_PATH": { - "description": "The base path to use when making API requests", - "required": false - }, "MITOPEN_LEARNING_RESOURCES_PLUGINS": { "description": "List of pluggy plugins to use for learning resources", "required": false }, - "MITOPEN_BASE_URL": { - "description": "Base url to link users to in emails" + "MITOPEN_APP_BASE_URL": { + "description": "Base url to create links to the app" }, "MITOPEN_COOKIE_NAME": { "description": "Name of the cookie for the JWT auth token" diff --git a/channels/models.py b/channels/models.py index c56967f5a6..22f7dd2308 100644 --- a/channels/models.py +++ b/channels/models.py @@ -1,8 +1,5 @@ """Models for channels""" -from urllib.parse import urljoin - -from django.conf import settings from django.contrib.auth.models import Group from django.core.validators import RegexValidator from django.db import models @@ -18,6 +15,7 @@ LearningResourceTopic, ) from main.models import TimestampedModel +from main.utils import frontend_absolute_url from profiles.utils import avatar_uri, banner_uri from widgets.models import WidgetList @@ -99,7 +97,7 @@ class Meta: @property def channel_url(self) -> str: """Return the channel url""" - return urljoin(settings.SITE_BASE_URL, f"/c/{self.channel_type}/{self.name}/") + return frontend_absolute_url(f"/c/{self.channel_type}/{self.name}/") class ChannelTopicDetail(TimestampedModel): diff --git a/channels/serializers_test.py b/channels/serializers_test.py index 30211e8571..cdd78d1dad 100644 --- a/channels/serializers_test.py +++ b/channels/serializers_test.py @@ -1,10 +1,8 @@ """Tests for channels.serializers""" from types import SimpleNamespace -from urllib.parse import urljoin import pytest -from django.conf import settings from django.core.files.uploadedfile import SimpleUploadedFile from channels.constants import CHANNEL_ROLE_MODERATORS, ChannelType @@ -37,6 +35,7 @@ ) from learning_resources.serializers import LearningResourceOfferorDetailSerializer from main.factories import UserFactory +from main.utils import frontend_absolute_url # pylint:disable=redefined-outer-name pytestmark = pytest.mark.django_db @@ -124,8 +123,8 @@ def test_serialize_channel( # pylint: disable=too-many-arguments "updated_on": mocker.ANY, "created_on": mocker.ANY, "id": channel.id, - "channel_url": urljoin( - settings.SITE_BASE_URL, f"/c/{channel.channel_type}/{channel.name}/" + "channel_url": frontend_absolute_url( + f"/c/{channel.channel_type}/{channel.name}/" ), "lists": [ LearningPathPreviewSerializer(channel_list.channel_list).data diff --git a/config/nginx.conf b/config/nginx.conf deleted file mode 100644 index f6f9bbc25a..0000000000 --- a/config/nginx.conf +++ /dev/null @@ -1,42 +0,0 @@ -# This is the version used in development environments -server { - server_name lemelsonx.mit.edu; - listen 8063; - return 301 https://open.mit.edu/c/lemelsoneducators; -} - -server { - server_name themove.mit.edu; - listen 8063; - return 301 https://open.mit.edu/c/themove; -} - -server { - listen 8063 default_server; - - root /src/frontends/mit-open/build; - - location / { - try_files /static$uri $uri @index; - } - - location ~ ^/program_letter/([0-9]+)/view$ { - try_files /index.html =404; - } - - location @index { - try_files /index.html =404; - } - - location = /.well-known/dnt-policy.txt { - return 204; - } - - location ~ ^/(api|login|complete/ol-oidc|logout|admin|static/admin|static/rest_framework|static/hijack|_/features/|scim/|o/|disconnect/|hijack/|podcasts/rss_feed|__debug__/|media/|profile/|program_letter/([0-9]+)/) { - include uwsgi_params; - uwsgi_pass web:8061; - uwsgi_pass_request_headers on; - uwsgi_pass_request_body on; - client_max_body_size 25M; - } -} diff --git a/config/nginx.conf.erb b/config/nginx.conf.erb index 29a7423481..04eb208e10 100644 --- a/config/nginx.conf.erb +++ b/config/nginx.conf.erb @@ -38,49 +38,15 @@ http { default_type application/octet-stream; sendfile on; - server { - server_name lemelsonx.mit.edu; - listen <%= ENV["PORT"] %>; - return 301 https://open.mit.edu/c/lemelsoneducators; - } - - server { - server_name themove.mit.edu; - listen <%= ENV["PORT"] %>; - return 301 https://open.mit.edu/c/themove; - } - - server { - server_name discussions.odl.mit.edu; - listen <%= ENV["PORT"] %>; - return 301 https://open.mit.edu$request_uri; - } - server { listen <%= ENV["PORT"] %> default_server; server_name _; - root /app; - - location / { - expires max; - try_files /frontends/mit-open/build/static$uri /frontends/mit-open/build$uri @index; - } - - location ~ ^/program_letter/([0-9]+)/view$ { - expires 1m; - try_files /frontends/mit-open/build/index.html =404; - } - - location @index { - expires 1m; - try_files /frontends/mit-open/build/index.html =404; - } location = /.well-known/dnt-policy.txt { return 204; } - location ~ ^/(api|login|complete/ol-oidc|logout|admin|static/admin|static/rest_framework|static/hijack|_/features/|hijack/|scim/|o/|disconnect/|podcasts/rss_feed|__debug__/|media/|profile/|program_letter/([0-9]+)/) { + location / { uwsgi_param QUERY_STRING $query_string; uwsgi_param REQUEST_METHOD $request_method; uwsgi_param CONTENT_TYPE $content_type; @@ -98,7 +64,7 @@ http { uwsgi_param X-Forwarded-Proto $http_x_forwarded_proto; uwsgi_param X-Forwarded-Port $http_x_forwarded_port; uwsgi_param X-Forwarded-Host $http_x_forwarded_host; - uwsgi_pass unix:/tmp/nginx.socket; + uwsgi_pass <%= ENV["NGINX_UWSGI_PASS"] || "unix:/tmp/nginx.socket" %>; uwsgi_pass_request_headers on; uwsgi_pass_request_body on; client_max_body_size 25M; diff --git a/config/static-app.conf b/config/static-app.conf new file mode 100644 index 0000000000..5a7d8c47d7 --- /dev/null +++ b/config/static-app.conf @@ -0,0 +1,10 @@ +# This is the version used ONLY for e2e tests because we statically compile to production mode +server { + listen 8063 $APP_BASE_URL; + root /src/frontends/mit-open/build; + + location / { + try_files /index.html =404; + } + +} diff --git a/docker-compose-e2e-tests.yml b/docker-compose-e2e-tests.yml index 826998eec9..56c922aa03 100644 --- a/docker-compose-e2e-tests.yml +++ b/docker-compose-e2e-tests.yml @@ -23,7 +23,15 @@ services: - web volumes: - ./config/nginx.conf:/etc/nginx/conf.d/web.conf + - ./config/static-app.conf:/etc/nginx/templates/static-app.template - ./frontends/mit-open/build:/src/frontends/mit-open/build + env_file: + - path: ./env/shared.env + networks: + default: + aliases: + - "open.odl.local" + - "api.open.odl.local" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8063"] interval: 30s @@ -48,7 +56,7 @@ services: environment: DATABASE_URL: postgres://postgres:postgres@db:5432/e2e_postgres # pragma: allowlist secret PORT: 8061 - env_file: .env.ci + env_file: env/ci.env depends_on: db: condition: service_healthy @@ -72,7 +80,7 @@ services: context: e2e_testing environment: - CI=true - - BASE_URL=http://nginx:8063 + - BASE_URL=http://open.odl.local:8063 depends_on: nginx: condition: service_healthy diff --git a/docker-compose.yml b/docker-compose.yml index ea06411db5..93c84fca44 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,4 @@ x-environment: &py-environment - DEBUG: ${DEBUG:-True} - DEV_ENV: "True" # necessary to have nginx connect to web container - NODE_ENV: ${NODE_ENV:-development} - DATABASE_URL: postgres://postgres:postgres@db:5432/postgres - MITOPEN_SECURE_SSL_REDIRECT: "False" - MITOPEN_DB_DISABLE_SSL: "True" - MITOPEN_FEATURES_DEFAULT: ${MITOPEN_FEATURES_DEFAULT:-True} - OPENSEARCH_URL: opensearch-node-mitopen:9200 - CELERY_TASK_ALWAYS_EAGER: "False" - CELERY_BROKER_URL: redis://redis:6379/4 - CELERY_RESULT_BACKEND: redis://redis:6379/4 - TIKA_SERVER_ENDPOINT: ${TIKA_SERVER_ENDPOINT:-http://tika:9998/} - TIKA_CLIENT_ONLY: "True" services: db: @@ -60,14 +47,18 @@ services: nginx: profiles: - backend - image: nginx:1.27.0 + build: + context: ./nginx ports: - "8063:8063" links: - web + environment: + PORT: 8063 + NGINX_WORKERS: 1 + NGINX_UWSGI_PASS: "web:8061" volumes: - - ./config/nginx.conf:/etc/nginx/conf.d/web.conf - - ./frontends/mit-open/build:/src/frontends/mit-open/build + - ./config:/etc/nginx/templates web: profiles: @@ -75,10 +66,16 @@ services: build: context: . dockerfile: Dockerfile - environment: - <<: *py-environment - PORT: 8061 - env_file: .env + env_file: + - path: env/shared.env + - path: env/shared.local.env + required: false + - path: env/backend.env + - path: env/backend.local.env + required: false + # DEPRECATED: legacy .env file at the repo root + - path: .env + required: false command: ./scripts/run-django-dev.sh stdin_open: true tty: true @@ -103,11 +100,16 @@ services: yarn install --immutable yarn workspace mit-open storybook & yarn workspace mit-open watch:docker - environment: - NODE_ENV: ${NODE_ENV:-development} - PORT: 8062 - MITOPEN_AXIOS_BASE_PATH: ${MITOPEN_BASE_URL} - env_file: .env + env_file: + - path: env/shared.env + - path: env/shared.local.env + required: false + - path: env/frontend.env + - path: env/frontend.local.env + required: false + # DEPRECATED: legacy .env file at the repo root + - path: .env + required: false ports: - "8062:8062" - "6006:6006" @@ -120,8 +122,16 @@ services: build: context: . dockerfile: Dockerfile - environment: *py-environment - env_file: .env + env_file: + - path: env/shared.env + - path: env/shared.local.env + required: false + - path: env/backend.env + - path: env/backend.local.env + required: false + # DEPRECATED: legacy .env file at the repo root + - path: .env + required: false command: > /bin/bash -c ' sleep 3; diff --git a/env/.gitignore b/env/.gitignore new file mode 100644 index 0000000000..dd8d87118d --- /dev/null +++ b/env/.gitignore @@ -0,0 +1 @@ +*.local.env diff --git a/env/backend.env b/env/backend.env new file mode 100644 index 0000000000..0ed73a7b1d --- /dev/null +++ b/env/backend.env @@ -0,0 +1,32 @@ +CELERY_BROKER_URL=redis://redis:6379/4 +CELERY_RESULT_BACKEND=redis://redis:6379/4 +CELERY_TASK_ALWAYS_EAGER=False + +# local hostname shenanigans +CORS_ALLOWED_ORIGINS='["http://open.odl.local:8062"]' +CSRF_TRUSTED_ORIGINS='["http://open.odl.local:8062", "http://api.open.odl.local:8063"]' +CSRF_COOKIE_DOMAIN=open.odl.local +CSRF_COOKIE_SECURE=False +MITOPEN_COOKIE_DOMAIN=open.odl.local +MITOPEN_COOKIE_NAME=discussions + + +DEBUG=True +DJANGO_LOG_LEVEL=INFO +LOG_LEVEL=info +DEV_ENV=true + +DATABASE_URL=postgres://postgres:postgres@db:5432/postgres + +MITOPEN_DB_DISABLE_SSL=True +MITOPEN_FEATURES_DEFAULT=True +MITOPEN_SECURE_SSL_REDIRECT=False + +OPENSEARCH_URL=opensearch-node-mitopen:9200 +OPENSEARCH_INDEX=discussions_local +OPENSEARCH_INDEXING_CHUNK_SIZE=100 + +PORT=8061 + +TIKA_SERVER_ENDPOINT=http://tika:9998/ +TIKA_CLIENT_ONLY=True diff --git a/env/backend.local.example.env b/env/backend.local.example.env new file mode 100644 index 0000000000..3180b3bd70 --- /dev/null +++ b/env/backend.local.example.env @@ -0,0 +1,22 @@ +# MITOPEN_JWT_SECRET= +# MITOPEN_USE_S3=True +# MAILGUN_SENDER_DOMAIN= +# MAILGUN_KEY= +# MAILGUN_RECIPIENT_OVERRIDE= +# STATUS_TOKEN= +# AWS_ACCESS_KEY_ID= +# AWS_SECRET_ACCESS_KEY= +# AWS_STORAGE_BUCKET_NAME= +# YOUTUBE_DEVELOPER_KEY= +# +# SOCIAL_AUTH_OL_OIDC_OIDC_ENDPOINT= +# SOCIAL_AUTH_OL_OIDC_KEY= +# SOCIAL_AUTH_OL_OIDC_SECRET= +# AUTHORIZATION_URL= +# ACCESS_TOKEN_URL= +# USERINFO_URL= +# KEYCLOAK_BASE_URL= +# KEYCLOAK_REALM_NAME= +# +# POSTHOG_PROJECT_ID= +# POSTHOG_PERSONAL_API_KEY= diff --git a/.env.ci b/env/ci.env similarity index 90% rename from .env.ci rename to env/ci.env index 83c2ade6b7..d86ad0ff77 100644 --- a/.env.ci +++ b/env/ci.env @@ -10,10 +10,9 @@ CELERY_TASK_ALWAYS_EAGER=False CELERY_BROKER_URL=redis://redis:6379/4 CELERY_RESULT_BACKEND=redis://redis:6379/4 TIKA_CLIENT_ONLY=True -MITOPEN_BASE_URL=http://localhost:8063/ +MITOPEN_APP_BASE_URL=http://localhost:8063/ MAILGUN_KEY=fake_mailgun_key MAILGUN_SENDER_DOMAIN=other.fake.site OPENSEARCH_INDEX=testindex -INDEXING_API_USERNAME=mitodl MITOPEN_COOKIE_DOMAIN=localhost MITOPEN_COOKIE_NAME=cookie_monster diff --git a/env/frontend.env b/env/frontend.env new file mode 100644 index 0000000000..a6adf9277f --- /dev/null +++ b/env/frontend.env @@ -0,0 +1,3 @@ +NODE_ENV=development +PORT=8062 +MITOPEN_AXIOS_WITH_CREDENTIALS=true diff --git a/env/frontend.local.example.env b/env/frontend.local.example.env new file mode 100644 index 0000000000..e69de29bb2 diff --git a/env/shared.env b/env/shared.env new file mode 100644 index 0000000000..67c604a1df --- /dev/null +++ b/env/shared.env @@ -0,0 +1,5 @@ +MITOPEN_API_BASE_URL=http://api.open.odl.local:8063 +MITOPEN_APP_BASE_URL=http://open.odl.local:8062 +MITOPEN_SUPPORT_EMAIL=support@localhost + +POSTHOG_TIMEOUT_MS=1500 diff --git a/env/shared.local.example.env b/env/shared.local.example.env new file mode 100644 index 0000000000..c18b9bdde5 --- /dev/null +++ b/env/shared.local.example.env @@ -0,0 +1,5 @@ +# MITOPEN_API_BASE_URL=http://api.open.odl.local:8063 +# MITOPEN_APP_BASE_URL=http://open.odl.local:8063 +# POSTHOG_PROJECT_API_KEY= +# POSTHOG_TIMEOUT_MS=1500 +# EMBEDLY_KEY= diff --git a/fixtures/common.py b/fixtures/common.py index fbd6097be8..2166abab96 100644 --- a/fixtures/common.py +++ b/fixtures/common.py @@ -11,8 +11,6 @@ from pytest_mock import PytestMockWarning from urllib3.exceptions import InsecureRequestWarning -from main.factories import UserFactory - @pytest.fixture(autouse=True) def silence_factory_logging(): # noqa: PT004 @@ -80,14 +78,6 @@ def mocked_celery(mocker): ) -@pytest.fixture() -def indexing_user(settings): - """Sets and returns the indexing user""" # noqa: D401 - user = UserFactory.create() - settings.INDEXING_API_USERNAME = user.username - return user - - @pytest.fixture() def mocked_responses(): """Mock responses fixture""" diff --git a/frontends/api/src/clients.ts b/frontends/api/src/clients.ts index e5a3c8249b..b0768a3f5a 100644 --- a/frontends/api/src/clients.ts +++ b/frontends/api/src/clients.ts @@ -24,7 +24,7 @@ import { import axiosInstance from "./axios" -const BASE_PATH = process.env.MITOPEN_AXIOS_BASE_PATH?.replace(/\/+$/, "") ?? "" +const BASE_PATH = process.env.MITOPEN_API_BASE_URL?.replace(/\/+$/, "") ?? "" const learningResourcesApi = new LearningResourcesApi( undefined, diff --git a/frontends/api/src/generated/v0/api.ts b/frontends/api/src/generated/v0/api.ts index 7ac7efb3a2..305da411e6 100644 --- a/frontends/api/src/generated/v0/api.ts +++ b/frontends/api/src/generated/v0/api.ts @@ -2071,18 +2071,6 @@ export interface ProgramCertificate { * @memberof ProgramCertificate */ record_hash: string - /** - * - * @type {string} - * @memberof ProgramCertificate - */ - program_letter_generate_url: string - /** - * - * @type {string} - * @memberof ProgramCertificate - */ - program_letter_share_url: string /** * * @type {string} diff --git a/frontends/api/src/generated/v1/api.ts b/frontends/api/src/generated/v1/api.ts index 273caa2827..73f118a282 100644 --- a/frontends/api/src/generated/v1/api.ts +++ b/frontends/api/src/generated/v1/api.ts @@ -4285,18 +4285,6 @@ export interface ProgramCertificate { * @memberof ProgramCertificate */ record_hash: string - /** - * - * @type {string} - * @memberof ProgramCertificate - */ - program_letter_generate_url: string - /** - * - * @type {string} - * @memberof ProgramCertificate - */ - program_letter_share_url: string /** * * @type {string} diff --git a/frontends/api/src/test-utils/urls.ts b/frontends/api/src/test-utils/urls.ts index c1478f4cdf..ca3575339f 100644 --- a/frontends/api/src/test-utils/urls.ts +++ b/frontends/api/src/test-utils/urls.ts @@ -26,7 +26,7 @@ import type { import type { BaseAPI } from "../generated/v1/base" import type { BaseAPI as BaseAPIv0 } from "../generated/v0/base" -const API_BASE_URL = process.env.MITOPEN_AXIOS_BASE_PATH +const API_BASE_URL = process.env.MITOPEN_API_BASE_URL // OpenAPI Generator declares parameters using interfaces, which makes passing // them to functions a little annoying. diff --git a/frontends/mit-open/jest.config.ts b/frontends/mit-open/jest.config.ts index 5500ff1746..0f60c4a731 100644 --- a/frontends/mit-open/jest.config.ts +++ b/frontends/mit-open/jest.config.ts @@ -18,7 +18,7 @@ const config: Config.InitialOptions = { embedlyKey: "embedly_key", axios_base_path: "https://api.mitopen-test.odl.mit.edu", }, - MITOPEN_AXIOS_BASE_PATH: "https://api.mitopen-test.odl.mit.edu", + MITOPEN_API_BASE_URL: "https://api.mitopen-test.odl.mit.edu", }, } diff --git a/frontends/mit-open/src/common/urls.test.ts b/frontends/mit-open/src/common/urls.test.ts index 7ffd04c531..d1f3ed85dc 100644 --- a/frontends/mit-open/src/common/urls.test.ts +++ b/frontends/mit-open/src/common/urls.test.ts @@ -1,22 +1,20 @@ import { login } from "./urls" -const { MITOPEN_AXIOS_BASE_PATH } = process.env +const { MITOPEN_API_BASE_URL } = process.env test("login encodes the next parameter appropriately", () => { expect(login()).toBe( - `${MITOPEN_AXIOS_BASE_PATH}/login/ol-oidc/?next=http://localhost/`, + `${MITOPEN_API_BASE_URL}/login/ol-oidc/?next=http://localhost/`, ) expect(login({})).toBe( - `${MITOPEN_AXIOS_BASE_PATH}/login/ol-oidc/?next=http://localhost/`, + `${MITOPEN_API_BASE_URL}/login/ol-oidc/?next=http://localhost/`, ) expect( login({ pathname: "/foo/bar", }), - ).toBe( - `${MITOPEN_AXIOS_BASE_PATH}/login/ol-oidc/?next=http://localhost/foo/bar`, - ) + ).toBe(`${MITOPEN_API_BASE_URL}/login/ol-oidc/?next=http://localhost/foo/bar`) expect( login({ @@ -24,6 +22,6 @@ test("login encodes the next parameter appropriately", () => { search: "?cat=meow", }), ).toBe( - `${MITOPEN_AXIOS_BASE_PATH}/login/ol-oidc/?next=http://localhost/foo/bar%3Fcat%3Dmeow`, + `${MITOPEN_API_BASE_URL}/login/ol-oidc/?next=http://localhost/foo/bar%3Fcat%3Dmeow`, ) }) diff --git a/frontends/mit-open/src/common/urls.ts b/frontends/mit-open/src/common/urls.ts index 623e603d98..0fafaf37c5 100644 --- a/frontends/mit-open/src/common/urls.ts +++ b/frontends/mit-open/src/common/urls.ts @@ -40,8 +40,8 @@ export const makeChannelManageWidgetsPath = ( name: string, ) => generatePath(CHANNEL_EDIT_WIDGETS, { channelType, name }) -export const LOGIN = `${process.env.MITOPEN_AXIOS_BASE_PATH}/login/ol-oidc/` -export const LOGOUT = `${process.env.MITOPEN_AXIOS_BASE_PATH}/logout/` +export const LOGIN = `${process.env.MITOPEN_API_BASE_URL}/login/ol-oidc/` +export const LOGOUT = `${process.env.MITOPEN_API_BASE_URL}/logout/` /** * Returns the URL to the login page, with a `next` parameter to redirect back diff --git a/frontends/mit-open/webpack.config.js b/frontends/mit-open/webpack.config.js index a345f23f68..931ab9cc48 100644 --- a/frontends/mit-open/webpack.config.js +++ b/frontends/mit-open/webpack.config.js @@ -19,7 +19,7 @@ const { NODE_ENV, ENVIRONMENT, PORT, - MITOPEN_AXIOS_BASE_PATH, + MITOPEN_API_BASE_URL, API_DEV_PROXY_BASE_URL, WEBPACK_ANALYZE, SITE_NAME, @@ -37,14 +37,14 @@ const { desc: "Port to run the development server on", default: 8062, }), - MITOPEN_AXIOS_BASE_PATH: str({ + MITOPEN_API_BASE_URL: str({ desc: "Base URL for API requests", devDefault: "", }), API_DEV_PROXY_BASE_URL: str({ desc: "API base URL to proxy to in development mode", default: "", - devDefault: process.env.MITOPEN_BASE_URL, + devDefault: process.env.MITOPEN_APP_BASE_URL, }), WEBPACK_ANALYZE: bool({ desc: "Whether to run webpack bundle analyzer", @@ -175,7 +175,7 @@ module.exports = (env, argv) => { axios_with_credentials: JSON.stringify( process.env.MITOPEN_AXIOS_WITH_CREDENTIALS, ), - axios_base_path: JSON.stringify(process.env.MITOPEN_AXIOS_BASE_PATH), + axios_base_path: JSON.stringify(process.env.MITOPEN_API_BASE_URL), embedlyKey: JSON.stringify(process.env.EMBEDLY_KEY), search_page_size: JSON.stringify( process.env.OPENSEARCH_DEFAULT_PAGE_SIZE, @@ -189,7 +189,7 @@ module.exports = (env, argv) => { }), new webpack.EnvironmentPlugin({ // within app, define process.env.VAR_NAME with default from cleanEnv - MITOPEN_AXIOS_BASE_PATH, + MITOPEN_API_BASE_URL, ENVIRONMENT, SITE_NAME, MITOPEN_SUPPORT_EMAIL, @@ -269,11 +269,11 @@ module.exports = (env, argv) => { "/static/admin", "/static/hijack", ], - target: API_DEV_PROXY_BASE_URL || MITOPEN_AXIOS_BASE_PATH, + target: API_DEV_PROXY_BASE_URL || MITOPEN_API_BASE_URL, changeOrigin: true, secure: false, headers: { - Origin: API_DEV_PROXY_BASE_URL || MITOPEN_AXIOS_BASE_PATH, + Origin: API_DEV_PROXY_BASE_URL || MITOPEN_API_BASE_URL, }, }, ], diff --git a/learning_resources/etl/constants.py b/learning_resources/etl/constants.py index 39eeb1be10..825f0f89b8 100644 --- a/learning_resources/etl/constants.py +++ b/learning_resources/etl/constants.py @@ -9,7 +9,7 @@ # A custom UA so that operators of OpenEdx will know who is pinging their service COMMON_HEADERS = { - "User-Agent": f"CourseCatalogBot/{settings.VERSION} ({settings.SITE_BASE_URL})" + "User-Agent": f"CourseCatalogBot/{settings.VERSION} ({settings.APP_BASE_URL})" } READABLE_ID_FIELD = "readable_id" diff --git a/learning_resources/etl/podcast.py b/learning_resources/etl/podcast.py index de65df8799..332dc944b0 100644 --- a/learning_resources/etl/podcast.py +++ b/learning_resources/etl/podcast.py @@ -1,7 +1,6 @@ """podcast ETL""" import logging -from urllib.parse import urljoin import github import requests @@ -15,7 +14,7 @@ from learning_resources.etl.constants import ETLSource from learning_resources.etl.utils import clean_data, generate_readable_id from learning_resources.models import PodcastEpisode -from main.utils import now_in_utc +from main.utils import frontend_absolute_url, now_in_utc CONFIG_FILE_REPO = "mitodl/open-podcast-data" CONFIG_FILE_FOLDER = "podcasts" @@ -242,10 +241,8 @@ def get_all_mit_podcasts_channel_rss(): """ # noqa: E501 current_timestamp = now_in_utc().strftime(TIMESTAMP_FORMAT) - podcasts_url = urljoin(settings.SITE_BASE_URL, "podcasts") - cover_image_url = urljoin( - settings.SITE_BASE_URL, "/static/images/podcast_cover_art.png" - ) + podcasts_url = frontend_absolute_url("/podcasts") + cover_image_url = frontend_absolute_url("/static/images/podcast_cover_art.png") rss = f""" diff --git a/learning_resources/etl/podcast_test.py b/learning_resources/etl/podcast_test.py index 7a3c770d42..3012c6de62 100644 --- a/learning_resources/etl/podcast_test.py +++ b/learning_resources/etl/podcast_test.py @@ -2,7 +2,6 @@ import datetime from unittest.mock import Mock -from urllib.parse import urljoin import pytest import yaml @@ -23,6 +22,7 @@ from learning_resources.factories import ( PodcastEpisodeFactory, ) +from main.utils import frontend_absolute_url pytestmark = pytest.mark.django_db @@ -251,10 +251,8 @@ def test_generate_aggregate_podcast_rss(): resource_2.last_modified = datetime.datetime(2020, 1, 1, tzinfo=datetime.UTC) resource_2.save() - podcasts_url = urljoin(settings.SITE_BASE_URL, "podcasts") - cover_image_url = urljoin( - settings.SITE_BASE_URL, "/static/images/podcast_cover_art.png" - ) + podcasts_url = frontend_absolute_url("/podcasts") + cover_image_url = frontend_absolute_url("/static/images/podcast_cover_art.png") expected_rss = f""" diff --git a/learning_resources/serializers_test.py b/learning_resources/serializers_test.py index 565937694b..e12adfe052 100644 --- a/learning_resources/serializers_test.py +++ b/learning_resources/serializers_test.py @@ -1,9 +1,6 @@ """Tests for learning_resources serializers""" -from urllib.parse import urljoin - import pytest -from django.conf import settings from channels.factories import ( ChannelDepartmentDetailFactory, @@ -23,6 +20,7 @@ from learning_resources.factories import LearningResourceFactory from learning_resources.serializers import LearningResourceSerializer from main.test_utils import assert_json_equal, drf_datetime +from main.utils import frontend_absolute_url pytestmark = pytest.mark.django_db @@ -246,8 +244,7 @@ def test_learning_resource_serializer( # noqa: PLR0913 { "department_id": dept.department_id, "name": dept.name, - "channel_url": urljoin( - settings.SITE_BASE_URL, + "channel_url": frontend_absolute_url( f"/c/department/{Channel.objects.get(department_detail__department=dept).name}/", ), "school": { @@ -463,7 +460,7 @@ def test_learningpathitem_serializer_validation(child_exists): @pytest.mark.parametrize("has_channels", [True, False]) def test_content_file_serializer(settings, expected_types, has_channels): """Verify that the ContentFileSerializer has the correct data""" - settings.SITE_BASE_URL = "https://test.edu/" + settings.APP_BASE_URL = "https://test.edu/" content_kwargs = { "content": "Test content", "content_author": "MIT", @@ -501,9 +498,8 @@ def test_content_file_serializer(settings, expected_types, has_channels): "offered_by": { "name": content_file.run.learning_resource.offered_by.name, "code": content_file.run.learning_resource.offered_by.code, - "channel_url": urljoin( - settings.SITE_BASE_URL, - f"/c/unit/{Channel.objects.get(unit_detail__unit=content_file.run.learning_resource.offered_by).name}/", + "channel_url": frontend_absolute_url( + f"/c/unit/{Channel.objects.get(unit_detail__unit=content_file.run.learning_resource.offered_by).name}/" ) if has_channels else None, @@ -514,9 +510,8 @@ def test_content_file_serializer(settings, expected_types, has_channels): { "name": dept.name, "department_id": dept.department_id, - "channel_url": urljoin( - settings.SITE_BASE_URL, - f"/c/department/{Channel.objects.get(department_detail__department=dept).name}/", + "channel_url": frontend_absolute_url( + f"/c/department/{Channel.objects.get(department_detail__department=dept).name}/" ) if has_channels else None, @@ -537,8 +532,7 @@ def test_content_file_serializer(settings, expected_types, has_channels): "name": topic.name, "id": topic.id, "parent": topic.parent, - "channel_url": urljoin( - settings.SITE_BASE_URL, + "channel_url": frontend_absolute_url( f"/c/topic/{Channel.objects.get(topic_detail__topic=topic).name}/" if has_channels else None, diff --git a/learning_resources_search/indexing_api_test.py b/learning_resources_search/indexing_api_test.py index 2859836de3..4befc98b58 100644 --- a/learning_resources_search/indexing_api_test.py +++ b/learning_resources_search/indexing_api_test.py @@ -192,7 +192,6 @@ def test_create_backing_index(mocked_es, mocker, temp_alias_exists): ) -@pytest.mark.usefixtures("indexing_user") @pytest.mark.parametrize("errors", [(), "error"]) @pytest.mark.parametrize( "index_types", @@ -253,7 +252,6 @@ def test_index_learning_resources( ) -@pytest.mark.usefixtures("indexing_user") @pytest.mark.parametrize("errors", [(), "error"]) def test_deindex_learning_resources(mocked_es, mocker, settings, errors): """ diff --git a/learning_resources_search/tasks.py b/learning_resources_search/tasks.py index b99881ff91..8d941b4cb1 100644 --- a/learning_resources_search/tasks.py +++ b/learning_resources_search/tasks.py @@ -50,7 +50,7 @@ serialize_percolate_query_for_update, ) from main.celery import app -from main.utils import chunks, merge_strings, now_in_utc +from main.utils import chunks, frontend_absolute_url, merge_strings, now_in_utc from profiles.utils import send_template_email User = get_user_model() @@ -141,7 +141,7 @@ def _infer_search_url(percolate_query): if "endpoint" in query_string_params: query_string_params.pop("endpoint") query_string = urlencode(query_string_params, doseq=True) - return f"{settings.SITE_BASE_URL}/search?{query_string}" + return frontend_absolute_url(f"/search?{query_string}") def _group_percolated_rows(rows): diff --git a/main/settings.py b/main/settings.py index 910e0c3242..d138828ea8 100644 --- a/main/settings.py +++ b/main/settings.py @@ -65,9 +65,9 @@ SECURE_SSL_REDIRECT = get_bool("MITOPEN_SECURE_SSL_REDIRECT", True) # noqa: FBT003 SITE_ID = 1 -SITE_BASE_URL = get_string("MITOPEN_BASE_URL", None) -if not SITE_BASE_URL: - msg = "MITOPEN_BASE_URL is not set" +APP_BASE_URL = get_string("MITOPEN_APP_BASE_URL", None) +if not APP_BASE_URL: + msg = "MITOPEN_APP_BASE_URL is not set" raise ImproperlyConfigured(msg) MITOPEN_TITLE = get_string("MITOPEN_TITLE", "MIT Open") @@ -174,14 +174,14 @@ SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies" -LOGIN_REDIRECT_URL = SITE_BASE_URL +LOGIN_REDIRECT_URL = "/app" LOGIN_URL = "/login" LOGIN_ERROR_URL = "/login" LOGOUT_URL = "/logout" -LOGOUT_REDIRECT_URL = SITE_BASE_URL +LOGOUT_REDIRECT_URL = "/app" MITOPEN_TOS_URL = get_string( - "MITOPEN_TOS_URL", urljoin(SITE_BASE_URL, "/terms-and-conditions/") + "MITOPEN_TOS_URL", urljoin(APP_BASE_URL, "/terms-and-conditions/") ) ROOT_URLCONF = "main.urls" @@ -259,7 +259,7 @@ "guardian.backends.ObjectPermissionBackend", ) -SOCIAL_AUTH_LOGIN_REDIRECT_URL = get_string("MITOPEN_LOGIN_REDIRECT_URL", "/") +SOCIAL_AUTH_LOGIN_REDIRECT_URL = get_string("MITOPEN_LOGIN_REDIRECT_URL", "/app") SOCIAL_AUTH_NEW_USER_LOGIN_REDIRECT_URL = get_string( "MITOPEN_NEW_USER_LOGIN_URL", "/onboarding" ) @@ -269,7 +269,7 @@ name="SOCIAL_AUTH_ALLOWED_REDIRECT_HOSTS", default=[], ), - urlparse(SITE_BASE_URL).netloc, + urlparse(APP_BASE_URL).netloc, ] SOCIAL_AUTH_PIPELINE = ( @@ -550,10 +550,6 @@ OPENSEARCH_SHARD_COUNT = get_int("OPENSEARCH_SHARD_COUNT", 2) OPENSEARCH_REPLICA_COUNT = get_int("OPENSEARCH_REPLICA_COUNT", 2) OPENSEARCH_MAX_REQUEST_SIZE = get_int("OPENSEARCH_MAX_REQUEST_SIZE", 10485760) -INDEXING_API_USERNAME = get_string("INDEXING_API_USERNAME", None) -if not INDEXING_API_USERNAME: - msg = "Missing setting INDEXING_API_USERNAME" - raise ImproperlyConfigured(msg) INDEXING_ERROR_RETRIES = get_int("INDEXING_ERROR_RETRIES", 1) # JWT authentication settings diff --git a/main/settings_test.py b/main/settings_test.py index 9a1b2fd225..f5ab465352 100644 --- a/main/settings_test.py +++ b/main/settings_test.py @@ -20,8 +20,7 @@ "MAILGUN_KEY": "fake_mailgun_key", "MITOPEN_COOKIE_NAME": "cookie_monster", "MITOPEN_COOKIE_DOMAIN": "od.fake.domain", - "MITOPEN_BASE_URL": "http:localhost:8063/", - "INDEXING_API_USERNAME": "mitodl", + "MITOPEN_APP_BASE_URL": "http:localhost:8063/", } diff --git a/main/urls.py b/main/urls.py index e74060c5ab..aec91e0ff5 100644 --- a/main/urls.py +++ b/main/urls.py @@ -18,6 +18,7 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path, re_path +from django.views.generic.base import RedirectView from rest_framework.routers import DefaultRouter from main.views import FeaturesViewSet, index @@ -65,6 +66,7 @@ # Hijack re_path(r"^hijack/", include("hijack.urls", namespace="hijack")), re_path(r"", include("news_events.urls")), + re_path(r"^app", RedirectView.as_view(url=settings.APP_BASE_URL)), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) if settings.DEBUG: diff --git a/main/utils.py b/main/utils.py index 0eb3049ffa..0a36fe6e4a 100644 --- a/main/utils.py +++ b/main/utils.py @@ -5,6 +5,7 @@ import os from enum import Flag, auto from itertools import islice +from urllib.parse import urljoin import markdown2 from bs4 import BeautifulSoup @@ -296,3 +297,16 @@ def write_x509_files(): """Write the x509 certificate and key to files""" write_to_file(settings.MIT_WS_CERTIFICATE_FILE, settings.MIT_WS_CERTIFICATE) write_to_file(settings.MIT_WS_PRIVATE_KEY_FILE, settings.MIT_WS_PRIVATE_KEY) + + +def frontend_absolute_url(relative_path: str) -> str: + """ + Create an absolute url to the frontend + + Args: + relative_path(str): path relative to the frontend root + + Returns: + str: absolute url path to the frontend + """ + return urljoin(settings.APP_BASE_URL, relative_path) diff --git a/main/utils_test.py b/main/utils_test.py index f838f38e46..8a6b3e74b0 100644 --- a/main/utils_test.py +++ b/main/utils_test.py @@ -13,6 +13,7 @@ extract_values, filter_dict_keys, filter_dict_with_renamed_keys, + frontend_absolute_url, html_to_plain_text, is_near_now, markdown_to_plain_text, @@ -199,3 +200,14 @@ def test_write_to_file(): write_to_file(outfile.name, content) with open(outfile.name, "rb") as infile: # noqa: PTH123 assert infile.read() == content + + +def test_frontend_absolute_url(settings): + """ + frontend_absolute_url should generate urls to the frontend + """ + settings.APP_BASE_URL = "http://example.com/" + + assert frontend_absolute_url("/") == "http://example.com/" + assert frontend_absolute_url("/path") == "http://example.com/path" + assert frontend_absolute_url("path") == "http://example.com/path" diff --git a/nginx/20-compile-nginx-conf-erb.sh b/nginx/20-compile-nginx-conf-erb.sh new file mode 100755 index 0000000000..ba5e84b72d --- /dev/null +++ b/nginx/20-compile-nginx-conf-erb.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# Based on https://github.com/nginxinc/docker-nginx/blob/6f0396c1e06837672698bc97865ffcea9dc841d5/mainline/debian/20-envsubst-on-templates.sh + +set -e + +ME=$(basename $0) + +auto_erb_build() { + template="/etc/nginx/templates/nginx.conf.erb" + output_path="/etc/nginx/nginx.conf" + + echo "$ME: Running erb on $template to $output_path" + erb "$template" >"$output_path" +} + +auto_erb_build + +exit 0 diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000000..697ed25893 --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,22 @@ +# NOTE: this dockerfile is primarilty for local development only +# it's primary purpose is to emulate heroku-buildpack-nginx's +# functionality that compiles config/nginx.conf.erb +# See https://github.com/heroku/heroku-buildpack-nginx/blob/fefac6c569f28182b3459cb8e34b8ccafc403fde/bin/start-nginx +FROM nginx:1.27.0 + +# Logs are configured to a relatic path under /etc/nginx +# but the container expects /var/log +RUN mkdir -p /etc/nginx/logs && ln -sf /var/log/nginx /etc/nginx/logs/ + +# erb unfortunately needs a whole ruby install +RUN apt-get update && apt-get install -y ruby + +# this gets run automatically by the nginx container's entrypoint +COPY 20-compile-nginx-conf-erb.sh /docker-entrypoint.d + +# NOTE: this removes the args "-g daemon off" because nginx is impolite +# and treats a cli flag and a config flag as duplicate: +# +# nginx: [emerg] "daemon" directive is duplicate in /etc/nginx/nginx.conf:3 +# +CMD ["nginx"] diff --git a/openapi/specs/v0.yaml b/openapi/specs/v0.yaml index 2845cc9635..f07e27708d 100644 --- a/openapi/specs/v0.yaml +++ b/openapi/specs/v0.yaml @@ -2170,12 +2170,6 @@ components: record_hash: type: string readOnly: true - program_letter_generate_url: - type: string - readOnly: true - program_letter_share_url: - type: string - readOnly: true program_title: type: string maxLength: 256 @@ -2249,8 +2243,6 @@ components: format: date-time nullable: true required: - - program_letter_generate_url - - program_letter_share_url - program_title - record_hash - user_email diff --git a/openapi/specs/v1.yaml b/openapi/specs/v1.yaml index 9574a562a5..1517ae72fc 100644 --- a/openapi/specs/v1.yaml +++ b/openapi/specs/v1.yaml @@ -10203,12 +10203,6 @@ components: record_hash: type: string readOnly: true - program_letter_generate_url: - type: string - readOnly: true - program_letter_share_url: - type: string - readOnly: true program_title: type: string maxLength: 256 @@ -10282,8 +10276,6 @@ components: format: date-time nullable: true required: - - program_letter_generate_url - - program_letter_share_url - program_title - record_hash - user_email diff --git a/package.json b/package.json index beab2f32e3..16fb54b84d 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "watch": "yarn workspace mit-open run watch", "watch:rc": "yarn workspace mit-open run watch:rc", "style-lint": "yarn workspace frontends run style-lint", - "test": "MITOPEN_AXIOS_BASE_PATH=https://api.mitopen-test.odl.mit.edu yarn workspace frontends global:test", - "test-watch": "MITOPEN_AXIOS_BASE_PATH=https://api.mitopen-test.odl.mit.edu yarn workspace frontends test-watch", + "test": "MITOPEN_API_BASE_URL=https://api.mitopen-test.odl.mit.edu yarn workspace frontends global:test", + "test-watch": "MITOPEN_API_BASE_URL=https://api.mitopen-test.odl.mit.edu yarn workspace frontends test-watch", "storybook": "yarn workspace frontends storybook", "lint-check": "yarn workspace frontends run lint-check", "typecheck": "yarn workspace frontends run typecheck" diff --git a/profiles/models.py b/profiles/models.py index debac54231..b3eb18d379 100644 --- a/profiles/models.py +++ b/profiles/models.py @@ -10,6 +10,7 @@ from django_scim.models import AbstractSCIMUserMixin from learning_resources.constants import LearningResourceFormat +from main.utils import frontend_absolute_url from profiles.utils import ( IMAGE_MEDIUM_MAX_DIMENSION, IMAGE_SMALL_MAX_DIMENSION, @@ -293,4 +294,4 @@ def __str__(self): ) def get_absolute_url(self): - return f"/program_letter/{self.id}/view" + return frontend_absolute_url(f"/program_letter/{self.id}/view") diff --git a/profiles/serializers.py b/profiles/serializers.py index 8ecd26d670..0b9aac5e22 100644 --- a/profiles/serializers.py +++ b/profiles/serializers.py @@ -7,7 +7,6 @@ import ulid from django.contrib.auth import get_user_model from django.db import transaction -from django.urls import reverse from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from rest_framework.exceptions import ValidationError @@ -364,31 +363,6 @@ class ProgramCertificateSerializer(serializers.ModelSerializer): Serializer for Program Certificates """ - program_letter_generate_url = serializers.SerializerMethodField() - program_letter_share_url = serializers.SerializerMethodField() - - def get_program_letter_generate_url(self, instance) -> str: - request = self.context.get("request") - letter_url = reverse( - "profile:program-letter-intercept", - kwargs={"program_id": instance.micromasters_program_id}, - ) - if request: - return request.build_absolute_uri(letter_url) - return letter_url - - def get_program_letter_share_url(self, instance) -> str: - request = self.context.get("request") - - user = User.objects.get(email=instance.user_email) - letter, created = ProgramLetter.objects.get_or_create( - user=user, certificate=instance - ) - letter_url = letter.get_absolute_url() - if request: - return request.build_absolute_uri(letter_url) - return letter_url - class Meta: model = ProgramCertificate fields = "__all__" diff --git a/profiles/urls.py b/profiles/urls.py index e5a18a0425..87f54ac7c5 100644 --- a/profiles/urls.py +++ b/profiles/urls.py @@ -6,7 +6,6 @@ from profiles.views import ( CurrentUserRetrieveViewSet, ProfileViewSet, - ProgramLetterInterceptView, ProgramLetterViewSet, UserProgramCertificateViewSet, UserViewSet, @@ -53,9 +52,4 @@ name_initials_avatar_view, name="name-initials-avatar", ), - re_path( - r"^program_letter/(?P[0-9]+)/", - ProgramLetterInterceptView.as_view(), - name="program-letter-intercept", - ), ] diff --git a/profiles/utils.py b/profiles/utils.py index 8b35272fdb..5d31696bc0 100644 --- a/profiles/utils.py +++ b/profiles/utils.py @@ -62,6 +62,9 @@ DEFAULT_PROFILE_IMAGE = urljoin(settings.STATIC_URL, "images/avatar_default.png") +# NOTE: this is probably a bit broken at the moment +# because of the API/frontend separation which +# means APP_BASE_URL has changed def generate_gravatar_image(user, image_field=None): """ Query gravatar for an image and return those image properties @@ -81,11 +84,11 @@ def generate_gravatar_image(user, image_field=None): size_param = f"&s={max_dimension}" if max_dimension else "" if user.profile.name: d_param = urljoin( - settings.SITE_BASE_URL, + settings.APP_BASE_URL, f"/profile/{user.username}/{max_dimension}/fff/579cf9.png", ) else: - d_param = urljoin(settings.SITE_BASE_URL, DEFAULT_PROFILE_IMAGE) + d_param = urljoin(settings.APP_BASE_URL, DEFAULT_PROFILE_IMAGE) return f"{gravatar_image_url}?d={quote(d_param)}{size_param}" diff --git a/profiles/views.py b/profiles/views.py index c157209d67..ef28fb4db2 100644 --- a/profiles/views.py +++ b/profiles/views.py @@ -2,12 +2,9 @@ from cairosvg import svg2png # pylint:disable=no-name-in-module from django.contrib.auth import get_user_model -from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User -from django.http import HttpResponse, HttpResponseRedirect -from django.shortcuts import get_object_or_404, redirect -from django.utils.decorators import method_decorator -from django.views import View +from django.http import HttpResponse +from django.shortcuts import redirect from django.views.decorators.cache import cache_page from django_filters.rest_framework import DjangoFilterBackend from rest_framework import mixins, viewsets @@ -142,23 +139,3 @@ def name_initials_avatar_view( return redirect(DEFAULT_PROFILE_IMAGE) svg = generate_svg_avatar(user.profile.name, int(size), color, bgcolor) return HttpResponse(svg2png(bytestring=svg), content_type="image/png") - - -@method_decorator(login_required, name="dispatch") -class ProgramLetterInterceptView(View): - """ - View that generates a uuid (via ProgramLetter instance) - and then passes the user along to the shareable letter view - """ - - def get(self, request, **kwargs): - program_id = kwargs.get("program_id") - certificate = get_object_or_404( - ProgramCertificate, - user_email=request.user.email, - micromasters_program_id=program_id, - ) - letter, created = ProgramLetter.objects.get_or_create( - user=request.user, certificate=certificate - ) - return HttpResponseRedirect(letter.get_absolute_url()) diff --git a/profiles/views_test.py b/profiles/views_test.py index 562d0e4c8e..f1c61c048a 100644 --- a/profiles/views_test.py +++ b/profiles/views_test.py @@ -14,7 +14,7 @@ from learning_resources.serializers import LearningResourceTopicSerializer from learning_resources_search.serializers_test import get_request_object from profiles.factories import ProgramCertificateFactory, ProgramLetterFactory -from profiles.models import Profile, ProgramLetter +from profiles.models import Profile from profiles.serializers import ( ProfileSerializer, ProgramCertificateSerializer, @@ -387,47 +387,6 @@ def test_get_user_by_me(mocker, client, user, is_anonymous): } -@pytest.mark.parametrize("is_anonymous", [True, False]) -def test_letter_intercept_view_generates_program_letter( - mocker, client, user, is_anonymous, settings -): - """ - Test that the letter intercept view generates a - ProgramLetter and then passes the user along to the display. - Also test that anonymous users do not generate letters and cant access this page - """ - settings.DATABASE_ROUTERS = [] - micromasters_program_id = 1 - if not is_anonymous: - client.force_login(user) - cert = ProgramCertificateFactory( - user_email=user.email, micromasters_program_id=micromasters_program_id - ) - assert ProgramLetter.objects.filter(user=user).count() == 0 - - response = client.get( - reverse( - "profile:program-letter-intercept", - kwargs={"program_id": micromasters_program_id}, - ) - ) - assert ProgramLetter.objects.filter(user=user).count() == 1 - letter_id = ProgramLetter.objects.get(user=user, certificate=cert).id - assert response.url == f"/program_letter/{letter_id}/view" - else: - cert = ProgramCertificateFactory( - user_email=user.email, micromasters_program_id=micromasters_program_id - ) - ProgramLetterFactory(user=user, certificate=cert) - response = client.get( - reverse( - "profile:program-letter-intercept", - kwargs={"program_id": micromasters_program_id}, - ) - ) - assert response.status_code == 302 - - @pytest.mark.parametrize("is_anonymous", [True, False]) def test_program_letter_api_view(mocker, client, rf, user, is_anonymous, settings): # noqa: PLR0913 """