├── .bumpversion.cfg ├── .dockerignore ├── .editorconfig ├── .flake8 ├── .github └── workflows │ ├── codeql-analysis.yml │ ├── docker.yml │ └── lint.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── backend ├── .flake8 ├── .gitignore ├── gunicorn.conf.py ├── manage.py ├── manage.sh ├── node_modules │ └── .yarn-integrity ├── poetry.lock ├── pyproject.toml ├── start.sh ├── tabby │ ├── __init__.py │ ├── app │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── api │ │ │ ├── __init__.py │ │ │ ├── app_version.py │ │ │ ├── auth.py │ │ │ ├── config.py │ │ │ ├── gateway.py │ │ │ └── user.py │ │ ├── apps.py │ │ ├── gateway.py │ │ ├── management │ │ │ ├── __init__.py │ │ │ └── commands │ │ │ │ ├── __init__.py │ │ │ │ └── add_version.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_gateway.py │ │ │ ├── 0003_auto_20210711_1855.py │ │ │ ├── 0004_sync_token.py │ │ │ ├── 0005_user_force_pro.py │ │ │ ├── 0006_config_name.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── sponsors.py │ │ ├── urls.py │ │ └── views.py │ ├── middleware.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── yarn.lock ├── docker-compose.yml ├── docs └── screenshot.png └── frontend ├── .eslintrc.yml ├── .gitignore ├── .pug-lintrc.js ├── assets ├── demo.jpeg ├── favicon.png ├── logo.svg ├── meta-preview.png └── screenshots │ ├── colors.png │ ├── fonts.png │ ├── history.png │ ├── hotkeys.png │ ├── paste.png │ ├── ports.png │ ├── profiles.png │ ├── progress.png │ ├── quake.png │ ├── serial.png │ ├── split.png │ ├── ssh.png │ ├── ssh2.png │ ├── tabs.png │ ├── win.png │ ├── window.png │ └── zmodem.png ├── package.json ├── src ├── api.ts ├── app.component.ts ├── app.module.ts ├── app.server.module.ts ├── app │ ├── components │ │ ├── configModal.component.pug │ │ ├── configModal.component.ts │ │ ├── connectionList.component.pug │ │ ├── connectionList.component.ts │ │ ├── main.component.pug │ │ ├── main.component.scss │ │ ├── main.component.ts │ │ ├── settingsModal.component.pug │ │ ├── settingsModal.component.ts │ │ ├── upgradeModal.component.pug │ │ └── upgradeModal.component.ts │ ├── index.ts │ └── services │ │ └── appConnector.service.ts ├── common │ ├── index.ts │ ├── interceptor.ts │ └── services │ │ ├── common.service.ts │ │ ├── config.service.ts │ │ └── login.service.ts ├── demo.html ├── demo.ts ├── index.html ├── index.server.ts ├── index.ts ├── login │ ├── components │ │ ├── login.component.pug │ │ ├── login.component.scss │ │ └── login.component.ts │ └── index.ts ├── server.ts ├── ssr-polyfills.ts ├── styles.scss ├── terminal-styles.scss ├── terminal.html └── terminal.ts ├── theme ├── index.scss └── vars.scss ├── tsconfig.json ├── webpack.config.base.js ├── webpack.config.js ├── webpack.config.server.js └── yarn.lock /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.0.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:frontend/package.json] 7 | search = "version": "{current_version}" 8 | replace = "version": "{new_version}" 9 | 10 | [bumpversion:file:backend/pyproject.toml] 11 | search = version = "{current_version}" 12 | replace = version = "{new_version}" 13 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | backend/__pycache__ 2 | backend/public 3 | frontend/build 4 | frontend/build-server 5 | frontend/node_modules 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.ts] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore=E501,D103,C901,D203,W504,S607,S603,S404,S606,S322,S410,S320,B010 3 | exclude = .git,__pycache__,help,static,misc,locale,templates,tests,deployment,migrations,elements/ai/scripts 4 | max-complexity = 40 5 | builtins = _ 6 | per-file-ignores = scripts/*:T001,E402 7 | select = C,E,F,W,B,B902 8 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '17 8 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript', 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | schedule: 5 | - cron: '25 12 * * *' 6 | push: 7 | branches: [ master ] 8 | # Publish semver tags as releases. 9 | tags: [ 'v*.*.*' ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | env: 14 | REGISTRY: ghcr.io 15 | IMAGE_NAME: eugeny/tabby-web 16 | 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: read 23 | packages: write 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v2 28 | 29 | # https://github.com/docker/setup-qemu-action 30 | - name: Set up QEMU 31 | uses: docker/setup-qemu-action@v1 32 | 33 | # https://github.com/docker/setup-buildx-action 34 | - name: Set up Docker Buildx 35 | id: buildx 36 | uses: docker/setup-buildx-action@v1 37 | 38 | - name: Log into registry ${{ env.REGISTRY }} 39 | if: github.event_name != 'pull_request' 40 | uses: docker/login-action@v1 41 | with: 42 | registry: ${{ env.REGISTRY }} 43 | username: ${{ github.actor }} 44 | password: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | - name: Extract Docker metadata 47 | id: meta 48 | uses: docker/metadata-action@v3 49 | with: 50 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 51 | tags: | 52 | type=schedule 53 | type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} 54 | type=semver,pattern={{version}} 55 | type=semver,pattern={{major}}.{{minor}} 56 | type=semver,pattern={{major}} 57 | 58 | - name: Build and push Docker image 59 | uses: docker/build-push-action@v2 60 | with: 61 | context: . 62 | build-args: EXTRA_DEPS=gcsfs 63 | platforms: linux/amd64,linux/arm64 64 | push: ${{ github.event_name != 'pull_request' }} 65 | tags: ${{ steps.meta.outputs.tags }} 66 | labels: ${{ steps.meta.outputs.labels }} 67 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: [push, pull_request] 3 | jobs: 4 | Lint: 5 | runs-on: ubuntu-latest 6 | 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v2.3.4 10 | with: 11 | fetch-depth: 0 12 | 13 | - name: Installing Node 14 | uses: actions/setup-node@v2.4.0 15 | with: 16 | node-version: 14 17 | 18 | - name: Install frontend deps 19 | working-directory: frontend 20 | run: | 21 | npm i -g yarn@1.19.1 22 | yarn 23 | 24 | - name: Lint frontend 25 | working-directory: frontend 26 | run: yarn lint 27 | 28 | - name: Install backend deps 29 | working-directory: backend 30 | run: | 31 | pip3 install poetry 32 | poetry install 33 | 34 | - name: Lint backend 35 | working-directory: backend 36 | run: poetry run flake8 . 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .env 3 | app-dist 4 | .mypy_cache 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM node:12-alpine AS frontend-build 3 | WORKDIR /app 4 | COPY frontend/package.json frontend/yarn.lock ./ 5 | RUN yarn install --frozen-lockfile --network-timeout 1000000 6 | COPY frontend/webpack* frontend/tsconfig.json ./ 7 | COPY frontend/assets assets 8 | COPY frontend/src src 9 | COPY frontend/theme theme 10 | RUN yarn run build 11 | RUN yarn run build:server 12 | 13 | FROM node:12-alpine AS frontend 14 | WORKDIR /app 15 | COPY --from=frontend-build /app/build build 16 | COPY --from=frontend-build /app/build-server build-server 17 | COPY frontend/package.json . 18 | 19 | CMD ["npm", "start"] 20 | 21 | # ---- 22 | 23 | FROM python:3.7-alpine AS build-backend 24 | ARG EXTRA_DEPS 25 | 26 | RUN apk add build-base musl-dev libffi-dev openssl-dev mariadb-dev bash curl 27 | 28 | WORKDIR /app 29 | 30 | # Rust (for python-cryptography) 31 | RUN curl https://sh.rustup.rs -sSf | bash -s -- -y 32 | ENV PATH /root/.cargo/bin:$PATH 33 | 34 | RUN pip install -U setuptools cryptography==37.0.4 poetry==1.1.7 35 | COPY backend/pyproject.toml backend/poetry.lock ./ 36 | RUN poetry config virtualenvs.path /venv 37 | RUN poetry install --no-dev --no-ansi --no-interaction 38 | RUN poetry run pip install -U setuptools psycopg2-binary $EXTRA_DEPS 39 | 40 | COPY backend/manage.py backend/gunicorn.conf.py ./ 41 | COPY backend/tabby tabby 42 | COPY --from=frontend /app/build /frontend 43 | 44 | ARG BUNDLED_TABBY=1.0.187-nightly.1 45 | 46 | RUN FRONTEND_BUILD_DIR=/frontend /venv/*/bin/python ./manage.py collectstatic --noinput 47 | RUN APP_DIST_STORAGE=file:///app-dist /venv/*/bin/python ./manage.py add_version ${BUNDLED_TABBY} 48 | 49 | # ---- 50 | 51 | FROM python:3.7-alpine AS backend 52 | 53 | ENV APP_DIST_STORAGE file:///app-dist 54 | ENV DOCKERIZE_VERSION v0.6.1 55 | ENV DOCKERIZE_ARCH amd64 56 | ARG TARGETPLATFORM 57 | RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; \ 58 | then export DOCKERIZE_ARCH=armhf; \ 59 | else export DOCKERIZE_ARCH=amd64; \ 60 | fi 61 | RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-$DOCKERIZE_ARCH-$DOCKERIZE_VERSION.tar.gz \ 62 | && tar -C /usr/local/bin -xzvf dockerize-linux-$DOCKERIZE_ARCH-$DOCKERIZE_VERSION.tar.gz \ 63 | && rm dockerize-linux-$DOCKERIZE_ARCH-$DOCKERIZE_VERSION.tar.gz 64 | 65 | RUN apk add mariadb-connector-c gcc 66 | 67 | COPY --from=build-backend /app /app 68 | COPY --from=build-backend /app-dist /app-dist 69 | COPY --from=build-backend /venv /venv 70 | 71 | COPY backend/start.sh backend/manage.sh / 72 | RUN chmod +x /start.sh /manage.sh 73 | CMD ["/start.sh"] 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Eugeny 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tabby Web 2 | 3 | ## Note on project status 4 | 5 | > [!IMPORTANT] 6 | > At this time I don't have the time to work on `tabby-web` and won't be able to provide help or support for it. I'm still happy to merge any fixes/improvement PRs. :v: 7 | 8 | 9 | ![](docs/screenshot.png) 10 | 11 | This is the Tabby terminal, served as a web app. It also provides the config sync service for the Tabby app. 12 | 13 | # How it works 14 | 15 | Tabby Web serves the [Tabby Terminal](https://github.com/Eugeny/tabby) as a web application while managing multiple config files, authentication, and providing TCP connections via a [separate gateway service](https://github.com/Eugeny/tabby-connection-gateway). 16 | 17 | # Requirements 18 | 19 | * Python 3.7+ 20 | * A database server supported by Django (MariaDB, Postgres, SQLite, etc.) 21 | * Storage for distribution files - local, S3, GCS or others supported by `fsspec` 22 | 23 | # Quickstart (using `docker-compose`) 24 | 25 | You'll need: 26 | 27 | * OAuth credentials from GitHub, GitLab, Google or Microsoft for authentication. 28 | * For SSH and Telnet: a [`tabby-connection-gateway`](https://github.com/Eugeny/tabby-connection-gateway) to forward traffic. 29 | * Docker BuildKit: `export DOCKER_BUILDKIT=1` 30 | 31 | ```bash 32 | docker-compose up -e SOCIAL_AUTH_GITHUB_KEY=xxx -e SOCIAL_AUTH_GITHUB_SECRET=yyy 33 | ``` 34 | 35 | will start Tabby Web on port 9090 with MariaDB as a storage backend. 36 | 37 | For SSH and Telnet, once logged in, enter your connection gateway address and auth token in the settings. 38 | 39 | ## Environment variables 40 | 41 | * `DATABASE_URL` (required). 42 | * `APP_DIST_STORAGE`: a `file://`, `s3://`, or `gcs://` URL to store app distros in. 43 | * `SOCIAL_AUTH_*_KEY` & `SOCIAL_AUTH_*_SECRET`: social login credentials, supported providers are `GITHUB`, `GITLAB`, `MICROSOFT_GRAPH` and `GOOGLE_OAUTH2`. 44 | 45 | ## Adding Tabby app versions 46 | 47 | * `docker-compose run tabby /manage.sh add_version 1.0.163` 48 | 49 | You can find the available version numbers [here](https://www.npmjs.com/package/tabby-web-container). 50 | 51 | # Development setup 52 | 53 | Put your environment vars (`DATABASE_URL`, etc.) in the `.env` file in the root of the repo. 54 | 55 | For the frontend: 56 | 57 | ```shell 58 | cd frontend 59 | yarn 60 | yarn run build # or yarn run watch 61 | ``` 62 | 63 | For the backend: 64 | 65 | ```shell 66 | cd backend 67 | poetry install 68 | ./manage.py migrate # set up the database 69 | ./manage.py add_version 1.0.156-nightly.2 # install an app distribution 70 | PORT=9000 poetry run gunicorn # optionally with --reload 71 | ``` 72 | 73 | # Security 74 | 75 | * When using Tabby Web for SSH/Telnet connectivity, your traffic will pass through a hosted gateway service. It's encrypted in transit (HTTPS) and the gateway servers authenticate themselves with a certificate before connections are made. However there's a non-zero risk of a MITM if a gateway service is compromised and the attacker gains access to the service's private key. 76 | * You can alleviate this risk by [hosting your own gateway service](https://github.com/Eugeny/tabby-connection-gateway), or your own copy of Tabby Web altogether. 77 | -------------------------------------------------------------------------------- /backend/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore=E501,D103,C901,D203,W504,S607,S603,S404,S606,S322,S410,S320,B010 3 | exclude = .git,__pycache__,help,static,misc,locale,templates,tests,deployment,migrations,elements/ai/scripts 4 | max-complexity = 40 5 | builtins = _ 6 | per-file-ignores = scripts/*:T001,E402 7 | select = C,E,F,W,B,B902 8 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | db.sqlite3 3 | public 4 | -------------------------------------------------------------------------------- /backend/gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | wsgi_app = "tabby.wsgi:application" 2 | workers = 4 3 | preload_app = True 4 | sendfile = True 5 | 6 | max_requests = 1000 7 | max_requests_jitter = 100 8 | -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tabby.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /backend/manage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [[ -n "$DOCKERIZE_ARGS" ]]; then 3 | dockerize $DOCKERIZE_ARGS 4 | fi 5 | cd /app 6 | /venv/*/bin/python ./manage.py $@ 7 | -------------------------------------------------------------------------------- /backend/node_modules/.yarn-integrity: -------------------------------------------------------------------------------- 1 | { 2 | "systemParams": "darwin-x64-83", 3 | "modulesFolders": [], 4 | "flags": [], 5 | "linkedModules": [ 6 | "elements-sdk", 7 | "elements-sdk-angular", 8 | "node-pty", 9 | "shift-protocol" 10 | ], 11 | "topLevelPatterns": [], 12 | "lockfileEntries": {}, 13 | "files": [], 14 | "artifacts": {} 15 | } -------------------------------------------------------------------------------- /backend/poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "asgiref" 3 | version = "3.3.4" 4 | description = "ASGI specs, helper code, and adapters" 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.6" 8 | 9 | [package.dependencies] 10 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 11 | 12 | [package.extras] 13 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] 14 | 15 | [[package]] 16 | name = "attrs" 17 | version = "21.2.0" 18 | description = "Classes Without Boilerplate" 19 | category = "main" 20 | optional = false 21 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 22 | 23 | [package.extras] 24 | dev = ["coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "sphinx", "sphinx-notfound-page", "zope.interface"] 25 | docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] 26 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "zope.interface"] 27 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six"] 28 | 29 | [[package]] 30 | name = "automat" 31 | version = "20.2.0" 32 | description = "Self-service finite-state machines for the programmer on the go." 33 | category = "main" 34 | optional = false 35 | python-versions = "*" 36 | 37 | [package.dependencies] 38 | attrs = ">=19.2.0" 39 | six = "*" 40 | 41 | [package.extras] 42 | visualize = ["Twisted (>=16.1.1)", "graphviz (>0.5.1)"] 43 | 44 | [[package]] 45 | name = "certifi" 46 | version = "2021.5.30" 47 | description = "Python package for providing Mozilla's CA Bundle." 48 | category = "main" 49 | optional = false 50 | python-versions = "*" 51 | 52 | [[package]] 53 | name = "cffi" 54 | version = "1.14.5" 55 | description = "Foreign Function Interface for Python calling C code." 56 | category = "main" 57 | optional = false 58 | python-versions = "*" 59 | 60 | [package.dependencies] 61 | pycparser = "*" 62 | 63 | [[package]] 64 | name = "chardet" 65 | version = "4.0.0" 66 | description = "Universal encoding detector for Python 2 and 3" 67 | category = "main" 68 | optional = false 69 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 70 | 71 | [[package]] 72 | name = "constantly" 73 | version = "15.1.0" 74 | description = "Symbolic constants in Python" 75 | category = "main" 76 | optional = false 77 | python-versions = "*" 78 | 79 | [[package]] 80 | name = "cryptography" 81 | version = "37.0.4" 82 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 83 | category = "main" 84 | optional = false 85 | python-versions = ">=3.6" 86 | 87 | [package.dependencies] 88 | cffi = ">=1.12" 89 | 90 | [package.extras] 91 | docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] 92 | docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] 93 | pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] 94 | sdist = ["setuptools_rust (>=0.11.4)"] 95 | ssh = ["bcrypt (>=3.1.5)"] 96 | test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] 97 | 98 | [[package]] 99 | name = "defusedxml" 100 | version = "0.7.1" 101 | description = "XML bomb protection for Python stdlib modules" 102 | category = "main" 103 | optional = false 104 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 105 | 106 | [[package]] 107 | name = "dj-database-url" 108 | version = "0.5.0" 109 | description = "Use Database URLs in your Django Application." 110 | category = "main" 111 | optional = false 112 | python-versions = "*" 113 | 114 | [[package]] 115 | name = "django" 116 | version = "3.2.12" 117 | description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." 118 | category = "main" 119 | optional = false 120 | python-versions = ">=3.6" 121 | 122 | [package.dependencies] 123 | asgiref = ">=3.3.2,<4" 124 | pytz = "*" 125 | sqlparse = ">=0.2.2" 126 | 127 | [package.extras] 128 | argon2 = ["argon2-cffi (>=19.1.0)"] 129 | bcrypt = ["bcrypt"] 130 | 131 | [[package]] 132 | name = "django-cors-headers" 133 | version = "3.7.0" 134 | description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." 135 | category = "main" 136 | optional = false 137 | python-versions = ">=3.6" 138 | 139 | [package.dependencies] 140 | Django = ">=2.2" 141 | 142 | [[package]] 143 | name = "django-rest-framework" 144 | version = "0.1.0" 145 | description = "alias." 146 | category = "main" 147 | optional = false 148 | python-versions = "*" 149 | 150 | [package.dependencies] 151 | djangorestframework = "*" 152 | 153 | [[package]] 154 | name = "djangorestframework" 155 | version = "3.12.4" 156 | description = "Web APIs for Django, made easy." 157 | category = "main" 158 | optional = false 159 | python-versions = ">=3.5" 160 | 161 | [package.dependencies] 162 | django = ">=2.2" 163 | 164 | [[package]] 165 | name = "djangorestframework-dataclasses" 166 | version = "0.9" 167 | description = "A dataclasses serializer for Django REST Framework" 168 | category = "main" 169 | optional = false 170 | python-versions = ">=3.7" 171 | 172 | [package.dependencies] 173 | django = ">=2.0" 174 | djangorestframework = ">=3.9" 175 | typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} 176 | 177 | [[package]] 178 | name = "flake8" 179 | version = "3.9.2" 180 | description = "the modular source code checker: pep8 pyflakes and co" 181 | category = "dev" 182 | optional = false 183 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 184 | 185 | [package.dependencies] 186 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 187 | mccabe = ">=0.6.0,<0.7.0" 188 | pycodestyle = ">=2.7.0,<2.8.0" 189 | pyflakes = ">=2.3.0,<2.4.0" 190 | 191 | [[package]] 192 | name = "fsspec" 193 | version = "2021.7.0" 194 | description = "File-system specification" 195 | category = "main" 196 | optional = false 197 | python-versions = ">=3.6" 198 | 199 | [package.extras] 200 | abfs = ["adlfs"] 201 | adl = ["adlfs"] 202 | dask = ["dask", "distributed"] 203 | dropbox = ["dropbox", "dropboxdrivefs", "requests"] 204 | entrypoints = ["importlib-metadata"] 205 | gcs = ["gcsfs"] 206 | git = ["pygit2"] 207 | github = ["requests"] 208 | gs = ["gcsfs"] 209 | hdfs = ["pyarrow (>=1)"] 210 | http = ["aiohttp", "requests"] 211 | s3 = ["s3fs"] 212 | sftp = ["paramiko"] 213 | smb = ["smbprotocol"] 214 | ssh = ["paramiko"] 215 | 216 | [[package]] 217 | name = "gql" 218 | version = "2.0.0" 219 | description = "GraphQL client for Python" 220 | category = "main" 221 | optional = false 222 | python-versions = "*" 223 | 224 | [package.dependencies] 225 | graphql-core = ">=2.3.2,<3" 226 | promise = ">=2.3,<3" 227 | requests = ">=2.12,<3" 228 | six = ">=1.10.0" 229 | 230 | [package.extras] 231 | dev = ["black (==19.10b0)", "check-manifest (>=0.42,<1)", "coveralls (==2.0.0)", "flake8 (==3.8.1)", "isort (==4.3.21)", "mock (==4.0.2)", "mypy (==0.770)", "pytest (==5.4.2)", "pytest-asyncio (==0.11.0)", "pytest-cov (==2.8.1)", "vcrpy (==4.0.2)"] 232 | test = ["coveralls (==2.0.0)", "mock (==4.0.2)", "pytest (==5.4.2)", "pytest-asyncio (==0.11.0)", "pytest-cov (==2.8.1)", "vcrpy (==4.0.2)"] 233 | 234 | [[package]] 235 | name = "graphql-core" 236 | version = "2.3.2" 237 | description = "GraphQL implementation for Python" 238 | category = "main" 239 | optional = false 240 | python-versions = "*" 241 | 242 | [package.dependencies] 243 | promise = ">=2.3,<3" 244 | rx = ">=1.6,<2" 245 | six = ">=1.10.0" 246 | 247 | [package.extras] 248 | gevent = ["gevent (>=1.1)"] 249 | test = ["coveralls (==1.11.1)", "cython (==0.29.17)", "gevent (==1.5.0)", "pyannotate (==1.2.0)", "pytest (==4.6.10)", "pytest-benchmark (==3.2.3)", "pytest-cov (==2.8.1)", "pytest-django (==3.9.0)", "pytest-mock (==2.0.0)", "six (==1.14.0)"] 250 | 251 | [[package]] 252 | name = "gunicorn" 253 | version = "20.1.0" 254 | description = "WSGI HTTP Server for UNIX" 255 | category = "main" 256 | optional = false 257 | python-versions = ">=3.5" 258 | 259 | [package.extras] 260 | eventlet = ["eventlet (>=0.24.1)"] 261 | gevent = ["gevent (>=1.4.0)"] 262 | setproctitle = ["setproctitle"] 263 | tornado = ["tornado (>=0.2)"] 264 | 265 | [[package]] 266 | name = "hyperlink" 267 | version = "21.0.0" 268 | description = "A featureful, immutable, and correct URL for Python." 269 | category = "main" 270 | optional = false 271 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 272 | 273 | [package.dependencies] 274 | idna = ">=2.5" 275 | 276 | [[package]] 277 | name = "idna" 278 | version = "2.10" 279 | description = "Internationalized Domain Names in Applications (IDNA)" 280 | category = "main" 281 | optional = false 282 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 283 | 284 | [[package]] 285 | name = "importlib-metadata" 286 | version = "4.4.0" 287 | description = "Read metadata from Python packages" 288 | category = "dev" 289 | optional = false 290 | python-versions = ">=3.6" 291 | 292 | [package.dependencies] 293 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 294 | zipp = ">=0.5" 295 | 296 | [package.extras] 297 | docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] 298 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] 299 | 300 | [[package]] 301 | name = "incremental" 302 | version = "21.3.0" 303 | description = "A small library that versions your Python projects." 304 | category = "main" 305 | optional = false 306 | python-versions = "*" 307 | 308 | [package.extras] 309 | scripts = ["click (>=6.0)", "twisted (>=16.4.0)"] 310 | 311 | [[package]] 312 | name = "mccabe" 313 | version = "0.6.1" 314 | description = "McCabe checker, plugin for flake8" 315 | category = "dev" 316 | optional = false 317 | python-versions = "*" 318 | 319 | [[package]] 320 | name = "mysqlclient" 321 | version = "2.0.3" 322 | description = "Python interface to MySQL" 323 | category = "main" 324 | optional = false 325 | python-versions = ">=3.5" 326 | 327 | [[package]] 328 | name = "oauthlib" 329 | version = "3.1.1" 330 | description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" 331 | category = "main" 332 | optional = false 333 | python-versions = ">=3.6" 334 | 335 | [package.extras] 336 | rsa = ["cryptography (>=3.0.0,<4)"] 337 | signals = ["blinker (>=1.4.0)"] 338 | signedtoken = ["cryptography (>=3.0.0,<4)", "pyjwt (>=2.0.0,<3)"] 339 | 340 | [[package]] 341 | name = "promise" 342 | version = "2.3" 343 | description = "Promises/A+ implementation for Python" 344 | category = "main" 345 | optional = false 346 | python-versions = "*" 347 | 348 | [package.dependencies] 349 | six = "*" 350 | 351 | [package.extras] 352 | test = ["coveralls", "futures", "mock", "pytest (>=2.7.3)", "pytest-benchmark", "pytest-cov"] 353 | 354 | [[package]] 355 | name = "pycodestyle" 356 | version = "2.7.0" 357 | description = "Python style guide checker" 358 | category = "dev" 359 | optional = false 360 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 361 | 362 | [[package]] 363 | name = "pycparser" 364 | version = "2.20" 365 | description = "C parser in Python" 366 | category = "main" 367 | optional = false 368 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 369 | 370 | [[package]] 371 | name = "pyflakes" 372 | version = "2.3.1" 373 | description = "passive checker of Python programs" 374 | category = "dev" 375 | optional = false 376 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 377 | 378 | [[package]] 379 | name = "pyga" 380 | version = "2.6.2" 381 | description = "Server side implementation of Google Analytics in Python." 382 | category = "main" 383 | optional = false 384 | python-versions = "*" 385 | 386 | [package.dependencies] 387 | six = "*" 388 | 389 | [[package]] 390 | name = "pyhamcrest" 391 | version = "2.0.2" 392 | description = "Hamcrest framework for matcher objects" 393 | category = "main" 394 | optional = false 395 | python-versions = ">=3.5" 396 | 397 | [[package]] 398 | name = "pyjwt" 399 | version = "2.1.0" 400 | description = "JSON Web Token implementation in Python" 401 | category = "main" 402 | optional = false 403 | python-versions = ">=3.6" 404 | 405 | [package.extras] 406 | crypto = ["cryptography (>=3.3.1,<4.0.0)"] 407 | dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.3.1,<4.0.0)", "mypy", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] 408 | docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] 409 | tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] 410 | 411 | [[package]] 412 | name = "python-dotenv" 413 | version = "0.17.1" 414 | description = "Read key-value pairs from a .env file and set them as environment variables" 415 | category = "main" 416 | optional = false 417 | python-versions = "*" 418 | 419 | [package.extras] 420 | cli = ["click (>=5.0)"] 421 | 422 | [[package]] 423 | name = "python3-openid" 424 | version = "3.2.0" 425 | description = "OpenID support for modern servers and consumers." 426 | category = "main" 427 | optional = false 428 | python-versions = "*" 429 | 430 | [package.dependencies] 431 | defusedxml = "*" 432 | 433 | [package.extras] 434 | mysql = ["mysql-connector-python"] 435 | postgresql = ["psycopg2"] 436 | 437 | [[package]] 438 | name = "pytz" 439 | version = "2021.1" 440 | description = "World timezone definitions, modern and historical" 441 | category = "main" 442 | optional = false 443 | python-versions = "*" 444 | 445 | [[package]] 446 | name = "requests" 447 | version = "2.25.1" 448 | description = "Python HTTP for Humans." 449 | category = "main" 450 | optional = false 451 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 452 | 453 | [package.dependencies] 454 | certifi = ">=2017.4.17" 455 | chardet = ">=3.0.2,<5" 456 | idna = ">=2.5,<3" 457 | urllib3 = ">=1.21.1,<1.27" 458 | 459 | [package.extras] 460 | security = ["cryptography (>=1.3.4)", "pyOpenSSL (>=0.14)"] 461 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 462 | 463 | [[package]] 464 | name = "requests-oauthlib" 465 | version = "1.3.0" 466 | description = "OAuthlib authentication support for Requests." 467 | category = "main" 468 | optional = false 469 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 470 | 471 | [package.dependencies] 472 | oauthlib = ">=3.0.0" 473 | requests = ">=2.0.0" 474 | 475 | [package.extras] 476 | rsa = ["oauthlib[signedtoken] (>=3.0.0)"] 477 | 478 | [[package]] 479 | name = "rx" 480 | version = "1.6.1" 481 | description = "Reactive Extensions (Rx) for Python" 482 | category = "main" 483 | optional = false 484 | python-versions = "*" 485 | 486 | [[package]] 487 | name = "semver" 488 | version = "2.13.0" 489 | description = "Python helper for Semantic Versioning (http://semver.org/)" 490 | category = "main" 491 | optional = false 492 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 493 | 494 | [[package]] 495 | name = "six" 496 | version = "1.16.0" 497 | description = "Python 2 and 3 compatibility utilities" 498 | category = "main" 499 | optional = false 500 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 501 | 502 | [[package]] 503 | name = "social-auth-app-django" 504 | version = "4.0.0" 505 | description = "Python Social Authentication, Django integration." 506 | category = "main" 507 | optional = false 508 | python-versions = "*" 509 | 510 | [package.dependencies] 511 | six = "*" 512 | social-auth-core = ">=3.3.0" 513 | 514 | [[package]] 515 | name = "social-auth-core" 516 | version = "4.1.0" 517 | description = "Python social authentication made simple." 518 | category = "main" 519 | optional = false 520 | python-versions = ">=3.6" 521 | 522 | [package.dependencies] 523 | cryptography = ">=1.4" 524 | defusedxml = ">=0.5.0rc1" 525 | oauthlib = ">=1.0.3" 526 | PyJWT = ">=2.0.0" 527 | python3-openid = ">=3.0.10" 528 | requests = ">=2.9.1" 529 | requests-oauthlib = ">=0.6.1" 530 | 531 | [package.extras] 532 | all = ["cryptography (>=2.1.1)", "python-jose (>=3.0.0)", "python3-saml (>=1.2.1)"] 533 | allpy3 = ["cryptography (>=2.1.1)", "python-jose (>=3.0.0)", "python3-saml (>=1.2.1)"] 534 | azuread = ["cryptography (>=2.1.1)"] 535 | openidconnect = ["python-jose (>=3.0.0)"] 536 | saml = ["python3-saml (>=1.2.1)"] 537 | 538 | [[package]] 539 | name = "sqlparse" 540 | version = "0.4.2" 541 | description = "A non-validating SQL parser." 542 | category = "main" 543 | optional = false 544 | python-versions = ">=3.5" 545 | 546 | [[package]] 547 | name = "twisted" 548 | version = "20.3.0" 549 | description = "An asynchronous networking framework written in Python" 550 | category = "main" 551 | optional = false 552 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" 553 | 554 | [package.dependencies] 555 | attrs = ">=19.2.0" 556 | Automat = ">=0.3.0" 557 | constantly = ">=15.1" 558 | hyperlink = ">=17.1.1" 559 | incremental = ">=16.10.1" 560 | PyHamcrest = ">=1.9.0,<1.10.0 || >1.10.0" 561 | "zope.interface" = ">=4.4.2" 562 | 563 | [package.extras] 564 | all_non_platform = ["appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "cryptography (>=2.5)", "h2 (>=3.0,<4.0)", "idna (>=0.6,!=2.3)", "priority (>=1.1.0,<2.0)", "pyasn1", "pyopenssl (>=16.0.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "service_identity (>=18.1.0)", "soappy"] 565 | conch = ["appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "cryptography (>=2.5)", "pyasn1"] 566 | dev = ["pyflakes (>=1.0.0)", "python-subunit", "sphinx (>=1.3.1)", "towncrier (>=17.4.0)", "twisted-dev-tools (>=0.0.2)"] 567 | http2 = ["h2 (>=3.0,<4.0)", "priority (>=1.1.0,<2.0)"] 568 | macos_platform = ["appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "cryptography (>=2.5)", "h2 (>=3.0,<4.0)", "idna (>=0.6,!=2.3)", "priority (>=1.1.0,<2.0)", "pyasn1", "pyobjc-core", "pyobjc-framework-cfnetwork", "pyobjc-framework-cocoa", "pyopenssl (>=16.0.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "service_identity (>=18.1.0)", "soappy"] 569 | osx_platform = ["appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "cryptography (>=2.5)", "h2 (>=3.0,<4.0)", "idna (>=0.6,!=2.3)", "priority (>=1.1.0,<2.0)", "pyasn1", "pyobjc-core", "pyobjc-framework-cfnetwork", "pyobjc-framework-cocoa", "pyopenssl (>=16.0.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "service_identity (>=18.1.0)", "soappy"] 570 | serial = ["pyserial (>=3.0)", "pywin32 (!=226)"] 571 | soap = ["soappy"] 572 | tls = ["idna (>=0.6,!=2.3)", "pyopenssl (>=16.0.0)", "service_identity (>=18.1.0)"] 573 | windows_platform = ["appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "cryptography (>=2.5)", "h2 (>=3.0,<4.0)", "idna (>=0.6,!=2.3)", "priority (>=1.1.0,<2.0)", "pyasn1", "pyopenssl (>=16.0.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "pywin32 (!=226)", "service_identity (>=18.1.0)", "soappy"] 574 | 575 | [[package]] 576 | name = "typing-extensions" 577 | version = "3.10.0.0" 578 | description = "Backported and Experimental Type Hints for Python 3.5+" 579 | category = "main" 580 | optional = false 581 | python-versions = "*" 582 | 583 | [[package]] 584 | name = "urllib3" 585 | version = "1.26.6" 586 | description = "HTTP library with thread-safe connection pooling, file post, and more." 587 | category = "main" 588 | optional = false 589 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 590 | 591 | [package.extras] 592 | brotli = ["brotlipy (>=0.6.0)"] 593 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"] 594 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 595 | 596 | [[package]] 597 | name = "websockets" 598 | version = "10.4" 599 | description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" 600 | category = "main" 601 | optional = false 602 | python-versions = ">=3.7" 603 | 604 | [[package]] 605 | name = "whitenoise" 606 | version = "5.3.0" 607 | description = "Radically simplified static file serving for WSGI applications" 608 | category = "main" 609 | optional = false 610 | python-versions = ">=3.5, <4" 611 | 612 | [package.extras] 613 | brotli = ["brotli"] 614 | 615 | [[package]] 616 | name = "zipp" 617 | version = "3.4.1" 618 | description = "Backport of pathlib-compatible object wrapper for zip files" 619 | category = "dev" 620 | optional = false 621 | python-versions = ">=3.6" 622 | 623 | [package.extras] 624 | docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] 625 | testing = ["func-timeout", "jaraco.itertools", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-enabler", "pytest-flake8", "pytest-mypy"] 626 | 627 | [[package]] 628 | name = "zope.interface" 629 | version = "5.4.0" 630 | description = "Interfaces for Python" 631 | category = "main" 632 | optional = false 633 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 634 | 635 | [package.extras] 636 | docs = ["repoze.sphinx.autointerface", "sphinx"] 637 | test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] 638 | testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] 639 | 640 | [metadata] 641 | lock-version = "1.1" 642 | python-versions = "^3.7" 643 | content-hash = "fffcdae72fa675e5c4b512e4613b42dcc3144efbddeef5cc8447af102abef9e1" 644 | 645 | [metadata.files] 646 | asgiref = [ 647 | {file = "asgiref-3.3.4-py3-none-any.whl", hash = "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee"}, 648 | {file = "asgiref-3.3.4.tar.gz", hash = "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"}, 649 | ] 650 | attrs = [ 651 | {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, 652 | {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, 653 | ] 654 | automat = [ 655 | {file = "Automat-20.2.0-py2.py3-none-any.whl", hash = "sha256:b6feb6455337df834f6c9962d6ccf771515b7d939bca142b29c20c2376bc6111"}, 656 | {file = "Automat-20.2.0.tar.gz", hash = "sha256:7979803c74610e11ef0c0d68a2942b152df52da55336e0c9d58daf1831cbdf33"}, 657 | ] 658 | certifi = [ 659 | {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, 660 | {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, 661 | ] 662 | cffi = [ 663 | {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"}, 664 | {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"}, 665 | {file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"}, 666 | {file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"}, 667 | {file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"}, 668 | {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"}, 669 | {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"}, 670 | {file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"}, 671 | {file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"}, 672 | {file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"}, 673 | {file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"}, 674 | {file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"}, 675 | {file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"}, 676 | {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"}, 677 | {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"}, 678 | {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"}, 679 | {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f"}, 680 | {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed"}, 681 | {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55"}, 682 | {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"}, 683 | {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"}, 684 | {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"}, 685 | {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"}, 686 | {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"}, 687 | {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"}, 688 | {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69"}, 689 | {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05"}, 690 | {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc"}, 691 | {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"}, 692 | {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"}, 693 | {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"}, 694 | {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"}, 695 | {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"}, 696 | {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"}, 697 | {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373"}, 698 | {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f"}, 699 | {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76"}, 700 | {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"}, 701 | {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"}, 702 | {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"}, 703 | {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"}, 704 | {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"}, 705 | {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"}, 706 | {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0"}, 707 | {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333"}, 708 | {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7"}, 709 | {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"}, 710 | {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"}, 711 | {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, 712 | ] 713 | chardet = [ 714 | {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, 715 | {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, 716 | ] 717 | constantly = [ 718 | {file = "constantly-15.1.0-py2.py3-none-any.whl", hash = "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d"}, 719 | {file = "constantly-15.1.0.tar.gz", hash = "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35"}, 720 | ] 721 | cryptography = [ 722 | {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"}, 723 | {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"}, 724 | {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"}, 725 | {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"}, 726 | {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"}, 727 | {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"}, 728 | {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"}, 729 | {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"}, 730 | {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"}, 731 | {file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"}, 732 | {file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"}, 733 | {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"}, 734 | {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"}, 735 | {file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"}, 736 | {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"}, 737 | {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"}, 738 | {file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"}, 739 | {file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"}, 740 | {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"}, 741 | {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"}, 742 | {file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"}, 743 | {file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"}, 744 | ] 745 | defusedxml = [ 746 | {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, 747 | {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, 748 | ] 749 | dj-database-url = [ 750 | {file = "dj-database-url-0.5.0.tar.gz", hash = "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163"}, 751 | {file = "dj_database_url-0.5.0-py2.py3-none-any.whl", hash = "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9"}, 752 | ] 753 | django = [ 754 | {file = "Django-3.2.12-py3-none-any.whl", hash = "sha256:9b06c289f9ba3a8abea16c9c9505f25107809fb933676f6c891ded270039d965"}, 755 | {file = "Django-3.2.12.tar.gz", hash = "sha256:9772e6935703e59e993960832d66a614cf0233a1c5123bc6224ecc6ad69e41e2"}, 756 | ] 757 | django-cors-headers = [ 758 | {file = "django-cors-headers-3.7.0.tar.gz", hash = "sha256:96069c4aaacace786a34ee7894ff680780ec2644e4268b31181044410fecd12e"}, 759 | {file = "django_cors_headers-3.7.0-py3-none-any.whl", hash = "sha256:1ac2b1213de75a251e2ba04448da15f99bcfcbe164288ae6b5ff929dc49b372f"}, 760 | ] 761 | django-rest-framework = [ 762 | {file = "django-rest-framework-0.1.0.tar.gz", hash = "sha256:47a8f496fa69e3b6bd79f68dd7a1527d907d6b77f009e9db7cf9bb21cc565e4a"}, 763 | ] 764 | djangorestframework = [ 765 | {file = "djangorestframework-3.12.4-py3-none-any.whl", hash = "sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf"}, 766 | {file = "djangorestframework-3.12.4.tar.gz", hash = "sha256:f747949a8ddac876e879190df194b925c177cdeb725a099db1460872f7c0a7f2"}, 767 | ] 768 | djangorestframework-dataclasses = [ 769 | {file = "djangorestframework-dataclasses-0.9.tar.gz", hash = "sha256:ad04e820ee3ce1ac44e38347a24f53c62b642f2a08d910e61c289d9d1a7a8b69"}, 770 | {file = "djangorestframework_dataclasses-0.9-py3-none-any.whl", hash = "sha256:2b05e08d3e9c52268d50a1b82ccafa52baac6b7df8ba08ce82b32e661c5a9434"}, 771 | ] 772 | flake8 = [ 773 | {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, 774 | {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, 775 | ] 776 | fsspec = [ 777 | {file = "fsspec-2021.7.0-py3-none-any.whl", hash = "sha256:86822ccf367da99957f49db64f7d5fd3d8d21444fac4dfdc8ebc38ee93d478c6"}, 778 | {file = "fsspec-2021.7.0.tar.gz", hash = "sha256:792ebd3b54de0b30f1ce73f0ba0a8bcc864724f2d9f248cb8d0ece47db0cbde8"}, 779 | ] 780 | gql = [ 781 | {file = "gql-2.0.0-py2.py3-none-any.whl", hash = "sha256:35032ddd4bfe6b8f3169f806b022168932385d751eacc5c5f7122e0b3f4d6b88"}, 782 | {file = "gql-2.0.0.tar.gz", hash = "sha256:fe8d3a08047f77362ddfcfddba7cae377da2dd66f5e61c59820419c9283d4fb5"}, 783 | ] 784 | graphql-core = [ 785 | {file = "graphql-core-2.3.2.tar.gz", hash = "sha256:aac46a9ac524c9855910c14c48fc5d60474def7f99fd10245e76608eba7af746"}, 786 | {file = "graphql_core-2.3.2-py2.py3-none-any.whl", hash = "sha256:44c9bac4514e5e30c5a595fac8e3c76c1975cae14db215e8174c7fe995825bad"}, 787 | ] 788 | gunicorn = [ 789 | {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, 790 | {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, 791 | ] 792 | hyperlink = [ 793 | {file = "hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4"}, 794 | {file = "hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b"}, 795 | ] 796 | idna = [ 797 | {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, 798 | {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, 799 | ] 800 | importlib-metadata = [ 801 | {file = "importlib_metadata-4.4.0-py3-none-any.whl", hash = "sha256:960d52ba7c21377c990412aca380bf3642d734c2eaab78a2c39319f67c6a5786"}, 802 | {file = "importlib_metadata-4.4.0.tar.gz", hash = "sha256:e592faad8de1bda9fe920cf41e15261e7131bcf266c30306eec00e8e225c1dd5"}, 803 | ] 804 | incremental = [ 805 | {file = "incremental-21.3.0-py2.py3-none-any.whl", hash = "sha256:92014aebc6a20b78a8084cdd5645eeaa7f74b8933f70fa3ada2cfbd1e3b54321"}, 806 | {file = "incremental-21.3.0.tar.gz", hash = "sha256:02f5de5aff48f6b9f665d99d48bfc7ec03b6e3943210de7cfc88856d755d6f57"}, 807 | ] 808 | mccabe = [ 809 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 810 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 811 | ] 812 | mysqlclient = [ 813 | {file = "mysqlclient-2.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:3381ca1a4f37ff1155fcfde20836b46416d66531add8843f6aa6d968982731c3"}, 814 | {file = "mysqlclient-2.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0ac0dd759c4ca02c35a9fedc24bc982cf75171651e8187c2495ec957a87dfff7"}, 815 | {file = "mysqlclient-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:71c4b330cf2313bbda0307fc858cc9055e64493ba9bf28454d25cf8b3ee8d7f5"}, 816 | {file = "mysqlclient-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:fc575093cf81b6605bed84653e48b277318b880dc9becf42dd47fa11ffd3e2b6"}, 817 | {file = "mysqlclient-2.0.3.tar.gz", hash = "sha256:f6ebea7c008f155baeefe16c56cd3ee6239f7a5a9ae42396c2f1860f08a7c432"}, 818 | ] 819 | oauthlib = [ 820 | {file = "oauthlib-3.1.1-py2.py3-none-any.whl", hash = "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc"}, 821 | {file = "oauthlib-3.1.1.tar.gz", hash = "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"}, 822 | ] 823 | promise = [ 824 | {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"}, 825 | ] 826 | pycodestyle = [ 827 | {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, 828 | {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, 829 | ] 830 | pycparser = [ 831 | {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, 832 | {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, 833 | ] 834 | pyflakes = [ 835 | {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, 836 | {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, 837 | ] 838 | pyga = [ 839 | {file = "pyga-2.6.2-py3-none-any.whl", hash = "sha256:062e0468915130cde882d52e4ca5bc0a01d21d66b5f23b6e1fec045dcefc5942"}, 840 | {file = "pyga-2.6.2.tar.gz", hash = "sha256:09da0e36bc4d44a82ab3dbc6128300b14715b902d98311f0866162de45d2fddc"}, 841 | ] 842 | pyhamcrest = [ 843 | {file = "PyHamcrest-2.0.2-py3-none-any.whl", hash = "sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29"}, 844 | {file = "PyHamcrest-2.0.2.tar.gz", hash = "sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316"}, 845 | ] 846 | pyjwt = [ 847 | {file = "PyJWT-2.1.0-py3-none-any.whl", hash = "sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1"}, 848 | {file = "PyJWT-2.1.0.tar.gz", hash = "sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130"}, 849 | ] 850 | python-dotenv = [ 851 | {file = "python-dotenv-0.17.1.tar.gz", hash = "sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f"}, 852 | {file = "python_dotenv-0.17.1-py2.py3-none-any.whl", hash = "sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544"}, 853 | ] 854 | python3-openid = [ 855 | {file = "python3-openid-3.2.0.tar.gz", hash = "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf"}, 856 | {file = "python3_openid-3.2.0-py3-none-any.whl", hash = "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"}, 857 | ] 858 | pytz = [ 859 | {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, 860 | {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, 861 | ] 862 | requests = [ 863 | {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, 864 | {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, 865 | ] 866 | requests-oauthlib = [ 867 | {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"}, 868 | {file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"}, 869 | {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"}, 870 | ] 871 | rx = [ 872 | {file = "Rx-1.6.1-py2.py3-none-any.whl", hash = "sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105"}, 873 | {file = "Rx-1.6.1.tar.gz", hash = "sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23"}, 874 | ] 875 | semver = [ 876 | {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, 877 | {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, 878 | ] 879 | six = [ 880 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 881 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 882 | ] 883 | social-auth-app-django = [ 884 | {file = "social-auth-app-django-4.0.0.tar.gz", hash = "sha256:2c69e57df0b30c9c1823519c5f1992cbe4f3f98fdc7d95c840e091a752708840"}, 885 | {file = "social_auth_app_django-4.0.0-py2-none-any.whl", hash = "sha256:df5212370bd250108987c4748419a1a1d0cec750878856c2644c36aaa0fd3e58"}, 886 | {file = "social_auth_app_django-4.0.0-py3-none-any.whl", hash = "sha256:567ad0e028311541d7dfed51d3bf2c60440a6fd236d5d4d06c5a618b3d6c57c5"}, 887 | ] 888 | social-auth-core = [ 889 | {file = "social-auth-core-4.1.0.tar.gz", hash = "sha256:5ab43b3b15dce5f059db69cc3082c216574739f0edbc98629c8c6e8769c67eb4"}, 890 | {file = "social_auth_core-4.1.0-py3-none-any.whl", hash = "sha256:983b53167ac56e7ba4909db555602a6e7a98c97ca47183bb222eb85ba627bf2b"}, 891 | ] 892 | sqlparse = [ 893 | {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"}, 894 | {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, 895 | ] 896 | twisted = [ 897 | {file = "Twisted-20.3.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:cdbc4c7f0cd7a2218b575844e970f05a1be1861c607b0e048c9bceca0c4d42f7"}, 898 | {file = "Twisted-20.3.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:d267125cc0f1e8a0eed6319ba4ac7477da9b78a535601c49ecd20c875576433a"}, 899 | {file = "Twisted-20.3.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:356e8d8dd3590e790e3dba4db139eb8a17aca64b46629c622e1b1597a4a92478"}, 900 | {file = "Twisted-20.3.0-cp27-cp27m-win32.whl", hash = "sha256:ca3a0b8c9110800e576d89b5337373e52018b41069bc879f12fa42b7eb2d0274"}, 901 | {file = "Twisted-20.3.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cd1dc5c85b58494138a3917752b54bb1daa0045d234b7c132c37a61d5483ebad"}, 902 | {file = "Twisted-20.3.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:94ac3d55a58c90e2075c5fe1853f2aa3892b73e3bf56395f743aefde8605eeaa"}, 903 | {file = "Twisted-20.3.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7408c6635ee1b96587289283ebe90ee15dbf9614b05857b446055116bc822d29"}, 904 | {file = "Twisted-20.3.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c09c47ff9750a8e3aa60ad169c4b95006d455a29b80ad0901f031a103b2991cd"}, 905 | {file = "Twisted-20.3.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:158ddb80719a4813d292293ac44ba41d8b56555ed009d90994a278237ee63d2c"}, 906 | {file = "Twisted-20.3.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:040eb6641125d2a9a09cf198ec7b83dd8858c6f51f6770325ed9959c00f5098f"}, 907 | {file = "Twisted-20.3.0-cp35-cp35m-win32.whl", hash = "sha256:147780b8caf21ba2aef3688628eaf13d7e7fe02a86747cd54bfaf2140538f042"}, 908 | {file = "Twisted-20.3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:25ffcf37944bdad4a99981bc74006d735a678d2b5c193781254fbbb6d69e3b22"}, 909 | {file = "Twisted-20.3.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:a58e61a2a01e5bcbe3b575c0099a2bcb8d70a75b1a087338e0c48dd6e01a5f15"}, 910 | {file = "Twisted-20.3.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:7c547fd0215db9da8a1bc23182b309e84a232364cc26d829e9ee196ce840b114"}, 911 | {file = "Twisted-20.3.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2182000d6ffc05d269e6c03bfcec8b57e20259ca1086180edaedec3f1e689292"}, 912 | {file = "Twisted-20.3.0-cp36-cp36m-win32.whl", hash = "sha256:70952c56e4965b9f53b180daecf20a9595cf22b8d0935cd3bd664c90273c3ab2"}, 913 | {file = "Twisted-20.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3281d9ce889f7b21bdb73658e887141aa45a102baf3b2320eafcfba954fcefec"}, 914 | {file = "Twisted-20.3.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:e92703bed0cc21d6cb5c61d66922b3b1564015ca8a51325bd164a5e33798d504"}, 915 | {file = "Twisted-20.3.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f058bd0168271de4dcdc39845b52dd0a4a2fecf5f1246335f13f5e96eaebb467"}, 916 | {file = "Twisted-20.3.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:894f6f3cfa57a15ea0d0714e4283913a5f2511dbd18653dd148eba53b3919797"}, 917 | {file = "Twisted-20.3.0-cp37-cp37m-win32.whl", hash = "sha256:f3c19e5bd42bbe4bf345704ad7c326c74d3fd7a1b3844987853bef180be638d4"}, 918 | {file = "Twisted-20.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d95803193561a243cb0401b0567c6b7987d3f2a67046770e1dccd1c9e49a9780"}, 919 | {file = "Twisted-20.3.0.tar.bz2", hash = "sha256:d72c55b5d56e176563b91d11952d13b01af8725c623e498db5507b6614fc1e10"}, 920 | ] 921 | typing-extensions = [ 922 | {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, 923 | {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, 924 | {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, 925 | ] 926 | urllib3 = [ 927 | {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, 928 | {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, 929 | ] 930 | websockets = [ 931 | {file = "websockets-10.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d58804e996d7d2307173d56c297cf7bc132c52df27a3efaac5e8d43e36c21c48"}, 932 | {file = "websockets-10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc0b82d728fe21a0d03e65f81980abbbcb13b5387f733a1a870672c5be26edab"}, 933 | {file = "websockets-10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba089c499e1f4155d2a3c2a05d2878a3428cf321c848f2b5a45ce55f0d7d310c"}, 934 | {file = "websockets-10.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33d69ca7612f0ddff3316b0c7b33ca180d464ecac2d115805c044bf0a3b0d032"}, 935 | {file = "websockets-10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62e627f6b6d4aed919a2052efc408da7a545c606268d5ab5bfab4432734b82b4"}, 936 | {file = "websockets-10.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ea7b82bfcae927eeffc55d2ffa31665dc7fec7b8dc654506b8e5a518eb4d50"}, 937 | {file = "websockets-10.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e0cb5cc6ece6ffa75baccfd5c02cffe776f3f5c8bf486811f9d3ea3453676ce8"}, 938 | {file = "websockets-10.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae5e95cfb53ab1da62185e23b3130e11d64431179debac6dc3c6acf08760e9b1"}, 939 | {file = "websockets-10.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7c584f366f46ba667cfa66020344886cf47088e79c9b9d39c84ce9ea98aaa331"}, 940 | {file = "websockets-10.4-cp310-cp310-win32.whl", hash = "sha256:b029fb2032ae4724d8ae8d4f6b363f2cc39e4c7b12454df8df7f0f563ed3e61a"}, 941 | {file = "websockets-10.4-cp310-cp310-win_amd64.whl", hash = "sha256:8dc96f64ae43dde92530775e9cb169979f414dcf5cff670455d81a6823b42089"}, 942 | {file = "websockets-10.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47a2964021f2110116cc1125b3e6d87ab5ad16dea161949e7244ec583b905bb4"}, 943 | {file = "websockets-10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e789376b52c295c4946403bd0efecf27ab98f05319df4583d3c48e43c7342c2f"}, 944 | {file = "websockets-10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d3f0b61c45c3fa9a349cf484962c559a8a1d80dae6977276df8fd1fa5e3cb8c"}, 945 | {file = "websockets-10.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f55b5905705725af31ccef50e55391621532cd64fbf0bc6f4bac935f0fccec46"}, 946 | {file = "websockets-10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00c870522cdb69cd625b93f002961ffb0c095394f06ba8c48f17eef7c1541f96"}, 947 | {file = "websockets-10.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f38706e0b15d3c20ef6259fd4bc1700cd133b06c3c1bb108ffe3f8947be15fa"}, 948 | {file = "websockets-10.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f2c38d588887a609191d30e902df2a32711f708abfd85d318ca9b367258cfd0c"}, 949 | {file = "websockets-10.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fe10ddc59b304cb19a1bdf5bd0a7719cbbc9fbdd57ac80ed436b709fcf889106"}, 950 | {file = "websockets-10.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:90fcf8929836d4a0e964d799a58823547df5a5e9afa83081761630553be731f9"}, 951 | {file = "websockets-10.4-cp311-cp311-win32.whl", hash = "sha256:b9968694c5f467bf67ef97ae7ad4d56d14be2751000c1207d31bf3bb8860bae8"}, 952 | {file = "websockets-10.4-cp311-cp311-win_amd64.whl", hash = "sha256:a7a240d7a74bf8d5cb3bfe6be7f21697a28ec4b1a437607bae08ac7acf5b4882"}, 953 | {file = "websockets-10.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:74de2b894b47f1d21cbd0b37a5e2b2392ad95d17ae983e64727e18eb281fe7cb"}, 954 | {file = "websockets-10.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3a686ecb4aa0d64ae60c9c9f1a7d5d46cab9bfb5d91a2d303d00e2cd4c4c5cc"}, 955 | {file = "websockets-10.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d15c968ea7a65211e084f523151dbf8ae44634de03c801b8bd070b74e85033"}, 956 | {file = "websockets-10.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00213676a2e46b6ebf6045bc11d0f529d9120baa6f58d122b4021ad92adabd41"}, 957 | {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e23173580d740bf8822fd0379e4bf30aa1d5a92a4f252d34e893070c081050df"}, 958 | {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:dd500e0a5e11969cdd3320935ca2ff1e936f2358f9c2e61f100a1660933320ea"}, 959 | {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4239b6027e3d66a89446908ff3027d2737afc1a375f8fd3eea630a4842ec9a0c"}, 960 | {file = "websockets-10.4-cp37-cp37m-win32.whl", hash = "sha256:8a5cc00546e0a701da4639aa0bbcb0ae2bb678c87f46da01ac2d789e1f2d2038"}, 961 | {file = "websockets-10.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a9f9a735deaf9a0cadc2d8c50d1a5bcdbae8b6e539c6e08237bc4082d7c13f28"}, 962 | {file = "websockets-10.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c1289596042fad2cdceb05e1ebf7aadf9995c928e0da2b7a4e99494953b1b94"}, 963 | {file = "websockets-10.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0cff816f51fb33c26d6e2b16b5c7d48eaa31dae5488ace6aae468b361f422b63"}, 964 | {file = "websockets-10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dd9becd5fe29773d140d68d607d66a38f60e31b86df75332703757ee645b6faf"}, 965 | {file = "websockets-10.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45ec8e75b7dbc9539cbfafa570742fe4f676eb8b0d3694b67dabe2f2ceed8aa6"}, 966 | {file = "websockets-10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f72e5cd0f18f262f5da20efa9e241699e0cf3a766317a17392550c9ad7b37d8"}, 967 | {file = "websockets-10.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185929b4808b36a79c65b7865783b87b6841e852ef5407a2fb0c03381092fa3b"}, 968 | {file = "websockets-10.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d27a7e34c313b3a7f91adcd05134315002aaf8540d7b4f90336beafaea6217c"}, 969 | {file = "websockets-10.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:884be66c76a444c59f801ac13f40c76f176f1bfa815ef5b8ed44321e74f1600b"}, 970 | {file = "websockets-10.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:931c039af54fc195fe6ad536fde4b0de04da9d5916e78e55405436348cfb0e56"}, 971 | {file = "websockets-10.4-cp38-cp38-win32.whl", hash = "sha256:db3c336f9eda2532ec0fd8ea49fef7a8df8f6c804cdf4f39e5c5c0d4a4ad9a7a"}, 972 | {file = "websockets-10.4-cp38-cp38-win_amd64.whl", hash = "sha256:48c08473563323f9c9debac781ecf66f94ad5a3680a38fe84dee5388cf5acaf6"}, 973 | {file = "websockets-10.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:40e826de3085721dabc7cf9bfd41682dadc02286d8cf149b3ad05bff89311e4f"}, 974 | {file = "websockets-10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:56029457f219ade1f2fc12a6504ea61e14ee227a815531f9738e41203a429112"}, 975 | {file = "websockets-10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5fc088b7a32f244c519a048c170f14cf2251b849ef0e20cbbb0fdf0fdaf556f"}, 976 | {file = "websockets-10.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc8709c00704194213d45e455adc106ff9e87658297f72d544220e32029cd3d"}, 977 | {file = "websockets-10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0154f7691e4fe6c2b2bc275b5701e8b158dae92a1ab229e2b940efe11905dff4"}, 978 | {file = "websockets-10.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c6d2264f485f0b53adf22697ac11e261ce84805c232ed5dbe6b1bcb84b00ff0"}, 979 | {file = "websockets-10.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9bc42e8402dc5e9905fb8b9649f57efcb2056693b7e88faa8fb029256ba9c68c"}, 980 | {file = "websockets-10.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:edc344de4dac1d89300a053ac973299e82d3db56330f3494905643bb68801269"}, 981 | {file = "websockets-10.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:84bc2a7d075f32f6ed98652db3a680a17a4edb21ca7f80fe42e38753a58ee02b"}, 982 | {file = "websockets-10.4-cp39-cp39-win32.whl", hash = "sha256:c94ae4faf2d09f7c81847c63843f84fe47bf6253c9d60b20f25edfd30fb12588"}, 983 | {file = "websockets-10.4-cp39-cp39-win_amd64.whl", hash = "sha256:bbccd847aa0c3a69b5f691a84d2341a4f8a629c6922558f2a70611305f902d74"}, 984 | {file = "websockets-10.4-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:82ff5e1cae4e855147fd57a2863376ed7454134c2bf49ec604dfe71e446e2193"}, 985 | {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d210abe51b5da0ffdbf7b43eed0cfdff8a55a1ab17abbec4301c9ff077dd0342"}, 986 | {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:942de28af58f352a6f588bc72490ae0f4ccd6dfc2bd3de5945b882a078e4e179"}, 987 | {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9b27d6c1c6cd53dc93614967e9ce00ae7f864a2d9f99fe5ed86706e1ecbf485"}, 988 | {file = "websockets-10.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3d3cac3e32b2c8414f4f87c1b2ab686fa6284a980ba283617404377cd448f631"}, 989 | {file = "websockets-10.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:da39dd03d130162deb63da51f6e66ed73032ae62e74aaccc4236e30edccddbb0"}, 990 | {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389f8dbb5c489e305fb113ca1b6bdcdaa130923f77485db5b189de343a179393"}, 991 | {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09a1814bb15eff7069e51fed0826df0bc0702652b5cb8f87697d469d79c23576"}, 992 | {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff64a1d38d156d429404aaa84b27305e957fd10c30e5880d1765c9480bea490f"}, 993 | {file = "websockets-10.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b343f521b047493dc4022dd338fc6db9d9282658862756b4f6fd0e996c1380e1"}, 994 | {file = "websockets-10.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:932af322458da7e4e35df32f050389e13d3d96b09d274b22a7aa1808f292fee4"}, 995 | {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a4162139374a49eb18ef5b2f4da1dd95c994588f5033d64e0bbfda4b6b6fcf"}, 996 | {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c57e4c1349fbe0e446c9fa7b19ed2f8a4417233b6984277cce392819123142d3"}, 997 | {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b627c266f295de9dea86bd1112ed3d5fafb69a348af30a2422e16590a8ecba13"}, 998 | {file = "websockets-10.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:05a7233089f8bd355e8cbe127c2e8ca0b4ea55467861906b80d2ebc7db4d6b72"}, 999 | {file = "websockets-10.4.tar.gz", hash = "sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3"}, 1000 | ] 1001 | whitenoise = [ 1002 | {file = "whitenoise-5.3.0-py2.py3-none-any.whl", hash = "sha256:d963ef25639d1417e8a247be36e6aedd8c7c6f0a08adcb5a89146980a96b577c"}, 1003 | {file = "whitenoise-5.3.0.tar.gz", hash = "sha256:d234b871b52271ae7ed6d9da47ffe857c76568f11dd30e28e18c5869dbd11e12"}, 1004 | ] 1005 | zipp = [ 1006 | {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, 1007 | {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, 1008 | ] 1009 | "zope.interface" = [ 1010 | {file = "zope.interface-5.4.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:7df1e1c05304f26faa49fa752a8c690126cf98b40b91d54e6e9cc3b7d6ffe8b7"}, 1011 | {file = "zope.interface-5.4.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c98384b254b37ce50eddd55db8d381a5c53b4c10ee66e1e7fe749824f894021"}, 1012 | {file = "zope.interface-5.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:08f9636e99a9d5410181ba0729e0408d3d8748026ea938f3b970a0249daa8192"}, 1013 | {file = "zope.interface-5.4.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0ea1d73b7c9dcbc5080bb8aaffb776f1c68e807767069b9ccdd06f27a161914a"}, 1014 | {file = "zope.interface-5.4.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:273f158fabc5ea33cbc936da0ab3d4ba80ede5351babc4f577d768e057651531"}, 1015 | {file = "zope.interface-5.4.0-cp27-cp27m-win32.whl", hash = "sha256:a1e6e96217a0f72e2b8629e271e1b280c6fa3fe6e59fa8f6701bec14e3354325"}, 1016 | {file = "zope.interface-5.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:877473e675fdcc113c138813a5dd440da0769a2d81f4d86614e5d62b69497155"}, 1017 | {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263"}, 1018 | {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:b0297b1e05fd128d26cc2460c810d42e205d16d76799526dfa8c8ccd50e74959"}, 1019 | {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:af310ec8335016b5e52cae60cda4a4f2a60a788cbb949a4fbea13d441aa5a09e"}, 1020 | {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:9a9845c4c6bb56e508651f005c4aeb0404e518c6f000d5a1123ab077ab769f5c"}, 1021 | {file = "zope.interface-5.4.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0b465ae0962d49c68aa9733ba92a001b2a0933c317780435f00be7ecb959c702"}, 1022 | {file = "zope.interface-5.4.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5dd9ca406499444f4c8299f803d4a14edf7890ecc595c8b1c7115c2342cadc5f"}, 1023 | {file = "zope.interface-5.4.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:469e2407e0fe9880ac690a3666f03eb4c3c444411a5a5fddfdabc5d184a79f05"}, 1024 | {file = "zope.interface-5.4.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:52de7fc6c21b419078008f697fd4103dbc763288b1406b4562554bd47514c004"}, 1025 | {file = "zope.interface-5.4.0-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:3dd4952748521205697bc2802e4afac5ed4b02909bb799ba1fe239f77fd4e117"}, 1026 | {file = "zope.interface-5.4.0-cp35-cp35m-win32.whl", hash = "sha256:dd93ea5c0c7f3e25335ab7d22a507b1dc43976e1345508f845efc573d3d779d8"}, 1027 | {file = "zope.interface-5.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:3748fac0d0f6a304e674955ab1365d515993b3a0a865e16a11ec9d86fb307f63"}, 1028 | {file = "zope.interface-5.4.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:66c0061c91b3b9cf542131148ef7ecbecb2690d48d1612ec386de9d36766058f"}, 1029 | {file = "zope.interface-5.4.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:d0c1bc2fa9a7285719e5678584f6b92572a5b639d0e471bb8d4b650a1a910920"}, 1030 | {file = "zope.interface-5.4.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2876246527c91e101184f63ccd1d716ec9c46519cc5f3d5375a3351c46467c46"}, 1031 | {file = "zope.interface-5.4.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:334701327f37c47fa628fc8b8d28c7d7730ce7daaf4bda1efb741679c2b087fc"}, 1032 | {file = "zope.interface-5.4.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:71aace0c42d53abe6fc7f726c5d3b60d90f3c5c055a447950ad6ea9cec2e37d9"}, 1033 | {file = "zope.interface-5.4.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5bb3489b4558e49ad2c5118137cfeaf59434f9737fa9c5deefc72d22c23822e2"}, 1034 | {file = "zope.interface-5.4.0-cp36-cp36m-win32.whl", hash = "sha256:1c0e316c9add0db48a5b703833881351444398b04111188069a26a61cfb4df78"}, 1035 | {file = "zope.interface-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f0c02cbb9691b7c91d5009108f975f8ffeab5dff8f26d62e21c493060eff2a1"}, 1036 | {file = "zope.interface-5.4.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:7d97a4306898b05404a0dcdc32d9709b7d8832c0c542b861d9a826301719794e"}, 1037 | {file = "zope.interface-5.4.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:867a5ad16892bf20e6c4ea2aab1971f45645ff3102ad29bd84c86027fa99997b"}, 1038 | {file = "zope.interface-5.4.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5f931a1c21dfa7a9c573ec1f50a31135ccce84e32507c54e1ea404894c5eb96f"}, 1039 | {file = "zope.interface-5.4.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:194d0bcb1374ac3e1e023961610dc8f2c78a0f5f634d0c737691e215569e640d"}, 1040 | {file = "zope.interface-5.4.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:8270252effc60b9642b423189a2fe90eb6b59e87cbee54549db3f5562ff8d1b8"}, 1041 | {file = "zope.interface-5.4.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:15e7d1f7a6ee16572e21e3576d2012b2778cbacf75eb4b7400be37455f5ca8bf"}, 1042 | {file = "zope.interface-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:8892f89999ffd992208754851e5a052f6b5db70a1e3f7d54b17c5211e37a98c7"}, 1043 | {file = "zope.interface-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2e5a26f16503be6c826abca904e45f1a44ff275fdb7e9d1b75c10671c26f8b94"}, 1044 | {file = "zope.interface-5.4.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:0f91b5b948686659a8e28b728ff5e74b1be6bf40cb04704453617e5f1e945ef3"}, 1045 | {file = "zope.interface-5.4.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:4de4bc9b6d35c5af65b454d3e9bc98c50eb3960d5a3762c9438df57427134b8e"}, 1046 | {file = "zope.interface-5.4.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bf68f4b2b6683e52bec69273562df15af352e5ed25d1b6641e7efddc5951d1a7"}, 1047 | {file = "zope.interface-5.4.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:63b82bb63de7c821428d513607e84c6d97d58afd1fe2eb645030bdc185440120"}, 1048 | {file = "zope.interface-5.4.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:db1fa631737dab9fa0b37f3979d8d2631e348c3b4e8325d6873c2541d0ae5a48"}, 1049 | {file = "zope.interface-5.4.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4"}, 1050 | {file = "zope.interface-5.4.0-cp38-cp38-win32.whl", hash = "sha256:a9506a7e80bcf6eacfff7f804c0ad5350c8c95b9010e4356a4b36f5322f09abb"}, 1051 | {file = "zope.interface-5.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:3c02411a3b62668200910090a0dff17c0b25aaa36145082a5a6adf08fa281e54"}, 1052 | {file = "zope.interface-5.4.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:0cee5187b60ed26d56eb2960136288ce91bcf61e2a9405660d271d1f122a69a4"}, 1053 | {file = "zope.interface-5.4.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:a8156e6a7f5e2a0ff0c5b21d6bcb45145efece1909efcbbbf48c56f8da68221d"}, 1054 | {file = "zope.interface-5.4.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:205e40ccde0f37496904572035deea747390a8b7dc65146d30b96e2dd1359a83"}, 1055 | {file = "zope.interface-5.4.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:3f24df7124c323fceb53ff6168da70dbfbae1442b4f3da439cd441681f54fe25"}, 1056 | {file = "zope.interface-5.4.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:5208ebd5152e040640518a77827bdfcc73773a15a33d6644015b763b9c9febc1"}, 1057 | {file = "zope.interface-5.4.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:17776ecd3a1fdd2b2cd5373e5ef8b307162f581c693575ec62e7c5399d80794c"}, 1058 | {file = "zope.interface-5.4.0-cp39-cp39-win32.whl", hash = "sha256:d4d9d6c1a455d4babd320203b918ccc7fcbefe308615c521062bc2ba1aa4d26e"}, 1059 | {file = "zope.interface-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:0cba8477e300d64a11a9789ed40ee8932b59f9ee05f85276dbb4b59acee5dd09"}, 1060 | {file = "zope.interface-5.4.0.tar.gz", hash = "sha256:5dba5f530fec3f0988d83b78cc591b58c0b6eb8431a85edd1569a0539a8a5a0e"}, 1061 | ] 1062 | -------------------------------------------------------------------------------- /backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "tabby-web" 3 | version = "1.0.0" 4 | description = "" 5 | authors = ["Eugeny "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.7" 9 | Django = "^3.2.12" 10 | django-rest-framework = "^0.1.0" 11 | djangorestframework-dataclasses = "^0.9" 12 | social-auth-app-django = "^4.0.0" 13 | python-dotenv = "^0.17.1" 14 | websockets = "^10.4" 15 | gql = "^2.0.0" 16 | dj-database-url = "^0.5.0" 17 | mysqlclient = "^2.0.3" 18 | gunicorn = "^20.1.0" 19 | Twisted = "20.3.0" 20 | semver = "^2.13.0" 21 | requests = "^2.25.1" 22 | pyga = "^2.6.2" 23 | django-cors-headers = "^3.7.0" 24 | cryptography = "^37.0.4" 25 | fsspec = "^2021.7.0" 26 | whitenoise = "^5.3.0" 27 | 28 | [tool.poetry.dev-dependencies] 29 | flake8 = "^3.9.2" 30 | 31 | [build-system] 32 | requires = ["poetry-core>=1.0.0"] 33 | build-backend = "poetry.core.masonry.api" 34 | -------------------------------------------------------------------------------- /backend/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [[ -n "$DOCKERIZE_ARGS" ]]; then 3 | dockerize $DOCKERIZE_ARGS 4 | fi 5 | cd /app 6 | /venv/*/bin/python ./manage.py migrate 7 | exec /venv/*/bin/gunicorn 8 | -------------------------------------------------------------------------------- /backend/tabby/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/backend/tabby/__init__.py -------------------------------------------------------------------------------- /backend/tabby/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/backend/tabby/app/__init__.py -------------------------------------------------------------------------------- /backend/tabby/app/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from .models import Gateway, User, Config 4 | 5 | 6 | class CustomUserAdmin(UserAdmin): 7 | fieldsets = UserAdmin.fieldsets + ( 8 | ( 9 | None, 10 | { 11 | "fields": ( 12 | "custom_connection_gateway", 13 | "custom_connection_gateway_token", 14 | ) 15 | }, 16 | ), 17 | ) 18 | 19 | 20 | admin.site.register(User, CustomUserAdmin) 21 | admin.site.register(Config) 22 | admin.site.register(Gateway) 23 | -------------------------------------------------------------------------------- /backend/tabby/app/api/__init__.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from rest_framework import routers 3 | from . import app_version, auth, config, gateway, user 4 | 5 | 6 | router = routers.DefaultRouter(trailing_slash=False) 7 | router.register("api/1/configs", config.ConfigViewSet) 8 | router.register( 9 | "api/1/versions", app_version.AppVersionViewSet, basename="app-versions" 10 | ) 11 | 12 | urlpatterns = [ 13 | path("api/1/auth/logout", auth.LogoutView.as_view()), 14 | path("api/1/user", user.UserViewSet.as_view({"get": "retrieve", "put": "update"})), 15 | path( 16 | "api/1/gateways/choose", 17 | gateway.ChooseGatewayViewSet.as_view({"post": "retrieve"}), 18 | ), 19 | path("", include(router.urls)), 20 | ] 21 | -------------------------------------------------------------------------------- /backend/tabby/app/api/app_version.py: -------------------------------------------------------------------------------- 1 | import fsspec 2 | import os 3 | from django.conf import settings 4 | from django.utils.decorators import method_decorator 5 | from django.views.decorators.cache import cache_page 6 | from dataclasses import dataclass 7 | from rest_framework.response import Response 8 | from rest_framework.mixins import ListModelMixin 9 | from rest_framework.viewsets import GenericViewSet 10 | from rest_framework_dataclasses.serializers import DataclassSerializer 11 | from typing import List 12 | from urllib.parse import urlparse 13 | 14 | 15 | @dataclass 16 | class AppVersion: 17 | version: str 18 | plugins: List[str] 19 | 20 | 21 | class AppVersionSerializer(DataclassSerializer): 22 | class Meta: 23 | dataclass = AppVersion 24 | 25 | 26 | class AppVersionViewSet(ListModelMixin, GenericViewSet): 27 | serializer_class = AppVersionSerializer 28 | lookup_field = "id" 29 | lookup_value_regex = r"[\w\d.-]+" 30 | queryset = "" 31 | 32 | def _get_versions(self): 33 | fs = fsspec.filesystem(urlparse(settings.APP_DIST_STORAGE).scheme) 34 | return [ 35 | self._get_version(x["name"]) 36 | for x in fs.listdir(settings.APP_DIST_STORAGE) 37 | if x["type"] == "directory" 38 | ] 39 | 40 | def _get_version(self, dir): 41 | fs = fsspec.filesystem(urlparse(settings.APP_DIST_STORAGE).scheme) 42 | plugins = [ 43 | os.path.basename(x["name"]) 44 | for x in fs.listdir(dir) 45 | if x["type"] == "directory" and os.path.basename(x["name"]) 46 | not in [ 47 | "tabby-web-container", 48 | "tabby-web-demo", 49 | ] 50 | ] 51 | 52 | return AppVersion( 53 | version=os.path.basename(dir), 54 | plugins=plugins, 55 | ) 56 | 57 | @method_decorator(cache_page(60)) 58 | def list(self, request, *args, **kwargs): 59 | return Response( 60 | self.serializer_class( 61 | self._get_versions(), 62 | many=True, 63 | ).data 64 | ) 65 | -------------------------------------------------------------------------------- /backend/tabby/app/api/auth.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import logout 2 | from rest_framework.response import Response 3 | from rest_framework.views import APIView 4 | 5 | 6 | class LogoutView(APIView): 7 | def post(self, request, format=None): 8 | logout(request) 9 | return Response(None) 10 | -------------------------------------------------------------------------------- /backend/tabby/app/api/config.py: -------------------------------------------------------------------------------- 1 | from rest_framework import fields 2 | from rest_framework.permissions import IsAuthenticated 3 | from rest_framework.viewsets import ModelViewSet 4 | from rest_framework.serializers import ModelSerializer 5 | from ..models import Config 6 | 7 | 8 | class ConfigSerializer(ModelSerializer): 9 | name = fields.CharField(required=False) 10 | 11 | class Meta: 12 | model = Config 13 | read_only_fields = ("user", "created_at", "modified_at") 14 | fields = "__all__" 15 | 16 | 17 | class ConfigViewSet(ModelViewSet): 18 | queryset = Config.objects.all() 19 | serializer_class = ConfigSerializer 20 | permission_classes = [IsAuthenticated] 21 | 22 | def get_queryset(self): 23 | if self.request.user.is_authenticated: 24 | return Config.objects.filter(user=self.request.user) 25 | return Config.objects.none() 26 | 27 | def perform_create(self, serializer): 28 | serializer.save(user=self.request.user) 29 | -------------------------------------------------------------------------------- /backend/tabby/app/api/gateway.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | from rest_framework import fields, status 4 | from rest_framework.exceptions import APIException 5 | from rest_framework.mixins import RetrieveModelMixin 6 | from rest_framework.viewsets import GenericViewSet 7 | from rest_framework.serializers import ModelSerializer 8 | from ..gateway import GatewayAdminConnection 9 | from ..models import Gateway 10 | 11 | 12 | class GatewaySerializer(ModelSerializer): 13 | url = fields.SerializerMethodField() 14 | auth_token = fields.CharField() 15 | 16 | class Meta: 17 | fields = "__all__" 18 | model = Gateway 19 | 20 | def get_url(self, gw): 21 | return f'{"wss" if gw.secure else "ws"}://{gw.host}:{gw.port}/' 22 | 23 | 24 | class NoGatewaysError(APIException): 25 | status_code = status.HTTP_503_SERVICE_UNAVAILABLE 26 | default_detail = "No connection gateways available." 27 | default_code = "no_gateways" 28 | 29 | 30 | class ChooseGatewayViewSet(RetrieveModelMixin, GenericViewSet): 31 | queryset = Gateway.objects.filter(enabled=True) 32 | serializer_class = GatewaySerializer 33 | 34 | async def _authorize_client(self, gw): 35 | c = GatewayAdminConnection(gw) 36 | await c.connect() 37 | token = await c.authorize_client() 38 | await c.close() 39 | return token 40 | 41 | def get_object(self): 42 | gateways = list(self.queryset) 43 | random.shuffle(gateways) 44 | if not len(gateways): 45 | raise NoGatewaysError() 46 | 47 | loop = asyncio.new_event_loop() 48 | try: 49 | for gw in gateways: 50 | try: 51 | gw.auth_token = loop.run_until_complete(self._authorize_client(gw)) 52 | except ConnectionError as e: 53 | print(e) 54 | continue 55 | return gw 56 | 57 | raise NoGatewaysError() 58 | finally: 59 | loop.close() 60 | -------------------------------------------------------------------------------- /backend/tabby/app/api/user.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from rest_framework import fields 3 | from rest_framework.exceptions import PermissionDenied 4 | from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin 5 | from rest_framework.viewsets import GenericViewSet 6 | from rest_framework.serializers import ModelSerializer 7 | from social_django.models import UserSocialAuth 8 | 9 | from ..sponsors import check_is_sponsor_cached 10 | from ..models import User 11 | 12 | 13 | class UserSerializer(ModelSerializer): 14 | id = fields.IntegerField() 15 | is_pro = fields.SerializerMethodField() 16 | is_sponsor = fields.SerializerMethodField() 17 | github_username = fields.SerializerMethodField() 18 | 19 | class Meta: 20 | model = User 21 | fields = ( 22 | "id", 23 | "username", 24 | "active_config", 25 | "custom_connection_gateway", 26 | "custom_connection_gateway_token", 27 | "config_sync_token", 28 | "is_pro", 29 | "is_sponsor", 30 | "github_username", 31 | ) 32 | read_only_fields = ("id", "username") 33 | 34 | def get_is_pro(self, obj): 35 | return ( 36 | obj.force_pro or not settings.GITHUB_ELIGIBLE_SPONSORSHIPS or check_is_sponsor_cached(obj) 37 | ) 38 | 39 | def get_is_sponsor(self, obj): 40 | return check_is_sponsor_cached(obj) 41 | 42 | def get_github_username(self, obj): 43 | social_auth = UserSocialAuth.objects.filter(user=obj, provider="github").first() 44 | if not social_auth: 45 | return None 46 | 47 | return social_auth.extra_data.get("login") 48 | 49 | 50 | class UserViewSet(RetrieveModelMixin, UpdateModelMixin, GenericViewSet): 51 | queryset = User.objects.all() 52 | serializer_class = UserSerializer 53 | 54 | def get_object(self): 55 | if self.request.user.is_authenticated: 56 | return self.request.user 57 | raise PermissionDenied() 58 | -------------------------------------------------------------------------------- /backend/tabby/app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AppConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "tabby.app" 7 | -------------------------------------------------------------------------------- /backend/tabby/app/gateway.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import os 4 | import secrets 5 | import ssl 6 | import websockets 7 | from django.conf import settings 8 | from urllib.parse import quote 9 | 10 | from .models import Gateway 11 | 12 | 13 | class GatewayConnection: 14 | _ssl_context: ssl.SSLContext = None 15 | 16 | def __init__(self, host: str, port: int): 17 | if settings.CONNECTION_GATEWAY_AUTH_KEY and not GatewayConnection._ssl_context: 18 | ctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) 19 | ctx.load_cert_chain( 20 | os.path.realpath(settings.CONNECTION_GATEWAY_AUTH_CERTIFICATE), 21 | os.path.realpath(settings.CONNECTION_GATEWAY_AUTH_KEY), 22 | ) 23 | if settings.CONNECTION_GATEWAY_AUTH_CA: 24 | ctx.load_verify_locations( 25 | cafile=os.path.realpath(settings.CONNECTION_GATEWAY_AUTH_CA), 26 | ) 27 | ctx.verify_mode = ssl.CERT_REQUIRED 28 | GatewayConnection._ssl_context = ctx 29 | 30 | proto = "wss" if GatewayConnection._ssl_context else "ws" 31 | self.url = f"{proto}://localhost:9000/connect/{quote(host)}:{quote(str(port))}" 32 | 33 | async def connect(self): 34 | self.context = websockets.connect(self.url, ssl=GatewayConnection._ssl_context) 35 | try: 36 | self.socket = await self.context.__aenter__() 37 | except OSError: 38 | raise ConnectionError() 39 | 40 | async def send(self, data): 41 | await self.socket.send(data) 42 | 43 | def recv(self, timeout=None): 44 | return asyncio.wait_for(self.socket.recv(), timeout) 45 | 46 | async def close(self): 47 | await self.socket.close() 48 | await self.context.__aexit__(None, None, None) 49 | 50 | 51 | class GatewayAdminConnection: 52 | _ssl_context: ssl.SSLContext = None 53 | 54 | def __init__(self, gateway: Gateway): 55 | if not settings.CONNECTION_GATEWAY_AUTH_KEY: 56 | raise RuntimeError( 57 | "CONNECTION_GATEWAY_AUTH_KEY is required to manage connection gateways" 58 | ) 59 | if not GatewayAdminConnection._ssl_context: 60 | ctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) 61 | ctx.load_cert_chain( 62 | os.path.realpath(settings.CONNECTION_GATEWAY_AUTH_CERTIFICATE), 63 | os.path.realpath(settings.CONNECTION_GATEWAY_AUTH_KEY), 64 | ) 65 | if settings.CONNECTION_GATEWAY_AUTH_CA: 66 | ctx.load_verify_locations( 67 | cafile=os.path.realpath(settings.CONNECTION_GATEWAY_AUTH_CA), 68 | ) 69 | ctx.verify_mode = ssl.CERT_REQUIRED 70 | GatewayAdminConnection._ssl_context = ctx 71 | 72 | self.url = f"wss://{gateway.host}:{gateway.admin_port}" 73 | 74 | async def connect(self): 75 | self.context = websockets.connect( 76 | self.url, ssl=GatewayAdminConnection._ssl_context 77 | ) 78 | try: 79 | self.socket = await self.context.__aenter__() 80 | except OSError: 81 | raise ConnectionError() 82 | 83 | async def authorize_client(self) -> str: 84 | token = secrets.token_hex(32) 85 | await self.send( 86 | json.dumps( 87 | { 88 | "_": "authorize-client", 89 | "token": token, 90 | } 91 | ) 92 | ) 93 | return token 94 | 95 | async def send(self, data): 96 | await self.socket.send(data) 97 | 98 | async def close(self): 99 | await self.socket.close() 100 | await self.context.__aexit__(None, None, None) 101 | -------------------------------------------------------------------------------- /backend/tabby/app/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/backend/tabby/app/management/__init__.py -------------------------------------------------------------------------------- /backend/tabby/app/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/backend/tabby/app/management/commands/__init__.py -------------------------------------------------------------------------------- /backend/tabby/app/management/commands/add_version.py: -------------------------------------------------------------------------------- 1 | import fsspec 2 | import logging 3 | import requests 4 | import shutil 5 | import subprocess 6 | import tempfile 7 | from django.core.management.base import BaseCommand 8 | from django.conf import settings 9 | from pathlib import Path 10 | from urllib.parse import urlparse 11 | 12 | 13 | class Command(BaseCommand): 14 | help = "Downloads a new app version" 15 | 16 | def add_arguments(self, parser): 17 | parser.add_argument("version", type=str) 18 | 19 | def handle(self, *args, **options): 20 | version = options["version"] 21 | target = f"{settings.APP_DIST_STORAGE}/{version}" 22 | 23 | fs = fsspec.filesystem(urlparse(settings.APP_DIST_STORAGE).scheme) 24 | 25 | plugin_list = [ 26 | "tabby-web-container", 27 | "tabby-core", 28 | "tabby-settings", 29 | "tabby-terminal", 30 | "tabby-ssh", 31 | "tabby-community-color-schemes", 32 | "tabby-serial", 33 | "tabby-telnet", 34 | "tabby-web", 35 | "tabby-web-demo", 36 | ] 37 | 38 | with tempfile.TemporaryDirectory() as tempdir: 39 | tempdir = Path(tempdir) 40 | for plugin in plugin_list: 41 | logging.info(f"Resolving {plugin}@{version}") 42 | response = requests.get(f"{settings.NPM_REGISTRY}/{plugin}/{version}") 43 | response.raise_for_status() 44 | info = response.json() 45 | url = info["dist"]["tarball"] 46 | 47 | logging.info(f"Downloading {plugin}@{version} from {url}") 48 | response = requests.get(url) 49 | 50 | with tempfile.NamedTemporaryFile("wb") as f: 51 | f.write(response.content) 52 | f.flush() 53 | plugin_final_target = Path(tempdir) / plugin 54 | 55 | with tempfile.TemporaryDirectory() as extraction_tmp: 56 | subprocess.check_call( 57 | ["tar", "-xzf", f.name, "-C", str(extraction_tmp)] 58 | ) 59 | shutil.move( 60 | Path(extraction_tmp) / "package", plugin_final_target 61 | ) 62 | 63 | if fs.exists(target): 64 | fs.rm(target, recursive=True) 65 | fs.mkdir(target) 66 | fs.put(str(tempdir), target, recursive=True) 67 | -------------------------------------------------------------------------------- /backend/tabby/app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.3 on 2021-07-08 17:43 2 | 3 | from django.conf import settings 4 | import django.contrib.auth.models 5 | import django.contrib.auth.validators 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | import django.utils.timezone 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | initial = True 14 | 15 | dependencies = [ 16 | ("auth", "0012_alter_user_first_name_max_length"), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name="User", 22 | fields=[ 23 | ( 24 | "id", 25 | models.BigAutoField( 26 | auto_created=True, 27 | primary_key=True, 28 | serialize=False, 29 | verbose_name="ID", 30 | ), 31 | ), 32 | ("password", models.CharField(max_length=128, verbose_name="password")), 33 | ( 34 | "last_login", 35 | models.DateTimeField( 36 | blank=True, null=True, verbose_name="last login" 37 | ), 38 | ), 39 | ( 40 | "is_superuser", 41 | models.BooleanField( 42 | default=False, 43 | help_text="Designates that this user has all permissions without explicitly assigning them.", 44 | verbose_name="superuser status", 45 | ), 46 | ), 47 | ( 48 | "username", 49 | models.CharField( 50 | error_messages={ 51 | "unique": "A user with that username already exists." 52 | }, 53 | help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", 54 | max_length=150, 55 | unique=True, 56 | validators=[ 57 | django.contrib.auth.validators.UnicodeUsernameValidator() 58 | ], 59 | verbose_name="username", 60 | ), 61 | ), 62 | ( 63 | "first_name", 64 | models.CharField( 65 | blank=True, max_length=150, verbose_name="first name" 66 | ), 67 | ), 68 | ( 69 | "last_name", 70 | models.CharField( 71 | blank=True, max_length=150, verbose_name="last name" 72 | ), 73 | ), 74 | ( 75 | "email", 76 | models.EmailField( 77 | blank=True, max_length=254, verbose_name="email address" 78 | ), 79 | ), 80 | ( 81 | "is_staff", 82 | models.BooleanField( 83 | default=False, 84 | help_text="Designates whether the user can log into this admin site.", 85 | verbose_name="staff status", 86 | ), 87 | ), 88 | ( 89 | "is_active", 90 | models.BooleanField( 91 | default=True, 92 | help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", 93 | verbose_name="active", 94 | ), 95 | ), 96 | ( 97 | "date_joined", 98 | models.DateTimeField( 99 | default=django.utils.timezone.now, verbose_name="date joined" 100 | ), 101 | ), 102 | ("active_version", models.CharField(max_length=32, null=True)), 103 | ( 104 | "custom_connection_gateway", 105 | models.CharField(max_length=255, null=True, blank=True), 106 | ), 107 | ( 108 | "custom_connection_gateway_token", 109 | models.CharField(max_length=255, null=True, blank=True), 110 | ), 111 | ("created_at", models.DateTimeField(auto_now_add=True)), 112 | ("modified_at", models.DateTimeField(auto_now=True)), 113 | ], 114 | options={ 115 | "verbose_name": "user", 116 | "verbose_name_plural": "users", 117 | "abstract": False, 118 | }, 119 | managers=[ 120 | ("objects", django.contrib.auth.models.UserManager()), 121 | ], 122 | ), 123 | migrations.CreateModel( 124 | name="Config", 125 | fields=[ 126 | ( 127 | "id", 128 | models.BigAutoField( 129 | auto_created=True, 130 | primary_key=True, 131 | serialize=False, 132 | verbose_name="ID", 133 | ), 134 | ), 135 | ("content", models.TextField(default="{}")), 136 | ("last_used_with_version", models.CharField(max_length=32, null=True)), 137 | ("created_at", models.DateTimeField(auto_now_add=True)), 138 | ("modified_at", models.DateTimeField(auto_now=True)), 139 | ( 140 | "user", 141 | models.ForeignKey( 142 | on_delete=django.db.models.deletion.CASCADE, 143 | related_name="configs", 144 | to=settings.AUTH_USER_MODEL, 145 | ), 146 | ), 147 | ], 148 | ), 149 | migrations.AddField( 150 | model_name="user", 151 | name="active_config", 152 | field=models.ForeignKey( 153 | null=True, 154 | on_delete=django.db.models.deletion.CASCADE, 155 | related_name="+", 156 | to="app.config", 157 | ), 158 | ), 159 | migrations.AddField( 160 | model_name="user", 161 | name="groups", 162 | field=models.ManyToManyField( 163 | blank=True, 164 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", 165 | related_name="user_set", 166 | related_query_name="user", 167 | to="auth.Group", 168 | verbose_name="groups", 169 | ), 170 | ), 171 | migrations.AddField( 172 | model_name="user", 173 | name="user_permissions", 174 | field=models.ManyToManyField( 175 | blank=True, 176 | help_text="Specific permissions for this user.", 177 | related_name="user_set", 178 | related_query_name="user", 179 | to="auth.Permission", 180 | verbose_name="user permissions", 181 | ), 182 | ), 183 | ] 184 | -------------------------------------------------------------------------------- /backend/tabby/app/migrations/0002_gateway.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.3 on 2021-07-08 20:25 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("app", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Gateway", 15 | fields=[ 16 | ( 17 | "id", 18 | models.BigAutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("host", models.CharField(max_length=255)), 26 | ("port", models.IntegerField(default=1234)), 27 | ("enabled", models.BooleanField(default=True)), 28 | ("secure", models.BooleanField(default=True)), 29 | ], 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /backend/tabby/app/migrations/0003_auto_20210711_1855.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.3 on 2021-07-11 18:55 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("app", "0002_gateway"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="gateway", 16 | name="admin_port", 17 | field=models.IntegerField(default=1235), 18 | ), 19 | migrations.AlterField( 20 | model_name="user", 21 | name="active_config", 22 | field=models.ForeignKey( 23 | null=True, 24 | on_delete=django.db.models.deletion.SET_NULL, 25 | related_name="+", 26 | to="app.config", 27 | ), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /backend/tabby/app/migrations/0004_sync_token.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | from django.db import migrations, models 3 | 4 | 5 | def run_forward(apps, schema_editor): 6 | for user in apps.get_model("app", "User").objects.all(): 7 | user.config_sync_token = secrets.token_hex(64) 8 | user.save() 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | dependencies = [ 14 | ("app", "0003_auto_20210711_1855"), 15 | ] 16 | 17 | operations = [ 18 | migrations.AddField( 19 | model_name="user", 20 | name="config_sync_token", 21 | field=models.CharField(blank=True, max_length=255, null=True), 22 | ), 23 | migrations.RunPython(run_forward, lambda _, __: None), 24 | migrations.AlterField( 25 | model_name="user", 26 | name="config_sync_token", 27 | field=models.CharField(max_length=255), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /backend/tabby/app/migrations/0005_user_force_pro.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.3 on 2021-07-24 10:13 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("app", "0004_sync_token"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="user", 15 | name="force_pro", 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/tabby/app/migrations/0006_config_name.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | def run_forward(apps, schema_editor): 5 | for config in apps.get_model("app", "Config").objects.all(): 6 | config.name = f"Unnamed config ({config.created_at.date()})" 7 | config.save() 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ("app", "0005_user_force_pro"), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name="config", 19 | name="name", 20 | field=models.CharField(max_length=255, null=True), 21 | ), 22 | migrations.RunPython(run_forward, lambda _, __: None), 23 | migrations.AlterField( 24 | model_name="config", 25 | name="name", 26 | field=models.CharField(max_length=255), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /backend/tabby/app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/backend/tabby/app/migrations/__init__.py -------------------------------------------------------------------------------- /backend/tabby/app/models.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | from datetime import date 3 | from django.db import models 4 | from django.contrib.auth.models import AbstractUser 5 | 6 | 7 | class Config(models.Model): 8 | user = models.ForeignKey( 9 | "app.User", related_name="configs", on_delete=models.CASCADE 10 | ) 11 | name = models.CharField(max_length=255) 12 | content = models.TextField(default="{}") 13 | last_used_with_version = models.CharField(max_length=32, null=True) 14 | created_at = models.DateTimeField(auto_now_add=True) 15 | modified_at = models.DateTimeField(auto_now=True) 16 | 17 | def save(self, *args, **kwargs): 18 | if not self.name: 19 | self.name = f"Unnamed config ({date.today()})" 20 | super().save(*args, **kwargs) 21 | 22 | 23 | class User(AbstractUser): 24 | active_config = models.ForeignKey( 25 | Config, null=True, on_delete=models.SET_NULL, related_name="+" 26 | ) 27 | active_version = models.CharField(max_length=32, null=True) 28 | custom_connection_gateway = models.CharField(max_length=255, null=True, blank=True) 29 | custom_connection_gateway_token = models.CharField( 30 | max_length=255, null=True, blank=True 31 | ) 32 | config_sync_token = models.CharField(max_length=255) 33 | force_pro = models.BooleanField(default=False) 34 | created_at = models.DateTimeField(auto_now_add=True) 35 | modified_at = models.DateTimeField(auto_now=True) 36 | 37 | def save(self, *args, **kwargs): 38 | if not self.config_sync_token: 39 | self.config_sync_token = secrets.token_hex(64) 40 | super().save(*args, **kwargs) 41 | 42 | 43 | class Gateway(models.Model): 44 | host = models.CharField(max_length=255) 45 | port = models.IntegerField(default=1234) 46 | admin_port = models.IntegerField(default=1235) 47 | enabled = models.BooleanField(default=True) 48 | secure = models.BooleanField(default=True) 49 | 50 | def __str__(self): 51 | return f"{self.host}:{self.port}" 52 | -------------------------------------------------------------------------------- /backend/tabby/app/sponsors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.cache import cache 3 | from gql import Client, gql 4 | from gql.transport.requests import RequestsHTTPTransport 5 | from social_django.models import UserSocialAuth 6 | 7 | from .models import User 8 | 9 | 10 | GQL_ENDPOINT = "https://api.github.com/graphql" 11 | CACHE_KEY = "cached-sponsors:%s" 12 | 13 | 14 | def check_is_sponsor(user: User) -> bool: 15 | try: 16 | token = user.social_auth.get(provider="github").extra_data.get("access_token") 17 | except UserSocialAuth.DoesNotExist: 18 | return False 19 | 20 | if not token: 21 | return False 22 | 23 | client = Client( 24 | transport=RequestsHTTPTransport( 25 | url=GQL_ENDPOINT, 26 | use_json=True, 27 | headers={ 28 | "Authorization": f"Bearer {token}", 29 | }, 30 | ) 31 | ) 32 | 33 | after = None 34 | 35 | while True: 36 | params = "first: 1" 37 | if after: 38 | params += f', after:"{after}"' 39 | 40 | query = """ 41 | query { 42 | viewer { 43 | sponsorshipsAsSponsor(%s) { 44 | pageInfo { 45 | startCursor 46 | hasNextPage 47 | endCursor 48 | } 49 | totalRecurringMonthlyPriceInDollars 50 | nodes { 51 | sponsorable { 52 | ... on Organization { login } 53 | ... on User { login } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | """ % ( 60 | params, 61 | ) 62 | 63 | response = client.execute(gql(query)) 64 | info = response["viewer"]["sponsorshipsAsSponsor"] 65 | after = info["pageInfo"]["endCursor"] 66 | nodes = info["nodes"] 67 | if not len(nodes): 68 | break 69 | for node in nodes: 70 | if ( 71 | node["sponsorable"]["login"].lower() 72 | not in settings.GITHUB_ELIGIBLE_SPONSORSHIPS 73 | ): 74 | continue 75 | if ( 76 | info["totalRecurringMonthlyPriceInDollars"] >= settings.GITHUB_SPONSORS_MIN_PAYMENT 77 | ): 78 | return True 79 | 80 | return False 81 | 82 | 83 | def check_is_sponsor_cached(user: User) -> bool: 84 | cache_key = CACHE_KEY % user.id 85 | if not cache.get(cache_key): 86 | cache.set(cache_key, check_is_sponsor(user), timeout=30) 87 | return cache.get(cache_key) 88 | -------------------------------------------------------------------------------- /backend/tabby/app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | 3 | from . import api 4 | from . import views 5 | 6 | 7 | urlpatterns = [ 8 | *[ 9 | path(p, views.IndexView.as_view()) 10 | for p in ["", "login", "app", "about", "about/features"] 11 | ], 12 | path("app-dist//", views.AppDistView.as_view()), 13 | path("terminal", views.TerminalView.as_view()), 14 | path("demo", views.DemoView.as_view()), 15 | path("", include(api.urlpatterns)), 16 | ] 17 | -------------------------------------------------------------------------------- /backend/tabby/app/views.py: -------------------------------------------------------------------------------- 1 | import fsspec 2 | import os 3 | from fsspec.implementations.local import LocalFileSystem 4 | from django.conf import settings 5 | from django.http.response import ( 6 | FileResponse, 7 | HttpResponseNotFound, 8 | HttpResponseRedirect, 9 | ) 10 | from django.views import static 11 | from rest_framework.views import APIView 12 | from urllib.parse import urlparse 13 | 14 | 15 | class IndexView(APIView): 16 | def get(self, request, format=None): 17 | if settings.FRONTEND_URL: 18 | return HttpResponseRedirect(settings.FRONTEND_URL) 19 | return static.serve( 20 | request, "index.html", document_root=str(settings.STATIC_ROOT) 21 | ) 22 | 23 | 24 | class TerminalView(APIView): 25 | def get(self, request, format=None): 26 | response = static.serve( 27 | request, "terminal.html", document_root=str(settings.STATIC_ROOT) 28 | ) 29 | response["Content-Security-Policy"] = "frame-ancestors 'self' https://tabby.sh;" 30 | response["X-Frame-Options"] = "SAMEORIGIN" 31 | return response 32 | 33 | 34 | class DemoView(APIView): 35 | def get(self, request, format=None): 36 | response = static.serve( 37 | request, "demo.html", document_root=str(settings.STATIC_ROOT) 38 | ) 39 | response["Content-Security-Policy"] = "frame-ancestors 'self' https://tabby.sh;" 40 | response['X-Frame-Options'] = 'ALLOW-FROM https://tabby.sh' 41 | return response 42 | 43 | 44 | class AppDistView(APIView): 45 | def get(self, request, version=None, path=None, format=None): 46 | fs = fsspec.filesystem(urlparse(settings.APP_DIST_STORAGE).scheme) 47 | url = f"{settings.APP_DIST_STORAGE}/{version}/{path}" 48 | if isinstance(fs, LocalFileSystem): 49 | if not fs.exists(url): 50 | return HttpResponseNotFound() 51 | return FileResponse(fs.open(url), filename=os.path.basename(url)) 52 | else: 53 | return HttpResponseRedirect(fs.url(url)) 54 | -------------------------------------------------------------------------------- /backend/tabby/middleware.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from tabby.app.models import User 3 | from django.conf import settings 4 | from django.contrib.auth import login 5 | from pyga.requests import Tracker, Page, Session, Visitor 6 | 7 | 8 | class BaseMiddleware: 9 | def __init__(self, get_response): 10 | self.get_response = get_response 11 | 12 | 13 | class TokenMiddleware(BaseMiddleware): 14 | def __call__(self, request): 15 | token_value = None 16 | if "auth_token" in request.GET: 17 | token_value = request.GET["auth_token"] 18 | if request.META.get("HTTP_AUTHORIZATION"): 19 | token_type, *credentials = request.META["HTTP_AUTHORIZATION"].split() 20 | if token_type == "Bearer" and len(credentials): 21 | token_value = credentials[0] 22 | 23 | user = User.objects.filter(config_sync_token=token_value).first() 24 | 25 | if user: 26 | request.session.save = lambda *args, **kwargs: None 27 | setattr(user, "backend", "django.contrib.auth.backends.ModelBackend") 28 | login(request, user) 29 | setattr(request, "_dont_enforce_csrf_checks", True) 30 | 31 | response = self.get_response(request) 32 | 33 | if user: 34 | response.set_cookie = lambda *args, **kwargs: None 35 | 36 | return response 37 | 38 | 39 | class GAMiddleware(BaseMiddleware): 40 | def __init__(self, get_response): 41 | super().__init__(get_response) 42 | if settings.GA_ID: 43 | self.tracker = Tracker(settings.GA_ID, settings.GA_DOMAIN) 44 | 45 | def __call__(self, request): 46 | response = self.get_response(request) 47 | if settings.GA_ID and request.path in ["/", "/app"]: 48 | try: 49 | self.tracker.track_pageview(Page(request.path), Session(), Visitor()) 50 | except Exception: 51 | logging.exception() 52 | 53 | return response 54 | -------------------------------------------------------------------------------- /backend/tabby/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import dj_database_url 3 | from dotenv import load_dotenv 4 | from pathlib import Path 5 | from urllib.parse import urlparse 6 | 7 | load_dotenv() 8 | 9 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 10 | BASE_DIR = Path(__file__).resolve().parent.parent 11 | 12 | SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "django-insecure") 13 | DEBUG = bool(os.getenv("DEBUG", False)) 14 | 15 | ALLOWED_HOSTS = ["*"] 16 | USE_X_FORWARDED_HOST = True 17 | SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") 18 | 19 | 20 | # Application definition 21 | 22 | INSTALLED_APPS = [ 23 | "django.contrib.admin", 24 | "django.contrib.auth", 25 | "django.contrib.contenttypes", 26 | "django.contrib.sessions", 27 | "django.contrib.messages", 28 | "django.contrib.staticfiles", 29 | "rest_framework", 30 | "social_django", 31 | "corsheaders", 32 | "tabby.app", 33 | ] 34 | 35 | MIDDLEWARE = [ 36 | "django.middleware.security.SecurityMiddleware", 37 | "whitenoise.middleware.WhiteNoiseMiddleware", 38 | "django.contrib.sessions.middleware.SessionMiddleware", 39 | "django.middleware.common.CommonMiddleware", 40 | "django.middleware.csrf.CsrfViewMiddleware", 41 | "django.contrib.auth.middleware.AuthenticationMiddleware", 42 | "django.contrib.messages.middleware.MessageMiddleware", 43 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 44 | "corsheaders.middleware.CorsMiddleware", 45 | "tabby.middleware.TokenMiddleware", 46 | "tabby.middleware.GAMiddleware", 47 | ] 48 | 49 | ROOT_URLCONF = "tabby.urls" 50 | 51 | TEMPLATES = [ 52 | { 53 | "BACKEND": "django.template.backends.django.DjangoTemplates", 54 | "DIRS": [], 55 | "APP_DIRS": True, 56 | "OPTIONS": { 57 | "context_processors": [ 58 | "django.template.context_processors.debug", 59 | "django.template.context_processors.request", 60 | "django.contrib.auth.context_processors.auth", 61 | "django.contrib.messages.context_processors.messages", 62 | ], 63 | }, 64 | }, 65 | ] 66 | 67 | WSGI_APPLICATION = "tabby.wsgi.application" 68 | 69 | DATABASES = {"default": dj_database_url.config(conn_max_age=600)} 70 | 71 | CACHES = { 72 | "default": { 73 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 74 | } 75 | } 76 | 77 | AUTH_PASSWORD_VALIDATORS = [ 78 | { 79 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 80 | }, 81 | { 82 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 83 | }, 84 | { 85 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 86 | }, 87 | { 88 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 89 | }, 90 | ] 91 | 92 | AUTH_USER_MODEL = "app.User" 93 | 94 | REST_FRAMEWORK = { 95 | "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",) 96 | } 97 | 98 | LANGUAGE_CODE = "en-us" 99 | 100 | TIME_ZONE = "UTC" 101 | 102 | USE_I18N = True 103 | 104 | USE_L10N = True 105 | 106 | USE_TZ = True 107 | 108 | LOGGING = { 109 | "version": 1, 110 | "disable_existing_loggers": False, 111 | "formatters": { 112 | "simple": {"format": "%(levelname)s %(message)s"}, 113 | }, 114 | "handlers": { 115 | "console": { 116 | "level": "INFO", 117 | "class": "logging.StreamHandler", 118 | "formatter": "simple", 119 | }, 120 | }, 121 | "loggers": { 122 | "": { 123 | "handlers": ["console"], 124 | "propagate": False, 125 | "level": "INFO", 126 | }, 127 | }, 128 | } 129 | 130 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 131 | 132 | CSRF_USE_SESSIONS = False 133 | CSRF_COOKIE_HTTPONLY = False 134 | CSRF_COOKIE_NAME = "XSRF-TOKEN" 135 | CSRF_HEADER_NAME = "HTTP_X_XSRF_TOKEN" 136 | 137 | AUTHENTICATION_BACKENDS = ( 138 | "social_core.backends.github.GithubOAuth2", 139 | "social_core.backends.gitlab.GitLabOAuth2", 140 | "social_core.backends.azuread.AzureADOAuth2", 141 | "social_core.backends.microsoft.MicrosoftOAuth2", 142 | "social_core.backends.google.GoogleOAuth2", 143 | "django.contrib.auth.backends.ModelBackend", 144 | ) 145 | 146 | SOCIAL_AUTH_GITHUB_SCOPE = ["read:user", "user:email"] 147 | SOCIAL_AUTH_PIPELINE = ( 148 | "social_core.pipeline.social_auth.social_details", 149 | "social_core.pipeline.social_auth.social_uid", 150 | "social_core.pipeline.social_auth.auth_allowed", 151 | "social_core.pipeline.social_auth.social_user", 152 | "social_core.pipeline.user.get_username", 153 | "social_core.pipeline.social_auth.associate_by_email", 154 | "social_core.pipeline.user.create_user", 155 | "social_core.pipeline.social_auth.associate_user", 156 | "social_core.pipeline.social_auth.load_extra_data", 157 | "social_core.pipeline.user.user_details", 158 | ) 159 | 160 | APP_DIST_STORAGE = os.getenv("APP_DIST_STORAGE", "file://" + str(BASE_DIR / "app-dist")) 161 | NPM_REGISTRY = os.getenv("NPM_REGISTRY", "https://registry.npmjs.org").rstrip("/") 162 | FRONTEND_BUILD_DIR = Path( 163 | os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "../frontend/build") 164 | ) 165 | 166 | FRONTEND_URL = None 167 | CORS_EXTRA_URL = None 168 | BACKEND_URL = None 169 | GITHUB_ELIGIBLE_SPONSORSHIPS = None 170 | 171 | for key in [ 172 | "CORS_EXTRA_URL", 173 | "FRONTEND_URL", 174 | "BACKEND_URL", 175 | "SOCIAL_AUTH_GITHUB_KEY", 176 | "SOCIAL_AUTH_GITHUB_SECRET", 177 | "SOCIAL_AUTH_GITLAB_KEY", 178 | "SOCIAL_AUTH_GITLAB_SECRET", 179 | "SOCIAL_AUTH_GOOGLE_OAUTH2_KEY", 180 | "SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET", 181 | "SOCIAL_AUTH_MICROSOFT_GRAPH_KEY", 182 | "SOCIAL_AUTH_MICROSOFT_GRAPH_SECRET", 183 | "CONNECTION_GATEWAY_AUTH_CA", 184 | "CONNECTION_GATEWAY_AUTH_CERTIFICATE", 185 | "CONNECTION_GATEWAY_AUTH_KEY", 186 | "GITHUB_ELIGIBLE_SPONSORSHIPS", 187 | "GITHUB_SPONSORS_MIN_PAYMENT", 188 | "GA_ID", 189 | "GA_DOMAIN", 190 | ]: 191 | globals()[key] = os.getenv(key) 192 | 193 | 194 | for key in [ 195 | "GITHUB_SPONSORS_MIN_PAYMENT", 196 | ]: 197 | globals()[key] = int(globals()[key]) if globals()[key] else None 198 | 199 | 200 | for key in [ 201 | "CONNECTION_GATEWAY_AUTH_CA", 202 | "CONNECTION_GATEWAY_AUTH_CERTIFICATE", 203 | "CONNECTION_GATEWAY_AUTH_KEY", 204 | ]: 205 | v = globals()[key] 206 | if v and not os.path.exists(v): 207 | raise ValueError(f"{v} does not exist") 208 | 209 | if GITHUB_ELIGIBLE_SPONSORSHIPS: 210 | GITHUB_ELIGIBLE_SPONSORSHIPS = GITHUB_ELIGIBLE_SPONSORSHIPS.split(",") 211 | else: 212 | GITHUB_ELIGIBLE_SPONSORSHIPS = [] 213 | 214 | 215 | STATIC_URL = "/static/" 216 | if FRONTEND_BUILD_DIR.exists(): 217 | STATICFILES_DIRS = [FRONTEND_BUILD_DIR] 218 | STATIC_ROOT = BASE_DIR / "public" 219 | 220 | 221 | if FRONTEND_URL or CORS_EXTRA_URL: 222 | cors_url = CORS_EXTRA_URL or FRONTEND_URL 223 | CORS_ALLOWED_ORIGINS = [cors_url, "https://tabby.sh"] 224 | CORS_ALLOW_CREDENTIALS = True 225 | CORS_ALLOW_HEADERS = [ 226 | "accept", 227 | "accept-encoding", 228 | "authorization", 229 | "content-type", 230 | "dnt", 231 | "origin", 232 | "user-agent", 233 | "x-xsrf-token", 234 | "x-requested-with", 235 | ] 236 | cors_domain = urlparse(cors_url).hostname 237 | CSRF_TRUSTED_ORIGINS = [cors_domain] 238 | if BACKEND_URL: 239 | CSRF_TRUSTED_ORIGINS.append(urlparse(BACKEND_URL).hostname) 240 | 241 | cors_url = cors_url.rstrip("/") 242 | 243 | if cors_url.startswith("https://"): 244 | CSRF_COOKIE_SECURE = True 245 | SESSION_COOKIE_SECURE = True 246 | else: 247 | FRONTEND_URL = "" 248 | 249 | if FRONTEND_URL: 250 | LOGIN_REDIRECT_URL = FRONTEND_URL 251 | frontend_domain = urlparse(FRONTEND_URL).hostname 252 | SESSION_COOKIE_DOMAIN = os.getenv("SESSION_COOKIE_DOMAIN", cors_domain) 253 | SESSION_COOKIE_SAMESITE = None 254 | CSRF_COOKIE_DOMAIN = cors_domain 255 | if FRONTEND_URL.startswith("https://"): 256 | CSRF_COOKIE_SECURE = True 257 | else: 258 | LOGIN_REDIRECT_URL = '/' 259 | -------------------------------------------------------------------------------- /backend/tabby/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | 4 | from .app.urls import urlpatterns as app_urlpatterns 5 | 6 | urlpatterns = [ 7 | path("", include(app_urlpatterns)), 8 | path("api/1/auth/social/", include("social_django.urls", namespace="social")), 9 | path("admin/", admin.site.urls), 10 | ] 11 | -------------------------------------------------------------------------------- /backend/tabby/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tabby.settings") 6 | 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /backend/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | tabby: 3 | build: . 4 | restart: always 5 | depends_on: 6 | - db 7 | ports: 8 | - 9090:80 9 | environment: 10 | - DATABASE_URL=mysql://root:123@db/tabby 11 | - PORT=80 12 | - DEBUG=False 13 | - DOCKERIZE_ARGS="-wait tcp://db:3306 -timeout 60s" 14 | # - APP_DIST_STORAGE="file:///app-dist" 15 | 16 | db: 17 | image: mariadb:10.7.1 18 | restart: always 19 | environment: 20 | MARIADB_DATABASE: tabby 21 | MARIADB_USER: user 22 | MARIADB_PASSWORD: 123 23 | MYSQL_ROOT_PASSWORD: 123 24 | -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/docs/screenshot.png -------------------------------------------------------------------------------- /frontend/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parser: '@typescript-eslint/parser' 2 | parserOptions: 3 | project: 4 | - tsconfig.json 5 | extends: 6 | - 'plugin:@typescript-eslint/all' 7 | plugins: 8 | - '@typescript-eslint' 9 | env: 10 | browser: true 11 | es6: true 12 | node: true 13 | commonjs: true 14 | rules: 15 | '@typescript-eslint/semi': 16 | - error 17 | - never 18 | '@typescript-eslint/indent': 19 | - error 20 | - 2 21 | '@typescript-eslint/explicit-member-accessibility': 22 | - error 23 | - accessibility: no-public 24 | overrides: 25 | parameterProperties: explicit 26 | '@typescript-eslint/no-require-imports': off 27 | '@typescript-eslint/no-parameter-properties': off 28 | '@typescript-eslint/explicit-function-return-type': off 29 | '@typescript-eslint/no-explicit-any': off 30 | '@typescript-eslint/no-magic-numbers': off 31 | '@typescript-eslint/member-delimiter-style': off 32 | '@typescript-eslint/promise-function-async': off 33 | '@typescript-eslint/require-array-sort-compare': off 34 | '@typescript-eslint/no-floating-promises': off 35 | '@typescript-eslint/prefer-readonly': off 36 | '@typescript-eslint/require-await': off 37 | '@typescript-eslint/strict-boolean-expressions': off 38 | '@typescript-eslint/no-misused-promises': 39 | - error 40 | - checksVoidReturn: false 41 | '@typescript-eslint/typedef': off 42 | '@typescript-eslint/consistent-type-imports': off 43 | '@typescript-eslint/sort-type-union-intersection-members': off 44 | '@typescript-eslint/no-use-before-define': 45 | - error 46 | - classes: false 47 | no-duplicate-imports: error 48 | array-bracket-spacing: 49 | - error 50 | - never 51 | block-scoped-var: error 52 | brace-style: off 53 | '@typescript-eslint/brace-style': 54 | - error 55 | - 1tbs 56 | - allowSingleLine: true 57 | computed-property-spacing: 58 | - error 59 | - never 60 | comma-dangle: off 61 | '@typescript-eslint/comma-dangle': 62 | - error 63 | - always-multiline 64 | curly: error 65 | eol-last: error 66 | eqeqeq: 67 | - error 68 | - smart 69 | max-depth: 70 | - 1 71 | - 5 72 | max-statements: 73 | - 1 74 | - 80 75 | no-multiple-empty-lines: error 76 | no-mixed-spaces-and-tabs: error 77 | no-trailing-spaces: error 78 | '@typescript-eslint/no-unused-vars': 79 | - error 80 | - vars: all 81 | args: after-used 82 | argsIgnorePattern: ^_ 83 | no-undef: error 84 | no-var: error 85 | object-curly-spacing: off 86 | '@typescript-eslint/object-curly-spacing': 87 | - error 88 | - always 89 | quote-props: 90 | - warn 91 | - as-needed 92 | - keywords: true 93 | numbers: true 94 | quotes: off 95 | '@typescript-eslint/quotes': 96 | - error 97 | - single 98 | - allowTemplateLiterals: true 99 | '@typescript-eslint/no-confusing-void-expression': 100 | - error 101 | - ignoreArrowShorthand: true 102 | '@typescript-eslint/no-non-null-assertion': off 103 | '@typescript-eslint/no-unnecessary-condition': 104 | - error 105 | - allowConstantLoopConditions: true 106 | '@typescript-eslint/restrict-template-expressions': off 107 | '@typescript-eslint/prefer-readonly-parameter-types': off 108 | '@typescript-eslint/no-unsafe-member-access': off 109 | '@typescript-eslint/no-unsafe-call': off 110 | '@typescript-eslint/no-unsafe-return': off 111 | '@typescript-eslint/no-unsafe-assignment': off 112 | '@typescript-eslint/naming-convention': off 113 | '@typescript-eslint/lines-between-class-members': 114 | - error 115 | - exceptAfterSingleLine: true 116 | '@typescript-eslint/dot-notation': off 117 | '@typescript-eslint/no-implicit-any-catch': off 118 | '@typescript-eslint/member-ordering': off 119 | '@typescript-eslint/no-var-requires': off 120 | '@typescript-eslint/no-unsafe-argument': off 121 | '@typescript-eslint/restrict-plus-operands': off 122 | '@typescript-eslint/space-infix-ops': off 123 | '@typescript-eslint/explicit-module-boundary-types': off 124 | 125 | overrides: 126 | - files: '*.service.ts' 127 | rules: 128 | '@typescript-eslint/explicit-module-boundary-types': 129 | - error 130 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.ignore.js 3 | *.ignore.js.map 4 | build 5 | build-server 6 | *.d.ts 7 | yarn-error.log 8 | static 9 | -------------------------------------------------------------------------------- /frontend/.pug-lintrc.js: -------------------------------------------------------------------------------- 1 | module.export = { 2 | } 3 | -------------------------------------------------------------------------------- /frontend/assets/demo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/demo.jpeg -------------------------------------------------------------------------------- /frontend/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/favicon.png -------------------------------------------------------------------------------- /frontend/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/assets/meta-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/meta-preview.png -------------------------------------------------------------------------------- /frontend/assets/screenshots/colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/screenshots/colors.png -------------------------------------------------------------------------------- /frontend/assets/screenshots/fonts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/screenshots/fonts.png -------------------------------------------------------------------------------- /frontend/assets/screenshots/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/screenshots/history.png -------------------------------------------------------------------------------- /frontend/assets/screenshots/hotkeys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/screenshots/hotkeys.png -------------------------------------------------------------------------------- /frontend/assets/screenshots/paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/screenshots/paste.png -------------------------------------------------------------------------------- /frontend/assets/screenshots/ports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/screenshots/ports.png -------------------------------------------------------------------------------- /frontend/assets/screenshots/profiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/screenshots/profiles.png -------------------------------------------------------------------------------- /frontend/assets/screenshots/progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/screenshots/progress.png -------------------------------------------------------------------------------- /frontend/assets/screenshots/quake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/screenshots/quake.png -------------------------------------------------------------------------------- /frontend/assets/screenshots/serial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/screenshots/serial.png -------------------------------------------------------------------------------- /frontend/assets/screenshots/split.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/screenshots/split.png -------------------------------------------------------------------------------- /frontend/assets/screenshots/ssh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/screenshots/ssh.png -------------------------------------------------------------------------------- /frontend/assets/screenshots/ssh2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/screenshots/ssh2.png -------------------------------------------------------------------------------- /frontend/assets/screenshots/tabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/screenshots/tabs.png -------------------------------------------------------------------------------- /frontend/assets/screenshots/win.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/screenshots/win.png -------------------------------------------------------------------------------- /frontend/assets/screenshots/window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/screenshots/window.png -------------------------------------------------------------------------------- /frontend/assets/screenshots/zmodem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eugeny/tabby-web/16847cea93f730814c1855241d8ebdea20b1ff6e/frontend/assets/screenshots/zmodem.png -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tabby-web", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "lint": "eslint src", 7 | "build": "webpack --progress", 8 | "watch": "DEV=1 webpack --progress --watch", 9 | "build:server": "webpack --progress -c webpack.config.server.js", 10 | "watch:server": "DEV=1 webpack --progress --watch -c webpack.config.server.js", 11 | "start": "node build-server/server.js" 12 | }, 13 | "private": true, 14 | "devDependencies": { 15 | "@angular/animations": "^12.2.11", 16 | "@angular/cdk": "^12.2.11", 17 | "@angular/common": "^12.2.11", 18 | "@angular/compiler": "^12.2.11", 19 | "@angular/compiler-cli": "^12.2.11", 20 | "@angular/core": "^12.2.11", 21 | "@angular/forms": "^12.2.11", 22 | "@angular/platform-browser": "^12.2.11", 23 | "@angular/platform-browser-dynamic": "^12.2.11", 24 | "@angular/platform-server": "^12.2.11", 25 | "@angular/router": "^12.2.11", 26 | "@fontsource/fira-code": "^4.5.0", 27 | "@fortawesome/angular-fontawesome": "0.8", 28 | "@fortawesome/fontawesome-free": "^5.7.2", 29 | "@fortawesome/fontawesome-svg-core": "^1.2.35", 30 | "@fortawesome/free-brands-svg-icons": "^5.15.3", 31 | "@fortawesome/free-solid-svg-icons": "^5.15.3", 32 | "@ng-bootstrap/ng-bootstrap": "11.0.0-beta.1", 33 | "@ngtools/webpack": "^12.2.12", 34 | "@nguniversal/express-engine": "^12.1.3", 35 | "@tabby-gang/to-string-loader": "^1.1.7-beta.2", 36 | "@types/node": "^11.9.5", 37 | "@typescript-eslint/eslint-plugin": "^5.1.0", 38 | "@typescript-eslint/parser": "^5.1.0", 39 | "apply-loader": "^2.0.0", 40 | "bootstrap": "^5.0.1", 41 | "buffer": "^6.0.3", 42 | "core-js": "^3.14.0", 43 | "css-loader": "^5.2.0", 44 | "deepmerge": "^4.2.2", 45 | "domino": "^2.1.6", 46 | "dotenv": "^10.0.0", 47 | "eslint": "^7.31.0", 48 | "express": "^4.17.1", 49 | "html-loader": "^2.1.2", 50 | "html-webpack-plugin": "^5.5.0", 51 | "js-yaml": "^4.1.0", 52 | "mini-css-extract-plugin": "^2.4.3", 53 | "ngx-image-zoom": "^0.6.0", 54 | "ngx-toastr": "^14.0.0", 55 | "pug": "^3.0.2", 56 | "pug-loader": "^2.4.0", 57 | "rxjs": "^7.1.0", 58 | "sass": "^1.43.4", 59 | "sass-loader": "^12.3.0", 60 | "script-loader": "^0.7.2", 61 | "semver": "^7.3.5", 62 | "source-code-pro": "^2.30.1", 63 | "source-map-support": "^0.5.19", 64 | "source-sans-pro": "^2.45.0", 65 | "style-loader": "^3.3.1", 66 | "three": "^0.119.0", 67 | "throng": "^5.0.0", 68 | "typescript": "~4.3.2", 69 | "vanta": "^0.5.21", 70 | "webpack": "^5.61.0", 71 | "webpack-bundle-analyzer": "^4.5.0", 72 | "webpack-cli": "^4.9.1", 73 | "zone.js": "^0.11.4" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /frontend/src/api.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number 3 | active_config: number 4 | active_version: string 5 | custom_connection_gateway: string|null 6 | custom_connection_gateway_token: string|null 7 | config_sync_token: string 8 | github_username: string 9 | is_pro: boolean 10 | is_sponsor: boolean 11 | } 12 | 13 | export interface Config { 14 | id: number 15 | content: string 16 | last_used_with_version: string 17 | created_at: Date 18 | modified_at: Date 19 | } 20 | 21 | export interface Version { 22 | version: string 23 | plugins: string[] 24 | } 25 | 26 | export interface Gateway { 27 | host: string 28 | port: number 29 | url: string 30 | auth_token: string 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/app.component.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-extraneous-class */ 2 | import { Component } from '@angular/core' 3 | 4 | @Component({ 5 | selector: 'app', 6 | template: '', 7 | }) 8 | export class AppComponent { } 9 | -------------------------------------------------------------------------------- /frontend/src/app.module.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-extraneous-class */ 2 | import { NgModule } from '@angular/core' 3 | import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser' 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations' 5 | import { CommonModule } from '@angular/common' 6 | import { FormsModule } from '@angular/forms' 7 | import { RouterModule } from '@angular/router' 8 | import { ClipboardModule } from '@angular/cdk/clipboard' 9 | import { TransferHttpCacheModule } from '@nguniversal/common' 10 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome' 11 | import { HttpClientModule } from '@angular/common/http' 12 | 13 | import { AppComponent } from './app.component' 14 | import { CommonAppModule } from 'src/common' 15 | 16 | import '@fortawesome/fontawesome-svg-core/styles.css' 17 | 18 | const ROUTES = [ 19 | { 20 | path: '', 21 | loadChildren: () => import(/* webpackChunkName: "app" */'./app').then(m => m.ApplicationModule), 22 | }, 23 | { 24 | path: 'app', 25 | redirectTo: '/', 26 | }, 27 | { 28 | path: 'login', 29 | loadChildren: () => import(/* webpackChunkName: "login" */'./login').then(m => m.LoginModule), 30 | }, 31 | ] 32 | 33 | @NgModule({ 34 | imports: [ 35 | BrowserModule.withServerTransition({ 36 | appId: 'tabby', 37 | }), 38 | BrowserTransferStateModule, 39 | CommonAppModule.forRoot(), 40 | TransferHttpCacheModule, 41 | BrowserAnimationsModule, 42 | CommonModule, 43 | FormsModule, 44 | FontAwesomeModule, 45 | ClipboardModule, 46 | HttpClientModule, 47 | RouterModule.forRoot(ROUTES), 48 | ], 49 | declarations: [ 50 | AppComponent, 51 | ], 52 | bootstrap: [AppComponent], 53 | }) 54 | export class AppModule { } 55 | -------------------------------------------------------------------------------- /frontend/src/app.server.module.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-extraneous-class */ 2 | import { NgModule } from '@angular/core' 3 | import { ServerModule, ServerTransferStateModule } from '@angular/platform-server' 4 | import { AppModule } from './app.module' 5 | import { AppComponent } from './app.component' 6 | 7 | @NgModule({ 8 | imports: [ 9 | AppModule, 10 | ServerModule, 11 | ServerTransferStateModule, 12 | ], 13 | bootstrap: [AppComponent], 14 | }) 15 | export class AppServerModule {} 16 | -------------------------------------------------------------------------------- /frontend/src/app/components/configModal.component.pug: -------------------------------------------------------------------------------- 1 | .modal-header 2 | h5.modal-title Config file 3 | 4 | .modal-body 5 | .header(*ngIf='configService.activeConfig') 6 | .d-flex.align-items-center.py-2 7 | .me-auto 8 | label Active config 9 | .title 10 | fa-icon([icon]='_configIcon') 11 | span.ms-2 {{configService.activeConfig.name}} 12 | 13 | button.btn.btn-semi.me-2((click)='configService.duplicateActiveConfig()') 14 | fa-icon([icon]='_copyIcon', [fixedWidth]='true') 15 | 16 | button.btn.btn-semi((click)='deleteConfig()') 17 | fa-icon([icon]='_deleteIcon', [fixedWidth]='true') 18 | 19 | .d-flex.align-items-center.py-2(*ngIf='configService.activeVersion') 20 | .me-auto App version: 21 | div(ngbDropdown) 22 | button.btn.btn-semi(ngbDropdownToggle) {{configService.activeVersion.version}} 23 | div(ngbDropdownMenu) 24 | button( 25 | *ngFor='let version of configService.versions', 26 | ngbDropdownItem, 27 | [class.active]='version == configService.activeVersion', 28 | (click)='selectVersion(version)' 29 | ) {{version.version}} 30 | 31 | .pt-3(*ngIf='configService.configs.length > 1') 32 | h5 Other configs 33 | 34 | .list-group.list-group-light 35 | ng-container(*ngFor='let config of configService.configs') 36 | button.list-group-item.list-group-item-action( 37 | *ngIf='config.id !== configService.activeConfig?.id', 38 | (click)='selectConfig(config)' 39 | ) 40 | fa-icon([icon]='_configIcon') 41 | span {{config.name}} 42 | 43 | .py-3 44 | button.btn.btn-semi.w-100((click)='createNewConfig()') 45 | fa-icon([icon]='_addIcon', [fixedWidth]='true') 46 | span New config 47 | -------------------------------------------------------------------------------- /frontend/src/app/components/configModal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core' 2 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' 3 | import { AppConnectorService } from '../services/appConnector.service' 4 | import { ConfigService } from 'src/common' 5 | import { faCopy, faFile, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons' 6 | import { Config, Version } from 'src/api' 7 | 8 | @Component({ 9 | selector: 'config-modal', 10 | templateUrl: './configModal.component.pug', 11 | // styleUrls: ['./settingsModal.component.scss'], 12 | }) 13 | export class ConfigModalComponent { 14 | _addIcon = faPlus 15 | _copyIcon = faCopy 16 | _deleteIcon = faTrash 17 | _configIcon = faFile 18 | 19 | constructor ( 20 | private modalInstance: NgbActiveModal, 21 | public appConnector: AppConnectorService, 22 | public configService: ConfigService, 23 | ) { 24 | } 25 | 26 | cancel () { 27 | this.modalInstance.dismiss() 28 | } 29 | 30 | async createNewConfig () { 31 | const config = await this.configService.createNewConfig() 32 | await this.configService.selectConfig(config) 33 | this.modalInstance.dismiss() 34 | } 35 | 36 | async selectConfig (config: Config) { 37 | await this.configService.selectConfig(config) 38 | this.modalInstance.dismiss() 39 | } 40 | 41 | async selectVersion (version: Version) { 42 | await this.configService.selectVersion(version) 43 | this.modalInstance.dismiss() 44 | } 45 | 46 | async deleteConfig () { 47 | if (!this.configService.activeConfig) { 48 | return 49 | } 50 | if (confirm('Delete this config? This cannot be undone.')) { 51 | await this.configService.deleteConfig(this.configService.activeConfig) 52 | } 53 | this.configService.selectDefaultConfig() 54 | this.modalInstance.dismiss() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/app/components/connectionList.component.pug: -------------------------------------------------------------------------------- 1 | .list-group.list-group-light 2 | .list-group-item.d-flex(*ngFor='let socket of appConnector.sockets') 3 | fa-icon.text-success.me-2([icon]='_circleIcon', [fixedWidth]='true') 4 | .me-auto 5 | div {{socket.options.host}}:{{socket.options.port}} 6 | .text-muted via {{socket.url}} 7 | button.btn.btn-link((click)='closeSocket(socket)') 8 | fa-icon([icon]='_closeIcon', [fixedWidth]='true') 9 | -------------------------------------------------------------------------------- /frontend/src/app/components/connectionList.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core' 2 | import { AppConnectorService, SocketProxy } from '../services/appConnector.service' 3 | import { faCircle, faTimes } from '@fortawesome/free-solid-svg-icons' 4 | 5 | @Component({ 6 | selector: 'connection-list', 7 | templateUrl: './connectionList.component.pug', 8 | }) 9 | export class ConnectionListComponent { 10 | _circleIcon = faCircle 11 | _closeIcon = faTimes 12 | 13 | constructor ( 14 | public appConnector: AppConnectorService, 15 | ) { } 16 | 17 | closeSocket (socket: SocketProxy) { 18 | socket.close(new Error('Connection closed by user')) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/app/components/main.component.pug: -------------------------------------------------------------------------------- 1 | .sidebar 2 | img.logo(src='{{_logo}}') 3 | 4 | button.btn.mt-auto( 5 | (click)='openConfig()', 6 | title='Manage configs' 7 | ) 8 | fa-icon([icon]='_configIcon', [fixedWidth]='true', size='lg') 9 | 10 | button.btn( 11 | (click)='openSettings()', 12 | *ngIf='loginService.user', 13 | title='Settings' 14 | ) 15 | fa-icon([icon]='_settingsIcon', [fixedWidth]='true', size='lg') 16 | 17 | a.btn.mt-3( 18 | href='/login', 19 | *ngIf='!loginService.user', 20 | title='Log in' 21 | ) 22 | fa-icon([icon]='_loginIcon', [fixedWidth]='true', size='lg') 23 | 24 | button.btn.mt-3( 25 | (click)='logout()', 26 | *ngIf='loginService.user', 27 | title='Log out' 28 | ) 29 | fa-icon([icon]='_logoutIcon', [fixedWidth]='true', size='lg') 30 | 31 | .terminal 32 | iframe(#iframe, [hidden]='!showApp') 33 | .alert.alert-warning.d-flex.border-0.m-0(*ngIf='noVersionsAdded') 34 | fa-icon.me-2([icon]='_warningIcon', [fixedWidth]='true') 35 | div 36 | div You haven't added any Tabby versions, see here #[a(href='https://github.com/Eugeny/tabby-web#adding-tabby-app-versions', target='_blank') for instructions]. 37 | .alert.alert-warning.d-flex.border-0.m-0(*ngIf='missingVersion') 38 | fa-icon.me-2([icon]='_warningIcon', [fixedWidth]='true') 39 | div 40 | div You've last used this config with Tabby v{{missingVersion}}, which is no longer available on this server. 41 | .alert.alert-warning.d-flex.border-0.m-0(*ngIf='showApp && !loginService.user') 42 | fa-icon.me-2([icon]='_saveIcon', [fixedWidth]='true') 43 | div 44 | div To save profiles and settings, #[a(href='/login') log in]. 45 | .alert.alert-warning.d-flex.border-0.m-0(*ngIf='showDiscontinuationWarning') 46 | fa-icon.me-2([icon]='_warningIcon', [fixedWidth]='true') 47 | div 48 | div app.tabby.sh will be shut down in September due to lack of funding, but you can run your own instance - check out the #[a(href='https://github.com/eugeny/tabby-web', target='_blank') tabby-web repo]. Make sure to copy your configuration as it won't be available later. 49 | -------------------------------------------------------------------------------- /frontend/src/app/components/main.component.scss: -------------------------------------------------------------------------------- 1 | @import "~theme/vars"; 2 | 3 | :host { 4 | position: absolute; 5 | left: 0; 6 | top: 0; 7 | width: 100vw; 8 | height: 100vh; 9 | overflow: hidden; 10 | display: flex; 11 | } 12 | 13 | .sidebar { 14 | width: 64px; 15 | flex: none; 16 | display: flex; 17 | flex-direction: column; 18 | align-items: stretch; 19 | 20 | .logo { 21 | width: 32px; 22 | height: 32px; 23 | align-self: center; 24 | margin-top: 15px; 25 | margin-bottom: 20px; 26 | } 27 | 28 | >.btn { 29 | width: 64px; 30 | height: 64px; 31 | background: transparent; 32 | box-shadow: none; 33 | 34 | &::after { 35 | display: none; 36 | } 37 | 38 | &:hover { 39 | color: white; 40 | } 41 | } 42 | } 43 | 44 | .terminal { 45 | flex: 1 1 0; 46 | overflow: hidden; 47 | position: relative; 48 | display: flex; 49 | flex-direction: column; 50 | 51 | > * { 52 | flex: none; 53 | } 54 | 55 | > iframe { 56 | background: $body-bg; 57 | border: none; 58 | flex: 1 1 0; 59 | } 60 | } 61 | 62 | .config-menu { 63 | .header { 64 | border-bottom: 1px solid black; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /frontend/src/app/components/main.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, ViewChild } from '@angular/core' 2 | import { HttpClient } from '@angular/common/http' 3 | import { Title } from '@angular/platform-browser' 4 | import { AppConnectorService } from '../services/appConnector.service' 5 | 6 | import { faCog, faExclamationTriangle, faFile, faPlus, faSave, faSignInAlt, faSignOutAlt } from '@fortawesome/free-solid-svg-icons' 7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 8 | import { SettingsModalComponent } from './settingsModal.component' 9 | import { ConfigModalComponent } from './configModal.component' 10 | import { ConfigService, LoginService } from 'src/common' 11 | import { combineLatest } from 'rxjs' 12 | import { Config, Version } from 'src/api' 13 | 14 | @Component({ 15 | selector: 'main', 16 | templateUrl: './main.component.pug', 17 | styleUrls: ['./main.component.scss'], 18 | }) 19 | export class MainComponent { 20 | _logo = require('../../../assets/logo.svg') 21 | _settingsIcon = faCog 22 | _loginIcon = faSignInAlt 23 | _logoutIcon = faSignOutAlt 24 | _addIcon = faPlus 25 | _configIcon = faFile 26 | _saveIcon = faSave 27 | _warningIcon = faExclamationTriangle 28 | 29 | showApp = false 30 | noVersionsAdded = false 31 | missingVersion: string|undefined 32 | showDiscontinuationWarning = location.hostname === 'app.tabby.sh' 33 | 34 | @ViewChild('iframe') iframe: ElementRef 35 | 36 | constructor ( 37 | titleService: Title, 38 | public appConnector: AppConnectorService, 39 | private http: HttpClient, 40 | public loginService: LoginService, 41 | private ngbModal: NgbModal, 42 | private config: ConfigService, 43 | ) { 44 | titleService.setTitle('Tabby') 45 | window.addEventListener('message', this.connectorRequestHandler) 46 | config.ready$.subscribe(() => { 47 | this.noVersionsAdded = config.configs.length === 0 48 | }) 49 | } 50 | 51 | connectorRequestHandler = event => { 52 | if (event.data === 'request-connector') { 53 | this.iframe.nativeElement.contentWindow['__connector__'] = this.appConnector 54 | this.iframe.nativeElement.contentWindow.postMessage('connector-ready', '*') 55 | } 56 | } 57 | 58 | async ngAfterViewInit () { 59 | await this.loginService.ready$.toPromise() 60 | 61 | combineLatest( 62 | this.config.activeConfig$, 63 | this.config.activeVersion$ 64 | ).subscribe(([config, version]) => { 65 | this.reloadApp(config, version) 66 | }) 67 | 68 | await this.config.ready$.toPromise() 69 | await this.config.selectDefaultConfig() 70 | } 71 | 72 | ngOnDestroy () { 73 | window.removeEventListener('message', this.connectorRequestHandler) 74 | } 75 | 76 | unloadApp () { 77 | this.showApp = false 78 | this.iframe.nativeElement.src = 'about:blank' 79 | } 80 | 81 | async loadApp (config, version) { 82 | this.showApp = true 83 | this.iframe.nativeElement.src = '/terminal' 84 | if (this.loginService.user) { 85 | await this.http.patch(`/api/1/configs/${config.id}`, { 86 | last_used_with_version: version.version, 87 | }).toPromise() 88 | } 89 | } 90 | 91 | reloadApp (config: Config, version?: Version) { 92 | if (!version) { 93 | this.missingVersion = config.last_used_with_version 94 | version = this.config.getLatestStableVersion() 95 | if (!version) { 96 | return 97 | } 98 | } 99 | this.missingVersion = undefined 100 | // TODO check config incompatibility 101 | setTimeout(() => { 102 | this.appConnector.setState(config, version!) 103 | this.loadApp(config, version!) 104 | }) 105 | } 106 | 107 | async openConfig () { 108 | await this.ngbModal.open(ConfigModalComponent).result 109 | } 110 | 111 | async openSettings () { 112 | await this.ngbModal.open(SettingsModalComponent).result 113 | } 114 | 115 | async logout () { 116 | await this.http.post('/api/1/auth/logout', null).toPromise() 117 | location.href = '/' 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /frontend/src/app/components/settingsModal.component.pug: -------------------------------------------------------------------------------- 1 | .modal-header 2 | h3.modal-title Settings 3 | 4 | .modal-body 5 | .mb-3 6 | h5 GitHub account 7 | a.btn.btn-info(href='{{commonService.backendURL}}/api/1/auth/social/login/github', *ngIf='!user.github_username') 8 | fa-icon([icon]='_githubIcon', [fixedWidth]='true') 9 | span Connect a GitHub account 10 | .alert.alert-success.d-flex(*ngIf='user.github_username') 11 | fa-icon.me-2([icon]='_okIcon', [fixedWidth]='true') 12 | div 13 | div Connected as #[strong {{user.github_username}}] 14 | div(*ngIf='user.is_sponsor') Thank you for supporting Tabby on GitHub! 15 | 16 | .mb-3.mt-4 17 | h5 Config sync 18 | .d-flex.aling-items-stretch.mb-3 19 | .form-floating.w-100 20 | input.form-control( 21 | type='text', 22 | readonly, 23 | [ngModel]='user.config_sync_token' 24 | ) 25 | label Sync token for the Tabby app 26 | button.btn.btn-dark([cdkCopyToClipboard]='user.config_sync_token') 27 | fa-icon([icon]='_copyIcon', [fixedWidth]='true') 28 | 29 | .mb-3.mt-4 30 | h5 Connection gateway 31 | .form-check.form-switch 32 | input.form-check-input( 33 | type='checkbox', 34 | [(ngModel)]='customGatewayEnabled' 35 | ) 36 | label(class='form-check-label') Use a custom connection gateway 37 | 38 | small.text-muted This allows you to securely route connections through your own hosted gateway. See #[a(href='https://github.com/Eugeny/tabby-connection-gateway#readme', target='_blank') tabby-connection-gateway] for setup instructions. 39 | 40 | form 41 | input.d-none(type='text', name='fakeusername') 42 | input.d-none(type='password', name='fakepassword') 43 | 44 | .mb-3(*ngIf='customGatewayEnabled') 45 | .form-floating 46 | input.form-control( 47 | type='text', 48 | name='custom_connection_gateway', 49 | [(ngModel)]='user.custom_connection_gateway', 50 | placeholder='wss://1.2.3.4', 51 | autocomplete='off' 52 | ) 53 | label Gateway address 54 | 55 | .mb-3(*ngIf='customGatewayEnabled') 56 | .form-floating 57 | input.form-control( 58 | type='password', 59 | name='custom_connection_gateway_token', 60 | [(ngModel)]='user.custom_connection_gateway_token', 61 | placeholder='123', 62 | autocomplete='new-password' 63 | ) 64 | label Gateway authentication token 65 | 66 | .mb-3.mt-4(*ngIf='appConnector.sockets.length') 67 | h5 Active connections 68 | connection-list 69 | 70 | .mb-3.mt-4 71 | h5 About 72 | a.btn.btn-secondary.me-2(href='https://github.com/eugeny/tabby-web', target='_blank') 73 | fa-icon([icon]='_githubIcon', [fixedWidth]='true') 74 | span eugeny/tabby-web 75 | a.btn.btn-secondary.me-2(href='https://github.com/eugeny/tabby', target='_blank') 76 | fa-icon([icon]='_githubIcon', [fixedWidth]='true') 77 | span eugeny/tabby 78 | 79 | .modal-footer 80 | .text-muted Account ID: {{user.id}} 81 | .ms-auto 82 | button.btn.btn-primary((click)='apply()') Apply 83 | button.btn.btn-secondary((click)='cancel()') Cancel 84 | -------------------------------------------------------------------------------- /frontend/src/app/components/settingsModal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core' 2 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' 3 | import { User } from 'src/api' 4 | import { CommonService, LoginService } from 'src/common' 5 | import { AppConnectorService } from '../services/appConnector.service' 6 | import { faGithub } from '@fortawesome/free-brands-svg-icons' 7 | import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons' 8 | 9 | @Component({ 10 | selector: 'settings-modal', 11 | templateUrl: './settingsModal.component.pug', 12 | }) 13 | export class SettingsModalComponent { 14 | user: User 15 | customGatewayEnabled = false 16 | _githubIcon = faGithub 17 | _copyIcon = faCopy 18 | _okIcon = faCheck 19 | 20 | constructor ( 21 | public appConnector: AppConnectorService, 22 | public commonService: CommonService, 23 | private modalInstance: NgbActiveModal, 24 | private loginService: LoginService, 25 | ) { 26 | if (!loginService.user) { 27 | return 28 | } 29 | this.user = { ...loginService.user } 30 | this.customGatewayEnabled = !!this.user.custom_connection_gateway 31 | } 32 | 33 | async apply () { 34 | Object.assign(this.loginService.user, this.user) 35 | this.modalInstance.close() 36 | await this.loginService.updateUser() 37 | } 38 | 39 | cancel () { 40 | this.modalInstance.dismiss() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/app/components/upgradeModal.component.pug: -------------------------------------------------------------------------------- 1 | .modal-header 2 | h1.modal-title Hey! 3 | 4 | .modal-body 5 | h4 It looks like you're enjoying Tabby a lot! 6 | 7 | p Tabby Web has a limit of {{appConnector.connectionLimit}} simultaneous connections due to the fact that I have to pay for hosting and traffic out of my own pocket. 8 | 9 | p #[strong You can have unlimited parallel connections] if you support Tabby on GitHub with #[code $3]/month or more. It's cancellable anytime, there are no hidden costs and it helps me pay my bills. 10 | 11 | a.btn.btn-primary.btn-lg.d-block.mb-3(href='https://github.com/sponsors/Eugeny', target='_blank') 12 | fa-icon.me-2([icon]='_loveIcon') 13 | span Support Tabby on GitHub 14 | 15 | button.btn.btn-warning.d-block.w-100((click)='skipOnce()', *ngIf='canSkip') 16 | fa-icon.me-2([icon]='_giftIcon') 17 | span Skip - just this one time 18 | 19 | p.mt-3 If you work in education, have already supported me on Ko-fi before, or your country isn't supported on GitHub Sponsors, just #[a(href='mailto:e@ajenti.org?subject=Help with Tabby Pro') let me know] and I'll hook you up. 20 | 21 | .mb-3(*ngIf='!loginService.user?.github_username') 22 | a.btn.btn-info(href='{{commonService.backendURL}}/api/1/auth/social/login/github') 23 | fa-icon([icon]='_githubIcon', [fixedWidth]='true') 24 | span Connect your GitHub account to link your sponsorship 25 | 26 | .mt-4 27 | p You can also kill any active connection from the list below to free up a slot. 28 | connection-list 29 | -------------------------------------------------------------------------------- /frontend/src/app/components/upgradeModal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core' 2 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' 3 | import { faGithub } from '@fortawesome/free-brands-svg-icons' 4 | import { faGift, faHeart } from '@fortawesome/free-solid-svg-icons' 5 | 6 | import { AppConnectorService } from '../services/appConnector.service' 7 | import { CommonService, LoginService } from 'src/common' 8 | import { User } from 'src/api' 9 | 10 | @Component({ 11 | selector: 'upgrade-modal', 12 | templateUrl: './upgradeModal.component.pug', 13 | }) 14 | export class UpgradeModalComponent { 15 | user: User 16 | _githubIcon = faGithub 17 | _loveIcon = faHeart 18 | _giftIcon = faGift 19 | canSkip = false 20 | 21 | constructor ( 22 | public appConnector: AppConnectorService, 23 | public commonService: CommonService, 24 | public loginService: LoginService, 25 | private modalInstance: NgbActiveModal, 26 | ) { 27 | this.canSkip = !window.localStorage['upgrade-modal-skipped'] 28 | } 29 | 30 | skipOnce () { 31 | window.localStorage['upgrade-modal-skipped'] = true 32 | window.sessionStorage['upgrade-skip-active'] = true 33 | this.modalInstance.close(true) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/app/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-extraneous-class */ 2 | import { NgModule } from '@angular/core' 3 | import { NgbDropdownModule, NgbModalModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' 4 | import { CommonModule } from '@angular/common' 5 | import { FormsModule } from '@angular/forms' 6 | import { RouterModule } from '@angular/router' 7 | import { ClipboardModule } from '@angular/cdk/clipboard' 8 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome' 9 | 10 | import { MainComponent } from './components/main.component' 11 | import { ConfigModalComponent } from './components/configModal.component' 12 | import { SettingsModalComponent } from './components/settingsModal.component' 13 | import { ConnectionListComponent } from './components/connectionList.component' 14 | import { UpgradeModalComponent } from './components/upgradeModal.component' 15 | import { CommonAppModule } from 'src/common' 16 | 17 | const ROUTES = [ 18 | { 19 | path: '', 20 | component: MainComponent, 21 | }, 22 | ] 23 | 24 | @NgModule({ 25 | imports: [ 26 | CommonAppModule, 27 | CommonModule, 28 | FormsModule, 29 | NgbDropdownModule, 30 | NgbModalModule, 31 | NgbTooltipModule, 32 | ClipboardModule, 33 | FontAwesomeModule, 34 | RouterModule.forChild(ROUTES), 35 | ], 36 | declarations: [ 37 | MainComponent, 38 | ConfigModalComponent, 39 | SettingsModalComponent, 40 | ConnectionListComponent, 41 | UpgradeModalComponent, 42 | ], 43 | }) 44 | export class ApplicationModule { } 45 | -------------------------------------------------------------------------------- /frontend/src/app/services/appConnector.service.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer' 2 | import { Subject } from 'rxjs' 3 | import { debounceTime } from 'rxjs/operators' 4 | import { HttpClient } from '@angular/common/http' 5 | import { Injectable, Injector, NgZone } from '@angular/core' 6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 7 | import { UpgradeModalComponent } from '../components/upgradeModal.component' 8 | import { Config, Gateway, Version } from 'src/api' 9 | import { LoginService, CommonService } from 'src/common' 10 | 11 | export interface ServiceMessage { 12 | _: string 13 | [k: string]: any 14 | } 15 | 16 | export class SocketProxy { 17 | connect$ = new Subject() 18 | data$ = new Subject() 19 | error$ = new Subject() 20 | close$ = new Subject() 21 | 22 | url: string 23 | authToken: string 24 | webSocket: WebSocket|null 25 | initialBuffers: any[] = [] 26 | options: { 27 | host: string 28 | port: number 29 | } 30 | 31 | private appConnector: AppConnectorService 32 | private loginService: LoginService 33 | private ngbModal: NgbModal 34 | private zone: NgZone 35 | 36 | constructor ( 37 | injector: Injector, 38 | ) { 39 | this.appConnector = injector.get(AppConnectorService) 40 | this.loginService = injector.get(LoginService) 41 | this.ngbModal = injector.get(NgbModal) 42 | this.zone = injector.get(NgZone) 43 | } 44 | 45 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 46 | async connect (options: any): Promise { 47 | if (!this.loginService.user?.is_pro && this.appConnector.sockets.length > this.appConnector.connectionLimit && !window.sessionStorage['upgrade-skip-active']) { 48 | let skipped = false 49 | try { 50 | skipped = await this.zone.run(() => this.ngbModal.open(UpgradeModalComponent)).result 51 | } catch { } 52 | if (!skipped) { 53 | this.close(new Error('Connection limit reached')) 54 | return 55 | } 56 | } 57 | 58 | this.options = options 59 | if (this.loginService.user?.custom_connection_gateway) { 60 | this.url = this.loginService.user.custom_connection_gateway 61 | } 62 | if (this.loginService.user?.custom_connection_gateway_token) { 63 | this.authToken = this.loginService.user.custom_connection_gateway_token 64 | } 65 | if (!this.url) { 66 | try { 67 | const gateway = await this.appConnector.chooseConnectionGateway() 68 | this.url = gateway.url 69 | this.authToken = gateway.auth_token 70 | } catch (err) { 71 | this.close(err) 72 | return 73 | } 74 | } 75 | try { 76 | this.webSocket = new WebSocket(this.url) 77 | } catch (err) { 78 | this.close(err) 79 | return 80 | } 81 | this.webSocket.onerror = () => { 82 | this.close(new Error(`Failed to connect to the connection gateway at ${this.url}`)) 83 | return 84 | } 85 | this.webSocket.onmessage = async event => { 86 | if (typeof event.data === 'string') { 87 | this.handleServiceMessage(JSON.parse(event.data)) 88 | } else { 89 | this.data$.next(Buffer.from(await event.data.arrayBuffer())) 90 | } 91 | } 92 | this.webSocket.onclose = () => { 93 | this.close() 94 | } 95 | } 96 | 97 | handleServiceMessage (msg: ServiceMessage): void { 98 | if (msg._ === 'hello') { 99 | this.sendServiceMessage({ 100 | _: 'hello', 101 | version: 1, 102 | auth_token: this.authToken, 103 | }) 104 | } else if (msg._ === 'ready') { 105 | this.sendServiceMessage({ 106 | _: 'connect', 107 | host: this.options.host, 108 | port: this.options.port, 109 | }) 110 | } else if (msg._ === 'connected') { 111 | this.connect$.next() 112 | this.connect$.complete() 113 | for (const b of this.initialBuffers) { 114 | this.webSocket?.send(b) 115 | } 116 | this.initialBuffers = [] 117 | } else if (msg._ === 'error') { 118 | console.error('Connection gateway error', msg) 119 | this.close(new Error(msg.details)) 120 | } else { 121 | console.warn('Unknown service message', msg) 122 | } 123 | } 124 | 125 | sendServiceMessage (msg: ServiceMessage): void { 126 | this.webSocket?.send(JSON.stringify(msg)) 127 | } 128 | 129 | write (chunk: Buffer): void { 130 | if (!this.webSocket?.readyState) { 131 | this.initialBuffers.push(chunk) 132 | } else { 133 | this.webSocket.send(chunk) 134 | } 135 | } 136 | 137 | close (error?: Error): void { 138 | this.webSocket?.close() 139 | if (error) { 140 | this.error$.next(error) 141 | } 142 | this.connect$.complete() 143 | this.data$.complete() 144 | this.error$.complete() 145 | this.close$.next() 146 | this.close$.complete() 147 | } 148 | } 149 | 150 | @Injectable({ providedIn: 'root' }) 151 | export class AppConnectorService { 152 | private configUpdate = new Subject() 153 | private config: Config 154 | private version: Version 155 | connectionLimit = 3 156 | sockets: SocketProxy[] = [] 157 | 158 | constructor ( 159 | private injector: Injector, 160 | private http: HttpClient, 161 | private commonService: CommonService, 162 | private zone: NgZone, 163 | private loginService: LoginService, 164 | ) { 165 | 166 | this.configUpdate.pipe(debounceTime(1000)).subscribe(async content => { 167 | if (this.loginService.user) { 168 | const result = await this.http.patch(`/api/1/configs/${this.config.id}`, { content }).toPromise() 169 | Object.assign(this.config, result) 170 | } 171 | }) 172 | } 173 | 174 | setState (config: Config, version: Version): void { 175 | this.config = config 176 | this.version = version 177 | } 178 | 179 | async loadConfig (): Promise { 180 | return this.config.content 181 | } 182 | 183 | async saveConfig (content: string): Promise { 184 | this.configUpdate.next(content) 185 | this.config.content = content 186 | } 187 | 188 | getAppVersion (): string { 189 | return this.version.version 190 | } 191 | 192 | getDistURL (): string { 193 | return this.commonService.backendURL + '/app-dist' 194 | } 195 | 196 | getPluginsToLoad (): string[] { 197 | const loadOrder = [ 198 | 'tabby-core', 199 | 'tabby-settings', 200 | 'tabby-terminal', 201 | 'tabby-ssh', 202 | 'tabby-community-color-schemes', 203 | 'tabby-web', 204 | ] 205 | 206 | return [ 207 | ...loadOrder.filter(x => this.version.plugins.includes(x)), 208 | ...this.version.plugins.filter(x => !loadOrder.includes(x)), 209 | ] 210 | } 211 | 212 | createSocket (): SocketProxy { 213 | return this.zone.run(() => { 214 | const socket = new SocketProxy(this.injector) 215 | this.sockets.push(socket) 216 | socket.close$.subscribe(() => { 217 | this.sockets = this.sockets.filter(x => x !== socket) 218 | }) 219 | return socket 220 | }) 221 | } 222 | 223 | async chooseConnectionGateway (): Promise { 224 | try { 225 | return await this.http.post('/api/1/gateways/choose', {}).toPromise() as Gateway 226 | } catch (err){ 227 | if (err.status === 503) { 228 | throw new Error('All connection gateways are unavailable right now') 229 | } 230 | throw err 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /frontend/src/common/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-extraneous-class */ 2 | import { ModuleWithProviders, NgModule } from '@angular/core' 3 | import { HttpClientXsrfModule, HTTP_INTERCEPTORS } from '@angular/common/http' 4 | import { BackendXsrfInterceptor, UniversalInterceptor } from './interceptor' 5 | 6 | @NgModule({ 7 | imports: [ 8 | HttpClientXsrfModule, 9 | ], 10 | }) 11 | export class CommonAppModule { 12 | static forRoot (): ModuleWithProviders { 13 | return { 14 | ngModule: CommonAppModule, 15 | providers: [ 16 | { provide: HTTP_INTERCEPTORS, useClass: UniversalInterceptor, multi: true }, 17 | { provide: HTTP_INTERCEPTORS, useClass: BackendXsrfInterceptor, multi: true }, 18 | ], 19 | } 20 | } 21 | } 22 | 23 | export { LoginService } from './services/login.service' 24 | export { ConfigService } from './services/config.service' 25 | export { CommonService } from './services/common.service' 26 | -------------------------------------------------------------------------------- /frontend/src/common/interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpXsrfTokenExtractor } from '@angular/common/http' 3 | import { Observable } from 'rxjs' 4 | import { CommonService } from './services/common.service' 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class UniversalInterceptor implements HttpInterceptor { 8 | constructor (private commonService: CommonService) { } 9 | 10 | intercept (request: HttpRequest, next: HttpHandler): Observable> { 11 | if (!request.url.startsWith('//') && request.url.startsWith('/')) { 12 | const endpoint = request.url 13 | request = request.clone({ 14 | url: `${this.commonService.backendURL}${endpoint}`, 15 | withCredentials: true, 16 | }) 17 | } 18 | return next.handle(request) 19 | } 20 | } 21 | 22 | @Injectable({ providedIn: 'root' }) 23 | export class BackendXsrfInterceptor implements HttpInterceptor { 24 | constructor ( 25 | private commonService: CommonService, 26 | private tokenExtractor: HttpXsrfTokenExtractor, 27 | ) { } 28 | 29 | intercept (req: HttpRequest, next: HttpHandler): Observable> { 30 | if (this.commonService.backendURL && req.url.startsWith(this.commonService.backendURL)) { 31 | const token = this.tokenExtractor.getToken() 32 | if (token !== null) { 33 | req = req.clone({ setHeaders: { 'X-XSRF-TOKEN': token } }) 34 | } 35 | } 36 | return next.handle(req) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/common/services/common.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, Optional } from '@angular/core' 2 | 3 | @Injectable({ providedIn: 'root' }) 4 | export class CommonService { 5 | backendURL: string 6 | 7 | constructor (@Inject('BACKEND_URL') @Optional() ssrBackendURL: string) { 8 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 9 | const tag = document.querySelector('meta[property=x-tabby-web-backend-url]')! as HTMLMetaElement 10 | if (ssrBackendURL) { 11 | this.backendURL = ssrBackendURL 12 | tag.content = ssrBackendURL 13 | } else { 14 | if (tag.content && !tag.content.startsWith('{{')) { 15 | this.backendURL = tag.content 16 | } else { 17 | this.backendURL = '' 18 | } 19 | } 20 | 21 | console.log(this.backendURL) 22 | if (this.backendURL.endsWith('/')) { 23 | this.backendURL = this.backendURL.slice(0, -1) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/common/services/config.service.ts: -------------------------------------------------------------------------------- 1 | import * as semverCompare from 'semver/functions/compare-loose' 2 | import { AsyncSubject, Subject } from 'rxjs' 3 | import { HttpClient } from '@angular/common/http' 4 | import { Injectable } from '@angular/core' 5 | import { Config, User, Version } from '../../api' 6 | import { LoginService } from './login.service' 7 | 8 | 9 | @Injectable({ providedIn: 'root' }) 10 | export class ConfigService { 11 | activeConfig$ = new Subject() 12 | activeVersion$ = new Subject() 13 | user: User 14 | 15 | configs: Config[] = [] 16 | versions: Version[] = [] 17 | ready$ = new AsyncSubject() 18 | 19 | get activeConfig (): Config | null { return this._activeConfig } 20 | get activeVersion (): Version | null { return this._activeVersion } 21 | 22 | private _activeConfig: Config|null = null 23 | private _activeVersion: Version|null = null 24 | 25 | constructor ( 26 | private http: HttpClient, 27 | private loginService: LoginService, 28 | ) { 29 | this.init() 30 | } 31 | 32 | async updateUser (): Promise { 33 | if (!this.loginService.user) { 34 | return 35 | } 36 | await this.http.put('/api/1/user', this.user).toPromise() 37 | } 38 | 39 | async createNewConfig (): Promise { 40 | const configData = { 41 | content: '{}', 42 | last_used_with_version: this._activeVersion?.version ?? this.getLatestStableVersion()?.version ?? '0.0.0', 43 | } 44 | if (!this.loginService.user) { 45 | const config = { 46 | id: Date.now(), 47 | name: `Temporary config at ${new Date()}`, 48 | created_at: new Date(), 49 | modified_at: new Date(), 50 | ...configData, 51 | } 52 | this.configs.push(config) 53 | return config 54 | } 55 | const config = (await this.http.post('/api/1/configs', configData).toPromise()) as Config 56 | this.configs.push(config) 57 | return config 58 | } 59 | 60 | getLatestStableVersion (): Version|undefined { 61 | return this.versions[0] 62 | } 63 | 64 | async duplicateActiveConfig (): Promise { 65 | let copy: any = { ...this._activeConfig, id: undefined } 66 | if (this.loginService.user) { 67 | copy = (await this.http.post('/api/1/configs', copy).toPromise()) as Config 68 | } 69 | this.configs.push(copy) 70 | } 71 | 72 | async selectVersion (version: Version): Promise { 73 | this._activeVersion = version 74 | this.activeVersion$.next(version) 75 | } 76 | 77 | async selectConfig (config: Config): Promise { 78 | let matchingVersion = this.versions.find(x => x.version === config.last_used_with_version) 79 | if (!matchingVersion) { 80 | // TODO ask to upgrade 81 | matchingVersion = this.versions[0] 82 | } 83 | 84 | this._activeConfig = config 85 | this.activeConfig$.next(config) 86 | this.selectVersion(matchingVersion) 87 | if (this.loginService.user) { 88 | this.loginService.user.active_config = config.id 89 | await this.loginService.updateUser() 90 | } 91 | } 92 | 93 | async selectDefaultConfig (): Promise { 94 | await this.ready$.toPromise() 95 | await this.loginService.ready$.toPromise() 96 | this.selectConfig(this.configs.find(c => c.id === this.loginService.user?.active_config) ?? this.configs[0]) 97 | } 98 | 99 | async deleteConfig (config: Config): Promise { 100 | if (this.loginService.user) { 101 | await this.http.delete(`/api/1/configs/${config.id}`).toPromise() 102 | } 103 | this.configs = this.configs.filter(x => x.id !== config.id) 104 | } 105 | 106 | private async init () { 107 | await this.loginService.ready$.toPromise() 108 | 109 | if (this.loginService.user) { 110 | this.configs = (await this.http.get('/api/1/configs').toPromise()) as Config[] 111 | } 112 | this.versions = (await this.http.get('/api/1/versions').toPromise()) as Version[] 113 | this.versions.sort((a, b) => -semverCompare(a.version, b.version)) 114 | 115 | if (!this.configs.length && this.versions.length) { 116 | await this.createNewConfig() 117 | } 118 | 119 | this.ready$.next() 120 | this.ready$.complete() 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /frontend/src/common/services/login.service.ts: -------------------------------------------------------------------------------- 1 | import { AsyncSubject } from 'rxjs' 2 | import { HttpClient } from '@angular/common/http' 3 | import { Injectable } from '@angular/core' 4 | import { User } from '../../api' 5 | 6 | 7 | @Injectable({ providedIn: 'root' }) 8 | export class LoginService { 9 | user: User | null 10 | ready$ = new AsyncSubject() 11 | 12 | constructor (private http: HttpClient) { 13 | this.init() 14 | } 15 | 16 | async updateUser (): Promise { 17 | if (!this.user) { 18 | return 19 | } 20 | await this.http.put('/api/1/user', this.user).toPromise() 21 | } 22 | 23 | private async init () { 24 | try { 25 | this.user = (await this.http.get('/api/1/user').toPromise()) as User 26 | } catch { 27 | this.user = null 28 | } 29 | 30 | this.ready$.next() 31 | this.ready$.complete() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /frontend/src/demo.ts: -------------------------------------------------------------------------------- 1 | import * as semverCompare from 'semver/functions/compare-loose' 2 | import { Subject } from 'rxjs' 3 | import { Version } from './api' 4 | 5 | class DemoConnector { 6 | constructor ( 7 | window: Window, 8 | private version: Version, 9 | ) { 10 | window['tabbyWebDemoDataPath'] = `${this.getDistURL()}/${version.version}/tabby-web-demo/data` 11 | } 12 | 13 | async loadConfig (): Promise { 14 | return `{ 15 | recoverTabs: false, 16 | web: { 17 | preventAccidentalTabClosure: false, 18 | }, 19 | terminal: { 20 | fontSize: 11, 21 | }, 22 | }` 23 | } 24 | 25 | // eslint-disable-next-line @typescript-eslint/no-empty-function 26 | async saveConfig (_content: string): Promise { } 27 | 28 | getAppVersion (): string { 29 | return this.version.version 30 | } 31 | 32 | getDistURL (): string { 33 | return 'https://api.tabby.sh/app-dist' 34 | } 35 | 36 | getPluginsToLoad (): string[] { 37 | return [ 38 | 'tabby-core', 39 | 'tabby-settings', 40 | 'tabby-terminal', 41 | 'tabby-community-color-schemes', 42 | 'tabby-ssh', 43 | 'tabby-telnet', 44 | 'tabby-web', 45 | 'tabby-web-demo', 46 | ] 47 | } 48 | 49 | createSocket () { 50 | return new DemoSocketProxy() 51 | } 52 | } 53 | 54 | export class DemoSocketProxy { 55 | connect$ = new Subject() 56 | data$ = new Subject() 57 | error$ = new Subject() 58 | close$ = new Subject() 59 | 60 | async connect () { 61 | this.error$.next(new Error('This web demo can\'t actually access Internet, but feel free to download the release and try it out!')) 62 | } 63 | } 64 | 65 | // eslint-disable-next-line @typescript-eslint/init-declarations 66 | let iframe: HTMLIFrameElement 67 | // eslint-disable-next-line @typescript-eslint/init-declarations 68 | let version: Version 69 | 70 | const connectorRequestHandler = event => { 71 | if (event.data === 'request-connector') { 72 | iframe.contentWindow!['__connector__'] = new DemoConnector(iframe.contentWindow!, version) 73 | iframe.contentWindow!.postMessage('connector-ready', '*') 74 | } 75 | } 76 | 77 | window.addEventListener('message', connectorRequestHandler) 78 | 79 | document.addEventListener('DOMContentLoaded', async () => { 80 | iframe = document.querySelector('iframe')! 81 | const versions = (await fetch('https://api.tabby.sh/api/1/versions').then(x => x.json())) as Version[] 82 | versions.sort((a, b) => -semverCompare(a.version, b.version)) 83 | version = versions[0]! 84 | iframe.src = '/terminal' 85 | }) 86 | -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Tabby - a terminal for a more modern age 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /frontend/src/index.server.ts: -------------------------------------------------------------------------------- 1 | import './styles.scss' 2 | export { AppServerModule } from './app.server.module' 3 | -------------------------------------------------------------------------------- /frontend/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js' 2 | import 'core-js/proposals/reflect-metadata' 3 | import 'core-js/features/array/flat' 4 | import 'rxjs' 5 | 6 | import { enableProdMode } from '@angular/core' 7 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' 8 | 9 | import './styles.scss' 10 | import { AppModule } from './app.module' 11 | 12 | if (!location.hostname.endsWith('.local')) { 13 | enableProdMode() 14 | } 15 | 16 | document.addEventListener('DOMContentLoaded', () => { 17 | platformBrowserDynamic().bootstrapModule(AppModule) 18 | }) 19 | -------------------------------------------------------------------------------- /frontend/src/login/components/login.component.pug: -------------------------------------------------------------------------------- 1 | .login-view(*ngIf='ready') 2 | .buttons 3 | a.btn( 4 | *ngFor='let provider of providers', 5 | [class]='provider.cls', 6 | href='{{commonService.backendURL}}/api/1/auth/social/login/{{provider.id}}' 7 | ) 8 | fa-icon([icon]='provider.icon', [fixedWidth]='true') 9 | span Log in with {{provider.name}} 10 | -------------------------------------------------------------------------------- /frontend/src/login/components/login.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | position: absolute; 3 | left: 0; 4 | top: 0; 5 | width: 100vw; 6 | height: 100vh; 7 | overflow: hidden; 8 | display: flex; 9 | } 10 | 11 | .login-view { 12 | margin: auto; 13 | flex: none; 14 | } 15 | 16 | .buttons > * { 17 | min-width: 200px; 18 | display: flex; 19 | align-items: center; 20 | margin-bottom: 10px; 21 | 22 | >span { 23 | margin: auto; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/login/components/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core' 2 | import { LoginService, CommonService } from 'src/common' 3 | 4 | import { faGithub, faGitlab, faGoogle, faMicrosoft } from '@fortawesome/free-brands-svg-icons' 5 | 6 | @Component({ 7 | selector: 'login', 8 | templateUrl: './login.component.pug', 9 | styleUrls: ['./login.component.scss'], 10 | }) 11 | export class LoginComponent { 12 | loggedIn: any 13 | ready = false 14 | 15 | providers = [ 16 | { name: 'GitHub', icon: faGithub, cls: 'btn-primary', id: 'github' }, 17 | { name: 'GitLab', icon: faGitlab, cls: 'btn-warning', id: 'gitlab' }, 18 | { name: 'Google', icon: faGoogle, cls: 'btn-secondary', id: 'google-oauth2' }, 19 | { name: 'Microsoft', icon: faMicrosoft, cls: 'btn-light', id: 'microsoft-graph' }, 20 | ] 21 | 22 | constructor ( 23 | private loginService: LoginService, 24 | public commonService: CommonService, 25 | ) { } 26 | 27 | async ngOnInit () { 28 | await this.loginService.ready$.toPromise() 29 | this.loggedIn = !!this.loginService.user 30 | this.ready = true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/login/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-extraneous-class */ 2 | import { NgModule } from '@angular/core' 3 | import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap' 4 | import { CommonModule } from '@angular/common' 5 | import { FormsModule } from '@angular/forms' 6 | import { RouterModule } from '@angular/router' 7 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome' 8 | import { NgxImageZoomModule } from 'ngx-image-zoom' 9 | 10 | import { LoginComponent } from './components/login.component' 11 | import { CommonAppModule } from 'src/common' 12 | 13 | const ROUTES = [ 14 | { 15 | path: '', 16 | component: LoginComponent, 17 | }, 18 | ] 19 | 20 | @NgModule({ 21 | imports: [ 22 | CommonAppModule, 23 | CommonModule, 24 | FormsModule, 25 | NgbNavModule, 26 | FontAwesomeModule, 27 | NgxImageZoomModule, 28 | RouterModule.forChild(ROUTES), 29 | ], 30 | declarations: [ 31 | LoginComponent, 32 | ], 33 | }) 34 | export class LoginModule { } 35 | -------------------------------------------------------------------------------- /frontend/src/server.ts: -------------------------------------------------------------------------------- 1 | import { install } from 'source-map-support' 2 | import * as throng from 'throng' 3 | 4 | import 'zone.js/dist/zone-node' 5 | import './ssr-polyfills' 6 | 7 | import { enableProdMode } from '@angular/core' 8 | import { ngExpressEngine } from '@nguniversal/express-engine' 9 | 10 | import * as express from 'express' 11 | 12 | import { join } from 'path' 13 | 14 | 15 | install() 16 | enableProdMode() 17 | 18 | import { AppServerModule } from './app.server.module' 19 | 20 | const engine = ngExpressEngine({ 21 | bootstrap: AppServerModule, 22 | }) 23 | 24 | const hardlinks = { 25 | 'cwd-detection': 'https://github.com/Eugeny/tabby/wiki/Shell-working-directory-reporting', 26 | 'privacy-policy': 'https://github.com/Eugeny/tabby/wiki/Privacy-Policy-for-Tabby-Web', 27 | 'terms-of-use': 'https://github.com/Eugeny/tabby/wiki/Terms-of-Use-of-Tabby-Web', 28 | } 29 | 30 | function start () { 31 | const app = express() 32 | 33 | const PORT = process.env.PORT ?? 8000 34 | const DIST_FOLDER = join(process.cwd(), 'build') 35 | 36 | app.engine('html', engine) 37 | 38 | app.set('view engine', 'html') 39 | app.set('views', DIST_FOLDER) 40 | 41 | app.use('/static', express.static(DIST_FOLDER, { 42 | maxAge: '1y', 43 | })) 44 | 45 | app.get(['/', '/app', '/login', '/about', '/about/:_'], (req, res) => { 46 | res.render( 47 | 'index', 48 | { 49 | req, 50 | providers: [ 51 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 52 | { provide: 'BACKEND_URL', useValue: process.env.BACKEND_URL ?? '' }, 53 | ], 54 | }, 55 | (err?: Error, html?: string) => { 56 | if (html) { 57 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 58 | html = html.replace('{{backendURL}}', process.env.BACKEND_URL ?? '') 59 | } 60 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 61 | res.status(err ? 500 : 200).send(html ?? err!.message) 62 | }, 63 | ) 64 | }) 65 | 66 | app.get(['/terminal'], (req, res) => { 67 | res.sendFile(join(DIST_FOLDER, 'terminal.html')) 68 | }) 69 | 70 | app.get(['/demo'], (req, res) => { 71 | res.sendFile(join(DIST_FOLDER, 'demo.html')) 72 | }) 73 | 74 | for (const [key, value] of Object.entries(hardlinks)) { 75 | app.get(`/go/${key}`, (req, res) => res.redirect(value)) 76 | } 77 | 78 | process.umask(0o002) 79 | app.listen(PORT, () => { 80 | console.log(`Node Express server listening on http://localhost:${PORT}`) 81 | }) 82 | } 83 | 84 | const WORKERS = process.env.WEB_CONCURRENCY ?? 4 85 | throng({ 86 | workers: WORKERS, 87 | lifetime: Infinity, 88 | start, 89 | }) 90 | -------------------------------------------------------------------------------- /frontend/src/ssr-polyfills.ts: -------------------------------------------------------------------------------- 1 | import * as domino from 'domino' 2 | import * as fs from 'fs' 3 | import * as path from 'path' 4 | 5 | const template = fs.readFileSync(path.join(process.cwd(), 'build', 'index.html')).toString() 6 | const win = domino.createWindow(template) 7 | 8 | global['window'] = win 9 | 10 | Object.defineProperty(win.document.body.style, 'transform', { 11 | value: () => { 12 | return { 13 | enumerable: true, 14 | configurable: true, 15 | } 16 | }, 17 | }) 18 | 19 | Object.defineProperty(win.document.body.style, 'z-index', { 20 | value: () => { 21 | return { 22 | enumerable: true, 23 | configurable: true, 24 | } 25 | }, 26 | }) 27 | 28 | global['document'] = win.document 29 | global['CSS'] = null 30 | // global['atob'] = win.atob; 31 | global['atob'] = (base64: string) => { 32 | return Buffer.from(base64, 'base64').toString() 33 | } 34 | 35 | function setDomTypes () { 36 | // Make all Domino types available as types in the global env. 37 | Object.assign(global, domino['impl']); 38 | (global as any)['KeyboardEvent'] = domino['impl'].Event 39 | } 40 | 41 | setDomTypes() 42 | -------------------------------------------------------------------------------- /frontend/src/styles.scss: -------------------------------------------------------------------------------- 1 | $font-family-sans-serif: "Source Sans Pro"; 2 | $border-radius-lg: 0; 3 | $btn-border-width: 3px; 4 | 5 | body { 6 | overscroll-behavior: none; 7 | } 8 | 9 | @import "~source-code-pro/source-code-pro.css"; 10 | @import "~source-sans-pro/source-sans-pro.css"; 11 | 12 | @import "theme/index"; 13 | 14 | .btn-lg { 15 | border-radius: 100px; 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/terminal-styles.scss: -------------------------------------------------------------------------------- 1 | .pre-bootstrap-spinner { 2 | position: absolute; 3 | bottom: 20px; 4 | left: 50%; 5 | width: 50px; 6 | height: 20px; 7 | margin-left: -25px; 8 | 9 | .spinner { 10 | display: inline-block; 11 | position: relative; 12 | width: 10px; 13 | height: 10px; 14 | border-radius: 50%; 15 | background: #fff; 16 | animation: spinner ease infinite 1s; 17 | opacity: 0.7; 18 | 19 | &.n2 { 20 | animation-delay: -0.33s; 21 | } 22 | 23 | &.n3 { 24 | animation-delay: -0.66s; 25 | } 26 | } 27 | } 28 | 29 | @keyframes spinner { 30 | 0% { 31 | left: -20px; 32 | top: 0; 33 | } 34 | 35 | 50% { 36 | left: 20px; 37 | top: 0; 38 | } 39 | 40 | 100% { 41 | left: -20px; 42 | top: 0; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/terminal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 19 | 20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /frontend/src/terminal.ts: -------------------------------------------------------------------------------- 1 | import './terminal-styles.scss' 2 | 3 | async function start () { 4 | window['__filename'] = '' 5 | 6 | await new Promise(resolve => { 7 | window.addEventListener('message', event => { 8 | if (event.data === 'connector-ready') { 9 | resolve() 10 | } 11 | }) 12 | window.parent.postMessage('request-connector', '*') 13 | }) 14 | 15 | const connector = window['__connector__'] 16 | 17 | const appVersion = connector.getAppVersion() 18 | 19 | async function webRequire (url) { 20 | console.log(`Loading ${url}`) 21 | const e = document.createElement('script') 22 | window['module'] = { exports: {} } as any 23 | window['exports'] = window['module'].exports 24 | await new Promise(resolve => { 25 | e.onload = resolve 26 | e.src = url 27 | document.head.appendChild(e) 28 | }) 29 | return window['module'].exports 30 | } 31 | 32 | async function prefetchURL (url) { 33 | await (await fetch(url)).text() 34 | } 35 | 36 | const baseUrl = `${connector.getDistURL()}/${appVersion}` 37 | const coreURLs = [ 38 | `${baseUrl}/tabby-web-container/dist/preload.js`, 39 | `${baseUrl}/tabby-web-container/dist/bundle.js`, 40 | ] 41 | 42 | await Promise.all(coreURLs.map(prefetchURL)) 43 | 44 | for (const url of coreURLs) { 45 | await webRequire(url) 46 | } 47 | 48 | document.querySelector('app-root')!['style'].display = 'flex' 49 | 50 | const tabby = window['Tabby'] 51 | 52 | const pluginURLs = connector.getPluginsToLoad().map(x => `${baseUrl}/${x}`) 53 | const pluginModules = await tabby.loadPlugins(pluginURLs, (current, total) => { 54 | (document.querySelector('.progress .bar') as HTMLElement).style.width = `${100 * current / total}%` // eslint-disable-line 55 | }) 56 | 57 | const config = connector.loadConfig() 58 | await tabby.bootstrap({ 59 | packageModules: pluginModules, 60 | bootstrapData: { 61 | config, 62 | executable: 'web', 63 | isFirstWindow: true, 64 | windowID: 1, 65 | installedPlugins: [], 66 | userPluginsPath: '/', 67 | }, 68 | debugMode: false, 69 | connector, 70 | }) 71 | } 72 | 73 | const spinner = document.body.querySelector('.pre-bootstrap-spinner') 74 | start().finally(() => { 75 | spinner?.remove() 76 | }) 77 | -------------------------------------------------------------------------------- /frontend/theme/index.scss: -------------------------------------------------------------------------------- 1 | @import "vars"; 2 | 3 | @import "~bootstrap/scss/functions"; 4 | @import "~bootstrap/scss/variables"; 5 | @import "~bootstrap/scss/mixins"; 6 | @import "~bootstrap/scss/utilities"; 7 | 8 | @import "~bootstrap/scss/root"; 9 | @import "~bootstrap/scss/reboot"; 10 | @import "~bootstrap/scss/type"; 11 | // @import "~bootstrap/scss/images"; 12 | @import "~bootstrap/scss/containers"; 13 | @import "~bootstrap/scss/grid"; 14 | // @import "~bootstrap/scss/tables"; 15 | @import "~bootstrap/scss/forms"; 16 | @import "~bootstrap/scss/buttons"; 17 | @import "~bootstrap/scss/transitions"; 18 | @import "~bootstrap/scss/dropdown"; 19 | @import "~bootstrap/scss/button-group"; 20 | @import "~bootstrap/scss/nav"; 21 | // @import "~bootstrap/scss/navbar"; 22 | @import "~bootstrap/scss/card"; 23 | // @import "~bootstrap/scss/accordion"; 24 | // @import "~bootstrap/scss/breadcrumb"; 25 | // @import "~bootstrap/scss/pagination"; 26 | @import "~bootstrap/scss/badge"; 27 | @import "~bootstrap/scss/alert"; 28 | // @import "~bootstrap/scss/progress"; 29 | @import "~bootstrap/scss/list-group"; 30 | // @import "~bootstrap/scss/close"; 31 | // @import "~bootstrap/scss/toasts"; 32 | @import "~bootstrap/scss/modal"; 33 | // @import "~bootstrap/scss/tooltip"; 34 | // @import "~bootstrap/scss/popover"; 35 | // @import "~bootstrap/scss/carousel"; 36 | // @import "~bootstrap/scss/spinners"; 37 | // @import "~bootstrap/scss/offcanvas"; 38 | 39 | // Helpers 40 | @import "~bootstrap/scss/helpers"; 41 | @import "~bootstrap/scss/utilities/api"; 42 | 43 | ::-webkit-scrollbar-track 44 | { 45 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); 46 | background-color: $gray-900; 47 | } 48 | 49 | ::-webkit-scrollbar 50 | { 51 | height: 6px; 52 | width: 6px; 53 | background-color: #F5F5F5; 54 | } 55 | 56 | ::-webkit-scrollbar-thumb 57 | { 58 | background-color: $gray-700; 59 | } 60 | 61 | body { 62 | min-height: 100vh; 63 | } 64 | 65 | 66 | .dropdown-menu { 67 | box-shadow: $dropdown-box-shadow; 68 | } 69 | 70 | .modal-header, .modal-body { 71 | padding: $modal-inner-padding $modal-inner-padding * 2; 72 | } 73 | 74 | .modal-footer { 75 | background: #00000030; 76 | } 77 | 78 | a, button { 79 | fa-icon { 80 | opacity: .75; 81 | } 82 | 83 | fa-icon + * { 84 | margin-left: 5px; 85 | } 86 | } 87 | 88 | lib-ngx-image-zoom { 89 | display: flex; 90 | } 91 | 92 | ngb-tooltip-window { 93 | z-index: 1; 94 | } 95 | -------------------------------------------------------------------------------- /frontend/theme/vars.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | $white: #fff; 4 | $gray-100: #f8f9fa; 5 | $gray-200: #e9ecef; 6 | $gray-300: #dee2e6; 7 | $gray-400: #ced4da; 8 | $gray-500: #adb5bd; 9 | $gray-600: #6c757d; 10 | $gray-700: #495057; 11 | $gray-800: #343a40; 12 | $gray-900: #212529; 13 | $black: #000; 14 | 15 | 16 | $red: #d9534f !default; 17 | $orange: #f0ad4e !default; 18 | $yellow: #ffd500 !default; 19 | $green: #5cb85c !default; 20 | $blue: #0275d8 !default; 21 | $teal: #5bc0de !default; 22 | $pink: #ff5b77 !default; 23 | $purple: #843cbb !default; 24 | $semi: rgba(0,0,0, .5); 25 | 26 | 27 | @import "~bootstrap/scss/functions"; 28 | 29 | $table-bg: rgba(255,255,255,.05); 30 | $table-bg-hover: rgba(255,255,255,.1); 31 | $table-border-color: rgba(255,255,255,.1); 32 | 33 | $theme-colors: ( 34 | primary: $blue, 35 | secondary: #38434e, 36 | success: $green, 37 | info: $blue, 38 | warning: $orange, 39 | danger: $red, 40 | light: $gray-300, 41 | dark: #0e151d, 42 | rare: $purple, 43 | semi: $semi 44 | ); 45 | 46 | $body-color: #ccc; 47 | $body-bg: #0c131b; 48 | 49 | $font-family-sans-serif: "Source Sans Pro"; 50 | $font-family-monospace: "Source Code Pro"; 51 | $font-size-base: math.div(14rem, 16); 52 | $font-size-lg: 1.28rem; 53 | $font-size-sm: .85rem; 54 | 55 | $line-height-base: 1.6; 56 | 57 | $border-radius: .35rem; 58 | $border-radius-lg: .35rem; 59 | $border-radius-sm: .2rem; 60 | 61 | $box-shadow: 0 .5rem 1rem rgba($black, .5) !default; 62 | 63 | // ----- 64 | 65 | $headings-color: #ced9e2; 66 | $headings-font-weight: lighter; 67 | 68 | $input-btn-padding-y: .3rem; 69 | $input-btn-padding-x: .9rem; 70 | $input-btn-line-height: 1.6; 71 | $input-btn-line-height-sm: 1.8; 72 | $input-btn-line-height-lg: 1.8; 73 | $btn-focus-width: 1px; 74 | 75 | $h4-font-size: 18px; 76 | 77 | $link-color: $gray-400; 78 | $link-hover-color: $white; 79 | $link-hover-decoration: none; 80 | 81 | $component-active-color: $white; 82 | $component-active-bg: $blue; 83 | 84 | $list-group-color: $body-color; 85 | $list-group-bg: $table-bg; 86 | $list-group-border-color: $table-border-color; 87 | 88 | $list-group-item-padding-y: 0.8rem; 89 | $list-group-item-padding-x: 1rem; 90 | 91 | $list-group-hover-bg: $table-bg-hover; 92 | $list-group-active-bg: rgba(255,255,255,.2); 93 | $list-group-active-color: $component-active-color; 94 | $list-group-active-border-color: translate; 95 | 96 | $list-group-action-color: $body-color; 97 | $list-group-action-hover-color: white; 98 | 99 | $list-group-action-active-color: $component-active-color; 100 | $list-group-action-active-bg: $list-group-active-bg; 101 | 102 | $alert-padding-y: 0.9rem; 103 | $alert-padding-x: 1.25rem; 104 | 105 | $transition-base: all .15s ease-in-out; 106 | $transition-fade: opacity .1s linear; 107 | $transition-collapse: height .35s ease; 108 | $btn-transition: all .15s ease-in-out; 109 | 110 | $popover-bg: $body-bg; 111 | $popover-body-color: $body-color; 112 | $popover-header-bg: $table-bg-hover; 113 | $popover-header-color: $headings-color; 114 | $popover-arrow-color: $popover-bg; 115 | $popover-max-width: 360px; 116 | 117 | $btn-border-width: 2px; 118 | 119 | $input-bg: $black; 120 | $input-disabled-bg: #2e3235; 121 | 122 | $input-color: #ddd; 123 | $input-border-color: $input-bg; 124 | $input-border-width: 2px; 125 | 126 | $input-focus-bg: $input-bg; 127 | $input-focus-border-color: rgba(171, 171, 171, 0.61); 128 | $input-focus-color: $input-color; 129 | 130 | $input-group-addon-color: $input-color; 131 | $input-group-addon-bg: $input-bg; 132 | $input-group-addon-border-color: transparent; 133 | $input-group-btn-border-color: $input-bg; 134 | 135 | $form-switch-color: rgba(255,255,255, .25); 136 | 137 | $nav-tabs-border-radius: 0; 138 | $nav-tabs-border-color: transparent; 139 | $nav-tabs-border-width: 2px; 140 | $nav-tabs-link-hover-border-color: transparent; 141 | $nav-tabs-link-active-color: #eee; 142 | $nav-tabs-link-active-bg: transparent; 143 | $nav-tabs-link-active-border-color: #eee; 144 | 145 | $nav-pills-link-active-bg: rgba(255, 255, 255, .125); 146 | 147 | $navbar-padding-y: 0; 148 | $navbar-padding-x: 0; 149 | 150 | $dropdown-bg: $body-bg; 151 | $dropdown-color: $body-color; 152 | $dropdown-border-width: 1px; 153 | $dropdown-border-color: #ffffff24; 154 | $dropdown-header-color: $gray-500; 155 | 156 | $dropdown-link-color: $body-color; 157 | $dropdown-link-hover-color: #eee; 158 | $dropdown-link-hover-bg: rgba(255,255,255,.04); 159 | $dropdown-link-active-color: white; 160 | $dropdown-link-active-bg: rgba(0, 0, 0, .2); 161 | $dropdown-item-padding-y: 0.5rem; 162 | $dropdown-item-padding-x: 1.5rem; 163 | 164 | 165 | $code-color: $orange; 166 | $code-bg: rgba(0, 0, 0, .25); 167 | $code-padding-y: 3px; 168 | $code-padding-x: 5px; 169 | $pre-bg: $dropdown-bg; 170 | $pre-color: $dropdown-link-color; 171 | 172 | $badge-font-size: 0.75rem; 173 | $badge-font-weight: bold; 174 | $badge-padding-y: 4px; 175 | $badge-padding-x: 6px; 176 | 177 | 178 | $custom-control-indicator-size: 1.2rem; 179 | $custom-control-indicator-bg: $body-bg; 180 | $custom-control-indicator-border-color: lighten($body-bg, 25%); 181 | $custom-control-indicator-checked-bg: theme-color("primary"); 182 | $custom-control-indicator-checked-color: $body-bg; 183 | $custom-control-indicator-checked-border-color: transparent; 184 | $custom-control-indicator-active-bg: rgba(255, 255, 0, 0.5); 185 | 186 | 187 | $modal-content-bg: $body-bg; 188 | $modal-content-border-color: $body-bg; 189 | $modal-header-border-width: 0; 190 | $modal-footer-border-width: 0; 191 | 192 | $modal-content-border-color: #ffffff24; 193 | $modal-content-border-width: 1px; 194 | 195 | 196 | $progress-bg: $table-bg; 197 | $progress-height: 3px; 198 | 199 | $alert-bg-scale: 90%; 200 | $alert-border-scale: 50%; 201 | $alert-color-scale: 50%; 202 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src/", 4 | "module": "esnext", 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "noImplicitAny": false, 8 | "removeComments": false, 9 | "emitDeclarationOnly": false, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "sourceMap": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noUnusedLocals": true, 16 | "skipLibCheck": true, 17 | "esModuleInterop": true, 18 | "allowSyntheticDefaultImports": true, 19 | "declaration": true, 20 | "strictNullChecks": true, 21 | "lib": [ 22 | "dom", 23 | "es5", 24 | "es6", 25 | "es7" 26 | ], 27 | "paths": { 28 | "src/*": ["./*"] 29 | } 30 | }, 31 | "include": ["src"] 32 | } 33 | -------------------------------------------------------------------------------- /frontend/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({path: '../.env'}) 2 | const webpack = require('webpack') 3 | const path = require('path') 4 | const MiniCssExtractPlugin = require("mini-css-extract-plugin") 5 | 6 | module.exports = { 7 | mode: process.env.DEV ? 'development' : 'production', 8 | context: __dirname, 9 | devtool: 'source-map', 10 | cache: !process.env.DEV ? false : { 11 | type: 'filesystem', 12 | }, 13 | resolve: { 14 | mainFields: ['esm2015', 'browser', 'module', 'main'], 15 | modules: [ 16 | 'src/', 17 | 'node_modules/', 18 | ], 19 | extensions: ['.ts', '.js'], 20 | alias: { 21 | assets: path.resolve(__dirname, 'assets'), 22 | src: path.resolve(__dirname, 'src'), 23 | theme: path.resolve(__dirname, 'theme'), 24 | }, 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.[jt]sx?$/, 30 | loader: '@ngtools/webpack', 31 | }, 32 | { test: /tabby\/app\/dist/, use: ['script-loader'] }, 33 | { 34 | test: /\.pug$/, 35 | use: ['apply-loader', 'pug-loader'], 36 | include: /component\.pug/ 37 | }, 38 | { 39 | test: /\.scss$/, 40 | use: ['@tabby-gang/to-string-loader', 'css-loader', 'sass-loader'], 41 | include: /component\.scss/ 42 | }, 43 | { 44 | test: /\.scss$/, 45 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], 46 | exclude: /component\.scss/ 47 | }, 48 | { 49 | test: /\.(ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 50 | type: 'asset/resource', 51 | }, 52 | { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'] }, 53 | { 54 | test: /\.(jpeg|png|svg)?$/, 55 | type: 'asset/resource', 56 | }, 57 | { 58 | test: /\.html$/, 59 | loader: 'html-loader', 60 | }, 61 | ], 62 | }, 63 | plugins: [ 64 | new MiniCssExtractPlugin(), 65 | ], 66 | } 67 | -------------------------------------------------------------------------------- /frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('./webpack.config.base.js') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | const { AngularWebpackPlugin } = require('@ngtools/webpack') 5 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 6 | const HtmlWebpackPlugin = require('html-webpack-plugin') 7 | 8 | const htmlPluginOptions = { 9 | hash: true, 10 | minify: false 11 | } 12 | 13 | const outputPath = path.join(__dirname, 'build') 14 | 15 | module.exports = { 16 | name: 'browser', 17 | target: 'web', 18 | ...baseConfig, 19 | entry: { 20 | index: path.resolve(__dirname, 'src/index.ts'), 21 | terminal: path.resolve(__dirname, 'src/terminal.ts'), 22 | demo: path.resolve(__dirname, 'src/demo.ts'), 23 | }, 24 | plugins: [ 25 | ...baseConfig.plugins, 26 | new AngularWebpackPlugin({ 27 | tsconfig: 'tsconfig.json', 28 | directTemplateLoading: false, 29 | jitMode: false, 30 | }), 31 | new HtmlWebpackPlugin({ 32 | template: './src/index.html', 33 | filename: 'index.html', 34 | chunks: ['index'], 35 | ...htmlPluginOptions, 36 | }), 37 | new HtmlWebpackPlugin({ 38 | template: './src/terminal.html', 39 | filename: 'terminal.html', 40 | chunks: ['terminal'], 41 | ...htmlPluginOptions, 42 | }), 43 | new HtmlWebpackPlugin({ 44 | template: './src/demo.html', 45 | filename: 'demo.html', 46 | chunks: ['demo'], 47 | ...htmlPluginOptions, 48 | }), 49 | ], 50 | output: { 51 | path: outputPath, 52 | pathinfo: true, 53 | publicPath: '/static/', 54 | filename: '[name].js', 55 | chunkFilename: '[name].bundle.js', 56 | }, 57 | } 58 | 59 | if (process.env.BUNDLE_ANALYZER) { 60 | module.exports.plugins.push(new BundleAnalyzerPlugin()) 61 | } 62 | -------------------------------------------------------------------------------- /frontend/webpack.config.server.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('./webpack.config.base.js') 2 | const path = require('path') 3 | const { AngularWebpackPlugin } = require('@ngtools/webpack') 4 | 5 | const outputPath = path.join(__dirname, 'build-server') 6 | 7 | module.exports = { 8 | name: 'server', 9 | target: 'node', 10 | ...baseConfig, 11 | entry: { 12 | // 'index.server': path.resolve(__dirname, 'src/index.server.ts'), 13 | 'server': path.resolve(__dirname, 'src/server.ts'), 14 | }, 15 | optimization: { 16 | minimize: false, 17 | }, 18 | resolve: { 19 | ...baseConfig.resolve, 20 | mainFields: ['esm2015', 'module', 'main'], 21 | }, 22 | plugins: [ 23 | ...baseConfig.plugins, 24 | new AngularWebpackPlugin({ 25 | entryModule: path.resolve(__dirname, 'src/app.server.module#AppServerModule'), 26 | mainPath: path.resolve(__dirname, 'src/server.ts'), 27 | tsconfig: 'tsconfig.json', 28 | directTemplateLoading: false, 29 | platform: 1, 30 | skipCodeGeneration: false, 31 | }), 32 | ], 33 | output: { 34 | // libraryTarget: 'commonjs', 35 | path: outputPath, 36 | pathinfo: true, 37 | publicPath: '/static/', 38 | filename: '[name].js', 39 | chunkFilename: '[name].bundle.js', 40 | }, 41 | } 42 | --------------------------------------------------------------------------------