mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-05-05 20:51:49 +02:00
Merge branch 'master' into almet/recaptcha
This commit is contained in:
commit
c7533ea6f2
29 changed files with 326 additions and 162 deletions
|
@ -9,6 +9,7 @@ This document describes changes between each past release.
|
|||
Breaking changes
|
||||
----------------
|
||||
|
||||
- Enable session cookie security by default (#845)
|
||||
- Drop support for Python 2 (#483)
|
||||
- Drop support for Python 3.5 (#571)
|
||||
- Drop support for MySQL (#743)
|
||||
|
@ -25,6 +26,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
|
||||
-----
|
||||
|
@ -37,7 +39,6 @@ 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 translations for Greek, Esperanto, Italian, Japanese, Portuguese and Swedish
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`
|
||||
---------------------
|
||||
|
||||
|
@ -148,12 +178,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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -44,3 +44,7 @@ ACTIVATE_ADMIN_DASHBOARD = False
|
|||
# ENABLE_RECAPTCHA = True
|
||||
# RECAPTCHA_PUBLIC_KEY = ""
|
||||
# RECAPTCHA_PRIVATE_KEY = ""
|
||||
|
||||
# Enable secure cookies. Requires HTTPS. Disable if you run your ihatemoney
|
||||
# service over plain HTTP.
|
||||
SESSION_COOKIE_SECURE = True
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
@ -292,7 +293,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(
|
||||
|
|
|
@ -7,8 +7,8 @@ from flask_sqlalchemy import BaseQuery, SQLAlchemy
|
|||
from itsdangerous import (
|
||||
BadSignature,
|
||||
SignatureExpired,
|
||||
TimedJSONWebSignatureSerializer,
|
||||
URLSafeSerializer,
|
||||
URLSafeTimedSerializer,
|
||||
)
|
||||
import sqlalchemy
|
||||
from sqlalchemy import orm
|
||||
|
@ -339,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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(".authenticate", _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.
|
||||
|
|
|
@ -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(".authenticate", _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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Hi,
|
||||
|
||||
You requested to reset the password of the following project: "{{ project.name }}".
|
||||
You can reset it here: {{ url_for(".reset_password", _external=True, token=project.generate_token(expiration=3600)) }}.
|
||||
You can reset it here: {{ url_for(".reset_password", _external=True, token=project.generate_token(token_type="reset")) }}.
|
||||
This link is only valid for one hour.
|
||||
|
||||
Hope this helps,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Salut,
|
||||
|
||||
Vous avez demandé à réinitialiser le mot de passe du projet suivant : "{{ project.name }}".
|
||||
Vous pouvez le réinitialiser ici : {{ url_for(".reset_password", _external=True, token=project.generate_token(expiration=3600)) }}.
|
||||
Vous pouvez le réinitialiser ici : {{ url_for(".reset_password", _external=True, token=project.generate_token(token_type="reset")) }}.
|
||||
Ce lien est seulement valide pendant 1 heure.
|
||||
|
||||
Faites-en bon usage !
|
||||
|
|
|
@ -21,8 +21,8 @@
|
|||
</td>
|
||||
<td>
|
||||
{{ _("You can directly share the following link via your prefered medium") }}</br>
|
||||
<a href="{{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}">
|
||||
{{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}
|
||||
<a href="{{ url_for(".authenticate", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}">
|
||||
{{ url_for(".authenticate", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -213,7 +213,9 @@ class APITestCase(IhatemoneyTestCase):
|
|||
"/api/projects/raclette/token", headers=self.get_auth("raclette")
|
||||
)
|
||||
decoded_resp = json.loads(resp.data.decode("utf-8"))
|
||||
resp = self.client.get("/authenticate?token={}".format(decoded_resp["token"]))
|
||||
resp = self.client.get(
|
||||
f"/authenticate?token={decoded_resp['token']}&project_id=raclette"
|
||||
)
|
||||
# Test that we are redirected.
|
||||
self.assertEqual(302, resp.status_code)
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import json
|
|||
import re
|
||||
from time import sleep
|
||||
import unittest
|
||||
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
from flask import session
|
||||
import pytest
|
||||
|
@ -11,6 +12,7 @@ from werkzeug.security import check_password_hash, generate_password_hash
|
|||
|
||||
from ihatemoney import models
|
||||
from ihatemoney.currency_convertor import CurrencyConverter
|
||||
from ihatemoney.tests.common.help_functions import extract_link
|
||||
from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
|
||||
from ihatemoney.versioning import LoggingMode
|
||||
|
||||
|
@ -87,11 +89,52 @@ class BudgetTestCase(IhatemoneyTestCase):
|
|||
)
|
||||
# Test empty and invalid tokens
|
||||
self.client.get("/exit")
|
||||
# Use another project_id
|
||||
parsed_url = urlparse(url)
|
||||
query = parse_qs(parsed_url.query)
|
||||
query["project_id"] = "invalid"
|
||||
resp = self.client.get(
|
||||
urlunparse(parsed_url._replace(query=urlencode(query, doseq=True)))
|
||||
)
|
||||
assert "You either provided a bad token" in resp.data.decode("utf-8")
|
||||
|
||||
resp = self.client.get("/authenticate")
|
||||
self.assertIn("You either provided a bad token", resp.data.decode("utf-8"))
|
||||
resp = self.client.get("/authenticate?token=token")
|
||||
self.assertIn("You either provided a bad token", resp.data.decode("utf-8"))
|
||||
|
||||
def test_invite_code_invalidation(self):
|
||||
"""Test that invitation link expire after code change"""
|
||||
self.login("raclette")
|
||||
self.post_project("raclette")
|
||||
response = self.client.get("/raclette/invite").data.decode("utf-8")
|
||||
link = extract_link(response, "share the following link")
|
||||
|
||||
self.client.get("/exit")
|
||||
response = self.client.get(link)
|
||||
# Link is valid
|
||||
assert response.status_code == 302
|
||||
|
||||
# Change password to invalidate token
|
||||
# Other data are required, but useless for the test
|
||||
response = self.client.post(
|
||||
"/raclette/edit",
|
||||
data={
|
||||
"name": "raclette",
|
||||
"contact_email": "zorglub@notmyidea.org",
|
||||
"password": "didoudida",
|
||||
"default_currency": "XXX",
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert "alert-danger" not in response.data.decode("utf-8")
|
||||
|
||||
self.client.get("/exit")
|
||||
response = self.client.get(link, follow_redirects=True)
|
||||
# Link is invalid
|
||||
self.assertIn("You either provided a bad token", response.data.decode("utf-8"))
|
||||
|
||||
def test_password_reminder(self):
|
||||
# test that it is possible to have an email containing the password of a
|
||||
# project in case people forget it (and it happens!)
|
||||
|
@ -632,6 +675,35 @@ class BudgetTestCase(IhatemoneyTestCase):
|
|||
bill = models.Bill.query.filter(models.Bill.date == "2011-08-01")[0]
|
||||
self.assertEqual(bill.amount, 25.02)
|
||||
|
||||
# add a bill with a valid external link
|
||||
self.client.post(
|
||||
"/raclette/add",
|
||||
data={
|
||||
"date": "2015-05-05",
|
||||
"what": "fromage à raclette",
|
||||
"payer": members_ids[0],
|
||||
"payed_for": members_ids,
|
||||
"amount": "42",
|
||||
"external_link": "https://example.com/fromage",
|
||||
},
|
||||
)
|
||||
bill = models.Bill.query.filter(models.Bill.date == "2015-05-05")[0]
|
||||
self.assertEqual(bill.external_link, "https://example.com/fromage")
|
||||
|
||||
# add a bill with an invalid external link
|
||||
resp = self.client.post(
|
||||
"/raclette/add",
|
||||
data={
|
||||
"date": "2015-05-06",
|
||||
"what": "mauvais fromage à raclette",
|
||||
"payer": members_ids[0],
|
||||
"payed_for": members_ids,
|
||||
"amount": "42000",
|
||||
"external_link": "javascript:alert('Tu bluffes, Martoni.')",
|
||||
},
|
||||
)
|
||||
self.assertIn("Invalid URL", resp.data.decode("utf-8"))
|
||||
|
||||
def test_weighted_balance(self):
|
||||
self.post_project("raclette")
|
||||
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
from markupsafe import Markup
|
||||
|
||||
|
||||
def em_surround(string, regex_escape=False):
|
||||
if regex_escape:
|
||||
return r'<em class="font-italic">%s<\/em>' % string
|
||||
else:
|
||||
return '<em class="font-italic">%s</em>' % string
|
||||
|
||||
|
||||
def extract_link(data, start_prefix):
|
||||
base_index = data.find(start_prefix)
|
||||
start = data.find('href="', base_index) + 6
|
||||
end = data.find('">', base_index)
|
||||
link = Markup(data[start:end]).unescape()
|
||||
return link
|
||||
|
|
Binary file not shown.
|
@ -3,8 +3,8 @@ msgstr ""
|
|||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-07-17 17:31+0200\n"
|
||||
"PO-Revision-Date: 2021-08-19 20:34+0000\n"
|
||||
"Last-Translator: corny <nico.eckstein+weblate@gmail.com>\n"
|
||||
"PO-Revision-Date: 2021-09-23 19:36+0000\n"
|
||||
"Last-Translator: Christian H. <sunrisechain@gmail.com>\n"
|
||||
"Language-Team: German <https://hosted.weblate.org/projects/i-hate-money/"
|
||||
"i-hate-money/de/>\n"
|
||||
"Language: de\n"
|
||||
|
@ -12,7 +12,7 @@ msgstr ""
|
|||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.8-dev\n"
|
||||
"X-Generator: Weblate 4.9-dev\n"
|
||||
"Generated-By: Babel 2.9.0\n"
|
||||
|
||||
msgid ""
|
||||
|
@ -74,7 +74,7 @@ msgstr ""
|
|||
"wähle eine andere Kennung"
|
||||
|
||||
msgid "Enter private code to confirm deletion"
|
||||
msgstr ""
|
||||
msgstr "Geben Sie Ihren privaten Code ein, um die Löschung zu bestätigen"
|
||||
|
||||
msgid "Unknown error"
|
||||
msgstr "Unbekannter Fehler"
|
||||
|
|
Binary file not shown.
|
@ -1,18 +1,18 @@
|
|||
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-07-17 17:31+0200\n"
|
||||
"PO-Revision-Date: 2021-07-10 15:34+0000\n"
|
||||
"PO-Revision-Date: 2021-10-01 20:35+0000\n"
|
||||
"Last-Translator: phlostically <phlostically@mailinator.com>\n"
|
||||
"Language-Team: Esperanto <https://hosted.weblate.org/projects/i-hate-money/"
|
||||
"i-hate-money/eo/>\n"
|
||||
"Language: eo\n"
|
||||
"Language-Team: Esperanto <https://hosted.weblate.org/projects/i-hate-"
|
||||
"money/i-hate-money/eo/>\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.9-dev\n"
|
||||
"Generated-By: Babel 2.9.0\n"
|
||||
|
||||
msgid ""
|
||||
|
@ -76,13 +76,11 @@ msgstr ""
|
|||
msgid "Enter private code to confirm deletion"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Unknown error"
|
||||
msgstr "Nekonata projekto"
|
||||
msgstr "Nekonata eraro"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Invalid private code."
|
||||
msgstr "Nova privata kodo"
|
||||
msgstr "Nevalida privata kodo."
|
||||
|
||||
msgid "Get in"
|
||||
msgstr "Eniri"
|
||||
|
@ -175,30 +173,30 @@ msgstr "La retpoŝta adreso %(email)s ne validas"
|
|||
|
||||
#. List with two items only
|
||||
msgid "{dual_object_0} and {dual_object_1}"
|
||||
msgstr ""
|
||||
msgstr "{dual_object_0} kaj {dual_object_1}"
|
||||
|
||||
#. Last two items of a list with more than 3 items
|
||||
msgid "{previous_object}, and {end_object}"
|
||||
msgstr ""
|
||||
msgstr "{previous_object} kaj {end_object}"
|
||||
|
||||
#. Two items in a middle of a list with more than 5 objects
|
||||
msgid "{previous_object}, {next_object}"
|
||||
msgstr ""
|
||||
msgstr "{previous_object}, {next_object}"
|
||||
|
||||
#. First two items of a list with more than 3 items
|
||||
msgid "{start_object}, {next_object}"
|
||||
msgstr ""
|
||||
msgstr "{start_object}, {next_object}"
|
||||
|
||||
msgid "No Currency"
|
||||
msgstr "Neniu valuto"
|
||||
|
||||
#. Form error with only one error
|
||||
msgid "{prefix}: {error}"
|
||||
msgstr ""
|
||||
msgstr "{prefix}: {error}"
|
||||
|
||||
#. Form error with a list of errors
|
||||
msgid "{prefix}:<br />{errors}"
|
||||
msgstr ""
|
||||
msgstr "{prefix}:<br />{errors}"
|
||||
|
||||
msgid "Too many failed login attempts, please retry later."
|
||||
msgstr "Tro da malsukcesaj provoj de salutado; bonvolu reprovi poste."
|
||||
|
@ -396,16 +394,14 @@ msgstr "Elŝuti programon por poŝaparato"
|
|||
msgid "Get it on"
|
||||
msgstr "Elŝuti ĝin ĉe"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Are you sure?"
|
||||
msgstr "ĉu vi certas?"
|
||||
msgstr "Ĉu vi certas?"
|
||||
|
||||
msgid "Edit project"
|
||||
msgstr "Redakti projekton"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Delete project"
|
||||
msgstr "Redakti projekton"
|
||||
msgstr "Forviŝi projekton"
|
||||
|
||||
msgid "Import JSON"
|
||||
msgstr "Enporti JSON-dosieron"
|
||||
|
@ -453,7 +449,7 @@ msgid "Everyone"
|
|||
msgstr "Ĉiuj"
|
||||
|
||||
msgid "No one"
|
||||
msgstr ""
|
||||
msgstr "Neniu"
|
||||
|
||||
msgid "More options"
|
||||
msgstr ""
|
||||
|
|
Binary file not shown.
|
@ -1,19 +1,19 @@
|
|||
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-07-17 17:31+0200\n"
|
||||
"PO-Revision-Date: 2021-05-10 11:33+0000\n"
|
||||
"Last-Translator: Vsevolod <sevauserg.com@gmail.com>\n"
|
||||
"PO-Revision-Date: 2021-09-20 12:38+0000\n"
|
||||
"Last-Translator: Роман Прокопов <pochta.romana.iz.vyborga@gmail.com>\n"
|
||||
"Language-Team: Russian <https://hosted.weblate.org/projects/i-hate-money/"
|
||||
"i-hate-money/ru/>\n"
|
||||
"Language: ru\n"
|
||||
"Language-Team: Russian <https://hosted.weblate.org/projects/i-hate-"
|
||||
"money/i-hate-money/ru/>\n"
|
||||
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
|
||||
"X-Generator: Weblate 4.9-dev\n"
|
||||
"Generated-By: Babel 2.9.0\n"
|
||||
|
||||
msgid ""
|
||||
|
@ -26,9 +26,8 @@ msgstr ""
|
|||
msgid "Project name"
|
||||
msgstr "Имя проекта"
|
||||
|
||||
#, fuzzy
|
||||
msgid "New private code"
|
||||
msgstr "Приватный код"
|
||||
msgstr "Новый приватный код"
|
||||
|
||||
msgid "Enter a new code if you want to change it"
|
||||
msgstr ""
|
||||
|
|
Binary file not shown.
|
@ -1,19 +1,18 @@
|
|||
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-07-17 17:31+0200\n"
|
||||
"PO-Revision-Date: 2020-10-12 04:47+0000\n"
|
||||
"Last-Translator: Jwen921 <yangjingwen0921@gmail.com>\n"
|
||||
"PO-Revision-Date: 2021-10-10 05:05+0000\n"
|
||||
"Last-Translator: Frank.wu <me@wuzhiping.top>\n"
|
||||
"Language-Team: Chinese (Simplified) <https://hosted.weblate.org/projects/"
|
||||
"i-hate-money/i-hate-money/zh_Hans/>\n"
|
||||
"Language: zh_Hans\n"
|
||||
"Language-Team: Chinese (Simplified) "
|
||||
"<https://hosted.weblate.org/projects/i-hate-money/i-hate-money/zh_Hans/>"
|
||||
"\n"
|
||||
"Plural-Forms: nplurals=1; plural=0\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 4.9-dev\n"
|
||||
"Generated-By: Babel 2.9.0\n"
|
||||
|
||||
msgid ""
|
||||
|
@ -24,12 +23,11 @@ msgstr "金额或符号无效。仅限数字与+-*/符号。"
|
|||
msgid "Project name"
|
||||
msgstr "账目名称"
|
||||
|
||||
#, fuzzy
|
||||
msgid "New private code"
|
||||
msgstr "共享密钥"
|
||||
msgstr "新的私人代码"
|
||||
|
||||
msgid "Enter a new code if you want to change it"
|
||||
msgstr ""
|
||||
msgstr "如要更改,请输入新代码"
|
||||
|
||||
msgid "Email"
|
||||
msgstr "邮箱"
|
||||
|
@ -46,7 +44,7 @@ msgstr "默认货币"
|
|||
msgid ""
|
||||
"This project cannot be set to 'no currency' because it contains bills in "
|
||||
"multiple currencies."
|
||||
msgstr ""
|
||||
msgstr "此项目不能设置为“无货币”,因为它包含多种货币的账单。"
|
||||
|
||||
msgid "Import previously exported JSON file"
|
||||
msgstr "导入之前的JSON 文件"
|
||||
|
@ -70,15 +68,13 @@ msgid ""
|
|||
msgstr "账目(“%(project)s”)已存在,请选择一个新名称"
|
||||
|
||||
msgid "Enter private code to confirm deletion"
|
||||
msgstr ""
|
||||
msgstr "请输入专用代码以确认删除"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Unknown error"
|
||||
msgstr "未知项目"
|
||||
msgstr "未知错误"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Invalid private code."
|
||||
msgstr "共享密钥"
|
||||
msgstr "无效的私人代码。"
|
||||
|
||||
msgid "Get in"
|
||||
msgstr "进入"
|
||||
|
@ -171,40 +167,40 @@ msgstr "此邮箱%(email)s不存在"
|
|||
|
||||
#. List with two items only
|
||||
msgid "{dual_object_0} and {dual_object_1}"
|
||||
msgstr ""
|
||||
msgstr "{dual_object_0} 和 {dual_object_1}"
|
||||
|
||||
#. Last two items of a list with more than 3 items
|
||||
msgid "{previous_object}, and {end_object}"
|
||||
msgstr ""
|
||||
msgstr "{previous_object} 和 {end_object}"
|
||||
|
||||
#. Two items in a middle of a list with more than 5 objects
|
||||
msgid "{previous_object}, {next_object}"
|
||||
msgstr ""
|
||||
msgstr "{previous_object},{next_object}"
|
||||
|
||||
#. First two items of a list with more than 3 items
|
||||
msgid "{start_object}, {next_object}"
|
||||
msgstr ""
|
||||
msgstr "{start_object}, {next_object}"
|
||||
|
||||
msgid "No Currency"
|
||||
msgstr "没有货币"
|
||||
msgstr "无货币"
|
||||
|
||||
#. Form error with only one error
|
||||
msgid "{prefix}: {error}"
|
||||
msgstr ""
|
||||
msgstr "{prefix}: {error}"
|
||||
|
||||
#. Form error with a list of errors
|
||||
msgid "{prefix}:<br />{errors}"
|
||||
msgstr ""
|
||||
msgstr "{prefix}:<br />{errors}"
|
||||
|
||||
msgid "Too many failed login attempts, please retry later."
|
||||
msgstr "输入错误太多次了,请稍后重试。"
|
||||
msgstr "登录失败次数过多,请稍后重试。"
|
||||
|
||||
#, python-format
|
||||
msgid "This admin password is not the right one. Only %(num)d attempts left."
|
||||
msgstr "管理密码有误,只剩 %(num)d次尝试机会"
|
||||
msgstr "管理密码有误,只剩 %(num)d次尝试机会。"
|
||||
|
||||
msgid "You either provided a bad token or no project identifier."
|
||||
msgstr "你输入了错误的符号或没有项目标识符。"
|
||||
msgstr "你输入了错误的令牌或没有项目标识符。"
|
||||
|
||||
msgid "This private code is not the right one"
|
||||
msgstr "专用码不正确"
|
||||
|
@ -241,7 +237,7 @@ msgid "Unknown project"
|
|||
msgstr "未知项目"
|
||||
|
||||
msgid "Password successfully reset."
|
||||
msgstr "密码重置成功"
|
||||
msgstr "密码重置成功。"
|
||||
|
||||
msgid "Project successfully uploaded"
|
||||
msgstr "项目成功上传"
|
||||
|
@ -253,7 +249,7 @@ msgid "Project successfully deleted"
|
|||
msgstr "项目成功删除"
|
||||
|
||||
msgid "Error deleting project"
|
||||
msgstr ""
|
||||
msgstr "删除项目时出错"
|
||||
|
||||
#, python-format
|
||||
msgid "You have been invited to share your expenses for %(project)s"
|
||||
|
@ -273,20 +269,20 @@ msgid "%(member)s has been added"
|
|||
msgstr "已添加%(member)s"
|
||||
|
||||
msgid "Error activating member"
|
||||
msgstr ""
|
||||
msgstr "激活成员时出错"
|
||||
|
||||
#, python-format
|
||||
msgid "%(name)s is part of this project again"
|
||||
msgstr "%(name)s 已经在项目里了"
|
||||
|
||||
msgid "Error removing member"
|
||||
msgstr ""
|
||||
msgstr "删除成员时出错"
|
||||
|
||||
#, python-format
|
||||
msgid ""
|
||||
"User '%(name)s' has been deactivated. It will still appear in the users "
|
||||
"list until its balance becomes zero."
|
||||
msgstr "用户 '%(name)s'已被暂停,在余额为0之前会继续显示在用户列表里"
|
||||
msgstr "用户 '%(name)s'已被暂停,在余额为0之前会继续显示在用户列表里。"
|
||||
|
||||
#, python-format
|
||||
msgid "User '%(name)s' has been removed"
|
||||
|
@ -300,7 +296,7 @@ msgid "The bill has been added"
|
|||
msgstr "帐单已添加"
|
||||
|
||||
msgid "Error deleting bill"
|
||||
msgstr ""
|
||||
msgstr "删除账单时出错"
|
||||
|
||||
msgid "The bill has been deleted"
|
||||
msgstr "账单已删除"
|
||||
|
@ -308,26 +304,23 @@ msgstr "账单已删除"
|
|||
msgid "The bill has been modified"
|
||||
msgstr "帐单已修改"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Error deleting project history"
|
||||
msgstr "启用项目历史"
|
||||
msgstr "删除项目历史记录时出错"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Deleted project history."
|
||||
msgstr "启用项目历史"
|
||||
msgstr "已删除的项目历史记录。"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Error deleting recorded IP addresses"
|
||||
msgstr "删除已储存的IP地址"
|
||||
msgstr "删除记录的IP地址时出错"
|
||||
|
||||
msgid "Deleted recorded IP addresses in project history."
|
||||
msgstr ""
|
||||
msgstr "删除项目历史记录中的 IP 地址。"
|
||||
|
||||
msgid "Sorry, we were unable to find the page you've asked for."
|
||||
msgstr "对不起,未找到该页面"
|
||||
msgstr "抱歉,我们无法找到您要求的页面。"
|
||||
|
||||
msgid "The best thing to do is probably to get back to the main page."
|
||||
msgstr "最好的办法是返回主页"
|
||||
msgstr "最好的办法是返回主页。"
|
||||
|
||||
msgid "Back to the list"
|
||||
msgstr "返回列表"
|
||||
|
@ -375,26 +368,22 @@ msgid "show"
|
|||
msgstr "显示"
|
||||
|
||||
msgid "The Dashboard is currently deactivated."
|
||||
msgstr "操作面板失效"
|
||||
msgstr "操作面板失效。"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Download Mobile Application"
|
||||
msgstr "手机软件"
|
||||
msgstr "下载移动应用程序"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Get it on"
|
||||
msgstr "进入"
|
||||
msgstr "获取"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Are you sure?"
|
||||
msgstr "确定?"
|
||||
msgstr "是否确定?"
|
||||
|
||||
msgid "Edit project"
|
||||
msgstr "编辑项目"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Delete project"
|
||||
msgstr "编辑项目"
|
||||
msgstr "删除项目"
|
||||
|
||||
msgid "Import JSON"
|
||||
msgstr "导入json文件"
|
||||
|
@ -430,7 +419,7 @@ msgid "Edit the project"
|
|||
msgstr "编辑项目"
|
||||
|
||||
msgid "This will remove all bills and participants in this project!"
|
||||
msgstr ""
|
||||
msgstr "这将删除此项目的所有账单和参与者!"
|
||||
|
||||
msgid "Edit this bill"
|
||||
msgstr "编辑帐单"
|
||||
|
@ -442,10 +431,10 @@ msgid "Everyone"
|
|||
msgstr "每个人"
|
||||
|
||||
msgid "No one"
|
||||
msgstr ""
|
||||
msgstr "无人"
|
||||
|
||||
msgid "More options"
|
||||
msgstr ""
|
||||
msgstr "更多选项"
|
||||
|
||||
msgid "Add participant"
|
||||
msgstr "添加参与人"
|
||||
|
@ -485,11 +474,11 @@ msgstr "历史设置改变"
|
|||
|
||||
#, python-format
|
||||
msgid "Bill %(name)s: %(property_name)s changed from %(before)s to %(after)s"
|
||||
msgstr ""
|
||||
msgstr "账单 %(name)s: %(property_name)s 从 %(before)s 改为 %(after)s"
|
||||
|
||||
#, python-format
|
||||
msgid "Bill %(name)s: %(property_name)s changed to %(after)s"
|
||||
msgstr ""
|
||||
msgstr "账单 %(name)s: %(property_name)s 改为 %(after)s"
|
||||
|
||||
msgid "Confirm Remove IP Adresses"
|
||||
msgstr "确认移除IP地址"
|
||||
|
@ -503,7 +492,6 @@ msgstr ""
|
|||
"你确定要删除此项目里所有的IP地址吗?\n"
|
||||
"项目其他内容不受影响,此操作不可撤回。"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Confirm deletion"
|
||||
msgstr "确认删除"
|
||||
|
||||
|
@ -520,11 +508,11 @@ msgstr "确定删除此项目所有记录?此操作不可撤回。"
|
|||
|
||||
#, python-format
|
||||
msgid "Bill %(name)s: added %(owers_list_str)s to owers list"
|
||||
msgstr ""
|
||||
msgstr "帐单 %(name)s:将 %(owers_list_str)s 添加到所有者列表"
|
||||
|
||||
#, python-format
|
||||
msgid "Bill %(name)s: removed %(owers_list_str)s from owers list"
|
||||
msgstr ""
|
||||
msgstr "账单 %(name)s:从所有者列表中删除了 %(owers_list_str)s"
|
||||
|
||||
#, python-format
|
||||
msgid ""
|
||||
|
@ -590,51 +578,51 @@ msgstr "IP地址记录可在设置里禁用"
|
|||
msgid "From IP"
|
||||
msgstr "从IP"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "Project %(name)s added"
|
||||
msgstr "账目名称"
|
||||
msgstr "项目 %(name)s 已添加"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "Bill %(name)s added"
|
||||
msgstr "帐单已添加"
|
||||
msgstr "帐单 %(name)s 已添加"
|
||||
|
||||
#, python-format
|
||||
msgid "Participant %(name)s added"
|
||||
msgstr ""
|
||||
msgstr "成员 %(name)s 已添加"
|
||||
|
||||
msgid "Project private code changed"
|
||||
msgstr "项目专用码已更改"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "Project renamed to %(new_project_name)s"
|
||||
msgstr "项目的标识符是%(project)s"
|
||||
msgstr "项目的标识符是 %(new_project_name)s"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "Project contact email changed to %(new_email)s"
|
||||
msgstr "项目联系邮箱更改为"
|
||||
msgstr "项目联系邮箱更改为 %(new_email)s"
|
||||
|
||||
msgid "Project settings modified"
|
||||
msgstr "项目设置已修改"
|
||||
|
||||
#, python-format
|
||||
msgid "Participant %(name)s deactivated"
|
||||
msgstr ""
|
||||
msgstr "成员 %(name)s 已停用"
|
||||
|
||||
#, python-format
|
||||
msgid "Participant %(name)s reactivated"
|
||||
msgstr ""
|
||||
msgstr "成员 %(name)s 被重新激活"
|
||||
|
||||
#, python-format
|
||||
msgid "Participant %(name)s renamed to %(new_name)s"
|
||||
msgstr ""
|
||||
msgstr "成员 %(name)s 重命名为 %(new_name)s"
|
||||
|
||||
#, python-format
|
||||
msgid "Bill %(name)s renamed to %(new_description)s"
|
||||
msgstr ""
|
||||
msgstr "账单 %(name)s 更名为 %(new_description)s"
|
||||
|
||||
#, python-format
|
||||
msgid "Participant %(name)s: weight changed from %(old_weight)s to %(new_weight)s"
|
||||
msgstr ""
|
||||
msgstr "成员 %(name)s:权重从%(old_weight)s变为%(new_weight)s"
|
||||
|
||||
msgid "Amount"
|
||||
msgstr "数量"
|
||||
|
@ -643,33 +631,33 @@ msgstr "数量"
|
|||
msgid "Amount in %(currency)s"
|
||||
msgstr "%(currency)s的数量是"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "Bill %(name)s modified"
|
||||
msgstr "帐单已修改"
|
||||
msgstr "帐单 %(name)s 已修改"
|
||||
|
||||
#, python-format
|
||||
msgid "Participant %(name)s modified"
|
||||
msgstr ""
|
||||
msgstr "成员 %(name)s 已修改"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "Bill %(name)s removed"
|
||||
msgstr "用户 '%(name)s'已被移除"
|
||||
msgstr "账单 %(name)s 已被移除"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "Participant %(name)s removed"
|
||||
msgstr "用户 '%(name)s'已被移除"
|
||||
msgstr "用户 %(name)s 已被移除"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "Project %(name)s changed in an unknown way"
|
||||
msgstr "未知的改变"
|
||||
msgstr "项目 %(name)s 以未知方式更改"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "Bill %(name)s changed in an unknown way"
|
||||
msgstr "未知的改变"
|
||||
msgstr "账单 %(name)s 以一种未知的方式更改"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "Participant %(name)s changed in an unknown way"
|
||||
msgstr "未知的改变"
|
||||
msgstr "成员 %(name)s 以未知方式更改"
|
||||
|
||||
msgid "Nothing to list"
|
||||
msgstr "无列表"
|
||||
|
@ -815,7 +803,7 @@ msgid "No bills"
|
|||
msgstr "没有账单"
|
||||
|
||||
msgid "Nothing to list yet."
|
||||
msgstr "没有列表"
|
||||
msgstr "没有列表。"
|
||||
|
||||
msgid "You probably want to"
|
||||
msgstr "你想要"
|
||||
|
|
|
@ -200,15 +200,21 @@ def admin():
|
|||
def authenticate(project_id=None):
|
||||
"""Authentication form"""
|
||||
form = AuthenticationForm()
|
||||
# Try to get project_id from token first
|
||||
token = request.args.get("token")
|
||||
if token:
|
||||
project_id = Project.verify_token(token, token_type="non_timed_token")
|
||||
token_auth = True
|
||||
else:
|
||||
|
||||
if not form.id.data and request.args.get("project_id"):
|
||||
form.id.data = request.args["project_id"]
|
||||
project_id = form.id.data
|
||||
# Try to get project_id from token first
|
||||
token = request.args.get("token")
|
||||
if token:
|
||||
verified_project_id = Project.verify_token(
|
||||
token, token_type="auth", project_id=project_id
|
||||
)
|
||||
if verified_project_id == project_id:
|
||||
token_auth = True
|
||||
else:
|
||||
project_id = None
|
||||
else:
|
||||
token_auth = False
|
||||
if project_id is None:
|
||||
# User doesn't provide project identifier or a valid token
|
||||
|
@ -389,7 +395,7 @@ def reset_password():
|
|||
return render_template(
|
||||
"reset_password.html", form=form, error=_("No token provided")
|
||||
)
|
||||
project_id = Project.verify_token(token)
|
||||
project_id = Project.verify_token(token, token_type="reset")
|
||||
if not project_id:
|
||||
return render_template(
|
||||
"reset_password.html", form=form, error=_("Invalid token")
|
||||
|
|
|
@ -33,6 +33,7 @@ install_requires =
|
|||
Flask-Migrate>=2.5.3,<4 # Not following semantic versioning (e.g. https://github.com/miguelgrinberg/flask-migrate/commit/1af28ba273de6c88544623b8dc02dd539340294b)
|
||||
Flask-RESTful>=0.3.9,<1
|
||||
Flask-SQLAlchemy>=2.4,<3
|
||||
Flask-Talisman>=0.8,<1
|
||||
Flask-WTF>=0.14.3,<1
|
||||
WTForms>=2.3.1,<2.4
|
||||
Flask>=2,<3
|
||||
|
|
Loading…
Reference in a new issue