diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..44f4c683 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +POSTGRES_USER= +POSTGRES_PASSWORD= +POSTGRES_DB= +DJANGO_SECRET_KEY= +APP_VERSION= diff --git a/.gitignore b/.gitignore index 21cabf95..497c49d0 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ frontend/src/environments/version.ts /backend/fracas.xml /backend/snli_1.0_dev.txt /backend/snli_1.0_test.txt + +# Docker container logs +logs/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b9a1cf98..cec19556 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,20 +55,36 @@ If you are reading this document, you'll likely be working with the integrated p ### Quickstart with Docker +The application can be run in two modes: development mode and production mode. The former is meant for live development, while the latter is meant for simulating deployment conditions as closely as possible. + +Begin by creating a `.env` file in the project root with the following contents: + +```env +POSTGRES_USER=my-user-name +POSTGRES_PASSWORD=my-password +POSTGRES_DB=langpro +DJANGO_SECRET_KEY=my-secret-key +APP_VERSION=1.0.0 +``` + +Then update the values after the `=` as needed. The `POSTGRES_*` variables are used to configure the PostgreSQL database. The `DJANGO_SECRET_KEY` variable is used to set the Django `SECRET_KEY` setting. The `APP_VERSION` variable is used to set the version of the application, which is displayed in the frontend. + +Finally, run the command below to start the application in development mode. + ```console -docker compose up -d +docker compose --profile dev up -d ``` This will run the frontend and backend applications and watch all source files for changes. To run the backend unittests: ```console -docker compose exec backend pytest +docker compose exec backend-dev pytest ``` To run the frontend unittests: ```console -docker compose exec frontend yarn ng test --no-browsers +docker compose exec frontend-dev yarn ng test --no-browsers ``` then open http://localhost:9876 in a browser of choice. diff --git a/backend/Dockerfile b/backend/Dockerfile index 85a54bc0..fe15497e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,11 +1,24 @@ -FROM python:3.11 +FROM python:3.11-slim-trixie + +# Set environment variables. +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +# Install dependencies. +RUN apt update +RUN pip install gunicorn -WORKDIR /usr/src/app/backend COPY requirements.txt . -RUN pip install -U pip pip-tools && pip-sync +RUN pip install -r requirements.txt --no-cache-dir +# Set working directory. +WORKDIR /usr/src/app + +# Copy project files. COPY . . -CMD python manage.py check && \ - python manage.py migrate && \ - python manage.py runserver --settings glue --pythonpath .. 0.0.0.0:8000 +# Create a directory for Gunicorn logs (production). +RUN mkdir -p /usr/src/app/logs + +# Expose port. +EXPOSE 8000 diff --git a/backend/langpro_annotator/common_settings.py b/backend/langpro_annotator/common_settings.py index 5d6d410e..00796714 100644 --- a/backend/langpro_annotator/common_settings.py +++ b/backend/langpro_annotator/common_settings.py @@ -1,107 +1,110 @@ -INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "livereload", - "django.contrib.staticfiles", - "rest_framework", - "django.contrib.sites", - "rest_framework.authtoken", - "dj_rest_auth", - "dj_rest_auth.registration", - "allauth", - "allauth.account", - # Required for deleting accounts, but not actually used, - # cf. https://github.com/iMerica/dj-rest-auth/pull/110. - "allauth.socialaccount", - "user", - "revproxy", - "problem", - "annotation", -] - -MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.locale.LocaleMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", - "allauth.account.middleware.AccountMiddleware", -] - -# Internationalization -# https://docs.djangoproject.com/en/3.0/topics/i18n/ -LANGUAGES = [ - ("en", "English"), - ("nl", "Nederlands"), -] -LANGUAGE_CODE = "en" - -TIME_ZONE = "Europe/Amsterdam" - -USE_I18N = True - -USE_TZ = True # Authentication -REST_FRAMEWORK = { - "DEFAULT_AUTHENTICATION_CLASSES": [ - "rest_framework.authentication.TokenAuthentication", - "rest_framework.authentication.SessionAuthentication", - ] -} - -AUTH_USER_MODEL = "user.User" -ACCOUNT_EMAIL_VERIFICATION = "mandatory" -ACCOUNT_SIGNUP_FIELDS = ["email*", "username*", "password1*", "password2*"] - -SITE_ID = 1 -SITE_NAME = "langpro_annotator" - -# Remove this setting in production! -EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" - -HOST = "localhost:8000" - -REST_AUTH = { - "USER_DETAILS_SERIALIZER": "user.serializers.CustomUserDetailsSerializer", -} - -LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "formatters": { - "verbose": { - "format": "{levelname} {asctime} {module} {message}", - "style": "{", - }, - "simple": { - "format": "{levelname} {message}", - "style": "{", - }, - }, - "handlers": { - "console": { - "level": "INFO", - "class": "logging.StreamHandler", - "formatter": "simple", - }, - }, - "loggers": { - "django": { - "handlers": ["console"], - "level": "INFO", - "propagate": False, - }, - "LangProAnnotator": { - "handlers": ["console"], - "level": "INFO", - "propagate": False, - }, - }, -} - -LANGPRO_URL = "http://localhost:8080" +import os + + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "livereload", + "django.contrib.staticfiles", + "rest_framework", + "django.contrib.sites", + "rest_framework.authtoken", + "dj_rest_auth", + "dj_rest_auth.registration", + "allauth", + "allauth.account", + # Required for deleting accounts, but not actually used, + # cf. https://github.com/iMerica/dj-rest-auth/pull/110. + "allauth.socialaccount", + "user", + "revproxy", + "problem", + "annotation", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "allauth.account.middleware.AccountMiddleware", +] + +# Internationalization +# https://docs.djangoproject.com/en/3.0/topics/i18n/ +LANGUAGES = [ + ("en", "English"), + ("nl", "Nederlands"), +] +LANGUAGE_CODE = "en" + +TIME_ZONE = "Europe/Amsterdam" + +USE_I18N = True + +USE_TZ = True # Authentication +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.TokenAuthentication", + "rest_framework.authentication.SessionAuthentication", + ] +} + +AUTH_USER_MODEL = "user.User" +ACCOUNT_EMAIL_VERIFICATION = "mandatory" +ACCOUNT_SIGNUP_FIELDS = ["email*", "username*", "password1*", "password2*"] + +SITE_ID = 1 +SITE_NAME = "langpro_annotator" + +# Remove this setting in production! +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +HOST = "localhost:8000" + +REST_AUTH = { + "USER_DETAILS_SERIALIZER": "user.serializers.CustomUserDetailsSerializer", +} + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "{levelname} {asctime} {module} {message}", + "style": "{", + }, + "simple": { + "format": "{levelname} {message}", + "style": "{", + }, + }, + "handlers": { + "console": { + "level": "INFO", + "class": "logging.StreamHandler", + "formatter": "simple", + }, + }, + "loggers": { + "django": { + "handlers": ["console"], + "level": "INFO", + "propagate": False, + }, + "LangProAnnotator": { + "handlers": ["console"], + "level": "INFO", + "propagate": False, + }, + }, +} + +LANGPRO_URL = os.environ.get('LANGPRO_CONTAINER') or 'http://localhost:8080' diff --git a/backend/langpro_annotator/settings.py b/backend/langpro_annotator/settings.py index 4bad28f2..a67cc77a 100644 --- a/backend/langpro_annotator/settings.py +++ b/backend/langpro_annotator/settings.py @@ -19,13 +19,22 @@ # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'kxreeb3bds$oibo7ex#f3bi5r+d(1x5zljo-#ms=i2%ih-!pvn' +SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "django-insecure-1234567890") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = int(os.getenv("DJANGO_DEBUG", 0)) == 1 -ALLOWED_HOSTS = ['localhost', '127.0.0.1'] +# CSRF trusted origins for cross-origin requests +CSRF_TRUSTED_ORIGINS = [] +ALLOWED_HOSTS = ["la-backend"] + +if DEBUG: + ALLOWED_HOSTS.append("localhost") + CSRF_TRUSTED_ORIGINS.extend([ + "http://localhost:5000", + "http://127.0.0.1:5000", + ]) # Application definition diff --git a/backend/langpro_annotator/urls.py b/backend/langpro_annotator/urls.py index e4808da0..2f5bec7f 100644 --- a/backend/langpro_annotator/urls.py +++ b/backend/langpro_annotator/urls.py @@ -22,6 +22,7 @@ from rest_framework import routers from annotation.views import LabelAnnotationView +from langpro_annotator.views import csrf_token from problem.views.problem import ProblemView from .index import index @@ -33,10 +34,10 @@ api_router.register(r"label", LabelAnnotationView, basename="labels") -if settings.PROXY_FRONTEND: - spa_url = re_path(r"^(?P.*)$", proxy_frontend) -else: - spa_url = re_path(r"", index) +# if settings.PROXY_FRONTEND: +# spa_url = re_path(r"^(?P.*)$", proxy_frontend) +# else: +# spa_url = re_path(r"", index) urlpatterns = [ path("admin", RedirectView.as_view(url="/admin/", permanent=True)), @@ -53,6 +54,7 @@ ), ), path("api/i18n/", i18n), + path("api/csrf", csrf_token), path("users/", include("user.urls")), - spa_url, # catch-all; unknown paths to be handled by a SPA + # spa_url, # catch-all; unknown paths to be handled by a SPA ] diff --git a/backend/langpro_annotator/views.py b/backend/langpro_annotator/views.py new file mode 100644 index 00000000..000d95a7 --- /dev/null +++ b/backend/langpro_annotator/views.py @@ -0,0 +1,6 @@ +from django.views.decorators.csrf import ensure_csrf_cookie +from django.http import JsonResponse + +@ensure_csrf_cookie +def csrf_token(request): + return JsonResponse({"detail": "CSRF cookie set"}) diff --git a/backend/requirements.in b/backend/requirements.in index 3637029a..c3fce476 100644 --- a/backend/requirements.in +++ b/backend/requirements.in @@ -2,7 +2,7 @@ Django>=4.0.1,<5 djangorestframework django-livereload-server django-revproxy>=0.10.0 -psycopg2 +psycopg2-binary pytest pytest-django pytest-xdist diff --git a/backend/requirements.txt b/backend/requirements.txt index b5b9c018..664e980e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -48,7 +48,7 @@ packaging==24.2 # via pytest pluggy==1.5.0 # via pytest -psycopg2==2.9.10 +psycopg2-binary==2.9.10 # via -r requirements.in pytest==8.3.5 # via diff --git a/docker-compose.yml b/docker-compose.yml index c81f8e5c..0594fd89 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,46 +2,113 @@ include: - ./langpro-container/compose.yaml services: + nginx: + container_name: la-nginx + restart: unless-stopped + image: nginx:latest + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + ports: + - 127.0.0.1:5000:80 + postgres: + container_name: la-postgres image: postgres:15 + restart: unless-stopped environment: - - POSTGRES_PASSWORD=postgres + - POSTGRES_USER=$POSTGRES_USER + - POSTGRES_PASSWORD=$POSTGRES_PASSWORD + - POSTGRES_DB=$POSTGRES_DB healthcheck: - test: 'psql -c "\l" postgres postgres' - restart: always + test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"] volumes: - postgres-data:/var/lib/postgresql/data - - ./backend/create_db.sql:/docker-entrypoint-initdb.d/langpro.sql ports: - 127.0.0.1:5433:5432 - backend: - build: - context: ./backend + + backend-prod: &backend-prod + container_name: la-backend + image: la-backend-prod + profiles: ["prod"] + build: ./backend + restart: unless-stopped environment: - PGHOST: postgres - LANGPRO_CONTAINER: 'http://langpro:80' - LANGPRO_FRONTEND: 'http://frontend:4200' + - PGHOST=postgres + - PGUSER=$POSTGRES_USER + - PGPASSWORD=$POSTGRES_PASSWORD + - PGDATABASE=$POSTGRES_DB + - DJANGO_SECRET_KEY=$DJANGO_SECRET_KEY + - LANGPRO_CONTAINER=http://langpro:80 + - DJANGO_DEBUG=0 depends_on: postgres: condition: service_healthy healthcheck: - test: 'curl -f localhost:8000/admin/' + test: "curl -f localhost:8000/admin/" volumes: - - ./:/usr/src/app + - ./logs/django:/usr/src/app/logs:z + - ./seed/:/usr/src/app/backend/problem/data:z + command: > + gunicorn langpro_annotator.wsgi:application + -w 4 + -b 0.0.0.0:8000 + --timeout 600 + --log-level debug + --access-logfile /usr/src/app/logs/access_log + --error-logfile /usr/src/app/logs/error_log + --capture-output + + backend-dev: + <<: *backend-prod + image: la-backend-dev + restart: no + profiles: ["dev"] + environment: + - PGHOST=postgres + - LANGPRO_CONTAINER=http://langpro:80 + - DJANGO_DEBUG=1 + volumes: + - ./backend:/usr/src/app ports: - 127.0.0.1:8000:8000 - frontend: + command: sh -c "python manage.py check && python manage.py migrate && python manage.py runserver 0.0.0.0:8000" + + frontend-dev: + image: la-frontend-dev + container_name: la-frontend + restart: no + profiles: ["dev"] build: context: ./frontend + dockerfile: Dockerfile.dev healthcheck: - test: 'curl -f localhost:4200' + test: "curl -f localhost:4200" + expose: + - 4200 + - 9876 volumes: - - ./:/usr/src/app - - frontend-node-modules:/usr/src/app/frontend/node_modules + - ./frontend:/usr/src/app + - frontend-node-modules:/usr/src/app/node_modules + - frontend-angular-cache:/usr/src/app/.angular ports: - 127.0.0.1:4200:4200 - 127.0.0.1:9876:9876 + frontend-prod: + image: la-frontend-prod + container_name: la-frontend + restart: unless-stopped + profiles: ["prod"] + build: + context: ./frontend + dockerfile: Dockerfile.prod + args: + APP_VERSION: ${APP_VERSION:-0.0.0} + SOURCE_URL: https://github.com/CentreForDigitalHumanities/langpro-annotator + healthcheck: + test: "curl -f localhost:4200" + volumes: postgres-data: frontend-node-modules: + frontend-angular-cache: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 00000000..7bb34ad8 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,5 @@ +node_modules +dist +.angular +.cache +.tmp diff --git a/frontend/Dockerfile b/frontend/Dockerfile deleted file mode 100644 index 950c2dce..00000000 --- a/frontend/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM node:20 - -WORKDIR /usr/src/app/frontend -COPY . . - -CMD yarn && yarn ng serve --host 0.0.0.0 --disable-host-check --proxy-config ../proxy.conf.docker.json diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev new file mode 100644 index 00000000..a9b9db37 --- /dev/null +++ b/frontend/Dockerfile.dev @@ -0,0 +1,17 @@ +FROM node:24.14-slim + +# Set the working directory. +WORKDIR /usr/src/app + +# Copy package.json and yarn.lock. +COPY package.json yarn.lock ./ + +# Install dependencies. +RUN yarn global add @angular/cli@19 +RUN yarn install --frozen-lockfile + +# Expose the port for the development server. +EXPOSE 4200 + +# Start the development server. The poll flag allows for live reloading. +CMD ["ng", "serve", "--host", "0.0.0.0", "--disable-host-check", "--poll", "200"] diff --git a/frontend/Dockerfile.prod b/frontend/Dockerfile.prod new file mode 100644 index 00000000..dc50215a --- /dev/null +++ b/frontend/Dockerfile.prod @@ -0,0 +1,39 @@ +ARG NODE_VERSION=24.12.0-alpine +ARG NGINX_VERSION=alpine3.22 +ARG APP_VERSION=0.0.0 +ARG SOURCE_URL=unknown + +FROM node:${NODE_VERSION} AS builder + +# Set the working directory inside the container. +WORKDIR /usr/src/app + +# Copy app dependencies. +COPY package.json yarn.lock ./ + +# Install app dependencies. +RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \ + yarn install --frozen-lockfile + +# Copy app source. +COPY . /usr/src/app + +# Pre-build. +RUN yarn prebuild + +# Build app. +RUN yarn build -- --output-path=./dist/out --configuration production + +FROM nginxinc/nginx-unprivileged:${NGINX_VERSION} AS runner + +# Copy the NGINX configuration file. +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy the built app to the NGINX server with proper ownership. +COPY --chown=nginx:nginx --from=builder /usr/src/app/dist/out/browser /usr/share/nginx/html + +# Use a built-in non-root user for security best practices. +USER nginx + +# Expose port 4200 to allow HTTP traffic. +EXPOSE 4200 diff --git a/frontend/angular.json b/frontend/angular.json index 36ac1c99..ded1ecf7 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -92,10 +92,7 @@ "buildTarget": "langpro-annotator:build:nl" } }, - "defaultConfiguration": "development", - "options": { - "proxyConfig": "../proxy.conf.json" - } + "defaultConfiguration": "development" }, "extract-i18n": { "builder": "ng-extract-i18n-merge:ng-extract-i18n-merge", diff --git a/frontend/build/build-pre.js b/frontend/build/build-pre.js index b7aca361..33d52715 100644 --- a/frontend/build/build-pre.js +++ b/frontend/build/build-pre.js @@ -1,12 +1,20 @@ const path = require('path'); const colors = require('colors/safe'); const fs = require('fs'); -const appVersion = require('../../package.json').version; +const appVersion = process.env.APP_VERSION; +const gitCommit = process.env.GIT_COMMIT; +const sourceUrl = process.env.SOURCE_URL; const { exec } = require('child_process'); console.log(colors.cyan('\nRunning pre-build tasks')); async function getHash() { + // Use environment variable if provided (Docker build) + if (gitCommit) { + return Promise.resolve(gitCommit); + } + + // Otherwise use git (local development) return new Promise((resolve, reject) => { exec('git rev-parse HEAD', (error, stdout, stderr) => { if (error) { @@ -24,6 +32,12 @@ async function getHash() { } async function getRemoteUrl() { + // Use environment variable if provided (Docker build) + if (sourceUrl) { + return Promise.resolve(sourceUrl); + } + + // Otherwise use git (local development) return new Promise((resolve, reject) => { exec('git config --get remote.origin.url', (error, stdout, stderr) => { if (error) { @@ -53,17 +67,28 @@ async function getRemoteUrl() { }); } -Promise.all([getHash(), getRemoteUrl()]).then(([hash, remoteUrl]) => { - writeVersion(hash, remoteUrl); +Promise.all([ + getHash().catch(() => 'unknown'), + getRemoteUrl().catch(() => 'unknown') +]).then(([hash, remoteUrl]) => { + if (hash === 'unknown' || remoteUrl === 'unknown') { + console.log(colors.yellow('Git repository not found, using fallback values')); + writeVersion(sourceUrl || 'unknown'); + return; + } + const sourceUrlWithHash = `${remoteUrl}/tree/${hash}`; + writeVersion(sourceUrlWithHash); }).catch((error) => { console.log(`${colors.red('Could not update version: ')} ${error}`); + // Write version with fallback values anyway + writeVersion(sourceUrl || 'unknown'); }); -function writeVersion(hash, remoteUrl) { +function writeVersion(sourceUrl) { const versionFilePath = path.join(__dirname + '/../src/environments/version.ts'); const src = `export const version = '${appVersion}'; export const buildTime = '${new Date()}'; -export const sourceUrl = '${remoteUrl}/tree/${hash}'; +export const sourceUrl = '${sourceUrl}'; `; // ensure version module pulls value from package.json diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 00000000..c4f476b8 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,30 @@ +server { + include /etc/nginx/mime.types; + + listen 4200; + + gzip on; + gzip_vary on; + gzip_min_length 256; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/x-javascript + application/json + application/xml + application/xml+rss + font/ttf + font/otf + image/svg+xml; + + # Serve the application + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html =404; + } +} diff --git a/frontend/package.json b/frontend/package.json index 2d74adc0..3b6f4b58 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,7 +2,7 @@ "name": "langpro-annotator", "scripts": { "start": "yarn serve", - "build": "ng build --base-href=/static/ --localize", + "build": "ng build", "watch": "ng build --watch", "test": "ng test --watch=true", "serve:ssr:langpro-annotator": "node dist/langpro-annotator/server/server.mjs", diff --git a/frontend/src/app/annotate/annotation-parse-results/annotation-parse-results.component.ts b/frontend/src/app/annotate/annotation-parse-results/annotation-parse-results.component.ts index 8f34ff67..e96ec5af 100644 --- a/frontend/src/app/annotate/annotation-parse-results/annotation-parse-results.component.ts +++ b/frontend/src/app/annotate/annotation-parse-results/annotation-parse-results.component.ts @@ -24,9 +24,9 @@ function unfoldParseResult(parse: CCGParse): UnfoldedParseResult { ...parse, ccgTrees: [ { type: "CCG Tree", tree: ccg_tree }, - // { type: "CCG Term", tree: ccg_term }, - // { type: "Corrected CCG Term", tree: corr_term }, - // { type: "Lambda Logical Form", tree: llf } + { type: "CCG Term", tree: ccg_term }, + { type: "Corrected CCG Term", tree: corr_term }, + { type: "Lambda Logical Form", tree: llf } ] }; } diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 366bccbc..65f4f47d 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,43 +1,51 @@ -import { Component, Inject, afterRender } from "@angular/core"; -import { DOCUMENT } from "@angular/common"; -import { RouterOutlet } from "@angular/router"; -import { MenuComponent } from "./menu/menu.component"; -import { FooterComponent } from "./footer/footer.component"; -import { DarkModeService } from "./services/dark-mode.service"; -import { ToastContainerComponent } from "./toast-container/toast-container.component"; - -@Component({ - selector: "la-root", - standalone: true, - imports: [ - RouterOutlet, - MenuComponent, - FooterComponent, - ToastContainerComponent, - ], - templateUrl: "./app.component.html", - styleUrl: "./app.component.scss", -}) -export class AppComponent { - title = "LangPro Annotator"; - - constructor( - @Inject(DOCUMENT) private document: Document, - private darkModeService: DarkModeService - ) { - // Using the DOM API to only render on the browser instead of on the server - afterRender(() => { - const style = this.document.createElement("link"); - style.rel = "stylesheet"; - this.document.head.append(style); - - this.darkModeService.theme$.subscribe((theme) => { - this.document.documentElement.setAttribute( - "data-bs-theme", - theme - ); - style.href = `${theme}.css`; - }); - }); - } -} +import { Component, DestroyRef, Inject, afterRender, inject } from "@angular/core"; +import { DOCUMENT } from "@angular/common"; +import { RouterOutlet } from "@angular/router"; +import { MenuComponent } from "./menu/menu.component"; +import { FooterComponent } from "./footer/footer.component"; +import { DarkModeService } from "./services/dark-mode.service"; +import { ToastContainerComponent } from "./toast-container/toast-container.component"; +import { HttpClient } from "@angular/common/http"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; + +@Component({ + selector: "la-root", + standalone: true, + imports: [ + RouterOutlet, + MenuComponent, + FooterComponent, + ToastContainerComponent, + ], + templateUrl: "./app.component.html", + styleUrl: "./app.component.scss", +}) +export class AppComponent { + title = "LangPro Annotator"; + http = inject(HttpClient); + destroyRef = inject(DestroyRef); + + constructor( + @Inject(DOCUMENT) private document: Document, + private darkModeService: DarkModeService + ) { + // Using the DOM API to only render on the browser instead of on the server + afterRender(() => { + const style = this.document.createElement("link"); + style.rel = "stylesheet"; + this.document.head.append(style); + + this.darkModeService.theme$.subscribe((theme) => { + this.document.documentElement.setAttribute( + "data-bs-theme", + theme + ); + style.href = `${theme}.css`; + }); + }); + + this.http.get("/api/csrf").pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe() + } +} diff --git a/langpro-container b/langpro-container index 0a358141..d0a12411 160000 --- a/langpro-container +++ b/langpro-container @@ -1 +1 @@ -Subproject commit 0a358141bf653843e91817c450f870a15553f8f9 +Subproject commit d0a12411b459316ac6d6474ca9efa3a5e8fdc023 diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 00000000..d8b327ca --- /dev/null +++ b/nginx.conf @@ -0,0 +1,45 @@ +events { worker_connections 1024; } + +http { + include /etc/nginx/mime.types; + + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Port $server_port; + + server { + listen 80; + + location /api { + proxy_pass http://la-backend:8000/api; + proxy_read_timeout 30s; + proxy_connect_timeout 30s; + } + + location /users { + proxy_pass http://la-backend:8000/users; + proxy_read_timeout 30s; + proxy_connect_timeout 30s; + } + + location /admin { + proxy_pass http://la-backend:8000/admin; + proxy_read_timeout 30s; + proxy_connect_timeout 30s; + } + + location /static { + proxy_pass http://la-backend:8000/static; + proxy_read_timeout 30s; + proxy_connect_timeout 30s; + } + + location / { + proxy_pass http://la-frontend:4200; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + } + } +} diff --git a/proxy.conf.docker.json b/proxy.conf.docker.json index 4f67c4aa..99de5119 100644 --- a/proxy.conf.docker.json +++ b/proxy.conf.docker.json @@ -1,18 +1,18 @@ { "/api": { - "target": "http://backend:8000", + "target": "http://la-backend:8000", "secure": false }, "/users": { - "target": "http://backend:8000", + "target": "http://la-backend:8000", "secure": false }, "/admin": { - "target": "http://backend:8000", + "target": "http://la-backend:8000", "secure": false }, "/static": { - "target": "http://backend:8000", + "target": "http://la-backend:8000", "secure": false } }