From 48072773e1b00d3493f230c8a3372afb63325d30 Mon Sep 17 00:00:00 2001 From: Gilles <43683714+corp-0@users.noreply.github.com> Date: Mon, 7 Feb 2022 01:57:03 -0300 Subject: [PATCH] feat: full rewrite of application (#48) * misc: trigger release * misc(deps): update dependencies (#41) * misc: rewrite (#43) * misc(repo): rebrand the project as central-command * feat(repo)!: first stone of the rewrite * feat(repo): feature parity with last attempt * misc: linter * misc: get rid of useless env for creating directories * misc: get rid of commented line in docker-compose.yml * misc: use Bob's email as example * feat: add is_active field to admin view for accounts * feat: improve models by adding help text instead of obscure comments * fix: fixes username validation doing literally nothing * misc: migration update * misc: rewrite continuation (#47) * fix: make public account data actually public * feat: readd needed html templates * fix: accounts that change their email will now have to confirm the new email * feat: attempting to log in before confirmation will now raise and explain the issue * feat: writing data to other-data will now only update keys if present or append new data if not. We also won't need a create endpoint anymore, since write will do it if the record doesn't previously exist. * feat: load environmental variables from a .env file with python-dotenv * feat: adds account verification endpoint for game servers --- .github/workflows/main.yml | 2 +- .github/workflows/release.yml | 2 +- Dockerfile | 24 +- README.md | 15 +- dev-compose.yml | 20 +- docker-compose.yml | 36 +- example.env | 11 +- nginx/Dockerfile | 7 + nginx/nginx.conf | 24 ++ poetry.lock | 391 ++++++++++++------ pyproject.toml | 8 +- src/account/admin.py | 41 -- src/account/api/serializers.py | 65 --- src/account/api/urls.py | 15 - src/account/api/views.py | 127 ------ src/account/apps.py | 5 - src/account/forms.py | 28 -- .../migrations/0002_auto_20201228_0006.py | 18 - src/account/models.py | 30 -- src/account/tests.py | 3 - src/account/utils.py | 68 --- src/account/validators.py | 41 -- src/{account => accounts}/__init__.py | 0 src/accounts/admin.py | 39 ++ src/{account => accounts}/api/__init__.py | 0 src/accounts/api/serializers.py | 101 +++++ src/accounts/api/urls.py | 37 ++ src/accounts/api/views.py | 224 ++++++++++ src/accounts/apps.py | 6 + src/accounts/exceptions.py | 8 + .../migrations/0001_initial.py | 16 +- .../migrations/__init__.py | 0 src/accounts/models.py | 69 ++++ src/accounts/validators.py | 22 + .../__init__.py | 0 .../asgi.py | 4 +- .../settings.py | 58 +-- .../urls.py | 10 +- .../wsgi.py | 4 +- src/entrypoint.sh | 36 ++ src/manage.py | 5 +- src/persistence/admin.py | 23 +- src/persistence/api/serializers.py | 13 +- src/persistence/api/urls.py | 16 +- src/persistence/api/views.py | 119 ++++-- src/persistence/migrations/0001_initial.py | 40 +- src/persistence/models.py | 55 +-- src/website/apps.py | 5 - src/website/migrations/__init__.py | 0 src/website/statics/css/login-dark.css | 65 --- src/website/statics/css/style.css | 4 - src/website/templates/base.html | 2 +- .../registration/confirmation_email.html | 5 +- src/website/templates/registration/login.html | 15 - .../registration/password_reset_complete.html | 7 - .../registration/password_reset_confirm.html | 19 - .../registration/password_reset_done.html | 12 - .../registration/password_reset_email.html | 12 - .../registration/password_reset_form.html | 12 - .../registration/password_reset_subject.txt | 1 - .../templates/registration/register.html | 12 - src/website/tests.py | 3 - src/website/urls.py | 7 - src/website/views.py | 10 - 64 files changed, 1095 insertions(+), 982 deletions(-) create mode 100644 nginx/Dockerfile create mode 100644 nginx/nginx.conf delete mode 100644 src/account/admin.py delete mode 100644 src/account/api/serializers.py delete mode 100644 src/account/api/urls.py delete mode 100644 src/account/api/views.py delete mode 100644 src/account/apps.py delete mode 100644 src/account/forms.py delete mode 100644 src/account/migrations/0002_auto_20201228_0006.py delete mode 100644 src/account/models.py delete mode 100644 src/account/tests.py delete mode 100644 src/account/utils.py delete mode 100644 src/account/validators.py rename src/{account => accounts}/__init__.py (100%) create mode 100644 src/accounts/admin.py rename src/{account => accounts}/api/__init__.py (100%) create mode 100644 src/accounts/api/serializers.py create mode 100644 src/accounts/api/urls.py create mode 100644 src/accounts/api/views.py create mode 100644 src/accounts/apps.py create mode 100644 src/accounts/exceptions.py rename src/{account => accounts}/migrations/0001_initial.py (53%) rename src/{account => accounts}/migrations/__init__.py (100%) create mode 100644 src/accounts/models.py create mode 100644 src/accounts/validators.py rename src/{unitystation_auth => central-command}/__init__.py (100%) rename src/{unitystation_auth => central-command}/asgi.py (70%) rename src/{unitystation_auth => central-command}/settings.py (72%) rename src/{unitystation_auth => central-command}/urls.py (73%) rename src/{unitystation_auth => central-command}/wsgi.py (70%) create mode 100644 src/entrypoint.sh delete mode 100644 src/website/apps.py delete mode 100644 src/website/migrations/__init__.py delete mode 100644 src/website/statics/css/login-dark.css delete mode 100644 src/website/statics/css/style.css delete mode 100644 src/website/templates/registration/login.html delete mode 100644 src/website/templates/registration/password_reset_complete.html delete mode 100644 src/website/templates/registration/password_reset_confirm.html delete mode 100644 src/website/templates/registration/password_reset_done.html delete mode 100644 src/website/templates/registration/password_reset_email.html delete mode 100644 src/website/templates/registration/password_reset_form.html delete mode 100644 src/website/templates/registration/password_reset_subject.txt delete mode 100644 src/website/templates/registration/register.html delete mode 100644 src/website/tests.py delete mode 100644 src/website/urls.py delete mode 100644 src/website/views.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a3ac8d3..2743ef0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,7 +11,7 @@ on: - "v*" env: - IMAGE_NAME: unitystation/unitystation_auth + IMAGE_NAME: unitystation/central-command jobs: lint: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 465be5e..5b87b2d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest needs: [release] env: - IMAGE_NAME: unitystation/unitystation_auth + IMAGE_NAME: unitystation/central-command steps: - uses: actions/checkout@v2 with: diff --git a/Dockerfile b/Dockerfile index e6888d0..390cd0f 100755 --- a/Dockerfile +++ b/Dockerfile @@ -30,11 +30,19 @@ RUN apk add --no-cache libpq \ COPY src . -RUN addgroup -S unitystation \ - && adduser -S auth_server -G unitystation \ - && chown -R auth_server:unitystation /src - -USER auth_server - -ENTRYPOINT ["python", "manage.py"] -CMD ["runserver", "0:8000"] \ No newline at end of file +RUN mkdir /home/website +RUN mkdir /home/website/statics +RUN mkdir /home/website/media + +# I'm too dumb to make user permissions over shared volumes work +#RUN addgroup -S unitystation \ +# && adduser -S central_command -G unitystation \ +# && chown -R central_command:unitystation /src \ +# && chown -R central_command:unitystation $home +# +#USER central_command + +RUN sed -i 's/\r$//g' entrypoint.sh +RUN chmod +x entrypoint.sh + +ENTRYPOINT ["./entrypoint.sh"] \ No newline at end of file diff --git a/README.md b/README.md index 201790b..8a8d721 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,15 @@ -# unitystation_auth -![Docker Image Version (latest by date)](https://img.shields.io/docker/v/unitystation/unitystation_auth) -[![Codacy Badge](https://app.codacy.com/project/badge/Grade/38cce37d4c854ca48645fd5ecc9cae61)](https://www.codacy.com/gh/unitystation/unitystation_auth/dashboard?utm_source=github.com&utm_medium=referral&utm_content=unitystation/unitystation_auth&utm_campaign=Badge_Grade) +# Central Command +![Docker Image Version (latest by date)](https://img.shields.io/docker/v/unitystation/central-command?sort=date) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/38cce37d4c854ca48645fd5ecc9cae61)](https://www.codacy.com/gh/unitystation/central-command/dashboard?utm_source=github.com&utm_medium=referral&utm_content=unitystation/central-command&utm_campaign=Badge_Grade) + +The all-in-one backend application for [Unitystation](https://github.com/unitystation/unitystation) + +### Features +- Account managment and user validation. +- Server list managment. +- In-game persistence. +- Works cross-fork! +- Modular architecture. ### Development guide #### Setting up Docker diff --git a/dev-compose.yml b/dev-compose.yml index b370c98..03b6a8b 100644 --- a/dev-compose.yml +++ b/dev-compose.yml @@ -1,12 +1,8 @@ version: "3" -x-django-defaults: &django_defaults - build: . - env_file: ./.env - services: db: - image: postgres + image: postgres:14.1-alpine environment: POSTGRES_HOST_AUTH_METHOD: trust volumes: @@ -14,20 +10,18 @@ services: ports: - "5432:5432" - web_migration: - <<: *django_defaults - command: migrate - depends_on: - - db - web: - <<: *django_defaults depends_on: - - web_migration + - db + build: . + env_file: ./.env ports: - "8000:8000" volumes: - ./src:/src + command: python manage.py runserver 0.0.0.0:8000 volumes: db-data: + static-volume: + media-volume: diff --git a/docker-compose.yml b/docker-compose.yml index d4fd7b7..eaac5e3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,29 +1,37 @@ version: "3" -x-django-defaults: &django_defaults - image: unitystation/unitystation_auth - env_file: ./.env +x-common-volumes: &common-volumes + volumes: + - static-volume:/home/website/statics + - media-volume:/home/website/media services: db: - image: postgres + image: postgres:14.1-alpine environment: POSTGRES_HOST_AUTH_METHOD: trust volumes: - db-data:/var/lib/postgresql/data - web_migration: - <<: *django_defaults - command: migrate - depends_on: - - db - web: - <<: *django_defaults - depends_on: - - web_migration + image: unitystation/central-command:latest + environment: + - DEBUG=0 + env_file: ./.env + expose: + - 8000 + command: gunicorn central-command.wsgi:application --bind 0.0.0.0:8000 + <<: *common-volumes + + nginx: + build: ./nginx ports: - - "8000:8000" + - 80:80 + depends_on: + - web + <<: *common-volumes volumes: db-data: + static-volume: + media-volume: diff --git a/example.env b/example.env index 58a9915..5b9c5ee 100644 --- a/example.env +++ b/example.env @@ -1,6 +1,11 @@ EMAIL_HOST=smtp.gmail.com EMAIL_PORT=587 -EMAIL_HOST_USER=me@gmail.com +EMAIL_HOST_USER=bob@example.com EMAIL_HOST_PASSWORD=password -EMAIL_PAGE_DOMAIN=http://mydomain.com/ -SECRET_KEY=MYSECRET \ No newline at end of file +EMAIL_PAGE_DOMAIN=http://localhost:8000 +SECRET_KEY=MYSECRET +DEBUG=1 +DB_ENGINE=django.db.backends.postgresql +DB_NAME=postgres +DB_HOST=db +DB_PORT=5432 \ No newline at end of file diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000..84140a2 --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,7 @@ +FROM nginx:1.21-alpine + +RUN rm /etc/nginx/conf.d/default.conf +RUN mkdir /home/website +RUN mkdir /home/website/statics +RUN mkdir /home/website/media +COPY nginx.conf /etc/nginx/conf.d \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..857aa01 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,24 @@ +upstream central_command { + server web:8000; +} + +server { + + listen 80; + + location / { + proxy_pass http://central_command; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_redirect off; + client_max_body_size 100M; + } + + location /static/ { + alias /home/website/statics/; + } + + location /media/ { + alias /home/website/media/; + } +} \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 664fa61..5793e5f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,6 +1,6 @@ [[package]] name = "asgiref" -version = "3.3.4" +version = "3.4.1" description = "ASGI specs, helper code, and adapters" category = "main" optional = false @@ -11,7 +11,7 @@ tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] [[package]] name = "astroid" -version = "2.5.6" +version = "2.8.5" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -19,11 +19,12 @@ python-versions = "~=3.6" [package.dependencies] lazy-object-proxy = ">=1.4.0" -wrapt = ">=1.11,<1.13" +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} +wrapt = ">=1.11,<1.14" [[package]] name = "cffi" -version = "1.14.5" +version = "1.15.0" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false @@ -42,7 +43,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "cryptography" -version = "3.4.7" +version = "35.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -55,13 +56,13 @@ cffi = ">=1.12" docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] -sdist = ["setuptools-rust (>=0.11.4)"] +sdist = ["setuptools_rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] [[package]] name = "django" -version = "3.2.3" +version = "3.2.9" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false @@ -108,18 +109,33 @@ python-versions = ">=3.5" [package.dependencies] django = ">=2.2" +[[package]] +name = "gunicorn" +version = "20.1.0" +description = "WSGI HTTP Server for UNIX" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + [[package]] name = "isort" -version = "5.8.0" +version = "5.10.1" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.6.1,<4.0" [package.extras] pipfile_deprecated_finder = ["pipreqs", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] [[package]] name = "lazy-object-proxy" @@ -137,17 +153,29 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "platformdirs" +version = "2.4.0" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + [[package]] name = "psycopg2-binary" -version = "2.8.6" +version = "2.9.2" description = "psycopg2 - Python-PostgreSQL Database Adapter" category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +python-versions = ">=3.6" [[package]] name = "pycparser" -version = "2.20" +version = "2.21" description = "C parser in Python" category = "main" optional = false @@ -155,22 +183,35 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pylint" -version = "2.8.2" +version = "2.11.1" description = "python code static checker" category = "dev" optional = false python-versions = "~=3.6" [package.dependencies] -astroid = ">=2.5.6,<2.7" +astroid = ">=2.8.0,<2.9" colorama = {version = "*", markers = "sys_platform == \"win32\""} isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.7" +platformdirs = ">=2.2.0" toml = ">=0.7.1" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[[package]] +name = "python-dotenv" +version = "0.19.2" +description = "Read key-value pairs from a .env file and set them as environment variables" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +cli = ["click (>=5.0)"] [[package]] name = "pytz" -version = "2021.1" +version = "2021.3" description = "World timezone definitions, modern and historical" category = "main" optional = false @@ -178,7 +219,7 @@ python-versions = "*" [[package]] name = "sqlparse" -version = "0.4.1" +version = "0.4.2" description = "A non-validating SQL parser." category = "main" optional = false @@ -192,100 +233,117 @@ category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "typing-extensions" +version = "4.0.0" +description = "Backported and Experimental Type Hints for Python 3.6+" +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "wrapt" -version = "1.12.1" +version = "1.13.3" description = "Module for decorators, wrappers and monkey patching." category = "dev" optional = false -python-versions = "*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "2ce6c352653669346666eebbea3dec836cdab8adc47184c0e5a7579b40891f37" +content-hash = "0135a1b2b6b9a2f4922bd92a21918235a25aa5ebc5436539a9348c605ab01ad7" [metadata.files] asgiref = [ - {file = "asgiref-3.3.4-py3-none-any.whl", hash = "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee"}, - {file = "asgiref-3.3.4.tar.gz", hash = "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"}, + {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, + {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, ] astroid = [ - {file = "astroid-2.5.6-py3-none-any.whl", hash = "sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e"}, - {file = "astroid-2.5.6.tar.gz", hash = "sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"}, + {file = "astroid-2.8.5-py3-none-any.whl", hash = "sha256:abc423a1e85bc1553954a14f2053473d2b7f8baf32eae62a328be24f436b5107"}, + {file = "astroid-2.8.5.tar.gz", hash = "sha256:11f7356737b624c42e21e71fe85eea6875cb94c03c82ac76bd535a0ff10b0f25"}, ] cffi = [ - {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"}, - {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"}, - {file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"}, - {file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"}, - {file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"}, - {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"}, - {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"}, - {file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"}, - {file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"}, - {file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"}, - {file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"}, - {file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"}, - {file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"}, - {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"}, - {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"}, - {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"}, - {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f"}, - {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed"}, - {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55"}, - {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"}, - {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"}, - {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"}, - {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"}, - {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"}, - {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"}, - {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69"}, - {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05"}, - {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc"}, - {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"}, - {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"}, - {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"}, - {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"}, - {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"}, - {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"}, - {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373"}, - {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f"}, - {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76"}, - {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"}, - {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"}, - {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"}, - {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"}, - {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"}, - {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"}, - {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0"}, - {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333"}, - {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7"}, - {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"}, - {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"}, - {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, + {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, + {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, + {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, + {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, + {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, + {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, + {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, + {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, + {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, + {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, + {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, + {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, + {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, + {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, + {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, + {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, + {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] cryptography = [ - {file = "cryptography-3.4.7-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1"}, - {file = "cryptography-3.4.7-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250"}, - {file = "cryptography-3.4.7-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2"}, - {file = "cryptography-3.4.7-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6"}, - {file = "cryptography-3.4.7-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959"}, - {file = "cryptography-3.4.7-cp36-abi3-win32.whl", hash = "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d"}, - {file = "cryptography-3.4.7-cp36-abi3-win_amd64.whl", hash = "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca"}, - {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873"}, - {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d"}, - {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177"}, - {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"}, - {file = "cryptography-3.4.7.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"}, + {file = "cryptography-35.0.0-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9"}, + {file = "cryptography-35.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:ced40344e811d6abba00295ced98c01aecf0c2de39481792d87af4fa58b7b4d6"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:54b2605e5475944e2213258e0ab8696f4f357a31371e538ef21e8d61c843c28d"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7b7ceeff114c31f285528ba8b390d3e9cfa2da17b56f11d366769a807f17cbaa"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d69645f535f4b2c722cfb07a8eab916265545b3475fdb34e0be2f4ee8b0b15e"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2d0e0acc20ede0f06ef7aa58546eee96d2592c00f450c9acb89c5879b61992"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:07bb7fbfb5de0980590ddfc7f13081520def06dc9ed214000ad4372fb4e3c7f6"}, + {file = "cryptography-35.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7eba2cebca600a7806b893cb1d541a6e910afa87e97acf2021a22b32da1df52d"}, + {file = "cryptography-35.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:18d90f4711bf63e2fb21e8c8e51ed8189438e6b35a6d996201ebd98a26abbbe6"}, + {file = "cryptography-35.0.0-cp36-abi3-win32.whl", hash = "sha256:c10c797ac89c746e488d2ee92bd4abd593615694ee17b2500578b63cad6b93a8"}, + {file = "cryptography-35.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:7075b304cd567694dc692ffc9747f3e9cb393cc4aa4fb7b9f3abd6f5c4e43588"}, + {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a688ebcd08250eab5bb5bca318cc05a8c66de5e4171a65ca51db6bd753ff8953"}, + {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99915d6ab265c22873f1b4d6ea5ef462ef797b4140be4c9d8b179915e0985c6"}, + {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:928185a6d1ccdb816e883f56ebe92e975a262d31cc536429041921f8cb5a62fd"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ebeddd119f526bcf323a89f853afb12e225902a24d29b55fe18dd6fcb2838a76"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22a38e96118a4ce3b97509443feace1d1011d0571fae81fc3ad35f25ba3ea999"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb80e8a1f91e4b7ef8b33041591e6d89b2b8e122d787e87eeb2b08da71bb16ad"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:abb5a361d2585bb95012a19ed9b2c8f412c5d723a9836418fab7aaa0243e67d2"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1ed82abf16df40a60942a8c211251ae72858b25b7421ce2497c2eb7a1cee817c"}, + {file = "cryptography-35.0.0.tar.gz", hash = "sha256:9933f28f70d0517686bd7de36166dda42094eac49415459d9bdf5e7df3e0086d"}, ] django = [ - {file = "Django-3.2.3-py3-none-any.whl", hash = "sha256:7e0a1393d18c16b503663752a8b6790880c5084412618990ce8a81cc908b4962"}, - {file = "Django-3.2.3.tar.gz", hash = "sha256:13ac78dbfd189532cad8f383a27e58e18b3d33f80009ceb476d7fcbfc5dcebd8"}, + {file = "Django-3.2.9-py3-none-any.whl", hash = "sha256:e22c9266da3eec7827737cde57694d7db801fedac938d252bf27377cec06ed1b"}, + {file = "Django-3.2.9.tar.gz", hash = "sha256:51284300f1522ffcdb07ccbdf676a307c6678659e1284f0618e5a774127a6a08"}, ] django-email-verification = [ {file = "django-email-verification-0.0.7.tar.gz", hash = "sha256:02ed6b47c1311a18475f11eb7d002381d13b1fba73b0ce30c3435cc887f86a3b"}, @@ -300,9 +358,13 @@ djangorestframework = [ {file = "djangorestframework-3.12.4-py3-none-any.whl", hash = "sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf"}, {file = "djangorestframework-3.12.4.tar.gz", hash = "sha256:f747949a8ddac876e879190df194b925c177cdeb725a099db1460872f7c0a7f2"}, ] +gunicorn = [ + {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, + {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, +] isort = [ - {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, - {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] lazy-object-proxy = [ {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, @@ -332,63 +394,126 @@ mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] +platformdirs = [ + {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, + {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, +] psycopg2-binary = [ - {file = "psycopg2-binary-2.8.6.tar.gz", hash = "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27m-win32.whl", hash = "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27m-win_amd64.whl", hash = "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1"}, - {file = "psycopg2_binary-2.8.6-cp34-cp34m-win32.whl", hash = "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2"}, - {file = "psycopg2_binary-2.8.6-cp34-cp34m-win_amd64.whl", hash = "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152"}, - {file = "psycopg2_binary-2.8.6-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449"}, - {file = "psycopg2_binary-2.8.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859"}, - {file = "psycopg2_binary-2.8.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550"}, - {file = "psycopg2_binary-2.8.6-cp35-cp35m-win32.whl", hash = "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd"}, - {file = "psycopg2_binary-2.8.6-cp35-cp35m-win_amd64.whl", hash = "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71"}, - {file = "psycopg2_binary-2.8.6-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4"}, - {file = "psycopg2_binary-2.8.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb"}, - {file = "psycopg2_binary-2.8.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da"}, - {file = "psycopg2_binary-2.8.6-cp36-cp36m-win32.whl", hash = "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2"}, - {file = "psycopg2_binary-2.8.6-cp36-cp36m-win_amd64.whl", hash = "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a"}, - {file = "psycopg2_binary-2.8.6-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679"}, - {file = "psycopg2_binary-2.8.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf"}, - {file = "psycopg2_binary-2.8.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b"}, - {file = "psycopg2_binary-2.8.6-cp37-cp37m-win32.whl", hash = "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67"}, - {file = "psycopg2_binary-2.8.6-cp37-cp37m-win_amd64.whl", hash = "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66"}, - {file = "psycopg2_binary-2.8.6-cp38-cp38-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f"}, - {file = "psycopg2_binary-2.8.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77"}, - {file = "psycopg2_binary-2.8.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94"}, - {file = "psycopg2_binary-2.8.6-cp38-cp38-win32.whl", hash = "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729"}, - {file = "psycopg2_binary-2.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77"}, - {file = "psycopg2_binary-2.8.6-cp39-cp39-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83"}, - {file = "psycopg2_binary-2.8.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52"}, - {file = "psycopg2_binary-2.8.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd"}, - {file = "psycopg2_binary-2.8.6-cp39-cp39-win32.whl", hash = "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056"}, - {file = "psycopg2_binary-2.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6"}, + {file = "psycopg2-binary-2.9.2.tar.gz", hash = "sha256:234b1f48488b2f86aac04fb00cb04e5e9bcb960f34fa8a8e41b73149d581a93b"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:c0e1fb7097ded2cc44d9037cfc68ad86a30341261492e7de95d180e534969fb2"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:717525cdc97b23182ff6f470fb5bf6f0bc796b5a7000c6f6699d6679991e4a5e"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3865d0cd919349c45603bd7e80249a382c5ecf8106304cfd153282adf9684b6a"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:daf6b5c62eb738872d61a1fa740d7768904911ba5a7e055ed72169d379b58beb"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:3ac83656ff4fbe7f2a956ab085e3eb1d678df54759965d509bdd6a06ce520d49"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-win32.whl", hash = "sha256:a04cfa231e7d9b63639e62166a4051cb47ca599fa341463fa3e1c48585fcee64"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:c6e16e085fe6dc6c099ee0be56657aa9ad71027465ef9591d302ba230c404c7e"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:53912199abb626a7249c662e72b70b4f57bf37f840599cec68625171435790dd"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:029e09a892b9ebc3c77851f69ce0720e1b72a9c6850460cee49b14dfbf9ccdd2"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db1b03c189f85b8df29030ad32d521dd7dcb862fd5f8892035314f5b886e70ce"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:2eecbdc5fa5886f2dd6cc673ce4291cc0fb8900965315268960ad9c2477f8276"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:a77e98c68b0e6c51d4d6a994d22b30e77276cbd33e4aabdde03b9ad3a2c148aa"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-win32.whl", hash = "sha256:bf31e6fdb4ec1f6d98a07f48836508ed6edd19b48b13bbf168fbc1bd014b4ca2"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f9c37ecb173d76cf49e519133fd70851b8f9c38b6b8c1cb7fcfc71368d4cc6fc"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:a507db7758953b1b170c4310691a1a89877029b1e11b08ba5fc8ae3ddb35596b"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e4bbcfb403221ea1953f3e0a85cef00ed15c1683a66cf35c956a7e37c33a4c4"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4dff0f15af6936c6fe6da7067b4216edbbe076ad8625da819cc066591b1133c"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:8d2aafe46eb87742425ece38130510fbb035787ee89a329af299029c4d9ae318"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:37c8f00f7a2860bac9f7a54f03c243fc1dd9b367e5b2b52f5a02e5f4e9d8c49b"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-win32.whl", hash = "sha256:ef97578fab5115e3af4334dd3376dea3c3a79328a3314b21ec7ced02920b916d"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7e6bd4f532c2cd297b81114526176b240109a1c52020adca69c3f3226c65dc18"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:eeee7b18c51d02e49bf1984d7af26e8843fe68e31fa1cbab5366ebdfa1c89ade"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:497372cc76e6cbce2f51b37be141f360a321423c03eb9be45524b1d123f4cd11"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671699aff57d22a245b7f4bba89e3de97dc841c5e98bd7f685429b2b20eca47"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:b9d45374ba98c1184df9cce93a0b766097544f8bdfcd5de83ff10f939c193125"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:a1852c5bef7e5f52bd43fde5eda610d4df0fb2efc31028150933e84b4140d47a"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-win32.whl", hash = "sha256:108b0380969ddab7c8ef2a813a57f87b308b2f88ec15f1a1e7b653964a3cfb25"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:14427437117f38e65f71db65d8eafd0e86837be456567798712b8da89db2b2dd"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:578c279cd1ce04f05ae0912530ece00bab92854911808e5aec27588aba87e361"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2dea4deac3dd3687e32daeb0712ee96c535970dfdded37a11de6a21145ab0e"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b592f09ff18cfcc9037b9a976fcd62db48cae9dbd5385f2471d4c2ba40c52b4d"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:3a320e7a804f3886a599fea507364aaafbb8387027fffcdfbd34d96316c806c7"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:7585ca73dcfe326f31fafa8f96e6bb98ea9e9e46c7a1924ec8101d797914ae27"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-win32.whl", hash = "sha256:9c0aaad07941419926b9bd00171e49fe6b06e42e5527fb91671e137fe6c93d77"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa2847d8073951dbc84c4f8b32c620764db3c2eb0d99a04835fecfab7d04816e"}, ] pycparser = [ - {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, - {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] pylint = [ - {file = "pylint-2.8.2-py3-none-any.whl", hash = "sha256:f7e2072654a6b6afdf5e2fb38147d3e2d2d43c89f648637baab63e026481279b"}, - {file = "pylint-2.8.2.tar.gz", hash = "sha256:586d8fa9b1891f4b725f587ef267abe2a1bad89d6b184520c7f07a253dd6e217"}, + {file = "pylint-2.11.1-py3-none-any.whl", hash = "sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126"}, + {file = "pylint-2.11.1.tar.gz", hash = "sha256:2c9843fff1a88ca0ad98a256806c82c5a8f86086e7ccbdb93297d86c3f90c436"}, +] +python-dotenv = [ + {file = "python-dotenv-0.19.2.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"}, + {file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"}, ] pytz = [ - {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, - {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, + {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, + {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, ] sqlparse = [ - {file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"}, - {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"}, + {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"}, + {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] +typing-extensions = [ + {file = "typing_extensions-4.0.0-py3-none-any.whl", hash = "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9"}, + {file = "typing_extensions-4.0.0.tar.gz", hash = "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed"}, +] wrapt = [ - {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, + {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"}, + {file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"}, + {file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"}, + {file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"}, + {file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"}, + {file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"}, + {file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"}, + {file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"}, + {file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"}, + {file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"}, + {file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"}, + {file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"}, + {file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"}, + {file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"}, + {file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"}, + {file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"}, + {file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"}, + {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, + {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, ] diff --git a/pyproject.toml b/pyproject.toml index 432f18d..ad658d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,10 +15,10 @@ lines_between_types = 1 skip = "migrations" [tool.poetry] -name = "unitystation_auth" +name = "central-command" version = "0.1.0" -description = "Authentication system for Unitystation" -authors = ["Your Name "] +description = "All-in-one backend application for Unitystation" +authors = ["Andrés Riquelme "] [tool.poetry.dependencies] python = "^3.9" @@ -27,6 +27,8 @@ djangorestframework = "^3.12.1" psycopg2-binary = "^2.8.6" django-email-verification = "^0.0.7" django-rest-knox = "^4.1.0" +gunicorn = "^20.1.0" +python-dotenv = "^0.19.2" [tool.poetry.dev-dependencies] pylint = "^2.6.0" diff --git a/src/account/admin.py b/src/account/admin.py deleted file mode 100644 index 14fe506..0000000 --- a/src/account/admin.py +++ /dev/null @@ -1,41 +0,0 @@ -from django.contrib import admin -from django.contrib.auth.admin import UserAdmin -from django.contrib.auth.models import Group - -from .models import Account - - -@admin.register(Account) -class AccountAdmin(UserAdmin): - fieldsets = ( - ( - "Account data", - { - "classes": ("wide",), - "fields": ("email", "username", "password"), - }, - ), - ("Permissions", {"fields": ("is_active", "is_staff", "is_superuser")}), - ( - "Character settings", - { - "fields": ("character_settings",), - }, - ), - ) - add_fieldsets = ( - ( - "Account data", - { - "classes": ("wide",), - "fields": ("email", "username", "password1", "password2"), - }, - ), - ) - - list_display = ["user_id", "email", "username", "is_staff", "is_active"] - readonly_fields = ["user_id"] - list_editable = ["email", "username"] - - -admin.site.unregister(Group) diff --git a/src/account/api/serializers.py b/src/account/api/serializers.py deleted file mode 100644 index 28d6c82..0000000 --- a/src/account/api/serializers.py +++ /dev/null @@ -1,65 +0,0 @@ -import django.contrib.auth.password_validation as validators - -from django.conf import settings -from django.core import exceptions -from rest_framework import serializers -from django_email_verification import sendConfirm - -from account.models import Account - - -class AccountSerializer(serializers.ModelSerializer): - class Meta: - model = Account - fields = ["user_id", "email", "username", "character_settings"] - - -class RegisterAccountSerializer(serializers.ModelSerializer): - class Meta: - model = Account - fields = ["email", "username", "password"] - extra_kwargs = {"password": {"write_only": True}} - - def save(self): - account = Account( - email=self.validated_data["email"], - username=self.validated_data["username"], - ) - - password = self.validated_data["password"] - account.set_password(password) - - if settings.REQUIRE_EMAIL_CONFIRMATION: - sendConfirm(account) - else: - account.save() - - return account - - def validate(self, data): - filtered_data = dict(data) - account = Account(**filtered_data) - - errors = dict() - - try: - validators.validate_password(data.get("password"), account) - except exceptions.ValidationError as e: - errors["password"] = list(e.messages) - - if errors: - raise serializers.ValidationError(errors) - - return super(RegisterAccountSerializer, self).validate(data) - - -class UpdateAccountSerializer(serializers.ModelSerializer): - class Meta: - model = Account - fields = ["email", "username"] - - -class UpdateCharacterSerializer(serializers.ModelSerializer): - class Meta: - model = Account - fields = ["character_settings"] diff --git a/src/account/api/urls.py b/src/account/api/urls.py deleted file mode 100644 index 4962801..0000000 --- a/src/account/api/urls.py +++ /dev/null @@ -1,15 +0,0 @@ -from knox import views as knox_views -from django.urls import path - -from account.api.views import Login, AccountById, CharacterById, RegisterAccount - -app_name = "account" - -urlpatterns = [ - path("register/", RegisterAccount.as_view(), name="Register"), - path("login/", Login.as_view(), name="Login"), - path("logout/", knox_views.LogoutView.as_view(), name="Logout"), - path("logoutall/", knox_views.LogoutAllView.as_view(), name="Logout All"), - path("users//", AccountById.as_view(), name="Get account"), - path("users//characters/", CharacterById.as_view(), name="Get Characters"), -] diff --git a/src/account/api/views.py b/src/account/api/views.py deleted file mode 100644 index f2b38d3..0000000 --- a/src/account/api/views.py +++ /dev/null @@ -1,127 +0,0 @@ -from knox.views import LoginView -from knox.models import AuthToken -from rest_framework import status, generics -from django.contrib.auth import login -from django.core.exceptions import PermissionDenied -from rest_framework.response import Response -from rest_framework.permissions import AllowAny -from rest_framework.authtoken.serializers import AuthTokenSerializer - -from account.models import Account -from account.api.serializers import ( - AccountSerializer, - UpdateAccountSerializer, - RegisterAccountSerializer, - UpdateCharacterSerializer, -) - - -class RegisterAccount(generics.GenericAPIView): - serializer_class = RegisterAccountSerializer - permission_classes = [AllowAny] - - def post(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - data = dict() - st = status.HTTP_400_BAD_REQUEST - - if serializer.is_valid(): - account = serializer.save() - data["success"] = "successfully registered new user." - data["email"] = account.email - data["username"] = account.username - data["token"] = AuthToken.objects.create(account)[1] - st = status.HTTP_201_CREATED - else: - data = serializer.errors - - return Response(status=st, data=data) - - -class Login(LoginView): - permission_classes = [AllowAny] - - def post(self, request, **kwargs): - serializer = AuthTokenSerializer(data=request.data) - st = status.HTTP_400_BAD_REQUEST - - if serializer.is_valid(): - account = serializer.validated_data["user"] - login(request, account) - data = super(Login, self).post(request, format=None) - data["success"] = "Successfully logged in" - return data - else: - data = serializer.errors - - return Response(data=data, status=st) - - -class AccountById(generics.RetrieveUpdateAPIView): - serializer_class = UpdateAccountSerializer - lookup_field = "user_id" - - def get(self, request, *args, **kwargs): - try: - account = Account.objects.get(user_id=self.kwargs["user_id"]) - - if not request.user.is_staff and not account == request.user: - raise PermissionDenied - - except Account.DoesNotExist: - return Response(status=status.HTTP_404_NOT_FOUND) - except PermissionDenied: - return Response(status=status.HTTP_403_FORBIDDEN) - - serialized = AccountSerializer(account) - - return Response(serialized.data, status=status.HTTP_200_OK) - - def put(self, request, *args, **kwargs): - try: - account = Account.objects.get(user_id=self.kwargs["user_id"]) - - if not request.user == account: - raise PermissionDenied - except Account.DoesNotExist: - return Response(status=status.HTTP_404_NOT_FOUND) - except PermissionDenied: - return Response(status=status.HTTP_403_FORBIDDEN) - - return self.update(request, *args, **kwargs) - - def get_queryset(self): - return Account.objects.filter(user_id=self.kwargs["user_id"]) - - -class CharacterById(generics.RetrieveUpdateAPIView): - serializer_class = UpdateCharacterSerializer - lookup_field = "user_id" - - def get(self, request, *args, **kwargs): - try: - character = Account.objects.get(user_id=self.kwargs["user_id"]) - if request.user != character: - raise PermissionDenied - except Account.DoesNotExist: - return Response(status=status.HTTP_403_FORBIDDEN) - except PermissionDenied: - return Response(status=status.HTTP_403_FORBIDDEN) - - serialized = AccountSerializer(character) - - return Response( - serialized.data.get("character_settings"), status=status.HTTP_200_OK - ) - - def put(self, request, *args, **kwargs): - try: - character = Account.objects.get(user_id=self.kwargs["user_id"]) - if request.user != character: - raise PermissionDenied - except Account.DoesNotExist: - return Response(status=status.HTTP_404_NOT_FOUND) - except PermissionDenied: - return Response(status=status.HTTP_403_FORBIDDEN) - - self.update(request, *args, **kwargs) diff --git a/src/account/apps.py b/src/account/apps.py deleted file mode 100644 index 9870351..0000000 --- a/src/account/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class AccountConfig(AppConfig): - name = "account" diff --git a/src/account/forms.py b/src/account/forms.py deleted file mode 100644 index 17ae4bf..0000000 --- a/src/account/forms.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.conf import settings -from django.contrib.auth.forms import UserChangeForm, UserCreationForm -from django_email_verification import sendConfirm - -from .models import Account - - -class AccountCreationForm(UserCreationForm): - class Meta: - model = Account - fields = ("email", "username") - - def save(self, commit=True): - self.clean() - user = super().save(commit=False) - user.set_password(self.cleaned_data["password1"]) - if commit and settings.REQUIRE_EMAIL_CONFIRMATION: - sendConfirm(user) - elif commit: - user.is_active = True - user.save() - return user - - -class AccountChangeForm(UserChangeForm): - class Meta: - model = Account - fields = ("email", "username") diff --git a/src/account/migrations/0002_auto_20201228_0006.py b/src/account/migrations/0002_auto_20201228_0006.py deleted file mode 100644 index 005a4b5..0000000 --- a/src/account/migrations/0002_auto_20201228_0006.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.1.2 on 2020-12-28 03:06 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('account', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='account', - name='username', - field=models.CharField(max_length=50, unique=True, verbose_name='account name'), - ), - ] diff --git a/src/account/models.py b/src/account/models.py deleted file mode 100644 index 6fbc8a9..0000000 --- a/src/account/models.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.db import models -from django.contrib.auth.models import AbstractUser - -from .utils import PushID -from .validators import NoBadWordsValidator, AccountNameValidator - - -class Account(AbstractUser): - email = models.EmailField(verbose_name="email address", unique=True) - username = models.CharField( - verbose_name="account name", - max_length=28, - unique=True, - blank=False, - validators=[AccountNameValidator(), NoBadWordsValidator()], - ) - user_id = models.CharField(verbose_name="user id", max_length=28, primary_key=True) - character_settings = models.JSONField(null=True) - - USERNAME_FIELD = "email" - REQUIRED_FIELDS = ["username"] - - def __str__(self): - return self.username - - def save(self, *args, **kwargs): - self.clean() - if not self.user_id: - self.user_id = PushID.next_id() - super().save(*args, **kwargs) diff --git a/src/account/tests.py b/src/account/tests.py deleted file mode 100644 index 3fdded7..0000000 --- a/src/account/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase # NOQA # remove when used - -# Create your tests here. diff --git a/src/account/utils.py b/src/account/utils.py deleted file mode 100644 index 265d7ed..0000000 --- a/src/account/utils.py +++ /dev/null @@ -1,68 +0,0 @@ -import math -import time -import random - - -class PushID: - # Modeled after base64 web-safe chars, but ordered by ASCII. - PUSH_CHARS = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz" - - # Lengths of resulting UID and it's parts - UID_CHARS = 20 - - UID_TIMESTAMP_CHARS = 8 - UID_RANDOM_CHARS = UID_CHARS - UID_TIMESTAMP_CHARS - - # Shortcut - _push_chars_len = len(PUSH_CHARS) - - # Timestamp of last push, used to prevent local collisions if you - # push multiple times in one ms. - _last_push_time = 0 - - # We generate 72-bits of randomness which get turned into 12 - # characters and appended to the timestamp to prevent - # collisions with other clients. We store the last characters - # we generated because in the event of a collision, we'll use - # those same characters except "incremented" by one. - _last_rand_chars = [0] * UID_RANDOM_CHARS - - @classmethod - def next_id(cls): - now = math.floor(time.time() * 1000) - same_millisecond = now == cls._last_push_time - - cls._last_push_time = now - - timestamp_chars = [0] * cls.UID_TIMESTAMP_CHARS - - for i in reversed(range(cls.UID_TIMESTAMP_CHARS)): - timestamp_chars[i] = cls.PUSH_CHARS[now % cls._push_chars_len] - now = math.floor(now / cls._push_chars_len) - - assert now == 0, "We should have converted the entire timestamp" - - uid = "".join(timestamp_chars) - - if same_millisecond: - # If the timestamp hasn't changed since last push, use the - # same random number, except we increment it. - for i in reversed(range(cls.UID_RANDOM_CHARS)): - if cls._last_rand_chars[i] == cls._push_chars_len - 1: - cls._last_rand_chars[i] = 0 - else: - break - - cls._last_rand_chars[i] += 1 - else: - for i in range(cls.UID_RANDOM_CHARS): - cls._last_rand_chars[i] = random.randint(0, cls._push_chars_len - 1) - - for i in range(cls.UID_RANDOM_CHARS): - uid += cls.PUSH_CHARS[cls._last_rand_chars[i]] - - assert ( - len(uid) == cls.UID_CHARS - ), f"UID length should be {cls.UID_CHARS}, got {len(uid)}: {uid}" - - return uid diff --git a/src/account/validators.py b/src/account/validators.py deleted file mode 100644 index 941387f..0000000 --- a/src/account/validators.py +++ /dev/null @@ -1,41 +0,0 @@ -import re - -from django.core import validators -from django.utils.deconstruct import deconstructible -from django.contrib.auth.validators import ASCIIUsernameValidator - - -@deconstructible() -class AccountNameValidator(ASCIIUsernameValidator): - regex = r"^[\w\s@+-]+\Z" - message = ( - "Enter a valid username. This value may contain only English letters, " - "numbers, spaces, and +/-/_ characters." - ) - - -@deconstructible() -class NoBadWordsValidator(validators.RegexValidator): - badwords: str - inverse_match = True - flags = re.IGNORECASE - message = ( - "Please don't use slurs or bad words for your username." - "\nUnitystation is meant to be a fun place for everyone!" - ) - - def __call__(self, value): - self.generate_bad_words_regex() - super().__call__(value) - - def generate_bad_words_regex(self): - formatted_regex = r"[\w@+-_]*({0})[\w@+-_]*" # todo this regex might be weak af, improve if you know how - - try: - with open("badwords.txt", "r", encoding="UTF-8") as f: - bad_list = f.readlines() - except FileNotFoundError: - self.inverse_match = False - return - bad_list = [w.replace("\n", "").strip() for w in bad_list] - self.regex = re.compile(formatted_regex.format("|".join(bad_list)), self.flags) diff --git a/src/account/__init__.py b/src/accounts/__init__.py similarity index 100% rename from src/account/__init__.py rename to src/accounts/__init__.py diff --git a/src/accounts/admin.py b/src/accounts/admin.py new file mode 100644 index 0000000..8410661 --- /dev/null +++ b/src/accounts/admin.py @@ -0,0 +1,39 @@ +from django.contrib import admin + +from .models import Account + + +@admin.register(Account) +class AccountAdminView(admin.ModelAdmin): + list_display = ( + "email", + "account_identifier", + "username", + "is_verified", + "legacy_id", + "characters_data", + "is_authorized_server", + ) + fieldsets = ( + ( + "Account basic data", + { + "classes": ("wide",), + "fields": ( + "email", + "account_identifier", + "username", + "verification_token", + ), + }, + ), + ("Characters", {"classes": ("wide",), "fields": ("characters_data",)}), + ( + "Authorization", + { + "classes": ("wide",), + "fields": ("is_active", "is_verified", "is_authorized_server"), + }, + ), + ("Legacy", {"classes": ("wide",), "fields": ("legacy_id",)}), + ) diff --git a/src/account/api/__init__.py b/src/accounts/api/__init__.py similarity index 100% rename from src/account/api/__init__.py rename to src/accounts/api/__init__.py diff --git a/src/accounts/api/serializers.py b/src/accounts/api/serializers.py new file mode 100644 index 0000000..4efd8c0 --- /dev/null +++ b/src/accounts/api/serializers.py @@ -0,0 +1,101 @@ +from django.conf import settings +from rest_framework import serializers +from django.contrib.auth import authenticate +from django_email_verification import sendConfirm + +from ..models import Account + + +class PublicAccountDataSerializer(serializers.ModelSerializer): + class Meta: + model = Account + fields = ( + "account_identifier", + "username", + "legacy_id", + "is_verified", + "is_authorized_server", + "characters_data", + ) + + +class RegisterAccountSerializer(serializers.ModelSerializer): + class Meta: + model = Account + fields = ("account_identifier", "username", "password", "email") + extra_kwargs = {"password": {"write_only": True}} + + def create(self, validated_data): + """Create and return a new account""" + account = Account.objects.create_user(**validated_data) + if settings.REQUIRE_EMAIL_CONFIRMATION: + sendConfirm(account) + return account + + +class LoginWithCredentialsSerializer(serializers.Serializer): + + email = serializers.EmailField() + password = serializers.CharField(style={"input_type": "password"}) + + def validate(self, data): + account = authenticate(username=data["email"], password=data["password"]) + if account is None: + raise serializers.ValidationError( + "Unable to login with provided credentials." + ) + if not account.is_active: + raise serializers.ValidationError( + "This account hasn't done the mail confirmation step or has been disabled." + ) + return account + + +class UpdateAccountSerializer(serializers.ModelSerializer): + class Meta: + model = Account + fields = ("username", "email", "password") + extra_kwargs = {"password": {"write_only": True}} + + def update(self, instance, validated_data): + old_email = instance.email + instance.username = validated_data.get("username", instance.username) + instance.email = validated_data.get("email", instance.email) + instance.set_password(validated_data.get("password", instance.password)) + + if old_email != instance.email and settings.REQUIRE_EMAIL_CONFIRMATION: + instance.is_active = False + sendConfirm(instance) + + instance.save() + return instance + + +class UpdateCharactersSerializer(serializers.ModelSerializer): + class Meta: + model = Account + fields = ("characters_data",) + + def update(self, instance, validated_data): + instance.characters_data = validated_data.get( + "characters_data", instance.characters_data + ) + instance.save() + return instance + + +class VerifyAccountSerializer(serializers.Serializer): + account_identifier = serializers.CharField() + verification_token = serializers.UUIDField() + + def validate(self, data): + account = Account.objects.get(account_identifier=data["account_identifier"]) + + data_token = data["verification_token"] + account_token = account.verification_token + + if account_token != data_token: + raise serializers.ValidationError( + "Verification token seems invalid or maybe outdated. Try requesting a new one." + ) + return account diff --git a/src/accounts/api/urls.py b/src/accounts/api/urls.py new file mode 100644 index 0000000..9e054d9 --- /dev/null +++ b/src/accounts/api/urls.py @@ -0,0 +1,37 @@ +from knox import views as knox_views +from django.urls import path + +from .views import ( + UpdateAccountView, + VerifyAccountView, + LoginWithTokenView, + RegisterAccountView, + UpdateCharactersView, + PublicAccountDataView, + LoginWithCredentialsView, + RequestVerificationTokenView, +) + +app_name = "account" + +urlpatterns = [ + path("login-token", LoginWithTokenView.as_view(), name="login-token"), + path( + "login-credentials", + LoginWithCredentialsView.as_view(), + name="login-credentials", + ), + path("register", RegisterAccountView.as_view(), name="register"), + path("update-account", UpdateAccountView.as_view(), name="update"), + path("update-characters", UpdateCharactersView.as_view(), name="update-characters"), + path("account", PublicAccountDataView.as_view(), name="public-data"), + path("account/", PublicAccountDataView.as_view(), name="public-data"), + path("logout", knox_views.LogoutView.as_view(), name="logout"), + path("logoutall", knox_views.LogoutAllView.as_view(), name="logoutall"), + path( + "request-verification-token", + RequestVerificationTokenView.as_view(), + name="request-verification-token", + ), + path("verify-account", VerifyAccountView.as_view(), name="verify-account"), +] diff --git a/src/accounts/api/views.py b/src/accounts/api/views.py new file mode 100644 index 0000000..4ccf0da --- /dev/null +++ b/src/accounts/api/views.py @@ -0,0 +1,224 @@ +from uuid import uuid4 + +from knox.views import LoginView as KnoxLoginView +from knox.models import AuthToken +from rest_framework import status +from django.core.exceptions import PermissionDenied, ObjectDoesNotExist +from rest_framework.generics import GenericAPIView, RetrieveAPIView +from rest_framework.response import Response +from rest_framework.permissions import AllowAny +from rest_framework.serializers import ValidationError + +from ..models import Account +from ..exceptions import MissingMailConfirmation +from .serializers import ( + UpdateAccountSerializer, + VerifyAccountSerializer, + RegisterAccountSerializer, + UpdateCharactersSerializer, + PublicAccountDataSerializer, + LoginWithCredentialsSerializer, +) + + +class PublicAccountDataView(RetrieveAPIView): + permission_classes = (AllowAny,) + queryset = Account.objects.all() + serializer_class = PublicAccountDataSerializer + + +class LoginWithTokenView(KnoxLoginView): + permission_classes = (AllowAny,) + + def get_post_response_data(self, request, token, instance): + try: + if not request.user.is_active: + raise MissingMailConfirmation() + except MissingMailConfirmation as e: + return {"error": e.detail} + + serializer = self.get_user_serializer_class() + + data = {"token": token} + if serializer is not None: + data["user"] = serializer(request.user, context=self.get_context()).data + return data + + def get_user_serializer_class(self): + return PublicAccountDataSerializer + + +class LoginWithCredentialsView(GenericAPIView): + permission_classes = (AllowAny,) + serializer_class = LoginWithCredentialsSerializer + + def post(self, request): + serializer = self.serializer_class(data=request.data) + try: + serializer.is_valid(raise_exception=True) + account = Account.objects.get(email=serializer.data["email"]) + if not account.is_active: + raise MissingMailConfirmation() + except ObjectDoesNotExist: + return Response( + data={"error": "account doesn't exist!"}, + status=status.HTTP_404_NOT_FOUND, + ) + except MissingMailConfirmation as e: + return Response(data={"error": e.detail}, status=e.status_code) + except ValidationError as e: + return Response(data={"error": e.detail}, status=e.status_code) + except Exception as e: + return Response( + data={"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + account = serializer.validated_data + + return Response( + { + "account": PublicAccountDataSerializer( + account, context=self.get_serializer_context() + ).data, + "token": AuthToken.objects.create(account)[1], + }, + status=status.HTTP_200_OK, + ) + + +class RegisterAccountView(GenericAPIView): + permission_classes = (AllowAny,) + serializer_class = RegisterAccountSerializer + + def post(self, request): + serializer = self.get_serializer(data=request.data) + try: + serializer.is_valid(raise_exception=True) + except ValidationError as e: + return Response(data={"error": str(e)}, status=e.status_code) + except Exception as e: + return Response( + data={"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + account = serializer.save() + + return Response( + { + "account": RegisterAccountSerializer( + account, context=self.get_serializer_context() + ).data, + "token": AuthToken.objects.create(account)[1], + }, + status=status.HTTP_200_OK, + ) + + +class UpdateAccountView(GenericAPIView): + serializer_class = UpdateAccountSerializer + + def post(self, request): + try: + account = Account.objects.get(pk=request.user.pk) + if request.user != account: + raise PermissionDenied + except ObjectDoesNotExist: + return Response( + {"error": "Account does not exist."}, status=status.HTTP_404_NOT_FOUND + ) + except PermissionDenied: + return Response( + {"error": "You have no permission to do this action."}, + status=status.HTTP_403_FORBIDDEN, + ) + + serializer = self.get_serializer(account, data=request.data) + try: + serializer.is_valid(raise_exception=True) + except ValidationError as e: + return Response(data={"error": str(e)}, status=e.status_code) + except Exception as e: + return Response( + data={"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + + +class UpdateCharactersView(GenericAPIView): + serializer_class = UpdateCharactersSerializer + + def post(self, request): + try: + account = Account.objects.get(pk=request.user.pk) + if request.user != account: + raise PermissionDenied + except ObjectDoesNotExist: + return Response( + {"error": "Account does not exist."}, status=status.HTTP_404_NOT_FOUND + ) + except PermissionDenied: + return Response( + {"error": "You have no permission to do this action."}, + status=status.HTTP_403_FORBIDDEN, + ) + + serializer = self.get_serializer(account, data=request.data) + try: + serializer.is_valid(raise_exception=True) + except ValidationError as e: + return Response(data={"error": str(e)}, status=e.status_code) + except Exception as e: + return Response( + data={"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + + +class RequestVerificationTokenView(GenericAPIView): + def get(self, *args, **kwargs): + verification_token = uuid4() + try: + account = Account.objects.get(pk=self.request.user.pk) + if self.request.user != account: + raise PermissionDenied + except ObjectDoesNotExist: + return Response( + {"error": "Account does not exist."}, status=status.HTTP_404_NOT_FOUND + ) + except PermissionDenied: + return Response( + {"error": "You have no permission to do this action."}, + status=status.HTTP_403_FORBIDDEN, + ) + account.verification_token = verification_token + account.save() + return Response( + { + "account_identifier": account.account_identifier, + "verification_token": verification_token, + }, + status=status.HTTP_200_OK, + ) + + +class VerifyAccountView(GenericAPIView): + permission_classes = (AllowAny,) + serializer_class = VerifyAccountSerializer + + def post(self, request): + serializer = self.get_serializer(data=request.data) + try: + serializer.is_valid(raise_exception=True) + except ValidationError as e: + return Response(data={"error": str(e)}, status=e.status_code) + except Exception as e: + return Response( + data={"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + account = Account.objects.get( + account_identifier=serializer.data["account_identifier"] + ) + public_data = PublicAccountDataSerializer(account).data + + return Response(public_data, status=status.HTTP_200_OK) diff --git a/src/accounts/apps.py b/src/accounts/apps.py new file mode 100644 index 0000000..0cb51e6 --- /dev/null +++ b/src/accounts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "accounts" diff --git a/src/accounts/exceptions.py b/src/accounts/exceptions.py new file mode 100644 index 0000000..3214bb6 --- /dev/null +++ b/src/accounts/exceptions.py @@ -0,0 +1,8 @@ +from rest_framework import status + + +class MissingMailConfirmation(Exception): + """Account is trying to login without confirming their email first""" + + detail = "You must confirm your email before attempting to login." + status_code = status.HTTP_418_IM_A_TEAPOT diff --git a/src/account/migrations/0001_initial.py b/src/accounts/migrations/0001_initial.py similarity index 53% rename from src/account/migrations/0001_initial.py rename to src/accounts/migrations/0001_initial.py index 4c0d313..d97fdc2 100644 --- a/src/account/migrations/0001_initial.py +++ b/src/accounts/migrations/0001_initial.py @@ -1,8 +1,10 @@ -# Generated by Django 3.1.2 on 2020-11-15 20:08 +# Generated by Django 3.2.9 on 2022-02-07 03:55 +import accounts.validators import django.contrib.auth.models from django.db import migrations, models import django.utils.timezone +import uuid class Migration(migrations.Migration): @@ -25,10 +27,14 @@ class Migration(migrations.Migration): ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), - ('username', models.CharField(max_length=50, verbose_name='account name')), - ('user_id', models.CharField(max_length=28, primary_key=True, serialize=False, verbose_name='user id')), - ('character_settings', models.JSONField(null=True)), + ('email', models.EmailField(help_text='Email address must be unique. It is used to login and confirm the account.', max_length=254, unique=True, verbose_name='Email address')), + ('account_identifier', models.CharField(help_text="Account identifier is used to identify your account. This will be used for bans, job bans, etc and can't ever be changed", max_length=28, primary_key=True, serialize=False, validators=[accounts.validators.AccountNameValidator()], verbose_name='Account identifier')), + ('username', models.CharField(help_text='Public username is used to identify your account publicly and shows in OOC. This can be changed at any time', max_length=28, validators=[accounts.validators.UsernameValidator()], verbose_name='Public username')), + ('is_verified', models.BooleanField(default=False, help_text='Is this account verified to be who they claim to be? Are they famous?!', verbose_name='Verified')), + ('legacy_id', models.CharField(blank=True, default='null', help_text="Legacy ID is used to identify your account in the old database. This is used for bans, job bans, etc and can't ever be changed", max_length=28, verbose_name='Legacy ID')), + ('characters_data', models.JSONField(default=dict, help_text='Characters data is used to store all the characters associated with this account.', verbose_name='Characters data')), + ('is_authorized_server', models.BooleanField(default=False, help_text='Can this account broadcast the server state to the server list api? Can this account write to persistence layer?', verbose_name='Authorized server')), + ('verification_token', models.UUIDField(blank=True, default=uuid.UUID('05e74bff-c4b1-4452-ac03-b20805ac4fef'), verbose_name='Verification token')), ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), ], diff --git a/src/account/migrations/__init__.py b/src/accounts/migrations/__init__.py similarity index 100% rename from src/account/migrations/__init__.py rename to src/accounts/migrations/__init__.py diff --git a/src/accounts/models.py b/src/accounts/models.py new file mode 100644 index 0000000..93c2d9e --- /dev/null +++ b/src/accounts/models.py @@ -0,0 +1,69 @@ +from uuid import uuid4 + +from django.db import models +from django.contrib.auth.models import AbstractUser + +from .validators import UsernameValidator, AccountNameValidator + + +class Account(AbstractUser): + + email = models.EmailField( + verbose_name="Email address", + unique=True, + help_text="Email address must be unique. It is used to login and confirm the account.", + ) + + account_identifier = models.CharField( + verbose_name="Account identifier", + max_length=28, + primary_key=True, + validators=[AccountNameValidator()], + help_text="Account identifier is used to identify your account. This will be used for bans, job bans, etc and can't ever be changed", + ) + + username = models.CharField( + verbose_name="Public username", + max_length=28, + unique=False, + validators=[UsernameValidator()], + help_text="Public username is used to identify your account publicly and shows in OOC. This can be changed at any time", + ) + + is_verified = models.BooleanField( + default=False, + verbose_name="Verified", + help_text="Is this account verified to be who they claim to be? Are they famous?!", + ) + + legacy_id = models.CharField( + verbose_name="Legacy ID", + max_length=28, + blank=True, + default="null", + help_text="Legacy ID is used to identify your account in the old database. This is used for bans, job bans, etc and can't ever be changed", + ) + + characters_data = models.JSONField( + verbose_name="Characters data", + default=dict, + help_text="Characters data is used to store all the characters associated with this account.", + ) + + is_authorized_server = models.BooleanField( + default=False, + verbose_name="Authorized server", + help_text="Can this account broadcast the server state to the server list api? Can this account write to persistence layer?", + ) + + verification_token = models.UUIDField( + verbose_name="Verification token", + blank=True, + default=uuid4(), + ) + + USERNAME_FIELD = "email" + REQUIRED_FIELDS = ["account_identifier", "username"] + + def __str__(self): + return f"{self.account_identifier} as {self.username}" diff --git a/src/accounts/validators.py b/src/accounts/validators.py new file mode 100644 index 0000000..9ef76f6 --- /dev/null +++ b/src/accounts/validators.py @@ -0,0 +1,22 @@ +from django.utils.deconstruct import deconstructible +from django.contrib.auth.validators import ASCIIUsernameValidator + + +@deconstructible() +class AccountNameValidator(ASCIIUsernameValidator): + # match account identifier that only has letters, numbers, dash and underscore and at least 3 characters + regex = r"^[a-zA-Z0-9_\-]{3,}$" + message = ( + "Enter a valid account identifier. This value may contain only English letters, " + "numbers, and -/_ characters." + ) + + +@deconstructible() +class UsernameValidator(ASCIIUsernameValidator): + # match username that has at least 3 characters, letters, numbers, dashes, underscores, dots, and spaces but no consecutive whitespaces + regex = r"^[a-zA-Z0-9\.\-_](?!.* {2})[ \w.-_]{2,}$" + message = ( + "Enter a valid username. This value may contain only English letters, " + "numbers, dashes, underscores, dots, and spaces." + ) diff --git a/src/unitystation_auth/__init__.py b/src/central-command/__init__.py similarity index 100% rename from src/unitystation_auth/__init__.py rename to src/central-command/__init__.py diff --git a/src/unitystation_auth/asgi.py b/src/central-command/asgi.py similarity index 70% rename from src/unitystation_auth/asgi.py rename to src/central-command/asgi.py index 0fabbd7..8f50bfd 100644 --- a/src/unitystation_auth/asgi.py +++ b/src/central-command/asgi.py @@ -1,5 +1,5 @@ """ -ASGI config for unitystation_auth project. +ASGI config for central-command project. It exposes the ASGI callable as a module-level variable named ``application``. @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "unitystation_auth.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "central-command.settings") application = get_asgi_application() diff --git a/src/unitystation_auth/settings.py b/src/central-command/settings.py similarity index 72% rename from src/unitystation_auth/settings.py rename to src/central-command/settings.py index fc998eb..1b6fb5c 100644 --- a/src/unitystation_auth/settings.py +++ b/src/central-command/settings.py @@ -1,5 +1,5 @@ """ -Django settings for unitystation_auth project. +Django settings for central-command project. Generated by 'django-admin startproject' using Django 3.1.2. @@ -23,12 +23,12 @@ # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.environ["SECRET_KEY"] +SECRET_KEY = os.environ.get("SECRET_KEY", "foo") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.environ.get("DJANGO_DEBUG", "1") == "1" +DEBUG = bool(os.environ.get("DJANGO_DEBUG", default="1")) -ALLOWED_HOSTS = ["*"] +ALLOWED_HOSTS = ["*"] if DEBUG else ["localhost", "127.0.0.1"] # Application definition @@ -43,12 +43,12 @@ "django_email_verification", "rest_framework", "knox", - "account", - "website", + "accounts", "persistence", ] -AUTH_USER_MODEL = "account.Account" +# What user model to use for authentication? +AUTH_USER_MODEL = "accounts.Account" MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", @@ -60,7 +60,7 @@ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = "unitystation_auth.urls" +ROOT_URLCONF = "central-command.urls" TEMPLATES = [ { @@ -78,7 +78,7 @@ }, ] -WSGI_APPLICATION = "unitystation_auth.wsgi.application" +WSGI_APPLICATION = "central-command.wsgi.application" # Database @@ -87,11 +87,11 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", - "NAME": os.environ.get("POSTGRES_DB", "postgres"), - "USER": os.environ.get("POSTGRES_USER", "postgres"), - "PASSWORD": os.environ.get("POSTGRES_PASSWORD", ""), - "HOST": os.environ.get("POSTGRES_HOST", "db"), - "PORT": os.environ.get("POSTGRES_PORT", ""), + "NAME": os.environ.get("DB_NAME", BASE_DIR / "db.sqlite3"), + "USER": os.environ.get("DB_USER", "postgres"), + "PASSWORD": os.environ.get("DB_PASSWORD", ""), + "HOST": os.environ.get("DB_HOST", "localhost"), + "PORT": os.environ.get("DB_PORT", ""), } } @@ -107,23 +107,23 @@ # Email settings EMAIL_USE_TLS = True -EMAIL_HOST = os.environ["EMAIL_HOST"] -EMAIL_PORT = int(os.environ["EMAIL_PORT"]) -EMAIL_HOST_USER = os.environ["EMAIL_HOST_USER"] -EMAIL_HOST_PASSWORD = os.environ["EMAIL_HOST_PASSWORD"] +EMAIL_HOST = os.environ.get("EMAIL_HOST") +EMAIL_PORT = int(os.environ.get("EMAIL_PORT", 1337)) +EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER") +EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD") EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" REQUIRE_EMAIL_CONFIRMATION = True # Email confirmation settings EMAIL_ACTIVE_FIELD = "is_active" -EMAIL_SERVER = os.environ["EMAIL_HOST"] -EMAIL_ADDRESS = os.environ["EMAIL_HOST_USER"] -EMAIL_FROM_ADDRESS = os.environ["EMAIL_HOST_USER"] -EMAIL_PASSWORD = os.environ["EMAIL_HOST_PASSWORD"] +EMAIL_SERVER = os.environ.get("EMAIL_HOST") +EMAIL_ADDRESS = os.environ.get("EMAIL_HOST_USER") +EMAIL_FROM_ADDRESS = os.environ.get("EMAIL_HOST_USER") +EMAIL_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD") EMAIL_MAIL_SUBJECT = "Confirm your Unitystation account" EMAIL_MAIL_HTML = "registration/confirmation_email.html" EMAIL_PAGE_TEMPLATE = "registration/confirm_template.html" -EMAIL_PAGE_DOMAIN = os.environ["EMAIL_PAGE_DOMAIN"] +EMAIL_PAGE_DOMAIN = os.environ.get("EMAIL_PAGE_DOMAIN") # Password validation # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators @@ -161,8 +161,12 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.1/howto/static-files/ -STATICFILES_DIRS = [Path(BASE_DIR, "website", "statics")] - STATIC_URL = "/static/" -LOGIN_REDIRECT_URL = "home" -LOGOUT_REDIRECT_URL = "home" +MEDIA_URL = "/media/" +STATIC_ROOT = Path("/home", "website", "statics") +MEDIA_ROOT = Path("/home", "website", "media") + +# LOGIN_REDIRECT_URL = "home" +# LOGOUT_REDIRECT_URL = "home" + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/src/unitystation_auth/urls.py b/src/central-command/urls.py similarity index 73% rename from src/unitystation_auth/urls.py rename to src/central-command/urls.py index bf7dfbf..f77c43f 100644 --- a/src/unitystation_auth/urls.py +++ b/src/central-command/urls.py @@ -1,4 +1,4 @@ -"""unitystation_auth URL Configuration +"""central-command URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/3.1/topics/http/urls/ @@ -15,16 +15,14 @@ """ from django.urls import path, include from django.contrib import admin -from django.views.generic.base import TemplateView from django_email_verification import urls as mail_urls urlpatterns = [ - path("", TemplateView.as_view(template_name="home.html"), name="home"), + # path(r'', admin.site.urls), path("admin/", admin.site.urls), - path("", include("website.urls")), path("", include("django.contrib.auth.urls")), path("email/", include(mail_urls)), # API REST FRAMEWORK - path("api/", include("account.api.urls", "Account api")), - path("api/", include("persistence.api.urls", "Poly says")), + path("api/accounts/", include("accounts.api.urls", "Accounts API")), + path("api/persistence/", include("persistence.api.urls")), ] diff --git a/src/unitystation_auth/wsgi.py b/src/central-command/wsgi.py similarity index 70% rename from src/unitystation_auth/wsgi.py rename to src/central-command/wsgi.py index 5040129..daef56e 100644 --- a/src/unitystation_auth/wsgi.py +++ b/src/central-command/wsgi.py @@ -1,5 +1,5 @@ """ -WSGI config for unitystation_auth project. +WSGI config for central-command project. It exposes the WSGI callable as a module-level variable named ``application``. @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "unitystation_auth.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "central-command.settings") application = get_wsgi_application() diff --git a/src/entrypoint.sh b/src/entrypoint.sh new file mode 100644 index 0000000..8656db8 --- /dev/null +++ b/src/entrypoint.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +if [ -n "$DB_NAME" ] && [ "$DB_NAME" = "postgres" ] +then + echo "Waiting for postgres..." + + if [ -z "$DB_HOST" ] + then host="db" + else host="$DB_HOST" + fi + + if [ -z "$DB_PORT" ] + then port="5432" + else port="$DB_PORT" + fi + + while ! nc -z $host $port; do + sleep 0.1 + done + + echo "PostgreSQL started" +fi + +# flush all data in the database +#python manage.py flush --no-input + +# takes ORM models and makes the sql statements, we shouldn't need to do this since we're committing the migrations +python manage.py makemigrations --noinput + +# applies migrations and creates/updates/deletes tables +python manage.py migrate --noinput + +# collects static files +python manage.py collectstatic --no-input + +exec "$@" \ No newline at end of file diff --git a/src/manage.py b/src/manage.py index 88a8e4b..ba17d9a 100644 --- a/src/manage.py +++ b/src/manage.py @@ -3,10 +3,13 @@ import os import sys +from dotenv import load_dotenv + def main(): """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "unitystation_auth.settings") + load_dotenv() + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "central-command.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/src/persistence/admin.py b/src/persistence/admin.py index 0c6a0d0..3bbf9ca 100644 --- a/src/persistence/admin.py +++ b/src/persistence/admin.py @@ -1,22 +1,13 @@ from django.contrib import admin -from .models import Book, BookPage, PolyPhrase, BookCategory +from .models import Other, PolyPhrase -class InlineBookPage(admin.StackedInline): - model = BookPage - extra = 1 +@admin.register(Other) +class OtherAdminView(admin.ModelAdmin): + pass -@admin.register(Book) -class BookAdmin(admin.ModelAdmin): - fields = ["title", "categories"] - inlines = [InlineBookPage] - - list_display = ["title", "isbn"] - list_filter = ["categories"] - search_fields = ["title", "isbn"] - - -admin.site.register(PolyPhrase) -admin.site.register(BookCategory) +@admin.register(PolyPhrase) +class PolyPhraseAdminView(admin.ModelAdmin): + pass diff --git a/src/persistence/api/serializers.py b/src/persistence/api/serializers.py index eae4ae5..047c9db 100644 --- a/src/persistence/api/serializers.py +++ b/src/persistence/api/serializers.py @@ -1,9 +1,18 @@ from rest_framework import serializers -from persistence.models import PolyPhrase +from ..models import Other, PolyPhrase -class PolyPhrasesSerializer(serializers.ModelSerializer): +class OtherSerializer(serializers.ModelSerializer): + class Meta: + model = Other + fields = ( + "account", + "other_data", + ) + + +class PolyPhraseSerializer(serializers.ModelSerializer): class Meta: model = PolyPhrase fields = ("id", "said_by", "phrase") diff --git a/src/persistence/api/urls.py b/src/persistence/api/urls.py index 2529f02..c041e0b 100644 --- a/src/persistence/api/urls.py +++ b/src/persistence/api/urls.py @@ -1,15 +1,17 @@ from django.urls import path -from persistence.api.views import ( - poly_phrase_by_id_view, - poly_store_phrase_view, - poly_random_phrase_view, +from .views import ( + ReadOtherDataView, + WriteOtherDataView, + WritePolyPhraseView, + RandomPolyPhraseView, ) app_name = "persistence" urlpatterns = [ - path("polysays/", poly_random_phrase_view, name="Poly says"), - path("polysays//", poly_phrase_by_id_view, name="Phrase by id"), - path("post/polyphrase", poly_store_phrase_view, name="Post poly phrase"), + path("other-data/read", ReadOtherDataView.as_view(), name="read"), + path("other-data/write", WriteOtherDataView.as_view(), name="write"), + path("poly-says", RandomPolyPhraseView.as_view(), name="poly-says"), + path("poly-hears", WritePolyPhraseView.as_view(), name="poly-hears"), ] diff --git a/src/persistence/api/views.py b/src/persistence/api/views.py index 3878082..47e74c1 100644 --- a/src/persistence/api/views.py +++ b/src/persistence/api/views.py @@ -1,50 +1,99 @@ from rest_framework import status, permissions +from django.core.exceptions import PermissionDenied, ObjectDoesNotExist +from rest_framework.generics import GenericAPIView from rest_framework.response import Response -from rest_framework.decorators import api_view, permission_classes +from rest_framework.exceptions import ValidationError -from persistence.models import PolyPhrase -from persistence.api.serializers import PolyPhrasesSerializer +from ..models import Other, PolyPhrase +from .serializers import OtherSerializer, PolyPhraseSerializer -@api_view(["GET"]) -def poly_phrase_by_id_view(request, phrase_id): - try: - phrase = PolyPhrase.objects.get(phrase_id=phrase_id) - except PolyPhrase.DoesNotExist: - return Response(status=status.HTTP_404_NOT_FOUND) +class ReadOtherDataView(GenericAPIView): + serializer_class = OtherSerializer - serialized = PolyPhrasesSerializer(phrase) - return Response(serialized.data) + def get(self, request): + try: + other = Other.objects.get(pk=request.user.pk) + except ObjectDoesNotExist: + data = {"error": "No data for this account could be found!"} + return Response(data, status=status.HTTP_404_NOT_FOUND) + except PermissionDenied: + data = {"error": "You do not have permission to view this data!"} + return Response(data, status=status.HTTP_403_FORBIDDEN) + serializer = self.serializer_class(other) + return Response(serializer.data, status=status.HTTP_200_OK) -@api_view(["GET"]) -@permission_classes([permissions.AllowAny]) -def poly_random_phrase_view(request): - try: - phrase = PolyPhrase.objects.order_by("?").first() - except Exception as e: - error = {"error": f"{e}"} - return Response(error, status=status.HTTP_500_INTERNAL_SERVER_ERROR) +class WriteOtherDataView(GenericAPIView): + serializer_class = OtherSerializer - serialized = PolyPhrasesSerializer(phrase) - return Response(serialized.data) + def post(self, request): + data = {"account": request.user.pk} + data["other_data"] = request.data.get("other_data") + try: + if not request.user.is_authorized_server: + raise PermissionDenied + other = Other.objects.get(pk=request.user.pk) + except ObjectDoesNotExist: + serializer = self.serializer_class(data=data) + return self.try_write_to_record(serializer) + except PermissionDenied: + data = {"error": "You do not have permission to edit this data!"} + return Response(data, status=status.HTTP_403_FORBIDDEN) + else: + data = self.update_other_data_dict(data, other.other_data) + serializer = self.serializer_class(other, data=data) + return self.try_write_to_record(serializer) -@api_view(["POST"]) -def poly_store_phrase_view(request): - try: - user = request.user - text = request.data.get("phrase") + def update_other_data_dict(self, new_data: dict, old_data: dict) -> dict: + final_data = {"account": new_data["account"]} + for key, value in new_data.get("other_data").items(): + old_data[key] = value + final_data["other_data"] = old_data + return final_data - if not user or not text: - raise Exception("Phrase and user saying it are required!") + def try_write_to_record(self, serializer: OtherSerializer) -> Response: + try: + serializer.is_valid(raise_exception=True) + except ValidationError as e: + data = {"error": e.detail} + return Response(data, status=status.HTTP_400_BAD_REQUEST) + else: + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) - phrase = PolyPhrase.objects.create(said_by=user, phrase=text) - phrase.save() +class RandomPolyPhraseView(GenericAPIView): + permission_classes = (permissions.AllowAny,) + serializer_class = PolyPhraseSerializer - except Exception as e: - error = {"error": f"{e}"} - return Response(error, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - else: - return Response(status=status.HTTP_200_OK) + def get(self, request): + try: + if PolyPhrase.objects.count() == 0: + raise ObjectDoesNotExist + phrase = PolyPhrase.objects.order_by("?").first() + except ObjectDoesNotExist: + data = {"error": "No phrases could be found!"} + return Response(data, status=status.HTTP_404_NOT_FOUND) + serializer = self.serializer_class(phrase) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class WritePolyPhraseView(GenericAPIView): + serializer_class = PolyPhraseSerializer + + def post(self, request): + serializer = self.serializer_class(data=request.data) + try: + serializer.is_valid(raise_exception=True) + if not request.user.is_authorized_server: + raise PermissionDenied + except ValidationError as e: + data = {"error": str(e)} + return Response(data, status=status.HTTP_400_BAD_REQUEST) + except PermissionDenied: + data = {"error": "You do not have permission to write this data!"} + return Response(data, status=status.HTTP_403_FORBIDDEN) + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) diff --git a/src/persistence/migrations/0001_initial.py b/src/persistence/migrations/0001_initial.py index fb31312..d738357 100644 --- a/src/persistence/migrations/0001_initial.py +++ b/src/persistence/migrations/0001_initial.py @@ -1,6 +1,5 @@ -# Generated by Django 3.1.2 on 2020-11-15 20:08 +# Generated by Django 3.2.9 on 2022-02-07 03:55 -from django.conf import settings from django.db import migrations, models import django.db.models.deletion @@ -10,46 +9,23 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('accounts', '0001_initial'), ] operations = [ migrations.CreateModel( - name='Book', + name='Other', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('isbn', models.CharField(max_length=13, unique=True, verbose_name='isbn')), - ('title', models.CharField(max_length=30, verbose_name='title')), - ], - ), - migrations.CreateModel( - name='BookCategory', - fields=[ - ('cat_id', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=50, verbose_name='category name')), - ('abbrev', models.CharField(max_length=3, verbose_name='abbreviation')), - ('description', models.CharField(max_length=150, verbose_name='description')), + ('account', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='accounts.account')), + ('other_data', models.JSONField(default=dict)), ], ), migrations.CreateModel( name='PolyPhrase', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('phrase', models.CharField(max_length=150, verbose_name='phrase')), - ('said_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL, verbose_name='said by')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('said_by', models.CharField(blank=True, default='Who knows?', max_length=28)), + ('phrase', models.CharField(max_length=128)), ], ), - migrations.CreateModel( - name='BookPage', - fields=[ - ('page_id', models.AutoField(primary_key=True, serialize=False)), - ('content', models.TextField(blank=True, max_length=600, verbose_name='page content')), - ('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='persistence.book')), - ], - ), - migrations.AddField( - model_name='book', - name='categories', - field=models.ManyToManyField(to='persistence.BookCategory'), - ), ] diff --git a/src/persistence/models.py b/src/persistence/models.py index 4ab8d9e..aa6cced 100644 --- a/src/persistence/models.py +++ b/src/persistence/models.py @@ -1,53 +1,26 @@ from django.db import models -from account.models import Account -from .utils import generate_isbn - - -class PolyPhrase(models.Model): - said_by = models.ForeignKey( - Account, - verbose_name="said by", - on_delete=models.DO_NOTHING, - null=True, +class Other(models.Model): + account = models.OneToOneField( + "accounts.Account", on_delete=models.CASCADE, primary_key=True ) - phrase = models.CharField(verbose_name="phrase", max_length=150) - - def __str__(self): - return f"phrase number {self.id}" - + """To what account/server is this extra unordered persistent data related to?""" -class BookCategory(models.Model): - cat_id = models.AutoField(primary_key=True) - name = models.CharField(verbose_name="category name", max_length=50) - abbrev = models.CharField(verbose_name="abbreviation", max_length=3) - description = models.CharField(verbose_name="description", max_length=150) + other_data = models.JSONField(default=dict) + """The extra unordered persistent data.""" def __str__(self): - return self.name + return f"{self.account.pk}'s other data" -class Book(models.Model): - isbn = models.CharField(verbose_name="isbn", max_length=13, unique=True) - title = models.CharField(verbose_name="title", max_length=30, blank=False) - categories = models.ManyToManyField(BookCategory) - - def __str__(self): - return f"{self.isbn} - {self.title}" - - def get_pages(self): - return self.bookpage_set.all().order_by("page_id") - - def save(self, *args, **kwargs): - self.isbn = generate_isbn() - super().save(*args, **kwargs) - +class PolyPhrase(models.Model): + said_by = models.CharField(max_length=28, blank=True, default="Who knows?") + """What account identifier said this phrase originally? Can be blank""" -class BookPage(models.Model): - page_id = models.AutoField(primary_key=True) - content = models.TextField(verbose_name="page content", max_length=600, blank=True) - book = models.ForeignKey(Book, models.CASCADE, null=False) + phrase = models.CharField(max_length=128) def __str__(self): - return f"{self.page_id} page. from {self.book.title}" + if self.said_by: + return f"{self.said_by}: {self.phrase}" + return f"{self.pk}: {self.phrase}" diff --git a/src/website/apps.py b/src/website/apps.py deleted file mode 100644 index f56d49f..0000000 --- a/src/website/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class WebsiteConfig(AppConfig): - name = "website" diff --git a/src/website/migrations/__init__.py b/src/website/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/website/statics/css/login-dark.css b/src/website/statics/css/login-dark.css deleted file mode 100644 index be4d2f4..0000000 --- a/src/website/statics/css/login-dark.css +++ /dev/null @@ -1,65 +0,0 @@ -.login-dark form { - max-width: 320px; - width: 90%; - background-color: #1e2833; - padding: 40px; - border-radius: 4px; - transform: translate(-50%, -50%); - position: absolute; - top: 50%; - left: 50%; - color: #fff; - box-shadow: 3px 3px 4px rgba(0,0,0,0.2); -} - -.login-dark .illustration { - text-align: center; - padding: 15px 0 20px; - font-size: 100px; - color: #2980ef; -} - -.login-dark form .form-control { - background: none; - border: none; - border-bottom: 1px solid #434a52; - border-radius: 0; - box-shadow: none; - outline: none; - color: inherit; -} - -.login-dark form .btn-primary { - background: #214a80; - border: none; - border-radius: 4px; - padding: 11px; - box-shadow: none; - margin-top: 26px; - text-shadow: none; - outline: none; -} - -.login-dark form .btn-primary:hover, .login-dark form .btn-primary:active { - background: #214a80; - outline: none; -} - -.login-dark form .forgot { - display: block; - text-align: center; - font-size: 12px; - color: #6f7a85; - opacity: 0.9; - text-decoration: none; -} - -.login-dark form .forgot:hover, .login-dark form .forgot:active { - opacity: 1; - text-decoration: none; -} - -.login-dark form .btn-primary:active { - transform: translateY(1px); -} - diff --git a/src/website/statics/css/style.css b/src/website/statics/css/style.css deleted file mode 100644 index c0b20f4..0000000 --- a/src/website/statics/css/style.css +++ /dev/null @@ -1,4 +0,0 @@ -html { - margin: 10px; - padding: 10px; -} \ No newline at end of file diff --git a/src/website/templates/base.html b/src/website/templates/base.html index 8729de5..cf5be44 100644 --- a/src/website/templates/base.html +++ b/src/website/templates/base.html @@ -1,6 +1,6 @@ {% load static %} - + diff --git a/src/website/templates/registration/confirmation_email.html b/src/website/templates/registration/confirmation_email.html index 5177b33..2962329 100644 --- a/src/website/templates/registration/confirmation_email.html +++ b/src/website/templates/registration/confirmation_email.html @@ -1,3 +1,4 @@ + @@ -5,7 +6,9 @@ + Unitystation mail confirmation +

You are almost there!


Please click the following link to confirm your account:

{{ link }} @@ -15,5 +18,5 @@

Please click the following link to confirm your account:

Sincerely, The Unitystation Team

- + \ No newline at end of file diff --git a/src/website/templates/registration/login.html b/src/website/templates/registration/login.html deleted file mode 100644 index 9747f87..0000000 --- a/src/website/templates/registration/login.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends 'base.html' %} - -{% block title %}Log In{% endblock %} - -{% block content %} -

Log In

-