├── .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 | 
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 |
33 | {% trans "The following people will receive the messages:" %}
34 | {% for recipient in object.recipients.all %}
35 |
{% trans "Name:" %} {{recipient.first_name}} {{recipient.last_name}}
36 |
{% trans "Email:" %} {{recipient.email}}
37 |
{% trans "Key fingerprint:" %} {{recipient.fingerprint}}
38 |
{{recipient.public_key}}
39 | {% endfor %}
40 |
41 |
42 |
43 |
44 |
45 |
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 |
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 |
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 |
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 |
6 |
7 |
8 |
9 |
62 |
63 | {% include "socialaccount/snippets/provider_list.html" with process="login" %}
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
107 |
108 | {% include "socialaccount/snippets/provider_list.html" with process="login" %}
109 |
110 | {% endif %}
111 |
112 |
{% blocktrans %}Already have an account? Then please Sign in {% endblocktrans %}
113 |
114 |
115 |
116 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
4 |
5 |
6 |
7 |
8 |
{% trans "Key Change Logs" %}
9 |
{% blocktrans %}The following table contains the list of all recent changes to the key used by your account.{% endblocktrans %}
10 |
11 |
33 |
34 |
35 |
36 |
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 |
12 |
{% trans "Profile" %}
13 | {% trans "Keys" %}
14 | {% if not form.instance.has_github_login %}
15 | {% trans "Password" %}
16 | {%endif %}
17 |
18 |
19 |
20 |
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 |
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 |
23 |
24 | {% endblock content %}
25 |
--------------------------------------------------------------------------------
/pages/templates/404.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/base.html" %}
2 | {% load i18n %}
3 |
4 | {% block content %}
5 |
6 |
23 |
24 | {% endblock content %}
25 |
--------------------------------------------------------------------------------
/pages/templates/500.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/base.html" %}
2 | {% load i18n %}
3 |
4 | {% block content %}
5 |
6 |
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 |
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 |
8 |
9 |
10 |
11 |
{% trans "SUPPORT" %}
12 |
13 |
21 |
22 |
23 |
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 |
--------------------------------------------------------------------------------