commit b5a5b973f73cedcf142e393f38e1a91cd8c08f83 Author: Emmanuel Date: Tue Jan 6 22:17:37 2026 +0100 Initial commit with GitLab CI/CD pipeline diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..71a16a5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,49 @@ +# Git +.git/ +.gitignore +.gitattributes + +# CI/CD +.gitlab-ci.yml + +# Environment files (never include in image) +.env +.env.* +!.env.example + +# Documentation +README.md +*.md +docs/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +logs/ + +# Test files (optional - include if you want tests in image) +*.test.ts +**/*.test.ts + +# Build artifacts +*.tar +image.tar + +# Deno cache (will be regenerated in container) +.deno/ + +# JetBrains +deep-lite.iml + +# Claude Code plans +.claude/ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..da15287 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# DeepL API Configuration +DEEPL_AUTH_KEY=your-deepl-api-key-here + +# Bearer Token for API Authentication +# Generate a secure random token (e.g., using: openssl rand -hex 32) +BEARER_TOKEN=your-secure-bearer-token-here + +# Server Port (optional, defaults to 8000) +PORT=8000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d8b22fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Environment variables +.env +.env.local + +# Deno +.deno/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +logs/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..c462780 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,170 @@ +stages: + - validate + - sonar + - test + - build + +variables: + DENO_VERSION: "2.6.4" + DOCKER_IMAGE_NAME: "$CI_REGISTRY/deeplite" + PORT: "8000" + +# Global template for Deno jobs +.deno_template: + image: denoland/deno:${DENO_VERSION} + before_script: + - deno --version + +# ===== VALIDATION STAGE ===== + +fmt:check: + extends: .deno_template + stage: validate + script: + - deno fmt --check + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + - if: "$CI_COMMIT_BRANCH" + +lint:check: + extends: .deno_template + stage: validate + script: + - deno lint + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + - if: "$CI_COMMIT_BRANCH" + +type:check: + extends: .deno_template + stage: validate + script: + - deno check src/main.ts + - deno check src/main.test.ts + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + - if: "$CI_COMMIT_BRANCH" + +# ===== SONARQUBE STAGE ===== + +sonarqube:scan: + stage: sonar + image: + name: sonarsource/sonar-scanner-cli:latest + entrypoint: [""] + variables: + SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" + GIT_DEPTH: "0" + cache: + key: "${CI_JOB_NAME}" + paths: + - .sonar/cache + script: + - | + sonar-scanner \ + -Dsonar.host.url="${SONAR_HOST}" \ + -Dsonar.token="${SONAR_TOKEN}" \ + -Dsonar.projectKey="${CI_PROJECT_PATH_SLUG}" \ + -Dsonar.projectName="${CI_PROJECT_NAME}" \ + -Dsonar.projectVersion="${CI_COMMIT_SHORT_SHA}" \ + -Dsonar.sources=src \ + -Dsonar.sourceEncoding=UTF-8 \ + -Dsonar.language=ts \ + -Dsonar.exclusions="**/*.test.ts,**/test/**" \ + -Dsonar.tests=src \ + -Dsonar.test.inclusions="**/*.test.ts" + allow_failure: true + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + - if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH" + dependencies: [] + +# ===== TEST STAGE ===== + +integration:test: + extends: .deno_template + stage: test + variables: + DEEPL_AUTH_KEY: "${DEEPL_AUTH_KEY}" + BEARER_TOKEN: "${BEARER_TOKEN}" + PORT: "8000" + script: + # Start server in background + - echo "Starting server in background..." + - deno run --allow-net --allow-env src/main.ts & + - SERVER_PID=$! + - echo "Server started with PID $SERVER_PID" + + # Wait for server to be ready (poll health endpoint) + - echo "Waiting for server to be ready..." + - | + deno eval " + for (let i = 1; i <= 30; i++) { + try { + await fetch('http://localhost:8000/health'); + console.log('Server is ready!'); + Deno.exit(0); + } catch { + console.log('Waiting for server... attempt ' + i + '/30'); + await new Promise(r => setTimeout(r, 1000)); + } + } + console.log('Server health check failed'); + Deno.exit(1); + " + + # Run tests + - echo "Running integration tests..." + - deno test --allow-net --allow-env src/main.test.ts + + # Cleanup: Kill the server + - echo "Stopping server..." + - kill $SERVER_PID || true + - wait $SERVER_PID || true + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + - if: "$CI_COMMIT_BRANCH" + retry: + max: 2 + when: + - runner_system_failure + - stuck_or_timeout_failure + +# ===== BUILD & PUSH STAGE (Tags only) ===== + +docker:build-push: + stage: build + image: docker:29-dind + services: + - docker:29-dind + variables: + DOCKER_TLS_CERTDIR: "/certs" + before_script: + - docker info + - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" + script: + - export VERSION=${CI_COMMIT_TAG#v} + - echo "Building and pushing Docker image version $VERSION" + + - | + docker build \ + --build-arg DENO_VERSION=${DENO_VERSION} \ + --tag ${DOCKER_IMAGE_NAME}:${VERSION} \ + --tag ${DOCKER_IMAGE_NAME}:${CI_COMMIT_SHA} \ + --tag ${DOCKER_IMAGE_NAME}:latest \ + . + + - docker push ${DOCKER_IMAGE_NAME}:${VERSION} + - docker push ${DOCKER_IMAGE_NAME}:${CI_COMMIT_SHA} + - docker push ${DOCKER_IMAGE_NAME}:latest + + - echo "Successfully pushed:" + - echo " - ${DOCKER_IMAGE_NAME}:${VERSION}" + - echo " - ${DOCKER_IMAGE_NAME}:${CI_COMMIT_SHA}" + - echo " - ${DOCKER_IMAGE_NAME}:latest" + rules: + - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/' + retry: + max: 2 + when: + - runner_system_failure diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..436e1e2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +# Use official Deno runtime as base image +ARG DENO_VERSION=2.6.4 +FROM denoland/deno:${DENO_VERSION} + +# Set working directory +WORKDIR /app + +# Copy dependency manifest first (better layer caching) +COPY deno.json deno.lock ./ + +# Copy source code +COPY src/ ./src/ + +# Cache dependencies by checking the main file +# This downloads and caches all imports +RUN deno cache src/main.ts + +# Expose port (default: 8000) +EXPOSE 8000 + +# Create non-root user for security +RUN useradd -m -u 1001 denouser && \ + chown -R denouser:denouser /app + +USER denouser + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD deno eval "fetch('http://localhost:8000/health').then(r => r.ok ? Deno.exit(0) : Deno.exit(1))" || exit 1 + +# Run the application +CMD ["deno", "run", "--allow-net", "--allow-env", "src/main.ts"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..235d98d --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# DeepLite BFF Server + +Backend-for-Frontend proxy server for the DeepLite PWA. + +## Configuration + +Create a `.env` file: + +```env +DEEPL_AUTH_KEY=your-deepl-api-key +BEARER_TOKEN=your-secure-token +PORT=8000 +``` + +## API Endpoints + +### POST /translate + +Translates text using the DeepL API. + +```bash +curl -X POST http://localhost:8000/translate \ + -H "Authorization: Bearer YOUR_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"text": "Hello world!", "target_lang": "FR"}' +``` + +**Parameters:** + +- `text` (required): Text to translate +- `target_lang` (required): Target language code (e.g., "FR", "DE", "ES") +- `source_lang` (optional): Source language code (auto-detected if not provided) + +### GET /languages + +Retrieves the list of supported languages. + +```bash +curl http://localhost:8000/languages \ + -H "Authorization: Bearer YOUR_BEARER_TOKEN" +``` + +### GET /health + +Health check endpoint (no authentication required). + +```bash +curl http://localhost:8000/health +``` diff --git a/deep-lite.iml b/deep-lite.iml new file mode 100644 index 0000000..9a5cfce --- /dev/null +++ b/deep-lite.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..0beb3ba --- /dev/null +++ b/deno.json @@ -0,0 +1,24 @@ +{ + "tasks": { + "dev": "deno run --allow-net --allow-env --watch src/main.ts", + "start": "deno run --allow-net --allow-env src/main.ts", + "test": "deno test --allow-net --allow-env src/main.test.ts" + }, + "fmt": { + "useTabs": false, + "lineWidth": 100, + "indentWidth": 2, + "semiColons": true, + "singleQuote": false, + "proseWrap": "preserve" + }, + "lint": { + "rules": { + "tags": ["recommended"] + } + }, + "compilerOptions": { + "lib": ["deno.window"], + "strict": true + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..7c366dc --- /dev/null +++ b/deno.lock @@ -0,0 +1,191 @@ +{ + "version": "5", + "remote": { + "https://deno.land/std@0.200.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", + "https://deno.land/std@0.200.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", + "https://deno.land/std@0.200.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", + "https://deno.land/std@0.200.0/async/deferred.ts": "42790112f36a75a57db4a96d33974a936deb7b04d25c6084a9fa8a49f135def8", + "https://deno.land/std@0.200.0/bytes/bytes_list.ts": "31d664f4d42fa922066405d0e421c56da89d751886ee77bbe25a88bf0310c9d0", + "https://deno.land/std@0.200.0/bytes/concat.ts": "d26d6f3d7922e6d663dacfcd357563b7bf4a380ce5b9c2bbe0c8586662f25ce2", + "https://deno.land/std@0.200.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", + "https://deno.land/std@0.200.0/bytes/ends_with.ts": "4228811ebc71615d27f065c54b5e815ec1972538772b0f413c0efe05245b472e", + "https://deno.land/std@0.200.0/bytes/equals.ts": "fc190cce412b2136979181b163ec7e05f7e7a947e39102eee4b8c0d62519ddf9", + "https://deno.land/std@0.200.0/bytes/includes_needle.ts": "76a8163126fb2f8bf86fd7f22192c3bb04bf6a20b987a095127c2ca08adf3ba6", + "https://deno.land/std@0.200.0/bytes/index_of_needle.ts": "9c06610e9611b5647ac25952e71a22e09227c9f1b8cbeeb33399bf8bf8a7f649", + "https://deno.land/std@0.200.0/bytes/last_index_of_needle.ts": "f1602f221c3b678bc4f1e1c88a70a15ab7da32c21751dbbc6c957c956951d784", + "https://deno.land/std@0.200.0/bytes/mod.ts": "e869bba1e7a2e3a9cc6c2d55471888429a544e70a840c087672e656e7ba21815", + "https://deno.land/std@0.200.0/bytes/repeat.ts": "6f5e490d8d72bcbf8d84a6bb04690b9b3eb5822c5a11687bca73a2318a842294", + "https://deno.land/std@0.200.0/bytes/starts_with.ts": "3e607a70c9c09f5140b7a7f17a695221abcc7244d20af3eb47ccbb63f5885135", + "https://deno.land/std@0.200.0/crypto/keystack.ts": "877ab0f19eb7d37ad6495190d3c3e39f58e9c52e0b6a966f82fd6df67ca55f90", + "https://deno.land/std@0.200.0/crypto/timing_safe_equal.ts": "7b0a4d2ef1c17590e0ad6c0cb1776369d2ba80cd99e945005e117851690507fe", + "https://deno.land/std@0.200.0/encoding/base64.ts": "144ae6234c1fbe5b68666c711dc15b1e9ee2aef6d42b3b4345bf9a6c91d70d0d", + "https://deno.land/std@0.200.0/encoding/base64url.ts": "2ed4ba122b20fedf226c5d337cf22ee2024fa73a8f85d915d442af7e9ce1fae1", + "https://deno.land/std@0.200.0/http/_negotiation/common.ts": "14d1a52427ab258a4b7161cd80e1d8a207b7cc64b46e911780f57ead5f4323c6", + "https://deno.land/std@0.200.0/http/_negotiation/encoding.ts": "ff747d107277c88cb7a6a62a08eeb8d56dad91564cbcccb30694d5dc126dcc53", + "https://deno.land/std@0.200.0/http/_negotiation/language.ts": "7bcddd8db3330bdb7ce4fc00a213c5547c1968139864201efd67ef2d0d51887d", + "https://deno.land/std@0.200.0/http/_negotiation/media_type.ts": "58847517cd549384ad677c0fe89e0a4815be36fe7a303ea63cee5f6a1d7e1692", + "https://deno.land/std@0.200.0/http/cookie_map.ts": "64601025a7d24c3ebd80a169ccc99145bdbfc60606935348e1d4366d0bf9010d", + "https://deno.land/std@0.200.0/http/etag.ts": "807382795850cde5c437c74bcc09392bc0fc56de348fc1271f383f4b28935b9f", + "https://deno.land/std@0.200.0/http/http_errors.ts": "bbda34819060af86537cecc9dc8e045f877130808b7e7acde4197c5328e852d0", + "https://deno.land/std@0.200.0/http/http_status.ts": "8a7bcfe3ac025199ad804075385e57f63d055b2aed539d943ccc277616d6f932", + "https://deno.land/std@0.200.0/http/method.ts": "e66c2a015cb46c21ab0bb3589aa4fca43143a506cb324ffdfd42d2edef7bc0c4", + "https://deno.land/std@0.200.0/http/negotiation.ts": "46e74a6bad4b857333a58dc5b50fe8e5a4d5267e97292293ea65f980bd918086", + "https://deno.land/std@0.200.0/http/server_sent_event.ts": "29f707c1afa5278ac0315ac115ee679d6b93596d5af3fad5ef33f04254ca76c1", + "https://deno.land/std@0.200.0/http/user_agent.ts": "35d3c70d0926b0e121b8c1bbc324b3522479158acaa4f0c43928362b7bf4e6f4", + "https://deno.land/std@0.200.0/io/buf_reader.ts": "2bccff0878537ef201c5051fc0db0ce8196388c5ea69d2be6be1900fe48c5f4b", + "https://deno.land/std@0.200.0/io/buf_writer.ts": "48c33c8f00b61dcbc7958706741cec8e59810bd307bc6a326cbd474fe8346dfd", + "https://deno.land/std@0.200.0/io/buffer.ts": "4d6883daeb2e698579c4064170515683d69f40f3de019bfe46c5cf31e74ae793", + "https://deno.land/std@0.200.0/io/copy_n.ts": "c055296297b9d4897d90d1ac056b072dc02614e60c67f438e23fbce052ea4c69", + "https://deno.land/std@0.200.0/io/limited_reader.ts": "6c9a216f8eef39c1ee2a6b37a29372c8fc63455b2eeb91f06d9646f8f759fc8b", + "https://deno.land/std@0.200.0/io/mod.ts": "2665bcccc1fd6e8627cca167c3e92aaecbd9897556b6f69e6d258070ef63fd9b", + "https://deno.land/std@0.200.0/io/multi_reader.ts": "9c2a0a31686c44b277e16da1d97b4686a986edcee48409b84be25eedbc39b271", + "https://deno.land/std@0.200.0/io/read_delim.ts": "c02b93cc546ae8caad8682ae270863e7ace6daec24c1eddd6faabc95a9d876a3", + "https://deno.land/std@0.200.0/io/read_int.ts": "7cb8bcdfaf1107586c3bacc583d11c64c060196cb070bb13ae8c2061404f911f", + "https://deno.land/std@0.200.0/io/read_lines.ts": "c526c12a20a9386dc910d500f9cdea43cba974e853397790bd146817a7eef8cc", + "https://deno.land/std@0.200.0/io/read_long.ts": "f0aaa420e3da1261c5d33c5e729f09922f3d9fa49f046258d4ff7a00d800c71e", + "https://deno.land/std@0.200.0/io/read_range.ts": "46a2263d0f8369b6d9abb0b25d99ceb65ff08d621fc57bcc53832e6979295043", + "https://deno.land/std@0.200.0/io/read_short.ts": "805cb329574b850b84bf14a92c052c59b5977a492cd780c41df8ad40826c1a20", + "https://deno.land/std@0.200.0/io/read_string_delim.ts": "5dc9f53bdf78e7d4ee1e56b9b60352238ab236a71c3e3b2a713c3d78472a53ce", + "https://deno.land/std@0.200.0/io/slice_long_to_bytes.ts": "48d9bace92684e880e46aa4a2520fc3867f9d7ce212055f76ecc11b22f9644b7", + "https://deno.land/std@0.200.0/io/string_reader.ts": "da0f68251b3d5b5112485dfd4d1b1936135c9b4d921182a7edaf47f74c25cc8f", + "https://deno.land/std@0.200.0/io/string_writer.ts": "8a03c5858c24965a54c6538bed15f32a7c72f5704a12bda56f83a40e28e5433e", + "https://deno.land/std@0.200.0/media_types/_db.ts": "7606d83e31f23ce1a7968cbaee852810c2cf477903a095696cdc62eaab7ce570", + "https://deno.land/std@0.200.0/media_types/_util.ts": "916efbd30b6148a716f110e67a4db29d6949bf4048997b754415dd7e42c52378", + "https://deno.land/std@0.200.0/media_types/content_type.ts": "ad98a5aa2d95f5965b2796072284258710a25e520952376ed432b0937ce743bc", + "https://deno.land/std@0.200.0/media_types/extension.ts": "a7cd28c9417143387cdfed27d4e8607ebcf5b1ec27eb8473d5b000144689fe65", + "https://deno.land/std@0.200.0/media_types/extensions_by_type.ts": "43806d6a52a0d6d965ada9d20e60a982feb40bc7a82268178d94edb764694fed", + "https://deno.land/std@0.200.0/media_types/format_media_type.ts": "f5e1073c05526a6f5a516ac5c5587a1abd043bf1039c71cde1166aa4328c8baf", + "https://deno.land/std@0.200.0/media_types/get_charset.ts": "18b88274796fda5d353806bf409eb1d2ddb3f004eb4bd311662c4cdd8ac173db", + "https://deno.land/std@0.200.0/media_types/mod.ts": "d3f0b99f85053bc0b98ecc24eaa3546dfa09b856dc0bbaf60d8956d2cdd710c8", + "https://deno.land/std@0.200.0/media_types/parse_media_type.ts": "8cb0144385c555c9ce81881b7cee3fbb746f23b4af988fecdb7bd01ef8cc35b1", + "https://deno.land/std@0.200.0/media_types/type_by_extension.ts": "daa801eb0f11cdf199445d0f1b656cf116d47dcf9e5b85cc1e6b4469f5ee0432", + "https://deno.land/std@0.200.0/media_types/vendor/mime-db.v1.52.0.ts": "6925bbcae81ca37241e3f55908d0505724358cda3384eaea707773b2c7e99586", + "https://deno.land/std@0.200.0/path/_basename.ts": "057d420c9049821f983f784fd87fa73ac471901fb628920b67972b0f44319343", + "https://deno.land/std@0.200.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", + "https://deno.land/std@0.200.0/path/_dirname.ts": "355e297236b2218600aee7a5301b937204c62e12da9db4b0b044993d9e658395", + "https://deno.land/std@0.200.0/path/_extname.ts": "eaaa5aae1acf1f03254d681bd6a8ce42a9cb5b7ff2213a9d4740e8ab31283664", + "https://deno.land/std@0.200.0/path/_format.ts": "4a99270d6810f082e614309164fad75d6f1a483b68eed97c830a506cc589f8b4", + "https://deno.land/std@0.200.0/path/_from_file_url.ts": "7e4e5626089785adddb061f1b9f4932d6b21c7df778e7449531a11e32048245c", + "https://deno.land/std@0.200.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", + "https://deno.land/std@0.200.0/path/_is_absolute.ts": "05dac10b5e93c63198b92e3687baa2be178df5321c527dc555266c0f4f51558c", + "https://deno.land/std@0.200.0/path/_join.ts": "fd78555bc34d5f188918fc7018dfe8fe2df5bbad94a3b30a433666c03934d77f", + "https://deno.land/std@0.200.0/path/_normalize.ts": "a19ec8706b2707f9dd974662a5cd89fad438e62ab1857e08b314a8eb49a34d81", + "https://deno.land/std@0.200.0/path/_parse.ts": "0f9b0ff43682dd9964eb1c4398610c4e165d8db9d3ac9d594220217adf480cfa", + "https://deno.land/std@0.200.0/path/_relative.ts": "27bdeffb5311a47d85be26d37ad1969979359f7636c5cd9fcf05dcd0d5099dc5", + "https://deno.land/std@0.200.0/path/_resolve.ts": "7a3616f1093735ed327e758313b79c3c04ea921808ca5f19ddf240cb68d0adf6", + "https://deno.land/std@0.200.0/path/_to_file_url.ts": "739bfda583598790b2e77ce227f2bb618f6ebdb939788cea47555b43970ec58c", + "https://deno.land/std@0.200.0/path/_to_namespaced_path.ts": "0d5f4caa2ed98ef7a8786286df6af804b50e38859ae897b5b5b4c8c5930a75c8", + "https://deno.land/std@0.200.0/path/_util.ts": "4e191b1bac6b3bf0c31aab42e5ca2e01a86ab5a0d2e08b75acf8585047a86221", + "https://deno.land/std@0.200.0/path/basename.ts": "6f08fbb90dbfcf320765b3abb01f995b1723f75e2534acfd5380e202c802a3aa", + "https://deno.land/std@0.200.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", + "https://deno.land/std@0.200.0/path/dirname.ts": "098996822a31b4c46e1eb52a19540d3c6f9f54b772fc8a197939eeabc29fca2f", + "https://deno.land/std@0.200.0/path/extname.ts": "9b83c62fd16505739541f7a3ab447d8972da39dbf668d47af2f93206c2480893", + "https://deno.land/std@0.200.0/path/format.ts": "cb22f95cc7853d590b87708cc9441785e760d711188facff3d225305a8213aca", + "https://deno.land/std@0.200.0/path/from_file_url.ts": "a6221cfc928928ec4d9786d767dfac98fa2ab746af0786446c9834a07b98817e", + "https://deno.land/std@0.200.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", + "https://deno.land/std@0.200.0/path/is_absolute.ts": "6b3d36352eb7fa29edb53f9e7b09b1aeb022a3c5465764f6cc5b8c41f9736197", + "https://deno.land/std@0.200.0/path/join.ts": "4a2867ff2f3c81ffc9eb3d56dade16db6f8bd3854f269306d23dad4115089c84", + "https://deno.land/std@0.200.0/path/mod.ts": "7765507696cb321994cdacfc19ee3ba61e8e3ebf4bd98fa75a276cf5dc18ce2a", + "https://deno.land/std@0.200.0/path/normalize.ts": "7d992cd262b2deefa842d93a8ba2ed51f3949ba595b1d07f627ac2cddbc74808", + "https://deno.land/std@0.200.0/path/parse.ts": "031fe488b3497fb8312fc1dc3c3d6c2d80707edd9c661e18ee9fd20f95edf322", + "https://deno.land/std@0.200.0/path/posix.ts": "0a1c1952d132323a88736d03e92bd236f3ed5f9f079e5823fae07c8d978ee61b", + "https://deno.land/std@0.200.0/path/relative.ts": "7db80c5035016174267da16321a742d76e875215c317859a383b12f413c6f5d6", + "https://deno.land/std@0.200.0/path/resolve.ts": "103b62207726a27f28177f397008545804ecb20aaf00623af1f622b18cd80b9f", + "https://deno.land/std@0.200.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", + "https://deno.land/std@0.200.0/path/to_file_url.ts": "dd32f7a01bbf3b15b5df46796659984b372973d9b2d7d59bcf0eb990763a0cb5", + "https://deno.land/std@0.200.0/path/to_namespaced_path.ts": "4e643ab729bf49ccdc166ad48d2de262ff462938fcf2a44a4425588f4a0bd690", + "https://deno.land/std@0.200.0/path/win32.ts": "8b3f80ef7a462511d5e8020ff490edcaa0a0d118f1b1e9da50e2916bdd73f9dd", + "https://deno.land/std@0.200.0/streams/_common.ts": "f45cba84f0d813de3326466095539602364a9ba521f804cc758f7a475cda692d", + "https://deno.land/std@0.200.0/streams/buffer.ts": "6cd773d22cf21bb988a98cc551b5abfc4c3b03516f93eaa3fb6f2f6e16032deb", + "https://deno.land/std@0.200.0/streams/byte_slice_stream.ts": "c46d7c74836fc8c1a9acd9fe211cbe1bbaaee1b36087c834fb03af4991135c3a", + "https://deno.land/std@0.200.0/streams/copy.ts": "75cbc795ff89291df22ddca5252de88b2e16d40c85d02840593386a8a1454f71", + "https://deno.land/std@0.200.0/streams/delimiter_stream.ts": "f69e849b3d1f59f02424497273f411105a6f76a9f13da92aeeb9a2d554236814", + "https://deno.land/std@0.200.0/streams/early_zip_readable_streams.ts": "4005fa74162b943f79899e5d7cb96adcbc0a6b867f9144974ed12d30e0a556e1", + "https://deno.land/std@0.200.0/streams/iterate_reader.ts": "bbec1d45c2df2c0c5920bad0549351446fdc8e0886d99e95959b259dbcdb6072", + "https://deno.land/std@0.200.0/streams/limited_bytes_transform_stream.ts": "05dc592ffaab83257494d22dd53917e56243c26e5e3129b3f13ddbbbc4785048", + "https://deno.land/std@0.200.0/streams/limited_transform_stream.ts": "d69ab790232c1b86f53621ad41ef03c235f2abb4b7a1cd51960ad6e12ee55e38", + "https://deno.land/std@0.200.0/streams/merge_readable_streams.ts": "dc2db0cbf1b14d999aa2aa2a2a1ba24ce58953878f29845ed9319321d0a01fab", + "https://deno.land/std@0.200.0/streams/mod.ts": "c07ec010e700b9ea887dc36ca08729828bc7912f711e4054e24d33fd46282252", + "https://deno.land/std@0.200.0/streams/read_all.ts": "ee319772fb0fd28302f97343cc48dfcf948f154fd0d755d8efe65814b70533be", + "https://deno.land/std@0.200.0/streams/readable_stream_from_iterable.ts": "a355e97ba8671a4611ae9671c1f33c4a7e6a1b42fbdc9a399338ac3d6f875364", + "https://deno.land/std@0.200.0/streams/readable_stream_from_reader.ts": "bfc416c4576a30aac6b9af22c9dc292c20c6742141ee7c55b5e85460beb0c54e", + "https://deno.land/std@0.200.0/streams/reader_from_iterable.ts": "55f68110dce3f8f2c87b834d95f153bc904257fc65175f9f2abe78455cb8047c", + "https://deno.land/std@0.200.0/streams/reader_from_stream_reader.ts": "fa4971e5615a010e49492c5d1688ca1a4d17472a41e98b498ab89a64ebd7ac73", + "https://deno.land/std@0.200.0/streams/text_delimiter_stream.ts": "20e680ab8b751390e359288ce764f9c47d164af11a263870746eeca4bc7d976b", + "https://deno.land/std@0.200.0/streams/text_line_stream.ts": "0f2c4b33a5fdb2476f2e060974cba1347cefe99a4af33c28a57524b1a34750fa", + "https://deno.land/std@0.200.0/streams/to_transform_stream.ts": "89fd367cafb3b6d80d61e2f4f1fcf66cc75723ecee8d474b495f022264ec6c3b", + "https://deno.land/std@0.200.0/streams/writable_stream_from_writer.ts": "56fff5c82fb736fdd669b567cc0b2bbbe0351002cd13254eae26c366e2bed89a", + "https://deno.land/std@0.200.0/streams/write_all.ts": "aec90152978581ea62d56bb53a5cbf487e6a89c902f87c5969681ffbdf32b998", + "https://deno.land/std@0.200.0/streams/writer_from_stream_writer.ts": "07c7ee025151a190f37fc42cbb01ff93afc949119ebddc6e0d0df14df1bf6950", + "https://deno.land/std@0.200.0/streams/zip_readable_streams.ts": "a9d81aa451240f79230add674809dbee038d93aabe286e2d9671e66591fc86ca", + "https://deno.land/std@0.208.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9", + "https://deno.land/std@0.208.0/assert/_diff.ts": "58e1461cc61d8eb1eacbf2a010932bf6a05b79344b02ca38095f9b805795dc48", + "https://deno.land/std@0.208.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", + "https://deno.land/std@0.208.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", + "https://deno.land/std@0.208.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c", + "https://deno.land/std@0.208.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9", + "https://deno.land/std@0.208.0/assert/assert_equals.ts": "d8ec8a22447fbaf2fc9d7c3ed2e66790fdb74beae3e482855d75782218d68227", + "https://deno.land/std@0.208.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7", + "https://deno.land/std@0.208.0/assert/assert_false.ts": "0ccbcaae910f52c857192ff16ea08bda40fdc79de80846c206bfc061e8c851c6", + "https://deno.land/std@0.208.0/assert/assert_greater.ts": "ae2158a2d19313bf675bf7251d31c6dc52973edb12ac64ac8fc7064152af3e63", + "https://deno.land/std@0.208.0/assert/assert_greater_or_equal.ts": "1439da5ebbe20855446cac50097ac78b9742abe8e9a43e7de1ce1426d556e89c", + "https://deno.land/std@0.208.0/assert/assert_instance_of.ts": "3aedb3d8186e120812d2b3a5dea66a6e42bf8c57a8bd927645770bd21eea554c", + "https://deno.land/std@0.208.0/assert/assert_is_error.ts": "c21113094a51a296ffaf036767d616a78a2ae5f9f7bbd464cd0197476498b94b", + "https://deno.land/std@0.208.0/assert/assert_less.ts": "aec695db57db42ec3e2b62e97e1e93db0063f5a6ec133326cc290ff4b71b47e4", + "https://deno.land/std@0.208.0/assert/assert_less_or_equal.ts": "5fa8b6a3ffa20fd0a05032fe7257bf985d207b85685fdbcd23651b70f928c848", + "https://deno.land/std@0.208.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b", + "https://deno.land/std@0.208.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754", + "https://deno.land/std@0.208.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22", + "https://deno.land/std@0.208.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0", + "https://deno.land/std@0.208.0/assert/assert_not_strict_equals.ts": "4cdef83df17488df555c8aac1f7f5ec2b84ad161b6d0645ccdbcc17654e80c99", + "https://deno.land/std@0.208.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54", + "https://deno.land/std@0.208.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057", + "https://deno.land/std@0.208.0/assert/assert_strict_equals.ts": "b1f538a7ea5f8348aeca261d4f9ca603127c665e0f2bbfeb91fa272787c87265", + "https://deno.land/std@0.208.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c", + "https://deno.land/std@0.208.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd", + "https://deno.land/std@0.208.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", + "https://deno.land/std@0.208.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece", + "https://deno.land/std@0.208.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278", + "https://deno.land/std@0.208.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085", + "https://deno.land/std@0.208.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a", + "https://deno.land/std@0.208.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536", + "https://deno.land/std@0.208.0/dotenv/load.ts": "0636983549b98f29ab75c9a22a42d9723f0a389ece5498fe971e7bb2556a12e2", + "https://deno.land/std@0.208.0/dotenv/mod.ts": "039468f5c87d39b69d7ca6c3d68ebca82f206ec0ff5e011d48205eea292ea5a6", + "https://deno.land/std@0.208.0/fmt/colors.ts": "34b3f77432925eb72cf0bfb351616949746768620b8e5ead66da532f93d10ba2", + "https://deno.land/x/cors@v1.2.2/abcCors.ts": "cdf83a7eaa69a1bf3ab910d18b9422217902fac47601adcaf0afac5a61845d48", + "https://deno.land/x/cors@v1.2.2/attainCors.ts": "7d6aba0f942495cc31119604e0895c9bb8edd8f8baa7fe78e6c655bd0b4cbf59", + "https://deno.land/x/cors@v1.2.2/cors.ts": "0e2d9167e3685f9bcf48f565e312b6e1883fa458f7337e5ce7bc2e3b29767980", + "https://deno.land/x/cors@v1.2.2/mithCors.ts": "3a359d6e716e0410ede278ab54d875b293a2d66d838aaa7cfbf9ddc1e9e990d3", + "https://deno.land/x/cors@v1.2.2/mod.ts": "2b351913f56d77ad80cb3b8633d4539c9eeddb426dae79437ada0e6a9cb4f1a6", + "https://deno.land/x/cors@v1.2.2/oakCors.ts": "1348dc7673c61b85d2e80559a7b44f8e0246eaa6bcc6ec744fafe5d9b13b5c71", + "https://deno.land/x/cors@v1.2.2/opineCors.ts": "fb5790115c26b7061d84b8d6c17d258a1e241bcab75b0bc3ca1fdb2e57bc5072", + "https://deno.land/x/cors@v1.2.2/types.ts": "97546633ccc7f0df7a29bacba5d91dc6f61decdd1b65258300244dba905d34b8", + "https://deno.land/x/oak@v12.6.1/application.ts": "3028d3f6fa5ee743de013881550d054372c11d83c45099c2d794033786d27008", + "https://deno.land/x/oak@v12.6.1/body.ts": "1899761b97fc9d776f3710b2637fb047ba29b968609afc6c0e5219b1108e703c", + "https://deno.land/x/oak@v12.6.1/buf_reader.ts": "26640736541598dbd9f2b84a9d0595756afff03c9ca55b66eef1911f7798b56d", + "https://deno.land/x/oak@v12.6.1/content_disposition.ts": "8b8c3cb2fba7138cd5b7f82fc3b5ea39b33db924a824b28261659db7e164621e", + "https://deno.land/x/oak@v12.6.1/context.ts": "895a2d40186b89c28ba3947bf08a9335f1a11fc33133f760082536b74c53d1ca", + "https://deno.land/x/oak@v12.6.1/deps.ts": "267ef76c25592101fe1f6c6d7730664015a9179c974da4f7441297d9367a9514", + "https://deno.land/x/oak@v12.6.1/etag.ts": "32e47726b41698aefdd71faac5aaf2907c9bdd41ef18a7693863be4f8fee115d", + "https://deno.land/x/oak@v12.6.1/forwarded.ts": "e656f96a85574e2a6ee54dc35efc9f72d543c9ae0923760ef426ee7369eef01c", + "https://deno.land/x/oak@v12.6.1/headers.ts": "769fd042d34fbcd5667cbd745b5c65d335cc8430e822dbf1f87d65313cab4b47", + "https://deno.land/x/oak@v12.6.1/helpers.ts": "6b03c6a2be06ec775d54449e442a2bac234aa952948ca758356eab6dc87af618", + "https://deno.land/x/oak@v12.6.1/http_server_native.ts": "98e12c50a959553cfc144bc00999c969fa69ca781cbd96bec563f55691ab82db", + "https://deno.land/x/oak@v12.6.1/http_server_native_request.ts": "552b174b5e13e92de8897d5b6f716b1e5a53543115481d65a651a41e4ca29ec9", + "https://deno.land/x/oak@v12.6.1/isMediaType.ts": "62d638abcf837ece3a8f07a4b7ca59794135cb0d4b73194c7d5837efd4161005", + "https://deno.land/x/oak@v12.6.1/mediaTyper.ts": "042b853fc8e9c3f6c628dd389e03ef481552bf07242efc3f8a1af042102a6105", + "https://deno.land/x/oak@v12.6.1/middleware.ts": "c7f7a0424a6dd99a00e4b8d7d6e131efc0facc8dea781845d713b63df8ef1862", + "https://deno.land/x/oak@v12.6.1/middleware/proxy.ts": "6f2799cf60d926e7a8d13ff757a59d7f0f930407db7ee9b81e7c064138eb89ff", + "https://deno.land/x/oak@v12.6.1/mod.ts": "f6aa47ad1b6099470c9a884cccad9d3ac0fd242ba940896291ab76cd26cf554b", + "https://deno.land/x/oak@v12.6.1/multipart.ts": "1484e01b98f5135f2aa09f7d0ce1e7be39109bf9f045ac660e941619d04e3d29", + "https://deno.land/x/oak@v12.6.1/range.ts": "1ca15fc1ac21c650c34e6997a75af2af9d9d8eb6fe2d5d1dadeac3dfd4a9c152", + "https://deno.land/x/oak@v12.6.1/request.ts": "32409827e285ee65889b22bbaaea5d6b280258124c2e9a4f724baa8e6d6375b7", + "https://deno.land/x/oak@v12.6.1/response.ts": "094d950a5158f5b3446ca8a7b6e975dd23afb42b38c38517cc2f41dc75b16b4c", + "https://deno.land/x/oak@v12.6.1/router.ts": "0f53d6249f9e8f89f2522b2b810b9302d0f22593c184b16b24b03bf2b7d42ea1", + "https://deno.land/x/oak@v12.6.1/send.ts": "5ec49f106294593f468317a0c885da4f3274bf6d0fe9e16a7304391730b4f4fb", + "https://deno.land/x/oak@v12.6.1/structured_clone.ts": "c3888b14d1eec558345bfbf13d0993d59bd45aaa8588444e35dd558c3a921cd8", + "https://deno.land/x/oak@v12.6.1/testing.ts": "37d684d57bb8e5150fb5eb2677e66b04dcb422709cf2c5a74c1df94d52aa02e2", + "https://deno.land/x/oak@v12.6.1/util.ts": "0a3fdffb114859c2de84e1783efa3a544af4d2af8c6f08e0d25655de9d3e69bb", + "https://deno.land/x/path_to_regexp@v6.2.1/index.ts": "894060567837bae8fc9c5cbd4d0a05e9024672083d5883b525c031eea940e556" + } +} diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..53e1681 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,21 @@ +# Project identification +sonar.projectKey=${env.CI_PROJECT_PATH_SLUG} +sonar.projectName=${env.CI_PROJECT_NAME} +sonar.projectVersion=${env.CI_COMMIT_SHORT_SHA} + +# Source code +sonar.sources=src +sonar.sourceEncoding=UTF-8 + +# Language +sonar.language=ts + +# Exclusions +sonar.exclusions=**/*.test.ts,**/test/**,**/*.config.* + +# Test files +sonar.tests=src +sonar.test.inclusions=**/*.test.ts + +# Coverage (if you add coverage in the future) +# sonar.javascript.lcov.reportPaths=coverage/lcov.info diff --git a/src/main.test.ts b/src/main.test.ts new file mode 100644 index 0000000..cec1cbc --- /dev/null +++ b/src/main.test.ts @@ -0,0 +1,64 @@ +import { assertEquals, assertExists } from "https://deno.land/std@0.208.0/assert/mod.ts"; + +// Test configuration +const BASE_URL = "http://localhost:8000"; +const VALID_TOKEN = Deno.env.get("BEARER_TOKEN") || "token"; + +Deno.test({ + name: "Authentication - should reject invalid or missing tokens", + async fn() { + // Test missing token + const noTokenResponse = await fetch(`${BASE_URL}/languages`); + const noTokenData = await noTokenResponse.json(); + + assertEquals(noTokenResponse.status, 401); + assertEquals(noTokenData.error, "Missing or invalid Authorization header"); + + // Test invalid token + const invalidTokenResponse = await fetch(`${BASE_URL}/languages`, { + headers: { "Authorization": "Bearer wrong-token" }, + }); + const invalidTokenData = await invalidTokenResponse.json(); + + assertEquals(invalidTokenResponse.status, 403); + assertEquals(invalidTokenData.error, "Invalid bearer token"); + + // Test valid token works + const validTokenResponse = await fetch(`${BASE_URL}/languages`, { + headers: { "Authorization": `Bearer ${VALID_TOKEN}` }, + }); + await validTokenResponse.json(); // Consume the response body + + assertEquals(validTokenResponse.status, 200); + }, +}); + +Deno.test({ + name: "Translation - should translate text with exclamation mark", + async fn() { + const response = await fetch(`${BASE_URL}/translate`, { + method: "POST", + headers: { + "Authorization": `Bearer ${VALID_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + text: "Hello world!", + target_lang: "FR", + }), + }); + const data = await response.json(); + + assertEquals(response.status, 200); + assertExists(data.translations); + assertEquals(Array.isArray(data.translations), true); + assertEquals(data.translations.length, 1); + assertExists(data.translations[0].text); + assertExists(data.translations[0].detected_source_language); + + // Verify the translation contains French text + const translatedText = data.translations[0].text; + assertEquals(typeof translatedText, "string"); + assertEquals(translatedText.length > 0, true); + }, +}); diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..7b7863a --- /dev/null +++ b/src/main.ts @@ -0,0 +1,141 @@ +import { Application, Router } from "https://deno.land/x/oak@v12.6.1/mod.ts"; +import { oakCors } from "https://deno.land/x/cors@v1.2.2/mod.ts"; + +const DEEPL_API_KEY = Deno.env.get("DEEPL_AUTH_KEY"); +const BEARER_TOKEN = Deno.env.get("BEARER_TOKEN"); +const PORT = parseInt(Deno.env.get("PORT") || "8000"); +const DEEPL_API_BASE = "https://api-free.deepl.com/v2"; +const FRONTEND_URL = "https://deep-lite.netlify.app"; + +if (!DEEPL_API_KEY) { + console.error("ERROR: DEEPL_AUTH_KEY environment variable is required"); + Deno.exit(1); +} + +if (!BEARER_TOKEN) { + console.error("ERROR: BEARER_TOKEN environment variable is required"); + Deno.exit(1); +} + +const app = new Application(); +const router = new Router(); + +// CORS middleware - only allow requests from the PWA frontend +app.use( + oakCors({ + origin: FRONTEND_URL, + credentials: true, + }), +); + +// Bearer token authentication middleware (skip /health endpoint) +app.use(async (ctx, next) => { + // Skip authentication for health check + if (ctx.request.url.pathname === "/health") { + await next(); + return; + } + + const authHeader = ctx.request.headers.get("Authorization"); + + if (!authHeader || !authHeader.startsWith("Bearer ")) { + ctx.response.status = 401; + ctx.response.body = { error: "Missing or invalid Authorization header" }; + return; + } + + const token = authHeader.substring(7); // Remove "Bearer " prefix + + if (token !== BEARER_TOKEN) { + ctx.response.status = 403; + ctx.response.body = { error: "Invalid bearer token" }; + return; + } + + await next(); +}); + +// POST /translate - Proxy to DeepL translate endpoint +router.post("/translate", async (ctx) => { + try { + const body = await ctx.request.body({ type: "json" }).value; + + if (!body.text || !body.target_lang) { + ctx.response.status = 400; + ctx.response.body = { error: "Missing required fields: text, target_lang" }; + return; + } + + // Build form data for DeepL API + const formData = new URLSearchParams(); + formData.append("text", body.text); + formData.append("target_lang", body.target_lang); + + if (body.source_lang) { + formData.append("source_lang", body.source_lang); + } + + const response = await fetch(`${DEEPL_API_BASE}/translate`, { + method: "POST", + headers: { + "Authorization": `DeepL-Auth-Key ${DEEPL_API_KEY}`, + "Content-Type": "application/x-www-form-urlencoded", + }, + body: formData.toString(), + }); + + const data = await response.json(); + + if (!response.ok) { + ctx.response.status = response.status; + ctx.response.body = data; + return; + } + + ctx.response.body = data; + } catch (error) { + console.error("Translation error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } +}); + +// GET /languages - Proxy to DeepL languages endpoint +router.get("/languages", async (ctx) => { + try { + const response = await fetch(`${DEEPL_API_BASE}/languages`, { + method: "GET", + headers: { + "Authorization": `DeepL-Auth-Key ${DEEPL_API_KEY}`, + }, + }); + + const data = await response.json(); + + if (!response.ok) { + ctx.response.status = response.status; + ctx.response.body = data; + return; + } + + ctx.response.body = data; + } catch (error) { + console.error("Languages error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } +}); + +// Health check endpoint +router.get("/health", (ctx) => { + ctx.response.body = { status: "ok", timestamp: new Date().toISOString() }; +}); + +app.use(router.routes()); +app.use(router.allowedMethods()); + +console.log(`🚀 DeepLite BFF server running on http://localhost:${PORT}`); +console.log(`📝 CORS enabled for: ${FRONTEND_URL}`); +console.log(`🔒 Bearer token authentication enabled`); + +await app.listen({ port: PORT });