├── .dockerignore ├── .env.local ├── .eslintignore ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── dependabot.yaml ├── labels.json └── workflows │ ├── container.yaml │ ├── labels.yaml │ └── testing.yaml ├── .gitignore ├── COPYRIGHT ├── Containerfile ├── LICENSE-AGPL_3_0 ├── LICENSE-MIT_0 ├── README.md ├── SECURITY.md ├── app.vue ├── assets └── css │ └── tailwind.css ├── bin └── install.sh ├── components ├── Breadcrumb.vue ├── FilterCategory.vue ├── Markdown.vue ├── Notifications.vue ├── Pagination.vue ├── ThemeToggle.vue ├── TorrustButton.vue ├── TorrustSelect.vue ├── authentication │ └── AuthenticationForm.vue ├── demo │ └── Banner.vue ├── form │ └── FormInputText.vue ├── license │ └── License.vue ├── navigation │ └── NavigationBar.vue ├── registration │ └── RegistrationForm.vue ├── torrent │ ├── CanonicalInfoHashGroup.vue │ ├── TorrentActionCard.vue │ ├── TorrentCommentTab.vue │ ├── TorrentCreatedByTab.vue │ ├── TorrentCreationDateTab.vue │ ├── TorrentDescriptionTab.vue │ ├── TorrentDetails.vue │ ├── TorrentEncodingTab.vue │ ├── TorrentFilesTab.vue │ ├── TorrentList.vue │ ├── TorrentListTorrentDetails.vue │ ├── TorrentTable.vue │ └── TorrentTrackersTab.vue ├── upload │ └── UploadFile.vue └── user │ ├── ChangePasswordForm.vue │ └── UserTable.vue ├── composables ├── helpers.ts ├── states.ts └── useFetchForTextFiles.ts ├── compose.yaml ├── contrib └── dev-tools │ ├── container │ ├── docker-build-debug.sh │ ├── docker-build.sh │ ├── docker-install.sh │ ├── docker-run-local.sh │ ├── docker-run-public.sh │ ├── e2e │ │ └── sqlite │ │ │ ├── install.sh │ │ │ ├── mode │ │ │ ├── private │ │ │ │ ├── e2e-env-down.sh │ │ │ │ └── e2e-env-up.sh │ │ │ └── public │ │ │ │ ├── e2e-env-down.sh │ │ │ │ └── e2e-env-up.sh │ │ │ └── run-e2e-tests.sh │ └── functions │ │ └── wait_for_container_to_be_healthy.sh │ └── su-exec │ ├── LICENSE │ ├── Makefile │ ├── README.md │ └── su-exec.c ├── cspell.json ├── cypress.config.ts ├── cypress ├── e2e │ ├── common │ │ ├── commands.ts │ │ └── database.ts │ └── contexts │ │ ├── category │ │ ├── commands.ts │ │ ├── fixtures.ts │ │ ├── specs │ │ │ ├── add.cy.ts │ │ │ └── delete.cy.ts │ │ └── tasks.ts │ │ ├── tag │ │ ├── commands.ts │ │ ├── random_data.ts │ │ ├── specs │ │ │ ├── add.cy.ts │ │ │ └── delete.cy.ts │ │ └── tasks.ts │ │ ├── torrent │ │ ├── api.ts │ │ ├── commands.ts │ │ ├── specs │ │ │ ├── details │ │ │ │ └── magnet_link.cy.ts │ │ │ ├── list │ │ │ │ ├── magnet_link.cy.ts │ │ │ │ └── no_torrents_to_display.cy.ts │ │ │ ├── private_download.cy.ts │ │ │ ├── public_download.cy.ts │ │ │ └── upload.cy.ts │ │ ├── tasks.ts │ │ └── test_torrent_info.ts │ │ └── user │ │ ├── commands.ts │ │ ├── registration.ts │ │ ├── specs │ │ ├── authentication.cy.ts │ │ └── registration.cy.ts │ │ └── tasks.ts ├── fixtures │ └── torrents │ │ └── mandelbrot_set_01.torrent └── support │ ├── commands.ts │ └── e2e.ts ├── docs ├── containerization_guide.md ├── development_guide.md ├── index.md ├── media │ ├── screenshots │ │ ├── admin-settings-backend.png │ │ ├── admin-settings-categories.png │ │ ├── admin-settings-tags.png │ │ ├── login.png │ │ ├── signup.png │ │ ├── torrent-details.png │ │ ├── torrent-list-default.png │ │ ├── torrent-list-table.png │ │ └── torrent-upload.png │ └── torrust_logo.png ├── release_process_guide.md ├── screenshots.md └── user_guide.md ├── dot.env.local ├── img └── Torrust_Repo_FrontEnd_Readme_Header-20220615.jpg ├── licensing ├── agpl-3.0.md ├── cc-by-sa.md ├── contributor_agreement_v01.md ├── file_header_agplv3.txt └── old_commits │ ├── cc0.md │ └── mit-0.md ├── nuxt.config.ts ├── package-lock.json ├── package.json ├── pages ├── admin │ ├── settings.vue │ └── settings │ │ ├── backend.vue │ │ ├── categories.vue │ │ ├── tags.vue │ │ └── users.vue ├── index.vue ├── license.vue ├── signin.vue ├── signup.vue ├── terms.vue ├── torrent │ ├── [infoHash].vue │ ├── [infoHash] │ │ └── [title].vue │ └── edit │ │ └── [infoHash].vue ├── torrents.vue ├── upload.vue └── user │ └── [username].vue ├── plugins └── notifications.client.ts ├── project-words.txt ├── public ├── COPYRIGHT.md ├── LICENSE-AGPL_3_0.md ├── LICENSE-MIT_0.md ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── contributor_agreement_v01.md ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── icons │ └── computer.svg ├── legacy_exception.md └── site.webmanifest ├── share ├── container │ ├── entry_script_sh │ ├── health_check.js │ └── message └── default │ └── config │ ├── index.private.e2e.container.sqlite3.toml │ ├── index.public.e2e.container.sqlite3.toml │ ├── tracker.private.e2e.container.sqlite3.toml │ └── tracker.public.e2e.container.sqlite3.toml ├── src ├── domain │ └── services │ │ ├── sanitizer.ts │ │ └── slug.ts └── helpers │ └── DateConverter.ts ├── tailwind.config.js └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | /.env 2 | /.env.local 3 | /.git 4 | /.git-blame-ignore 5 | /.github 6 | /.gitignore 7 | /.vscode 8 | /bin/ 9 | /cspell.json 10 | /docker/ 11 | /project-words.txt 12 | /README.md 13 | -------------------------------------------------------------------------------- /.env.local: -------------------------------------------------------------------------------- 1 | # App build variables 2 | API_BASE_URL=http://localhost:3001/v1 3 | 4 | # Rust SQLx 5 | DATABASE_URL=sqlite://storage/database/data.db?mode=rwc 6 | 7 | # Docker compose 8 | USER_ID=1000 9 | # Tracker 10 | TORRUST_TRACKER_CONFIG_TOML= 11 | TORRUST_TRACKER_USER_UID=1000 12 | TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN='MyAccessToken' 13 | # Index 14 | TORRUST_INDEX_CONFIG_TOML= 15 | TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN='MyAccessToken' 16 | TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__USER_CLAIM_TOKEN_PEPPER='MaxVerstappenWC2021' 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /share/container/health_check.js 2 | ../torrust-index-api-lib 3 | ../torrust-index-types-lib 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true 6 | }, 7 | extends: [ 8 | "eslint:recommended", 9 | "plugin:vue/essential", 10 | "plugin:@typescript-eslint/recommended", 11 | "@nuxtjs/eslint-config-typescript" 12 | ], 13 | parserOptions: { 14 | ecmaVersion: "latest", 15 | parser: "@typescript-eslint/parser", 16 | sourceType: "module" 17 | }, 18 | plugins: [ 19 | "vue", 20 | "@typescript-eslint" 21 | ], 22 | rules: { 23 | "vue/script-setup-no-uses-vars": "off", 24 | "vue/require-v-for-key": "off", 25 | "vue/no-mutating-props": "off", 26 | "vue/require-default-prop": "off", 27 | "vue/multi-word-component-names": "off", 28 | "vue/multiline-html-element-content-newline": "off", 29 | "vue/valid-v-for": "off", 30 | "@typescript-eslint/no-unused-vars": "off", 31 | "@typescript-eslint/brace-style": "off", 32 | camelcase: "off", 33 | quotes: [ 34 | "error", 35 | "double" 36 | ], 37 | semi: [ 38 | "error", 39 | "always" 40 | ], 41 | indent: [ 42 | "warn", 43 | 2 44 | ], 45 | "no-multi-spaces": [ 46 | "error" 47 | ], 48 | "brace-style": "off" 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /.github/**/* @torrust/maintainers 2 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | target-branch: 'develop' 8 | labels: 9 | - "Continuous Integration" 10 | - "Dependencies" 11 | 12 | - package-ecosystem: npm 13 | directory: / 14 | schedule: 15 | interval: daily 16 | target-branch: 'develop' 17 | labels: 18 | - "Build | Project System" 19 | - "Dependencies" 20 | -------------------------------------------------------------------------------- /.github/workflows/container.yaml: -------------------------------------------------------------------------------- 1 | name: Container 2 | 3 | on: 4 | push: 5 | branches: 6 | - "develop" 7 | - "main" 8 | - "releases/**/*" 9 | pull_request: 10 | branches: 11 | - "develop" 12 | - "main" 13 | 14 | env: 15 | CARGO_TERM_COLOR: always 16 | 17 | jobs: 18 | test: 19 | name: Test (Docker) 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | matrix: 24 | target: [debug, release] 25 | 26 | steps: 27 | - id: setup 28 | name: Setup Toolchain 29 | uses: docker/setup-buildx-action@v3 30 | 31 | - id: build 32 | name: Build 33 | uses: docker/build-push-action@v5 34 | with: 35 | file: ./Containerfile 36 | push: false 37 | load: true 38 | target: ${{ matrix.target }} 39 | tags: torrust-index-gui:local 40 | cache-from: type=gha 41 | cache-to: type=gha 42 | 43 | - id: inspect 44 | name: Inspect 45 | run: docker image inspect torrust-index-gui:local 46 | 47 | - id: checkout 48 | name: Checkout Repository 49 | uses: actions/checkout@v4 50 | 51 | - id: compose 52 | name: Compose 53 | run: docker compose build 54 | 55 | context: 56 | name: Context 57 | needs: test 58 | runs-on: ubuntu-latest 59 | 60 | outputs: 61 | continue: ${{ steps.check.outputs.continue }} 62 | type: ${{ steps.check.outputs.type }} 63 | version: ${{ steps.check.outputs.version }} 64 | 65 | steps: 66 | - id: check 67 | name: Check Context 68 | run: | 69 | if [[ "${{ github.repository }}" == "torrust/torrust-index-gui" ]]; then 70 | if [[ "${{ github.event_name }}" == "push" ]]; then 71 | if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then 72 | 73 | echo "type=development" >> $GITHUB_OUTPUT 74 | echo "continue=true" >> $GITHUB_OUTPUT 75 | echo "On \`main\` Branch, Type: \`development\`" 76 | 77 | elif [[ "${{ github.ref }}" == "refs/heads/develop" ]]; then 78 | 79 | echo "type=development" >> $GITHUB_OUTPUT 80 | echo "continue=true" >> $GITHUB_OUTPUT 81 | echo "On \`develop\` Branch, Type: \`development\`" 82 | 83 | elif [[ $(echo "${{ github.ref }}" | grep -P '^(refs\/heads\/releases\/)(v)(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$') ]]; then 84 | 85 | version=$(echo "${{ github.ref }}" | sed -n -E 's/^(refs\/heads\/releases\/)//p') 86 | echo "version=$version" >> $GITHUB_OUTPUT 87 | echo "type=release" >> $GITHUB_OUTPUT 88 | echo "continue=true" >> $GITHUB_OUTPUT 89 | echo "In \`releases/$version\` Branch, Type: \`release\`" 90 | 91 | else 92 | echo "Not Correct Branch. Will Not Continue" 93 | fi 94 | else 95 | echo "Not a Push Event. Will Not Continue" 96 | fi 97 | else 98 | echo "On a Forked Repository. Will Not Continue" 99 | fi 100 | 101 | publish_development: 102 | name: Publish (Development) 103 | environment: dockerhub-torrust 104 | needs: context 105 | if: needs.context.outputs.continue == 'true' && needs.context.outputs.type == 'development' 106 | runs-on: ubuntu-latest 107 | 108 | steps: 109 | - id: meta 110 | name: Docker Meta 111 | uses: docker/metadata-action@v5 112 | with: 113 | images: | 114 | "${{ vars.DOCKER_HUB_USERNAME }}/${{vars.DOCKER_HUB_REPOSITORY_NAME }}" 115 | tags: | 116 | type=ref,event=branch 117 | 118 | - id: login 119 | name: Login to Docker Hub 120 | uses: docker/login-action@v3 121 | with: 122 | username: ${{ vars.DOCKER_HUB_USERNAME }} 123 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 124 | 125 | - id: setup 126 | name: Setup Toolchain 127 | uses: docker/setup-buildx-action@v3 128 | 129 | - name: Build and push 130 | uses: docker/build-push-action@v5 131 | with: 132 | file: ./Containerfile 133 | push: true 134 | tags: ${{ steps.meta.outputs.tags }} 135 | labels: ${{ steps.meta.outputs.labels }} 136 | cache-from: type=gha 137 | cache-to: type=gha 138 | 139 | publish_release: 140 | name: Publish (Release) 141 | environment: dockerhub-torrust 142 | needs: context 143 | if: needs.context.outputs.continue == 'true' && needs.context.outputs.type == 'release' 144 | runs-on: ubuntu-latest 145 | 146 | steps: 147 | - id: meta 148 | name: Docker Meta 149 | uses: docker/metadata-action@v5 150 | with: 151 | images: | 152 | "${{ vars.DOCKER_HUB_USERNAME }}/${{vars.DOCKER_HUB_REPOSITORY_NAME }}" 153 | tags: | 154 | type=semver,value=${{ needs.context.outputs.version }},pattern={{raw}} 155 | type=semver,value=${{ needs.context.outputs.version }},pattern={{version}} 156 | type=semver,value=${{ needs.context.outputs.version }},pattern=v{{major}} 157 | type=semver,value=${{ needs.context.outputs.version }},pattern={{major}}.{{minor}} 158 | 159 | - id: login 160 | name: Login to Docker Hub 161 | uses: docker/login-action@v3 162 | with: 163 | username: ${{ vars.DOCKER_HUB_USERNAME }} 164 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 165 | 166 | - id: setup 167 | name: Setup Toolchain 168 | uses: docker/setup-buildx-action@v3 169 | 170 | - name: Build and push 171 | uses: docker/build-push-action@v5 172 | with: 173 | file: ./Containerfile 174 | push: true 175 | tags: ${{ steps.meta.outputs.tags }} 176 | labels: ${{ steps.meta.outputs.labels }} 177 | cache-from: type=gha 178 | cache-to: type=gha 179 | -------------------------------------------------------------------------------- /.github/workflows/labels.yaml: -------------------------------------------------------------------------------- 1 | name: Labels 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - develop 7 | paths: 8 | - "/.github/labels.json" 9 | 10 | jobs: 11 | export: 12 | name: Export Existing Labels 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - id: backup 17 | name: Export to Workflow Artifact 18 | uses: EndBug/export-label-config@v1 19 | 20 | sync: 21 | name: Synchronize Labels from Repo 22 | needs: export 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - id: checkout 27 | name: Checkout Repository 28 | uses: actions/checkout@v4 29 | 30 | - id: sync 31 | name: Apply Labels from File 32 | uses: EndBug/label-sync@v2 33 | with: 34 | config-file: .github/labels.json 35 | delete-other-labels: true 36 | token: ${{ secrets.UPDATE_LABELS }} 37 | -------------------------------------------------------------------------------- /.github/workflows/testing.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | 14 | - name: Install dependencies 15 | run: npm install 16 | 17 | - name: Lint 18 | run: npm run lint 19 | 20 | - name: Build 21 | run: npm run build 22 | 23 | - name: Generate 24 | run: npm run generate 25 | 26 | - name: E2E Tests 27 | run: ./contrib/dev-tools/container/e2e/sqlite/run-e2e-tests.sh 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .env 3 | .env* 4 | !.env.local 5 | .idea 6 | .nitro 7 | .nuxt 8 | .output 9 | *.log* 10 | /temp-package/ 11 | /temp-package/api/ 12 | cypress/downloads/**/* 13 | cypress/fixtures/torrents/file-*.txt.torrent 14 | cypress/screenshots/**/*.png 15 | cypress/videos/**/*.mp4 16 | dist 17 | node_modules 18 | storage 19 | temp-package/types 20 | vue-2 21 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright 2023 in the Torrust-Index-Frontend project are retained by their contributors. No 2 | copyright assignment is required to contribute to the Torrust-Index-Frontend project. 3 | 4 | Some files include explicit copyright notices and/or license notices. 5 | 6 | Except as otherwise noted (below and/or in individual files), Torrust-Index-Frontend is 7 | licensed under the GNU Affero General Public License, Version 3.0 . This license applies to all files in the Torrust-Index-Frontend project, except as noted below. 8 | 9 | Except as otherwise noted (below and/or in individual files), Torrust-Index-Frontend is licensed under the MIT-0 license for all commits made after 5 years of merging. This license applies to the version of the files merged into the Torrust-Index-Frontend project at the time of merging, and does not apply to subsequent updates or revisions to those files. 10 | 11 | The contributors to the Torrust-Index-Frontend project disclaim all liability for any damages or losses that may arise from the use of the project. -------------------------------------------------------------------------------- /Containerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:latest 2 | 3 | # Torrust Index GUI 4 | 5 | ## Su Exe Compile 6 | FROM docker.io/library/gcc:bookworm as gcc 7 | COPY ./contrib/dev-tools/su-exec/ /usr/local/src/su-exec/ 8 | RUN cc -Wall -Werror -g /usr/local/src/su-exec/su-exec.c -o /usr/local/bin/su-exec; chmod +x /usr/local/bin/su-exec 9 | 10 | 11 | ## Builder Image 12 | FROM node:21-bookworm as builder 13 | RUN mkdir -p /app 14 | WORKDIR /app 15 | 16 | 17 | ## Build dependencies 18 | FROM builder as dependencies 19 | COPY package.json . 20 | COPY package-lock.json . 21 | RUN npm install-clean 22 | 23 | 24 | ## Build application 25 | FROM dependencies as test 26 | ARG API_BASE_URL=http://localhost:3001/v1 27 | ENV API_BASE_URL=${API_BASE_URL} 28 | COPY . . 29 | RUN npm run build 30 | 31 | 32 | ## Runtime 33 | FROM gcr.io/distroless/nodejs20-debian12:debug as runtime 34 | RUN ["/busybox/cp", "-sp", "/busybox/sh","/busybox/cat","/busybox/ls","/busybox/env", "/bin/"] 35 | COPY --from=gcc --chmod=0555 /usr/local/bin/su-exec /bin/su-exec 36 | 37 | ARG USER_ID=1000 38 | ARG NUXT_PUBLIC_API_BASE0=http://localhost:3001/v1 39 | ARG NITRO_HOST=:: 40 | ARG NITRO_PORT=3000 41 | 42 | ENV TZ=Etc/UTC 43 | ENV USER_ID=${USER_ID} 44 | ENV NUXT_PUBLIC_API_BASE=${NUXT_PUBLIC_API_BASE} 45 | ENV NITRO_HOST=${NITRO_HOST} 46 | ENV NITRO_PORT=${NITRO_PORT} 47 | 48 | EXPOSE $NITRO_PORT/tcp 49 | 50 | RUN mkdir -p /var/log/torrust/tracker 51 | 52 | ENV ENV=/etc/profile 53 | COPY --chmod=0555 ./share/container/entry_script_sh /usr/local/bin/entry.sh 54 | COPY --chmod=0555 ./share/container/health_check.js /usr/local/bin/health_check.js 55 | 56 | VOLUME ["/var/log/torrust/index-gui"] 57 | 58 | ENV RUNTIME="runtime" 59 | ENTRYPOINT ["/usr/local/bin/entry.sh"] 60 | 61 | ## Torrust-Index-GUI (debug) 62 | FROM runtime as debug 63 | ENV RUNTIME="debug" 64 | COPY --from=test /app/.output /app/.output 65 | RUN env 66 | CMD ["sh"] 67 | 68 | 69 | ## Torrust-Index-GUI (release) (default) 70 | FROM runtime as release 71 | ENV RUNTIME="release" 72 | COPY --from=test /app/.output /app/.output 73 | HEALTHCHECK --interval=5s --timeout=5s --start-period=3s --retries=3 \ 74 | CMD /nodejs/bin/node /usr/local/bin/health_check.js ${NITRO_PORT} || exit 1 75 | CMD [ "/nodejs/bin/node", "/app/.output/server/index.mjs" ] 76 | -------------------------------------------------------------------------------- /LICENSE-MIT_0: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Thanks for helping make Torrust Index Frontend safe for everyone. 4 | 5 | ## Security 6 | 7 | [Torrust](https://github.com/torrust) takes the security of our software products and services seriously. 8 | 9 | ## Reporting Security Issues 10 | 11 | If you believe you have found a security vulnerability in any of our repositories, please report it to us through coordinated disclosure. 12 | 13 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 14 | 15 | Instead, please send an email to info[@]nautilus-cyberneering.de. 16 | 17 | Please include as much of the information listed below as you can to help us better understand and resolve the issue: 18 | 19 | - The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting) 20 | - Full paths of source file(s) related to the manifestation of the issue 21 | - The location of the affected source code (tag/branch/commit or direct URL) 22 | - Any special configuration required to reproduce the issue 23 | - Step-by-step instructions to reproduce the issue 24 | - Proof-of-concept or exploit code (if possible) 25 | - Impact of the issue, including how an attacker might exploit the issue 26 | 27 | This information will help us triage your report more quickly. 28 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 39 | -------------------------------------------------------------------------------- /assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /*.theme-light {*/ 6 | /* --primary: 248 250 252;*/ 7 | /* --secondary: 226 232 240;*/ 8 | /* --base-300: 203 213 225;*/ 9 | /* --text: 5 5 5;*/ 10 | /* --accent: 245 158 11;*/ 11 | /* --accent-dark: 217 119 6;*/ 12 | /*}*/ 13 | /*.theme-dark {*/ 14 | /* --primary: 23 23 23;*/ 15 | /* --secondary: 38 38 38;*/ 16 | /* --base-300: 64 64 64;*/ 17 | /* --text: 245 245 245;*/ 18 | /* --accent: 245 158 11;*/ 19 | /* --accent-dark: 217 119 6;*/ 20 | /*}*/ 21 | -------------------------------------------------------------------------------- /bin/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Generate the .env file if it does not exist 4 | if ! [ -f "./.env" ]; then 5 | # Copy .env file from development template 6 | cp dot.env.local .env 7 | fi 8 | 9 | # Generate storage directory if it does not exist 10 | mkdir -p "./storage/index/lib/database/" 11 | mkdir -p "./storage/tracker/lib/database/" 12 | 13 | # Generate the sqlite database for the index backend if it does not exist 14 | if ! [ -f "./storage/index/lib/database/sqlite3.db" ]; then 15 | sqlite3 ./storage/index/lib/database/sqlite3.db "VACUUM;" 16 | fi 17 | 18 | # Generate the sqlite database for the tracker if it does not exist 19 | if ! [ -f "./storage/tracker/lib/database/sqlite3.db" ]; then 20 | sqlite3 ./storage/tracker/lib/database/sqlite3.db "VACUUM;" 21 | fi 22 | 23 | npm install 24 | -------------------------------------------------------------------------------- /components/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 46 | 47 | 58 | -------------------------------------------------------------------------------- /components/FilterCategory.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 66 | 67 | 92 | -------------------------------------------------------------------------------- /components/Markdown.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 45 | 46 | 60 | -------------------------------------------------------------------------------- /components/Notifications.vue: -------------------------------------------------------------------------------- 1 | 85 | 86 | 89 | 90 | 93 | -------------------------------------------------------------------------------- /components/ThemeToggle.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 74 | 75 | 81 | -------------------------------------------------------------------------------- /components/TorrustButton.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | 27 | 30 | -------------------------------------------------------------------------------- /components/TorrustSelect.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 181 | 182 | 185 | -------------------------------------------------------------------------------- /components/authentication/AuthenticationForm.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 75 | 76 | 79 | -------------------------------------------------------------------------------- /components/demo/Banner.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 49 | 50 | 72 | -------------------------------------------------------------------------------- /components/form/FormInputText.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 60 | 61 | 64 | -------------------------------------------------------------------------------- /components/license/License.vue: -------------------------------------------------------------------------------- 1 | 6 | 18 | 19 | -------------------------------------------------------------------------------- /components/navigation/NavigationBar.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 85 | 86 | 95 | -------------------------------------------------------------------------------- /components/registration/RegistrationForm.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 120 | 121 | 124 | -------------------------------------------------------------------------------- /components/torrent/CanonicalInfoHashGroup.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 54 | 55 | 58 | -------------------------------------------------------------------------------- /components/torrent/TorrentCommentTab.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 43 | 44 | 47 | -------------------------------------------------------------------------------- /components/torrent/TorrentCreatedByTab.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 43 | 44 | 47 | -------------------------------------------------------------------------------- /components/torrent/TorrentCreationDateTab.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 46 | 47 | 50 | -------------------------------------------------------------------------------- /components/torrent/TorrentDescriptionTab.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 43 | 44 | 47 | -------------------------------------------------------------------------------- /components/torrent/TorrentDetails.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 105 | 106 | 111 | 112 | 145 | -------------------------------------------------------------------------------- /components/torrent/TorrentEncodingTab.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 43 | 44 | 47 | -------------------------------------------------------------------------------- /components/torrent/TorrentFilesTab.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 78 | 79 | 82 | -------------------------------------------------------------------------------- /components/torrent/TorrentList.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 81 | -------------------------------------------------------------------------------- /components/torrent/TorrentListTorrentDetails.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 38 | 39 | 42 | -------------------------------------------------------------------------------- /components/torrent/TorrentTable.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 82 | -------------------------------------------------------------------------------- /components/torrent/TorrentTrackersTab.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 53 | 54 | 57 | -------------------------------------------------------------------------------- /components/upload/UploadFile.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 109 | 110 | 115 | -------------------------------------------------------------------------------- /components/user/ChangePasswordForm.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 112 | 113 | 116 | -------------------------------------------------------------------------------- /components/user/UserTable.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 37 | -------------------------------------------------------------------------------- /composables/helpers.ts: -------------------------------------------------------------------------------- 1 | import { type TorrentResponse, type PublicSettings } from "torrust-index-types-lib"; 2 | import { useRestApi, useSettings, useUser } from "~/composables/states"; 3 | 4 | export function isTrackerPublic (): boolean { 5 | return !isTrackerPrivate(); 6 | } 7 | 8 | export function isTrackerPrivate (): boolean { 9 | const settings = useSettings(); 10 | return settings.value.tracker_private === true; 11 | } 12 | 13 | export function isUserLoggedIn (): boolean { 14 | return !!useUser().value?.username; 15 | } 16 | 17 | export function canEditThisTorrent (torrent: TorrentResponse): boolean { 18 | const user = useUser().value; 19 | 20 | if (!user?.username) { 21 | return false; 22 | } 23 | 24 | return user.admin || user.username === torrent.uploader; 25 | } 26 | 27 | export function downloadTorrent (infoHash: string, fileName?: string) { 28 | if (fileName === undefined) { 29 | fileName = "torrent"; 30 | } 31 | 32 | useRestApi().value.torrent.downloadTorrent(infoHash) 33 | .then((blob: Blob) => { 34 | const url = window.URL.createObjectURL(blob); 35 | const link = document.createElement("a"); 36 | link.href = url; 37 | link.setAttribute("download", `${fileName}.torrent`); 38 | document.body.appendChild(link); 39 | link.click(); 40 | }); 41 | } 42 | 43 | export function fileSizeDecimal (size: number): string { 44 | if (!size) { size = 0; } 45 | let sizeString = `${(size).toFixed(2)} B`; 46 | 47 | if (size / Math.pow(1000, 3) < 1000) { sizeString = `${(size / Math.pow(1000, 3)).toFixed(2)} GB`; } 48 | if (size / Math.pow(1000, 2) < 1000) { sizeString = `${(size / Math.pow(1000, 2)).toFixed(2)} MB`; } 49 | if (size / Math.pow(1000, 1) < 1000) { sizeString = `${(size / Math.pow(1000, 1)).toFixed(2)} KB`; } 50 | 51 | return sizeString; 52 | } 53 | 54 | export function fileSizeBinary (size: number): string { 55 | if (!size) { size = 0; } 56 | let sizeString = `${(size).toFixed(2)} B`; 57 | 58 | if (size / Math.pow(1024, 3) < 1024) { sizeString = `${(size / Math.pow(1024, 3)).toFixed(2)} GiB`; } 59 | if (size / Math.pow(1024, 2) < 1024) { sizeString = `${(size / Math.pow(1024, 2)).toFixed(2)} MiB`; } 60 | if (size / Math.pow(1024, 1) < 1024) { sizeString = `${(size / Math.pow(1024, 1)).toFixed(2)} KiB`; } 61 | 62 | return sizeString; 63 | } 64 | 65 | export function timeSince (date: Date): string { 66 | // convert datetime to unix timestamp 67 | const unix = Math.floor(date.getTime() / 1000); 68 | 69 | const seconds = Math.floor(((+new Date() / 1000) - unix)); 70 | let interval = Math.floor(seconds / 31536000); 71 | if (interval >= 1) { 72 | return `${interval} year${(interval > 1 ? "s" : "")}`; 73 | } 74 | interval = Math.floor(seconds / 2592000); 75 | if (interval >= 1) { 76 | return `${interval} month${(interval > 1 ? "s" : "")}`; 77 | } 78 | interval = Math.floor(seconds / 86400); 79 | if (interval >= 1) { 80 | return `${interval} day${(interval > 1 ? "s" : "")}`; 81 | } 82 | interval = Math.floor(seconds / 3600); 83 | if (interval >= 1) { 84 | return `${interval} hour${(interval > 1 ? "s" : "")}`; 85 | } 86 | interval = Math.floor(seconds / 60); 87 | if (interval >= 1) { 88 | return `${interval} minute${(interval > 1 ? "s" : "")}`; 89 | } 90 | return `${Math.floor(seconds)} seconds`; 91 | } 92 | -------------------------------------------------------------------------------- /composables/states.ts: -------------------------------------------------------------------------------- 1 | import type { PublicSettings, Category, TokenResponse, TorrentTag } from "torrust-index-types-lib"; 2 | import { Rest } from "torrust-index-api-lib"; 3 | import { notify } from "notiwind-ts"; 4 | import { useRuntimeConfig, useState } from "#imports"; 5 | 6 | export const useRestApi = () => useState("rest-api", () => new Rest(useRuntimeConfig().public.apiBase)); 7 | export const useCategories = () => useState>("categories", () => new Array()); 8 | export const useTags = () => useState>("tags", () => new Array()); 9 | export const useSettings = () => useState("public-settings", () => null); 10 | export const useUser = () => useState("user", () => null); 11 | 12 | export function getSettings () { 13 | useRestApi().value.settings.getPublicSettings() 14 | .then((publicSettings) => { 15 | useSettings().value = publicSettings; 16 | }) 17 | .catch((err) => { 18 | notify({ 19 | group: "error", 20 | title: "Error", 21 | text: `Trying to get public settings. ${err.message}.` 22 | }, 10000); 23 | }); 24 | } 25 | 26 | export function getCategories () { 27 | useRestApi().value.category.getCategories() 28 | .then((res) => { 29 | useCategories().value = res; 30 | }) 31 | .catch((err) => { 32 | notify({ 33 | group: "error", 34 | title: "Error", 35 | text: `Trying to get categories. ${err.message}.` 36 | }, 10000); 37 | }); 38 | } 39 | 40 | export function getTags () { 41 | useRestApi().value.tag.getTags() 42 | .then((res) => { 43 | useTags().value = res; 44 | }) 45 | .catch((err) => { 46 | notify({ 47 | group: "error", 48 | title: "Error", 49 | text: `Trying to get tags. ${err.message}.` 50 | }, 10000); 51 | }); 52 | } 53 | 54 | export async function loginUser (login: string, password: string): Promise { 55 | let authenticated = false; 56 | await useRestApi().value.user.loginUser({ 57 | login, 58 | password 59 | }) 60 | .then((user) => { 61 | useUser().value = user; 62 | authenticated = true; 63 | }) 64 | .catch((err) => { 65 | notify({ 66 | group: "error", 67 | title: "Error", 68 | text: `Trying to login. ${err.message}.` 69 | }, 10000); 70 | }); 71 | return authenticated; 72 | } 73 | 74 | export function logoutUser () { 75 | useUser().value = null; 76 | 77 | useRestApi().value.deleteToken(); 78 | } 79 | 80 | export async function getUser () { 81 | if (!useRestApi().value.authToken) { 82 | return; 83 | } 84 | 85 | return await useRestApi().value.user.renewToken() 86 | .then((user) => { 87 | useUser().value = user; 88 | }) 89 | .catch((err) => { 90 | notify({ 91 | group: "error", 92 | title: "Error", 93 | text: `Trying to get user info. ${err.message}.` 94 | }, 10000); 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /composables/useFetchForTextFiles.ts: -------------------------------------------------------------------------------- 1 | // Fetches a file from the server and 2 | // returns the content of the file as 3 | // text 4 | // 5 | // Throws an error if the fetch request 6 | // went wrong or if the file returned 7 | // is not of type text/markdown 8 | 9 | class FetchTextFileError extends Error { } 10 | 11 | export async function fetchTextFile (fileName: string) { 12 | try { 13 | const fetchResponse = await fetch(fileName); 14 | if (!fetchResponse.ok || !fetchResponse.headers.get("content-type").includes("text/markdown")) { 15 | throw new FetchTextFileError(`Couldn't fetch the file: ${fileName}`); 16 | } 17 | const fileContent = await fetchResponse.text(); 18 | return fileContent; 19 | } catch (error) { 20 | throw new FetchTextFileError(`Couldn't fetch the file: ${fileName}`); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | name: torrust 2 | services: 3 | 4 | index-gui: 5 | build: 6 | context: . 7 | dockerfile: ./Containerfile 8 | target: release 9 | args: 10 | - TORRUST_INDEX_GUI_API_BASE_URL=${TORRUST_INDEX_GUI_API_BASE_URL:-http://localhost:3001/v1} 11 | tty: true 12 | environment: 13 | - USER_ID=${USER_ID} 14 | - NUXT_PUBLIC_API_BASE=${TORRUST_INDEX_GUI_API_BASE_URL:-http://localhost:3001/v1} 15 | - NITRO_HOST=${NITRO_HOST:-::} 16 | - NITRO_PORT=${NITRO_PORT:-3000} 17 | ports: 18 | - 3000:3000 19 | - 24678:24678 20 | volumes: 21 | - ./storage/index-gui/log:/var/log/torrust/index-gui:Z 22 | depends_on: 23 | - index 24 | - tracker 25 | - mailcatcher 26 | - mysql 27 | 28 | index: 29 | image: torrust/index:develop 30 | tty: true 31 | environment: 32 | - USER_ID=${USER_ID} 33 | - TORRUST_INDEX_CONFIG_TOML=${TORRUST_INDEX_CONFIG_TOML} 34 | - TORRUST_INDEX_DATABASE=${TORRUST_INDEX_DATABASE:-e2e_testing_sqlite3} 35 | - TORRUST_INDEX_DATABASE_DRIVER=${TORRUST_INDEX_DATABASE_DRIVER:-sqlite3} 36 | - TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN=${TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN:-MyAccessToken} 37 | - TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__USER_CLAIM_TOKEN_PEPPER=${TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__USER_CLAIM_TOKEN_PEPPER:-MaxVerstappenWC2021} 38 | - TORRUST_INDEX_API_CORS_PERMISSIVE=${TORRUST_INDEX_API_CORS_PERMISSIVE:-true} 39 | networks: 40 | - server_side 41 | ports: 42 | - 3001:3001 43 | volumes: 44 | - ./storage/index/lib:/var/lib/torrust/index:Z 45 | - ./storage/index/log:/var/log/torrust/index:Z 46 | - ./storage/index/etc:/etc/torrust/index:Z 47 | depends_on: 48 | - tracker 49 | - mailcatcher 50 | - mysql 51 | 52 | tracker: 53 | image: torrust/tracker:develop 54 | tty: true 55 | environment: 56 | - USER_ID=${USER_ID} 57 | - TORRUST_TRACKER_CONFIG_TOML=${TORRUST_TRACKER_CONFIG_TOML} 58 | - TORRUST_TRACKER_DATABASE=${TORRUST_TRACKER_DATABASE:-e2e_testing_sqlite3} 59 | - TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER:-sqlite3} 60 | - TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN=${TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN:-MyAccessToken} 61 | networks: 62 | - server_side 63 | ports: 64 | - 6969:6969/udp 65 | - 7070:7070 66 | - 1212:1212 67 | volumes: 68 | - ./storage/tracker/lib:/var/lib/torrust/tracker:Z 69 | - ./storage/tracker/log:/var/log/torrust/tracker:Z 70 | - ./storage/tracker/etc:/etc/torrust/tracker:Z 71 | depends_on: 72 | - mysql 73 | 74 | mailcatcher: 75 | image: dockage/mailcatcher:0.8.2 76 | networks: 77 | - server_side 78 | ports: 79 | - 1080:1080 80 | - 1025:1025 81 | 82 | mysql: 83 | image: mysql:8.0 84 | command: '--default-authentication-plugin=mysql_native_password' 85 | healthcheck: 86 | test: 87 | [ 88 | 'CMD-SHELL', 89 | 'mysqladmin ping -h 127.0.0.1 --password="$$(cat /run/secrets/db-password)" --silent' 90 | ] 91 | interval: 3s 92 | retries: 5 93 | start_period: 30s 94 | environment: 95 | - MYSQL_ROOT_HOST=% 96 | - MYSQL_ROOT_PASSWORD=root_secret_password 97 | - MYSQL_DATABASE=torrust_index_backend 98 | - MYSQL_USER=db_user 99 | - MYSQL_PASSWORD=db_user_secret_password 100 | networks: 101 | - server_side 102 | ports: 103 | - 3306:3306 104 | volumes: 105 | - mysql_data:/var/lib/mysql 106 | 107 | networks: 108 | server_side: {} 109 | 110 | volumes: 111 | mysql_data: {} 112 | -------------------------------------------------------------------------------- /contrib/dev-tools/container/docker-build-debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Building docker image ..." 4 | 5 | USER_ID=${USER_ID:-1001} 6 | TORRUST_INDEX_GUI_API_BASE_URL=${TORRUST_INDEX_GUI_API_BASE_URL:-http://localhost:3001/v1} 7 | 8 | echo "USER_ID: $USER_ID" 9 | echo "TORRUST_INDEX_GUI_API_BASE_URL: $TORRUST_INDEX_GUI_API_BASE_URL" 10 | 11 | docker build \ 12 | --build-arg USER_ID="$USER_ID" \ 13 | --build-arg API_BASE_URL="$TORRUST_INDEX_GUI_API_BASE_URL" \ 14 | --target debug \ 15 | --tag torrust-index-gui:debug \ 16 | --file Containerfile . 17 | -------------------------------------------------------------------------------- /contrib/dev-tools/container/docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Building docker image ..." 4 | 5 | USER_ID=${USER_ID:-1001} 6 | TORRUST_INDEX_GUI_API_BASE_URL=${TORRUST_INDEX_GUI_API_BASE_URL:-http://localhost:3001/v1} 7 | 8 | echo "USER_ID: $USER_ID" 9 | echo "TORRUST_INDEX_GUI_API_BASE_URL: $TORRUST_INDEX_GUI_API_BASE_URL" 10 | 11 | docker build \ 12 | --build-arg USER_ID="$USER_ID" \ 13 | --build-arg API_BASE_URL="$TORRUST_INDEX_GUI_API_BASE_URL" \ 14 | --target release \ 15 | --tag torrust-index-gui:release \ 16 | --file Containerfile . 17 | -------------------------------------------------------------------------------- /contrib/dev-tools/container/docker-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./contrib/dev-tools/containers/docker-build.sh 4 | -------------------------------------------------------------------------------- /contrib/dev-tools/container/docker-run-local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p ./storage/index-gui/lib/ ./storage/index-gui/log/ ./storage/index-gui/etc/ 4 | 5 | docker run -it \ 6 | --env USER_ID="$(id -u)" \ 7 | --env NUXT_PUBLIC_API_BASE="http://localhost:3001/v1" \ 8 | --env NITRO_HOST="::" \ 9 | --env NITRO_PORT="3000" \ 10 | --publish "3000:3000/tcp" \ 11 | torrust-index-gui:release 12 | -------------------------------------------------------------------------------- /contrib/dev-tools/container/docker-run-public.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p ./storage/index-gui/log/ 4 | 5 | docker run -it \ 6 | --env USER_ID="$(id -u)" \ 7 | --env NUXT_PUBLIC_API_BASE="http://localhost:3001/v1" \ 8 | --env NITRO_HOST="::" \ 9 | --env NITRO_PORT="3000" \ 10 | --publish "3000:3000/tcp" \ 11 | torrust/index-gui:latest 12 | -------------------------------------------------------------------------------- /contrib/dev-tools/container/e2e/sqlite/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is only intended to be used for E2E testing environment. 4 | 5 | ## Index 6 | 7 | # Generate the Index sqlite database directory and file if it does not exist 8 | mkdir -p ./storage/index/lib/database 9 | 10 | if ! [ -f "./storage/index/lib/database/${TORRUST_INDEX_DATABASE}.db" ]; then 11 | echo "Creating index database '${TORRUST_INDEX_DATABASE}.db'" 12 | sqlite3 "./storage/index/lib/database/${TORRUST_INDEX_DATABASE}.db" "VACUUM;" 13 | fi 14 | 15 | ## Tracker 16 | 17 | # Generate the Tracker sqlite database directory and file if it does not exist 18 | mkdir -p ./storage/tracker/lib/database 19 | 20 | if ! [ -f "./storage/tracker/lib/database/${TORRUST_TRACKER_DATABASE}.db" ]; then 21 | echo "Creating tracker database '${TORRUST_TRACKER_DATABASE}.db'" 22 | sqlite3 "./storage/tracker/lib/database/${TORRUST_TRACKER_DATABASE}.db" "VACUUM;" 23 | fi 24 | -------------------------------------------------------------------------------- /contrib/dev-tools/container/e2e/sqlite/mode/private/e2e-env-down.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | USER_ID=${USER_ID:-1000} \ 4 | TORRUST_INDEX_CONFIG_TOML=$(cat ./share/default/config/index.private.e2e.container.sqlite3.toml) \ 5 | TORRUST_TRACKER_CONFIG_TOML=$(cat ./share/default/config/tracker.private.e2e.container.sqlite3.toml) \ 6 | docker compose down 7 | -------------------------------------------------------------------------------- /contrib/dev-tools/container/e2e/sqlite/mode/private/e2e-env-up.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | USER_ID=${USER_ID:-1000} \ 4 | TORRUST_INDEX_CONFIG_TOML=$(cat ./share/default/config/index.private.e2e.container.sqlite3.toml) \ 5 | TORRUST_TRACKER_CONFIG_TOML=$(cat ./share/default/config/tracker.private.e2e.container.sqlite3.toml) \ 6 | docker compose build 7 | 8 | USER_ID=${USER_ID:-1000} \ 9 | TORRUST_INDEX_CONFIG_TOML=$(cat ./share/default/config/index.private.e2e.container.sqlite3.toml) \ 10 | TORRUST_INDEX_DATABASE="e2e_testing_sqlite3" \ 11 | TORRUST_INDEX_DATABASE_DRIVER="sqlite3" \ 12 | TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN="MyAccessToken" \ 13 | TORRUST_INDEX_CONFIG_OVERRIDE_USER_CLAIM_TOKEN_PEPPER="MyAccessToken" \ 14 | TORRUST_TRACKER_CONFIG_TOML=$(cat ./share/default/config/tracker.private.e2e.container.sqlite3.toml) \ 15 | TORRUST_TRACKER_DATABASE="e2e_testing_sqlite3" \ 16 | TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER="sqlite3" \ 17 | TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN="MyAccessToken" \ 18 | docker compose up --detach --pull always --remove-orphans 19 | -------------------------------------------------------------------------------- /contrib/dev-tools/container/e2e/sqlite/mode/public/e2e-env-down.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | USER_ID=${USER_ID:-1000} \ 4 | TORRUST_INDEX_CONFIG_TOML=$(cat ./share/default/config/index.public.e2e.container.sqlite3.toml) \ 5 | TORRUST_TRACKER_CONFIG_TOML=$(cat ./share/default/config/tracker.public.e2e.container.sqlite3.toml) \ 6 | docker compose down 7 | -------------------------------------------------------------------------------- /contrib/dev-tools/container/e2e/sqlite/mode/public/e2e-env-up.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | USER_ID=${USER_ID:-1000} \ 4 | TORRUST_INDEX_CONFIG_TOML=$(cat ./share/default/config/index.public.e2e.container.sqlite3.toml) \ 5 | TORRUST_TRACKER_CONFIG_TOML=$(cat ./share/default/config/tracker.public.e2e.container.sqlite3.toml) \ 6 | docker compose build 7 | 8 | USER_ID=${USER_ID:-1000} \ 9 | TORRUST_INDEX_CONFIG_TOML=$(cat ./share/default/config/index.public.e2e.container.sqlite3.toml) \ 10 | TORRUST_INDEX_DATABASE="e2e_testing_sqlite3" \ 11 | TORRUST_INDEX_DATABASE_DRIVER="sqlite3" \ 12 | TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN="MyAccessToken" \ 13 | TORRUST_INDEX_CONFIG_OVERRIDE_USER_CLAIM_TOKEN_PEPPER="MyAccessToken" \ 14 | TORRUST_TRACKER_CONFIG_TOML=$(cat ./share/default/config/tracker.public.e2e.container.sqlite3.toml) \ 15 | TORRUST_TRACKER_DATABASE="e2e_testing_sqlite3" \ 16 | TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER="sqlite3" \ 17 | TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN="MyAccessToken" \ 18 | docker compose up --detach --pull always --remove-orphans 19 | -------------------------------------------------------------------------------- /contrib/dev-tools/container/e2e/sqlite/run-e2e-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CURRENT_USER_NAME=$(whoami) 4 | CURRENT_USER_ID=$(id -u) 5 | echo "User name: $CURRENT_USER_NAME" 6 | echo "User id: $CURRENT_USER_ID" 7 | 8 | TORRUST_INDEX_GUI_USER_UID=$CURRENT_USER_ID 9 | USER_ID=$CURRENT_USER_ID 10 | export USER_ID 11 | export TORRUST_INDEX_GUI_USER_UID 12 | 13 | export TORRUST_INDEX_DATABASE="e2e_testing_sqlite3" 14 | export TORRUST_TRACKER_DATABASE="e2e_testing_sqlite3" 15 | 16 | # Install app 17 | cp .env.local .env || exit 1 18 | ./contrib/dev-tools/container/e2e/sqlite/install.sh || exit 1 19 | 20 | # Run E2E tests with Tracker in public mode 21 | 22 | # Start E2E testing environment 23 | ./contrib/dev-tools/container/e2e/sqlite/mode/public/e2e-env-up.sh || exit 1 24 | 25 | # Wait for conatiners to be healthy 26 | ./contrib/dev-tools/container/functions/wait_for_container_to_be_healthy.sh torrust-mysql-1 10 3 || exit 1 27 | ./contrib/dev-tools/container/functions/wait_for_container_to_be_healthy.sh torrust-tracker-1 10 3 || exit 1 28 | ./contrib/dev-tools/container/functions/wait_for_container_to_be_healthy.sh torrust-index-1 10 3 || exit 1 29 | ./contrib/dev-tools/container/functions/wait_for_container_to_be_healthy.sh torrust-index-gui-1 10 3 || exit 1 30 | 31 | # Just to make sure that everything is up and running 32 | docker ps 33 | 34 | # Run E2E tests with shared app instance 35 | CYPRESS_TRACKER_MODE=public npm run cypress:run || { 36 | ./contrib/dev-tools/container/e2e/sqlite/mode/public/e2e-env-down.sh 37 | exit 1 38 | } 39 | 40 | # Stop E2E testing environment 41 | ./contrib/dev-tools/container/e2e/sqlite/mode/public/e2e-env-down.sh 42 | 43 | # Run E2E tests with Tracker in private mode 44 | 45 | # Start E2E testing environment 46 | ./contrib/dev-tools/container/e2e/sqlite/mode/private/e2e-env-up.sh || exit 1 47 | 48 | # Wait for conatiners to be healthy 49 | ./contrib/dev-tools/container/functions/wait_for_container_to_be_healthy.sh torrust-mysql-1 10 3 || exit 1 50 | ./contrib/dev-tools/container/functions/wait_for_container_to_be_healthy.sh torrust-tracker-1 10 3 || exit 1 51 | ./contrib/dev-tools/container/functions/wait_for_container_to_be_healthy.sh torrust-index-1 10 3 || exit 1 52 | ./contrib/dev-tools/container/functions/wait_for_container_to_be_healthy.sh torrust-index-gui-1 10 3 || exit 1 53 | 54 | # Just to make sure that everything is up and running 55 | docker ps 56 | 57 | # Run E2E tests with shared app instance 58 | CYPRESS_TRACKER_MODE=private npm run cypress:run || { 59 | ./contrib/dev-tools/container/e2e/sqlite/mode/private/e2e-env-down.sh 60 | exit 1 61 | } 62 | 63 | # Stop E2E testing environment 64 | ./contrib/dev-tools/container/e2e/sqlite/mode/private/e2e-env-down.sh 65 | -------------------------------------------------------------------------------- /contrib/dev-tools/container/functions/wait_for_container_to_be_healthy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | wait_for_container_to_be_healthy() { 4 | local container_name="$1" 5 | local max_retries="$2" 6 | local retry_interval="$3" 7 | local retry_count=0 8 | 9 | while [ $retry_count -lt "$max_retries" ]; do 10 | container_health="$(docker inspect --format='{{json .State.Health}}' "$container_name")" 11 | if [ "$container_health" != "{}" ]; then 12 | container_status="$(echo "$container_health" | jq -r '.Status')" 13 | if [ "$container_status" == "healthy" ]; then 14 | echo "Container $container_name is healthy" 15 | return 0 16 | fi 17 | fi 18 | 19 | retry_count=$((retry_count + 1)) 20 | echo "Waiting for container $container_name to become healthy (attempt $retry_count of $max_retries)..." 21 | sleep "$retry_interval" 22 | done 23 | 24 | echo "Timeout reached, container $container_name is not healthy" 25 | return 1 26 | } -------------------------------------------------------------------------------- /contrib/dev-tools/su-exec/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 ncopa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /contrib/dev-tools/su-exec/Makefile: -------------------------------------------------------------------------------- 1 | 2 | CFLAGS ?= -Wall -Werror -g 3 | LDFLAGS ?= 4 | 5 | PROG := su-exec 6 | SRCS := $(PROG).c 7 | 8 | all: $(PROG) 9 | 10 | $(PROG): $(SRCS) 11 | $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) 12 | 13 | $(PROG)-static: $(SRCS) 14 | $(CC) $(CFLAGS) -o $@ $^ -static $(LDFLAGS) 15 | 16 | clean: 17 | rm -f $(PROG) $(PROG)-static 18 | -------------------------------------------------------------------------------- /contrib/dev-tools/su-exec/README.md: -------------------------------------------------------------------------------- 1 | # su-exec 2 | switch user and group id, setgroups and exec 3 | 4 | ## Purpose 5 | 6 | This is a simple tool that will simply execute a program with different 7 | privileges. The program will be executed directly and not run as a child, 8 | like su and sudo does, which avoids TTY and signal issues (see below). 9 | 10 | Notice that su-exec depends on being run by the root user, non-root 11 | users do not have permission to change uid/gid. 12 | 13 | ## Usage 14 | 15 | ```shell 16 | su-exec user-spec command [ arguments... ] 17 | ``` 18 | 19 | `user-spec` is either a user name (e.g. `nobody`) or user name and group 20 | name separated with colon (e.g. `nobody:ftp`). Numeric uid/gid values 21 | can be used instead of names. Example: 22 | 23 | ```shell 24 | $ su-exec apache:1000 /usr/sbin/httpd -f /opt/www/httpd.conf 25 | ``` 26 | 27 | ## TTY & parent/child handling 28 | 29 | Notice how `su` will make `ps` be a child of a shell while `su-exec` 30 | just executes `ps` directly. 31 | 32 | ```shell 33 | $ docker run -it --rm alpine:edge su postgres -c 'ps aux' 34 | PID USER TIME COMMAND 35 | 1 postgres 0:00 ash -c ps aux 36 | 12 postgres 0:00 ps aux 37 | $ docker run -it --rm -v $PWD/su-exec:/sbin/su-exec:ro alpine:edge su-exec postgres ps aux 38 | PID USER TIME COMMAND 39 | 1 postgres 0:00 ps aux 40 | ``` 41 | 42 | ## Why reinvent gosu? 43 | 44 | This does more or less exactly the same thing as [gosu](https://github.com/tianon/gosu) 45 | but it is only 10kb instead of 1.8MB. 46 | 47 | -------------------------------------------------------------------------------- /contrib/dev-tools/su-exec/su-exec.c: -------------------------------------------------------------------------------- 1 | /* set user and group id and exec */ 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | static char *argv0; 15 | 16 | static void usage(int exitcode) 17 | { 18 | printf("Usage: %s user-spec command [args]\n", argv0); 19 | exit(exitcode); 20 | } 21 | 22 | int main(int argc, char *argv[]) 23 | { 24 | char *user, *group, **cmdargv; 25 | char *end; 26 | 27 | uid_t uid = getuid(); 28 | gid_t gid = getgid(); 29 | 30 | argv0 = argv[0]; 31 | if (argc < 3) 32 | usage(0); 33 | 34 | user = argv[1]; 35 | group = strchr(user, ':'); 36 | if (group) 37 | *group++ = '\0'; 38 | 39 | cmdargv = &argv[2]; 40 | 41 | struct passwd *pw = NULL; 42 | if (user[0] != '\0') { 43 | uid_t nuid = strtol(user, &end, 10); 44 | if (*end == '\0') 45 | uid = nuid; 46 | else { 47 | pw = getpwnam(user); 48 | if (pw == NULL) 49 | err(1, "getpwnam(%s)", user); 50 | } 51 | } 52 | if (pw == NULL) { 53 | pw = getpwuid(uid); 54 | } 55 | if (pw != NULL) { 56 | uid = pw->pw_uid; 57 | gid = pw->pw_gid; 58 | } 59 | 60 | setenv("HOME", pw != NULL ? pw->pw_dir : "/", 1); 61 | 62 | if (group && group[0] != '\0') { 63 | /* group was specified, ignore grouplist for setgroups later */ 64 | pw = NULL; 65 | 66 | gid_t ngid = strtol(group, &end, 10); 67 | if (*end == '\0') 68 | gid = ngid; 69 | else { 70 | struct group *gr = getgrnam(group); 71 | if (gr == NULL) 72 | err(1, "getgrnam(%s)", group); 73 | gid = gr->gr_gid; 74 | } 75 | } 76 | 77 | if (pw == NULL) { 78 | if (setgroups(1, &gid) < 0) 79 | err(1, "setgroups(%i)", gid); 80 | } else { 81 | int ngroups = 0; 82 | gid_t *glist = NULL; 83 | 84 | while (1) { 85 | int r = getgrouplist(pw->pw_name, gid, glist, &ngroups); 86 | 87 | if (r >= 0) { 88 | if (setgroups(ngroups, glist) < 0) 89 | err(1, "setgroups"); 90 | break; 91 | } 92 | 93 | glist = realloc(glist, ngroups * sizeof(gid_t)); 94 | if (glist == NULL) 95 | err(1, "malloc"); 96 | } 97 | } 98 | 99 | if (setgid(gid) < 0) 100 | err(1, "setgid(%i)", gid); 101 | 102 | if (setuid(uid) < 0) 103 | err(1, "setuid(%i)", uid); 104 | 105 | execvp(cmdargv[0], cmdargv); 106 | err(1, "%s", cmdargv[0]); 107 | 108 | return 1; 109 | } 110 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", 3 | "version": "0.2", 4 | "dictionaryDefinitions": [ 5 | { 6 | "name": "project-words", 7 | "path": "./project-words.txt", 8 | "addWords": true 9 | } 10 | ], 11 | "dictionaries": ["project-words"], 12 | "ignorePaths": ["target", "/project-words.txt"] 13 | } 14 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | import { grantAdminRole, deleteUser } from "./cypress/e2e/contexts/user/tasks"; 3 | import { deleteTorrent, deleteTorrentsInfoFromDatabase } from "./cypress/e2e/contexts/torrent/tasks"; 4 | import { deleteCategory, addCategory } from "./cypress/e2e/contexts/category/tasks"; 5 | import { deleteTags, addTag } from "./cypress/e2e/contexts/tag/tasks"; 6 | import type { DatabaseConfig } from "./cypress/e2e/common/database"; 7 | 8 | function databaseConfig (config: Cypress.PluginConfigOptions): DatabaseConfig { 9 | return { 10 | filepath: config.env.db_file_path 11 | }; 12 | } 13 | 14 | export default defineConfig({ 15 | e2e: { 16 | baseUrl: "http://localhost:3000", 17 | setupNodeEvents (on, config) { 18 | on("task", { 19 | // Category context 20 | deleteCategory: ({ name }) => { 21 | return deleteCategory(name, databaseConfig(config)); 22 | }, 23 | addCategory: ({ name }) => { 24 | return addCategory(name, databaseConfig(config)); 25 | }, 26 | // Tag context 27 | deleteTags: () => { 28 | return deleteTags(databaseConfig(config)); 29 | }, 30 | addTag: ({ name }) => { 31 | return addTag(name, databaseConfig(config)); 32 | }, 33 | // Torrent context 34 | deleteTorrent: ({ infohash }) => { 35 | return deleteTorrent(infohash, databaseConfig(config)); 36 | }, 37 | deleteTorrentsInfoFromDatabase: () => { 38 | return deleteTorrentsInfoFromDatabase(databaseConfig(config)); 39 | }, 40 | // User context 41 | grantAdminRole: ({ username }) => { 42 | return grantAdminRole(username, databaseConfig(config)); 43 | }, 44 | deleteUser: ({ username }) => { 45 | return deleteUser(username, databaseConfig(config)); 46 | } 47 | }); 48 | } 49 | }, 50 | env: { 51 | db_file_path: "./storage/index/lib/database/e2e_testing_sqlite3.db" 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /cypress/e2e/common/commands.ts: -------------------------------------------------------------------------------- 1 | // Common commands 2 | 3 | Cypress.Commands.add("go_to_settings", () => { 4 | cy.get("div[data-cy=\"user-menu\"]").click(); 5 | cy.get("li[data-cy=\"admin-settings-link\"]").click(); 6 | }); 7 | -------------------------------------------------------------------------------- /cypress/e2e/common/database.ts: -------------------------------------------------------------------------------- 1 | import { Database } from "sqlite3"; 2 | 3 | export interface DatabaseConfig { 4 | filepath: string; // Relative path from project root to the SQLite database file 5 | } 6 | export interface DatabaseQuery { 7 | query: string; 8 | params: Array; 9 | } 10 | 11 | export const runDatabaseQuery = ({ query, params }: DatabaseQuery, config: DatabaseConfig): Promise => { 12 | return new Promise((resolve, reject) => { 13 | const db = new Database(config.filepath, (err) => { 14 | if (err) { 15 | reject(err.message); 16 | } 17 | }); 18 | 19 | db.get(query, params, function (err, row) { 20 | if (err) { 21 | reject(err); 22 | } else { 23 | resolve(row); 24 | } 25 | }); 26 | 27 | db.close((err) => { 28 | if (err) { 29 | reject(err); 30 | } 31 | }); 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/category/commands.ts: -------------------------------------------------------------------------------- 1 | // Custom commands for category context 2 | 3 | Cypress.Commands.add("delete_category_from_database", (name) => { 4 | cy.task("deleteCategory", { name }); 5 | }); 6 | 7 | Cypress.Commands.add("add_category_to_database", (name) => { 8 | cy.task("addCategory", { name }); 9 | }); 10 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/category/fixtures.ts: -------------------------------------------------------------------------------- 1 | export function random_category_name (): string { 2 | return `category-${random_category_id()}`; 3 | } 4 | 5 | function random_category_id (): number { 6 | return Math.floor(Math.random() * 1000000); 7 | } 8 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/category/specs/add.cy.ts: -------------------------------------------------------------------------------- 1 | import { RegistrationForm, random_user_registration_data } from "../../user/registration"; 2 | import { random_category_name } from "../fixtures"; 3 | 4 | describe("The admin user", () => { 5 | let registration_form: RegistrationForm; 6 | 7 | before(() => { 8 | registration_form = random_user_registration_data(); 9 | cy.register_as_admin_and_login(registration_form); 10 | }); 11 | 12 | after(() => { 13 | cy.delete_user_from_database(registration_form.username); 14 | }); 15 | 16 | it("should be able to add a new category", () => { 17 | const category_name = random_category_name(); 18 | 19 | // Make sure the category does not exist 20 | cy.delete_category_from_database(category_name); 21 | 22 | cy.go_to_settings(); 23 | 24 | // Click categories tab 25 | cy.contains("a", "categories").click(); 26 | 27 | // Fill new category name 28 | cy.get("input[data-cy=\"add-category-input\"]").type(category_name); 29 | 30 | // Add category 31 | cy.get("button[data-cy=\"add-category-button\"]").click(); 32 | 33 | // The new category should appear in the list 34 | cy.contains(`${category_name} (0)`); 35 | 36 | cy.delete_category_from_database(category_name); 37 | }); 38 | }); 39 | 40 | describe("A non admin authenticated user", () => { 41 | let registration_form: RegistrationForm; 42 | 43 | before(() => { 44 | registration_form = random_user_registration_data(); 45 | cy.register_and_login(registration_form); 46 | }); 47 | 48 | after(() => { 49 | cy.delete_user_from_database(registration_form.username); 50 | }); 51 | 52 | it("should not be able to add a new category", () => { 53 | cy.visit("/admin/settings/categories"); 54 | cy.contains("Please login to manage admin settings."); 55 | }); 56 | }); 57 | 58 | describe("A guest user", () => { 59 | it("should not be able to add a new category", () => { 60 | cy.visit("/admin/settings/categories"); 61 | cy.contains("Please login to manage admin settings."); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/category/specs/delete.cy.ts: -------------------------------------------------------------------------------- 1 | import { RegistrationForm, random_user_registration_data } from "../../user/registration"; 2 | import { random_category_name } from "../fixtures"; 3 | 4 | describe("The admin user", () => { 5 | let registration_form: RegistrationForm; 6 | 7 | before(() => { 8 | registration_form = random_user_registration_data(); 9 | cy.register_as_admin_and_login(registration_form); 10 | }); 11 | 12 | after(() => { 13 | cy.delete_user_from_database(registration_form.username); 14 | }); 15 | 16 | it("should be able to delete a category", () => { 17 | const category_name = random_category_name(); 18 | 19 | cy.add_category_to_database(category_name); 20 | 21 | cy.go_to_settings(); 22 | 23 | // Click categories tab 24 | cy.contains("a", "categories").click(); 25 | 26 | // Delete the category 27 | cy.get(`button[data-cy="delete-category-${category_name}"]`).click(); 28 | 29 | // Confirm alert should pop up 30 | cy.on("window:confirm", (str) => { 31 | expect(str).to.equal(`Are you sure you want to delete ${category_name}?`); 32 | }); 33 | 34 | // Confirm delete 35 | cy.on("window:confirm", () => true); 36 | 37 | cy.get(`[data-cy="delete-category-${category_name}"]`).should("not.exist"); 38 | }); 39 | }); 40 | 41 | describe("A non admin authenticated user", () => { 42 | let registration_form: RegistrationForm; 43 | 44 | before(() => { 45 | registration_form = random_user_registration_data(); 46 | cy.register_and_login(registration_form); 47 | }); 48 | 49 | after(() => { 50 | cy.delete_user_from_database(registration_form.username); 51 | }); 52 | 53 | it("should not be able to delete category", () => { 54 | cy.visit("/admin/settings/categories"); 55 | cy.contains("Please login to manage admin settings."); 56 | }); 57 | }); 58 | 59 | describe("A guest user", () => { 60 | it("should not be able to delete a category", () => { 61 | cy.visit("/admin/settings/categories"); 62 | cy.contains("Please login to manage admin settings."); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/category/tasks.ts: -------------------------------------------------------------------------------- 1 | // Custom tasks for category context 2 | 3 | import { DatabaseConfig, DatabaseQuery, runDatabaseQuery } from "../../common/database"; 4 | 5 | // Task to delete a category 6 | export const deleteCategory = async (name: string, db_config: DatabaseConfig): Promise => { 7 | try { 8 | const result = await runDatabaseQuery(deleteCategoryQuery(name), db_config); 9 | return name; 10 | } catch (err) { 11 | return await Promise.reject(err); 12 | } 13 | }; 14 | 15 | // Task to add a new category 16 | export const addCategory = async (name: string, db_config: DatabaseConfig): Promise => { 17 | try { 18 | const result = await runDatabaseQuery(addCategoryQuery(name), db_config); 19 | return name; 20 | } catch (err) { 21 | return await Promise.reject(err); 22 | } 23 | }; 24 | 25 | // Database query specifications 26 | 27 | function deleteCategoryQuery (name: string): DatabaseQuery { 28 | return { 29 | query: "DELETE FROM torrust_categories WHERE name = ?", 30 | params: [name] 31 | }; 32 | } 33 | 34 | function addCategoryQuery (name: string): DatabaseQuery { 35 | return { 36 | query: "INSERT INTO torrust_categories (name) VALUES (?)", 37 | params: [name] 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/tag/commands.ts: -------------------------------------------------------------------------------- 1 | // Custom commands for tag context 2 | 3 | Cypress.Commands.add("delete_tags_from_database", () => { 4 | cy.task("deleteTags"); 5 | }); 6 | 7 | Cypress.Commands.add("add_tag_to_database", (name) => { 8 | cy.task("addTag", { name }); 9 | }); 10 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/tag/random_data.ts: -------------------------------------------------------------------------------- 1 | // Logic for generating random data for tests 2 | 3 | export function randomTagName (): string { 4 | return `tag-${randomTagId()}`; 5 | } 6 | function randomTagId (): number { 7 | return Math.floor(Math.random() * 1000000); 8 | } 9 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/tag/specs/add.cy.ts: -------------------------------------------------------------------------------- 1 | import { RegistrationForm, random_user_registration_data } from "../../user/registration"; 2 | import { randomTagName } from "../random_data"; 3 | 4 | describe("The admin user", () => { 5 | const registration_form = random_user_registration_data(); 6 | 7 | before(() => { 8 | cy.delete_tags_from_database(); 9 | cy.register_as_admin_and_login(registration_form); 10 | }); 11 | 12 | after(() => { 13 | cy.delete_user_from_database(registration_form.username); 14 | }); 15 | 16 | it("should be able to add a tag", () => { 17 | const tag_name = randomTagName(); 18 | 19 | cy.go_to_settings(); 20 | 21 | // Click tags tab 22 | cy.contains("a", "tags").click(); 23 | 24 | // Add the tag 25 | cy.get("[data-cy=\"add-tag-text-input\"]").type(tag_name); 26 | 27 | cy.get("[data-cy=\"add-tag-button\"]").click(); 28 | 29 | cy.get(`[data-cy="delete-tag-${tag_name}"]`).should("exist"); 30 | }); 31 | }); 32 | 33 | describe("A non admin authenticated user", () => { 34 | const registration_form = random_user_registration_data(); 35 | 36 | before(() => { 37 | cy.delete_tags_from_database(); 38 | cy.register_and_login(registration_form); 39 | }); 40 | 41 | after(() => { 42 | cy.delete_user_from_database(registration_form.username); 43 | }); 44 | 45 | it("should not be able to delete tags", () => { 46 | cy.visit("/admin/settings/tags"); 47 | cy.contains("Please login to manage admin settings."); 48 | }); 49 | }); 50 | 51 | describe("A guest user", () => { 52 | it("should not be able to delete a tag", () => { 53 | cy.visit("/admin/settings/tags"); 54 | cy.contains("Please login to manage admin settings."); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/tag/specs/delete.cy.ts: -------------------------------------------------------------------------------- 1 | import { RegistrationForm, random_user_registration_data } from "../../user/registration"; 2 | import { randomTagName } from "../random_data"; 3 | 4 | describe("The admin user", () => { 5 | const registration_form = random_user_registration_data(); 6 | const tag_name = randomTagName(); 7 | before(() => { 8 | cy.delete_tags_from_database(); 9 | cy.register_as_admin_and_login(registration_form); 10 | }); 11 | 12 | after(() => { 13 | cy.delete_user_from_database(registration_form.username); 14 | }); 15 | 16 | it("should be able to delete a tag", () => { 17 | cy.add_tag_to_database(tag_name); 18 | 19 | cy.go_to_settings(); 20 | 21 | // Click tags tab 22 | cy.contains("a", "tags").click(); 23 | 24 | // Delete the tag 25 | cy.get(`button[data-cy="delete-tag-${tag_name}"]`).click(); 26 | 27 | // Confirm alert should pop up 28 | cy.on("window:confirm", (str) => { 29 | expect(str).to.equal(`Are you sure you want to delete ${tag_name}?`); 30 | }); 31 | 32 | // Confirm delete 33 | cy.on("window:confirm", () => true); 34 | 35 | cy.get(`[data-cy="delete-tag-${tag_name}"]`).should("not.exist"); 36 | }); 37 | }); 38 | 39 | describe("A non admin authenticated user", () => { 40 | const registration_form = random_user_registration_data(); 41 | 42 | before(() => { 43 | cy.delete_tags_from_database(); 44 | cy.register_and_login(registration_form); 45 | }); 46 | 47 | after(() => { 48 | cy.delete_user_from_database(registration_form.username); 49 | }); 50 | 51 | it("should not be able to delete tags", () => { 52 | cy.visit("/admin/settings/tags"); 53 | cy.contains("Please login to manage admin settings."); 54 | }); 55 | }); 56 | 57 | describe("A guest user", () => { 58 | it("should not be able to delete a tag", () => { 59 | cy.visit("/admin/settings/tags"); 60 | cy.contains("Please login to manage admin settings."); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/tag/tasks.ts: -------------------------------------------------------------------------------- 1 | // Custom tasks for tag context 2 | 3 | import { DatabaseConfig, DatabaseQuery, runDatabaseQuery } from "../../common/database"; 4 | 5 | // Task to delete a tag 6 | export const deleteTags = async (db_config: DatabaseConfig): Promise => { 7 | try { 8 | const result = await runDatabaseQuery(deleteTagsQuery(), db_config); 9 | return {}; 10 | } catch (err) { 11 | return await Promise.reject(err); 12 | } 13 | }; 14 | 15 | // Task to add a new tag 16 | export const addTag = async (name: string, db_config: DatabaseConfig): Promise => { 17 | try { 18 | const result = await runDatabaseQuery(addTagQuery(name), db_config); 19 | return name; 20 | } catch (err) { 21 | return await Promise.reject(err); 22 | } 23 | }; 24 | 25 | // Database query specifications 26 | 27 | function deleteTagsQuery (): DatabaseQuery { 28 | return { 29 | query: "DELETE FROM torrust_torrent_tags", 30 | params: [] 31 | }; 32 | } 33 | 34 | function addTagQuery (name: string): DatabaseQuery { 35 | return { 36 | query: "INSERT INTO torrust_torrent_tags (name) VALUES (?)", 37 | params: [name] 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/torrent/api.ts: -------------------------------------------------------------------------------- 1 | // It extracts the torrent info-hash from a custom HTTP header `x-torrust-torrent-infohash`. 2 | export function extractInfoHashFromResponse (response: Cypress.Response): string { 3 | return parseInfoHash(response.headers["x-torrust-torrent-infohash"]); 4 | } 5 | 6 | // It parses the torrent info-hash from a Cypress response header. 7 | export function parseInfoHash (headerValue: string | string[]): string { 8 | if (typeof headerValue === "string") { 9 | return headerValue; 10 | } else { 11 | return headerValue.join(", "); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/torrent/commands.ts: -------------------------------------------------------------------------------- 1 | // Custom commands for torrent context 2 | import { parseInfoHash } from "./api"; 3 | 4 | Cypress.Commands.add("upload_torrent", (torrent_info) => { 5 | cy.request({ 6 | url: `http://localhost:3001/v1/torrent/meta-info/random/${torrent_info.id}`, 7 | encoding: "binary" 8 | }).then((response) => { 9 | const torrentInfoHash = parseInfoHash(response.headers["x-torrust-torrent-infohash"]); 10 | cy.wrap(torrentInfoHash).as("infohash"); 11 | cy.log(`random torrent with info-hash '${torrentInfoHash}' downloaded to '${torrent_info.path}'`); 12 | cy.writeFile(torrent_info.path, response.body, "binary"); 13 | }); 14 | 15 | cy.visit("/upload"); 16 | 17 | cy.get("input[data-cy=\"upload-form-title\"]").type(torrent_info.title); 18 | cy.get("textarea[data-cy=\"upload-form-description\"]").type(torrent_info.description); 19 | cy.get("select[data-cy=\"upload-form-category\"]").select("software"); 20 | cy.get("input[data-cy=\"upload-form-torrent-upload\"]").selectFile( 21 | { 22 | contents: torrent_info.path, 23 | fileName: torrent_info.filename, 24 | mimeType: "application/x-bittorrent" 25 | }, { force: true }); 26 | // todo: add tag. 27 | // By default there are no tags, so we need to create them first with 28 | // a custom command. We can enable this feature after writing the test for 29 | // the tags context. We could even create some tags before running all the 30 | // tests. 31 | // cy.get("input[data-cy=\"upload-form-torrent-upload\"]").select('fractals'); 32 | cy.get("input[data-cy=\"upload-form-agree-terms\"]").check(); 33 | cy.get("button[data-cy=\"upload-form-submit\"]").click(); 34 | }); 35 | 36 | Cypress.Commands.add("delete_torrent_from_database_and_fixture", (torrent_info, infohash) => { 37 | // Delete the torrent in the database 38 | cy.task("deleteTorrent", { infohash }); 39 | 40 | // Delete the torrent file in the fixtures folder 41 | cy.exec(`rm ${torrent_info.path}`); 42 | }); 43 | 44 | Cypress.Commands.add("clear_torrents_info_from_database", () => { 45 | cy.task("deleteTorrentsInfoFromDatabase"); 46 | }); 47 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/torrent/specs/details/magnet_link.cy.ts: -------------------------------------------------------------------------------- 1 | import { type RegistrationForm, random_user_registration_data } from "../../../user/registration"; 2 | import { generateRandomTestTorrentInfo } from "../../test_torrent_info"; 3 | 4 | describe("A guest user", () => { 5 | let registration_form: RegistrationForm; 6 | 7 | before(() => { 8 | registration_form = random_user_registration_data(); 9 | cy.register_and_login(registration_form); 10 | }); 11 | 12 | after(() => { 13 | cy.delete_user_from_database(registration_form.username); 14 | }); 15 | 16 | it("should be able get the a torrent magnet link", () => { 17 | const torrent_info = generateRandomTestTorrentInfo(); 18 | 19 | cy.upload_torrent(torrent_info); 20 | 21 | // Get the infohash 22 | cy.get("[data-cy=\"torrent-action-info-hash\"]").invoke("text").then((infoHash) => { 23 | // Get the magnet link 24 | cy.get("[data-cy=\"torrent-action-magnet-link\"]").invoke("attr", "href").then((href) => { 25 | // cspell:disable-next-line 26 | expect(href).to.include(`magnet:?xt=urn:btih:${infoHash}&dn=${torrent_info.title}`); 27 | }); 28 | 29 | cy.delete_torrent_from_database_and_fixture(torrent_info, infoHash); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/torrent/specs/list/magnet_link.cy.ts: -------------------------------------------------------------------------------- 1 | import { type RegistrationForm, random_user_registration_data } from "../../../user/registration"; 2 | import { generateRandomTestTorrentInfo } from "../../test_torrent_info"; 3 | 4 | describe("A guest user", () => { 5 | let registration_form: RegistrationForm; 6 | 7 | before(() => { 8 | registration_form = random_user_registration_data(); 9 | cy.register_and_login(registration_form); 10 | // Generates and upload a random torrent file for the tests 11 | const torrent_info = generateRandomTestTorrentInfo(); 12 | Cypress.env("torrent_info", torrent_info); 13 | cy.upload_torrent(torrent_info); 14 | // Stores the infoHash in the Cypress's env variables 15 | cy.get("[data-cy=\"torrent-action-info-hash\"]").invoke("text").then((infoHash) => { 16 | Cypress.env("infoHash", infoHash); 17 | }); 18 | }); 19 | 20 | beforeEach(() => { 21 | cy.visit("/torrents"); 22 | }); 23 | 24 | after(() => { 25 | cy.delete_user_from_database(registration_form.username); 26 | }); 27 | 28 | it("should be able get the a torrent magnet link from the torrents list", () => { 29 | // Get the magnet link 30 | cy.get("[data-cy=\"torrent-list-magnet-link\"]").invoke("attr", "href").then((href) => { 31 | expect(href).to.include(`magnet:?xt=urn:btih:${Cypress.env("infoHash")}`); 32 | }); 33 | }); 34 | 35 | it("should be able get the a torrent magnet link from the torrents table", () => { 36 | // Sets the layout to "table" 37 | cy.get("[data-cy=\"torrents-table-layout-selector\"]").click(); 38 | // Gets the magnet link 39 | cy.get("[data-cy=\"torrent-table-magnet-link\"]").invoke("attr", "href").then((href) => { 40 | expect(href).to.include(`magnet:?xt=urn:btih:${Cypress.env("infoHash")}`); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/torrent/specs/list/no_torrents_to_display.cy.ts: -------------------------------------------------------------------------------- 1 | describe("Users", () => { 2 | before(() => { 3 | // Deletes all torrents and their related info from the database so the test can pass 4 | cy.clear_torrents_info_from_database(); 5 | }); 6 | 7 | it("Should be able to see the list page when there are no torrents", () => { 8 | cy.visit("/torrents"); 9 | cy.url().should("match", /\/torrents$/); 10 | cy.get("[data-cy=\"no-results-element\"]").invoke("text").should("match", /No results./i); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/torrent/specs/private_download.cy.ts: -------------------------------------------------------------------------------- 1 | import { type RegistrationForm, random_user_registration_data } from "../../user/registration"; 2 | import { parseInfoHash } from "../api"; 3 | import { generateRandomTestTorrentInfo } from "../test_torrent_info"; 4 | 5 | describe("In private mode, a registered user", () => { 6 | let registration_form: RegistrationForm; 7 | 8 | before(() => { 9 | registration_form = random_user_registration_data(); 10 | cy.register_and_login(registration_form); 11 | }); 12 | 13 | after(() => { 14 | cy.delete_user_from_database(registration_form.username); 15 | }); 16 | 17 | if (Cypress.env("TRACKER_MODE") === "private") { 18 | it("should be able to download a preexisting torrent with the tracker key", () => { 19 | const torrent_info = generateRandomTestTorrentInfo(); 20 | 21 | cy.upload_torrent(torrent_info); 22 | 23 | cy.intercept({ 24 | method: "GET", 25 | url: "/*/torrent/download/*" 26 | }).as("download"); 27 | 28 | cy.get("button[data-cy=\"torrent-action-download\"]").click(); 29 | 30 | cy.wait("@download").then((interception) => { 31 | // Ensure the filename is correct 32 | expect(interception.response.headers["content-disposition"]).to.include(torrent_info.filename); 33 | 34 | // todo: ensure that the torrent contains the tracker key 35 | 36 | // Delete the test torrent generated for this test 37 | const torrentInfoHash = parseInfoHash(interception.response.headers["x-torrust-torrent-infohash"]); 38 | cy.delete_torrent_from_database_and_fixture(torrent_info, torrentInfoHash); 39 | }); 40 | }); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/torrent/specs/public_download.cy.ts: -------------------------------------------------------------------------------- 1 | import { type RegistrationForm, random_user_registration_data } from "../../user/registration"; 2 | import { parseInfoHash } from "../api"; 3 | import { generateRandomTestTorrentInfo } from "../test_torrent_info"; 4 | 5 | describe("A registered user", () => { 6 | let registration_form: RegistrationForm; 7 | 8 | before(() => { 9 | registration_form = random_user_registration_data(); 10 | cy.register_and_login(registration_form); 11 | }); 12 | 13 | after(() => { 14 | cy.delete_user_from_database(registration_form.username); 15 | }); 16 | 17 | it("should be able to download a preexisting torrent", () => { 18 | const torrent_info = generateRandomTestTorrentInfo(); 19 | 20 | cy.upload_torrent(torrent_info); 21 | 22 | cy.intercept({ 23 | method: "GET", 24 | url: "/*/torrent/download/*" 25 | }).as("download"); 26 | 27 | cy.get("button[data-cy=\"torrent-action-download\"]").click(); 28 | 29 | cy.wait("@download").then((interception) => { 30 | // Ensure the filename is correct 31 | expect(interception.response.headers["content-disposition"]).to.include(torrent_info.filename); 32 | 33 | // Delete the test torrent generated for this test 34 | const torrentInfoHash = parseInfoHash(interception.response.headers["x-torrust-torrent-infohash"]); 35 | cy.delete_torrent_from_database_and_fixture(torrent_info, torrentInfoHash); 36 | }); 37 | }); 38 | }); 39 | 40 | describe("A guest user", () => { 41 | let uploader_registration_form: RegistrationForm; 42 | 43 | before(() => { 44 | uploader_registration_form = random_user_registration_data(); 45 | 46 | cy.visit("/"); 47 | cy.visit("/signup"); 48 | cy.register(uploader_registration_form); 49 | }); 50 | 51 | after(() => { 52 | cy.delete_user_from_database(uploader_registration_form.username); 53 | }); 54 | 55 | if (Cypress.env("TRACKER_MODE") === "public") { 56 | it("should be able to download a preexisting torrent", () => { 57 | const torrent_info = generateRandomTestTorrentInfo(); 58 | 59 | cy.login(uploader_registration_form.username, uploader_registration_form.password); 60 | 61 | cy.upload_torrent(torrent_info); 62 | 63 | cy.get("div[data-cy=\"user-menu\"]").click(); 64 | 65 | cy.logout(); 66 | 67 | // Ensure we are still on the torrent details page 68 | cy.url().should("include", "/torrent/"); 69 | 70 | cy.intercept({ 71 | method: "GET", 72 | url: "/*/torrent/download/*" 73 | }).as("download"); 74 | 75 | cy.get("button[data-cy=\"torrent-action-download\"]").click(); 76 | 77 | cy.wait("@download").then((interception) => { 78 | // Ensure the filename is correct 79 | expect(interception.response.headers["content-disposition"]).to.include(torrent_info.filename); 80 | 81 | // Delete the test torrent generated for this test 82 | const torrentInfoHash = parseInfoHash(interception.response.headers["x-torrust-torrent-infohash"]); 83 | cy.delete_torrent_from_database_and_fixture(torrent_info, torrentInfoHash); 84 | }); 85 | }); 86 | } 87 | }); 88 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/torrent/specs/upload.cy.ts: -------------------------------------------------------------------------------- 1 | import { type RegistrationForm, random_user_registration_data } from "../../user/registration"; 2 | import { parseInfoHash } from "../api"; 3 | import { generateRandomTestTorrentInfo } from "../test_torrent_info"; 4 | 5 | describe("A registered user", () => { 6 | let registration_form: RegistrationForm; 7 | 8 | before(() => { 9 | registration_form = random_user_registration_data(); 10 | cy.register_and_login(registration_form); 11 | }); 12 | 13 | after(() => { 14 | cy.delete_user_from_database(registration_form.username); 15 | }); 16 | 17 | it("should be able to upload a torrent", () => { 18 | const torrent_info = generateRandomTestTorrentInfo(); 19 | 20 | cy.request({ 21 | url: `http://localhost:3001/v1/torrent/meta-info/random/${torrent_info.id}`, 22 | encoding: "binary" 23 | }).then((response) => { 24 | const torrentInfoHash = parseInfoHash(response.headers["x-torrust-torrent-infohash"]); 25 | cy.wrap(torrentInfoHash).as("infohash"); 26 | cy.log(`random torrent with info-hash '${torrentInfoHash}' downloaded to '${torrent_info.path}'`); 27 | cy.writeFile(torrent_info.path, response.body, "binary"); 28 | }); 29 | 30 | cy.visit("/upload"); 31 | 32 | cy.get("input[data-cy=\"upload-form-title\"]").type(torrent_info.title); 33 | cy.get("textarea[data-cy=\"upload-form-description\"]").type(torrent_info.description); 34 | cy.get("select[data-cy=\"upload-form-category\"]").select("software"); 35 | cy.get("input[data-cy=\"upload-form-torrent-upload\"]").selectFile( 36 | { 37 | contents: torrent_info.path, 38 | fileName: torrent_info.filename, 39 | mimeType: "application/x-bittorrent" 40 | }, { force: true }); 41 | // todo: add tag. 42 | // By default there are no tags, so we need to create them first with 43 | // a custom command. We can enable this feature after writing the test for 44 | // the tags context. We could even create some tags before running all the 45 | // tests. 46 | // cy.get("input[data-cy=\"upload-form-torrent-upload\"]").select('fractals'); 47 | cy.get("input[data-cy=\"upload-form-agree-terms\"]").check(); 48 | cy.get("button[data-cy=\"upload-form-submit\"]").click(); 49 | 50 | cy.get("@infohash").then((infohash) => { 51 | // It should redirect to the torrent detail page. 52 | cy.url().should("include", `/torrent/${infohash}`); 53 | 54 | // Delete the torrent in the database 55 | cy.task("deleteTorrent", { infohash }); 56 | 57 | // Delete the torrent file in the fixtures folder 58 | cy.exec(`rm ${torrent_info.path}`); 59 | }); 60 | }); 61 | }); 62 | 63 | describe("A guest user", () => { 64 | before(() => { 65 | cy.visit("/"); 66 | }); 67 | 68 | it("should not be able to upload a torrent", () => { 69 | cy.visit("/upload"); 70 | cy.contains("Please sign in to upload"); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/torrent/tasks.ts: -------------------------------------------------------------------------------- 1 | // Custom tasks for user context 2 | 3 | import { type DatabaseConfig, type DatabaseQuery, runDatabaseQuery } from "../../common/database"; 4 | 5 | // Task to grant admin role to a user by username 6 | export const deleteTorrent = async (infohash: string, db_config: DatabaseConfig): Promise => { 7 | try { 8 | await runDatabaseQuery(deleteTorrentQuery(infohash), db_config); 9 | return true; 10 | } catch (err) { 11 | return await Promise.reject(err); 12 | } 13 | }; 14 | 15 | // Database query specifications 16 | 17 | function deleteTorrentQuery (infohash: string): DatabaseQuery { 18 | return { 19 | query: "DELETE FROM torrust_torrents WHERE info_hash = ?", 20 | params: [infohash] 21 | }; 22 | } 23 | 24 | // Task to delete all torrents from the database before running any test 25 | export const deleteTorrentsInfoFromDatabase = async (db_config: DatabaseConfig): Promise => { 26 | try { 27 | await runDatabaseQuery(clearTorrentsTableQuery(), db_config); 28 | await runDatabaseQuery(clearAnnounceUrlsTableQuery(), db_config); 29 | await runDatabaseQuery(clearTorrentFilesTableQuery(), db_config); 30 | await runDatabaseQuery(clearTorrentInfoTableQuery(), db_config); 31 | await runDatabaseQuery(clearTorrentInfoHashesTableQuery(), db_config); 32 | await runDatabaseQuery(clearTagLinkTableQuery(), db_config); 33 | await runDatabaseQuery(clearTrackerStatsTableQuery(), db_config); 34 | return true; 35 | } catch (err) { 36 | return await Promise.reject(err); 37 | } 38 | }; 39 | 40 | // Database query specifications 41 | function clearTorrentsTableQuery (): DatabaseQuery { 42 | return { 43 | query: "DELETE FROM torrust_torrents", 44 | params: [] 45 | }; 46 | } 47 | 48 | function clearAnnounceUrlsTableQuery (): DatabaseQuery { 49 | return { 50 | query: "DELETE FROM torrust_torrent_announce_urls", 51 | params: [] 52 | }; 53 | } 54 | 55 | function clearTorrentFilesTableQuery (): DatabaseQuery { 56 | return { 57 | query: "DELETE FROM torrust_torrent_files", 58 | params: [] 59 | }; 60 | } 61 | 62 | function clearTorrentInfoTableQuery (): DatabaseQuery { 63 | return { 64 | query: "DELETE FROM torrust_torrent_info", 65 | params: [] 66 | }; 67 | } 68 | 69 | function clearTorrentInfoHashesTableQuery (): DatabaseQuery { 70 | return { 71 | query: "DELETE FROM torrust_torrent_info_hashes", 72 | params: [] 73 | }; 74 | } 75 | 76 | function clearTagLinkTableQuery (): DatabaseQuery { 77 | return { 78 | query: "DELETE FROM torrust_torrent_tag_links", 79 | params: [] 80 | }; 81 | } 82 | 83 | function clearTrackerStatsTableQuery (): DatabaseQuery { 84 | return { 85 | query: "DELETE FROM torrust_torrent_tracker_stats", 86 | params: [] 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/torrent/test_torrent_info.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from "uuid"; 2 | 3 | export type TestTorrentInfo = { 4 | id: string, 5 | title: string, 6 | description: string, 7 | filename: string, 8 | path: string 9 | }; 10 | 11 | // It generates the information for a random torrent file. 12 | // You can download the torrent file (meta-info file) from the server and saved 13 | // in the `cypress/fixtures/torrents` folder. 14 | export function generateRandomTestTorrentInfo (): TestTorrentInfo { 15 | const torrentId = uuidv4(); 16 | const torrentFilename = `file-${torrentId}.txt.torrent`; 17 | 18 | return { 19 | id: torrentId, 20 | title: `title-${torrentId}`, 21 | description: `description-${torrentId}`, 22 | filename: `file-${torrentId}.txt.torrent`, 23 | path: `cypress/fixtures/torrents/${torrentFilename}` 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/user/commands.ts: -------------------------------------------------------------------------------- 1 | // Custom commands for user context 2 | 3 | // Registration 4 | 5 | Cypress.Commands.add("register", (registration_form) => { 6 | cy.visit("/signup"); 7 | 8 | cy.get("input[data-cy=\"registration-form-username\"]").type(registration_form.username); 9 | cy.get("input[data-cy=\"registration-form-email\"]").type(registration_form.email); 10 | cy.get("input[data-cy=\"registration-form-password\"]").type(registration_form.password); 11 | cy.get("input[data-cy=\"registration-form-confirm-password\"]").type(registration_form.confirm_password); 12 | 13 | cy.get("button[data-cy=\"registration-form-submit\"]").click(); 14 | 15 | cy.contains("Your account was registered!"); 16 | }); 17 | 18 | Cypress.Commands.add("register_as_admin", (registration_form) => { 19 | cy.register(registration_form); 20 | 21 | cy.task("grantAdminRole", { username: registration_form.username }); 22 | }); 23 | 24 | Cypress.Commands.add("delete_user_from_database", (username) => { 25 | cy.task("deleteUser", { username }); 26 | }); 27 | 28 | // Authentication 29 | 30 | Cypress.Commands.add("login", (username: string, password: string) => { 31 | cy.visit("/signin"); 32 | 33 | cy.get("input[data-cy=\"login-form-username\"]").type(username); 34 | cy.get("input[data-cy=\"login-form-password\"]").type(password); 35 | 36 | cy.get("button[data-cy=\"login-form-submit\"]").click(); 37 | 38 | cy.url().should("include", "/torrents"); 39 | }); 40 | 41 | Cypress.Commands.add("logout", () => { 42 | cy.get("a[data-cy=\"logout-link\"]").click(); 43 | }); 44 | 45 | // Others 46 | 47 | Cypress.Commands.add("register_and_login", (registration_form) => { 48 | cy.visit("/"); 49 | cy.visit("/signup"); 50 | cy.register(registration_form); 51 | cy.login(registration_form.username, registration_form.password); 52 | }); 53 | 54 | Cypress.Commands.add("register_as_admin_and_login", (registration_form) => { 55 | cy.visit("/"); 56 | cy.visit("/signup"); 57 | cy.register_as_admin(registration_form); 58 | cy.login(registration_form.username, registration_form.password); 59 | }); 60 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/user/registration.ts: -------------------------------------------------------------------------------- 1 | export type RegistrationForm = { 2 | username: string 3 | email: string 4 | password: string 5 | confirm_password: string 6 | } 7 | 8 | export function random_user_registration_data (): RegistrationForm { 9 | return { 10 | username: `user${random_user_id()}`, 11 | email: `user${random_user_id()}@example.com`, 12 | password: "12345678", 13 | confirm_password: "12345678" 14 | }; 15 | } 16 | 17 | function random_user_id (): number { 18 | return Math.floor(Math.random() * 1000000); 19 | } 20 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/user/specs/authentication.cy.ts: -------------------------------------------------------------------------------- 1 | import { random_user_registration_data } from "../registration"; 2 | 3 | describe("A registered user", () => { 4 | beforeEach(() => { 5 | cy.visit("/"); 6 | }); 7 | 8 | it("should be able to sign in", () => { 9 | cy.visit("/signup"); 10 | 11 | const registration_form = random_user_registration_data(); 12 | 13 | cy.register(registration_form); 14 | 15 | cy.visit("/signin"); 16 | 17 | cy.get("input[data-cy=\"login-form-username\"]").type(registration_form.username); 18 | cy.get("input[data-cy=\"login-form-password\"]").type(registration_form.password); 19 | 20 | cy.get("button[data-cy=\"login-form-submit\"]").click(); 21 | 22 | cy.url().should("include", "/torrents"); 23 | 24 | cy.delete_user_from_database(registration_form.username); 25 | }); 26 | }); 27 | 28 | describe("The website admin", () => { 29 | beforeEach(() => { 30 | cy.visit("/"); 31 | }); 32 | 33 | it("should be able to sign in as admin", () => { 34 | const registration_form = random_user_registration_data(); 35 | 36 | cy.register_as_admin(registration_form); 37 | 38 | cy.login(registration_form.username, registration_form.password); 39 | 40 | // If the user is an admin, the link to admin settings should be available 41 | cy.get("li[data-cy=\"admin-settings-link\"]"); 42 | 43 | cy.delete_user_from_database(registration_form.username); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/user/specs/registration.cy.ts: -------------------------------------------------------------------------------- 1 | import { random_user_registration_data } from "../registration"; 2 | 3 | describe("A guest", () => { 4 | beforeEach(() => { 5 | cy.visit("/"); 6 | }); 7 | 8 | it("should be able to sign up", () => { 9 | const registration_form = random_user_registration_data(); 10 | 11 | cy.visit("/signup"); 12 | 13 | cy.get("input[data-cy=\"registration-form-username\"]").type(registration_form.username); 14 | cy.get("input[data-cy=\"registration-form-email\"]").type(registration_form.email); 15 | cy.get("input[data-cy=\"registration-form-password\"]").type(registration_form.password); 16 | cy.get("input[data-cy=\"registration-form-confirm-password\"]").type(registration_form.confirm_password); 17 | 18 | cy.get("button[data-cy=\"registration-form-submit\"]").click(); 19 | 20 | cy.contains("Your account was registered!"); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /cypress/e2e/contexts/user/tasks.ts: -------------------------------------------------------------------------------- 1 | // Custom tasks for user context 2 | 3 | import { type DatabaseConfig, type DatabaseQuery, runDatabaseQuery } from "../../common/database"; 4 | 5 | // Task to grant admin role to a user by username 6 | export const grantAdminRole = async (username: string, db_config: DatabaseConfig): Promise => { 7 | let user_id: number; 8 | 9 | try { 10 | const result = await runDatabaseQuery(getUserIdByUsernameQuery(username), db_config); 11 | 12 | if (result === undefined || result.user_id === undefined) { 13 | throw new Error(`Can't grant admin role to user. No user found with username: ${username} in database: ${db_config.filepath}`); 14 | } 15 | 16 | const user_id = result.user_id; 17 | 18 | await runDatabaseQuery(grantAdminRoleQuery(user_id), db_config); 19 | 20 | return user_id; 21 | } catch (err) { 22 | return await Promise.reject(err); 23 | } 24 | }; 25 | 26 | // Task to delete a user by username 27 | export const deleteUser = async (username: string, db_config: DatabaseConfig): Promise => { 28 | let user_id: number; 29 | 30 | try { 31 | const result = await runDatabaseQuery(getUserIdByUsernameQuery(username), db_config); 32 | 33 | if (result === undefined || result.user_id === undefined) { 34 | throw new Error(`Can't delete user. No user found with username: ${username} in database: ${db_config.filepath}`); 35 | } 36 | 37 | const user_id = result.user_id; 38 | 39 | await runDatabaseQuery(deleteUserAuthenticationQuery(user_id), db_config); 40 | await runDatabaseQuery(deleteUserBansQuery(user_id), db_config); 41 | await runDatabaseQuery(deleteUserInvitationUsesQuery(user_id), db_config); 42 | await runDatabaseQuery(deleteUserInvitationsQuery(user_id), db_config); 43 | await runDatabaseQuery(deleteUserProfileQuery(user_id), db_config); 44 | await runDatabaseQuery(deleteUserPublicKeysQuery(user_id), db_config); 45 | 46 | await runDatabaseQuery(deleteUserQuery(user_id), db_config); 47 | 48 | return user_id; 49 | } catch (err) { 50 | return await Promise.reject(err); 51 | } 52 | }; 53 | 54 | // Database query specifications 55 | 56 | function getUserIdByUsernameQuery (username: string): DatabaseQuery { 57 | return { 58 | query: "SELECT user_id FROM torrust_user_profiles WHERE username = ?", 59 | params: [username] 60 | }; 61 | } 62 | 63 | function grantAdminRoleQuery (user_id: number): DatabaseQuery { 64 | return { 65 | query: "UPDATE torrust_users SET administrator = ? WHERE user_id = ?", 66 | params: [true, user_id] 67 | }; 68 | } 69 | 70 | function deleteUserAuthenticationQuery (user_id: number): DatabaseQuery { 71 | return { 72 | query: "DELETE FROM torrust_user_authentication WHERE user_id = ?", 73 | params: [user_id] 74 | }; 75 | } 76 | 77 | function deleteUserBansQuery (user_id: number): DatabaseQuery { 78 | return { 79 | query: "DELETE FROM torrust_user_bans WHERE user_id = ?", 80 | params: [user_id] 81 | }; 82 | } 83 | 84 | function deleteUserInvitationUsesQuery (user_id: number): DatabaseQuery { 85 | return { 86 | query: "DELETE FROM torrust_user_invitation_uses WHERE registered_user_id = ?", 87 | params: [user_id] 88 | }; 89 | } 90 | 91 | function deleteUserInvitationsQuery (user_id: number): DatabaseQuery { 92 | return { 93 | query: "DELETE FROM torrust_user_invitations WHERE user_id = ?", 94 | params: [user_id] 95 | }; 96 | } 97 | 98 | function deleteUserProfileQuery (user_id: number): DatabaseQuery { 99 | return { 100 | query: "DELETE FROM torrust_user_profiles WHERE user_id = ?", 101 | params: [user_id] 102 | }; 103 | } 104 | 105 | function deleteUserPublicKeysQuery (user_id: number): DatabaseQuery { 106 | return { 107 | query: "DELETE FROM torrust_user_public_keys WHERE user_id = ?", 108 | params: [user_id] 109 | }; 110 | } 111 | 112 | function deleteUserQuery (user_id: number): DatabaseQuery { 113 | return { 114 | query: "DELETE FROM torrust_users WHERE user_id = ?", 115 | params: [user_id] 116 | }; 117 | } 118 | -------------------------------------------------------------------------------- /cypress/fixtures/torrents/mandelbrot_set_01.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrust/torrust-index-gui/6d7a6213670819bf708e280eb01126a451de5982/cypress/fixtures/torrents/mandelbrot_set_01.torrent -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | import "../e2e/contexts/user/commands"; 2 | import "../e2e/contexts/torrent/commands"; 3 | import "../e2e/contexts/category/commands"; 4 | import "../e2e/contexts/tag/commands"; 5 | import "../e2e/common/commands"; 6 | import type { TestTorrentInfo } from "../e2e/contexts/torrent/test_torrent_info"; 7 | import type { RegistrationForm } from "../e2e/contexts/user/registration"; 8 | 9 | declare global { 10 | namespace Cypress { 11 | interface Chainable { 12 | // Common command 13 | go_to_settings(): Chainable 14 | 15 | // User context: Registration 16 | register(registration_form: RegistrationForm): Chainable 17 | register_as_admin(registration_form: RegistrationForm): Chainable 18 | 19 | // User context: Authentication 20 | login(username: string, password: string): Chainable 21 | logout(): Chainable 22 | 23 | // User context: Others 24 | register_and_login(registration_form: RegistrationForm): Chainable 25 | register_as_admin_and_login(registration_form: RegistrationForm): Chainable 26 | delete_user_from_database(username: string): Chainable 27 | 28 | // Torrent context 29 | upload_torrent(torrent_info: TestTorrentInfo): Chainable 30 | delete_torrent_from_database_and_fixture(torrent_info: TestTorrentInfo, infohash: string): Chainable 31 | clear_torrents_info_from_database(): Chainable; 32 | 33 | // Category context 34 | delete_category_from_database(name: string): Chainable 35 | add_category_to_database(name: string): Chainable 36 | 37 | // Tag context 38 | delete_tags_from_database(): Chainable 39 | add_tag_to_database(name: string): Chainable 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands"; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Torrust Index GUI Documentation 2 | 3 | - [User guide](./user_guide.md) 4 | - [Screenshots](./screenshots.md) 5 | - [Development Guide](./development_guide.md) 6 | - [Containerization Guide](./containerization_guide.md) 7 | - [Release Process Guide](./release_process_guide.md) 8 | -------------------------------------------------------------------------------- /docs/media/screenshots/admin-settings-backend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrust/torrust-index-gui/6d7a6213670819bf708e280eb01126a451de5982/docs/media/screenshots/admin-settings-backend.png -------------------------------------------------------------------------------- /docs/media/screenshots/admin-settings-categories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrust/torrust-index-gui/6d7a6213670819bf708e280eb01126a451de5982/docs/media/screenshots/admin-settings-categories.png -------------------------------------------------------------------------------- /docs/media/screenshots/admin-settings-tags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrust/torrust-index-gui/6d7a6213670819bf708e280eb01126a451de5982/docs/media/screenshots/admin-settings-tags.png -------------------------------------------------------------------------------- /docs/media/screenshots/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrust/torrust-index-gui/6d7a6213670819bf708e280eb01126a451de5982/docs/media/screenshots/login.png -------------------------------------------------------------------------------- /docs/media/screenshots/signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrust/torrust-index-gui/6d7a6213670819bf708e280eb01126a451de5982/docs/media/screenshots/signup.png -------------------------------------------------------------------------------- /docs/media/screenshots/torrent-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrust/torrust-index-gui/6d7a6213670819bf708e280eb01126a451de5982/docs/media/screenshots/torrent-details.png -------------------------------------------------------------------------------- /docs/media/screenshots/torrent-list-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrust/torrust-index-gui/6d7a6213670819bf708e280eb01126a451de5982/docs/media/screenshots/torrent-list-default.png -------------------------------------------------------------------------------- /docs/media/screenshots/torrent-list-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrust/torrust-index-gui/6d7a6213670819bf708e280eb01126a451de5982/docs/media/screenshots/torrent-list-table.png -------------------------------------------------------------------------------- /docs/media/screenshots/torrent-upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrust/torrust-index-gui/6d7a6213670819bf708e280eb01126a451de5982/docs/media/screenshots/torrent-upload.png -------------------------------------------------------------------------------- /docs/media/torrust_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrust/torrust-index-gui/6d7a6213670819bf708e280eb01126a451de5982/docs/media/torrust_logo.png -------------------------------------------------------------------------------- /docs/release_process_guide.md: -------------------------------------------------------------------------------- 1 | # Release Process (v1.0.0) 2 | 3 | ## Version 4 | 5 | > **The `[semantic version]` is bumped according to releases, new features, and breaking changes.** 6 | > 7 | > *The `main` branch uses the semantic version of the last released version. 8 | 9 | ## Process 10 | 11 | **Note**: this guide assumes that the your git `torrust` remote is like this: 12 | 13 | ```sh 14 | git remote show torrust 15 | ``` 16 | 17 | ```s 18 | * remote torrust 19 | Fetch URL: git@github.com:torrust/torrust-index-gui.git 20 | Push URL: git@github.com:torrust/torrust-index-gui.git 21 | ... 22 | ``` 23 | 24 | ### 1. The `main` branch is ready for a release 25 | 26 | ```sh 27 | npm install && npm run lint && npm run build 28 | ``` 29 | 30 | There should be no errors installing or building the library. 31 | 32 | ### 2. Change the version in the `package.json` file 33 | 34 | Change the version in the `package.json` file. For example `3.0.0`. 35 | 36 | You might need to update also the torrust dependencies: 37 | 38 | - torrust-index-api-lib 39 | - torrust-index-types-lib 40 | 41 | ```sh 42 | npm update torrust-index-types-lib 43 | npm update torrust-index-api-lib 44 | ``` 45 | 46 | > NOTICE: The `v` prefix is not needed. 47 | 48 | Install and run linter and build to double-check: 49 | 50 | ```sh 51 | npm install && npm run lint && npm run build 52 | ``` 53 | 54 | At this point, you should check that the new version is working wit the Index, before creating the tag. 55 | 56 | Commit the changes: 57 | 58 | ```sh 59 | git add -A 60 | git commit -m "feat: release [semantic version]" 61 | ``` 62 | 63 | ### 3. Create a new tag an push to the remote 64 | 65 | ```sh 66 | git tag v[semantic version] 67 | git push torrust && git push torrust v[semantic version] 68 | ``` 69 | 70 | For example: 71 | 72 | ```sh 73 | git tag v3.0.0 74 | git push torrust && git push torrust v3.0.0 75 | ``` 76 | 77 | ### 4. Manually publish the NPM package 78 | 79 | ```sh 80 | npm publish 81 | ``` 82 | 83 | > IMPORTANT: 84 | > 85 | > - You will require to login. 86 | > - You have to have permission for publishing on the Torrust namespace. 87 | 88 | If you get an error because you were not logged in, just retry the same command after the login. 89 | 90 | ### 4. Check the package is published 91 | 92 | You should receive an email when the package is published. 93 | 94 | You can also check on the NPM registry: . 95 | -------------------------------------------------------------------------------- /docs/screenshots.md: -------------------------------------------------------------------------------- 1 | # Screenshots 2 | 3 | ![Sign Up](./media/screenshots/signup.png) 4 | 5 | ![Login](./media/screenshots/login.png) 6 | 7 | ![Upload torrent](./media/screenshots/torrent-upload.png) 8 | 9 | ![Torrent list](./media/screenshots/torrent-list-default.png) 10 | 11 | ![Torrent list](./media/screenshots/torrent-list-table.png) 12 | 13 | ![Torrent details](./media/screenshots/torrent-details.png) 14 | 15 | ![Admin settings](./media/screenshots/admin-settings-backend.png) 16 | 17 | ![Admin settings](./media/screenshots/admin-settings-categories.png) 18 | 19 | ![Admin settings](./media/screenshots/admin-settings-tags.png) 20 | -------------------------------------------------------------------------------- /docs/user_guide.md: -------------------------------------------------------------------------------- 1 | # User guide 2 | 3 | ## Roles 4 | 5 | There are only three roles: 6 | 7 | - `Guest`: unauthenticated user. 8 | - `User`: authenticated user. 9 | - `Admin`: authenticated user with admin privileges. 10 | 11 | > **NOTICE**: there is only one "admin" and it's the account of the first registered user. 12 | 13 | ## Upload a torrent 14 | 15 | The torrent description supports markdown syntax. You can use it to add links, images, etc. 16 | 17 | > **NOTICE** Only PNG images are supported at the moment. 18 | 19 | You can add a PNG image with: 20 | 21 | ```text 22 | ![alternative description for the image](https://raw.githubusercontent.com/torrust/torrust-index-gui/develop/docs/media/torrust_logo.png) 23 | ``` 24 | 25 | The image will be proxied by the backend. This means that the image will be downloaded by the backend and served by the backend itself. The backend will cache the image but you have to make sure that the image is available at the URL you provided. 26 | 27 | ### Canonical Infohash Group 28 | 29 | We only support standard fields in the torrent info dictionary. 30 | 31 | ```rust 32 | pub struct TorrentInfoDictionary { 33 | pub name: String, 34 | pub pieces: Option, 35 | pub piece_length: i64, 36 | pub md5sum: Option, 37 | pub length: Option, 38 | pub files: Option>, 39 | pub private: Option, 40 | pub path: Option>, 41 | pub root_hash: Option, 42 | pub source: Option, 43 | } 44 | ``` 45 | 46 | Check the data structure [TorrentInfoDictionary](https://github.com/torrust/torrust-index/blob/develop/src/models/torrent_file.rs) for an updated version of the supported fields. 47 | 48 | We allow uploading torrents with other custom fields, however those extra fields are removed from the torrent `info` dictionary. That causes the infohash to change. We call the "Canonical Infohash" the resulting infohash after removing the non-standard fields from the `info` dictionary. 49 | 50 | You can use the original infohash in URLs to navigate to the torrent details and you also have a list of original infohashes belonging to the same infohash group in the torrent details. 51 | 52 | If you think there is an important field missing in the `info` dictionary, please open an issue. 53 | 54 | ## Categories 55 | 56 | Torrents can have only one category. You have to assign a category to your torrent when you upload it. 57 | 58 | If the "admin" deletes the category used by a torrent, the torrent category will be set to `null`. 59 | 60 | ## Tags 61 | 62 | Torrents can have multiple tags. You can assign tags to your torrent when you upload it. Tags are created by the "admin" and users can only choose from the existing tags. 63 | 64 | If the "admin" deletes a tag, the tag will be removed from all the torrents that use it. 65 | 66 | ## Notes 67 | 68 | - The application does not support [BitTorrent Version 2][BEP_52]. 69 | 70 | [BEP_52]: https://www.bittorrent.org/beps/bep_0052.html 71 | -------------------------------------------------------------------------------- /dot.env.local: -------------------------------------------------------------------------------- 1 | # App build variables 2 | API_BASE_URL=http://localhost:3001/v1 3 | 4 | # Rust SQLx 5 | DATABASE_URL=sqlite://storage/database/data.db?mode=rwc 6 | 7 | # Docker compose 8 | TORRUST_INDEX_CONFIG_TOML= 9 | USER_ID=1000 10 | TORRUST_TRACKER_CONFIG_TOML= 11 | TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN=MyAccessToken 12 | -------------------------------------------------------------------------------- /img/Torrust_Repo_FrontEnd_Readme_Header-20220615.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrust/torrust-index-gui/6d7a6213670819bf708e280eb01126a451de5982/img/Torrust_Repo_FrontEnd_Readme_Header-20220615.jpg -------------------------------------------------------------------------------- /licensing/file_header_agplv3.txt: -------------------------------------------------------------------------------- 1 | Torrust Index 2 | 3 | Project owner: Nautilus Cyberneering GmbH. 4 | Github repository: https://github.com/torrust/torrust-index 5 | Project description: 6 | Torrust is a suite of client-server software for hosting online torrent indexes. 7 | 8 | Copyright (C) 2021 Nautilus Cyberneering GmbH 9 | 10 | This program is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as 12 | published by the Free Software Foundation, either version 3 of the 13 | License, or (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with this program. If not, see . 22 | -------------------------------------------------------------------------------- /licensing/old_commits/mit-0.md: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Nautilus Cyberneering GmbH 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://v3.nuxtjs.org/api/configuration/nuxt.config 2 | import eslintPlugin from "vite-plugin-eslint"; 3 | 4 | export default defineNuxtConfig({ 5 | ssr: false, 6 | 7 | runtimeConfig: { 8 | public: { 9 | apiBase: process.env.API_BASE_URL 10 | } 11 | }, 12 | 13 | modules: [ 14 | "@nuxtjs/tailwindcss", 15 | "@nuxtjs/color-mode" 16 | ], 17 | 18 | colorMode: { 19 | preference: "dark", // default value of $colorMode.preference 20 | fallback: "dark", // fallback value if not system preference found 21 | hid: "nuxt-color-mode-script", 22 | globalName: "__NUXT_COLOR_MODE__", 23 | componentName: "ColorScheme", 24 | classPrefix: "", 25 | classSuffix: "", 26 | storageKey: "nuxt-color-mode", 27 | dataValue: "theme" 28 | }, 29 | 30 | vite: { 31 | server: { 32 | fs: { 33 | // Allow serving files from one level up to the project root 34 | allow: [".."] 35 | } 36 | }, 37 | plugins: [ 38 | eslintPlugin() 39 | ] 40 | }, 41 | 42 | devtools: { 43 | enabled: true 44 | }, 45 | 46 | postcss: { 47 | plugins: { 48 | "tailwindcss/nesting": {}, 49 | tailwindcss: {}, 50 | autoprefixer: {} 51 | } 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "torrust-index-gui", 3 | "version": "3.1.0", 4 | "description": "The frontend for the Torrust Index project.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/torrust/torrust-index-gui.git" 8 | }, 9 | "license": "SEE LICENSE IN COPYRIGHT", 10 | "scripts": { 11 | "build": "nuxt build", 12 | "dev": "nuxt dev", 13 | "generate": "nuxt generate", 14 | "preview": "nuxt preview", 15 | "postinstall": "nuxt prepare", 16 | "lint": "eslint --ext \".ts,.js,.vue\" . --max-warnings=0", 17 | "lintfix": "eslint --fix --ext \".ts,.js,.vue\" .", 18 | "cypress:open": "cypress open", 19 | "cypress:run": "cypress run" 20 | }, 21 | "devDependencies": { 22 | "@nuxt/devtools": "^1.3.1", 23 | "@nuxtjs/color-mode": "^3.4.1", 24 | "@nuxtjs/eslint-config-typescript": "^12.1.0", 25 | "@nuxtjs/tailwindcss": "^6.12.0", 26 | "@tailwindcss/typography": "^0.5.13", 27 | "@types/dompurify": "^3.0.5", 28 | "@types/marked": "^5.0.2", 29 | "@types/node": "^20.12.2", 30 | "@types/sqlite3": "^3.1.11", 31 | "@typescript-eslint/eslint-plugin": "^6.21.0", 32 | "@typescript-eslint/parser": "^6.21.0", 33 | "cypress": "^13.10.0", 34 | "eslint": "^8.57.0", 35 | "eslint-plugin-vue": "^9.26.0", 36 | "i": "^0.3.7", 37 | "npm": "^10.8.0", 38 | "nuxt": "^3.11.2", 39 | "sqlite3": "^5.1.7", 40 | "typescript": "^5.4.5", 41 | "vite-plugin-eslint": "^1.8.1" 42 | }, 43 | "dependencies": { 44 | "@heroicons/vue": "^2.1.5", 45 | "@types/uuid": "^9.0.8", 46 | "canvas": "^2.11.2", 47 | "daisyui": "^4.12.10", 48 | "dompurify": "^3.1.6", 49 | "marked": "^12.0.2", 50 | "notiwind-ts": "^2.0.2", 51 | "torrust-index-api-lib": "^3.2.0", 52 | "torrust-index-types-lib": "^3.1.0", 53 | "uuid": "^9.0.1" 54 | } 55 | } -------------------------------------------------------------------------------- /pages/admin/settings.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 70 | 71 | 76 | -------------------------------------------------------------------------------- /pages/admin/settings/backend.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 23 | 24 | 33 | -------------------------------------------------------------------------------- /pages/admin/settings/categories.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 78 | -------------------------------------------------------------------------------- /pages/admin/settings/tags.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 79 | -------------------------------------------------------------------------------- /pages/admin/settings/users.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 102 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /pages/license.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 25 | 26 | 30 | -------------------------------------------------------------------------------- /pages/signin.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /pages/signup.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /pages/terms.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 31 | 32 | 35 | -------------------------------------------------------------------------------- /pages/torrent/[infoHash].vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /pages/torrent/[infoHash]/[title].vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /pages/user/[username].vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /plugins/notifications.client.ts: -------------------------------------------------------------------------------- 1 | import Notifications from "notiwind-ts"; 2 | import { defineNuxtPlugin } from "#imports"; 3 | 4 | export default defineNuxtPlugin((nuxtApp: { vueApp: { use: (arg0: any) => void; }; }) => { 5 | nuxtApp.vueApp.use(Notifications); 6 | }); 7 | -------------------------------------------------------------------------------- /project-words.txt: -------------------------------------------------------------------------------- 1 | caniuse 2 | codecov 3 | composables 4 | Containerfile 5 | daisyui 6 | dinamically 7 | dompurify 8 | filedrag 9 | filelist 10 | HEALTHCHECK 11 | heroicons 12 | infohash 13 | lintfix 14 | mailcatcher 15 | notiwind 16 | Nuxi 17 | Nuxt 18 | nuxtjs 19 | proxied 20 | Quickstart 21 | signin 22 | struct 23 | uuidv 24 | vuex 25 | -------------------------------------------------------------------------------- /public/COPYRIGHT.md: -------------------------------------------------------------------------------- 1 | # Copyright 2024 2 | 3 | Copyright 2024 in the Torrust-Index-Frontend project are retained by their contributors. No 4 | copyright assignment is required to contribute to the Torrust-Index-Frontend project. 5 | 6 | Some files include explicit copyright notices and/or license notices. 7 | 8 | Except as otherwise noted (below and/or in individual files), Torrust-Index-Frontend is 9 | licensed under the GNU Affero General Public License, Version 3.0 . This license applies to all files in the Torrust-Index-Frontend project, except as noted below. 10 | 11 | Except as otherwise noted (below and/or in individual files), Torrust-Index-Frontend is licensed under the MIT-0 license for all commits made after 5 years of merging. This license applies to the version of the files merged into the Torrust-Index-Frontend project at the time of merging, and does not apply to subsequent updates or revisions to those files. 12 | 13 | The contributors to the Torrust-Index-Frontend project disclaim all liability for any damages or losses that may arise from the use of the project. 14 | 15 | -------------------------------------------------------------------------------- /public/LICENSE-MIT_0.md: -------------------------------------------------------------------------------- 1 | # MIT No Attribution 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrust/torrust-index-gui/6d7a6213670819bf708e280eb01126a451de5982/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrust/torrust-index-gui/6d7a6213670819bf708e280eb01126a451de5982/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrust/torrust-index-gui/6d7a6213670819bf708e280eb01126a451de5982/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrust/torrust-index-gui/6d7a6213670819bf708e280eb01126a451de5982/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrust/torrust-index-gui/6d7a6213670819bf708e280eb01126a451de5982/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrust/torrust-index-gui/6d7a6213670819bf708e280eb01126a451de5982/public/favicon.ico -------------------------------------------------------------------------------- /public/icons/computer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/legacy_exception.md: -------------------------------------------------------------------------------- 1 | # Legacy Exception 2 | 3 | For prosperity, versions of Torrust Tracker that are older than five years are automatically granted the [MIT-0][MIT_0] license in addition to the existing [AGPL-3.0-only][AGPL_3_0] license. -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /share/container/entry_script_sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -x 3 | 4 | to_lc() { echo "$1" | tr '[:upper:]' '[:lower:]'; } 5 | clean() { echo "$1" | tr -d -c 'a-zA-Z0-9-' ; } 6 | cmp_lc() { [ "$(to_lc "$(clean "$1")")" = "$(to_lc "$(clean "$2")")" ]; } 7 | 8 | # Add torrust user, based upon supplied user-id. 9 | if [ -z "$USER_ID" ] && [ "$USER_ID" -lt 1000 ]; then 10 | echo "ERROR: USER_ID is not set, or less than 1000" 11 | exit 1 12 | fi 13 | 14 | adduser --disabled-password --shell "/bin/sh" --uid "$USER_ID" "torrust" 15 | 16 | # Configure Permissions for Torrust Folders 17 | chown -R "${USER_ID}" /var/log/torrust 18 | chmod -R 2770 /var/log/torrust 19 | 20 | # Make Minimal Message of the Day 21 | if cmp_lc "$RUNTIME" "runtime"; then 22 | printf '\n in runtime \n' >> /etc/motd; 23 | elif cmp_lc "$RUNTIME" "debug"; then 24 | printf '\n in debug mode \n' >> /etc/motd; 25 | elif cmp_lc "$RUNTIME" "release"; then 26 | printf '\n in release mode \n' >> /etc/motd; 27 | else 28 | echo "ERROR: running in unknown mode: \"$RUNTIME\""; exit 1 29 | fi 30 | 31 | if [ -e "/usr/share/torrust/container/message" ]; then 32 | cat "/usr/share/torrust/container/message" >> /etc/motd; chmod 0644 /etc/motd 33 | fi 34 | 35 | # Load message of the day from Profile 36 | # shellcheck disable=SC2016 37 | echo '[ ! -z "$TERM" -a -r /etc/motd ] && cat /etc/motd' >> /etc/profile 38 | 39 | cd /home/torrust || exit 1 40 | 41 | # Switch to torrust user 42 | exec /bin/su-exec torrust "$@" 43 | -------------------------------------------------------------------------------- /share/container/health_check.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This application is used to create a `HEALTHCHECK` instruction in the 4 | `Dockerfile` or `Containerfile`. 5 | 6 | Usage: 7 | 8 | node health_check.js 3000 9 | node health_check.js PORT 10 | 11 | */ 12 | 13 | const http = require("http"); 14 | 15 | // Retrieve the port number from the command-line arguments. 16 | // Default to 3000 if no argument is provided 17 | const port = process.argv[2] || "3000"; 18 | 19 | const options = { 20 | host: "localhost", 21 | port, 22 | timeout: 2000 23 | }; 24 | 25 | const request = http.request(options, (res) => { 26 | console.log(`STATUS: ${res.statusCode}`); 27 | if (res.statusCode === 200) { 28 | process.exit(0); 29 | } 30 | else { 31 | process.exit(1); 32 | } 33 | }); 34 | 35 | request.on("error", function (err) { 36 | console.log("ERROR"); 37 | process.exit(1); 38 | }); 39 | 40 | request.end(); 41 | -------------------------------------------------------------------------------- /share/container/message: -------------------------------------------------------------------------------- 1 | 2 | Lovely welcome to our Torrust Index GUI Container! 3 | 4 | run 'torrust-index-gui' to start the index 5 | -------------------------------------------------------------------------------- /share/default/config/index.private.e2e.container.sqlite3.toml: -------------------------------------------------------------------------------- 1 | [metadata] 2 | app = "torrust-index" 3 | purpose = "configuration" 4 | schema_version = "2.0.0" 5 | 6 | [logging] 7 | #threshold = "off" 8 | #threshold = "error" 9 | #threshold = "warn" 10 | threshold = "info" 11 | #threshold = "debug" 12 | #threshold = "trace" 13 | 14 | [tracker] 15 | api_url = "http://tracker:1212" 16 | private = true 17 | url = "http://tracker:7070" 18 | token = "MyAccessToken" 19 | 20 | [auth] 21 | user_claim_token_pepper = "MaxVerstappenWC2021" 22 | 23 | [database] 24 | connect_url = "sqlite:///var/lib/torrust/index/database/e2e_testing_sqlite3.db?mode=rwc" 25 | 26 | [mail.smtp] 27 | port = 1025 28 | server = "mailcatcher" 29 | 30 | [registration] 31 | 32 | [registration.email] 33 | #required = false 34 | #verified = false -------------------------------------------------------------------------------- /share/default/config/index.public.e2e.container.sqlite3.toml: -------------------------------------------------------------------------------- 1 | [metadata] 2 | app = "torrust-index" 3 | purpose = "configuration" 4 | schema_version = "2.0.0" 5 | 6 | [logging] 7 | #threshold = "off" 8 | #threshold = "error" 9 | #threshold = "warn" 10 | threshold = "info" 11 | #threshold = "debug" 12 | #threshold = "trace" 13 | 14 | [tracker] 15 | api_url = "http://tracker:1212" 16 | private = false 17 | url = "udp://tracker:6969" 18 | token = "MyAccessToken" 19 | 20 | [auth] 21 | user_claim_token_pepper = "MaxVerstappenWC2021" 22 | 23 | [database] 24 | connect_url = "sqlite:///var/lib/torrust/index/database/e2e_testing_sqlite3.db?mode=rwc" 25 | 26 | [mail.smtp] 27 | port = 1025 28 | server = "mailcatcher" 29 | 30 | [registration] 31 | 32 | [registration.email] 33 | #required = false 34 | #verified = false -------------------------------------------------------------------------------- /share/default/config/tracker.private.e2e.container.sqlite3.toml: -------------------------------------------------------------------------------- 1 | [metadata] 2 | schema_version = "2.0.0" 3 | 4 | [logging] 5 | #threshold = "off" 6 | #threshold = "error" 7 | #threshold = "warn" 8 | threshold = "info" 9 | #threshold = "debug" 10 | #threshold = "trace" 11 | 12 | [core] 13 | listed = false 14 | private = true 15 | 16 | [core.database] 17 | path = "/var/lib/torrust/tracker/database/e2e_testing_sqlite3.db" 18 | 19 | [[udp_trackers]] 20 | bind_address = "0.0.0.0:6969" 21 | 22 | [http_api] 23 | bind_address = "0.0.0.0:1212" 24 | 25 | [http_api.access_tokens] 26 | admin = "MyAccessToken" -------------------------------------------------------------------------------- /share/default/config/tracker.public.e2e.container.sqlite3.toml: -------------------------------------------------------------------------------- 1 | [metadata] 2 | schema_version = "2.0.0" 3 | 4 | [logging] 5 | #threshold = "off" 6 | #threshold = "error" 7 | #threshold = "warn" 8 | threshold = "info" 9 | #threshold = "debug" 10 | #threshold = "trace" 11 | 12 | [core] 13 | listed = false 14 | private = false 15 | 16 | [core.database] 17 | path = "/var/lib/torrust/tracker/database/e2e_testing_sqlite3.db" 18 | 19 | [http_api] 20 | bind_address = "0.0.0.0:1212" 21 | 22 | [http_api.access_tokens] 23 | admin = "MyAccessToken" -------------------------------------------------------------------------------- /src/domain/services/sanitizer.ts: -------------------------------------------------------------------------------- 1 | import { createCanvas, registerFont, CanvasRenderingContext2D } from "canvas"; 2 | import DOMPurify from "dompurify"; 3 | import { useRestApi } from "#imports"; 4 | 5 | const rest = useRestApi().value; 6 | 7 | const allowedTags = ["h1", "h2", "h3", "h4", "h5", "h6", "em", "strong", "del", "a", "img", "ul", "ol", "li", "hr", "p"]; 8 | const allowedImageExtensions = ["png", "PNG", "jpg", "JPG", "jpeg", "JPEG", "gif", "GIF"]; 9 | 10 | export async function sanitize (html: string) { 11 | const safeHtml = remove_harmful_code(html); 12 | const htmlWithNoUserTracking = await remove_user_tracking(safeHtml); 13 | return htmlWithNoUserTracking; 14 | } 15 | 16 | function remove_harmful_code (html: string) { 17 | return DOMPurify.sanitize(html, { ALLOWED_TAGS: allowedTags }); 18 | } 19 | 20 | async function remove_user_tracking (html: string) { 21 | // Parse the description as HTML to easily manipulate it. 22 | const parser = new DOMParser(); 23 | 24 | const htmlDoc = parser.parseFromString(html, "text/html"); 25 | 26 | remove_all_external_links(htmlDoc); 27 | 28 | await replace_images_with_proxied_images(htmlDoc); 29 | 30 | return document_to_html(htmlDoc); 31 | } 32 | 33 | function remove_all_external_links (htmlDoc: Document) { 34 | const links = htmlDoc.querySelectorAll("a"); 35 | links.forEach((link) => { 36 | const href = link.getAttribute("href"); 37 | if (href && !href.startsWith("#")) { 38 | link.removeAttribute("href"); 39 | } 40 | }); 41 | } 42 | 43 | async function replace_images_with_proxied_images (htmlDoc: Document) { 44 | const images = htmlDoc.querySelectorAll("img"); 45 | for (let i = 0; i < images.length; i++) { 46 | const img = images[i]; 47 | const src = img.getAttribute("src"); 48 | 49 | if (src) { 50 | if (isAllowedImage(src)) { 51 | try { 52 | const imageDataSrc = await getImageDataUrl(src); 53 | img.setAttribute("src", imageDataSrc); 54 | } catch (e) { 55 | const imageDataUrl = createImageWithText(`Can't load proxied image: ${src}`, 1000, 50, 15); 56 | img.setAttribute("src", imageDataUrl); 57 | } 58 | } else { 59 | const imageDataUrl = createImageWithText(`Not allowed image extension. It must be: ${allowedImageExtensions.concat()}`, 1000, 50, 15); 60 | img.setAttribute("src", imageDataUrl); 61 | } 62 | } 63 | } 64 | } 65 | 66 | function document_to_html (descriptionHtml: Document) { 67 | const body = descriptionHtml.querySelector("body"); 68 | const serializer = new XMLSerializer(); 69 | let html = ""; 70 | if (body) { 71 | html = serializer.serializeToString(body); 72 | html = html 73 | .replace("", "") 74 | .replace("", "") 75 | .replace("", ""); 76 | } 77 | return html; 78 | } 79 | 80 | // Returns true if the image is allowed to be displayed. 81 | function isAllowedImage (href: string): boolean { 82 | const extension = href.split(".").pop().trim(); 83 | return allowedImageExtensions.includes(extension); 84 | } 85 | 86 | // Returns a base64 string ready to be use in a "src" attribute in a "img" html tag, 87 | // like this ``. 88 | async function getImageDataUrl (url: string): Promise { 89 | const imageBlob = await rest.torrent.proxiedImage(url); 90 | const data = await blobToDataURL(imageBlob); 91 | return data; 92 | } 93 | 94 | // Convert binary data into a base64 encoded string ready to be use in a "src" 95 | // attribute in a "img" html tag, like the following: 96 | // ``. 97 | function blobToDataURL (blob: Blob): Promise { 98 | return new Promise((resolve, reject) => { 99 | const reader = new FileReader(); 100 | reader.onload = _e => resolve(reader.result as string); 101 | reader.onerror = _e => reject(reader.error); 102 | reader.onabort = _e => reject(new Error("Read aborted")); 103 | reader.readAsDataURL(blob); 104 | }); 105 | } 106 | 107 | function createImageWithText (text: string, width: number, height: number, font: number): string { 108 | // Create a canvas element 109 | const canvas = document.createElement("canvas"); 110 | canvas.width = width; 111 | canvas.height = height; 112 | const context = canvas.getContext("2d"); 113 | 114 | if (context) { 115 | // Set background color (optional) 116 | context.fillStyle = "white"; // or "transparent" for a transparent background 117 | context.fillRect(0, 0, width, height); 118 | 119 | // Set text properties 120 | context.fillStyle = "black"; 121 | context.font = `${font}px Arial`; // Change font size and family as needed 122 | 123 | // Align text 124 | context.textAlign = "center"; 125 | context.textBaseline = "middle"; 126 | 127 | // Draw text 128 | context.fillText(text, width / 2, height / 2); 129 | } 130 | 131 | // Convert canvas to data URL (base64 encoded string) 132 | return canvas.toDataURL("image/png"); 133 | } 134 | -------------------------------------------------------------------------------- /src/domain/services/slug.ts: -------------------------------------------------------------------------------- 1 | export function generateSlug (input: string): string { 2 | return input 3 | .toLowerCase() 4 | .replace(/[\s_]+/g, "-") // Replace spaces and underscores with - 5 | .replace(/[^\w-]+/g, "") // Remove all non-word characters 6 | .replace(/--+/g, "-") // Replace multiple - with single - 7 | .replace(/^-+/, "") // Trim - from start of text 8 | .replace(/-+$/, ""); // Trim - from end of text 9 | } 10 | -------------------------------------------------------------------------------- /src/helpers/DateConverter.ts: -------------------------------------------------------------------------------- 1 | type UnixTimestamp = number; 2 | type FormattedDate = string; 3 | 4 | class InvalidDateError extends Error {} 5 | 6 | /** 7 | * Takes the date in seconds from Unix Epoch time and converts it to human readable format. 8 | * 9 | * For example: 1701688451 -> "Mon Dec 04 2023" 10 | */ 11 | 12 | export function formatTimestamp (creationDate: UnixTimestamp): FormattedDate | Error { 13 | const milliseconds = creationDate * 1000; 14 | 15 | const convertedDate = new Date(milliseconds); 16 | 17 | return isNaN(convertedDate.valueOf()) 18 | ? new InvalidDateError( 19 | `Invalid date. Could not create a new date from timestamp value: ${creationDate}`) 20 | : convertedDate.toDateString(); 21 | } 22 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "components/**/*.{vue,js,ts}", 5 | "layouts/**/*.vue", 6 | "pages/**/*.vue", 7 | "app.vue", 8 | "plugins/**/*.{js,ts}", 9 | "nuxt.config.{js,ts}" 10 | ], 11 | theme: { 12 | fontFamily: { 13 | display: ["Inter", "system-ui", "sans-serif"], 14 | body: ["Inter", "system-ui", "sans-serif"] 15 | } 16 | }, 17 | daisyui: { 18 | themes: [ 19 | { 20 | dark: { 21 | primary: "#f28c18", 22 | "primary-content": "#fff7ee", 23 | secondary: "#187EF2", 24 | accent: "#51a800", 25 | neutral: "#1b1d1d", 26 | "base-100": "#212121", 27 | info: "#2563eb", 28 | success: "#16a34a", 29 | warning: "#d97706", 30 | error: "#dc2626" 31 | }, 32 | light: { 33 | primary: "#f28c18", 34 | "primary-content": "#ffffff", 35 | secondary: "#187EF2", 36 | accent: "#51a800", 37 | neutral: "#3b424e", 38 | "base-100": "#f0f0f0", 39 | "base-200": "#f5f5f5", 40 | "base-300": "#ffffff", 41 | info: "#2563eb", 42 | success: "#16a34a", 43 | warning: "#d97706", 44 | error: "#dc2626", 45 | "neutral-content": "#333333" 46 | } 47 | } 48 | ] 49 | }, 50 | darkMode: "class", 51 | plugins: [ 52 | require("daisyui"), 53 | require("@tailwindcss/typography") 54 | ] 55 | }; 56 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://v3.nuxtjs.org/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json", 4 | "compilerOptions": { 5 | "strictPropertyInitialization": false, 6 | "strictNullChecks": false, 7 | "baseUrl": "./", 8 | "paths": { 9 | "#imports": [".nuxt/imports.d.ts"] 10 | } 11 | } 12 | } 13 | --------------------------------------------------------------------------------