├── .dockerignore ├── .env.sample ├── .github ├── dependabot.yml └── workflows │ ├── automated-tests.yml │ └── codeql-analysis.yml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile.dev ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── boxes ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_box_uuid.py │ ├── 0003_box_closed.py │ ├── 0004_auto_20160415_1428.py │ ├── 0005_box_last_sent_at.py │ ├── 0006_auto_20160429_1748.py │ ├── 0007_auto_20160603_1138.py │ ├── 0008_auto_20170519_1431.py │ ├── 0009_box_verified_only.py │ └── __init__.py ├── models.py ├── static │ ├── javascripts │ │ ├── box_create.js │ │ ├── box_list.js │ │ ├── box_submit.js │ │ └── openpgp.min.js │ └── stylesheets │ │ └── style.css ├── tasks.py ├── templates │ └── boxes │ │ ├── box_list.html │ │ ├── box_submit.html │ │ ├── closed.html │ │ ├── success.html │ │ └── verified_only.html ├── test_constants.py ├── tests.py ├── urls.py └── views.py ├── common.yml ├── docker-compose.yml ├── hawkpost ├── __init__.py ├── celery.py ├── middleware.py ├── settings │ ├── common.py │ ├── development.py │ └── production.py ├── static │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── Raleway-Bold.ttf │ │ ├── Raleway-ExtraBold.ttf │ │ ├── Roboto-Bold.ttf │ │ ├── Roboto-Bold.woff │ │ ├── Roboto-Bold.woff2 │ │ ├── Roboto-Medium.ttf │ │ ├── Roboto-Medium.woff │ │ ├── Roboto-Medium.woff2 │ │ ├── Roboto-Regular.ttf │ │ ├── Roboto-Regular.woff │ │ ├── Roboto-Regular.woff2 │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── images │ │ ├── close.png │ │ ├── external.png │ │ ├── favicon.ico │ │ ├── favicon.png │ │ ├── icon1.svg │ │ ├── icon2.svg │ │ ├── icon3.svg │ │ ├── keyserver_key.png │ │ ├── logob.png │ │ ├── logow-small.png │ │ ├── logow.png │ │ ├── ls.svg │ │ ├── paper-bin.png │ │ ├── rs.svg │ │ └── static_key.png │ └── javascripts │ │ ├── auth_modals.js │ │ ├── classie.js │ │ ├── global.js │ │ ├── jquery.datetimepicker.min.js │ │ ├── jquery.min.js │ │ └── modalEffects.js ├── templates │ ├── account │ │ ├── account_inactive.html │ │ ├── auth_modals.html │ │ ├── email.html │ │ ├── email │ │ │ ├── email_confirmation_message.txt │ │ │ ├── email_confirmation_signup_message.txt │ │ │ ├── email_confirmation_signup_subject.txt │ │ │ ├── email_confirmation_subject.txt │ │ │ ├── password_reset_key_message.txt │ │ │ └── password_reset_key_subject.txt │ │ ├── email_confirm.html │ │ ├── logout.html │ │ ├── messages │ │ │ ├── cannot_delete_primary_email.txt │ │ │ ├── email_confirmation_sent.txt │ │ │ ├── email_confirmed.txt │ │ │ ├── email_deleted.txt │ │ │ ├── logged_in.txt │ │ │ ├── logged_out.txt │ │ │ ├── password_changed.txt │ │ │ ├── password_set.txt │ │ │ ├── primary_email_set.txt │ │ │ └── unverified_primary_email.txt │ │ ├── password_change.html │ │ ├── password_reset.html │ │ ├── password_reset_done.html │ │ ├── password_reset_from_key.html │ │ ├── password_reset_from_key_done.html │ │ ├── password_set.html │ │ ├── signup_closed.html │ │ ├── snippets │ │ │ └── already_logged_in.html │ │ ├── verification_sent.html │ │ └── verified_email_required.html │ ├── layout │ │ ├── base.html │ │ ├── messages.html │ │ └── navbar.html │ └── socialaccount │ │ ├── authentication_error.html │ │ ├── base.html │ │ ├── connections.html │ │ ├── login_cancelled.html │ │ ├── messages │ │ ├── account_connected.txt │ │ ├── account_connected_other.txt │ │ └── account_disconnected.txt │ │ ├── signup.html │ │ └── snippets │ │ ├── login_extra.html │ │ └── provider_list.html ├── urls.py └── wsgi.py ├── humans ├── __init__.py ├── adapter.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20160310_2236.py │ ├── 0003_user_organization.py │ ├── 0004_auto_20160330_1430.py │ ├── 0005_user_timezone.py │ ├── 0006_user_server_signed.py │ ├── 0007_notification.py │ ├── 0008_auto_20160903_1850.py │ ├── 0009_auto_20170519_1431.py │ ├── 0010_auto_20170526_1326.py │ ├── 0011_keychangerecord.py │ ├── 0012_remove_user_server_signed.py │ ├── 0013_auto_20201204_1807.py │ └── __init__.py ├── models.py ├── static │ └── javascripts │ │ └── update_user_form.js ├── tasks.py ├── templates │ └── humans │ │ ├── emails │ │ ├── fingerprint_changed.txt │ │ ├── key_expired.txt │ │ ├── key_invalid.txt │ │ ├── key_revoked.txt │ │ └── key_will_expire.txt │ │ ├── key_change_modal.html │ │ ├── update_user_form.html │ │ └── user_confirm_delete.html ├── test_constants.py ├── tests.py ├── urls.py ├── utils.py └── views.py ├── locale └── pt_PT │ └── LC_MESSAGES │ ├── django.mo │ └── django.po ├── manage.py └── pages ├── __init__.py ├── admin.py ├── apps.py ├── migrations └── __init__.py ├── models.py ├── static └── javascripts │ └── authform.js ├── templates ├── 403.html ├── 404.html ├── 500.html └── pages │ ├── about.html │ ├── help.html │ └── index.html ├── tests.py ├── urls.py └── views.py /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # Environment (values: development or production) 2 | HAWKPOST_ENV= 3 | 4 | # Django Settings 5 | SECRET_KEY= 6 | 7 | # Database Configuration 8 | DB_NAME= 9 | DB_USER= 10 | DB_PASSWORD= 11 | DB_HOST= 12 | DB_PORT= 13 | 14 | # REDIS configuration (remove if localhost and default port) 15 | REDIS_URL= 16 | 17 | # Specific Instance variables 18 | SITE_DOMAIN= 19 | DEFAULT_FROM_EMAIL= 20 | 21 | # (Optional) Sentry project to report errors 22 | SENTRY_URL= 23 | 24 | # Information about the hawkpost instance and administrator 25 | SUPPORT_NAME= 26 | SUPPORT_EMAIL= 27 | # (Optional) 28 | INSTANCE_DESCRIPTION= 29 | 30 | # Development (Docker Setup only) 31 | INTERNAL_IPS= 32 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | reviewers: 8 | - "dethos" 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | reviewers: 14 | - "dethos" 15 | -------------------------------------------------------------------------------- /.github/workflows/automated-tests.yml: -------------------------------------------------------------------------------- 1 | name: Automated Tests 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | env: 10 | DB_HOST: localhost 11 | DB_PASSWORD: ${{ secrets.DB_PASSWORD }} 12 | SECRET_KEY: ${{ secrets.SECRET_KEY }} 13 | USERNAME: root 14 | 15 | jobs: 16 | unittest: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | python-version: [3.9] 21 | 22 | services: 23 | postgres: 24 | image: postgres:16 25 | env: 26 | POSTGRES_PASSWORD: postgres 27 | POSTGRES_DB: hawkpost_dev 28 | options: >- 29 | --health-cmd pg_isready 30 | --health-interval 10s 31 | --health-timeout 5s 32 | --health-retries 5 33 | ports: 34 | - 5432:5432 35 | redis: 36 | image: redis:7 37 | ports: 38 | - 6379:6379 39 | 40 | steps: 41 | - uses: actions/checkout@v4 42 | - name: Set up Python ${{ matrix.python-version }} 43 | uses: actions/setup-python@v3 44 | with: 45 | python-version: ${{ matrix.python-version }} 46 | - name: Install Dependencies 47 | run: | 48 | python -m pip install --upgrade pip pipenv 49 | pipenv install --dev 50 | - name: Collect Static assets 51 | run: | 52 | pipenv run python manage.py collectstatic --no-input 53 | - name: Run Tests 54 | run: | 55 | pipenv run python manage.py test 56 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 18 * * 0' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['python', 'javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | __pycache__ 3 | .sqlite3 4 | 5 | # Static files stuff 6 | hawkpost/staticfiles/ 7 | /hawkpost/static/css/style.css 8 | /node_modules 9 | # Celery 10 | celerybeat-schedule 11 | celerybeat-schedule.db 12 | 13 | # Testing files 14 | .coverage 15 | 16 | # gpg homedir for signing 17 | gpg_home 18 | # Other gpg stuff 19 | *.gpg 20 | *.gpg~ 21 | random_seed 22 | trustdb.gpg 23 | 24 | # Vim 25 | [._]*.s[a-w][a-z] 26 | [._]s[a-w][a-z] 27 | *.un~ 28 | Session.vim 29 | .netrwhist 30 | *~ 31 | 32 | # MacOS Stuff 33 | .DS_Store 34 | 35 | # Editor Stuff 36 | .vscode 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Hawkpost 2 | 3 | Given this project is in active development, with the desire to continually provide more value and better serve its users, you are more than welcome to join in and help improve Hawkpost. The project will mostly use the Github issues to keep track of bugs, feature requests and milestones. So an account should be all you need to start contributing. 4 | 5 | Below are a few things we follow and would appreciate if you do too. 6 | 7 | ## Issues 8 | 9 | When reporting bugs or requesting new features, please provide as much detail and context as you can. This will make things much easier to the people trying to address your issue. 10 | 11 | ## Dependencies 12 | 13 | If you need to use other dependencies, please make sure you install them using `pipenv` with the command: 14 | 15 | >$ pipenv install 16 | 17 | or in case it is only useful for developers add the `--dev` flag. 18 | 19 | You should add the changes in `Pipfile` and `Pipfile.lock` to your commit. 20 | 21 | ## Style 22 | 23 | In order to facilitate the task of everyone who is contributing to this project, we have opted to present a few guidelines here about the "code style" . These guidelines are not rigid and common sense should be taken into account when discussing this matter. 24 | 25 | For python code, unless it is prejudicial to the given situation or makes it harder to understand the code, you should follow the [PEP8](https://www.python.org/dev/peps/pep-0008/) convention. 26 | 27 | For the HTML templates, you should make use of indentation (2 spaces) to make a clear distinction between parent and children elements (siblings should be on the same indentation level). 28 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | 3 | ENV HOME /home/user 4 | RUN useradd --create-home --home-dir $HOME user \ 5 | && chown -R user:user $HOME 6 | 7 | RUN pip install pipenv 8 | 9 | USER user 10 | 11 | COPY . /code 12 | WORKDIR /code 13 | 14 | RUN pipenv install --dev 15 | 16 | VOLUME ["/code"] 17 | VOLUME ["$HOME/.gnupg"] 18 | 19 | CMD ["pipenv", "run", "python", "manage.py", "runserver", "0.0.0.0:8000"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Whitesmith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [dev-packages] 7 | django-debug-toolbar = "==3.8.1" 8 | django-extensions = "==3.2.1" 9 | 10 | [packages] 11 | django-dotenv = "==1.4.2" 12 | "psycopg2-binary" = "==2.9.5" 13 | django-allauth = {version = "==64.2.1", extras=["socialaccount"]} 14 | gnupg = "==2.3.1" 15 | whitenoise = "==5.3.0" 16 | gunicorn = "==23.0.0" 17 | raven = "==6.4.0" 18 | django-timezone-field = "==4.2.3" 19 | django-braces = "==1.15.0" 20 | django-axes = "==5.40.1" 21 | Django = "==4.2.15" 22 | celery = {version = "==5.2.7", extras = ["redis"]} 23 | requests = "==2.32.3" 24 | 25 | [requires] 26 | python_version = "3.9" 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hawkpost 2 | 3 | Hawkpost lets you create unique links that you can share with the person that desires to send you important information but doesn't know how to deal with PGP. 4 | 5 | You can deploy your own server using the code from this repository or use the official server (that is running an exact copy of this repo) at [https://hawkpost.co](https://hawkpost.co). 6 | 7 | ## NOTICE 8 | 9 | **Hawkpost is currently in maintenance mode. This means that bug fixes will be merged and vulnerabilities in the codebase and its dependencies will be patched; however, improvements and new features will not be handled or included.** 10 | 11 | **This status might change in the future, but for the moment it will stay as is.** 12 | 13 | ## Rationale 14 | 15 | For many web and mobile development studios, no matter how hard they try to secure their client secrets (passwords, API keys, etc), the weakest link resides on the client most of the times, specially when he's not a tech savvy person. This project tries to help minimize this issue on the communication between both parties. 16 | 17 | The way it works is like this: 18 | 19 | 1. It fetches your public key. 20 | 1. When the box is open and the secrets submitted, all the content is encrypted on the client side. 21 | 1. The server then signs (**experimental**) the encrypted content. 22 | 1. Finally the server forwards it to your e-mail address. 23 | 24 | # Setting up a development environment 25 | 26 | In this section you can find the steps to setup a minimal development environment on your machine. 27 | 28 | Base requirements: 29 | 30 | - Python 3 31 | - Redis 32 | - PostgreSQL 33 | 34 | ## On Linux 35 | 36 | On a **Debian** based operating system execute the following steps, after cloning the repository: 37 | 38 | - Make sure you have `pipenv` installed. You can check [this page for more information](https://pipenv.readthedocs.io/en/latest/install/#installing-pipenv) 39 | 40 | * Install the dependencies 41 | 42 | ``` 43 | $ pipenv install 44 | ``` 45 | 46 | - Create the local postgreSQL database with your user and no password 47 | 48 | - Migrate the database 49 | 50 | ``` 51 | $ pipenv run python manage.py migrate 52 | ``` 53 | 54 | - Generate stylesheet with gulp (installation instructions for gulp can be found [here](https://gulpjs.com/)) 55 | 56 | ``` 57 | $ gulp build 58 | ``` 59 | 60 | - Now you should be able to launch the server and its workers 61 | 62 | ``` 63 | $ pipenv run python manage.py runserver 64 | $ pipenv run celery -A hawkpost worker --beat -l info 65 | ``` 66 | 67 | You can avoid `pipenv run` in every command if you first active the virtual environment with `pipenv shell`. 68 | 69 | ## Using Docker 70 | 71 | To use this approach you need to have [Docker][docker-overview] and 72 | [Docker Compose][docker-compose-overview] installed. 73 | Please note that since **this project uses version 2 of the 74 | [Compose file format][docker-compose-versioning]** you may need 75 | to update your Docker and Docker Compose to their latest versions. 76 | 77 | Installation instructions for every platform are available at the 78 | [Docker Engine Documentation][docker-install-docs]. If you use Linux you'll 79 | have to [install Docker Compose][docker-compose-install-docs] manually. 80 | 81 | After having the latest Docker and Docker Compose installed, **make the 82 | folder** that will hold the **GPG public keys keyring**: 83 | 84 | ``` 85 | $ mkdir -p gpg_home 86 | ``` 87 | 88 | Some environment variables need to be set so the application works properly. 89 | **Copy** the provided **[.env.sample](.env.sample)** and name it **`.env`**: 90 | 91 | ``` 92 | $ cp .env.sample .env 93 | ``` 94 | 95 | Since this setup assumes containers talk to each other some of the variables 96 | need to be set in order to point to the containers' names. 97 | 98 | **Edit `.env`** and set the following variables to these values: 99 | 100 | ``` 101 | DB_HOST=db 102 | DB_USER=hawkpost 103 | DB_PASSWORD=hawkpost 104 | REDIS_URL=redis://redis:6379/0 105 | ``` 106 | 107 | **Don't forget to set the remaining variables** as well. 108 | 109 | After setting `.env` correctly, just **run** (you may need to `sudo` depending 110 | on your setup) 111 | 112 | ```bash 113 | # Run the databases in detached mode to avoid seeing the logs 114 | $ docker-compose up -d db redis 115 | 116 | # Perform the migrations 117 | # (using `--rm` to remove the temporary container afterwards) 118 | $ docker-compose run --rm web pipenv run python manage.py migrate 119 | 120 | # Run the web and celery containers 121 | # (`docker-compose up` would log db and redis as well) 122 | $ docker-compose up web celery 123 | ``` 124 | 125 | These commands 126 | 127 | 1. **Run the `db` and the `redis` containers** detached from the console, so 128 | we're not bothered by their logs while working on the application. 129 | 1. **Perform the migrations** using a temporary `web` container; it is removed 130 | afterwards. 131 | 1. **Run the `web` and `celery`** attached to the 132 | console. 133 | 134 | The `web` container will reload on code changes. 135 | 136 | You may access the application by **opening `http://` on 137 | your browser**, which you can find by **running** (you may need to run this as 138 | `root` depending on your setup). 139 | 140 | ``` 141 | CID=$(docker ps | grep 'hawkpost_web' | cut -d ' ' -f 1) 142 | docker inspect -f "{{ .NetworkSettings.Networks.hawkpost_default.Gateway }}" $CID 143 | ``` 144 | 145 | This IP won't change unless you remove every container and the corresponding 146 | network (manually), so you may alias it on your `/etc/hosts` (to something like 147 | `hawkpost.test`). 148 | 149 | **Note:** This approach was not tested on OS X or Windows platforms, so the 150 | network feature may require additional steps. 151 | 152 | [docker-overview]: https://www.docker.com/products/docker-engine 153 | [docker-compose-overview]: https://www.docker.com/products/docker-compose 154 | [docker-compose-versioning]: https://docs.docker.com/compose/compose-file/#versioning 155 | [docker-install-docs]: https://docs.docker.com/engine/installation 156 | [docker-compose-install-docs]: https://github.com/docker/compose/releases 157 | 158 | # Running the test suite 159 | 160 | To execute our current test suite, you just need to execute the following command after setting up your local development environment: 161 | 162 | > \$ pipenv run python manage.py test 163 | 164 | In case you are using our docker setup the command should be: 165 | 166 | > \$ docker-compose run --rm web pipenv run python manage.py test 167 | 168 | # Credits 169 | 170 | ![Whitesmith](http://i.imgur.com/Si2l3kd.png) 171 | 172 | This project was born during an internal hackathon at [Whitesmith](https://whitesmith.co), which is helping and supporting the current development. 173 | -------------------------------------------------------------------------------- /boxes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/boxes/__init__.py -------------------------------------------------------------------------------- /boxes/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Box, Membership, Message 3 | 4 | 5 | class MessageInline(admin.TabularInline): 6 | model = Message 7 | extra = 0 8 | 9 | 10 | @admin.register(Box) 11 | class BoxAdmin(admin.ModelAdmin): 12 | list_display = ('name', 'owner', 'created_at', 'expires_at') 13 | list_filter = ('status', 'created_at', 'expires_at') 14 | search_fields = ['name', 'owner__email'] 15 | inlines = [MessageInline] 16 | 17 | 18 | @admin.register(Membership) 19 | class MembershipAdmin(admin.ModelAdmin): 20 | list_display = ('user', 'box', 'access', 'created_at') 21 | list_filter = ('access', 'created_at',) 22 | search_fields = ['box__name', 'user__email'] 23 | 24 | 25 | @admin.register(Message) 26 | class MessageAdmin(admin.ModelAdmin): 27 | list_display = ('box', 'status', 'created_at', 'sent_at') 28 | list_filter = ('status', 'created_at', 'sent_at') 29 | -------------------------------------------------------------------------------- /boxes/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BoxesConfig(AppConfig): 5 | name = 'boxes' 6 | -------------------------------------------------------------------------------- /boxes/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import ModelForm, Form, CharField, Textarea, BooleanField 2 | from .models import Box 3 | from django.utils import timezone 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from sys import getsizeof 7 | 8 | MAX_MESSAGE_SIZE = 10485760 # Bytes = 10 Mb 9 | 10 | 11 | class CreateBoxForm(ModelForm): 12 | never_expires = BooleanField(required=False) 13 | 14 | field_order = [ 15 | "name", 16 | "never_expires", 17 | "expires_at", 18 | "max_messages", 19 | "description"] 20 | 21 | class Meta: 22 | model = Box 23 | fields = [ 24 | "name", 25 | "description", 26 | "expires_at", 27 | "max_messages", 28 | "verified_only"] 29 | 30 | def clean_expires_at(self): 31 | # Validate the expiration date 32 | expires_at = self.cleaned_data.get("expires_at", "") 33 | never_expires = self.cleaned_data.get("never_expires", "") 34 | current_tz = timezone.get_current_timezone() 35 | if never_expires: 36 | expires_at = None 37 | self.cleaned_data["expires_at"] = expires_at 38 | if expires_at: 39 | # Check if the expiration date is a past date 40 | if timezone.localtime(timezone.now(), current_tz) > expires_at: 41 | self.add_error('expires_at', _('The date must be on the future.')) 42 | if not expires_at and not never_expires: 43 | self.add_error('expires_at', 44 | _('This field is required, unless box is set to ' 45 | 'never expire.')) 46 | return expires_at 47 | 48 | def clean(self): 49 | cleaned_data = super().clean() 50 | cleaned_data.pop("never_expires") 51 | return cleaned_data 52 | 53 | 54 | class SubmitBoxForm(Form): 55 | message = CharField(widget=Textarea, required=True) 56 | file_name = CharField(required=False) 57 | add_reply_to = BooleanField(required=False) 58 | 59 | def clean_message(self): 60 | # Quick check if the message really came encrypted 61 | message = self.cleaned_data.get("message") 62 | lines = message.split("\r\n") 63 | 64 | if getsizeof(message) > MAX_MESSAGE_SIZE: 65 | self.add_error( 66 | "message", 67 | _('The message or file exceeds your allowed size limit.') 68 | ) 69 | 70 | begin = "-----BEGIN PGP MESSAGE-----" 71 | end = "-----END PGP MESSAGE-----" 72 | 73 | try: 74 | if lines[0] != begin or lines[-1] != end: 75 | self.add_error("message", _('Invalid PGP message')) 76 | except IndexError: 77 | self.add_error("message", _('Invalid PGP message')) 78 | return message 79 | -------------------------------------------------------------------------------- /boxes/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.2 on 2016-03-10 22:18 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Box', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('name', models.CharField(max_length=128)), 24 | ('description', models.TextField(blank=True, null=True)), 25 | ('created_at', models.DateTimeField(auto_now_add=True)), 26 | ('updated_at', models.DateTimeField(auto_now=True)), 27 | ('expires_at', models.DateTimeField()), 28 | ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='own_boxes', to=settings.AUTH_USER_MODEL)), 29 | ], 30 | options={ 31 | 'verbose_name_plural': 'Boxes', 32 | 'verbose_name': 'Box', 33 | }, 34 | ), 35 | migrations.CreateModel( 36 | name='Membership', 37 | fields=[ 38 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 39 | ('access', models.IntegerField(choices=[(10, 'Knowledge of existence'), (20, 'Activity notifications'), (30, 'Full access to content')], default=30)), 40 | ('created_at', models.DateTimeField(auto_now_add=True)), 41 | ('updated_at', models.DateTimeField(auto_now=True)), 42 | ('box', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='boxes.Box')), 43 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 44 | ], 45 | options={ 46 | 'verbose_name_plural': 'Memberships', 47 | 'verbose_name': 'Membership', 48 | }, 49 | ), 50 | migrations.AddField( 51 | model_name='box', 52 | name='recipients', 53 | field=models.ManyToManyField(related_name='boxes', through='boxes.Membership', to=settings.AUTH_USER_MODEL), 54 | ), 55 | ] 56 | -------------------------------------------------------------------------------- /boxes/migrations/0002_box_uuid.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.4 on 2016-03-31 11:58 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import uuid 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('boxes', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='box', 18 | name='uuid', 19 | field=models.UUIDField(default=uuid.uuid4, editable=False), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /boxes/migrations/0003_box_closed.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.4 on 2016-03-31 19:38 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('boxes', '0002_box_uuid'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='box', 17 | name='closed', 18 | field=models.BooleanField(default=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /boxes/migrations/0004_auto_20160415_1428.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.4 on 2016-04-15 14:28 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('boxes', '0003_box_closed'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='box', 17 | name='closed', 18 | ), 19 | migrations.AddField( 20 | model_name='box', 21 | name='status', 22 | field=models.IntegerField(choices=[(10, 'Open'), (20, 'Expired'), (30, 'Sent'), (40, 'Closed')], default=10), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /boxes/migrations/0005_box_last_sent_at.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.4 on 2016-04-15 14:59 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('boxes', '0004_auto_20160415_1428'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='box', 17 | name='last_sent_at', 18 | field=models.DateTimeField(null=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /boxes/migrations/0006_auto_20160429_1748.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.4 on 2016-04-29 17:48 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('boxes', '0005_box_last_sent_at'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='box', 17 | name='status', 18 | field=models.IntegerField(choices=[(10, 'Open'), (20, 'Expired'), (30, 'Sent'), (50, 'On Queue'), (40, 'Closed')], default=10), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /boxes/migrations/0007_auto_20160603_1138.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.6 on 2016-06-03 11:38 3 | from __future__ import unicode_literals 4 | 5 | import django.core.validators 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('boxes', '0006_auto_20160429_1748'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Message', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('status', models.IntegerField(choices=[(10, 'OnQueue'), (20, 'Sent'), (30, 'Failed')], default=10)), 22 | ('created_at', models.DateTimeField(auto_now_add=True)), 23 | ('updated_at', models.DateTimeField(auto_now=True)), 24 | ('sent_at', models.DateTimeField(blank=True, null=True)), 25 | ], 26 | ), 27 | migrations.AddField( 28 | model_name='box', 29 | name='max_messages', 30 | field=models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1)]), 31 | ), 32 | migrations.AlterField( 33 | model_name='box', 34 | name='expires_at', 35 | field=models.DateTimeField(blank=True, null=True), 36 | ), 37 | migrations.AlterField( 38 | model_name='box', 39 | name='status', 40 | field=models.IntegerField(choices=[(10, 'Open'), (20, 'Expired'), (30, 'Done'), (40, 'Closed')], default=10), 41 | ), 42 | migrations.AddField( 43 | model_name='message', 44 | name='box', 45 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='boxes.Box'), 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /boxes/migrations/0008_auto_20170519_1431.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.7 on 2017-05-19 14:31 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | import django.core.validators 7 | from django.db import migrations, models 8 | import django.db.models.deletion 9 | import uuid 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | dependencies = [ 15 | ('boxes', '0007_auto_20160603_1138'), 16 | ] 17 | 18 | operations = [ 19 | migrations.AlterField( 20 | model_name='box', 21 | name='created_at', 22 | field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), 23 | ), 24 | migrations.AlterField( 25 | model_name='box', 26 | name='description', 27 | field=models.TextField(blank=True, null=True, verbose_name='Description'), 28 | ), 29 | migrations.AlterField( 30 | model_name='box', 31 | name='expires_at', 32 | field=models.DateTimeField(blank=True, null=True, verbose_name='Expires at'), 33 | ), 34 | migrations.AlterField( 35 | model_name='box', 36 | name='last_sent_at', 37 | field=models.DateTimeField(null=True, verbose_name='Last sent at'), 38 | ), 39 | migrations.AlterField( 40 | model_name='box', 41 | name='max_messages', 42 | field=models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Max. messages'), 43 | ), 44 | migrations.AlterField( 45 | model_name='box', 46 | name='name', 47 | field=models.CharField(max_length=128, verbose_name='Name'), 48 | ), 49 | migrations.AlterField( 50 | model_name='box', 51 | name='owner', 52 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='own_boxes', to=settings.AUTH_USER_MODEL, verbose_name='Owner'), 53 | ), 54 | migrations.AlterField( 55 | model_name='box', 56 | name='recipients', 57 | field=models.ManyToManyField(related_name='boxes', through='boxes.Membership', to=settings.AUTH_USER_MODEL, verbose_name='Recipients'), 58 | ), 59 | migrations.AlterField( 60 | model_name='box', 61 | name='status', 62 | field=models.IntegerField(choices=[(10, 'Open'), (20, 'Expired'), (30, 'Done'), (40, 'Closed')], default=10, verbose_name='Status'), 63 | ), 64 | migrations.AlterField( 65 | model_name='box', 66 | name='updated_at', 67 | field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), 68 | ), 69 | migrations.AlterField( 70 | model_name='box', 71 | name='uuid', 72 | field=models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='Unique ID'), 73 | ), 74 | migrations.AlterField( 75 | model_name='membership', 76 | name='access', 77 | field=models.IntegerField(choices=[(10, 'Knowledge of existence'), (20, 'Activity notifications'), (30, 'Full access to content')], default=30, verbose_name='Rights'), 78 | ), 79 | migrations.AlterField( 80 | model_name='membership', 81 | name='created_at', 82 | field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), 83 | ), 84 | migrations.AlterField( 85 | model_name='membership', 86 | name='updated_at', 87 | field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), 88 | ), 89 | migrations.AlterField( 90 | model_name='message', 91 | name='created_at', 92 | field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), 93 | ), 94 | migrations.AlterField( 95 | model_name='message', 96 | name='sent_at', 97 | field=models.DateTimeField(blank=True, null=True, verbose_name='Sent at'), 98 | ), 99 | migrations.AlterField( 100 | model_name='message', 101 | name='status', 102 | field=models.IntegerField(choices=[(10, 'OnQueue'), (20, 'Sent'), (30, 'Failed')], default=10, verbose_name='Status'), 103 | ), 104 | migrations.AlterField( 105 | model_name='message', 106 | name='updated_at', 107 | field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), 108 | ), 109 | ] 110 | -------------------------------------------------------------------------------- /boxes/migrations/0009_box_verified_only.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2021-03-15 19:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('boxes', '0008_auto_20170519_1431'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='box', 15 | name='verified_only', 16 | field=models.BooleanField(default=False, verbose_name='Restrict to verified users'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /boxes/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/boxes/migrations/__init__.py -------------------------------------------------------------------------------- /boxes/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.conf import settings 3 | from django.core.validators import MinValueValidator 4 | from django.utils.translation import gettext_lazy as _ 5 | import uuid 6 | 7 | 8 | class Box(models.Model): 9 | 10 | OPEN = 10 11 | EXPIRED = 20 12 | DONE = 30 13 | CLOSED = 40 14 | 15 | STATUSES = ( 16 | (OPEN, _('Open')), 17 | (EXPIRED, _('Expired')), 18 | (DONE, _('Done')), 19 | (CLOSED, _('Closed')) 20 | ) 21 | 22 | name = models.CharField(max_length=128, verbose_name=_('Name')) 23 | description = models.TextField(null=True, blank=True, 24 | verbose_name=_('Description')) 25 | uuid = models.UUIDField(default=uuid.uuid4, editable=False, 26 | verbose_name=_('Unique ID')) 27 | 28 | owner = models.ForeignKey(settings.AUTH_USER_MODEL, 29 | on_delete=models.CASCADE, 30 | related_name='own_boxes', 31 | verbose_name=_('Owner')) 32 | 33 | recipients = models.ManyToManyField(settings.AUTH_USER_MODEL, 34 | related_name='boxes', 35 | through='Membership', 36 | through_fields=('box', 'user'), 37 | verbose_name=_('Recipients')) 38 | created_at = models.DateTimeField(auto_now_add=True, 39 | verbose_name=_('Created at')) 40 | updated_at = models.DateTimeField(auto_now=True, 41 | verbose_name=_('Updated at')) 42 | expires_at = models.DateTimeField(null=True, blank=True, 43 | verbose_name=_('Expires at')) 44 | 45 | status = models.IntegerField(choices=STATUSES, default=OPEN, 46 | verbose_name=_('Status')) 47 | max_messages = models.PositiveIntegerField(default=1, validators=[ 48 | MinValueValidator(1)], 49 | verbose_name=_('Max. messages')) 50 | 51 | last_sent_at = models.DateTimeField(null=True, 52 | verbose_name=_('Last sent at')) 53 | 54 | verified_only = models.BooleanField(default=False, 55 | verbose_name=_('Restrict to verified users')) 56 | 57 | class Meta: 58 | verbose_name = _('Box') 59 | verbose_name_plural = _('Boxes') 60 | 61 | def __str__(self): 62 | return self.name 63 | 64 | @staticmethod 65 | def get_status(name): 66 | return { 67 | "Open": Box.OPEN, 68 | "Expired": Box.EXPIRED, 69 | "Done": Box.DONE, 70 | "Closed": Box.CLOSED 71 | }.get(name, Box.OPEN) 72 | 73 | 74 | class Membership(models.Model): 75 | 76 | KNOWLEDGE = 10 77 | NOTIFICATION = 20 78 | FULL = 30 79 | 80 | LEVELS = ( 81 | (KNOWLEDGE, _('Knowledge of existence')), 82 | (NOTIFICATION, _('Activity notifications')), 83 | (FULL, _('Full access to content')), 84 | ) 85 | 86 | access = models.IntegerField(choices=LEVELS, default=FULL, 87 | verbose_name=_('Rights')) 88 | created_at = models.DateTimeField(auto_now_add=True, 89 | verbose_name=_('Created at')) 90 | updated_at = models.DateTimeField(auto_now=True, 91 | verbose_name=_('Updated at')) 92 | box = models.ForeignKey("Box", on_delete=models.CASCADE) 93 | user = models.ForeignKey(settings.AUTH_USER_MODEL, 94 | on_delete=models.CASCADE) 95 | 96 | class Meta: 97 | verbose_name = _('Membership') 98 | verbose_name_plural = _('Memberships') 99 | 100 | def __str__(self): 101 | return "{}.{}".format(self.id, self.get_access_display()) 102 | 103 | 104 | class Message(models.Model): 105 | 106 | ONQUEUE = 10 107 | SENT = 20 108 | FAILED = 30 109 | 110 | STATUSES = ( 111 | (ONQUEUE, _('OnQueue')), 112 | (SENT, _('Sent')), 113 | (FAILED, _('Failed')) 114 | ) 115 | 116 | box = models.ForeignKey( 117 | "Box", on_delete=models.CASCADE, related_name='messages') 118 | status = models.IntegerField(choices=STATUSES, default=ONQUEUE, 119 | verbose_name=_('Status')) 120 | created_at = models.DateTimeField(auto_now_add=True, 121 | verbose_name=_('Created at')) 122 | updated_at = models.DateTimeField(auto_now=True, 123 | verbose_name=_('Updated at')) 124 | sent_at = models.DateTimeField(null=True, blank=True, 125 | verbose_name=_('Sent at')) 126 | -------------------------------------------------------------------------------- /boxes/static/javascripts/box_create.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $("#id_expires_at").datetimepicker({ 3 | format: "Y-m-d H:i" 4 | }); 5 | 6 | $("#id_never_expires").on("change", function(){ 7 | if ($(this).is(":checked")){ 8 | $("#id_expires_at").prop("disabled", true).val(""); 9 | } else { 10 | $("#id_expires_at").prop("disabled", false); 11 | } 12 | }); 13 | 14 | $("#box-create-form-js").on("submit", function(){ 15 | var $this = $(this); 16 | $.ajax({ 17 | url: $this.attr("action"), 18 | method: $this.attr("method"), 19 | data: $this.serialize() 20 | }).done(function(data){ 21 | window.location = data.location; 22 | }).fail(function(data){ 23 | var fields = ["__all__", "name", "description", "max_messages", 24 | "expires_at", "never_expires"]; 25 | var form_errors = data.responseJSON.form_errors; 26 | for(var i=0;i" + form_errors[fields[i]] + "

"); 31 | } 32 | } 33 | }); 34 | return false; 35 | }); 36 | }); 37 | $(".md-modal-xs label").addClass("smalltext"); 38 | $(".md-modal-xs input[type=text]").addClass("text padding-modals1"); 39 | $(".md-modal-xs input[type=number]").addClass("text padding-modals1"); 40 | $(".md-modal-xs textarea").addClass("text padding-modals2"); -------------------------------------------------------------------------------- /boxes/static/javascripts/box_list.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | /* 3 | On click copy the box link to the clipboard 4 | */ 5 | $(".box-options").click(function(){ 6 | $(this).submit(); 7 | }); 8 | $(".copy-to-clipboard-js").on("click", function(){ 9 | $this = $(this); 10 | var previousText = $this.html(); 11 | var input = $("#" + $this.attr("data-src")); 12 | input.select(); 13 | try{ 14 | var result = document.execCommand('copy'); 15 | if(result){ 16 | $this.html("URL copied"); 17 | } else { 18 | $this.html("Unable to copy to clipboard"); 19 | } 20 | setTimeout(function(){ 21 | $this.html(previousText); 22 | }, 1500); 23 | } catch (err) { 24 | //Browser does not support "copy"? 25 | $this.html("Unable to copy to clipboard"); 26 | $(".copy-to-clipboard-js").prop("disabled", true); 27 | } 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /boxes/static/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | /* Temporary Remove at will */ 2 | .hidden { 3 | display:none; 4 | } 5 | -------------------------------------------------------------------------------- /boxes/tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from django.core.mail import EmailMultiAlternatives 3 | from django.conf import settings 4 | from django.utils import timezone 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | from .models import Message 8 | 9 | from celery import shared_task 10 | 11 | 12 | @shared_task 13 | def process_email(message_id, form_data, sent_by=None): 14 | message = Message.objects.get(id=message_id) 15 | box = message.box 16 | msg = form_data["message"] 17 | file_name = form_data.get("file_name", "") 18 | subject = _('New submission to your box: {}').format(box) 19 | reply_to = [sent_by] if sent_by else [] 20 | 21 | if file_name: 22 | body = _("The submitted file can be found in the attachments.") 23 | email = EmailMultiAlternatives( 24 | subject, 25 | body, 26 | settings.DEFAULT_FROM_EMAIL, 27 | [box.owner.email], 28 | reply_to=reply_to, 29 | attachments=[(file_name, msg, "application/octet-stream")] 30 | ) 31 | else: 32 | email = EmailMultiAlternatives(subject, 33 | msg, 34 | settings.DEFAULT_FROM_EMAIL, 35 | [box.owner.email], 36 | reply_to=reply_to) 37 | 38 | # Output e-mail message for debug purposes 39 | # with open('email.mbox', 'w') as f: 40 | # f.write(email.message().as_string(unixfrom=True)) 41 | 42 | email.send() 43 | now = timezone.now() 44 | box.last_sent_at = now 45 | box.save() 46 | message.sent_at = now 47 | message.status = Message.SENT 48 | message.save() 49 | -------------------------------------------------------------------------------- /boxes/templates/boxes/box_submit.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | {% load i18n %} 3 | {% load static %} 4 | 5 | {% block header %} 6 | {% include "layout/messages.html" %} 7 | {% endblock header %} 8 | 9 | {% block content %} 10 |
11 |
12 |
13 | 14 | 15 | 16 |

{% trans "Sending your confidential information safely" %}

17 |
18 |

{% trans "What is Hawkpost?" %}

19 |

20 | {% trans "Hawkpost is the easiest way to securely receive sensible information from people who don't know how to use PGP. They don't even need to install anything: you just share your link and the person who receives it just needs to submit the information. Our hawk will send it back to you." %} 21 |

22 |
23 |
24 |
25 |
26 |
27 |

{% trans "Compose new message" %}

28 |

{% trans "To:" %} {{object.owner.first_name}} {{object.owner.last_name}} ({{object.owner.email}})

29 |

{% trans "Organization:" %} {{object.owner.organization}}

30 |

{% trans "Key fingerprint:" %} {{object.owner.fingerprint}}

31 |
32 | 41 |
42 |
43 |
44 |
45 |
46 | 50 |
51 |
52 |
53 |

{{object.name}}

54 |

{{object.description|linebreaks}}

55 |
56 |
57 | {% if request.user.is_authenticated %} 58 | 59 | {% endif %} 60 |
61 |
62 |
63 |
64 |
65 | {% endblock content %} 66 | 67 | {% block scripts %} 68 | {{ block.super }} 69 | 70 | 71 | {% endblock scripts %} 72 | -------------------------------------------------------------------------------- /boxes/templates/boxes/closed.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |
9 |
10 |

{% trans "THIS LINK IS NO LONGER ACTIVE" %}

11 |

{% trans "That could be because:" %}

12 |
    13 |
  • - {% trans "The link expired" %}
  • 14 |
  • - {% trans "The recipient closed or deactivated the link." %}
  • 15 |
  • - {% trans "It was already submitted." %}
  • 16 |
  • - {% trans "The recipient did not suply a valid public key in order to correctly encrypt the content." %}
  • 17 |
    18 |
19 |

20 | {% blocktrans with name=box.owner.get_full_name|default:box.owner.email email=box.owner.email %}In any of the above cases, you should contact the recipient ({{name}}) in order to fix the issue. 21 | {% endblocktrans %} 22 |

23 |
24 |
25 |
26 |
27 |
28 | {% endblock content %} 29 | -------------------------------------------------------------------------------- /boxes/templates/boxes/success.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | {% load i18n %} 3 | {% load static %} 4 | 5 | {% block header %} 6 | {% include "layout/messages.html" %} 7 | {% endblock header %} 8 | 9 | {% block content %} 10 |
11 |
12 |
13 |
14 |
15 |

{% trans "MESSAGE SENT WITH SUCCESS!" %}

16 |
17 |
18 |
19 |
20 |

{% trans "by:" %}

21 | 22 | 23 | 24 |
25 |
26 |
27 |
28 |

{% trans "Sending your confidential information safely" %}

29 |
30 |
31 |
32 |
33 |
34 | {% endblock content %} 35 | -------------------------------------------------------------------------------- /boxes/templates/boxes/verified_only.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block head_title %}{% trans "Verify Your Identity" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |
10 |
11 |
12 |

13 | {% trans "Verify Your Identity" %} 14 |

15 |

16 | {% trans "To access this page you need to verify your identity. Please login to your Hawkpost account or sign up if you don't have one." %} 17 |

18 |

19 | {% trans "Note that if you've signed up using your email address, you need to verify it to get access. To do this, simply visit the link provided in the email that was sent to you upon registration." %} 20 |

21 |
22 |
23 |
24 |
25 |
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /boxes/test_constants.py: -------------------------------------------------------------------------------- 1 | 2 | ENCRYPTED_MESSAGE = """-----BEGIN PGP MESSAGE-----\r 3 | Version: OpenPGP.js v2.2.0\r 4 | Comment: http://openpgpjs.org\r 5 | \r 6 | wcFMA9K5fTFi5lXtAQ//WLWrSXgc5YRgL8sGCLPFVoFb5ootYlCphhk+hWSD\r 7 | Wr7U4IXYr+mHvhX3twigK+io4EVoBtSg5+e+Nkvbj4HTIq/XH2DGHLFDMKTc\r 8 | zeY7kNHxKuWB4ZCDJPTlwQHfY5hyBZQTkhRDXsA4Y79OZo9gN9FwvbKjSwUM\r 9 | AG+fhorLT/nEoHexZt1vohhobAQkRaVLh+NMKRLgHXdhPhbO92JIvel/TwZs\r 10 | 5Iub8S11bOOsbT4a6y9yxQk2rL8lct77nygjlrK6ejlwdHWzT6OG3yS1YoaK\r 11 | jpx3frXnITCgy8oNPn1Pxn4S7Pmmq4xl4JmH4inmlzRMZDzKG/5kVesh6pwH\r 12 | nJyJCMuyIj60gNhxBF91ndLgg+PJWwjZ3I+E/M5mo0ZscCpAnxKh/qmt4I6j\r 13 | rn/jEp1djY6nFGmuzmHIGYjhvxkGFfFEIwsqkHfGTBOkWIjK5T4WzpGNa4Dr\r 14 | k285x1R+3lzUBzl674Rl74+8wLm9DESd3k6+yOhibkv29kVxEcaCsOOzpNOx\r 15 | q/1vWCRjqoOnVXrx3tbzpjLjb647Hf/+DYA6ENpNyohAZv9bp3J1ZTrcpMXg\r 16 | /6mOxDt+C3G5ARmH/FG6JYtge1ck4GQh02CQxiYG8psaqAntd+VzaRUl+lVC\r 17 | 1pbvd9ToxWD3HaVdoGsZBEjIWt9gEtCv+RTvPY6EmrbSPQGI40HjcYG8sbY+\r 18 | OZ/Lsv1Kz9Rg/VSvCxknTCncdju07kD5eiLJUAt1u6JYd4mn/TGZBlmtRo/J\r 19 | CIuqkcg=\r 20 | =6WtN\r 21 | -----END PGP MESSAGE-----""" 22 | -------------------------------------------------------------------------------- /boxes/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, re_path 2 | from .views import (BoxListView, BoxCreateView, BoxDeleteView, BoxSubmitView, 3 | BoxCloseView) 4 | 5 | urlpatterns = [ 6 | path('', BoxListView.as_view(), name="boxes_list"), 7 | path('create', BoxCreateView.as_view(), name="boxes_create"), 8 | path('/delete', BoxDeleteView.as_view(), name="boxes_delete"), 9 | path('/closed', BoxCloseView.as_view(), name="boxes_close"), 10 | re_path(r'^(?P[0-9a-z-]+)$', BoxSubmitView.as_view(), name="boxes_show"), 11 | ] 12 | -------------------------------------------------------------------------------- /common.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile.dev 8 | image: hawkpost_app:development 9 | env_file: .env 10 | volumes: 11 | - ".:/code" 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | web: 5 | extends: 6 | file: common.yml 7 | service: app 8 | ports: 9 | - "80:8000" 10 | depends_on: 11 | - db 12 | - redis 13 | 14 | celery: 15 | extends: 16 | file: common.yml 17 | service: app 18 | command: pipenv run celery -A hawkpost worker --beat -l info 19 | volumes: 20 | - "./gpg_home:/home/user/.gnupg" 21 | depends_on: 22 | - db 23 | - redis 24 | 25 | db: 26 | image: postgres:16-alpine 27 | ports: 28 | - "5432:5432" 29 | environment: 30 | - POSTGRES_USER=hawkpost 31 | - POSTGRES_PASSWORD=hawkpost 32 | - POSTGRES_DB=hawkpost_dev 33 | 34 | redis: 35 | image: redis:7-alpine 36 | ports: 37 | - "6379:6379" 38 | -------------------------------------------------------------------------------- /hawkpost/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | # This will make sure the app is always imported when 4 | # Django starts so that shared_task will use this app. 5 | from .celery import app as celery_app # noqa 6 | -------------------------------------------------------------------------------- /hawkpost/celery.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from dotenv import read_dotenv 3 | from raven import Client 4 | from raven.contrib.celery import register_signal, register_logger_signal 5 | from celery import Celery as BaseCelery 6 | import os 7 | 8 | read_dotenv(os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')) 9 | # set the default Django settings module for the 'celery' program. 10 | environment = os.environ.get("HAWKPOST_ENV", "development") 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", 12 | "hawkpost.settings.{}".format(environment)) 13 | 14 | from django.conf import settings # noqa 15 | 16 | 17 | class Celery(BaseCelery): 18 | def on_configure(self): 19 | client = Client(settings.RAVEN_CONFIG["dsn"]) 20 | # register a custom filter to filter out duplicate logs 21 | register_logger_signal(client) 22 | # hook into the Celery error handler 23 | register_signal(client) 24 | 25 | 26 | if environment == "development": 27 | app = BaseCelery('hawkpost') 28 | else: 29 | app = Celery('hawkpost') 30 | # Using a string here means the worker will not have to 31 | # pickle the object when using Windows. 32 | app.config_from_object('django.conf:settings', namespace='CELERY') 33 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 34 | -------------------------------------------------------------------------------- /hawkpost/middleware.py: -------------------------------------------------------------------------------- 1 | from django.utils import timezone, translation 2 | from django.utils.deprecation import MiddlewareMixin 3 | 4 | 5 | class TimezoneMiddleware(MiddlewareMixin): 6 | def process_request(self, request): 7 | if request.user.is_authenticated: 8 | timezone.activate(request.user.timezone) 9 | else: 10 | timezone.deactivate() 11 | 12 | 13 | class LanguageMiddleware(MiddlewareMixin): 14 | def process_request(self, request): 15 | if request.user.is_authenticated: 16 | translation.activate(request.user.language) 17 | -------------------------------------------------------------------------------- /hawkpost/settings/common.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for hawkpost project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.9.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.9/ref/settings/ 11 | """ 12 | from django.urls import reverse_lazy 13 | from django.utils.translation import gettext_lazy as _ 14 | from celery.schedules import crontab 15 | import os 16 | 17 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 18 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 19 | 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = os.environ.get("SECRET_KEY") 26 | 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'django.contrib.sites', 41 | 'allauth', 42 | 'allauth.account', 43 | 'allauth.socialaccount', 44 | 'allauth.socialaccount.providers.github', 45 | 'timezone_field', 46 | 'axes', 47 | 'humans', 48 | 'boxes', 49 | 'pages', 50 | ] 51 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 52 | 53 | MIDDLEWARE = [ 54 | 'django.middleware.security.SecurityMiddleware', 55 | 'whitenoise.middleware.WhiteNoiseMiddleware', 56 | 'django.contrib.sessions.middleware.SessionMiddleware', 57 | 'django.middleware.locale.LocaleMiddleware', 58 | 'django.middleware.common.CommonMiddleware', 59 | 'django.middleware.csrf.CsrfViewMiddleware', 60 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 61 | 'django.contrib.messages.middleware.MessageMiddleware', 62 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 63 | 'hawkpost.middleware.TimezoneMiddleware', 64 | 'hawkpost.middleware.LanguageMiddleware', 65 | 'axes.middleware.AxesMiddleware', 66 | 'allauth.account.middleware.AccountMiddleware' 67 | ] 68 | 69 | ROOT_URLCONF = 'hawkpost.urls' 70 | 71 | TEMPLATES = [ 72 | { 73 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 74 | 'DIRS': [BASE_DIR + "/templates/"], 75 | 'APP_DIRS': True, 76 | 'OPTIONS': { 77 | 'context_processors': [ 78 | 'django.template.context_processors.debug', 79 | 'django.template.context_processors.request', 80 | 'django.contrib.auth.context_processors.auth', 81 | 'django.contrib.messages.context_processors.messages', 82 | ], 83 | }, 84 | }, 85 | ] 86 | 87 | WSGI_APPLICATION = 'hawkpost.wsgi.application' 88 | 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 105 | }, 106 | ] 107 | 108 | 109 | # Authentication settings 110 | 111 | AUTHENTICATION_BACKENDS = ( 112 | 'axes.backends.AxesBackend', 113 | # Needed to login by username in Django admin, regardless of `allauth` 114 | 'django.contrib.auth.backends.ModelBackend', 115 | # `allauth` specific authentication methods, such as login by e-mail 116 | 'allauth.account.auth_backends.AuthenticationBackend', 117 | ) 118 | 119 | 120 | # For Django AllAuth 121 | 122 | SITE_ID = 1 123 | 124 | 125 | # Internationalization 126 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 127 | 128 | LANGUAGE_CODE = 'en-us' 129 | LANGUAGES = [ 130 | ('en-us', _('English')), 131 | ('pt-pt', _('Portuguese')) 132 | ] 133 | 134 | TIME_ZONE = 'UTC' 135 | 136 | USE_I18N = True 137 | 138 | 139 | USE_TZ = True 140 | 141 | 142 | # Static files (CSS, JavaScript, Images) 143 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 144 | 145 | STATIC_URL = '/static/' 146 | 147 | STATICFILES_DIRS = [ 148 | os.path.join(BASE_DIR, "static"), 149 | ] 150 | 151 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 152 | 153 | STORAGES = { 154 | "default": { 155 | "BACKEND": "django.core.files.storage.FileSystemStorage", 156 | }, 157 | "staticfiles": { 158 | "BACKEND": 'whitenoise.storage.CompressedStaticFilesStorage', 159 | }, 160 | } 161 | 162 | # Media Files 163 | 164 | MEDIA_URL = '/media/' 165 | 166 | # Custom User Model 167 | 168 | AUTH_USER_MODEL = 'humans.User' 169 | 170 | 171 | # AllAUth Config 172 | ACCOUNT_AUTHENTICATION_METHOD = "email" 173 | ACCOUNT_EMAIL_REQUIRED = True 174 | ACCOUNT_USERNAME_REQUIRED = False 175 | ACCOUNT_UNIQUE_EMAIL = True 176 | ACCOUNT_AUTHENTICATED_LOGIN_REDIRECTS = True 177 | LOGIN_REDIRECT_URL = reverse_lazy("boxes_list") 178 | LOGIN_URL = reverse_lazy("account_login") 179 | 180 | ACCOUNT_CONFIRM_EMAIL_ON_GET = True 181 | ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True 182 | 183 | ACCOUNT_LOGOUT_ON_GET = True 184 | 185 | SOCIALACCOUNT_PROVIDERS = { 186 | 'github': { 187 | 'SCOPE': ['user:email'] 188 | } 189 | } 190 | 191 | SOCIALACCOUNT_ADAPTER = 'humans.adapter.SocialAccountAdapter' 192 | 193 | # Authentication Limits Config (AXES) 194 | AXES_FAILURE_LIMIT = 5 195 | AXES_COOLOFF_TIME = 1 # hour 196 | AXES_USERNAME_FORM_FIELD = 'login' 197 | 198 | # Email Settings 199 | DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL") 200 | 201 | # Celerey Settings 202 | CELERY_BROKER_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0") 203 | CELERY_BEAT_SCHEDULE = { 204 | "update_public_keys": { 205 | "task": "humans.tasks.update_public_keys", 206 | "schedule": crontab(minute=0, hour=4), # Every day at 4 AM UTC 207 | }, 208 | "validate_public_keys": { 209 | "task": "humans.tasks.validate_public_keys", 210 | "schedule": crontab(minute=30, hour=5), # Every day at 5:30 AM UTC 211 | } 212 | } 213 | 214 | # SITE DOMAIN 215 | SITE_DOMAIN = os.environ.get("SITE_DOMAIN") 216 | 217 | # LOCALE_PATH for translations 218 | # https://docs.djangoproject.com/en/1.11/ref/settings/#std:setting-LOCALE_PATHS 219 | LOCALE_PATHS = ['locale'] 220 | 221 | 222 | # File Uploads 223 | DATA_UPLOAD_MAX_MEMORY_SIZE = 15728640 # bytes == 15Mb 224 | 225 | # Data about the instance 226 | SUPPORT_NAME = os.environ.get("SUPPORT_NAME") 227 | SUPPORT_EMAIL = os.environ.get("SUPPORT_EMAIL") 228 | INSTANCE_DESCRIPTION = os.environ.get("INSTANCE_DESCRIPTION", "") 229 | VERSION = "1.4.0" 230 | -------------------------------------------------------------------------------- /hawkpost/settings/development.py: -------------------------------------------------------------------------------- 1 | from .common import * 2 | 3 | # SECURITY WARNING: don't run with debug turned on in production! 4 | DEBUG = True 5 | 6 | if 'ALLOWED_HOSTS' in os.environ: 7 | ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS").split(',') 8 | 9 | # Database 10 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 11 | 12 | DATABASES = { 13 | 'default': { 14 | 'ENGINE': 'django.db.backends.postgresql', 15 | 'NAME': "hawkpost_dev", 16 | } 17 | } 18 | 19 | # If the DB_HOST was specified it is overriding the default connection 20 | if 'DB_HOST' in os.environ: 21 | DATABASES['default']['HOST'] = os.environ.get("DB_HOST", "localhost") 22 | DATABASES['default']['PORT'] = os.environ.get("DB_PORT", 5432) 23 | DATABASES['default']['USER'] = os.environ.get("DB_USER", "postgres") 24 | DATABASES['default']['NAME'] = os.environ.get("DB_NAME", "hawkpost_dev") 25 | 26 | if 'DB_PASSWORD' in os.environ: 27 | DATABASES['default']['PASSWORD'] = os.environ.get("DB_PASSWORD", "postgres") 28 | 29 | # Development Applications 30 | INSTALLED_APPS += ( 31 | 'debug_toolbar', 32 | 'django_extensions', 33 | ) 34 | 35 | MIDDLEWARE.insert(0, 'debug_toolbar.middleware.DebugToolbarMiddleware') 36 | 37 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 38 | 39 | DEBUG_TOOLBAR_PANELS = [ 40 | 'debug_toolbar.panels.versions.VersionsPanel', 41 | 'debug_toolbar.panels.timer.TimerPanel', 42 | 'debug_toolbar.panels.settings.SettingsPanel', 43 | 'debug_toolbar.panels.headers.HeadersPanel', 44 | 'debug_toolbar.panels.request.RequestPanel', 45 | 'debug_toolbar.panels.sql.SQLPanel', 46 | 'debug_toolbar.panels.staticfiles.StaticFilesPanel', 47 | 'debug_toolbar.panels.templates.TemplatesPanel', 48 | 'debug_toolbar.panels.cache.CachePanel', 49 | 'debug_toolbar.panels.signals.SignalsPanel', 50 | 'debug_toolbar.panels.logging.LoggingPanel', 51 | 'debug_toolbar.panels.redirects.RedirectsPanel', 52 | ] 53 | 54 | INTERNAL_IPS = ['127.0.0.1'] 55 | if 'INTERNAL_IPS' in os.environ: 56 | INTERNAL_IPS += os.environ.get("INTERNAL_IPS").split(',') 57 | -------------------------------------------------------------------------------- /hawkpost/settings/production.py: -------------------------------------------------------------------------------- 1 | from .common import * 2 | import os 3 | 4 | 5 | # SECURITY WARNING: don't run with debug turned on in production! 6 | DEBUG = False 7 | 8 | ALLOWED_HOSTS = [SITE_DOMAIN] 9 | 10 | 11 | # Database 12 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 13 | DATABASES = { 14 | 'default': { 15 | 'ENGINE': 'django.db.backends.postgresql', 16 | 'NAME': os.environ.get("DB_NAME"), 17 | 'USER': os.environ.get("DB_USER"), 18 | 'PASSWORD': os.environ.get("DB_PASSWORD"), 19 | 'HOST': os.environ.get("DB_HOST"), 20 | 'PORT': os.environ.get("DB_PORT"), 21 | } 22 | } 23 | 24 | 25 | # Installed Apps 26 | # Development Applications 27 | INSTALLED_APPS += ( 28 | 'raven.contrib.django.raven_compat', 29 | ) 30 | 31 | 32 | # Email Settings 33 | EMAIL_HOST = os.environ.get("EMAIL_HOST") 34 | EMAIL_PORT = os.environ.get("EMAIL_PORT") 35 | EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER") 36 | EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD") 37 | EMAIL_USE_TLS = True 38 | 39 | 40 | # Security Setings 41 | CSRF_COOKIE_SECURE = True 42 | SECURE_BROWSER_XSS_FILTER = True 43 | SECURE_CONTENT_TYPE_NOSNIFF = True 44 | SECURE_HSTS_SECONDS = 600000 45 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 46 | # SECURE_SSL_REDIRECT = True 47 | SESSION_COOKIE_SECURE = True 48 | USE_X_FORWARDED_HOST = True 49 | X_FRAME_OPTIONS = 'DENY' 50 | CSRF_COOKIE_HTTPONLY = True 51 | 52 | 53 | # Allauth Config 54 | ACCOUNT_EMAIL_VERIFICATION = "mandatory" 55 | # We trust that the chosen providers did the verification 56 | SOCIALACCOUNT_EMAIL_VERIFICATION = "none" 57 | 58 | 59 | # Sentry Configuration 60 | RAVEN_CONFIG = { 61 | 'dsn': os.environ.get("SENTRY_URL") 62 | } 63 | 64 | # Axes Behind proxy 65 | AXES_META_PRECEDENCE_ORDER = [ 66 | 'HTTP_X_FORWARDED_FOR', 67 | 'REMOTE_ADDR', 68 | ] 69 | -------------------------------------------------------------------------------- /hawkpost/static/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /hawkpost/static/fonts/Raleway-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/fonts/Raleway-Bold.ttf -------------------------------------------------------------------------------- /hawkpost/static/fonts/Raleway-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/fonts/Raleway-ExtraBold.ttf -------------------------------------------------------------------------------- /hawkpost/static/fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /hawkpost/static/fonts/Roboto-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/fonts/Roboto-Bold.woff -------------------------------------------------------------------------------- /hawkpost/static/fonts/Roboto-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/fonts/Roboto-Bold.woff2 -------------------------------------------------------------------------------- /hawkpost/static/fonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/fonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /hawkpost/static/fonts/Roboto-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/fonts/Roboto-Medium.woff -------------------------------------------------------------------------------- /hawkpost/static/fonts/Roboto-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/fonts/Roboto-Medium.woff2 -------------------------------------------------------------------------------- /hawkpost/static/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /hawkpost/static/fonts/Roboto-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/fonts/Roboto-Regular.woff -------------------------------------------------------------------------------- /hawkpost/static/fonts/Roboto-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/fonts/Roboto-Regular.woff2 -------------------------------------------------------------------------------- /hawkpost/static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /hawkpost/static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /hawkpost/static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /hawkpost/static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /hawkpost/static/images/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/images/close.png -------------------------------------------------------------------------------- /hawkpost/static/images/external.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/images/external.png -------------------------------------------------------------------------------- /hawkpost/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/images/favicon.ico -------------------------------------------------------------------------------- /hawkpost/static/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/images/favicon.png -------------------------------------------------------------------------------- /hawkpost/static/images/icon1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | icon1 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /hawkpost/static/images/icon2.svg: -------------------------------------------------------------------------------- 1 | icon2 -------------------------------------------------------------------------------- /hawkpost/static/images/icon3.svg: -------------------------------------------------------------------------------- 1 | icon3 -------------------------------------------------------------------------------- /hawkpost/static/images/keyserver_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/images/keyserver_key.png -------------------------------------------------------------------------------- /hawkpost/static/images/logob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/images/logob.png -------------------------------------------------------------------------------- /hawkpost/static/images/logow-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/images/logow-small.png -------------------------------------------------------------------------------- /hawkpost/static/images/logow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/images/logow.png -------------------------------------------------------------------------------- /hawkpost/static/images/ls.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Combined Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /hawkpost/static/images/paper-bin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/images/paper-bin.png -------------------------------------------------------------------------------- /hawkpost/static/images/rs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Combined Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /hawkpost/static/images/static_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/hawkpost/static/images/static_key.png -------------------------------------------------------------------------------- /hawkpost/static/javascripts/auth_modals.js: -------------------------------------------------------------------------------- 1 | $("#id_email").addClass("text padding-modals1"); 2 | $("#id_password1").addClass("text padding-modals1"); 3 | $("#id_password2").addClass("text padding-modals1"); 4 | $("#id_login").addClass("text padding-modals1"); 5 | $("#id_password").addClass("text padding-modals1"); -------------------------------------------------------------------------------- /hawkpost/static/javascripts/classie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * classie - class helper functions 3 | * from bonzo https://github.com/ded/bonzo 4 | * 5 | * classie.has( elem, 'my-class' ) -> true/false 6 | * classie.add( elem, 'my-new-class' ) 7 | * classie.remove( elem, 'my-unwanted-class' ) 8 | * classie.toggle( elem, 'my-class' ) 9 | */ 10 | 11 | /*jshint browser: true, strict: true, undef: true */ 12 | /*global define: false */ 13 | 14 | (function(window){ 15 | 'use strict'; 16 | 17 | // class helper functions from bonzo https://github.com/ded/bonzo 18 | function classReg( className ) { 19 | return new RegExp("(^|\\s+)" + className + "(\\s+|$)"); 20 | } 21 | 22 | // classList support for class management 23 | // altho to be fair, the api sucks because it won't accept multiple classes at once 24 | var hasClass, addClass, removeClass; 25 | 26 | if ( 'classList' in document.documentElement ) { 27 | hasClass = function( elem, c ) { 28 | return elem.classList.contains( c ); 29 | }; 30 | addClass = function( elem, c ) { 31 | elem.classList.add( c ); 32 | }; 33 | removeClass = function( elem, c ) { 34 | elem.classList.remove( c ); 35 | }; 36 | } 37 | else { 38 | hasClass = function( elem, c ) { 39 | return classReg( c ).test( elem.className ); 40 | }; 41 | addClass = function( elem, c ) { 42 | if ( !hasClass( elem, c ) ) { 43 | elem.className = elem.className + ' ' + c; 44 | } 45 | }; 46 | removeClass = function( elem, c ) { 47 | elem.className = elem.className.replace( classReg( c ), ' ' ); 48 | }; 49 | } 50 | 51 | function toggleClass( elem, c ) { 52 | var fn = hasClass( elem, c ) ? removeClass : addClass; 53 | fn( elem, c ); 54 | } 55 | 56 | var classie = { 57 | // full names 58 | hasClass: hasClass, 59 | addClass: addClass, 60 | removeClass: removeClass, 61 | toggleClass: toggleClass, 62 | // short names 63 | has: hasClass, 64 | add: addClass, 65 | remove: removeClass, 66 | toggle: toggleClass 67 | }; 68 | 69 | // transport 70 | if ( typeof define === 'function' && define.amd ) { 71 | // AMD 72 | define( classie ); 73 | } else { 74 | // browser global 75 | window.classie = classie; 76 | } 77 | })(window); 78 | -------------------------------------------------------------------------------- /hawkpost/static/javascripts/global.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | setTimeout(function(){ 3 | $(".messages-elem-js").remove(); 4 | }, 3000); 5 | 6 | /* 7 | On user settings page, open the tooltips/popups 8 | */ 9 | 10 | $(".errorlist").addClass("smalltext"); 11 | 12 | $(".server-signed-info-js").on("click", function () { 13 | var popup = document.getElementById('server-signed-content-js'); 14 | popup.classList.toggle('show'); 15 | }) 16 | 17 | $(".keys-help-popup-js").on("click", function () { 18 | var popup = document.getElementById('keys-help-content-js'); 19 | popup.classList.toggle('show'); 20 | }) 21 | 22 | $(".faq__box").click(function(){ 23 | $(this).toggleClass("open__faq__box"); 24 | $(".faq__box").not(this).removeClass("open__faq__box"); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /hawkpost/static/javascripts/modalEffects.js: -------------------------------------------------------------------------------- 1 | /** 2 | * modalEffects.js v1.0.0 3 | * http://www.codrops.com 4 | * 5 | * Licensed under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 8 | * Copyright 2013, Codrops 9 | * http://www.codrops.com 10 | */ 11 | var ModalEffects = (function() { 12 | function init() { 13 | var overlay = document.querySelector( '.md-overlay' ); 14 | [].slice.call( document.querySelectorAll( '.md-trigger' ) ).forEach( function( el, i ) { 15 | var modal = document.querySelector( '#' + el.getAttribute( 'data-modal' ) ), 16 | close = modal.querySelector( '.md-close' ); 17 | 18 | function removeModal( hasPerspective ) { 19 | classie.remove( modal, 'md-show' ); 20 | 21 | if( hasPerspective ) { 22 | classie.remove( document.documentElement, 'md-perspective' ); 23 | } 24 | } 25 | 26 | function removeModalHandler() { 27 | removeModal( classie.has( el, 'md-setperspective' ) ); 28 | } 29 | 30 | el.addEventListener( 'click', function( ev ) { 31 | if(el.getAttribute('data-modal') == 'modal-login') { 32 | aux = document.querySelector( '#modal-signup' ); 33 | classie.remove(aux, 'md-show'); 34 | } else { 35 | aux = document.querySelector( '#modal-login' ); 36 | classie.remove(aux, 'md-show'); 37 | } 38 | classie.add( modal, 'md-show' ); 39 | overlay.removeEventListener( 'click', removeModalHandler ); 40 | //overlay.addEventListener( 'click', removeModalHandler ); 41 | 42 | if( classie.has( el, 'md-setperspective' ) ) { 43 | setTimeout( function() { 44 | classie.add( document.documentElement, 'md-perspective' ); 45 | }, 25 ); 46 | } 47 | }); 48 | 49 | close.addEventListener( 'click', function( ev ) { 50 | ev.stopPropagation(); 51 | removeModalHandler(); 52 | }); 53 | }); 54 | } 55 | 56 | init(); 57 | })(); 58 | -------------------------------------------------------------------------------- /hawkpost/templates/account/account_inactive.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Account Inactive" %}{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "Account Inactive" %}

9 | 10 |

{% trans "This account is inactive." %}

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /hawkpost/templates/account/auth_modals.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load account socialaccount %} 3 | {% load static %} 4 | 5 | 68 | 69 | 117 |
118 | 119 | -------------------------------------------------------------------------------- /hawkpost/templates/account/email.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Account" %}{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "E-mail Addresses" %}

9 | {% if user.emailaddress_set.all %} 10 |

{% trans 'The following e-mail addresses are associated with your account:' %}

11 | 12 | 41 | 42 | {% else %} 43 |

{% trans 'Warning:'%} {% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}

44 | 45 | {% endif %} 46 | 47 | 48 |

{% trans "Add E-mail Address" %}

49 | 50 |
51 | {% csrf_token %} 52 | {{ form.as_p}} 53 | 54 |
55 | 56 | {% endblock %} 57 | 58 | 59 | {% block extra_body %} 60 | 73 | {% endblock %} 74 | -------------------------------------------------------------------------------- /hawkpost/templates/account/email/email_confirmation_message.txt: -------------------------------------------------------------------------------- 1 | {% load account %} 2 | {% user_display user as user_display %} 3 | {% load i18n %} 4 | {% autoescape off %} 5 | {% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Hello there, 6 | 7 | This is an confirmation email, sent by {{ site_name }} to every new user of the service in order to verify their email address. 8 | 9 | You're receiving it, because you (or someone else) provided it during registration. 10 | 11 | To confirm this is correct, please visit the following address: {{ activate_url }} 12 | 13 | Otherwise you can either ignore this message or contact us through the support channels (at {{ site_domain }}{% endblocktrans %}{% url 'pages_help' %}){% blocktrans %}, so this account can be removed. 14 | {% endblocktrans %} 15 | {% endautoescape %} 16 | 17 | {% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Best regards, 18 | {{ site_name }} 19 | {{ site_domain }} 20 | {% endblocktrans %} 21 | -------------------------------------------------------------------------------- /hawkpost/templates/account/email/email_confirmation_signup_message.txt: -------------------------------------------------------------------------------- 1 | {% include "account/email/email_confirmation_message.txt" %} 2 | -------------------------------------------------------------------------------- /hawkpost/templates/account/email/email_confirmation_signup_subject.txt: -------------------------------------------------------------------------------- 1 | {% include "account/email/email_confirmation_subject.txt" %} 2 | -------------------------------------------------------------------------------- /hawkpost/templates/account/email/email_confirmation_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans with site_name=current_site.name %}{{site_name}} Please Confirm Your E-mail Address{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /hawkpost/templates/account/email/password_reset_key_message.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans with site_name=current_site.name site_domain=current_site.domain %} 3 | Hi there, 4 | 5 | You're receiving this e-mail because you or someone else has requested a password for your user account at {{ site_domain }}. 6 | 7 | If you did not request a password reset, this message can be safely ignored. 8 | 9 | Otherwise, to continue with the process of reseting you password visit the following page: 10 | {% endblocktrans %} 11 | 12 | {{ password_reset_url }} 13 | 14 | {% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Thank you for using {{ site_name }}! 15 | {{ site_domain }} 16 | {% endblocktrans %} 17 | -------------------------------------------------------------------------------- /hawkpost/templates/account/email/password_reset_key_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans with site_name=current_site.name %}{{site_name}} Password Reset{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /hawkpost/templates/account/email_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account %} 5 | 6 | {% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %} 7 | 8 | 9 | {% block content %} 10 |

{% trans "Confirm E-mail Address" %}

11 | 12 | {% if confirmation %} 13 | 14 | {% user_display confirmation.email_address.user as user_display %} 15 | 16 |

{% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is an e-mail address for user {{ user_display }}.{% endblocktrans %}

17 | 18 |
19 | {% csrf_token %} 20 | 21 |
22 | 23 | {% else %} 24 | 25 | {% url 'account_email' as email_url %} 26 | 27 |

{% blocktrans %}This e-mail confirmation link expired or is invalid. Please issue a new e-mail confirmation request.{% endblocktrans %}

28 | 29 | {% endif %} 30 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /hawkpost/templates/account/logout.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Sign Out" %}{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "Sign Out" %}

9 | 10 |

{% trans "Are you sure you want to sign out?" %}

11 | 12 |
13 | {% csrf_token %} 14 | {% if redirect_field_value %} 15 | 16 | {% endif %} 17 | 18 |
19 | 20 | 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /hawkpost/templates/account/messages/cannot_delete_primary_email.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}You cannot remove your primary e-mail address ({{email}}).{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /hawkpost/templates/account/messages/email_confirmation_sent.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Confirmation e-mail sent to {{email}}.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /hawkpost/templates/account/messages/email_confirmed.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}You have confirmed {{email}}.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /hawkpost/templates/account/messages/email_deleted.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Removed e-mail address {{email}}.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /hawkpost/templates/account/messages/logged_in.txt: -------------------------------------------------------------------------------- 1 | {% load account %} 2 | {% load i18n %} 3 | {% user_display user as name %} 4 | {% blocktrans %}Successfully signed in as {{name}}.{% endblocktrans %} 5 | -------------------------------------------------------------------------------- /hawkpost/templates/account/messages/logged_out.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}You have signed out.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /hawkpost/templates/account/messages/password_changed.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Password successfully changed.{% endblocktrans %} 3 | 4 | -------------------------------------------------------------------------------- /hawkpost/templates/account/messages/password_set.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Password successfully set.{% endblocktrans %} 3 | 4 | -------------------------------------------------------------------------------- /hawkpost/templates/account/messages/primary_email_set.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Primary e-mail address set.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /hawkpost/templates/account/messages/unverified_primary_email.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Your primary e-mail address must be verified.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /hawkpost/templates/account/password_change.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Change Password" %}{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |
11 |
12 |
13 |

{% trans "Change Password" %}

14 |
15 |
16 | 17 |
18 |
19 |
20 | {% csrf_token %} 21 | 22 |
23 | 24 | 25 |
26 | 27 |
28 | 29 | 30 |
31 | 32 |
33 | 34 | 35 |
36 | 37 | 38 |
39 |
40 |
41 |
42 |
43 |
44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /hawkpost/templates/account/password_reset.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account %} 5 | 6 | {% block head_title %}{% trans "Password Reset" %}{% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 |
12 |
13 |
14 |

{% trans "PASSWORD RESET" %}

15 | {% if user.is_authenticated %} 16 | {% include "account/snippets/already_logged_in.html" %} 17 | {% endif %} 18 |

{% trans "Forgot your password?
Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}

19 |
20 |
21 |
22 |
23 |
24 | {% csrf_token %} 25 |
26 | 27 | 28 |
29 | 30 | 31 |
32 | 33 |

{% blocktrans %}Please contact us if you have any trouble resetting your password.{% endblocktrans %}

34 |
35 |
36 |
37 |
38 |
39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /hawkpost/templates/account/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account %} 5 | 6 | {% block head_title %}{% trans "Password Reset" %}{% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 |
12 |
13 |
14 |

{% trans "PASSWORD RESET" %}

15 | 16 | {% if user.is_authenticated %} 17 | {% include "account/snippets/already_logged_in.html" %} 18 | {% endif %} 19 | 20 |

{% blocktrans %}We have sent you an e-mail. 21 |
Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

22 |
23 |
24 |
25 |
26 |
27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /hawkpost/templates/account/password_reset_from_key.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | 3 | {% load i18n %} 4 | {% block head_title %}{% trans "Change Password" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |
10 |
11 |
12 |

{% if token_fail %}{% trans "Bad Token" %}{% else %}{% trans "CHANGE PASSWORD" %}{% endif %}

13 | 14 | {% if token_fail %} 15 | {% url 'account_reset_password' as passwd_reset_url %} 16 |

{% blocktrans %}The password reset link was invalid, possibly because it has already been used. Please request a new password reset.{% endblocktrans %}

17 | {% else %} 18 | {% if form %} 19 |
20 | {% csrf_token %} 21 |
22 |
23 | 24 | {{form.password1}} 25 | {{form.password1.errors}} 26 |
27 | 28 |
29 | 30 | {{form.password2}} 31 | {{form.password2.errors}} 32 |
33 |
34 | 35 | 36 |
37 | {% else %} 38 |

{% trans "Your password is now changed." %}

39 | {% endif %} 40 | {% endif %} 41 |
42 |
43 |
44 |
45 |
46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /hawkpost/templates/account/password_reset_from_key_done.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | 3 | {% load i18n %} 4 | {% block head_title %}{% trans "Change Password" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |
10 |
11 |
12 |

{% trans "CHANGE PASSWORD" %}

13 |

{% trans "Your password is now changed." %}

14 |

{% trans "Take me back!" %}

15 |
16 |
17 |
18 |
19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /hawkpost/templates/account/password_set.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Set Password" %}{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "Set Password" %}

9 | 10 |
11 | {% csrf_token %} 12 | {{ form.as_p }} 13 | 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /hawkpost/templates/account/signup_closed.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Sign Up Closed" %}{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "Sign Up Closed" %}

9 | 10 |

{% trans "We are sorry, but the sign up is currently closed." %}

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /hawkpost/templates/account/snippets/already_logged_in.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load account %} 3 | 4 | {% user_display user as user_display %} 5 |

{% trans "Note" %}: {% blocktrans %}you are already logged in as {{ user_display }}.{% endblocktrans %}

6 | -------------------------------------------------------------------------------- /hawkpost/templates/account/verification_sent.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |
11 |
12 |
13 |

{% trans "VERIFY YOUR E-MAIL ADDRESS" %}

14 | 15 |

{% blocktrans %}We have sent an e-mail to you for verification.

16 |

Follow the link provided to finalize the signup process. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

17 |
18 |
19 |
20 |
21 |
22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /hawkpost/templates/account/verified_email_required.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |
11 |
12 |
13 |

{% trans "VERIFY YOUR E-MAIL ADDRESS" %}

14 | 15 | {% url 'account_email' as email_url %} 16 | 17 |

{% blocktrans %}This part of the site requires us to verify that you are who you claim to be. For this purpose, we require that you verify ownership of your e-mail address. {% endblocktrans %}

18 | 19 |

{% blocktrans %}We have sent an e-mail to you for verification. Please click on the link inside this e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

20 | 21 |

{% blocktrans %}Note: you can still change your e-mail address.{% endblocktrans %}

22 |
23 |
24 |
25 |
26 |
27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /hawkpost/templates/layout/base.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load static %} 3 | 4 | 5 | 6 | 7 | {% trans "Hawkpost - Submit your content securely" %} 8 | 9 | {% block styles %} 10 | 11 | 12 | 13 | 14 | {% endblock styles %} 15 | 16 | 17 | 18 | 19 | {% block header %} {% include "layout/navbar.html" %} {% include "layout/messages.html" %} {% endblock header %} 20 |
21 | {% block content %} {% endblock content %} 22 |
23 | {% if not request.user.is_authenticaded %} 24 | {% include "account/auth_modals.html" %} 25 | {% endif %} 26 | {% block footer %} 27 | 82 | {% endblock footer %} 83 | {% block scripts %} 84 | 85 | 86 | 87 | {% if not request.user.is_authenticated %} 88 | 89 | {% endif %} 90 | {% endblock scripts %} 91 | 92 | 93 | -------------------------------------------------------------------------------- /hawkpost/templates/layout/messages.html: -------------------------------------------------------------------------------- 1 | {% if messages %} 2 |
    3 | {% for message in messages %} 4 | {{ message }} 5 | {% endfor %} 6 |
7 | {% endif %} 8 | -------------------------------------------------------------------------------- /hawkpost/templates/layout/navbar.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load static %} 3 | 4 | 29 | 30 | 64 | -------------------------------------------------------------------------------- /hawkpost/templates/socialaccount/authentication_error.html: -------------------------------------------------------------------------------- 1 | {% extends "socialaccount/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Social Network Login Failure" %}{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "Social Network Login Failure" %}

9 | 10 |

{% trans "An error occurred while attempting to login via your social network account." %}

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /hawkpost/templates/socialaccount/base.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | -------------------------------------------------------------------------------- /hawkpost/templates/socialaccount/connections.html: -------------------------------------------------------------------------------- 1 | {% extends "socialaccount/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Account Connections" %}{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "Account Connections" %}

9 | 10 | {% if form.accounts %} 11 |

{% blocktrans %}You can sign in to your account using any of the following third party accounts:{% endblocktrans %}

12 | 13 | 14 |
15 | {% csrf_token %} 16 | 17 |
18 | {% if form.non_field_errors %} 19 |
{{ form.non_field_errors }}
20 | {% endif %} 21 | 22 | {% for base_account in form.accounts %} 23 | {% with base_account.get_provider_account as account %} 24 |
25 | 30 |
31 | {% endwith %} 32 | {% endfor %} 33 | 34 |
35 | 36 |
37 | 38 |
39 | 40 |
41 | 42 | {% else %} 43 |

{% trans 'You currently have no social network accounts connected to this account.' %}

44 | {% endif %} 45 | 46 |

{% trans 'Add a 3rd Party Account' %}

47 | 48 |
    49 | {% include "socialaccount/snippets/provider_list.html" with process="connect" %} 50 |
51 | 52 | {% include "socialaccount/snippets/login_extra.html" %} 53 | 54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /hawkpost/templates/socialaccount/login_cancelled.html: -------------------------------------------------------------------------------- 1 | {% extends "socialaccount/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Login Cancelled" %}{% endblock %} 6 | 7 | {% block content %} 8 | 9 |

{% trans "Login Cancelled" %}

10 | 11 | {% url 'account_login' as login_url %} 12 | 13 |

{% blocktrans %}You decided to cancel logging in to our site using one of your existing accounts. If this was a mistake, please proceed to sign in.{% endblocktrans %}

14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /hawkpost/templates/socialaccount/messages/account_connected.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}The social account has been connected.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /hawkpost/templates/socialaccount/messages/account_connected_other.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}The social account is already connected to a different account.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /hawkpost/templates/socialaccount/messages/account_disconnected.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}The social account has been disconnected.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /hawkpost/templates/socialaccount/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "socialaccount/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Signup" %}{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "Sign Up" %}

9 | 10 |

{% blocktrans with provider_name=account.get_provider.name site_name=site.name %}You are about to use your {{provider_name}} account to login to 11 | {{site_name}}. As a final step, please complete the following form:{% endblocktrans %}

12 | 13 | 21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /hawkpost/templates/socialaccount/snippets/login_extra.html: -------------------------------------------------------------------------------- 1 | {% load socialaccount %} 2 | 3 | {% providers_media_js %} 4 | 5 | -------------------------------------------------------------------------------- /hawkpost/templates/socialaccount/snippets/provider_list.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load socialaccount %} 3 | 4 | {% get_providers as socialaccount_providers %} 5 | 6 | {% for provider in socialaccount_providers %} 7 | {% if provider.id == "openid" %} 8 | {% for brand in provider.get_brands %} 9 | 10 | {{brand.name}} 14 | 15 | {% endfor %} 16 | {% endif %} 17 | 18 | 20 |
21 | 22 |

{% blocktrans with site.name as site_name %}Sign in with GitHub{% endblocktrans %}

23 |
24 |
25 | 26 | 27 | {% endfor %} 28 | 29 | -------------------------------------------------------------------------------- /hawkpost/urls.py: -------------------------------------------------------------------------------- 1 | """hawkpost URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.9/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.urls import include, path, re_path 18 | from django.contrib import admin 19 | from django.conf.urls.i18n import i18n_patterns 20 | from django.utils.decorators import method_decorator 21 | 22 | from allauth.account.views import LoginView 23 | from axes.decorators import axes_dispatch 24 | from axes.decorators import axes_form_invalid 25 | 26 | from humans.forms import LoginForm 27 | 28 | LoginView.dispatch = method_decorator(axes_dispatch)(LoginView.dispatch) 29 | LoginView.form_invalid = method_decorator( 30 | axes_form_invalid)(LoginView.form_invalid) 31 | 32 | urlpatterns = [ 33 | path('admin/login/', admin.site.login), 34 | re_path(r'^admin/', admin.site.urls), 35 | path('users/login/', LoginView.as_view(form_class=LoginForm), 36 | name='account_login'), 37 | path('users/', include('allauth.urls')), 38 | path('users/', include('humans.urls')), 39 | path('box/', include('boxes.urls')), 40 | path('', include('pages.urls')) 41 | ] 42 | 43 | urlpatterns += i18n_patterns( 44 | path('', include('pages.urls')), 45 | ) 46 | 47 | if settings.DEBUG: 48 | import debug_toolbar 49 | urlpatterns = [ 50 | path('__debug__/', include(debug_toolbar.urls)), 51 | ] + urlpatterns 52 | -------------------------------------------------------------------------------- /hawkpost/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for hawkpost project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | from raven.contrib.django.raven_compat.middleware.wsgi import Sentry 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hawkpost.settings") 16 | 17 | application = Sentry(get_wsgi_application()) 18 | -------------------------------------------------------------------------------- /humans/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/humans/__init__.py -------------------------------------------------------------------------------- /humans/adapter.py: -------------------------------------------------------------------------------- 1 | from allauth.account.models import EmailAddress 2 | from allauth.socialaccount.adapter import DefaultSocialAccountAdapter 3 | 4 | class SocialAccountAdapter(DefaultSocialAccountAdapter): 5 | def pre_social_login(self, request, sociallogin): 6 | if sociallogin.is_existing: 7 | return 8 | 9 | try: 10 | email = EmailAddress.objects.get(email=sociallogin.user.email) 11 | except EmailAddress.DoesNotExist: 12 | return 13 | 14 | sociallogin.connect(request, email.user) 15 | -------------------------------------------------------------------------------- /humans/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin 3 | from django.contrib import messages 4 | from django.utils import timezone 5 | from django.utils.translation import gettext_lazy as _ 6 | from .models import User, Notification, KeyChangeRecord 7 | from .tasks import enqueue_email_notifications 8 | 9 | 10 | @admin.register(User) 11 | class UserAdmin(DefaultUserAdmin): 12 | list_display = ('username', 13 | 'email', 14 | "first_name", 15 | "last_name", 16 | "is_staff", 17 | "has_public_key", 18 | "has_keyserver_url") 19 | 20 | def __init__(self, *args, **kwargs): 21 | super().__init__(*args, **kwargs) 22 | self.fieldsets += ((_('Key options'), { 23 | 'classes': ('collapse',), 24 | 'fields': ('fingerprint', 'keyserver_url', 'public_key'), 25 | }),) 26 | 27 | 28 | @admin.register(Notification) 29 | class NotificationAdmin(admin.ModelAdmin): 30 | list_display = ('subject', 31 | 'sent_at', 32 | 'send_to') 33 | list_filter = ('send_to', 'sent_at') 34 | 35 | fields = ["subject", "body", "send_to"] 36 | search_fields = ['subject', 'body'] 37 | 38 | actions = ["send_notification"] 39 | 40 | def delete_model(self, request, obj): 41 | if obj.sent_at: 42 | msg = _('Cannot delete "{}", the notification was already sent') 43 | messages.error(request, msg.format(obj.subject)) 44 | else: 45 | obj.delete() 46 | 47 | @admin.action( 48 | description=_('Delete selected notifications') 49 | ) 50 | def delete_selected(self, request, queryset): 51 | queryset.filter(sent_at=None).delete() 52 | msg = _('Removed all unsent notifications in selection') 53 | messages.success(request, msg) 54 | 55 | @admin.action( 56 | description=_('Send selected notifications') 57 | ) 58 | def send_notification(self, request, queryset): 59 | queryset = queryset.filter(sent_at=None) 60 | for notification in queryset: 61 | send_to = notification.send_to.id if notification.send_to else None 62 | enqueue_email_notifications.delay(notification.id, 63 | send_to) 64 | queryset.update(sent_at=timezone.now()) 65 | messages.success(request, _('All notifications enqueued for sending')) 66 | 67 | 68 | 69 | @admin.register(KeyChangeRecord) 70 | class KeyChangeRecordAdmin(admin.ModelAdmin): 71 | list_display = ('user', 72 | 'prev_fingerprint', 73 | 'to_fingerprint', 74 | 'ip_address', 75 | 'agent', 76 | 'created_at') 77 | search_fields = ['user__email', 'ip_address', 'agent'] 78 | list_filter = ('created_at',) 79 | 80 | def __init__(self, *args, **kwargs): 81 | super().__init__(*args, **kwargs) 82 | self.readonly_fields = [f.name for f in self.model._meta.get_fields()] 83 | 84 | def has_add_permission(self, request): 85 | return False 86 | 87 | def has_delete_permission(self, request, obj=None): 88 | return False 89 | 90 | 91 | admin.site.site_header = _('Hawkpost Administration') 92 | admin.site.site_title = _('Hawkpost Admin') 93 | admin.site.index_title = _('Project Models') 94 | -------------------------------------------------------------------------------- /humans/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AuthConfig(AppConfig): 5 | name = 'humans' 6 | -------------------------------------------------------------------------------- /humans/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import ModelForm 2 | from django import forms 3 | from django.contrib.auth.password_validation import validate_password 4 | from django.utils.translation import gettext_lazy as _ 5 | from allauth.account.forms import LoginForm as BaseLoginForm 6 | from allauth.account.forms import SignupForm as BaseSignupForm 7 | from .models import User 8 | from .utils import key_state 9 | import requests 10 | 11 | 12 | class UpdateUserInfoForm(ModelForm): 13 | 14 | class Meta: 15 | model = User 16 | fields = [ 17 | "first_name", 18 | "last_name", 19 | "organization", 20 | "keyserver_url", 21 | "public_key", 22 | "fingerprint", 23 | "timezone", 24 | "language" 25 | ] 26 | 27 | widgets = { 28 | 'keyserver_url': forms.TextInput(attrs={'placeholder': _("https://example.com/key.asc")}), 29 | 'public_key': forms.Textarea(attrs={'placeholder': _("-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: SKS 1.1.1\n\n-----END PGP PUBLIC KEY BLOCK-----")}) 30 | } 31 | 32 | current_password = forms.CharField(label=_('Current password'), 33 | required=False, 34 | widget=forms.PasswordInput) 35 | new_password1 = forms.CharField(label=_('New password'), 36 | required=False, 37 | widget=forms.PasswordInput) 38 | new_password2 = forms.CharField(label=_('New password confirmation'), 39 | required=False, 40 | widget=forms.PasswordInput) 41 | 42 | def __init__(self, *args, **kwargs): 43 | # Flag to let the save method know when to call set_password 44 | self.change_password = False 45 | self.pub_key = None 46 | return super().__init__(*args, **kwargs) 47 | 48 | def save(self, commit=True, **kwargs): 49 | new_password = self.cleaned_data.get('new_password2') 50 | if self.change_password: 51 | self.instance.set_password(new_password) 52 | self.instance.save(**kwargs) 53 | return self.instance 54 | 55 | def clean_current_password(self): 56 | """ 57 | Validates that the current_password field is correct. 58 | """ 59 | current_password = self.cleaned_data.get('current_password') 60 | if len(current_password) > 0: 61 | if not self.instance.check_password(current_password): 62 | self.add_error('current_password', 63 | _('Your current password was entered incorrectly.')) 64 | return current_password 65 | 66 | def clean_new_password2(self): 67 | """ 68 | Validates that both new password entries are equal. 69 | """ 70 | password1 = self.cleaned_data.get('new_password1') 71 | password2 = self.cleaned_data.get('new_password2') 72 | if password1 and password2: 73 | validate_password(password1, self.instance) 74 | if password1 != password2: 75 | self.add_error('new_password2', 76 | _("The two password fields didn't match.")) 77 | else: 78 | self.change_password = True 79 | return password2 80 | 81 | def clean_public_key(self): 82 | # Validate the public key 83 | if self.pub_key: 84 | pub_key = self.pub_key 85 | else: 86 | pub_key = self.cleaned_data.get("public_key", "") 87 | 88 | if pub_key: 89 | fingerprint, *state = key_state(pub_key) 90 | # Check if has valid format 91 | if state[0] == "invalid": 92 | self.add_error('public_key', _('This key is not valid')) 93 | # Check if it is not expired 94 | elif state[0] == "revoked": 95 | self.add_error('public_key', _('This key is revoked')) 96 | # Check if is was not revoked 97 | elif state[0] == "expired": 98 | self.add_error('public_key', _('This key is expired')) 99 | return pub_key 100 | 101 | def clean_fingerprint(self): 102 | # Fingerprint provided must match with one provided 103 | pub_key = self.cleaned_data.get("public_key", "") 104 | fingerprint = self.cleaned_data.get("fingerprint", "") 105 | if fingerprint: 106 | fingerprint = fingerprint.replace(" ", "") 107 | if pub_key: 108 | key_fingerprint, *state = key_state(pub_key) 109 | if fingerprint != key_fingerprint: 110 | self.add_error('fingerprint', _('Fingerprint does not match')) 111 | return fingerprint 112 | 113 | def clean_keyserver_url(self): 114 | url = self.cleaned_data.get("keyserver_url", "") 115 | if url: 116 | try: 117 | res = requests.get(url) 118 | except: 119 | self.add_error("keyserver_url", 120 | _("Could not access the specified url")) 121 | return url 122 | begin = res.text.find("-----BEGIN PGP PUBLIC KEY BLOCK-----") 123 | end = res.text.find("-----END PGP PUBLIC KEY BLOCK-----") 124 | if 200 <= res.status_code < 300 and begin >= 0 and end > begin: 125 | self.pub_key = res.text[begin:end + 34] 126 | else: 127 | self.add_error("keyserver_url", 128 | _('This url does not have a pgp key')) 129 | return url 130 | 131 | 132 | class LoginForm(BaseLoginForm): 133 | def __init__(self, *args, **kwargs): 134 | super().__init__(*args, **kwargs) 135 | self.fields['login'].widget.attrs["placeholder"] = "" 136 | self.fields['password'].widget.attrs["placeholder"] = "" 137 | self.fields['password'].widget.attrs["autocomplete"] = "off" 138 | 139 | def user_credentials(self): 140 | credentials = super().user_credentials() 141 | credentials['login'] = credentials.get( 142 | 'email') or credentials.get('username') 143 | return credentials 144 | 145 | 146 | class SignupForm(BaseSignupForm): 147 | def __init__(self, *args, **kwargs): 148 | super().__init__(*args, **kwargs) 149 | self.fields['email'].widget.attrs["placeholder"] = "" 150 | self.fields['password1'].widget.attrs["placeholder"] = "" 151 | self.fields['password2'].widget.attrs["placeholder"] = "" 152 | self.fields['password1'].widget.attrs["autocomplete"] = "off" 153 | self.fields['password2'].widget.attrs["autocomplete"] = "off" 154 | -------------------------------------------------------------------------------- /humans/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.2 on 2016-02-27 23:21 3 | from __future__ import unicode_literals 4 | 5 | import django.contrib.auth.models 6 | import django.core.validators 7 | from django.db import migrations, models 8 | import django.utils.timezone 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | initial = True 14 | 15 | dependencies = [ 16 | ('auth', '0007_alter_validators_add_error_messages'), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='User', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('password', models.CharField(max_length=128, verbose_name='password')), 25 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 26 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 27 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=30, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.')], verbose_name='username')), 28 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 29 | ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')), 30 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 31 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 32 | ('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')), 33 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 34 | ('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')), 35 | ('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')), 36 | ], 37 | options={ 38 | 'verbose_name': 'user', 39 | 'verbose_name_plural': 'users', 40 | 'abstract': False, 41 | }, 42 | managers=[ 43 | ('objects', django.contrib.auth.models.UserManager()), 44 | ], 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /humans/migrations/0002_auto_20160310_2236.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.2 on 2016-03-10 22:36 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('humans', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='user', 17 | name='fingerprint', 18 | field=models.CharField(max_length=50, null=True), 19 | ), 20 | migrations.AddField( 21 | model_name='user', 22 | name='keyserver_url', 23 | field=models.URLField(blank=True, null=True), 24 | ), 25 | migrations.AddField( 26 | model_name='user', 27 | name='public_key', 28 | field=models.TextField(blank=True, null=True), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /humans/migrations/0003_user_organization.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.4 on 2016-03-30 12:32 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('humans', '0002_auto_20160310_2236'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='user', 17 | name='organization', 18 | field=models.CharField(blank=True, max_length=80, null=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /humans/migrations/0004_auto_20160330_1430.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.4 on 2016-03-30 14:30 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('humans', '0003_user_organization'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='user', 17 | name='fingerprint', 18 | field=models.CharField(blank=True, max_length=50, null=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /humans/migrations/0005_user_timezone.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.4 on 2016-04-29 17:48 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | import timezone_field.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('humans', '0004_auto_20160330_1430'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='user', 18 | name='timezone', 19 | field=timezone_field.fields.TimeZoneField(default='UTC'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /humans/migrations/0006_user_server_signed.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.6 on 2016-06-03 19:11 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('humans', '0005_user_timezone'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='user', 17 | name='server_signed', 18 | field=models.BooleanField(default=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /humans/migrations/0007_notification.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.7 on 2016-08-27 15:52 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('auth', '0007_alter_validators_add_error_messages'), 13 | ('humans', '0006_user_server_signed'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Notification', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('subject', models.CharField(max_length=150)), 22 | ('body', models.TextField()), 23 | ('created_at', models.DateTimeField(auto_now_add=True)), 24 | ('updated_at', models.DateTimeField(auto_now=True)), 25 | ('sent_at', models.DateTimeField(null=True)), 26 | ('send_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='auth.Group')), 27 | ], 28 | options={ 29 | 'verbose_name': 'Notification', 30 | 'verbose_name_plural': 'Notifications', 31 | }, 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /humans/migrations/0008_auto_20160903_1850.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-03 18:50 3 | from __future__ import unicode_literals 4 | 5 | import django.contrib.auth.validators 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('humans', '0007_notification'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='user', 18 | name='username', 19 | field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /humans/migrations/0009_auto_20170519_1431.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.7 on 2017-05-19 14:31 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | import timezone_field.fields 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('humans', '0008_auto_20160903_1850'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name='user', 19 | name='language', 20 | field=models.CharField(default='en-us', max_length=5, verbose_name='Prefered language'), 21 | ), 22 | migrations.AlterField( 23 | model_name='notification', 24 | name='body', 25 | field=models.TextField(verbose_name='Body'), 26 | ), 27 | migrations.AlterField( 28 | model_name='notification', 29 | name='created_at', 30 | field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), 31 | ), 32 | migrations.AlterField( 33 | model_name='notification', 34 | name='send_to', 35 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='auth.Group', verbose_name='Send to'), 36 | ), 37 | migrations.AlterField( 38 | model_name='notification', 39 | name='sent_at', 40 | field=models.DateTimeField(null=True, verbose_name='Sent at'), 41 | ), 42 | migrations.AlterField( 43 | model_name='notification', 44 | name='subject', 45 | field=models.CharField(max_length=150, verbose_name='Subject'), 46 | ), 47 | migrations.AlterField( 48 | model_name='notification', 49 | name='updated_at', 50 | field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), 51 | ), 52 | migrations.AlterField( 53 | model_name='user', 54 | name='fingerprint', 55 | field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Fingerprint'), 56 | ), 57 | migrations.AlterField( 58 | model_name='user', 59 | name='keyserver_url', 60 | field=models.URLField(blank=True, null=True, verbose_name='Key server URL'), 61 | ), 62 | migrations.AlterField( 63 | model_name='user', 64 | name='organization', 65 | field=models.CharField(blank=True, max_length=80, null=True, verbose_name='Organization'), 66 | ), 67 | migrations.AlterField( 68 | model_name='user', 69 | name='public_key', 70 | field=models.TextField(blank=True, null=True, verbose_name='Public key'), 71 | ), 72 | migrations.AlterField( 73 | model_name='user', 74 | name='server_signed', 75 | field=models.BooleanField(default=False, verbose_name='Server signed'), 76 | ), 77 | migrations.AlterField( 78 | model_name='user', 79 | name='timezone', 80 | field=timezone_field.fields.TimeZoneField(default='UTC', verbose_name='Timezone'), 81 | ), 82 | ] 83 | -------------------------------------------------------------------------------- /humans/migrations/0010_auto_20170526_1326.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.7 on 2017-05-26 13:26 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('humans', '0009_auto_20170519_1431'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='user', 17 | name='language', 18 | field=models.CharField(choices=[('en-us', 'English'), ('pt-pt', 'Portuguese')], default='en-us', max_length=16, verbose_name='Language'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /humans/migrations/0011_keychangerecord.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-03-23 16:30 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('humans', '0010_auto_20170526_1326'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='KeyChangeRecord', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('prev_fingerprint', models.CharField(blank=True, max_length=50, null=True, verbose_name='Previous Fingerprint')), 22 | ('to_fingerprint', models.CharField(blank=True, max_length=50, null=True, verbose_name='To Fingerprint')), 23 | ('ip_address', models.GenericIPAddressField(blank=True, null=True)), 24 | ('agent', models.TextField(blank=True)), 25 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), 26 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='keychanges', to=settings.AUTH_USER_MODEL, verbose_name='User')), 27 | ], 28 | options={ 29 | 'verbose_name': 'KeyChangeRecord', 30 | 'verbose_name_plural': 'KeyChangeRecords', 31 | }, 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /humans/migrations/0012_remove_user_server_signed.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.13 on 2018-10-29 15:17 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('humans', '0011_keychangerecord'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='user', 17 | name='server_signed', 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /humans/migrations/0013_auto_20201204_1807.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-12-04 18:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('humans', '0012_remove_user_server_signed'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='user', 15 | name='last_name', 16 | field=models.CharField(blank=True, max_length=150, verbose_name='last name'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /humans/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/humans/migrations/__init__.py -------------------------------------------------------------------------------- /humans/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import AbstractUser, Group 3 | from django.utils.translation import gettext_lazy as _ 4 | from timezone_field import TimeZoneField 5 | from django.db import transaction 6 | 7 | 8 | class User(AbstractUser): 9 | """ 10 | Project's base user model 11 | """ 12 | LANGUAGE_CHOICES = ( 13 | ('en-us', 'English'), 14 | ('pt-pt', _('Portuguese')), 15 | ) 16 | 17 | organization = models.CharField( 18 | null=True, blank=True, max_length=80, verbose_name=_('Organization')) 19 | public_key = models.TextField( 20 | blank=True, null=True, verbose_name=_('Public key')) 21 | fingerprint = models.CharField( 22 | null=True, blank=True, max_length=50, verbose_name=_('Fingerprint')) 23 | keyserver_url = models.URLField( 24 | null=True, blank=True, verbose_name=_('Key server URL')) 25 | timezone = TimeZoneField(default='UTC', verbose_name=_('Timezone')) 26 | language = models.CharField( 27 | default="en-us", max_length=16, choices=LANGUAGE_CHOICES, verbose_name=_('Language')) 28 | 29 | def __init__(self, *args, **kwargs): 30 | super().__init__(*args, **kwargs) 31 | self.base_fingerprint = self.fingerprint 32 | 33 | def save(self, *args, **kwargs): 34 | ip = kwargs.pop('ip', None) 35 | agent = kwargs.pop('agent', '') 36 | with transaction.atomic(): 37 | super().save(*args, **kwargs) 38 | if self.base_fingerprint != self.fingerprint: 39 | self.keychanges.create(user=self, 40 | prev_fingerprint=self.base_fingerprint, 41 | to_fingerprint=self.fingerprint, 42 | ip_address=ip, 43 | agent=agent) 44 | self.base_fingerprint = self.fingerprint 45 | 46 | def has_setup_complete(self): 47 | if self.public_key and self.fingerprint: 48 | return True 49 | return False 50 | 51 | @property 52 | def has_github_login(self): 53 | return self.socialaccount_set.filter(provider='github').count() >= 1 54 | 55 | @property 56 | def has_public_key(self): 57 | return True if self.public_key else False 58 | 59 | @property 60 | def has_keyserver_url(self): 61 | return True if self.keyserver_url else False 62 | 63 | 64 | class Notification(models.Model): 65 | """ These notifications are emails sent to all users (or some subset) 66 | by an Administrator. Just once. 67 | """ 68 | 69 | subject = models.CharField( 70 | null=False, blank=False, max_length=150, verbose_name=_('Subject')) 71 | body = models.TextField(null=False, blank=False, verbose_name=_('Body')) 72 | 73 | created_at = models.DateTimeField( 74 | auto_now_add=True, verbose_name=_('Created at')) 75 | updated_at = models.DateTimeField( 76 | auto_now=True, verbose_name=_('Updated at')) 77 | 78 | sent_at = models.DateTimeField(null=True, verbose_name=_('Sent at')) 79 | send_to = models.ForeignKey( 80 | Group, null=True, blank=True, on_delete=models.CASCADE, verbose_name=_('Send to')) 81 | 82 | class Meta: 83 | verbose_name = _('Notification') 84 | verbose_name_plural = _('Notifications') 85 | 86 | def __str__(self): 87 | return self.subject 88 | 89 | def delete(self): 90 | return super().delete() if not self.sent_at else False 91 | 92 | 93 | class KeyChangeRecord(models.Model): 94 | """ Records the information about the change of a key by the user. 95 | This allows the user to be aware of any suspicious activity 96 | """ 97 | user = models.ForeignKey(User, 98 | on_delete=models.CASCADE, 99 | related_name='keychanges', 100 | verbose_name=_('User')) 101 | prev_fingerprint = models.CharField(null=True, 102 | blank=True, 103 | max_length=50, 104 | verbose_name=_('Previous Fingerprint')) 105 | to_fingerprint = models.CharField(null=True, 106 | blank=True, 107 | max_length=50, 108 | verbose_name=_('To Fingerprint')) 109 | ip_address = models.GenericIPAddressField(blank=True, null=True) 110 | agent = models.TextField(blank=True) 111 | created_at = models.DateTimeField(auto_now_add=True, 112 | verbose_name=_('Created at')) 113 | 114 | class Meta: 115 | verbose_name = _('KeyChangeRecord') 116 | verbose_name_plural = _('KeyChangeRecords') 117 | -------------------------------------------------------------------------------- /humans/static/javascripts/update_user_form.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | $(".form__block label").addClass("smallmedium text-darkest"); 3 | $(".form__block input[type=text]").addClass("text padding-settings"); 4 | $(".form__block textarea").addClass("xsmalltext"); 5 | $(".label-form-b label").addClass("smallmedium text-darkest"); 6 | $(".checkbox- label").addClass("smallmedium text-darkest"); 7 | 8 | $(".sett__nav__item").click(function(evt) { 9 | var tabId = evt.target.id; 10 | var index = $(evt.target).index() + 1; 11 | 12 | $(".sett__nav__item").removeClass("active"); 13 | $(tabId).addClass("active"); 14 | $(".section").hide(); 15 | $("#section" + index).show(); 16 | }); 17 | 18 | $('.radio_button').on('change', function() { 19 | if ($('.radio_button:checked').val() == "keyserver") { 20 | $("#keyserver_url").show(); 21 | $("#public_key").hide(); 22 | } else { 23 | $("#keyserver_url").hide(); 24 | $("#public_key").show(); 25 | } 26 | }); 27 | if($("#id_keyserver_url").val().length != 0) { 28 | $("#keyserver_option").prop("checked", true); 29 | $("#keyserver_url").show(); 30 | $("#public_key").hide(); 31 | } 32 | $("#form").submit(function(event){ 33 | if ($('.radio_button:checked').val() == "keyserver") { 34 | $("#id_public_key").val(''); 35 | $("#id_public_key").text(''); 36 | } else { 37 | $("#id_keyserver_url").val(''); 38 | $("#id_keyserver_url").text(''); 39 | } 40 | }); 41 | $(".errorlist").each(function(){ 42 | if ($("li",this).length >= 1) { 43 | index = $(this).closest(".section").index(); 44 | if(index == 0) { 45 | $("#tab1").click(); 46 | } else if(index == 1) { 47 | $("#tab2").click(); 48 | } else { 49 | $("#tab3").click(); 50 | } 51 | return; 52 | } 53 | }); 54 | }); 55 | 56 | -------------------------------------------------------------------------------- /humans/tasks.py: -------------------------------------------------------------------------------- 1 | from celery import shared_task 2 | from celery.utils.log import get_task_logger 3 | from django.core.mail import EmailMultiAlternatives 4 | from django.conf import settings 5 | from django.db.models import Q 6 | from django.template.loader import render_to_string 7 | from django.utils.translation import gettext_lazy as _ 8 | from .models import User, Notification 9 | from .utils import key_state 10 | import requests 11 | 12 | logger = get_task_logger(__name__) 13 | 14 | def fetch_key(url): 15 | res = requests.get(url) 16 | begin = res.text.find("-----BEGIN PGP PUBLIC KEY BLOCK-----") 17 | end = res.text.find("-----END PGP PUBLIC KEY BLOCK-----") 18 | if 200 <= res.status_code < 300 and begin >= 0 and end > begin: 19 | return res.text[begin:end + 34] 20 | else: 21 | raise ValueError(_('The Url provided does not contain a public key')) 22 | 23 | 24 | def send_email(user, subject, template): 25 | content = render_to_string(template, context={"user": user}) 26 | email = EmailMultiAlternatives(subject, content, 27 | settings.DEFAULT_FROM_EMAIL, 28 | [user.email]) 29 | email.send() 30 | 31 | 32 | @shared_task(ignore_result=True) 33 | def update_public_keys(): 34 | users = User.objects.exclude( 35 | Q(keyserver_url__isnull=True) | Q(keyserver_url__exact='')) 36 | logger.info(_('Start updating user keys')) 37 | for user in users: 38 | logger.info(_('Working on user: {}').format(user.email)) 39 | logger.info(_('URL: {}').format(user.keyserver_url)) 40 | try: 41 | key = fetch_key(user.keyserver_url) 42 | except: 43 | logger.error(_('Unable to fetch new key')) 44 | continue 45 | 46 | # Check key 47 | fingerprint, *state = key_state(key) 48 | 49 | if state[0] in ["expired", "revoked"]: 50 | # Email user and disable/remove key 51 | send_email(user, _('Hawkpost: {} key').format(state[0]), 52 | "humans/emails/key_{}.txt".format(state[0])) 53 | user.fingerprint = "" 54 | user.public_key = "" 55 | user.keyserver_url = "" 56 | user.save() 57 | elif state[0] == "invalid": 58 | # Alert the user and remove keyserver_url 59 | send_email(user, 60 | _('Hawkpost: Keyserver Url providing an invalid key'), 61 | "humans/emails/key_invalid.txt") 62 | user.keyserver_url = "" 63 | user.save() 64 | elif fingerprint != user.fingerprint: 65 | # Email user and remove the keyserver url 66 | send_email(user, _('Hawkpost: Fingerprint mismatch'), 67 | "humans/emails/fingerprint_changed.txt") 68 | user.keyserver_url = "" 69 | user.save() 70 | elif state[0] == "valid": 71 | user.public_key = key 72 | user.save() 73 | 74 | logger.info(_('Finished Updating user keys')) 75 | 76 | 77 | @shared_task(ignore_result=True) 78 | def validate_public_keys(): 79 | users = User.objects.exclude( 80 | Q(public_key__isnull=True) | Q(public_key__exact='')) 81 | logger.info(_('Start validating user keys')) 82 | for user in users: 83 | logger.info(_('Working on user: {}').format(user.email)) 84 | key = user.public_key 85 | # Check key 86 | fingerprint, *state = key_state(key) 87 | 88 | if state[0] == "expired": 89 | # Email user and disable/remove key 90 | send_email(user, _('Hawkpost: {} key').format(state[0]), 91 | "humans/emails/key_{}.txt".format(state[0])) 92 | user.fingerprint = "" 93 | user.public_key = "" 94 | user.save() 95 | 96 | elif state[0] == "valid": 97 | # Checks if key is about to expire 98 | days_to_expire = state[1] 99 | if days_to_expire == 7 or days_to_expire == 1: 100 | # Warns user if key about to expire 101 | send_email(user, 102 | _('Hawkpost: Key will expire in {} day(s)').format(days_to_expire), 103 | "humans/emails/key_will_expire.txt") 104 | 105 | 106 | @shared_task 107 | def send_email_notification(subject, body, email): 108 | email = EmailMultiAlternatives(subject, body, 109 | settings.DEFAULT_FROM_EMAIL, 110 | [email]) 111 | email.send() 112 | 113 | 114 | @shared_task 115 | def enqueue_email_notifications(notification_id, group_id): 116 | notification = Notification.objects.get(id=notification_id) 117 | if group_id: 118 | users = User.objects.filter(groups__id=group_id) 119 | else: 120 | users = User.objects.all() 121 | 122 | for user in users: 123 | send_email_notification.delay(notification.subject, 124 | notification.body, 125 | user.email) 126 | -------------------------------------------------------------------------------- /humans/templates/humans/emails/fingerprint_changed.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% trans "Hello" %} {% if user.first_name %} {{user.first_name}} {{user.last_name}} {% else %} {{user.username}} {% endif %}, 4 | {% blocktrans %} 5 | During our automatic updates and checks, using the keyserver url that you provided, the system spotted that the key present on that URL, no longer has the same fingerprint that the one you provided on your settings. 6 | 7 | To avoid using the wrong key, we removed the provided URL and will continue to use the key we have stored (all open boxes will remmain active). 8 | 9 | To enable the automatic verifications again, go to the settings and update your keyserver url. 10 | 11 | best regards, 12 | Hawkpost Team 13 | {% endblocktrans %} 14 | -------------------------------------------------------------------------------- /humans/templates/humans/emails/key_expired.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% trans "Hello" %} {% if user.first_name %} {{user.first_name}} {{user.last_name}} {% else %} {{user.username}} {% endif %}, 4 | {% blocktrans %} 5 | During our automatic updates and checks, using the keyserver url that you provided, the system spotted that your current key is already expired. To avoid any issues, the key was removed and all open boxes deactivated until a new one is added. 6 | 7 | To enable all functionality again, please go to the settings and add your new public key. 8 | 9 | best regards, 10 | Hawkpost Team 11 | {% endblocktrans %} 12 | -------------------------------------------------------------------------------- /humans/templates/humans/emails/key_invalid.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% trans "Hello" %} {% if user.first_name %} {{user.first_name}} {{user.last_name}} {% else %} {{user.username}} {% endif %}, 4 | {% blocktrans %} 5 | During our automatic updates and checks, using the keyserver url that you provided, the system spotted that the key that is currently present on that URL is invalid (probably due a change in the document). 6 | 7 | To avoid using the wrong key, we removed the provided URL and will continue to use the key that we have stored (all open boxes will remmain active). 8 | 9 | To enable the automatic verifications again, go to the settings and update your keyserver url. 10 | 11 | best regards, 12 | Hawkpost Team 13 | {% endblocktrans %} 14 | -------------------------------------------------------------------------------- /humans/templates/humans/emails/key_revoked.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% trans "Hello" %} {% if user.first_name %} {{user.first_name}} {{user.last_name}} {% else %} {{user.username}} {% endif %}, 4 | {% blocktrans %} 5 | During our automatic updates and checks, using the keyserver url that you provided, the system spotted that your current key was revoked. To avoid any issue or compromise, the key was removed and all open boxes deactivated until a new one is added. 6 | 7 | To enable all functionality again, please go to the settings and add your new public key. 8 | 9 | best regards, 10 | Hawkpost Team 11 | {% endblocktrans %} 12 | -------------------------------------------------------------------------------- /humans/templates/humans/emails/key_will_expire.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% trans "Hello" %} {% if user.first_name %} {{user.first_name}} {{user.last_name}} {% else %} {{user.username}} {% endif %}, 4 | {% blocktrans %} 5 | During our automatic updates and checks, using the key that you provided, the system spotted that the key you provided will soon expire. 6 | 7 | To avoid any issues, the key will be removed from Hawkpost when it expires and all open boxes deactivated until a new one is added. As such, please make sure to update your key as soon as possible. 8 | 9 | Best Regards, 10 | {% endblocktrans %} 11 | -------------------------------------------------------------------------------- /humans/templates/humans/key_change_modal.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 37 |
38 | -------------------------------------------------------------------------------- /humans/templates/humans/update_user_form.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | 5 | {% block content %} 6 |
7 |
8 |

{% trans "UPDATE YOUR SETTINGS" %}

9 |
10 |
    11 | 18 |
19 | 20 |
21 |
22 | {% csrf_token %} 23 |
{{ form.non_field_errors }} 24 |
25 |
26 |
    27 |
  • 28 |
    29 |
    30 | {{ form.first_name.errors }} {{ form.first_name.label_tag }} {{ form.first_name }} 31 |
    32 |
    33 | {{ form.last_name.errors }} {{ form.last_name.label_tag }} {{ form.last_name }} 34 |
    35 |
    36 | {{ form.organization.errors }} {{ form.organization.label_tag }} {{ form.organization }} 37 |
    38 |
    39 | {{ form.timezone.errors }} {{ form.timezone.label_tag }} {{ form.timezone }} 40 |
    41 |
    42 | {{ form.language.errors }} {{ form.language.label_tag }} {{ form.language }} 43 |
    44 |
    45 |
  • 46 |
  • 47 |
    48 |
    49 | 50 | {% blocktrans %}You can provide the public key in a static away or provide a link to your key in a public key server so we can fetch it for you (recommended).{% endblocktrans %} 51 | 52 |
    53 | 54 | {% trans "To verify the log of the last changes to your key please click" %} {% trans "here" %}. 55 | 56 |
    57 |
    58 |
    {{ form.public_key.label_tag }}
    59 |
    {{ form.keyserver_url.label_tag }} 60 |
    61 |
    62 |
    63 | {{ form.keyserver_url.errors }} {{ form.keyserver_url }} 64 |
    65 |
    66 | {{ form.public_key.errors }} {{ form.public_key }} 67 |
    68 |
    69 | {{ form.fingerprint.errors }} {{ form.fingerprint.label_tag }} {{ form.fingerprint }} 70 |
    71 |
    72 |
  • 73 |
  • 74 |
    75 |
    76 | {{ form.current_password.errors }} {{ form.current_password.label_tag }} {{ form.current_password }} 77 |
    78 |
    79 | {{ form.new_password2.errors }} {{ form.new_password1.label_tag }} {{ form.new_password1 }} 80 |
    81 |
    82 | {{ form.new_password2.errors }} {{ form.new_password2.label_tag }} {{ form.new_password2 }} 83 |
    84 |
    85 |
  • 86 |
87 |
88 |
89 | 90 |
91 |
92 |
93 |
94 |
95 | 96 | {% trans "Do you wish to delete your account? if yes, just click" %} {% trans "here" %} 97 | 98 |
99 |
100 |
101 |
102 |
103 | {% include "humans/key_change_modal.html" %} 104 | {% endblock content %} 105 | 106 | {% block scripts %} 107 | {{ block.super }} 108 | 109 | {% endblock scripts %} 110 | -------------------------------------------------------------------------------- /humans/templates/humans/user_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |
9 |
10 |

{% trans "DELETE ACCOUNT" %}

11 |

12 | {% blocktrans %}Your account in Hawkpost is about to be deleted along with all its contents and settings. Are you sure you want to continue?{% endblocktrans %} 13 |

14 |
15 |
16 |
17 |
18 |
19 | {% csrf_token %} 20 | {% if request.user.has_usable_password %} 21 |
22 | 23 | 24 |
25 |
26 | {% endif %} 27 |
28 | 29 |
30 | 33 |
34 |
35 |
36 |
37 |
38 | 39 | {% endblock content %} 40 | -------------------------------------------------------------------------------- /humans/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import UpdateSettingsView, DeleteUserView 3 | 4 | urlpatterns = [ 5 | path('settings', UpdateSettingsView.as_view(), name="humans_update"), 6 | path('delete', DeleteUserView.as_view(), name="humans_delete"), 7 | ] 8 | -------------------------------------------------------------------------------- /humans/utils.py: -------------------------------------------------------------------------------- 1 | from django.utils import timezone 2 | from datetime import datetime 3 | from functools import wraps 4 | from shutil import rmtree 5 | import gnupg 6 | import tempfile 7 | 8 | 9 | def with_gpg_obj(func): 10 | @wraps(func) 11 | def inner(key): 12 | # create temp gpg keyring 13 | temp_dir = tempfile.mkdtemp() 14 | gpg_obj = gnupg.GPG(homedir=temp_dir, 15 | keyring="pub.gpg", 16 | secring="sec.gpg") 17 | gpg_obj.encoding = 'utf-8' 18 | ret = func(key, gpg_obj) 19 | # remove the keyring 20 | rmtree(temp_dir, ignore_errors=True) 21 | 22 | return ret 23 | return inner 24 | 25 | 26 | @with_gpg_obj 27 | def key_state(key, gpg): 28 | INVALID = (None, "invalid", -1) 29 | if not key: 30 | return INVALID 31 | results = gpg.import_keys(key).results 32 | if not results: 33 | return INVALID 34 | # Key data is present in the last element of the list 35 | key_fingerprint = results[-1].get("fingerprint") 36 | if not key_fingerprint: 37 | return INVALID 38 | 39 | # Since the keyring is exclusive for this import 40 | # only the imported key exists in it. 41 | key = gpg.list_keys()[0] 42 | exp_timestamp = int(key["expires"]) if key["expires"] else 0 43 | expires = datetime.fromtimestamp(exp_timestamp, timezone.utc) 44 | to_expire = expires - timezone.now() 45 | days_to_expire = to_expire.days 46 | 47 | if key["trust"] == "r": 48 | state = "revoked" 49 | elif exp_timestamp and expires < timezone.now(): 50 | state = "expired" 51 | else: 52 | state = "valid" 53 | 54 | return key_fingerprint, state, days_to_expire 55 | 56 | 57 | def request_ip_address(request): 58 | """Takes a Request Object and returns the caller IP address""" 59 | x_forward_for = request.headers.get('x-forwarded-for', None) 60 | return x_forward_for if x_forward_for else request.META.get('REMOTE_ADDR') 61 | -------------------------------------------------------------------------------- /humans/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.mixins import LoginRequiredMixin as LoginRequired 2 | from django.contrib.auth import logout 3 | from django.contrib.auth import update_session_auth_hash 4 | from django.views.generic import FormView, DeleteView 5 | from django.urls import reverse_lazy 6 | from django.utils.translation import gettext_lazy as _ 7 | from django.contrib import messages 8 | from .forms import UpdateUserInfoForm, LoginForm, SignupForm 9 | from .models import User 10 | from .utils import request_ip_address 11 | 12 | 13 | class LoginRequiredMixin(LoginRequired): 14 | login_url = reverse_lazy("account_login") 15 | redirect_field_name = 'next' 16 | 17 | 18 | class AuthMixin(): 19 | def get_context_data(self, **kwargs): 20 | context = super().get_context_data(**kwargs) 21 | if not self.request.user.is_authenticated: 22 | context["login_form"] = LoginForm() 23 | context["signup_form"] = SignupForm() 24 | return context 25 | 26 | 27 | class UpdateSettingsView(LoginRequiredMixin, FormView): 28 | """Lets the user update his setttings""" 29 | template_name = "humans/update_user_form.html" 30 | form_class = UpdateUserInfoForm 31 | success_url = reverse_lazy("humans_update") 32 | 33 | def get(self, request, *args, **kwargs): 34 | if request.GET.get('setup', None): 35 | msg = _('To start using hawkpost, you must add a valid public key') 36 | messages.error(request, msg) 37 | return super().get(request, *args, **kwargs) 38 | 39 | def get_context_data(self, **kwargs): 40 | user = self.request.user 41 | context = super().get_context_data(**kwargs) 42 | context["key_changes"] = user.keychanges.order_by('-created_at')[:20] 43 | return context 44 | 45 | def get_form_kwargs(self): 46 | kwargs = super().get_form_kwargs() 47 | kwargs["instance"] = self.request.user 48 | return kwargs 49 | 50 | def form_valid(self, form): 51 | ip = request_ip_address(self.request) 52 | agent = self.request.headers.get('user-agent') 53 | form.save(ip=ip, agent=agent) 54 | if form.change_password: 55 | update_session_auth_hash(self.request, form.instance) 56 | messages.success(self.request, _('Settings successfully updated')) 57 | return super().form_valid(form) 58 | 59 | def form_invalid(self, form): 60 | messages.error(self.request, _('Please check the invalid fields')) 61 | return super().form_invalid(form) 62 | 63 | 64 | class DeleteUserView(LoginRequiredMixin, DeleteView): 65 | model = User 66 | success_url = reverse_lazy("pages_index") 67 | 68 | def get_object(self, queryset=None): 69 | return self.request.user 70 | 71 | def form_valid(self, form): 72 | current_pw = self.request.POST.get("current_password", "") 73 | user = self.request.user 74 | if not user.has_usable_password() or user.check_password(current_pw): 75 | response = super().form_valid(form) 76 | logout(self.request) 77 | messages.success(self.request, 78 | _('Account deleted successfully.' 79 | ' We hope you comeback soon.')) 80 | return response 81 | else: 82 | messages.error(self.request, 83 | _('In order to delete the account you must provide' 84 | ' the current password.')) 85 | return self.get(self.request) 86 | -------------------------------------------------------------------------------- /locale/pt_PT/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/locale/pt_PT/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | import dotenv 5 | 6 | if __name__ == "__main__": 7 | dotenv.read_dotenv() 8 | environment = os.environ.get("HAWKPOST_ENV", "development") 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", 10 | "hawkpost.settings.{}".format(environment)) 11 | 12 | from django.core.management import execute_from_command_line 13 | 14 | execute_from_command_line(sys.argv) 15 | -------------------------------------------------------------------------------- /pages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/pages/__init__.py -------------------------------------------------------------------------------- /pages/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /pages/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PagesConfig(AppConfig): 5 | name = 'pages' 6 | -------------------------------------------------------------------------------- /pages/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitesmith/hawkpost/1a6d064a7def9bc6081746361ba01c50561f0feb/pages/migrations/__init__.py -------------------------------------------------------------------------------- /pages/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /pages/static/javascripts/authform.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | $("#login-form-js").on("submit", function(){ 3 | var $this = $(this); 4 | $.ajax({ 5 | url: $this.attr("action"), 6 | method: $this.attr("method"), 7 | data: $this.serialize() 8 | }).done(function(data){ 9 | document.location = data.location; 10 | }).fail(function(data){ 11 | var errorContainer = $("#login-form-errors-js"); 12 | errorContainer.html(""); 13 | if (data.status === 400){ 14 | var errors = data.responseJSON.form.errors; 15 | var form_fields = data.responseJSON.form.fields; 16 | if(form_fields.login.errors){ 17 | errors = errors.concat(form_fields.login.errors); 18 | } 19 | if(form_fields.password.errors){ 20 | errors = errors.concat(form_fields.password.errors); 21 | } 22 | for(var i=0;i" + errors[i] + "

"); 24 | } 25 | } else if (data.status === 403){ 26 | var msg = "Too many failed attempts. The account is locked for 1 hour. Please try again later."; 27 | errorContainer.append("

"+ msg +"

"); 28 | } 29 | }); 30 | return false; 31 | }); 32 | 33 | $("#signup-form-js").on("submit", function(){ 34 | var $this = $(this); 35 | $.ajax({ 36 | url: $this.attr("action"), 37 | method: $this.attr("method"), 38 | data: $this.serialize() 39 | }).done(function(data){ 40 | document.location = data.location; 41 | }).fail(function(data){ 42 | var errorContainer = $("#signup-form-errors-js"); 43 | errorContainer.html(""); 44 | var errors = data.responseJSON.form.errors; 45 | var form_fields = data.responseJSON.form.fields; 46 | if(form_fields.email.errors){ 47 | errors = errors.concat(form_fields.email.errors); 48 | } 49 | if(form_fields.password1.errors){ 50 | errors = errors.concat(form_fields.password1.errors); 51 | } 52 | if(form_fields.password2.errors){ 53 | errors = errors.concat(form_fields.password2.errors); 54 | } 55 | for(var i=0;i" + errors[i] + "

"); 57 | } 58 | }); 59 | return false; 60 | }); 61 | 62 | if(window.location.href.indexOf('#modal-login') != -1) { 63 | $(".md-show").css('visibility', 'visible'); 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /pages/templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |
9 |
10 |

{% trans "Forbidden" %}

11 |
12 |

13 | {% blocktrans %}You are not allowed to access this page.{% endblocktrans %} 14 |

15 |

16 | {% blocktrans %}You can click here to go to Hawkpost's main page.{% endblocktrans %} 17 |

18 |
19 |
20 |
21 |
22 |
23 |
24 | {% endblock content %} 25 | -------------------------------------------------------------------------------- /pages/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |
9 |
10 |

{% trans "NOT FOUND" %}

11 |
12 |

13 | {% blocktrans %}The page you are looking for, does not exist. Please check the URL to check you entered it correctly.{% endblocktrans %} 14 |

15 |

16 | {% blocktrans %}Otherwise you can click here to go to Hawkpost's main page.{% endblocktrans %} 17 |

18 |
19 |
20 |
21 |
22 |
23 |
24 | {% endblock content %} 25 | -------------------------------------------------------------------------------- /pages/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |
9 |
10 |

{% trans "Server Error" %}

11 |
12 |

13 | {% blocktrans %}Somewhere, somehow something went wrong and we weren't able to handle your request.{% endblocktrans %} 14 |

15 |

16 | {% blocktrans %}An administrator was notified of the occurrence and will try fix the problem. Please try again later.{% endblocktrans %} 17 |

18 |
19 |
20 |
21 |
22 |
23 |
24 | {% endblock content %} 25 | -------------------------------------------------------------------------------- /pages/templates/pages/about.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 | 6 |
7 |
8 |
9 |
10 |
11 |

{% trans "INSTANCE" %}

12 |
13 |

{% trans "This website is an instance of the Hawkpost software project (see more details about it below). For your information here are a few details about the people responsible for this particular website:" %}

14 |

{% trans "Main Administrator" %}: {{ admin_name }}

15 |

{% trans "Contact" %}: {{ admin_email }}

16 | {% if description %} 17 |

{% trans "Instance description" %}: {{ description }}

18 | {% endif %} 19 |

{% trans "Version" %}: {{ version }}

20 |
21 |
22 |
23 |
24 |
25 |

{% trans "HAWKPOST" %}

26 |
27 |

{% trans "For many years pigeons were used to send private messages to people. But today, to send private and confidential information a pigeon is not enough. We upgraded the pigeon for a hawk - it's stronger, faster and will protect all of your messages better." %}

28 |

{% trans "We know that sometimes it's difficult to explain how to encrypt messages or how to install that software to protect confidential information. That's why we've created Hawkpost - a place where people can send you sensible information in an easy and secure way." %}

29 |

{% trans "This project was made at Whitesmith's Hackathon during our company retreat. The aim of this hackathon was to launch a side-project in 24h." %}

30 |

{% blocktrans %}You can read more about the project in our FAQ at the bottom of our landing page.{% endblocktrans %}

31 |
32 |
33 |
34 |
35 |
36 |
37 |

{% trans "WHITESMITH" %}

38 |

{% blocktrans %}Whitesmith is a web and mobile product development studio founded in 2013 and based in Coimbra (Portugal) and London. From Service Oriented Architecture and Internet of Things to Web & Mobile apps, we converge into solving real problems with great user experience. We are more than simple designers & developers - we are an empowered team of problem solvers.{% endblocktrans %}

39 |
40 |
41 |
42 |
43 |
44 |
45 | {% endblock content %} 46 | -------------------------------------------------------------------------------- /pages/templates/pages/help.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.html" %} 2 | {% load i18n %} 3 | {% load static %} 4 | 5 | {% block content %} 6 |
7 | 24 | 25 |
26 |
27 |
28 |
29 |

{% trans "In this page we try to explain common procedures and features that can be complex and hard to grasp for newcomers." %}

30 |

{% trans "Some are related to Hawkpost directly, but others are related to PGP and associated tasks and tools." %}

31 |

{% trans "Of course we can't cover all the surface or explicitly address all questions in this document. So if your question isn't here feel free to drop a line through our support channels, and we'll try to help you as soon as possible." %}

32 | 38 |
39 |
40 |

{% trans "HOW TO SETUP HAWKPOST" %}

41 |
42 |
43 |

{% trans "In order to start using Hawkpost as a user that receives confidential information, you have to fill certain requirements, such as:" %}

44 |
    45 |
  • {% trans "Provide a PGP public key" %}
  • 46 |
  • {% trans "Provide your key's fingerprint" %}
  • 47 |
  • {% trans "Have the required software to decrypt the messages" %}
  • 48 |
49 | 50 |

{% blocktrans %}If you don't know how to fulfill the first and third point, please check the next section. Otherwise the configuration is quite simple.{% endblocktrans %}

51 |
52 | 53 |
54 | 55 |
56 |
57 |
58 | 59 |
60 |
61 |

{% trans "As you can see in the above figure, you just have to fill the public key field and the matching fingerprint. After this you're ready to go. We strongly recommend to also fill your personal data (as name) so it can show up on the submission page." %}

62 | 63 |

{% trans "To find your key's fingerprint, you can run the following command if you're using Gnu Privacy Guard (GPG):" %}

64 | 65 |
gpg --fingerprint <key id or email>
66 | 67 |

{% trans "If you're using a graphical tool, you can generally find this info on the key properties section of your tool." %}

68 |
69 |
70 | 71 |
72 |
73 |

{% trans "For extra security and features you can specify the keyserver URL of your key instead of providing it directly. This way the system will fetch the key and regularly check that URL for key updates and the system will stop allowing submissions to your boxes once the key is revoked or expired (the fingerprint is always required)." %}

74 |
75 |
76 | 77 | 78 |
79 |

{% trans "HOW TO GENERATE A PGP KEY" %}

80 | 81 |

{% blocktrans %}Hawkpost works with an encryption technology known as PGP. So to create boxes you'll need to setup a PGP key pair and provide us the public part/key. If you already have one, this section is not for you.{% endblocktrans %}

82 | 83 |

{% trans "Since Hawkpost works only through email, the easiest way to setup a key pair is using your email client, or through a compatible plugin/extension in case the email client doesn't support PGP natively. There are various articles in the Internet about this topic, so it's easy to find some that match your specific setup." %}

84 | 85 |

{% trans "As an example, in the next paragraphs we point to some tutorials addressing popular email clients." %}

86 | 87 |

{% blocktrans %}For webmail users, one tool that is very useful is Mailvelope. This browser extension allows you to easily open the encrypted messages, and also to generate your keys. An introductory tutorial can be found here.{% endblocktrans %}

88 | 89 |

{% blocktrans %}For Thunderbird users, the common practice is to install a plugin called Enigmail, which like Mailvelope, will allow you to decrypt Hawkpost messages, and manage your keys as well. An introduction can be found here.{% endblocktrans %}

90 | 91 |

{% blocktrans %}Check this video for an Outlook tutorial and this guide for the case of Apple Mail.{% endblocktrans %}

92 |
93 |
94 |
95 |
96 |
97 | 98 | {% endblock content %} 99 | -------------------------------------------------------------------------------- /pages/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.urls import reverse 3 | from django.conf import settings 4 | 5 | 6 | class TestPages(TestCase): 7 | """Some very redimentary tests to make sure pages are being delivered.""" 8 | 9 | def test_help_page_rendered(self): 10 | response = self.client.get(reverse("pages_help")) 11 | self.assertEqual(response.status_code, 200) 12 | self.assertIn("pages/help.html", [t.name for t in response.templates]) 13 | self.assertEqual( 14 | response.context["support_email"], settings.SUPPORT_EMAIL) 15 | 16 | def test_about_page_rendered(self): 17 | response = self.client.get(reverse("pages_about")) 18 | self.assertEqual(response.status_code, 200) 19 | self.assertIn("pages/about.html", [t.name for t in response.templates]) 20 | self.assertEqual( 21 | response.context["admin_name"], settings.SUPPORT_NAME) 22 | self.assertEqual( 23 | response.context["admin_email"], settings.SUPPORT_EMAIL) 24 | self.assertEqual( 25 | response.context["description"], settings.INSTANCE_DESCRIPTION) 26 | self.assertEqual( 27 | response.context["version"], settings.VERSION) 28 | 29 | def test_home_page_rendered(self): 30 | response = self.client.get(reverse("pages_index")) 31 | self.assertEqual(response.status_code, 200) 32 | self.assertIn("pages/index.html", [t.name for t in response.templates]) 33 | -------------------------------------------------------------------------------- /pages/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import HomeView, AboutView, HelpView 3 | 4 | urlpatterns = [ 5 | path('', HomeView.as_view(), name="pages_index"), 6 | path('about', AboutView.as_view(), name="pages_about"), 7 | path('help', HelpView.as_view(), name="pages_help") 8 | ] 9 | -------------------------------------------------------------------------------- /pages/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import TemplateView 2 | from django.conf import settings 3 | from humans.views import AuthMixin 4 | 5 | 6 | class HomeView(AuthMixin, TemplateView): 7 | """View for the Index page of the website""" 8 | template_name = "pages/index.html" 9 | 10 | 11 | class AboutView(AuthMixin, TemplateView): 12 | """View for the About page of the website""" 13 | template_name = "pages/about.html" 14 | 15 | def get_context_data(self, **kwargs): 16 | context = super().get_context_data(**kwargs) 17 | context["admin_name"] = settings.SUPPORT_NAME 18 | context["admin_email"] = settings.SUPPORT_EMAIL 19 | context["description"] = settings.INSTANCE_DESCRIPTION 20 | context["version"] = settings.VERSION 21 | return context 22 | 23 | 24 | class HelpView(AuthMixin, TemplateView): 25 | """View for the About page of the website""" 26 | template_name = "pages/help.html" 27 | 28 | def get_context_data(self, **kwargs): 29 | context = super().get_context_data(**kwargs) 30 | context["support_email"] = settings.SUPPORT_EMAIL 31 | return context 32 | --------------------------------------------------------------------------------