Merge pull request #181 from SamR1/smtp-optional-and-cli-refacto
make SMTP provider optional
This commit is contained in:
commit
a5e5dcda6c
@ -1,4 +1,4 @@
|
||||
FROM python:3.9
|
||||
FROM python:3.10
|
||||
|
||||
# set working directory
|
||||
RUN mkdir -p /usr/src/app
|
||||
|
24
Makefile
24
Makefile
@ -89,11 +89,11 @@ html:
|
||||
|
||||
install-db:
|
||||
psql -U postgres -f db/create.sql
|
||||
$(FLASK) db upgrade --directory $(MIGRATIONS)
|
||||
$(FTCLI) db upgrade
|
||||
|
||||
init-db:
|
||||
$(FLASK) drop-db
|
||||
$(FLASK) db upgrade --directory $(MIGRATIONS)
|
||||
$(FTCLI) db drop
|
||||
$(FTCLI) db upgrade
|
||||
|
||||
install: install-client install-python
|
||||
|
||||
@ -164,7 +164,8 @@ serve-python-dev:
|
||||
$(FLASK) run --with-threads -h $(HOST) -p $(PORT) --cert=adhoc
|
||||
|
||||
set-admin:
|
||||
$(FLASK) users set-admin $(USERNAME)
|
||||
echo "Deprecated command, will be removed in a next version. Use 'user-set-admin' instead."
|
||||
$(FTCLI) users update $(USERNAME) --set-admin true
|
||||
|
||||
test-e2e:
|
||||
$(PYTEST) e2e --driver firefox $(PYTEST_ARGS)
|
||||
@ -185,4 +186,17 @@ type-check:
|
||||
$(MYPY) fittrackee
|
||||
|
||||
upgrade-db:
|
||||
$(FLASK) db upgrade --directory $(MIGRATIONS)
|
||||
$(FTCLI) db upgrade
|
||||
|
||||
user-activate:
|
||||
$(FTCLI) users update $(USERNAME) --activate
|
||||
|
||||
user-reset-password:
|
||||
$(FTCLI) users update $(USERNAME) --reset-password
|
||||
|
||||
ADMIN := true
|
||||
user-set-admin:
|
||||
$(FTCLI) users update $(USERNAME) --set-admin $(ADMIN)
|
||||
|
||||
user-update-email:
|
||||
$(FTCLI) users update $(USERNAME) --update-email $(EMAIL)
|
||||
|
@ -26,6 +26,7 @@ PYTEST = $(VENV)/bin/py.test -c pyproject.toml -W ignore::DeprecationWarning
|
||||
GUNICORN = $(VENV)/bin/gunicorn
|
||||
BLACK = $(VENV)/bin/black
|
||||
MYPY = $(VENV)/bin/mypy
|
||||
FTCLI = $(VENV)/bin/ftcli
|
||||
|
||||
# Node env
|
||||
NODE_MODULES = $(PWD)/fittrackee_client/node_modules
|
||||
|
@ -4,5 +4,5 @@ cd /usr/src/app
|
||||
|
||||
source .env.docker
|
||||
|
||||
flask drop-db
|
||||
flask db upgrade --directory fittrackee/migrations
|
||||
ftcli db drop
|
||||
ftcli db upgrade
|
@ -4,4 +4,4 @@ cd /usr/src/app
|
||||
|
||||
source .env.docker
|
||||
|
||||
flask users set-admin $1
|
||||
ftcli users update $1 --set-admin true
|
||||
|
67
docs/_sources/cli.rst.txt
Normal file
67
docs/_sources/cli.rst.txt
Normal file
@ -0,0 +1,67 @@
|
||||
Command line interface
|
||||
######################
|
||||
|
||||
A command line interface (CLI) is available to manage database and users.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ ftcli
|
||||
Usage: ftcli [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
FitTrackee Command Line Interface
|
||||
|
||||
Options:
|
||||
--help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
db Manage database.
|
||||
users Manage users.
|
||||
|
||||
.. warning::
|
||||
| The following commands are now deprecated and will be removed in a next version:
|
||||
| - ``fittrackee_set_admin``
|
||||
| - ``fittrackee_upgrade_db``
|
||||
|
||||
|
||||
Database
|
||||
~~~~~~~~
|
||||
|
||||
``ftcli db upgrade``
|
||||
""""""""""""""""""""
|
||||
.. versionadded:: 0.6.5
|
||||
|
||||
Apply migrations.
|
||||
|
||||
|
||||
``ftcli db drop``
|
||||
"""""""""""""""""
|
||||
.. versionadded:: 0.6.5
|
||||
|
||||
Empty database and delete uploaded files, only on development environments.
|
||||
|
||||
|
||||
|
||||
Users
|
||||
~~~~~
|
||||
|
||||
``ftcli users update``
|
||||
""""""""""""""""""""""
|
||||
.. versionadded:: 0.6.5
|
||||
|
||||
Modify a user account (admin rights, active status, email and password).
|
||||
|
||||
.. cssclass:: table-bordered
|
||||
.. list-table::
|
||||
:widths: 25 50
|
||||
:header-rows: 1
|
||||
|
||||
* - Options
|
||||
- Description
|
||||
* - ``--set-admin BOOLEAN``
|
||||
- Add/remove admin rights (when adding admin rights, it also activates user account if not active).
|
||||
* - ``--activate``
|
||||
- Activate user account.
|
||||
* - ``--reset-password``
|
||||
- Reset user password (a new password will be displayed).
|
||||
* - ``--update-email EMAIL``
|
||||
- Update user email.
|
@ -60,10 +60,11 @@ Workouts
|
||||
Account & preferences
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
- A user can create, update and deleted his account.
|
||||
- After registration, the user account is inactive and an email with confirmation instructions is sent to activate it. A user with an inactive account cannot log in. (*new in 0.6.0*)
|
||||
- After registration, the user account is inactive and an email with confirmation instructions is sent to activate it.
|
||||
A user with an inactive account cannot log in. (*new in 0.6.0*)
|
||||
|
||||
.. note::
|
||||
The command line to add admin rights activates the account if it is inactive.
|
||||
In case email sending is not configured, a `command line <cli.html#ftcli-users-update>`__ allows to activate users account.
|
||||
|
||||
- A user can set language, timezone and first day of week.
|
||||
- A user can reset his password (*new in 0.3.0*)
|
||||
@ -97,6 +98,9 @@ Administration
|
||||
.. warning::
|
||||
Updating server configuration may be necessary to handle large files (like `nginx <https://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size>`_ for instance).
|
||||
|
||||
.. note::
|
||||
If email sending is disabled, a warning is displayed.
|
||||
|
||||
|
||||
- **Users**
|
||||
|
||||
|
@ -34,6 +34,7 @@ Table of contents
|
||||
|
||||
features
|
||||
installation
|
||||
cli
|
||||
api/index
|
||||
troubleshooting/index
|
||||
changelog
|
||||
|
@ -22,9 +22,8 @@ Prerequisites
|
||||
|
||||
- Python 3.7+
|
||||
- PostgreSQL database (10+)
|
||||
- SMTP provider
|
||||
- Redis for task queue (to send emails)
|
||||
- API key from `Dark Sky <https://darksky.net/dev>`__ [not mandatory]
|
||||
- SMTP provider and Redis for task queue (if email sending is enabled)
|
||||
- API key from `Dark Sky <https://darksky.net/dev>`__ (not mandatory)
|
||||
- `Poetry <https://poetry.eustace.io>`__ (for installation from sources only)
|
||||
- `Yarn <https://yarnpkg.com>`__ (for development only)
|
||||
- Docker and Docker Compose (for development or evaluation purposes)
|
||||
@ -133,6 +132,13 @@ deployment method.
|
||||
|
||||
Email URL with credentials, see `Emails <installation.html#emails>`__.
|
||||
|
||||
.. versionchanged:: 0.6.5
|
||||
|
||||
:default: empty string
|
||||
|
||||
.. danger::
|
||||
If the email URL is empty, email sending will be disabled.
|
||||
|
||||
.. warning::
|
||||
If the email URL is invalid, the application may not start.
|
||||
|
||||
@ -214,7 +220,7 @@ To send emails, a valid ``EMAIL_URL`` must be provided:
|
||||
| - If the email URL is invalid, the application may not start.
|
||||
| - Sending emails with Office365 may not work if SMTP auth is disabled.
|
||||
|
||||
.. versionadded:: 0.5.3
|
||||
.. versionchanged:: 0.5.3
|
||||
|
||||
| Credentials can be omitted: ``smtp://smtp.example.com:25``.
|
||||
| If ``:<port>`` is omitted, the port defaults to 25.
|
||||
@ -229,6 +235,11 @@ Emails sent by FitTrackee are:
|
||||
- email change (to old and new email adresses)
|
||||
- password change
|
||||
|
||||
.. versionchanged:: 0.6.5
|
||||
|
||||
| For single-user instance, it is possible to disable email sending with an empty ``EMAIL_URL`` (in this case, no need to start dramatiq workers).
|
||||
| A `CLI <cli.html#ftcli-users-update>`__ is available to activate account and modify email and password.
|
||||
|
||||
|
||||
Map tile server
|
||||
^^^^^^^^^^^^^^^
|
||||
@ -288,7 +299,7 @@ For instance, copy and update ``.env`` file from ``.env.example`` and source the
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ fittrackee_upgrade_db
|
||||
$ ftcli db upgrade
|
||||
|
||||
- Start the application
|
||||
|
||||
@ -296,7 +307,7 @@ For instance, copy and update ``.env`` file from ``.env.example`` and source the
|
||||
|
||||
$ fittrackee
|
||||
|
||||
- Start task queue workers
|
||||
- Start task queue workers if email sending is enabled.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@ -311,7 +322,7 @@ For instance, copy and update ``.env`` file from ``.env.example`` and source the
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ fittrackee_set_admin <username>
|
||||
$ ftcli users update <username> --set-admin true
|
||||
|
||||
.. note::
|
||||
If the user account is inactive, it activates it.
|
||||
@ -373,7 +384,7 @@ Dev environment
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ make set-admin USERNAME=<username>
|
||||
$ make user-set-admin USERNAME=<username>
|
||||
|
||||
.. note::
|
||||
If the user account is inactive, it activates it.
|
||||
@ -415,13 +426,16 @@ Production environment
|
||||
|
||||
$ make run
|
||||
|
||||
.. note::
|
||||
If email sending is disabled: ``$ make run-server``
|
||||
|
||||
- Open http://localhost:5000 and register
|
||||
|
||||
- To set admin rights to the newly created account, use the following command line:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ make set-admin USERNAME=<username>
|
||||
$ make user-set-admin USERNAME=<username>
|
||||
|
||||
.. note::
|
||||
If the user account is inactive, it activates it.
|
||||
@ -457,9 +471,9 @@ From PyPI
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ fittrackee_upgrade_db
|
||||
$ ftcli db upgrade
|
||||
|
||||
- Restart the application and task queue workers.
|
||||
- Restart the application and task queue workers (if email sending is enabled).
|
||||
|
||||
|
||||
From sources
|
||||
@ -536,6 +550,8 @@ Prod environment
|
||||
|
||||
$ make run
|
||||
|
||||
.. note::
|
||||
If email sending is disabled: ``$ make run-server``
|
||||
|
||||
Deployment
|
||||
~~~~~~~~~~
|
||||
|
@ -61,6 +61,7 @@
|
||||
aria-labelledby="dLabelGlobalToc"><ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="../features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../installation.html">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1 current"><a class="reference internal" href="index.html">API documentation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../troubleshooting/index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../changelog.html">Change log</a></li>
|
||||
@ -237,6 +238,7 @@ character “_” allowed</p></li>
|
||||
<dt class="sig sig-object http" id="post--api-auth-account-resend-confirmation">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/auth/account/resend-confirmation</span></span><a class="headerlink" href="#post--api-auth-account-resend-confirmation" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>resend email with instructions to confirm account</p>
|
||||
<p>If email sending is disabled, this endpoint is not available</p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">POST</span> <span class="nn">/api/auth/account/resend-confirmation</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
@ -262,6 +264,7 @@ character “_” allowed</p></li>
|
||||
<dd class="field-even"><ul class="simple">
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.1">200 OK</a></span> – confirmation email resent</p></li>
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1">400 Bad Request</a></span> – invalid payload</p></li>
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5">404 Not Found</a></span> – the requested URL was not found on the server</p></li>
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.1">500 Internal Server Error</a></span> – error, please try again or contact the administrator</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
@ -857,6 +860,7 @@ character “_” allowed</p></li>
|
||||
<dt class="sig sig-object http" id="post--api-auth-password-reset-request">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/auth/password/reset-request</span></span><a class="headerlink" href="#post--api-auth-password-reset-request" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>handle password reset request</p>
|
||||
<p>If email sending is disabled, this endpoint is not available</p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">POST</span> <span class="nn">/api/auth/password/reset-request</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
@ -882,6 +886,7 @@ character “_” allowed</p></li>
|
||||
<dd class="field-even"><ul class="simple">
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.1">200 OK</a></span> – password reset request processed</p></li>
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1">400 Bad Request</a></span> – invalid payload</p></li>
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5">404 Not Found</a></span> – the requested URL was not found on the server</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
@ -891,7 +896,7 @@ character “_” allowed</p></li>
|
||||
<dt class="sig sig-object http" id="patch--api-auth-profile-edit-account">
|
||||
<span class="sig-name descname"><span class="pre">PATCH</span> </span><span class="sig-name descname"><span class="pre">/api/auth/profile/edit/account</span></span><a class="headerlink" href="#patch--api-auth-profile-edit-account" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>update authenticated user email and password</p>
|
||||
<p>It sends emails:</p>
|
||||
<p>It sends emails if sending is enabled:</p>
|
||||
<ul class="simple">
|
||||
<li><p>Password change</p></li>
|
||||
<li><p>Email change:</p>
|
||||
@ -1021,6 +1026,7 @@ character “_” allowed</p></li>
|
||||
<dt class="sig sig-object http" id="post--api-auth-password-update">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/auth/password/update</span></span><a class="headerlink" href="#post--api-auth-password-update" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>update user password after password reset request</p>
|
||||
<p>It sends emails if sending is enabled</p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">POST</span> <span class="nn">/api/auth/password/update</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
|
@ -61,6 +61,7 @@
|
||||
aria-labelledby="dLabelGlobalToc"><ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="../features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../installation.html">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1 current"><a class="reference internal" href="index.html">API documentation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../troubleshooting/index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../changelog.html">Change log</a></li>
|
||||
@ -144,6 +145,7 @@
|
||||
<span class="w"> </span><span class="nt">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"admin_contact"</span><span class="p">:</span><span class="w"> </span><span class="s2">"admin@example.com"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"gpx_limit_import"</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"is_email_sending_enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"is_registration_enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"max_single_file_size"</span><span class="p">:</span><span class="w"> </span><span class="mi">1048576</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"max_users"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"></span>
|
||||
@ -183,6 +185,7 @@
|
||||
<span class="w"> </span><span class="nt">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"admin_contact"</span><span class="p">:</span><span class="w"> </span><span class="s2">"admin@example.com"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"gpx_limit_import"</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"is_email_sending_enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"is_registration_enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"max_single_file_size"</span><span class="p">:</span><span class="w"> </span><span class="mi">1048576</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"max_users"</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w"></span>
|
||||
|
@ -17,7 +17,7 @@
|
||||
<link rel="index" title="Index" href="../genindex.html" />
|
||||
<link rel="search" title="Search" href="../search.html" />
|
||||
<link rel="next" title="Authentication" href="auth.html" />
|
||||
<link rel="prev" title="Installation" href="../installation.html" />
|
||||
<link rel="prev" title="Command line interface" href="../cli.html" />
|
||||
<meta charset='utf-8'>
|
||||
<meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1'>
|
||||
@ -61,6 +61,7 @@
|
||||
aria-labelledby="dLabelGlobalToc"><ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="../features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../installation.html">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1 current"><a class="current reference internal" href="#">API documentation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../troubleshooting/index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../changelog.html">Change log</a></li>
|
||||
@ -87,7 +88,7 @@
|
||||
|
||||
|
||||
<li>
|
||||
<a href="../installation.html" title="Previous Chapter: Installation"><span class="glyphicon glyphicon-chevron-left visible-sm"></span><span class="hidden-sm hidden-tablet">« Installation</span>
|
||||
<a href="../cli.html" title="Previous Chapter: Command line interface"><span class="glyphicon glyphicon-chevron-left visible-sm"></span><span class="hidden-sm hidden-tablet">« Command line ...</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
|
@ -61,6 +61,7 @@
|
||||
aria-labelledby="dLabelGlobalToc"><ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="../features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../installation.html">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1 current"><a class="reference internal" href="index.html">API documentation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../troubleshooting/index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../changelog.html">Change log</a></li>
|
||||
|
@ -61,6 +61,7 @@
|
||||
aria-labelledby="dLabelGlobalToc"><ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="../features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../installation.html">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1 current"><a class="reference internal" href="index.html">API documentation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../troubleshooting/index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../changelog.html">Change log</a></li>
|
||||
|
@ -61,6 +61,7 @@
|
||||
aria-labelledby="dLabelGlobalToc"><ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="../features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../installation.html">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1 current"><a class="reference internal" href="index.html">API documentation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../troubleshooting/index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../changelog.html">Change log</a></li>
|
||||
|
@ -61,6 +61,7 @@
|
||||
aria-labelledby="dLabelGlobalToc"><ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="../features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../installation.html">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1 current"><a class="reference internal" href="index.html">API documentation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../troubleshooting/index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../changelog.html">Change log</a></li>
|
||||
@ -429,8 +430,9 @@ details.</p>
|
||||
<dd><p>Update user account</p>
|
||||
<ul class="simple">
|
||||
<li><p>add/remove admin rights (regardless user account status)</p></li>
|
||||
<li><p>reset password (and send email to update user password)</p></li>
|
||||
<li><p>update user email (and send email to update user password)</p></li>
|
||||
<li><p>reset password (and send email to update user password,
|
||||
if sending enabled)</p></li>
|
||||
<li><p>update user email (and send email to new user email, if sending enabled)</p></li>
|
||||
<li><p>activate account for an inactive user</p></li>
|
||||
</ul>
|
||||
<p>Only user with admin rights can modify another user</p>
|
||||
|
@ -61,6 +61,7 @@
|
||||
aria-labelledby="dLabelGlobalToc"><ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="../features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../installation.html">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1 current"><a class="reference internal" href="index.html">API documentation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../troubleshooting/index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../changelog.html">Change log</a></li>
|
||||
|
@ -60,6 +60,7 @@
|
||||
aria-labelledby="dLabelGlobalToc"><ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="api/index.html">API documentation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="troubleshooting/index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1 current"><a class="current reference internal" href="#">Change log</a></li>
|
||||
|
236
docs/cli.html
Normal file
236
docs/cli.html
Normal file
@ -0,0 +1,236 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
||||
|
||||
<title>Command line interface — FitTrackee 0.6.4
|
||||
documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/bootstrap-sphinx.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css" />
|
||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||
<script src="_static/jquery.js"></script>
|
||||
<script src="_static/underscore.js"></script>
|
||||
<script src="_static/doctools.js"></script>
|
||||
<link rel="index" title="Index" href="genindex.html" />
|
||||
<link rel="search" title="Search" href="search.html" />
|
||||
<link rel="next" title="API documentation" href="api/index.html" />
|
||||
<link rel="prev" title="Installation" href="installation.html" />
|
||||
<meta charset='utf-8'>
|
||||
<meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1'>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<script type="text/javascript" src="_static/js/jquery-1.12.4.min.js"></script>
|
||||
<script type="text/javascript" src="_static/js/jquery-fix.js"></script>
|
||||
<script type="text/javascript" src="_static/bootstrap-3.4.1/js/bootstrap.min.js"></script>
|
||||
<script type="text/javascript" src="_static/bootstrap-sphinx.js"></script>
|
||||
|
||||
</head><body>
|
||||
|
||||
<div id="navbar" class="navbar navbar-default navbar-fixed-top">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<!-- .btn-navbar is used as the toggle for collapsed navbar content -->
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".nav-collapse">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="index.html">
|
||||
FitTrackee</a>
|
||||
<span class="navbar-text navbar-version pull-left"><b>0.6.4
|
||||
</b></span>
|
||||
</div>
|
||||
|
||||
<div class="collapse navbar-collapse nav-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
|
||||
<li><a href="https://github.com/SamR1/FitTrackee">GitHub</a></li>
|
||||
|
||||
|
||||
<li class="dropdown globaltoc-container">
|
||||
<a role="button"
|
||||
id="dLabelGlobalToc"
|
||||
data-toggle="dropdown"
|
||||
data-target="#"
|
||||
href="index.html">Docs <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu globaltoc"
|
||||
role="menu"
|
||||
aria-labelledby="dLabelGlobalToc"><ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
|
||||
<li class="toctree-l1 current"><a class="current reference internal" href="#">Command line interface</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="api/index.html">API documentation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="troubleshooting/index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="changelog.html">Change log</a></li>
|
||||
</ul>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="dropdown">
|
||||
<a role="button"
|
||||
id="dLabelLocalToc"
|
||||
data-toggle="dropdown"
|
||||
data-target="#"
|
||||
href="#">Page <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu localtoc"
|
||||
role="menu"
|
||||
aria-labelledby="dLabelLocalToc"><ul>
|
||||
<li><a class="reference internal" href="#">Command line interface</a><ul>
|
||||
<li><a class="reference internal" href="#database">Database</a><ul>
|
||||
<li><a class="reference internal" href="#ftcli-db-upgrade"><code class="docutils literal notranslate"><span class="pre">ftcli</span> <span class="pre">db</span> <span class="pre">upgrade</span></code></a></li>
|
||||
<li><a class="reference internal" href="#ftcli-db-drop"><code class="docutils literal notranslate"><span class="pre">ftcli</span> <span class="pre">db</span> <span class="pre">drop</span></code></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#users">Users</a><ul>
|
||||
<li><a class="reference internal" href="#ftcli-users-update"><code class="docutils literal notranslate"><span class="pre">ftcli</span> <span class="pre">users</span> <span class="pre">update</span></code></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li>
|
||||
<a href="installation.html" title="Previous Chapter: Installation"><span class="glyphicon glyphicon-chevron-left visible-sm"></span><span class="hidden-sm hidden-tablet">« Installation</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="api/index.html" title="Next Chapter: API documentation"><span class="glyphicon glyphicon-chevron-right visible-sm"></span><span class="hidden-sm hidden-tablet">API documentation »</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="hidden-sm">
|
||||
<div id="sourcelink">
|
||||
<a href="_sources/cli.rst.txt"
|
||||
rel="nofollow">Source</a>
|
||||
</div></li>
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
<form class="navbar-form navbar-right" action="search.html" method="get">
|
||||
<div class="form-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search" />
|
||||
</div>
|
||||
<input type="hidden" name="check_keywords" value="yes" />
|
||||
<input type="hidden" name="area" value="default" />
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="body col-md-12 content" role="main">
|
||||
|
||||
<section id="command-line-interface">
|
||||
<h1>Command line interface<a class="headerlink" href="#command-line-interface" title="Permalink to this headline">¶</a></h1>
|
||||
<p>A command line interface (CLI) is available to manage database and users.</p>
|
||||
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ ftcli
|
||||
Usage: ftcli <span class="o">[</span>OPTIONS<span class="o">]</span> COMMAND <span class="o">[</span>ARGS<span class="o">]</span>...
|
||||
|
||||
FitTrackee Command Line Interface
|
||||
|
||||
Options:
|
||||
--help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
db Manage database.
|
||||
users Manage users.
|
||||
</pre></div>
|
||||
</div>
|
||||
<div class="admonition warning">
|
||||
<p class="admonition-title">Warning</p>
|
||||
<div class="line-block">
|
||||
<div class="line">The following commands are now deprecated and will be removed in a next version:</div>
|
||||
<div class="line">- <code class="docutils literal notranslate"><span class="pre">fittrackee_set_admin</span></code></div>
|
||||
<div class="line">- <code class="docutils literal notranslate"><span class="pre">fittrackee_upgrade_db</span></code></div>
|
||||
</div>
|
||||
</div>
|
||||
<section id="database">
|
||||
<h2>Database<a class="headerlink" href="#database" title="Permalink to this headline">¶</a></h2>
|
||||
<section id="ftcli-db-upgrade">
|
||||
<h3><code class="docutils literal notranslate"><span class="pre">ftcli</span> <span class="pre">db</span> <span class="pre">upgrade</span></code><a class="headerlink" href="#ftcli-db-upgrade" title="Permalink to this headline">¶</a></h3>
|
||||
<div class="versionadded">
|
||||
<p><span class="versionmodified added">New in version 0.6.5.</span></p>
|
||||
</div>
|
||||
<p>Apply migrations.</p>
|
||||
</section>
|
||||
<section id="ftcli-db-drop">
|
||||
<h3><code class="docutils literal notranslate"><span class="pre">ftcli</span> <span class="pre">db</span> <span class="pre">drop</span></code><a class="headerlink" href="#ftcli-db-drop" title="Permalink to this headline">¶</a></h3>
|
||||
<div class="versionadded">
|
||||
<p><span class="versionmodified added">New in version 0.6.5.</span></p>
|
||||
</div>
|
||||
<p>Empty database and delete uploaded files, only on development environments.</p>
|
||||
</section>
|
||||
</section>
|
||||
<section id="users">
|
||||
<h2>Users<a class="headerlink" href="#users" title="Permalink to this headline">¶</a></h2>
|
||||
<section id="ftcli-users-update">
|
||||
<h3><code class="docutils literal notranslate"><span class="pre">ftcli</span> <span class="pre">users</span> <span class="pre">update</span></code><a class="headerlink" href="#ftcli-users-update" title="Permalink to this headline">¶</a></h3>
|
||||
<div class="versionadded">
|
||||
<p><span class="versionmodified added">New in version 0.6.5.</span></p>
|
||||
</div>
|
||||
<p>Modify a user account (admin rights, active status, email and password).</p>
|
||||
<table class="colwidths-given table-bordered docutils align-default">
|
||||
<colgroup>
|
||||
<col style="width: 33%" />
|
||||
<col style="width: 67%" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr class="row-odd"><th class="head"><p>Options</p></th>
|
||||
<th class="head"><p>Description</p></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="row-even"><td><p><code class="docutils literal notranslate"><span class="pre">--set-admin</span> <span class="pre">BOOLEAN</span></code></p></td>
|
||||
<td><p>Add/remove admin rights (when adding admin rights, it also activates user account if not active).</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p><code class="docutils literal notranslate"><span class="pre">--activate</span></code></p></td>
|
||||
<td><p>Activate user account.</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p><code class="docutils literal notranslate"><span class="pre">--reset-password</span></code></p></td>
|
||||
<td><p>Reset user password (a new password will be displayed).</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p><code class="docutils literal notranslate"><span class="pre">--update-email</span> <span class="pre">EMAIL</span></code></p></td>
|
||||
<td><p>Update user email.</p></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p class="pull-right">
|
||||
<a href="#">Back to top</a>
|
||||
|
||||
</p>
|
||||
<p>
|
||||
© Copyright 2018 - 2022, SamR1.<br/>
|
||||
Created using <a href="http://sphinx-doc.org/">Sphinx</a> 4.5.0.<br/>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
@ -61,6 +61,7 @@
|
||||
aria-labelledby="dLabelGlobalToc"><ul class="current">
|
||||
<li class="toctree-l1 current"><a class="current reference internal" href="#">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="api/index.html">API documentation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="troubleshooting/index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="changelog.html">Change log</a></li>
|
||||
@ -233,11 +234,12 @@
|
||||
<h2>Account & preferences<a class="headerlink" href="#account-preferences" title="Permalink to this headline">¶</a></h2>
|
||||
<ul class="simple">
|
||||
<li><p>A user can create, update and deleted his account.</p></li>
|
||||
<li><p>After registration, the user account is inactive and an email with confirmation instructions is sent to activate it. A user with an inactive account cannot log in. (<em>new in 0.6.0</em>)</p></li>
|
||||
<li><p>After registration, the user account is inactive and an email with confirmation instructions is sent to activate it.
|
||||
A user with an inactive account cannot log in. (<em>new in 0.6.0</em>)</p></li>
|
||||
</ul>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>The command line to add admin rights activates the account if it is inactive.</p>
|
||||
<p>In case email sending is not configured, a <a class="reference external" href="cli.html#ftcli-users-update">command line</a> allows to activate users account.</p>
|
||||
</div>
|
||||
<ul class="simple">
|
||||
<li><p>A user can set language, timezone and first day of week.</p></li>
|
||||
@ -280,6 +282,10 @@
|
||||
<p class="admonition-title">Warning</p>
|
||||
<p>Updating server configuration may be necessary to handle large files (like <a class="reference external" href="https://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size">nginx</a> for instance).</p>
|
||||
</div>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>If email sending is disabled, a warning is displayed.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li><p><strong>Users</strong></p>
|
||||
<ul class="simple">
|
||||
|
@ -58,6 +58,7 @@
|
||||
aria-labelledby="dLabelGlobalToc"><ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="api/index.html">API documentation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="troubleshooting/index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="changelog.html">Change log</a></li>
|
||||
|
@ -65,6 +65,7 @@
|
||||
aria-labelledby="dLabelGlobalToc"><ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="api/index.html">API documentation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="troubleshooting/index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="changelog.html">Change log</a></li>
|
||||
|
@ -60,6 +60,7 @@
|
||||
aria-labelledby="dLabelGlobalToc"><ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="api/index.html">API documentation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="troubleshooting/index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="changelog.html">Change log</a></li>
|
||||
@ -153,6 +154,7 @@ Map</a>.</div>
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="api/index.html">API documentation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="troubleshooting/index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="changelog.html">Change log</a></li>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<script src="_static/doctools.js"></script>
|
||||
<link rel="index" title="Index" href="genindex.html" />
|
||||
<link rel="search" title="Search" href="search.html" />
|
||||
<link rel="next" title="API documentation" href="api/index.html" />
|
||||
<link rel="next" title="Command line interface" href="cli.html" />
|
||||
<link rel="prev" title="Features" href="features.html" />
|
||||
<meta charset='utf-8'>
|
||||
<meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'>
|
||||
@ -61,6 +61,7 @@
|
||||
aria-labelledby="dLabelGlobalToc"><ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="features.html">Features</a></li>
|
||||
<li class="toctree-l1 current"><a class="current reference internal" href="#">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="api/index.html">API documentation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="troubleshooting/index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="changelog.html">Change log</a></li>
|
||||
@ -123,7 +124,7 @@
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="api/index.html" title="Next Chapter: API documentation"><span class="glyphicon glyphicon-chevron-right visible-sm"></span><span class="hidden-sm hidden-tablet">API documentation »</span>
|
||||
<a href="cli.html" title="Next Chapter: Command line interface"><span class="glyphicon glyphicon-chevron-right visible-sm"></span><span class="hidden-sm hidden-tablet">Command line ... »</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@ -191,9 +192,8 @@
|
||||
<ul class="simple">
|
||||
<li><p>Python 3.7+</p></li>
|
||||
<li><p>PostgreSQL database (10+)</p></li>
|
||||
<li><p>SMTP provider</p></li>
|
||||
<li><p>Redis for task queue (to send emails)</p></li>
|
||||
<li><p>API key from <a class="reference external" href="https://darksky.net/dev">Dark Sky</a> [not mandatory]</p></li>
|
||||
<li><p>SMTP provider and Redis for task queue (if email sending is enabled)</p></li>
|
||||
<li><p>API key from <a class="reference external" href="https://darksky.net/dev">Dark Sky</a> (not mandatory)</p></li>
|
||||
<li><p><a class="reference external" href="https://poetry.eustace.io">Poetry</a> (for installation from sources only)</p></li>
|
||||
<li><p><a class="reference external" href="https://yarnpkg.com">Yarn</a> (for development only)</p></li>
|
||||
<li><p>Docker and Docker Compose (for development or evaluation purposes)</p></li>
|
||||
@ -352,6 +352,18 @@ see <a class="reference external" href="https://docs.sqlalchemy.org/en/13/core/p
|
||||
<p><span class="versionmodified added">New in version 0.3.0.</span></p>
|
||||
</div>
|
||||
<p>Email URL with credentials, see <a class="reference external" href="installation.html#emails">Emails</a>.</p>
|
||||
<div class="versionchanged">
|
||||
<p><span class="versionmodified changed">Changed in version 0.6.5.</span></p>
|
||||
</div>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Default</dt>
|
||||
<dd class="field-odd"><p>empty string</p>
|
||||
</dd>
|
||||
</dl>
|
||||
<div class="admonition danger">
|
||||
<p class="admonition-title">Danger</p>
|
||||
<p>If the email URL is empty, email sending will be disabled.</p>
|
||||
</div>
|
||||
<div class="admonition warning">
|
||||
<p class="admonition-title">Warning</p>
|
||||
<p>If the email URL is invalid, the application may not start.</p>
|
||||
@ -468,8 +480,8 @@ see <a class="reference external" href="https://docs.sqlalchemy.org/en/13/core/p
|
||||
<div class="line">- Sending emails with Office365 may not work if SMTP auth is disabled.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="versionadded">
|
||||
<p><span class="versionmodified added">New in version 0.5.3.</span></p>
|
||||
<div class="versionchanged">
|
||||
<p><span class="versionmodified changed">Changed in version 0.5.3.</span></p>
|
||||
</div>
|
||||
<div class="line-block">
|
||||
<div class="line">Credentials can be omitted: <code class="docutils literal notranslate"><span class="pre">smtp://smtp.example.com:25</span></code>.</div>
|
||||
@ -488,6 +500,13 @@ see <a class="reference external" href="https://docs.sqlalchemy.org/en/13/core/p
|
||||
<li><p>email change (to old and new email adresses)</p></li>
|
||||
<li><p>password change</p></li>
|
||||
</ul>
|
||||
<div class="versionchanged">
|
||||
<p><span class="versionmodified changed">Changed in version 0.6.5.</span></p>
|
||||
</div>
|
||||
<div class="line-block">
|
||||
<div class="line">For single-user instance, it is possible to disable email sending with an empty <code class="docutils literal notranslate"><span class="pre">EMAIL_URL</span></code> (in this case, no need to start dramatiq workers).</div>
|
||||
<div class="line">A <a class="reference external" href="cli.html#ftcli-users-update">CLI</a> is available to activate account and modify email and password.</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="map-tile-server">
|
||||
<h3>Map tile server<a class="headerlink" href="#map-tile-server" title="Permalink to this headline">¶</a></h3>
|
||||
@ -552,7 +571,7 @@ $ <span class="nb">source</span> .env
|
||||
<ul class="simple">
|
||||
<li><p>Initialize database schema</p></li>
|
||||
</ul>
|
||||
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ fittrackee_upgrade_db
|
||||
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ ftcli db upgrade
|
||||
</pre></div>
|
||||
</div>
|
||||
<ul class="simple">
|
||||
@ -562,7 +581,7 @@ $ <span class="nb">source</span> .env
|
||||
</pre></div>
|
||||
</div>
|
||||
<ul class="simple">
|
||||
<li><p>Start task queue workers</p></li>
|
||||
<li><p>Start task queue workers if email sending is enabled.</p></li>
|
||||
</ul>
|
||||
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ fittrackee_worker --processes <span class="m">2</span>
|
||||
</pre></div>
|
||||
@ -577,7 +596,7 @@ $ <span class="nb">source</span> .env
|
||||
<li><p>Open <a class="reference external" href="http://localhost:3000">http://localhost:3000</a> and register</p></li>
|
||||
<li><p>To set admin rights to the newly created account, use the following command line:</p></li>
|
||||
</ul>
|
||||
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ fittrackee_set_admin <username>
|
||||
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ ftcli users update <username> --set-admin <span class="nb">true</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<div class="admonition note">
|
||||
@ -638,7 +657,7 @@ $ make install-db
|
||||
<li><p>Open <a class="reference external" href="http://localhost:3000">http://localhost:3000</a> and register</p></li>
|
||||
<li><p>To set admin rights to the newly created account, use the following command line:</p></li>
|
||||
</ul>
|
||||
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ make set-admin <span class="nv">USERNAME</span><span class="o">=</span><username>
|
||||
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ make user-set-admin <span class="nv">USERNAME</span><span class="o">=</span><username>
|
||||
</pre></div>
|
||||
</div>
|
||||
<div class="admonition note">
|
||||
@ -684,11 +703,15 @@ database credentials</strong>):</p></li>
|
||||
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ make run
|
||||
</pre></div>
|
||||
</div>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>If email sending is disabled: <code class="docutils literal notranslate"><span class="pre">$</span> <span class="pre">make</span> <span class="pre">run-server</span></code></p>
|
||||
</div>
|
||||
<ul class="simple">
|
||||
<li><p>Open <a class="reference external" href="http://localhost:5000">http://localhost:5000</a> and register</p></li>
|
||||
<li><p>To set admin rights to the newly created account, use the following command line:</p></li>
|
||||
</ul>
|
||||
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ make set-admin <span class="nv">USERNAME</span><span class="o">=</span><username>
|
||||
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ make user-set-admin <span class="nv">USERNAME</span><span class="o">=</span><username>
|
||||
</pre></div>
|
||||
</div>
|
||||
<div class="admonition note">
|
||||
@ -727,11 +750,11 @@ $ <span class="nb">source</span> .env
|
||||
<ul class="simple">
|
||||
<li><p>Upgrade database if needed (see changelog for migrations):</p></li>
|
||||
</ul>
|
||||
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ fittrackee_upgrade_db
|
||||
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ ftcli db upgrade
|
||||
</pre></div>
|
||||
</div>
|
||||
<ul class="simple">
|
||||
<li><p>Restart the application and task queue workers.</p></li>
|
||||
<li><p>Restart the application and task queue workers (if email sending is enabled).</p></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section id="id3">
|
||||
@ -802,6 +825,10 @@ $ <span class="nb">cd</span> FitTrackee
|
||||
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ make run
|
||||
</pre></div>
|
||||
</div>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>If email sending is disabled: <code class="docutils literal notranslate"><span class="pre">$</span> <span class="pre">make</span> <span class="pre">run-server</span></code></p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
|
BIN
docs/objects.inv
BIN
docs/objects.inv
Binary file not shown.
@ -65,6 +65,7 @@
|
||||
aria-labelledby="dLabelGlobalToc"><ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="api/index.html">API documentation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="troubleshooting/index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="changelog.html">Change log</a></li>
|
||||
|
File diff suppressed because one or more lines are too long
@ -61,6 +61,7 @@
|
||||
aria-labelledby="dLabelGlobalToc"><ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="../features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../installation.html">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../api/index.html">API documentation</a></li>
|
||||
<li class="toctree-l1 current"><a class="reference internal" href="index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../changelog.html">Change log</a></li>
|
||||
|
@ -61,6 +61,7 @@
|
||||
aria-labelledby="dLabelGlobalToc"><ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="../features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../installation.html">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../api/index.html">API documentation</a></li>
|
||||
<li class="toctree-l1 current"><a class="current reference internal" href="#">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../changelog.html">Change log</a></li>
|
||||
|
@ -61,6 +61,7 @@
|
||||
aria-labelledby="dLabelGlobalToc"><ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="../features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../installation.html">Installation</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../cli.html">Command line interface</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../api/index.html">API documentation</a></li>
|
||||
<li class="toctree-l1 current"><a class="reference internal" href="index.html">Troubleshooting</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../changelog.html">Change log</a></li>
|
||||
|
67
docsrc/source/cli.rst
Normal file
67
docsrc/source/cli.rst
Normal file
@ -0,0 +1,67 @@
|
||||
Command line interface
|
||||
######################
|
||||
|
||||
A command line interface (CLI) is available to manage database and users.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ ftcli
|
||||
Usage: ftcli [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
FitTrackee Command Line Interface
|
||||
|
||||
Options:
|
||||
--help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
db Manage database.
|
||||
users Manage users.
|
||||
|
||||
.. warning::
|
||||
| The following commands are now deprecated and will be removed in a next version:
|
||||
| - ``fittrackee_set_admin``
|
||||
| - ``fittrackee_upgrade_db``
|
||||
|
||||
|
||||
Database
|
||||
~~~~~~~~
|
||||
|
||||
``ftcli db upgrade``
|
||||
""""""""""""""""""""
|
||||
.. versionadded:: 0.6.5
|
||||
|
||||
Apply migrations.
|
||||
|
||||
|
||||
``ftcli db drop``
|
||||
"""""""""""""""""
|
||||
.. versionadded:: 0.6.5
|
||||
|
||||
Empty database and delete uploaded files, only on development environments.
|
||||
|
||||
|
||||
|
||||
Users
|
||||
~~~~~
|
||||
|
||||
``ftcli users update``
|
||||
""""""""""""""""""""""
|
||||
.. versionadded:: 0.6.5
|
||||
|
||||
Modify a user account (admin rights, active status, email and password).
|
||||
|
||||
.. cssclass:: table-bordered
|
||||
.. list-table::
|
||||
:widths: 25 50
|
||||
:header-rows: 1
|
||||
|
||||
* - Options
|
||||
- Description
|
||||
* - ``--set-admin BOOLEAN``
|
||||
- Add/remove admin rights (when adding admin rights, it also activates user account if not active).
|
||||
* - ``--activate``
|
||||
- Activate user account.
|
||||
* - ``--reset-password``
|
||||
- Reset user password (a new password will be displayed).
|
||||
* - ``--update-email EMAIL``
|
||||
- Update user email.
|
@ -60,10 +60,11 @@ Workouts
|
||||
Account & preferences
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
- A user can create, update and deleted his account.
|
||||
- After registration, the user account is inactive and an email with confirmation instructions is sent to activate it. A user with an inactive account cannot log in. (*new in 0.6.0*)
|
||||
- After registration, the user account is inactive and an email with confirmation instructions is sent to activate it.
|
||||
A user with an inactive account cannot log in. (*new in 0.6.0*)
|
||||
|
||||
.. note::
|
||||
The command line to add admin rights activates the account if it is inactive.
|
||||
In case email sending is not configured, a `command line <cli.html#ftcli-users-update>`__ allows to activate users account.
|
||||
|
||||
- A user can set language, timezone and first day of week.
|
||||
- A user can reset his password (*new in 0.3.0*)
|
||||
@ -97,6 +98,9 @@ Administration
|
||||
.. warning::
|
||||
Updating server configuration may be necessary to handle large files (like `nginx <https://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size>`_ for instance).
|
||||
|
||||
.. note::
|
||||
If email sending is disabled, a warning is displayed.
|
||||
|
||||
|
||||
- **Users**
|
||||
|
||||
|
@ -34,6 +34,7 @@ Table of contents
|
||||
|
||||
features
|
||||
installation
|
||||
cli
|
||||
api/index
|
||||
troubleshooting/index
|
||||
changelog
|
||||
|
@ -22,9 +22,8 @@ Prerequisites
|
||||
|
||||
- Python 3.7+
|
||||
- PostgreSQL database (10+)
|
||||
- SMTP provider
|
||||
- Redis for task queue (to send emails)
|
||||
- API key from `Dark Sky <https://darksky.net/dev>`__ [not mandatory]
|
||||
- SMTP provider and Redis for task queue (if email sending is enabled)
|
||||
- API key from `Dark Sky <https://darksky.net/dev>`__ (not mandatory)
|
||||
- `Poetry <https://poetry.eustace.io>`__ (for installation from sources only)
|
||||
- `Yarn <https://yarnpkg.com>`__ (for development only)
|
||||
- Docker and Docker Compose (for development or evaluation purposes)
|
||||
@ -133,6 +132,13 @@ deployment method.
|
||||
|
||||
Email URL with credentials, see `Emails <installation.html#emails>`__.
|
||||
|
||||
.. versionchanged:: 0.6.5
|
||||
|
||||
:default: empty string
|
||||
|
||||
.. danger::
|
||||
If the email URL is empty, email sending will be disabled.
|
||||
|
||||
.. warning::
|
||||
If the email URL is invalid, the application may not start.
|
||||
|
||||
@ -214,7 +220,7 @@ To send emails, a valid ``EMAIL_URL`` must be provided:
|
||||
| - If the email URL is invalid, the application may not start.
|
||||
| - Sending emails with Office365 may not work if SMTP auth is disabled.
|
||||
|
||||
.. versionadded:: 0.5.3
|
||||
.. versionchanged:: 0.5.3
|
||||
|
||||
| Credentials can be omitted: ``smtp://smtp.example.com:25``.
|
||||
| If ``:<port>`` is omitted, the port defaults to 25.
|
||||
@ -229,6 +235,11 @@ Emails sent by FitTrackee are:
|
||||
- email change (to old and new email adresses)
|
||||
- password change
|
||||
|
||||
.. versionchanged:: 0.6.5
|
||||
|
||||
| For single-user instance, it is possible to disable email sending with an empty ``EMAIL_URL`` (in this case, no need to start dramatiq workers).
|
||||
| A `CLI <cli.html#ftcli-users-update>`__ is available to activate account and modify email and password.
|
||||
|
||||
|
||||
Map tile server
|
||||
^^^^^^^^^^^^^^^
|
||||
@ -288,7 +299,7 @@ For instance, copy and update ``.env`` file from ``.env.example`` and source the
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ fittrackee_upgrade_db
|
||||
$ ftcli db upgrade
|
||||
|
||||
- Start the application
|
||||
|
||||
@ -296,7 +307,7 @@ For instance, copy and update ``.env`` file from ``.env.example`` and source the
|
||||
|
||||
$ fittrackee
|
||||
|
||||
- Start task queue workers
|
||||
- Start task queue workers if email sending is enabled.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@ -311,7 +322,7 @@ For instance, copy and update ``.env`` file from ``.env.example`` and source the
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ fittrackee_set_admin <username>
|
||||
$ ftcli users update <username> --set-admin true
|
||||
|
||||
.. note::
|
||||
If the user account is inactive, it activates it.
|
||||
@ -373,7 +384,7 @@ Dev environment
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ make set-admin USERNAME=<username>
|
||||
$ make user-set-admin USERNAME=<username>
|
||||
|
||||
.. note::
|
||||
If the user account is inactive, it activates it.
|
||||
@ -415,13 +426,16 @@ Production environment
|
||||
|
||||
$ make run
|
||||
|
||||
.. note::
|
||||
If email sending is disabled: ``$ make run-server``
|
||||
|
||||
- Open http://localhost:5000 and register
|
||||
|
||||
- To set admin rights to the newly created account, use the following command line:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ make set-admin USERNAME=<username>
|
||||
$ make user-set-admin USERNAME=<username>
|
||||
|
||||
.. note::
|
||||
If the user account is inactive, it activates it.
|
||||
@ -457,9 +471,9 @@ From PyPI
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ fittrackee_upgrade_db
|
||||
$ ftcli db upgrade
|
||||
|
||||
- Restart the application and task queue workers.
|
||||
- Restart the application and task queue workers (if email sending is enabled).
|
||||
|
||||
|
||||
From sources
|
||||
@ -536,6 +550,8 @@ Prod environment
|
||||
|
||||
$ make run
|
||||
|
||||
.. note::
|
||||
If email sending is disabled: ``$ make run-server``
|
||||
|
||||
Deployment
|
||||
~~~~~~~~~~
|
||||
|
@ -1,7 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from importlib import import_module, reload
|
||||
from typing import Any
|
||||
|
||||
@ -42,7 +41,7 @@ class CustomFlask(Flask):
|
||||
request_class = CustomRequest
|
||||
|
||||
|
||||
def create_app() -> Flask:
|
||||
def create_app(init_email: bool = True) -> Flask:
|
||||
# instantiate the app
|
||||
app = CustomFlask(
|
||||
__name__, static_folder='dist/static', template_folder='dist'
|
||||
@ -65,8 +64,15 @@ def create_app() -> Flask:
|
||||
migrate.init_app(app, db)
|
||||
dramatiq.init_app(app)
|
||||
|
||||
# set up email
|
||||
# set up email if 'EMAIL_URL' is initialized
|
||||
if init_email:
|
||||
if app.config['EMAIL_URL']:
|
||||
email_service.init_email(app)
|
||||
app.config['CAN_SEND_EMAILS'] = True
|
||||
else:
|
||||
appLog.warning(
|
||||
'EMAIL_URL is not provided, email sending is deactivated.'
|
||||
)
|
||||
|
||||
# get configuration from database
|
||||
from .application.utils import (
|
||||
@ -147,17 +153,4 @@ def create_app() -> Flask:
|
||||
else:
|
||||
return render_template('index.html')
|
||||
|
||||
@app.cli.command('drop-db')
|
||||
def drop_db() -> None:
|
||||
"""Empty database and delete uploaded files for dev environments."""
|
||||
if app_settings == 'fittrackee.config.ProductionConfig':
|
||||
print('This is a production server, aborting!')
|
||||
return
|
||||
db.engine.execute("DROP TABLE IF EXISTS alembic_version;")
|
||||
db.drop_all()
|
||||
db.session.commit()
|
||||
print('Database dropped.')
|
||||
shutil.rmtree(app.config['UPLOAD_FOLDER'], ignore_errors=True)
|
||||
print('Uploaded files deleted.')
|
||||
|
||||
return app
|
||||
|
@ -3,16 +3,23 @@
|
||||
import os
|
||||
from typing import Dict, Optional
|
||||
|
||||
import click
|
||||
import gunicorn.app.base
|
||||
from flask import Flask
|
||||
from flask_migrate import upgrade
|
||||
|
||||
from fittrackee import create_app
|
||||
from fittrackee.users.exceptions import UserNotFoundException
|
||||
from fittrackee.users.utils.admin import UserManagerService
|
||||
|
||||
HOST = os.getenv('HOST', '0.0.0.0')
|
||||
PORT = os.getenv('PORT', '5000')
|
||||
WORKERS = os.getenv('APP_WORKERS', 1)
|
||||
BASEDIR = os.path.abspath(os.path.dirname(__file__))
|
||||
WARNING_MESSAGE = (
|
||||
"\nThis command is deprecated, it will be removed in a next version.\n"
|
||||
"Please use ftcli instead.\n"
|
||||
)
|
||||
app = create_app()
|
||||
|
||||
|
||||
@ -37,7 +44,39 @@ class StandaloneApplication(gunicorn.app.base.BaseApplication):
|
||||
return self.application
|
||||
|
||||
|
||||
# DEPRECATED COMMANDS
|
||||
@click.group()
|
||||
def users_cli() -> None:
|
||||
pass
|
||||
|
||||
|
||||
@users_cli.command('set_admin')
|
||||
@click.argument('username')
|
||||
def set_admin(username: str) -> None:
|
||||
"""
|
||||
[deprecated] Set admin rights for given user.
|
||||
|
||||
It will be removed in a next version.
|
||||
"""
|
||||
print(WARNING_MESSAGE)
|
||||
with app.app_context():
|
||||
try:
|
||||
user_manager_service = UserManagerService(username)
|
||||
user_manager_service.update(
|
||||
is_admin=True,
|
||||
)
|
||||
print(f"User '{username}' updated.")
|
||||
except UserNotFoundException:
|
||||
print(f"User '{username}' not found.")
|
||||
|
||||
|
||||
def upgrade_db() -> None:
|
||||
"""
|
||||
[deprecated] Apply migrations.
|
||||
|
||||
It will be removed in a next version.
|
||||
"""
|
||||
print(WARNING_MESSAGE)
|
||||
with app.app_context():
|
||||
upgrade(directory=BASEDIR + '/migrations')
|
||||
|
||||
|
@ -42,6 +42,7 @@ def get_application_config() -> Union[Dict, HttpResponse]:
|
||||
"data": {
|
||||
"admin_contact": "admin@example.com",
|
||||
"gpx_limit_import": 10,
|
||||
"is_email_sending_enabled": true,
|
||||
"is_registration_enabled": false,
|
||||
"max_single_file_size": 1048576,
|
||||
"max_users": 0,
|
||||
@ -91,6 +92,7 @@ def update_application_config(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
"data": {
|
||||
"admin_contact": "admin@example.com",
|
||||
"gpx_limit_import": 10,
|
||||
"is_email_sending_enabled": true,
|
||||
"is_registration_enabled": false,
|
||||
"max_single_file_size": 1048576,
|
||||
"max_users": 10,
|
||||
|
@ -46,6 +46,7 @@ class AppConfig(BaseModel):
|
||||
return {
|
||||
'admin_contact': self.admin_contact,
|
||||
'gpx_limit_import': self.gpx_limit_import,
|
||||
'is_email_sending_enabled': current_app.config['CAN_SEND_EMAILS'],
|
||||
'is_registration_enabled': self.is_registration_enabled,
|
||||
'max_single_file_size': self.max_single_file_size,
|
||||
'max_zip_file_size': self.max_zip_file_size,
|
||||
|
14
fittrackee/cli/__init__.py
Normal file
14
fittrackee/cli/__init__.py
Normal file
@ -0,0 +1,14 @@
|
||||
import click
|
||||
|
||||
from fittrackee.migrations.commands import db_cli
|
||||
from fittrackee.users.commands import users_cli
|
||||
|
||||
|
||||
@click.group()
|
||||
def cli() -> None:
|
||||
"""FitTrackee Command Line Interface"""
|
||||
pass
|
||||
|
||||
|
||||
cli.add_command(db_cli)
|
||||
cli.add_command(users_cli)
|
3
fittrackee/cli/app.py
Normal file
3
fittrackee/cli/app.py
Normal file
@ -0,0 +1,3 @@
|
||||
from fittrackee import create_app
|
||||
|
||||
app = create_app(init_email=False)
|
@ -29,6 +29,7 @@ class BaseConfig:
|
||||
UI_URL = os.environ.get('UI_URL')
|
||||
EMAIL_URL = os.environ.get('EMAIL_URL')
|
||||
SENDER_EMAIL = os.environ.get('SENDER_EMAIL')
|
||||
CAN_SEND_EMAILS = False
|
||||
DRAMATIQ_BROKER = broker
|
||||
TILE_SERVER = {
|
||||
'URL': os.environ.get(
|
||||
|
2
fittrackee/dist/index.html
vendored
2
fittrackee/dist/index.html
vendored
@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"/><link rel="stylesheet" href="/static/css/leaflet.css"/><title>FitTrackee</title><script defer="defer" src="/static/js/chunk-vendors.6b8389c5.js"></script><script defer="defer" src="/static/js/app.756f8c8c.js"></script><link href="/static/css/app.3729aa92.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="fittrackee_client"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but FitTrackee doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"/><link rel="stylesheet" href="/static/css/leaflet.css"/><title>FitTrackee</title><script defer="defer" src="/static/js/chunk-vendors.6b8389c5.js"></script><script defer="defer" src="/static/js/app.fa6f4b25.js"></script><link href="/static/css/app.e8b7692c.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="fittrackee_client"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but FitTrackee doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
|
2
fittrackee/dist/service-worker.js
vendored
2
fittrackee/dist/service-worker.js
vendored
File diff suppressed because one or more lines are too long
2
fittrackee/dist/service-worker.js.map
vendored
2
fittrackee/dist/service-worker.js.map
vendored
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/css/app.3729aa92.css
vendored
1
fittrackee/dist/static/css/app.3729aa92.css
vendored
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/css/app.e8b7692c.css
vendored
Normal file
1
fittrackee/dist/static/css/app.e8b7692c.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/app.756f8c8c.js
vendored
2
fittrackee/dist/static/js/app.756f8c8c.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/app.fa6f4b25.js
vendored
Normal file
2
fittrackee/dist/static/js/app.fa6f4b25.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/js/app.fa6f4b25.js.map
vendored
Normal file
1
fittrackee/dist/static/js/app.fa6f4b25.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,2 +1,2 @@
|
||||
"use strict";(self["webpackChunkfittrackee_client"]=self["webpackChunkfittrackee_client"]||[]).push([[845],{4264:function(e,t,r){r.r(t),r.d(t,{default:function(){return m}});var n=r(5793),a=r(2715),s=r(3577),u=r(2119),o=r(7167),c=r(8602),i=r(9917);const l={key:0,id:"account-confirmation",class:"center-card with-margin"},E={class:"error-message"};var d=(0,n.aZ)({setup(e){const t=(0,u.yj)(),r=(0,u.tv)(),d=(0,i.o)(),S=(0,n.Fl)((()=>d.getters[c.SY.GETTERS.ERROR_MESSAGES])),_=(0,n.Fl)((()=>t.query.token));function m(){_.value?d.dispatch(c.YN.ACTIONS.CONFIRM_ACCOUNT,{token:_.value}):r.push("/")}return(0,n.wF)((()=>m())),(0,n.Ah)((()=>d.commit(c.SY.MUTATIONS.EMPTY_ERROR_MESSAGES))),(e,t)=>{const r=(0,n.up)("router-link");return(0,a.SU)(S)?((0,n.wg)(),(0,n.iD)("div",l,[(0,n.Wm)(o.Z),(0,n._)("p",E,[(0,n._)("span",null,(0,s.zw)(e.$t("error.SOMETHING_WRONG"))+".",1),(0,n.Wm)(r,{class:"links",to:"/account-confirmation/resend"},{default:(0,n.w5)((()=>[(0,n.Uk)((0,s.zw)(e.$t("buttons.ACCOUNT-CONFIRMATION-RESEND"))+"? ",1)])),_:1})])])):(0,n.kq)("",!0)}}}),S=r(3744);const _=(0,S.Z)(d,[["__scopeId","data-v-785df978"]]);var m=_},8160:function(e,t,r){r.r(t),r.d(t,{default:function(){return m}});var n=r(5793),a=r(2715),s=r(3577),u=r(2119),o=r(7167),c=r(8602),i=r(9917);const l={key:0,id:"email-update",class:"center-card with-margin"},E={class:"error-message"};var d=(0,n.aZ)({setup(e){const t=(0,u.yj)(),r=(0,u.tv)(),d=(0,i.o)(),S=(0,n.Fl)((()=>d.getters[c.YN.GETTERS.AUTH_USER_PROFILE])),_=(0,n.Fl)((()=>d.getters[c.YN.GETTERS.IS_AUTHENTICATED])),m=(0,n.Fl)((()=>d.getters[c.SY.GETTERS.ERROR_MESSAGES])),R=(0,n.Fl)((()=>t.query.token));function T(){R.value?d.dispatch(c.YN.ACTIONS.CONFIRM_EMAIL,{token:R.value,refreshUser:_.value}):r.push("/")}return(0,n.wF)((()=>T())),(0,n.Ah)((()=>d.commit(c.SY.MUTATIONS.EMPTY_ERROR_MESSAGES))),(0,n.YP)((()=>m.value),(e=>{S.value.username&&e&&r.push("/")})),(e,t)=>{const r=(0,n.up)("router-link"),u=(0,n.up)("i18n-t");return(0,a.SU)(m)&&!(0,a.SU)(S).username?((0,n.wg)(),(0,n.iD)("div",l,[(0,n.Wm)(o.Z),(0,n._)("p",E,[(0,n._)("span",null,(0,s.zw)(e.$t("error.SOMETHING_WRONG"))+".",1),(0,n._)("span",null,[(0,n.Wm)(u,{keypath:"user.PROFILE.ERRORED_EMAIL_UPDATE"},{default:(0,n.w5)((()=>[(0,n.Wm)(r,{to:"/login"},{default:(0,n.w5)((()=>[(0,n.Uk)((0,s.zw)(e.$t("user.LOG_IN")),1)])),_:1})])),_:1})])])])):(0,n.kq)("",!0)}}}),S=r(3744);const _=(0,S.Z)(d,[["__scopeId","data-v-8c2ec9ce"]]);var m=_},6266:function(e,t,r){r.r(t),r.d(t,{default:function(){return S}});var n=r(5793),a=r(2715),s=r(8602),u=r(9917);const o=e=>((0,n.dD)("data-v-05463732"),e=e(),(0,n.Cn)(),e),c={key:0,id:"profile",class:"container view"},i=o((()=>(0,n._)("div",{id:"bottom"},null,-1)));var l=(0,n.aZ)({setup(e){const t=(0,u.o)(),r=(0,n.Fl)((()=>t.getters[s.YN.GETTERS.AUTH_USER_PROFILE]));return(e,t)=>{const s=(0,n.up)("router-view");return(0,a.SU)(r).username?((0,n.wg)(),(0,n.iD)("div",c,[(0,n.Wm)(s,{user:(0,a.SU)(r)},null,8,["user"]),i])):(0,n.kq)("",!0)}}}),E=r(3744);const d=(0,E.Z)(l,[["__scopeId","data-v-05463732"]]);var S=d},9453:function(e,t,r){r.r(t),r.d(t,{default:function(){return m}});var n=r(5793),a=r(2715),s=r(2119),u=r(2179),o=r(4317),c=r(8602),i=r(9917);const l={key:0,id:"user",class:"view"},E={class:"box"};var d=(0,n.aZ)({props:{fromAdmin:{type:Boolean}},setup(e){const t=e,{fromAdmin:r}=(0,a.BK)(t),d=(0,s.yj)(),S=(0,i.o)(),_=(0,n.Fl)((()=>S.getters[c.RT.GETTERS.USER]));return(0,n.wF)((()=>{d.params.username&&"string"===typeof d.params.username&&S.dispatch(c.RT.ACTIONS.GET_USER,d.params.username)})),(0,n.Jd)((()=>{S.dispatch(c.RT.ACTIONS.EMPTY_USER)})),(e,t)=>(0,a.SU)(_).username?((0,n.wg)(),(0,n.iD)("div",l,[(0,n.Wm)(u.Z,{user:(0,a.SU)(_)},null,8,["user"]),(0,n._)("div",E,[(0,n.Wm)(o.Z,{user:(0,a.SU)(_),"from-admin":(0,a.SU)(r)},null,8,["user","from-admin"])])])):(0,n.kq)("",!0)}}),S=r(3744);const _=(0,S.Z)(d,[["__scopeId","data-v-af7007f4"]]);var m=_}}]);
|
||||
//# sourceMappingURL=profile.6a52c2c3.js.map
|
||||
"use strict";(self["webpackChunkfittrackee_client"]=self["webpackChunkfittrackee_client"]||[]).push([[845],{4264:function(e,t,r){r.r(t),r.d(t,{default:function(){return m}});var n=r(5793),a=r(2715),s=r(3577),u=r(2119),o=r(7167),c=r(8602),i=r(9917);const l={key:0,id:"account-confirmation",class:"center-card with-margin"},E={class:"error-message"};var d=(0,n.aZ)({setup(e){const t=(0,u.yj)(),r=(0,u.tv)(),d=(0,i.o)(),S=(0,n.Fl)((()=>d.getters[c.SY.GETTERS.ERROR_MESSAGES])),_=(0,n.Fl)((()=>t.query.token));function m(){_.value?d.dispatch(c.YN.ACTIONS.CONFIRM_ACCOUNT,{token:_.value}):r.push("/")}return(0,n.wF)((()=>m())),(0,n.Ah)((()=>d.commit(c.SY.MUTATIONS.EMPTY_ERROR_MESSAGES))),(e,t)=>{const r=(0,n.up)("router-link");return(0,a.SU)(S)?((0,n.wg)(),(0,n.iD)("div",l,[(0,n.Wm)(o.Z),(0,n._)("p",E,[(0,n._)("span",null,(0,s.zw)(e.$t("error.SOMETHING_WRONG"))+".",1),(0,n.Wm)(r,{class:"links",to:"/account-confirmation/resend"},{default:(0,n.w5)((()=>[(0,n.Uk)((0,s.zw)(e.$t("buttons.ACCOUNT-CONFIRMATION-RESEND"))+"? ",1)])),_:1})])])):(0,n.kq)("",!0)}}}),S=r(3744);const _=(0,S.Z)(d,[["__scopeId","data-v-785df978"]]);var m=_},8160:function(e,t,r){r.r(t),r.d(t,{default:function(){return m}});var n=r(5793),a=r(2715),s=r(3577),u=r(2119),o=r(7167),c=r(8602),i=r(9917);const l={key:0,id:"email-update",class:"center-card with-margin"},E={class:"error-message"};var d=(0,n.aZ)({setup(e){const t=(0,u.yj)(),r=(0,u.tv)(),d=(0,i.o)(),S=(0,n.Fl)((()=>d.getters[c.YN.GETTERS.AUTH_USER_PROFILE])),_=(0,n.Fl)((()=>d.getters[c.YN.GETTERS.IS_AUTHENTICATED])),m=(0,n.Fl)((()=>d.getters[c.SY.GETTERS.ERROR_MESSAGES])),R=(0,n.Fl)((()=>t.query.token));function T(){R.value?d.dispatch(c.YN.ACTIONS.CONFIRM_EMAIL,{token:R.value,refreshUser:_.value}):r.push("/")}return(0,n.wF)((()=>T())),(0,n.Ah)((()=>d.commit(c.SY.MUTATIONS.EMPTY_ERROR_MESSAGES))),(0,n.YP)((()=>m.value),(e=>{S.value.username&&e&&r.push("/")})),(e,t)=>{const r=(0,n.up)("router-link"),u=(0,n.up)("i18n-t");return(0,a.SU)(m)&&!(0,a.SU)(S).username?((0,n.wg)(),(0,n.iD)("div",l,[(0,n.Wm)(o.Z),(0,n._)("p",E,[(0,n._)("span",null,(0,s.zw)(e.$t("error.SOMETHING_WRONG"))+".",1),(0,n._)("span",null,[(0,n.Wm)(u,{keypath:"user.PROFILE.ERRORED_EMAIL_UPDATE"},{default:(0,n.w5)((()=>[(0,n.Wm)(r,{to:"/login"},{default:(0,n.w5)((()=>[(0,n.Uk)((0,s.zw)(e.$t("user.LOG_IN")),1)])),_:1})])),_:1})])])])):(0,n.kq)("",!0)}}}),S=r(3744);const _=(0,S.Z)(d,[["__scopeId","data-v-8c2ec9ce"]]);var m=_},6266:function(e,t,r){r.r(t),r.d(t,{default:function(){return S}});var n=r(5793),a=r(2715),s=r(8602),u=r(9917);const o=e=>((0,n.dD)("data-v-05463732"),e=e(),(0,n.Cn)(),e),c={key:0,id:"profile",class:"container view"},i=o((()=>(0,n._)("div",{id:"bottom"},null,-1)));var l=(0,n.aZ)({setup(e){const t=(0,u.o)(),r=(0,n.Fl)((()=>t.getters[s.YN.GETTERS.AUTH_USER_PROFILE]));return(e,t)=>{const s=(0,n.up)("router-view");return(0,a.SU)(r).username?((0,n.wg)(),(0,n.iD)("div",c,[(0,n.Wm)(s,{user:(0,a.SU)(r)},null,8,["user"]),i])):(0,n.kq)("",!0)}}}),E=r(3744);const d=(0,E.Z)(l,[["__scopeId","data-v-05463732"]]);var S=d},9453:function(e,t,r){r.r(t),r.d(t,{default:function(){return m}});var n=r(5793),a=r(2715),s=r(2119),u=r(2179),o=r(4980),c=r(8602),i=r(9917);const l={key:0,id:"user",class:"view"},E={class:"box"};var d=(0,n.aZ)({props:{fromAdmin:{type:Boolean}},setup(e){const t=e,{fromAdmin:r}=(0,a.BK)(t),d=(0,s.yj)(),S=(0,i.o)(),_=(0,n.Fl)((()=>S.getters[c.RT.GETTERS.USER]));return(0,n.wF)((()=>{d.params.username&&"string"===typeof d.params.username&&S.dispatch(c.RT.ACTIONS.GET_USER,d.params.username)})),(0,n.Jd)((()=>{S.dispatch(c.RT.ACTIONS.EMPTY_USER)})),(e,t)=>(0,a.SU)(_).username?((0,n.wg)(),(0,n.iD)("div",l,[(0,n.Wm)(u.Z,{user:(0,a.SU)(_)},null,8,["user"]),(0,n._)("div",E,[(0,n.Wm)(o.Z,{user:(0,a.SU)(_),"from-admin":(0,a.SU)(r)},null,8,["user","from-admin"])])])):(0,n.kq)("",!0)}}),S=r(3744);const _=(0,S.Z)(d,[["__scopeId","data-v-af7007f4"]]);var m=_}}]);
|
||||
//# sourceMappingURL=profile.52d627f4.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
44
fittrackee/migrations/commands.py
Normal file
44
fittrackee/migrations/commands.py
Normal file
@ -0,0 +1,44 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import click
|
||||
from flask_migrate import upgrade
|
||||
|
||||
from fittrackee import db
|
||||
from fittrackee.cli.app import app
|
||||
|
||||
BASEDIR = os.path.abspath(os.path.dirname(__file__))
|
||||
app_settings = os.getenv('APP_SETTINGS', 'fittrackee.config.ProductionConfig')
|
||||
|
||||
|
||||
@click.group(name='db')
|
||||
def db_cli() -> None:
|
||||
"""Manage database."""
|
||||
pass
|
||||
|
||||
|
||||
@db_cli.command('upgrade')
|
||||
def upgrade_db() -> None:
|
||||
"""Apply migrations."""
|
||||
with app.app_context():
|
||||
upgrade(directory=BASEDIR)
|
||||
|
||||
|
||||
@db_cli.command('drop')
|
||||
def drop_db() -> None:
|
||||
"""Empty database and delete uploaded files for dev environments."""
|
||||
with app.app_context():
|
||||
if app_settings == 'fittrackee.config.ProductionConfig':
|
||||
click.echo(
|
||||
click.style(
|
||||
'This is a production server, aborting!', bold=True
|
||||
),
|
||||
err=True,
|
||||
)
|
||||
return
|
||||
db.engine.execute("DROP TABLE IF EXISTS alembic_version;")
|
||||
db.drop_all()
|
||||
db.session.commit()
|
||||
click.echo('Database dropped.')
|
||||
shutil.rmtree(app.config['UPLOAD_FOLDER'], ignore_errors=True)
|
||||
click.echo('Uploaded files deleted.')
|
@ -24,6 +24,7 @@ class TestConfigModel:
|
||||
serialized_app_config['gpx_limit_import']
|
||||
== app_config.gpx_limit_import
|
||||
)
|
||||
assert serialized_app_config['is_email_sending_enabled'] is True
|
||||
assert serialized_app_config['is_registration_enabled'] is True
|
||||
assert (
|
||||
serialized_app_config['max_single_file_size']
|
||||
@ -49,3 +50,11 @@ class TestConfigModel:
|
||||
|
||||
assert app_config.is_registration_enabled is False
|
||||
assert serialized_app_config['is_registration_enabled'] is False
|
||||
|
||||
def test_it_returns_email_sending_disabled_when_no_email_url_provided(
|
||||
self, app_wo_email_activation: Flask, user_1: User, user_2: User
|
||||
) -> None:
|
||||
app_config = AppConfig.query.first()
|
||||
serialized_app_config = app_config.serialize()
|
||||
|
||||
assert serialized_app_config['is_email_sending_enabled'] is False
|
||||
|
6
fittrackee/tests/fixtures/fixtures_app.py
vendored
6
fittrackee/tests/fixtures/fixtures_app.py
vendored
@ -146,6 +146,12 @@ def app_wo_email_auth(monkeypatch: pytest.MonkeyPatch) -> Generator:
|
||||
yield from get_app(with_config=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_wo_email_activation(monkeypatch: pytest.MonkeyPatch) -> Generator:
|
||||
monkeypatch.setenv('EMAIL_URL', '')
|
||||
yield from get_app(with_config=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_wo_domain() -> Generator:
|
||||
yield from get_app(with_config=True)
|
||||
|
@ -294,6 +294,31 @@ class TestUserRegistration(ApiTestCaseMixin):
|
||||
},
|
||||
)
|
||||
|
||||
def test_it_does_not_call_account_confirmation_email_when_email_sending_is_disabled( # noqa
|
||||
self,
|
||||
app_wo_email_activation: Flask,
|
||||
account_confirmation_email_mock: Mock,
|
||||
) -> None:
|
||||
client = app_wo_email_activation.test_client()
|
||||
email = self.random_email()
|
||||
username = self.random_string()
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/register',
|
||||
data=json.dumps(
|
||||
dict(
|
||||
username=username,
|
||||
email=email,
|
||||
password='12345678',
|
||||
)
|
||||
),
|
||||
content_type='application/json',
|
||||
environ_base={'HTTP_USER_AGENT': USER_AGENT},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
account_confirmation_email_mock.send.assert_not_called()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'text_transformation',
|
||||
['upper', 'lower'],
|
||||
@ -773,6 +798,36 @@ class TestUserAccountUpdate(ApiTestCaseMixin):
|
||||
assert new_email == user_1.email_to_confirm
|
||||
assert user_1.confirmation_token is not None
|
||||
|
||||
def test_it_updates_email_when_email_sending_is_disabled(
|
||||
self,
|
||||
app_wo_email_activation: Flask,
|
||||
user_1: User,
|
||||
email_updated_to_current_address_mock: MagicMock,
|
||||
email_updated_to_new_address_mock: MagicMock,
|
||||
password_change_email_mock: MagicMock,
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
app_wo_email_activation, user_1.email
|
||||
)
|
||||
new_email = 'new.email@example.com'
|
||||
|
||||
response = client.patch(
|
||||
'/api/auth/profile/edit/account',
|
||||
content_type='application/json',
|
||||
data=json.dumps(
|
||||
dict(
|
||||
email=new_email,
|
||||
password='12345678',
|
||||
)
|
||||
),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert user_1.email == new_email
|
||||
assert user_1.email_to_confirm is None
|
||||
assert user_1.confirmation_token is None
|
||||
|
||||
def test_it_calls_email_updated_to_current_email_send_when_new_email_provided( # noqa
|
||||
self,
|
||||
app: Flask,
|
||||
@ -1107,6 +1162,37 @@ class TestUserAccountUpdate(ApiTestCaseMixin):
|
||||
email_updated_to_new_address_mock.send.assert_called_once()
|
||||
password_change_email_mock.send.assert_called_once()
|
||||
|
||||
def test_it_does_not_calls_all_email_send_when_email_sending_is_disabled(
|
||||
self,
|
||||
app_wo_email_activation: Flask,
|
||||
user_1: User,
|
||||
email_updated_to_current_address_mock: MagicMock,
|
||||
email_updated_to_new_address_mock: MagicMock,
|
||||
password_change_email_mock: MagicMock,
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
app_wo_email_activation, user_1.email
|
||||
)
|
||||
|
||||
client.patch(
|
||||
'/api/auth/profile/edit/account',
|
||||
content_type='application/json',
|
||||
data=json.dumps(
|
||||
dict(
|
||||
email='new.email@example.com',
|
||||
password='12345678',
|
||||
new_password=self.random_string(),
|
||||
)
|
||||
),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
self.assert_no_emails_sent(
|
||||
email_updated_to_current_address_mock,
|
||||
email_updated_to_new_address_mock,
|
||||
password_change_email_mock,
|
||||
)
|
||||
|
||||
|
||||
class TestUserPreferencesUpdate(ApiTestCaseMixin):
|
||||
def test_it_returns_error_if_payload_is_empty(
|
||||
@ -1648,6 +1734,21 @@ class TestPasswordResetRequest(ApiTestCaseMixin):
|
||||
|
||||
self.assert_400(response)
|
||||
|
||||
def test_it_returns_error_when_email_sending_is_disabled(
|
||||
self, app_wo_email_activation: Flask
|
||||
) -> None:
|
||||
client = app_wo_email_activation.test_client()
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/password/reset-request',
|
||||
data=json.dumps(dict(email='test@test.com')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
self.assert_404_with_message(
|
||||
response, 'the requested URL was not found on the server'
|
||||
)
|
||||
|
||||
def test_it_requests_password_reset_when_user_exists(
|
||||
self, app: Flask, user_1: User, user_reset_password_email: Mock
|
||||
) -> None:
|
||||
@ -1873,7 +1974,7 @@ class TestPasswordUpdate(ApiTestCaseMixin):
|
||||
assert data['status'] == 'success'
|
||||
assert data['message'] == 'password updated'
|
||||
|
||||
def test_it_send_email_after_successful_update(
|
||||
def test_it_sends_email_after_successful_update(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
@ -1908,6 +2009,29 @@ class TestPasswordUpdate(ApiTestCaseMixin):
|
||||
},
|
||||
)
|
||||
|
||||
def test_it_does_not_send_email_when_email_sending_is_disabled(
|
||||
self,
|
||||
app_wo_email_activation: Flask,
|
||||
user_1: User,
|
||||
password_change_email_mock: MagicMock,
|
||||
) -> None:
|
||||
token = get_user_token(user_1.id, password_reset=True)
|
||||
client = app_wo_email_activation.test_client()
|
||||
|
||||
client.post(
|
||||
'/api/auth/password/update',
|
||||
data=json.dumps(
|
||||
dict(
|
||||
token=token,
|
||||
password=self.random_string(),
|
||||
)
|
||||
),
|
||||
content_type='application/json',
|
||||
environ_base={'HTTP_USER_AGENT': USER_AGENT},
|
||||
)
|
||||
|
||||
password_change_email_mock.send.assert_not_called()
|
||||
|
||||
|
||||
class TestEmailUpdateWitUnauthenticatedUser(ApiTestCaseMixin):
|
||||
def test_it_returns_error_if_token_is_missing(self, app: Flask) -> None:
|
||||
@ -2138,3 +2262,18 @@ class TestResendAccountConfirmationEmail(ApiTestCaseMixin):
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
def test_it_returns_error_if_email_sending_is_disabled(
|
||||
self, app_wo_email_activation: Flask, inactive_user: User
|
||||
) -> None:
|
||||
client = app_wo_email_activation.test_client()
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/account/resend-confirmation',
|
||||
data=json.dumps(dict(email=inactive_user.email)),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
self.assert_404_with_message(
|
||||
response, 'the requested URL was not found on the server'
|
||||
)
|
||||
|
@ -1077,6 +1077,27 @@ class TestUpdateUser(ApiTestCaseMixin):
|
||||
},
|
||||
)
|
||||
|
||||
def test_it_does_not_call_password_change_email_when_email_sending_is_disabled( # noqa
|
||||
self,
|
||||
app_wo_email_activation: Flask,
|
||||
user_1_admin: User,
|
||||
user_2: User,
|
||||
user_password_change_email_mock: MagicMock,
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
app_wo_email_activation, user_1_admin.email
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
f'/api/users/{user_2.username}',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(reset_password=True)),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
user_password_change_email_mock.send.assert_not_called()
|
||||
|
||||
def test_it_calls_reset_password_email_when_password_reset_is_successful(
|
||||
self,
|
||||
app: Flask,
|
||||
@ -1118,6 +1139,27 @@ class TestUpdateUser(ApiTestCaseMixin):
|
||||
},
|
||||
)
|
||||
|
||||
def test_it_does_not_call_reset_password_email_when_email_sending_is_disabled( # noqa
|
||||
self,
|
||||
app_wo_email_activation: Flask,
|
||||
user_1_admin: User,
|
||||
user_2: User,
|
||||
user_reset_password_email: MagicMock,
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
app_wo_email_activation, user_1_admin.email
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
f'/api/users/{user_2.username}',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(reset_password=True)),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
user_reset_password_email.send.assert_not_called()
|
||||
|
||||
def test_it_returns_error_when_updating_email_with_invalid_address(
|
||||
self, app: Flask, user_1_admin: User, user_2: User
|
||||
) -> None:
|
||||
@ -1172,27 +1214,48 @@ class TestUpdateUser(ApiTestCaseMixin):
|
||||
|
||||
user_email_updated_to_new_address_mock.send.assert_not_called()
|
||||
|
||||
def test_it_updates_user_email(
|
||||
def test_it_updates_user_email_to_confirm_when_email_sending_is_enabled(
|
||||
self, app: Flask, user_1_admin: User, user_2: User
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
app, user_1_admin.email
|
||||
)
|
||||
new_email = 'new.' + user_2.email
|
||||
user_2_email = user_2.email
|
||||
user_2_confirmation_token = user_2.confirmation_token
|
||||
|
||||
response = client.patch(
|
||||
f'/api/users/{user_2.username}',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(new_email='new.' + user_2.email)),
|
||||
data=json.dumps(dict(new_email=new_email)),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert user_2.email == user_2_email
|
||||
assert user_2.email_to_confirm == 'new.' + user_2.email
|
||||
assert user_2.email_to_confirm == new_email
|
||||
assert user_2.confirmation_token != user_2_confirmation_token
|
||||
|
||||
def test_it_updates_user_email_when_email_sending_is_disabled(
|
||||
self, app_wo_email_activation: Flask, user_1_admin: User, user_2: User
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
app_wo_email_activation, user_1_admin.email
|
||||
)
|
||||
new_email = 'new.' + user_2.email
|
||||
|
||||
response = client.patch(
|
||||
f'/api/users/{user_2.username}',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(new_email=new_email)),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert user_2.email == new_email
|
||||
assert user_2.email_to_confirm is None
|
||||
assert user_2.confirmation_token is None
|
||||
|
||||
def test_it_calls_email_updated_to_new_address_when_password_reset_is_successful( # noqa
|
||||
self,
|
||||
app: Flask,
|
||||
@ -1229,6 +1292,28 @@ class TestUpdateUser(ApiTestCaseMixin):
|
||||
},
|
||||
)
|
||||
|
||||
def test_it_does_not_call_email_updated_to_new_address_when_email_sending_is_disabled( # noqa
|
||||
self,
|
||||
app_wo_email_activation: Flask,
|
||||
user_1_admin: User,
|
||||
user_2: User,
|
||||
user_email_updated_to_new_address_mock: MagicMock,
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
app_wo_email_activation, user_1_admin.email
|
||||
)
|
||||
new_email = 'new.' + user_2.email
|
||||
|
||||
response = client.patch(
|
||||
f'/api/users/{user_2.username}',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(new_email=new_email)),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
user_email_updated_to_new_address_mock.send.assert_not_called()
|
||||
|
||||
def test_it_activates_user_account(
|
||||
self, app: Flask, user_1_admin: User, inactive_user: User
|
||||
) -> None:
|
||||
|
@ -3,10 +3,14 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from fittrackee import bcrypt
|
||||
from fittrackee.tests.utils import random_string
|
||||
from fittrackee.users.exceptions import UserNotFoundException
|
||||
from fittrackee.users.exceptions import (
|
||||
InvalidEmailException,
|
||||
UserNotFoundException,
|
||||
)
|
||||
from fittrackee.users.models import User
|
||||
from fittrackee.users.utils.admin import set_admin_rights
|
||||
from fittrackee.users.utils.admin import UserManagerService
|
||||
from fittrackee.users.utils.controls import (
|
||||
check_password,
|
||||
check_username,
|
||||
@ -14,37 +18,166 @@ from fittrackee.users.utils.controls import (
|
||||
register_controls,
|
||||
)
|
||||
|
||||
from ..utils import random_email
|
||||
|
||||
class TestSetAdminRights:
|
||||
|
||||
class TestUserManagerService:
|
||||
def test_it_raises_exception_if_user_does_not_exist(
|
||||
self, app: Flask
|
||||
) -> None:
|
||||
user_manager_service = UserManagerService(username=random_string())
|
||||
|
||||
with pytest.raises(UserNotFoundException):
|
||||
set_admin_rights(random_string())
|
||||
user_manager_service.update()
|
||||
|
||||
def test_it_does_not_update_user_when_no_args_provided(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
user_manager_service = UserManagerService(username=user_1.username)
|
||||
|
||||
_, user_updated, _ = user_manager_service.update()
|
||||
|
||||
assert user_updated is False
|
||||
|
||||
def test_it_returns_user(self, app: Flask, user_1: User) -> None:
|
||||
user_manager_service = UserManagerService(username=user_1.username)
|
||||
|
||||
user, _, _ = user_manager_service.update()
|
||||
|
||||
assert user == user_1
|
||||
|
||||
def test_it_sets_admin_right_for_a_given_user(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
set_admin_rights(user_1.username)
|
||||
user_manager_service = UserManagerService(username=user_1.username)
|
||||
|
||||
user_manager_service.update(is_admin=True)
|
||||
|
||||
assert user_1.admin is True
|
||||
|
||||
def test_it_return_updated_user_flag_to_true(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
user_manager_service = UserManagerService(username=user_1.username)
|
||||
|
||||
_, user_updated, _ = user_manager_service.update(is_admin=True)
|
||||
|
||||
assert user_updated is True
|
||||
|
||||
def test_it_does_not_raise_exception_when_user_has_already_admin_right(
|
||||
self, app: Flask, user_1_admin: User
|
||||
) -> None:
|
||||
set_admin_rights(user_1_admin.username)
|
||||
user_manager_service = UserManagerService(
|
||||
username=user_1_admin.username
|
||||
)
|
||||
|
||||
user_manager_service.update(is_admin=True)
|
||||
|
||||
assert user_1_admin.admin is True
|
||||
|
||||
def test_it_activates_account_if_user_is_inactive(
|
||||
self, app: Flask, inactive_user: User
|
||||
) -> None:
|
||||
set_admin_rights(inactive_user.username)
|
||||
user_manager_service = UserManagerService(
|
||||
username=inactive_user.username
|
||||
)
|
||||
|
||||
user_manager_service.update(is_admin=True)
|
||||
|
||||
assert inactive_user.admin is True
|
||||
assert inactive_user.is_active is True
|
||||
assert inactive_user.confirmation_token is None
|
||||
|
||||
def test_it_activates_given_user_account(
|
||||
self, app: Flask, inactive_user: User
|
||||
) -> None:
|
||||
user_manager_service = UserManagerService(
|
||||
username=inactive_user.username
|
||||
)
|
||||
|
||||
user_manager_service.update(activate=True)
|
||||
|
||||
assert inactive_user.is_active is True
|
||||
|
||||
def test_it_empties_confirmation_token(
|
||||
self, app: Flask, inactive_user: User
|
||||
) -> None:
|
||||
user_manager_service = UserManagerService(
|
||||
username=inactive_user.username
|
||||
)
|
||||
|
||||
user_manager_service.update(activate=True)
|
||||
|
||||
assert inactive_user.confirmation_token is None
|
||||
|
||||
def test_it_does_not_raise_error_if_user_account_already_activated(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
user_manager_service = UserManagerService(username=user_1.username)
|
||||
|
||||
user_manager_service.update(activate=True)
|
||||
|
||||
assert user_1.is_active is True
|
||||
|
||||
def test_it_resets_user_password(self, app: Flask, user_1: User) -> None:
|
||||
previous_password = user_1.password
|
||||
user_manager_service = UserManagerService(username=user_1.username)
|
||||
|
||||
user_manager_service.update(reset_password=True)
|
||||
|
||||
assert user_1.password != previous_password
|
||||
|
||||
def test_new_password_is_encrypted(self, app: Flask, user_1: User) -> None:
|
||||
user_manager_service = UserManagerService(username=user_1.username)
|
||||
|
||||
_, _, new_password = user_manager_service.update(reset_password=True)
|
||||
|
||||
assert bcrypt.check_password_hash(user_1.password, new_password)
|
||||
|
||||
def test_it_raises_exception_if_provided_email_is_invalid(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
user_manager_service = UserManagerService(username=user_1.username)
|
||||
with pytest.raises(
|
||||
InvalidEmailException, match='valid email must be provided'
|
||||
):
|
||||
user_manager_service.update(new_email=random_string())
|
||||
|
||||
def test_it_raises_exception_if_provided_email_is_current_user_email(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
user_manager_service = UserManagerService(username=user_1.username)
|
||||
with pytest.raises(
|
||||
InvalidEmailException,
|
||||
match='new email must be different than curent email',
|
||||
):
|
||||
user_manager_service.update(new_email=user_1.email)
|
||||
|
||||
def test_it_updates_user_email_to_confirm(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
new_email = random_email()
|
||||
current_email = user_1.email
|
||||
user_manager_service = UserManagerService(username=user_1.username)
|
||||
|
||||
user_manager_service.update(new_email=new_email)
|
||||
|
||||
assert user_1.email == current_email
|
||||
assert user_1.email_to_confirm == new_email
|
||||
assert user_1.confirmation_token is not None
|
||||
|
||||
def test_it_updates_user_email(self, app: Flask, user_1: User) -> None:
|
||||
new_email = random_email()
|
||||
user_manager_service = UserManagerService(username=user_1.username)
|
||||
|
||||
user_manager_service.update(
|
||||
new_email=new_email, with_confirmation=False
|
||||
)
|
||||
|
||||
assert user_1.email == new_email
|
||||
assert user_1.email_to_confirm is None
|
||||
assert user_1.confirmation_token is None
|
||||
|
||||
|
||||
class TestIsValidEmail:
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -40,9 +40,11 @@ from .utils.token import decode_user_token
|
||||
auth_blueprint = Blueprint('auth', __name__)
|
||||
|
||||
HEX_COLOR_REGEX = regex = "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
|
||||
NOT_FOUND_MESSAGE = 'the requested URL was not found on the server'
|
||||
|
||||
|
||||
def send_account_confirmation_email(user: User) -> None:
|
||||
if current_app.config['CAN_SEND_EMAILS']:
|
||||
ui_url = current_app.config['UI_URL']
|
||||
email_data = {
|
||||
'username': user.username,
|
||||
@ -505,7 +507,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
update authenticated user email and password
|
||||
|
||||
It sends emails:
|
||||
It sends emails if sending is enabled:
|
||||
|
||||
- Password change
|
||||
- Email change:
|
||||
@ -634,8 +636,12 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
try:
|
||||
if email_to_confirm != auth_user.email:
|
||||
if is_valid_email(email_to_confirm):
|
||||
if current_app.config['CAN_SEND_EMAILS']:
|
||||
auth_user.email_to_confirm = email_to_confirm
|
||||
auth_user.confirmation_token = secrets.token_urlsafe(30)
|
||||
else:
|
||||
auth_user.email = email_to_confirm
|
||||
auth_user.confirmation_token = None
|
||||
else:
|
||||
error_messages = 'email: valid email must be provided\n'
|
||||
|
||||
@ -652,6 +658,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
|
||||
db.session.commit()
|
||||
|
||||
if current_app.config['CAN_SEND_EMAILS']:
|
||||
ui_url = current_app.config['UI_URL']
|
||||
user_data = {
|
||||
'language': (
|
||||
@ -688,7 +695,10 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
)
|
||||
},
|
||||
}
|
||||
user_data = {**user_data, **{'email': auth_user.email_to_confirm}}
|
||||
user_data = {
|
||||
**user_data,
|
||||
**{'email': auth_user.email_to_confirm},
|
||||
}
|
||||
email_updated_to_new_address.send(user_data, email_data)
|
||||
|
||||
return {
|
||||
@ -1139,6 +1149,8 @@ def request_password_reset() -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
handle password reset request
|
||||
|
||||
If email sending is disabled, this endpoint is not available
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@ -1162,8 +1174,12 @@ def request_password_reset() -> Union[Dict, HttpResponse]:
|
||||
|
||||
:statuscode 200: password reset request processed
|
||||
:statuscode 400: invalid payload
|
||||
:statuscode 404: the requested URL was not found on the server
|
||||
|
||||
"""
|
||||
if not current_app.config['CAN_SEND_EMAILS']:
|
||||
return NotFoundErrorResponse(NOT_FOUND_MESSAGE)
|
||||
|
||||
post_data = request.get_json()
|
||||
if not post_data or post_data.get('email') is None:
|
||||
return InvalidPayloadErrorResponse()
|
||||
@ -1203,6 +1219,8 @@ def update_password() -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
update user password after password reset request
|
||||
|
||||
It sends emails if sending is enabled
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@ -1259,9 +1277,12 @@ def update_password() -> Union[Dict, HttpResponse]:
|
||||
).decode()
|
||||
db.session.commit()
|
||||
|
||||
if current_app.config['CAN_SEND_EMAILS']:
|
||||
password_change_email.send(
|
||||
{
|
||||
'language': ('en' if user.language is None else user.language),
|
||||
'language': (
|
||||
'en' if user.language is None else user.language
|
||||
),
|
||||
'email': user.email,
|
||||
},
|
||||
{
|
||||
@ -1406,6 +1427,8 @@ def resend_account_confirmation_email() -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
resend email with instructions to confirm account
|
||||
|
||||
If email sending is disabled, this endpoint is not available
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@ -1429,9 +1452,13 @@ def resend_account_confirmation_email() -> Union[Dict, HttpResponse]:
|
||||
|
||||
:statuscode 200: confirmation email resent
|
||||
:statuscode 400: invalid payload
|
||||
:statuscode 404: the requested URL was not found on the server
|
||||
:statuscode 500: error, please try again or contact the administrator
|
||||
|
||||
"""
|
||||
if not current_app.config['CAN_SEND_EMAILS']:
|
||||
return NotFoundErrorResponse(NOT_FOUND_MESSAGE)
|
||||
|
||||
post_data = request.get_json()
|
||||
if not post_data or post_data.get('email') is None:
|
||||
return InvalidPayloadErrorResponse()
|
||||
|
62
fittrackee/users/commands.py
Normal file
62
fittrackee/users/commands.py
Normal file
@ -0,0 +1,62 @@
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
|
||||
from fittrackee.cli.app import app
|
||||
from fittrackee.users.exceptions import UserNotFoundException
|
||||
from fittrackee.users.utils.admin import UserManagerService
|
||||
|
||||
|
||||
@click.group(name='users')
|
||||
def users_cli() -> None:
|
||||
"""Manage users."""
|
||||
pass
|
||||
|
||||
|
||||
@users_cli.command('update')
|
||||
@click.argument('username')
|
||||
@click.option(
|
||||
'--set-admin',
|
||||
type=bool,
|
||||
help='Add/remove admin rights (when adding admin rights, '
|
||||
'it also activates user account if not active).',
|
||||
)
|
||||
@click.option('--activate', is_flag=True, help='Activate user account.')
|
||||
@click.option(
|
||||
'--reset-password',
|
||||
is_flag=True,
|
||||
help='Reset user password (a new password will be displayed).',
|
||||
)
|
||||
@click.option('--update-email', type=str, help='Update user email.')
|
||||
def manage_user(
|
||||
username: str,
|
||||
set_admin: Optional[bool],
|
||||
activate: bool,
|
||||
reset_password: bool,
|
||||
update_email: Optional[str],
|
||||
) -> None:
|
||||
"""Manage given user account."""
|
||||
with app.app_context():
|
||||
try:
|
||||
user_manager_service = UserManagerService(username)
|
||||
_, is_user_updated, password = user_manager_service.update(
|
||||
is_admin=set_admin,
|
||||
with_confirmation=False,
|
||||
activate=activate,
|
||||
reset_password=reset_password,
|
||||
new_email=update_email,
|
||||
)
|
||||
if is_user_updated:
|
||||
click.echo(f"User '{username}' updated.")
|
||||
if password:
|
||||
click.echo(f"The new password is: {password}")
|
||||
else:
|
||||
click.echo("No updates.")
|
||||
except UserNotFoundException:
|
||||
click.echo(
|
||||
f"User '{username}' not found.\n"
|
||||
"Check the provided user name (case sensitive).",
|
||||
err=True,
|
||||
)
|
||||
except Exception as e:
|
||||
click.echo(f'An error occurred: {e}', err=True)
|
@ -1,2 +1,6 @@
|
||||
class InvalidEmailException(Exception):
|
||||
...
|
||||
|
||||
|
||||
class UserNotFoundException(Exception):
|
||||
...
|
||||
|
@ -1,13 +1,11 @@
|
||||
import os
|
||||
import secrets
|
||||
import shutil
|
||||
from typing import Any, Dict, Tuple, Union
|
||||
|
||||
import click
|
||||
from flask import Blueprint, current_app, request, send_file
|
||||
from sqlalchemy import exc
|
||||
|
||||
from fittrackee import bcrypt, db
|
||||
from fittrackee import db
|
||||
from fittrackee.emails.tasks import (
|
||||
email_updated_to_new_address,
|
||||
password_change_email,
|
||||
@ -22,31 +20,19 @@ from fittrackee.responses import (
|
||||
UserNotFoundErrorResponse,
|
||||
handle_error_and_return_response,
|
||||
)
|
||||
from fittrackee.users.utils.controls import is_valid_email
|
||||
from fittrackee.utils import get_readable_duration
|
||||
from fittrackee.workouts.models import Record, Workout, WorkoutSegment
|
||||
|
||||
from .decorators import authenticate, authenticate_as_admin
|
||||
from .exceptions import UserNotFoundException
|
||||
from .exceptions import InvalidEmailException, UserNotFoundException
|
||||
from .models import User, UserSportPreference
|
||||
from .utils.admin import set_admin_rights
|
||||
from .utils.admin import UserManagerService
|
||||
|
||||
users_blueprint = Blueprint('users', __name__)
|
||||
|
||||
USER_PER_PAGE = 10
|
||||
|
||||
|
||||
@users_blueprint.cli.command('set-admin')
|
||||
@click.argument('username')
|
||||
def set_admin(username: str) -> None:
|
||||
"""Set admin rights for given user"""
|
||||
try:
|
||||
set_admin_rights(username)
|
||||
print(f"User '{username}' updated.")
|
||||
except UserNotFoundException:
|
||||
print(f"User '{username}' not found.")
|
||||
|
||||
|
||||
@users_blueprint.route('/users', methods=['GET'])
|
||||
@authenticate_as_admin
|
||||
def get_users(auth_user: User) -> Dict:
|
||||
@ -414,8 +400,9 @@ def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
|
||||
Update user account
|
||||
|
||||
- add/remove admin rights (regardless user account status)
|
||||
- reset password (and send email to update user password)
|
||||
- update user email (and send email to update user password)
|
||||
- reset password (and send email to update user password,
|
||||
if sending enabled)
|
||||
- update user email (and send email to new user email, if sending enabled)
|
||||
- activate account for an inactive user
|
||||
|
||||
Only user with admin rights can modify another user
|
||||
@ -530,46 +517,22 @@ def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
|
||||
if not user_data:
|
||||
return InvalidPayloadErrorResponse()
|
||||
|
||||
send_password_emails = False
|
||||
send_new_address_email = False
|
||||
try:
|
||||
user = User.query.filter_by(username=user_name).first()
|
||||
if not user:
|
||||
return UserNotFoundErrorResponse()
|
||||
|
||||
if 'admin' in user_data:
|
||||
user.admin = user_data['admin']
|
||||
|
||||
if user_data.get('activate', False):
|
||||
user.is_active = True
|
||||
user.confirmation_token = None
|
||||
|
||||
if user_data.get('reset_password', False):
|
||||
new_password = secrets.token_urlsafe(30)
|
||||
user.password = bcrypt.generate_password_hash(
|
||||
new_password, current_app.config.get('BCRYPT_LOG_ROUNDS')
|
||||
).decode()
|
||||
send_password_emails = True
|
||||
|
||||
if 'new_email' in user_data:
|
||||
if is_valid_email(user_data['new_email']):
|
||||
if user_data['new_email'] == user.email:
|
||||
return InvalidPayloadErrorResponse(
|
||||
'new email must be different than curent email'
|
||||
)
|
||||
user.email_to_confirm = user_data['new_email']
|
||||
user.confirmation_token = secrets.token_urlsafe(30)
|
||||
send_new_address_email = True
|
||||
else:
|
||||
return InvalidPayloadErrorResponse(
|
||||
'valid email must be provided'
|
||||
reset_password = user_data.get('reset_password', False)
|
||||
new_email = user_data.get('new_email')
|
||||
user_manager_service = UserManagerService(username=user_name)
|
||||
user, _, _ = user_manager_service.update(
|
||||
is_admin=user_data.get('admin'),
|
||||
activate=user_data.get('activate', False),
|
||||
reset_password=reset_password,
|
||||
new_email=new_email,
|
||||
with_confirmation=current_app.config['CAN_SEND_EMAILS'],
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
if current_app.config['CAN_SEND_EMAILS']:
|
||||
user_language = 'en' if user.language is None else user.language
|
||||
ui_url = current_app.config['UI_URL']
|
||||
if send_password_emails:
|
||||
if reset_password:
|
||||
user_data = {
|
||||
'language': user_language,
|
||||
'email': user.email,
|
||||
@ -581,7 +544,9 @@ def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
|
||||
'fittrackee_url': ui_url,
|
||||
},
|
||||
)
|
||||
password_reset_token = user.encode_password_reset_token(user.id)
|
||||
password_reset_token = user.encode_password_reset_token(
|
||||
user.id
|
||||
)
|
||||
reset_password_email.send(
|
||||
user_data,
|
||||
{
|
||||
@ -593,13 +558,14 @@ def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
|
||||
),
|
||||
'username': user.username,
|
||||
'password_reset_url': (
|
||||
f'{ui_url}/password-reset?token={password_reset_token}'
|
||||
f'{ui_url}/password-reset?'
|
||||
f'token={password_reset_token}'
|
||||
),
|
||||
'fittrackee_url': ui_url,
|
||||
},
|
||||
)
|
||||
|
||||
if send_new_address_email:
|
||||
if new_email:
|
||||
user_data = {
|
||||
'language': user_language,
|
||||
'email': user.email_to_confirm,
|
||||
@ -618,6 +584,10 @@ def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
|
||||
'status': 'success',
|
||||
'data': {'users': [user.serialize(auth_user)]},
|
||||
}
|
||||
except UserNotFoundException:
|
||||
return UserNotFoundErrorResponse()
|
||||
except InvalidEmailException as e:
|
||||
return InvalidPayloadErrorResponse(str(e))
|
||||
except exc.StatementError as e:
|
||||
return handle_error_and_return_response(e, db=db)
|
||||
|
||||
|
@ -1,14 +1,86 @@
|
||||
from fittrackee import db
|
||||
import secrets
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from ..exceptions import UserNotFoundException
|
||||
from flask import current_app
|
||||
|
||||
from fittrackee import bcrypt, db
|
||||
|
||||
from ..exceptions import InvalidEmailException, UserNotFoundException
|
||||
from ..models import User
|
||||
from ..utils.controls import is_valid_email
|
||||
|
||||
|
||||
def set_admin_rights(username: str) -> None:
|
||||
user = User.query.filter_by(username=username).first()
|
||||
class UserManagerService:
|
||||
def __init__(self, username: str):
|
||||
self.username = username
|
||||
|
||||
def _get_user(self) -> User:
|
||||
user = User.query.filter_by(username=self.username).first()
|
||||
if not user:
|
||||
raise UserNotFoundException()
|
||||
user.admin = True
|
||||
return user
|
||||
|
||||
def _update_admin_rights(self, user: User, is_admin: bool) -> None:
|
||||
user.admin = is_admin
|
||||
if is_admin:
|
||||
self._activate_user(user)
|
||||
|
||||
@staticmethod
|
||||
def _activate_user(user: User) -> None:
|
||||
user.is_active = True
|
||||
user.confirmation_token = None
|
||||
|
||||
@staticmethod
|
||||
def _reset_user_password(user: User) -> str:
|
||||
new_password = secrets.token_urlsafe(30)
|
||||
user.password = bcrypt.generate_password_hash(
|
||||
new_password, current_app.config.get('BCRYPT_LOG_ROUNDS')
|
||||
).decode()
|
||||
return new_password
|
||||
|
||||
@staticmethod
|
||||
def _update_user_email(
|
||||
user: User, new_email: str, with_confirmation: bool
|
||||
) -> None:
|
||||
if not is_valid_email(new_email):
|
||||
raise InvalidEmailException('valid email must be provided')
|
||||
if user.email == new_email:
|
||||
raise InvalidEmailException(
|
||||
'new email must be different than curent email'
|
||||
)
|
||||
if with_confirmation:
|
||||
user.email_to_confirm = new_email
|
||||
user.confirmation_token = secrets.token_urlsafe(30)
|
||||
else:
|
||||
user.email = new_email
|
||||
|
||||
def update(
|
||||
self,
|
||||
is_admin: Optional[bool] = None,
|
||||
activate: bool = False,
|
||||
reset_password: bool = False,
|
||||
new_email: Optional[str] = None,
|
||||
with_confirmation: bool = True,
|
||||
) -> Tuple[User, bool, Optional[str]]:
|
||||
user_updated = False
|
||||
new_password = None
|
||||
user = self._get_user()
|
||||
|
||||
if is_admin is not None:
|
||||
self._update_admin_rights(user, is_admin)
|
||||
user_updated = True
|
||||
|
||||
if activate:
|
||||
self._activate_user(user)
|
||||
user_updated = True
|
||||
|
||||
if reset_password:
|
||||
new_password = self._reset_user_password(user)
|
||||
user_updated = True
|
||||
|
||||
if new_email is not None:
|
||||
self._update_user_email(user, new_email, with_confirmation)
|
||||
user_updated = True
|
||||
|
||||
db.session.commit()
|
||||
return user, user_updated, new_password
|
||||
|
@ -11,7 +11,7 @@
|
||||
{{ $t('admin.APPLICATION') }}
|
||||
</router-link>
|
||||
</dt>
|
||||
<dd>
|
||||
<dd class="application-config-details">
|
||||
{{ $t('admin.UPDATE_APPLICATION_DESCRIPTION') }}<br />
|
||||
<span class="registration-status">
|
||||
{{
|
||||
@ -22,6 +22,13 @@
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
class="email-sending-status"
|
||||
v-if="!appConfig.is_email_sending_enabled"
|
||||
>
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true" />
|
||||
{{ $t('admin.EMAIL_SENDING_DISABLED') }}
|
||||
</span>
|
||||
</dd>
|
||||
<dt>
|
||||
<router-link to="/admin/sports">
|
||||
@ -82,10 +89,15 @@
|
||||
dd {
|
||||
margin-bottom: $default-margin * 3;
|
||||
}
|
||||
.application-config-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.email-sending-status,
|
||||
.registration-status {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -93,7 +93,10 @@
|
||||
{{ $t('admin.UPDATE_USER_EMAIL') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="authUser.username !== user.username"
|
||||
v-if="
|
||||
authUser.username !== user.username &&
|
||||
appConfig.is_email_sending_enabled
|
||||
"
|
||||
@click.prevent="updateDisplayModal('reset')"
|
||||
>
|
||||
{{ $t('admin.RESET_USER_PASSWORD') }}
|
||||
@ -124,6 +127,7 @@
|
||||
} from 'vue'
|
||||
|
||||
import { AUTH_USER_STORE, ROOT_STORE, USERS_STORE } from '@/store/constants'
|
||||
import { TAppConfig } from '@/types/application'
|
||||
import { IAuthUserProfile, IUserProfile } from '@/types/user'
|
||||
import { useStore } from '@/use/useStore'
|
||||
|
||||
@ -157,6 +161,9 @@
|
||||
const errorMessages: ComputedRef<string | string[] | null> = computed(
|
||||
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
|
||||
)
|
||||
const appConfig: ComputedRef<TAppConfig> = computed(
|
||||
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
|
||||
)
|
||||
let displayModal: Ref<string> = ref('')
|
||||
const formErrors = ref(false)
|
||||
const displayUserEmailForm: Ref<boolean> = ref(false)
|
||||
|
@ -10,7 +10,13 @@
|
||||
<div class="profile-form form-box">
|
||||
<ErrorMessage :message="errorMessages" v-if="errorMessages" />
|
||||
<div class="info-box success-message" v-if="isSuccess">
|
||||
{{ $t(`user.PROFILE.SUCCESSFUL_${emailUpdate ? 'EMAIL_' : ''}UPDATE`) }}
|
||||
{{
|
||||
$t(
|
||||
`user.PROFILE.SUCCESSFUL_${
|
||||
emailUpdate && appConfig.is_email_sending_enabled ? 'EMAIL_' : ''
|
||||
}UPDATE`
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<form :class="{ errors: formErrors }" @submit.prevent="updateProfile">
|
||||
<label class="form-items" for="email">
|
||||
@ -77,6 +83,7 @@
|
||||
|
||||
import PasswordInput from '@/components/Common/PasswordInput.vue'
|
||||
import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants'
|
||||
import { TAppConfig } from '@/types/application'
|
||||
import { IUserProfile, IUserAccountPayload } from '@/types/user'
|
||||
import { useStore } from '@/use/useStore'
|
||||
|
||||
@ -95,6 +102,9 @@
|
||||
const loading = computed(
|
||||
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
|
||||
)
|
||||
const appConfig: ComputedRef<TAppConfig> = computed(
|
||||
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
|
||||
)
|
||||
const isSuccess: ComputedRef<boolean> = computed(
|
||||
() => store.getters[AUTH_USER_STORE.GETTERS.IS_SUCCESS]
|
||||
)
|
||||
|
@ -16,6 +16,10 @@
|
||||
message="user.REGISTER_DISABLED"
|
||||
v-if="registration_disabled"
|
||||
/>
|
||||
<AlertMessage
|
||||
message="admin.EMAIL_SENDING_DISABLED"
|
||||
v-if="sendingEmailDisabled"
|
||||
/>
|
||||
<div
|
||||
class="info-box success-message"
|
||||
v-if="isSuccess || isRegistrationSuccess"
|
||||
@ -23,7 +27,11 @@
|
||||
{{
|
||||
$t(
|
||||
`user.PROFILE.SUCCESSFUL_${
|
||||
isRegistrationSuccess ? 'REGISTRATION' : 'UPDATE'
|
||||
isRegistrationSuccess
|
||||
? `REGISTRATION${
|
||||
appConfig.is_email_sending_enabled ? '_WITH_EMAIL' : ''
|
||||
}`
|
||||
: 'UPDATE'
|
||||
}`
|
||||
)
|
||||
}}
|
||||
@ -52,7 +60,7 @@
|
||||
<input
|
||||
v-if="action !== 'reset'"
|
||||
id="email"
|
||||
:disabled="registration_disabled"
|
||||
:disabled="registration_disabled || sendingEmailDisabled"
|
||||
required
|
||||
@invalid="invalidateForm"
|
||||
type="email"
|
||||
@ -91,7 +99,10 @@
|
||||
@passwordError="invalidateForm"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" :disabled="registration_disabled">
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="registration_disabled || sendingEmailDisabled"
|
||||
>
|
||||
{{ $t(buttonText) }}
|
||||
</button>
|
||||
</form>
|
||||
@ -99,8 +110,12 @@
|
||||
<router-link class="links" to="/register">
|
||||
{{ $t('user.REGISTER') }}
|
||||
</router-link>
|
||||
-
|
||||
<router-link class="links" to="/password-reset/request">
|
||||
<span v-if="appConfig.is_email_sending_enabled">-</span>
|
||||
<router-link
|
||||
v-if="appConfig.is_email_sending_enabled"
|
||||
class="links"
|
||||
to="/password-reset/request"
|
||||
>
|
||||
{{ $t('user.PASSWORD_FORGOTTEN') }}
|
||||
</router-link>
|
||||
</div>
|
||||
@ -110,7 +125,12 @@
|
||||
{{ $t('user.LOGIN') }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div v-if="['login', 'register'].includes(action)">
|
||||
<div
|
||||
v-if="
|
||||
['login', 'register'].includes(action) &&
|
||||
appConfig.is_email_sending_enabled
|
||||
"
|
||||
>
|
||||
<router-link class="links" to="/account-confirmation/resend">
|
||||
{{ $t('user.ACCOUNT_CONFIRMATION_NOT_RECEIVED') }}
|
||||
</router-link>
|
||||
@ -175,6 +195,11 @@
|
||||
() =>
|
||||
props.action === 'register' && !appConfig.value.is_registration_enabled
|
||||
)
|
||||
const sendingEmailDisabled: ComputedRef<boolean> = computed(
|
||||
() =>
|
||||
['reset-request', 'account-confirmation-resend'].includes(props.action) &&
|
||||
!appConfig.value.is_email_sending_enabled
|
||||
)
|
||||
const formErrors = ref(false)
|
||||
|
||||
function getButtonText(action: string): string {
|
||||
|
@ -21,6 +21,7 @@
|
||||
"CONFIRM_USER_PASSWORD_RESET": "Are you sure you want to reset {0} password?",
|
||||
"CURRENT_EMAIL": "Current email",
|
||||
"DELETE_USER": "Delete user",
|
||||
"EMAIL_SENDING_DISABLED": "Email sending is disabled.",
|
||||
"ENABLE_DISABLE_SPORTS": "Enable/disable sports.",
|
||||
"NEW_EMAIL": "New email",
|
||||
"PASSWORD_RESET_SUCCESSFUL": "The password has been reset.",
|
||||
|
@ -23,7 +23,7 @@
|
||||
"signature expired, please log in again": "Signature expired. Please log in again.",
|
||||
"successfully registered": "Successfully registered.",
|
||||
"user does not exist": "User does not exist.",
|
||||
"valid email must be provided for admin contact": "A valid email must be provided for admininstrator contact",
|
||||
"valid email must be provided for admin contact": "A valid email must be provided for administrator contact",
|
||||
"you can not delete your account, no other user has admin rights": "You can not delete your account, no other user has admin rights.",
|
||||
"you do not have permissions": "You do not have permissions."
|
||||
},
|
||||
|
@ -86,7 +86,8 @@
|
||||
"STOPPED_SPEED_THRESHOLD": "stopped speed threshold"
|
||||
},
|
||||
"SUCCESSFUL_EMAIL_UPDATE": "Your account has been updated successfully. Please check your email to confirm your new email address.",
|
||||
"SUCCESSFUL_REGISTRATION": "A link to activate your account has been emailed to the address provided.",
|
||||
"SUCCESSFUL_REGISTRATION": "Your account has been created successfully.",
|
||||
"SUCCESSFUL_REGISTRATION_WITH_EMAIL": "A link to activate your account has been emailed to the address provided.",
|
||||
"SUCCESSFUL_UPDATE": "Your account has been updated successfully.",
|
||||
"UNITS": {
|
||||
"LABEL": "Units for distance",
|
||||
|
@ -21,6 +21,7 @@
|
||||
"CONFIRM_USER_PASSWORD_RESET": "Êtes-vous sûr de vouloir réinitialiser le mot de passe de l'utilisateur {0} ?",
|
||||
"CURRENT_EMAIL": "Adresse email actuelle",
|
||||
"DELETE_USER": "Supprimer l'utilisateur",
|
||||
"EMAIL_SENDING_DISABLED": "L'envoi d'emails est désactivé.",
|
||||
"ENABLE_DISABLE_SPORTS": "Activer/désactiver des sports.",
|
||||
"NEW_EMAIL": "Nouvelle adresse email",
|
||||
"PASSWORD_RESET_SUCCESSFUL": "Le mot de passe a été réinitialisé.",
|
||||
|
@ -91,7 +91,8 @@
|
||||
"STOPPED_SPEED_THRESHOLD": "seuil de vitesse arrêtée"
|
||||
},
|
||||
"SUCCESSFUL_EMAIL_UPDATE": "Votre compte a été modifié avec succès. Veuillez vérifier votre boite email pour valider votre nouvelle adresse email.",
|
||||
"SUCCESSFUL_REGISTRATION": "Un lien pour activer votre compte a été envoyé à l'adresse email fournie.",
|
||||
"SUCCESSFUL_REGISTRATION": "Votre compte a été créé avec succès.",
|
||||
"SUCCESSFUL_REGISTRATION_WITH_EMAIL": "Un lien pour activer votre compte a été envoyé à l'adresse email fournie.",
|
||||
"SUCCESSFUL_UPDATE": "Votre compte a été modifié avec succès.",
|
||||
"TIMEZONE": "Fuseau horaire"
|
||||
},
|
||||
|
@ -9,6 +9,7 @@ export type TAppConfig = {
|
||||
[key: string]: number | boolean | string
|
||||
admin_contact: string
|
||||
gpx_limit_import: number
|
||||
is_email_sending_enabled: boolean
|
||||
is_registration_enabled: boolean
|
||||
map_attribution: string
|
||||
max_single_file_size: number
|
||||
|
@ -63,9 +63,10 @@ Sphinx = "^4.5"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
fittrackee = 'fittrackee.__main__:main'
|
||||
fittrackee_set_admin = 'fittrackee.users.users:set_admin'
|
||||
fittrackee_upgrade_db = 'fittrackee.__main__:upgrade_db'
|
||||
fittrackee_worker = 'flask_dramatiq:worker'
|
||||
ftcli = 'fittrackee.cli:cli'
|
||||
fittrackee_set_admin = 'fittrackee.__main__:set_admin' # deprecated
|
||||
fittrackee_upgrade_db = 'fittrackee.__main__:upgrade_db' # deprecated
|
||||
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
|
Loading…
Reference in New Issue
Block a user