diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml new file mode 100644 index 00000000..7577b7fc --- /dev/null +++ b/.github/workflows/dockerhub.yml @@ -0,0 +1,52 @@ +name: CI to Docker Hub + +on: + push: + tags: + - '*' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v3 + with: + images: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPOSITORY }} + + - uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./Dockerfile + builder: ${{ steps.buildx.outputs.name }} + push: true + labels: ${{ steps.meta.outputs.labels }} + tags: ${{ steps.meta.outputs.tags }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b6d6c6b4..c38aa6ea 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,11 +9,12 @@ This document describes changes between each past release. Breaking changes ---------------- +- Include project code into project authentication token. This invalidates all existing API tokens and invitation links from previous versions (#802) - Drop support for Python 2 (#483) - Drop support for Python 3.5 (#571) - Drop support for MySQL (#743) - Require MariaDB version 10.3.2 or above (#632) -- Authentication page URL needs a `project_id` parameter (#802) +- Enable session cookie security by default (#845) The minimum supported version is now Python 3.6, and the project is tested with up to Python 3.9 @@ -26,6 +27,7 @@ Security - Add CSRF validation on destructive actions (#796) - Ask for private code to delete project or project history (#796) +- Add headers to mitigate Clickjacking, XSS, and other attacks: `X-Frame-Options`, `X-XSS-Protection`, `X-Content-Type-Options`, `Content-Security-Policy`, `Referrer-Policy` (#845) Added ----- @@ -38,10 +40,11 @@ Added - Add sorting, pagination, and searching to the admin dashboard (#538) - Add Project History page that records all changes (#553) - Add token-based authentication to the API (#504) -- Add translations for Hindi, Portuguese (Brazil), Tamil - Add illustrations as a showcase, currently only for French (#544) - Add a page for downloading mobile application (#688) +- Add optional support for a simple CAPTCHA (#844) - Add translations for Greek, Esperanto, Italian, Japanese, Portuguese and Swedish +- Publish an `official docker image `_ Changed ------- @@ -51,6 +54,7 @@ Changed - Make language choice persistent (#547) - Localize date strings in the current language (#590) - Differenciate "flash alerts" notifications (#594) +- Display "flash messages" persistently instead of making them disappear (#856) - Improve menu bar spacing, put history and settings in a submenu (#739) - Change Dockerfile to install python dependencies at build time (#793) - Updating project settings doesn't require to enter or update project code (#774) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index bd4d363e..fb6f4244 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -29,8 +29,8 @@ Glandos Heimen Stoffels James Leong Jocelyn Delalande -Lucas Verney Luc Didry +Lucas Verney Marien Fressinaud Mathieu Leplatre mcnesium @@ -44,6 +44,7 @@ Richard Coates THANOS SIOURDAKIS Toover Xavier Mehrenberger +Youe Graillot zorun The manual drawings are from Coline Billon, they are under CC BY 4.0. diff --git a/conf/entrypoint.sh b/conf/entrypoint.sh index dfe5e12c..e66ad7c8 100755 --- a/conf/entrypoint.sh +++ b/conf/entrypoint.sh @@ -21,6 +21,7 @@ ADMIN_PASSWORD = '$ADMIN_PASSWORD' ALLOW_PUBLIC_PROJECT_CREATION = $ALLOW_PUBLIC_PROJECT_CREATION ACTIVATE_ADMIN_DASHBOARD = $ACTIVATE_ADMIN_DASHBOARD BABEL_DEFAULT_TIMEZONE = "$BABEL_DEFAULT_TIMEZONE" +SESSION_COOKIE_SECURE = $SESSION_COOKIE_SECURE EOF # Start gunicorn without forking diff --git a/docs/api.rst b/docs/api.rst index 93ca36e9..982f828f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -4,7 +4,9 @@ The REST API All of what's possible to do with the website is also possible via a web API. This document explains how the API is organized and how you can query it. -The only supported data format is JSON. +The main supported data format is JSON. When using POST or PUT, you can +either pass data encoded in JSON or in ``application/x-www-form-urlencoded`` +format. Overall organisation ==================== @@ -31,7 +33,7 @@ instead of basic auth. For instance, start by generating the token (of course, you need to authenticate):: $ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo/token - {"token": "eyJwcm9qZWN0X2lkIjoiZGVtbyJ9.M86C3AiZa_SFEyiddYXdTh2-OOI"} + {"token": "WyJ0ZXN0Il0.Rt04fNMmxp9YslCRq8hB6jE9s1Q"} Make sure to store this token securely: it allows full access to the project. For instance, use it to obtain information about the project (replace PROJECT_TOKEN with @@ -47,7 +49,7 @@ looks like:: This token can also be used to authenticate for a project on the web interface, which can be useful to generate invitation links. You would simply create an URL of the form:: - https://ihatemoney.org/authenticate?token=PROJECT_TOKEN + https://ihatemoney.org/demo/join/PROJECT_TOKEN Such a link grants full access to the project associated with the token. @@ -64,10 +66,16 @@ Creating a project A project needs the following arguments: -* ``name``: The project name (string) +* ``name``: the project name (string) * ``id``: the project identifier (string without special chars or spaces) * ``password``: the project password / secret code (string) -* ``contact_email``: the contact email +* ``contact_email``: the contact email (string) + +Optional arguments: + +* ``default_currency``: the default currency to use for a multi-currency project, + in ISO 4217 format. Bills are converted to this currency for operations like balance + or statistics. Default value: ``XXX`` (no currency). :: @@ -75,7 +83,7 @@ A project needs the following arguments: -d 'name=yay&id=yay&password=yay&contact_email=yay@notmyidea.org' "yay" -As you can see, the API returns the identifier of the project +As you can see, the API returns the identifier of the project. Getting information about the project ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -88,6 +96,7 @@ Getting information about the project:: "id": "demo", "name": "demonstration", "contact_email": "demo@notmyidea.org", + "default_currency": "XXX", "members": [{"id": 11515, "name": "f", "weight": 1.0, "activated": true, "balance": 0}, {"id": 11531, "name": "g", "weight": 1.0, "activated": true, "balance": 0}, {"id": 11532, "name": "peter", "weight": 1.0, "activated": true, "balance": 5.0}, @@ -151,22 +160,55 @@ You can get the list of bills by doing a ``GET`` on $ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo/bills -Add a bill with a ``POST`` query on ``/api/projects//bills``. you need the -following params: +Or get a specific bill by ID:: -* ``date``: the date of the bill; defaults to current date if not - provided. (format is ``yyyy-mm-dd``) -* ``what``: what have been payed -* ``payer``: by who ? (id) -* ``payed_for``: for who ? (id, to set multiple id use a list, - e.g. ``["id1", "id2"]``) -* ``amount``: amount payed + $ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo/bills/42 + { + "id": 42, + "payer_id": 11, + "owers": [ + { + "id": 22, + "name": "Alexis", + "weight": 1, + "activated": true + } + ], + "amount": 100, + "date": "2020-12-24", + "creation_date": "2021-01-13", + "what": "Raclette du nouvel an", + "external_link": "", + "original_currency": "XXX", + "converted_amount": 100 + } + +``amount`` is expressed in the ``original_currency`` of the bill, while +``converted_amount`` is expressed in the project ``default_currency``. +Here, they are the same. + +Add a bill with a ``POST`` query on ``/api/projects//bills``. You need the +following required parameters: + +* ``what``: what has been paid (string) +* ``payer``: paid by who? (id) +* ``payed_for``: for who ? (id). To set multiple id, simply pass + the parameter multiple times (x-www-form-urlencoded) or pass a list of id (JSON). +* ``amount``: amount payed (float) + +And optional parameters: + +* ``date``: the date of the bill (``yyyy-mm-dd`` format). Defaults to current date + if not provided. +* ``original_currency``: the currency in which ``amount`` has been paid (ISO 4217 code). + Only makes sense for a project with currencies. Defaults to the project ``default_currency``. +* ``external_link``: an optional URL associated with the bill. Returns the id of the created bill :: $ curl --basic -u demo:demo -X POST\ https://ihatemoney.org/api/projects/demo/bills\ - -d "date=2011-09-10&what=raclette&payer=31&payed_for=31&amount=200" + -d "date=2011-09-10&what=raclette&payer=1&payed_for=3&payed_for=5&amount=200" 80 You can also ``PUT`` a new version of the bill at @@ -174,7 +216,7 @@ You can also ``PUT`` a new version of the bill at $ curl --basic -u demo:demo -X PUT\ https://ihatemoney.org/api/projects/demo/bills/80\ - -d "date=2011-09-10&what=raclette&payer=31&payed_for=31&amount=250" + -d "date=2011-09-10&what=raclette&payer=1&payed_for=3&payed_for=5&payed_for=1&amount=250" 80 And you can of course ``DELETE`` them at @@ -194,15 +236,15 @@ You can get some project stats with a ``GET`` on $ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo/statistics [ { - "balance": 12.5, - "member": {"activated": True, "id": 1, "name": "alexis", "weight": 1.0}, - "paid": 25.0, - "spent": 12.5 + "member": {"activated": true, "id": 1, "name": "alexis", "weight": 1.0}, + "paid": 25.5, + "spent": 15, + "balance": 10.5 }, { - "balance": -12.5, - "member": {"activated": True, "id": 2, "name": "fred", "weight": 1.0}, - "paid": 0, - "spent": 12.5 + "member": {"activated": true, "id": 2, "name": "fred", "weight": 1.0}, + "paid": 5, + "spent": 15.5, + "balance": -10.5 } ] diff --git a/docs/configuration.rst b/docs/configuration.rst index b0733a8a..140a12e6 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -13,6 +13,21 @@ To know defaults on your deployed instance, simply look at your "Production values" are the recommended values for use in production. +Configuration files +------------------- + +By default, Ihatemoney loads its configuration from ``/etc/ihatemoney/ihatemoney.cfg``. + +If you need to load the configuration from a custom path, you can define the +``IHATEMONEY_SETTINGS_FILE_PATH`` environment variable with the path to the configuration +file. +For instance :: + + export IHATEMONEY_SETTINGS_FILE_PATH="/path/to/your/conf/file.cfg" + +The path should be absolute. A relative path will be interpreted as being +inside ``/etc/ihatemoney/``. + `SQLALCHEMY_DATABASE_URI` ------------------------- @@ -49,6 +64,21 @@ of the secret key could easily access any project and bypass the private code ve - **Production value:** `ihatemoney conf-example ihatemoney.cfg` sets it to something random, which is good. +`SESSION_COOKIE_SECURE` +----------------------- + +A boolean that controls whether the session cookie will be marked "secure". +If this is the case, browsers will refuse to send the session cookie over plain HTTP. + +- **Default value:** ``True`` +- **Production value:** ``True`` if you run your service over HTTPS, ``False`` if you run + your service over plain HTTP. + +Note: this setting is actually interpreted by Flask, see the +`Flask documentation`_ for details. + +.. _Flask documentation: https://flask.palletsprojects.com/en/2.0.x/config/#SESSION_COOKIE_SECURE + `MAIL_DEFAULT_SENDER` --------------------- @@ -125,6 +155,11 @@ Note: this setting is actually interpreted by Flask-Babel, see the .. _Flask-Babel guide for formatting dates: https://pythonhosted.org/Flask-Babel/#formatting-dates +`ENABLE_CAPTCHA` +--------------- + +It is possible to add a simple captcha in order to filter out spammer bots on the form creation. +In order to do so, you just have to set `ENABLE_CAPTCHA = True`. Configuring emails sending -------------------------- @@ -142,12 +177,3 @@ possible to configure it to act differently, thanks to the great * **MAIL_PASSWORD** : default **None** * **DEFAULT_MAIL_SENDER** : default **None** -Using an alternate settings path --------------------------------- - -You can put your settings file where you want, and pass its path to the -application using the ``IHATEMONEY_SETTINGS_FILE_PATH`` environment variable. - -For instance :: - - export IHATEMONEY_SETTINGS_FILE_PATH="/path/to/your/conf/file.cfg" diff --git a/docs/contributing.rst b/docs/contributing.rst index 80174b76..5d597b25 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -104,12 +104,20 @@ You can create a ``settings.cfg`` file, with the following content:: DEBUG = True SQLACHEMY_ECHO = DEBUG -You can also set the `TESTING` flag to `True` so no mails are sent -(and no exception is raised) while you're on development mode. Then before running the application, declare its path with :: export IHATEMONEY_SETTINGS_FILE_PATH="$(pwd)/settings.cfg" +You can also set the ``TESTING`` flag to ``True`` so no mails are sent +(and no exception is raised) while you're on development mode. + +In some cases, you may need to disable secure cookies by setting +``SESSION_COOKIE_SECURE`` to ``False``. This is needed if you +access your dev server over the network: with the default value +of ``SESSION_COOKIE_SECURE``, the browser will refuse to send +the session cookie over insecure HTTP, so many features of Ihatemoney +won't work (project login, language change, etc). + .. _contributing-developer: Contributing as a developer diff --git a/docs/upgrade.rst b/docs/upgrade.rst index ec846324..53185413 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -65,6 +65,17 @@ If so, pick the ``pip`` commands to use in the relevant section(s) of Then follow :ref:`general-procedure` from step 1. in order to complete the update. +Disable session cookie security if running over plain HTTP +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +.. note:: If you are running Ihatemoney over HTTPS, no special action is required. + +Session cookies are now marked "secure" by default to increase security. + +If you run Ihatemoney over plain HTTP, you need to explicitly disable this security +feature by setting ``SESSION_COOKIE_SECURE`` to ``False``, see :ref:`configuration`. + + Switch to MariaDB >= 10.3.2 instead of MySQL ++++++++++++++++++++++++++++++++++++++++++++ diff --git a/ihatemoney/api/common.py b/ihatemoney/api/common.py index ede76e46..fa097dec 100644 --- a/ihatemoney/api/common.py +++ b/ihatemoney/api/common.py @@ -35,7 +35,9 @@ def need_auth(f): auth_token = auth_header.split(" ")[1] except IndexError: abort(401) - project_id = Project.verify_token(auth_token, token_type="non_timed_token") + project_id = Project.verify_token( + auth_token, token_type="auth", project_id=project_id + ) if auth_token and project_id: project = Project.query.get(project_id) if project: diff --git a/ihatemoney/conf-templates/ihatemoney.cfg.j2 b/ihatemoney/conf-templates/ihatemoney.cfg.j2 index 0188c6b1..9d117c18 100644 --- a/ihatemoney/conf-templates/ihatemoney.cfg.j2 +++ b/ihatemoney/conf-templates/ihatemoney.cfg.j2 @@ -38,3 +38,11 @@ ACTIVATE_ADMIN_DASHBOARD = False # You can change the timezone used to display time. By default it will be #derived from the server OS. #BABEL_DEFAULT_TIMEZONE = "Europe/Paris" + +# Enable secure cookies. Requires HTTPS. Disable if you run your ihatemoney +# service over plain HTTP. +SESSION_COOKIE_SECURE = True + +# You can activate an optional CAPTCHA if you want to. It can be helpful +# to filter spammer bots. +# ENABLE_CAPTCHA = True diff --git a/ihatemoney/default_settings.py b/ihatemoney/default_settings.py index 9050bbeb..3204ed3b 100644 --- a/ihatemoney/default_settings.py +++ b/ihatemoney/default_settings.py @@ -8,6 +8,7 @@ ACTIVATE_DEMO_PROJECT = True ADMIN_PASSWORD = "" ALLOW_PUBLIC_PROJECT_CREATION = True ACTIVATE_ADMIN_DASHBOARD = False +SESSION_COOKIE_SECURE = True SUPPORTED_LANGUAGES = [ "de", "el", @@ -31,3 +32,4 @@ SUPPORTED_LANGUAGES = [ "uk", "zh_Hans", ] +ENABLE_CAPTCHA = False diff --git a/ihatemoney/forms.py b/ihatemoney/forms.py index 4abc86d3..7839a5d9 100644 --- a/ihatemoney/forms.py +++ b/ihatemoney/forms.py @@ -13,6 +13,7 @@ from wtforms.fields.core import Label, SelectField, SelectMultipleField from wtforms.fields.html5 import DateField, DecimalField, URLField from wtforms.fields.simple import BooleanField, PasswordField, StringField, SubmitField from wtforms.validators import ( + URL, DataRequired, Email, EqualTo, @@ -112,7 +113,11 @@ class EditProjectForm(FlaskForm): project_history = BooleanField(_("Enable project history")) ip_recording = BooleanField(_("Use IP tracking for project history")) currency_helper = CurrencyConverter() - default_currency = SelectField(_("Default Currency"), validators=[DataRequired()]) + default_currency = SelectField( + _("Default Currency"), + validators=[DataRequired()], + default=CurrencyConverter.no_currency, + ) def __init__(self, *args, **kwargs): if not hasattr(self, "id"): @@ -220,6 +225,19 @@ class ProjectForm(EditProjectForm): ) raise ValidationError(Markup(message)) + @classmethod + def enable_captcha(cls): + captchaField = StringField( + _("Which is a real currency: Euro or Petro dollar?"), + validators=[DataRequired()], + ) + setattr(cls, "captcha", captchaField) + + def validate_captcha(form, field): + if not field.data.lower() == _("euro"): + message = _("Please, validate the captcha to proceed.") + raise ValidationError(Markup(message)) + class DestructiveActionProjectForm(FlaskForm): """Used for any important "delete" action linked to a project: @@ -292,7 +310,7 @@ class BillForm(FlaskForm): original_currency = SelectField(_("Currency"), validators=[DataRequired()]) external_link = URLField( _("External link"), - validators=[Optional()], + validators=[Optional(), URL()], description=_("A link to an external document, related to this bill"), ) payed_for = SelectMultipleField( @@ -321,7 +339,7 @@ class BillForm(FlaskForm): bill.external_link = "" bill.date = self.date bill.owers = [Person.query.get(ower, project) for ower in self.payed_for] - bill.original_currency = CurrencyConverter.no_currency + bill.original_currency = self.original_currency bill.converted_amount = self.currency_helper.exchange_currency( bill.amount, bill.original_currency, project.default_currency ) diff --git a/ihatemoney/models.py b/ihatemoney/models.py index 04415fa6..e0ca0704 100644 --- a/ihatemoney/models.py +++ b/ihatemoney/models.py @@ -7,8 +7,8 @@ from flask_sqlalchemy import BaseQuery, SQLAlchemy from itsdangerous import ( BadSignature, SignatureExpired, - TimedJSONWebSignatureSerializer, URLSafeSerializer, + URLSafeTimedSerializer, ) import sqlalchemy from sqlalchemy import orm @@ -179,6 +179,7 @@ class Project(db.Model): "ower": transaction["ower"].name, "receiver": transaction["receiver"].name, "amount": round(transaction["amount"], 2), + "currency": transaction["currency"], } ) return pretty_transactions @@ -192,6 +193,7 @@ class Project(db.Model): "ower": members[ower_id], "receiver": members[receiver_id], "amount": amount, + "currency": self.default_currency, } for ower_id, amount, receiver_id in settle_plan ] @@ -269,6 +271,7 @@ class Project(db.Model): { "what": bill.what, "amount": round(bill.amount, 2), + "currency": bill.original_currency, "date": str(bill.date), "payer_name": Person.query.get(bill.payer_id).name, "payer_weight": Person.query.get(bill.payer_id).weight, @@ -336,41 +339,61 @@ class Project(db.Model): db.session.delete(self) db.session.commit() - def generate_token(self, expiration=0): + def generate_token(self, token_type="auth"): """Generate a timed and serialized JsonWebToken - :param expiration: Token expiration time (in seconds) + :param token_type: Either "auth" for authentication (invalidated when project code changed), + or "reset" for password reset (invalidated after expiration) """ - if expiration: - serializer = TimedJSONWebSignatureSerializer( - current_app.config["SECRET_KEY"], expiration + + if token_type == "reset": + serializer = URLSafeTimedSerializer( + current_app.config["SECRET_KEY"], salt=token_type ) - token = serializer.dumps({"project_id": self.id}).decode("utf-8") + token = serializer.dumps([self.id]) else: - serializer = URLSafeSerializer(current_app.config["SECRET_KEY"]) - token = serializer.dumps({"project_id": self.id}) + serializer = URLSafeSerializer( + current_app.config["SECRET_KEY"] + self.password, salt=token_type + ) + token = serializer.dumps([self.id]) + return token @staticmethod - def verify_token(token, token_type="timed_token"): + def verify_token(token, token_type="auth", project_id=None, max_age=3600): """Return the project id associated to the provided token, None if the provided token is expired or not valid. :param token: Serialized TimedJsonWebToken + :param token_type: Either "auth" for authentication (invalidated when project code changed), + or "reset" for password reset (invalidated after expiration) + :param project_id: Project ID. Used for token_type "auth" to use the password as serializer + secret key. + :param max_age: Token expiration time (in seconds). Only used with token_type "reset" """ - if token_type == "timed_token": - serializer = TimedJSONWebSignatureSerializer( - current_app.config["SECRET_KEY"] + loads_kwargs = {} + if token_type == "reset": + serializer = URLSafeTimedSerializer( + current_app.config["SECRET_KEY"], salt=token_type ) + loads_kwargs["max_age"] = max_age else: - serializer = URLSafeSerializer(current_app.config["SECRET_KEY"]) + project = Project.query.get(project_id) if project_id is not None else None + password = project.password if project is not None else "" + serializer = URLSafeSerializer( + current_app.config["SECRET_KEY"] + password, salt=token_type + ) try: - data = serializer.loads(token) + data = serializer.loads(token, **loads_kwargs) except SignatureExpired: return None except BadSignature: return None - return data["project_id"] + + data_project = data[0] if isinstance(data, list) else None + return ( + data_project if project_id is None or data_project == project_id else None + ) def __str__(self): return self.name @@ -385,7 +408,7 @@ class Project(db.Model): name="demonstration", password=generate_password_hash("demo"), contact_email="demo@notmyidea.org", - default_currency="EUR", + default_currency="XXX", ) db.session.add(project) db.session.commit() @@ -413,7 +436,7 @@ class Project(db.Model): bill.what = subject bill.owers = [members[name] for name in owers] bill.amount = amount - bill.original_currency = "EUR" + bill.original_currency = "XXX" bill.converted_amount = amount db.session.add(bill) diff --git a/ihatemoney/run.py b/ihatemoney/run.py index c8fc5b25..cea6f93e 100644 --- a/ihatemoney/run.py +++ b/ihatemoney/run.py @@ -7,6 +7,7 @@ from flask import Flask, g, render_template, request, session from flask_babel import Babel, format_currency from flask_mail import Mail from flask_migrate import Migrate, stamp, upgrade +from flask_talisman import Talisman from jinja2 import pass_context from markupsafe import Markup import pytz @@ -126,6 +127,24 @@ def create_app( instance_relative_config=instance_relative_config, ) + # If we need to load external JS/CSS/image resources, it needs to be added here, see + # https://github.com/wntrblm/flask-talisman#content-security-policy + csp = { + "default-src": ["'self'"], + # We have several inline javascript scripts :( + "script-src": ["'self'", "'unsafe-inline'"], + "object-src": "'none'", + } + + Talisman( + app, + # Forcing HTTPS is the job of a reverse proxy + force_https=False, + # This is handled separately through the SESSION_COOKIE_SECURE Flask setting + session_cookie_secure=False, + content_security_policy=csp, + ) + # If a configuration object is passed, use it. Otherwise try to find one. load_configuration(app, configuration) app.wsgi_app = PrefixedWSGI(app) diff --git a/ihatemoney/static/css/main.css b/ihatemoney/static/css/main.css index 0cd4368e..d71c9bd3 100644 --- a/ihatemoney/static/css/main.css +++ b/ihatemoney/static/css/main.css @@ -28,6 +28,7 @@ body { } .navbar-brand { font-family: "Lobster", arial, serif; + font-size: 1.5rem; } @media (min-width: 992px) { @@ -468,7 +469,6 @@ tr.payer_line .balance-name { position: absolute; top: 4.5rem; width: 100%; - pointer-events: none; } .light { diff --git a/ihatemoney/templates/forms.html b/ihatemoney/templates/forms.html index a9965564..9d8e8c99 100644 --- a/ihatemoney/templates/forms.html +++ b/ihatemoney/templates/forms.html @@ -75,6 +75,9 @@ {{ input(form.name) }} {{ input(form.password) }} {{ input(form.contact_email) }} + {% if config['ENABLE_CAPTCHA'] %} + {{ input(form.captcha) }} + {% endif %} {{ input(form.default_currency) }} {% if not home %} {{ submit(form.submit, home=True) }} @@ -171,7 +174,7 @@ - +
{{ _("More options") }} {% if g.project.default_currency != "XXX" %} diff --git a/ihatemoney/templates/invitation_mail.en.j2 b/ihatemoney/templates/invitation_mail.en.j2 index 79fcc427..bb38b9aa 100644 --- a/ihatemoney/templates/invitation_mail.en.j2 +++ b/ihatemoney/templates/invitation_mail.en.j2 @@ -4,7 +4,7 @@ Someone using the email address {{ g.project.contact_email }} invited you to sha It's as simple as saying what did you pay for, for whom, and how much did it cost you, we are caring about the rest. -You can log in using this link: {{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}. +You can log in using this link: {{ url_for(".join_project", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}. Once logged-in, you can use the following link which is easier to remember: {{ url_for(".list_bills", _external=True) }} If your cookie gets deleted or if you log out, you will need to log back in using the first link. diff --git a/ihatemoney/templates/invitation_mail.fr.j2 b/ihatemoney/templates/invitation_mail.fr.j2 index e57d7035..0cf02f43 100644 --- a/ihatemoney/templates/invitation_mail.fr.j2 +++ b/ihatemoney/templates/invitation_mail.fr.j2 @@ -4,7 +4,7 @@ Quelqu'un dont l'adresse email est {{ g.project.contact_email }} vous a invité Il suffit de renseigner qui a payé pour quoi, pour qui, combien ça a coûté, et on s’occupe du reste. -Vous pouvez vous connecter grâce à ce lien : {{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}. +Vous pouvez vous connecter grâce à ce lien : {{ url_for(".join_project", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}. Une fois connecté, vous pourrez utiliser le lien suivant qui est plus facile à mémoriser : {{ url_for(".list_bills", _external=True) }} Si vous êtes déconnecté volontairement ou non, vous devrez utiliser à nouveau le premier lien. diff --git a/ihatemoney/templates/layout.html b/ihatemoney/templates/layout.html index bd5a412a..137c2a59 100644 --- a/ihatemoney/templates/layout.html +++ b/ihatemoney/templates/layout.html @@ -21,12 +21,6 @@ {% block head %}{% endblock %}