Merge pull request #181 from SamR1/smtp-optional-and-cli-refacto

make SMTP provider optional
This commit is contained in:
Sam 2022-04-24 15:23:50 +02:00 committed by GitHub
commit a5e5dcda6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 1435 additions and 286 deletions

View File

@ -1,4 +1,4 @@
FROM python:3.9
FROM python:3.10
# set working directory
RUN mkdir -p /usr/src/app

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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
View 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.

View File

@ -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**

View File

@ -34,6 +34,7 @@ Table of contents
features
installation
cli
api/index
troubleshooting/index
changelog

View File

@ -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
~~~~~~~~~~

View File

@ -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>

View File

@ -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">&quot;data&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;admin_contact&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;admin@example.com&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;gpx_limit_import&quot;</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">&quot;is_email_sending_enabled&quot;</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">&quot;is_registration_enabled&quot;</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">&quot;max_single_file_size&quot;</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">&quot;max_users&quot;</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">&quot;data&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;admin_contact&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;admin@example.com&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;gpx_limit_import&quot;</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">&quot;is_email_sending_enabled&quot;</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">&quot;is_registration_enabled&quot;</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">&quot;max_single_file_size&quot;</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">&quot;max_users&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w"></span>

View File

@ -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">&laquo; 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">&laquo; Command line ...</span>
</a>
</li>
<li>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
View 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 &#8212; 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">&laquo; 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 &raquo;</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>
&copy; Copyright 2018 - 2022, SamR1.<br/>
Created using <a href="http://sphinx-doc.org/">Sphinx</a> 4.5.0.<br/>
</p>
</div>
</footer>
</body>
</html>

View File

@ -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 &amp; 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">

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 &raquo;</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 ... &raquo;</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 &lt;username&gt;
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ ftcli users update &lt;username&gt; --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>&lt;username&gt;
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ make user-set-admin <span class="nv">USERNAME</span><span class="o">=</span>&lt;username&gt;
</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>&lt;username&gt;
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ make user-set-admin <span class="nv">USERNAME</span><span class="o">=</span>&lt;username&gt;
</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>

Binary file not shown.

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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
View 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.

View File

@ -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**

View File

@ -34,6 +34,7 @@ Table of contents
features
installation
cli
api/index
troubleshooting/index
changelog

View File

@ -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
~~~~~~~~~~

View File

@ -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

View File

@ -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')

View File

@ -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,

View File

@ -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,

View 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
View File

@ -0,0 +1,3 @@
from fittrackee import create_app
app = create_app(init_email=False)

View File

@ -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(

View File

@ -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>

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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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

View 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.')

View File

@ -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

View File

@ -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)

View File

@ -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'
)

View File

@ -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:

View File

@ -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(

View File

@ -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()

View 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)

View File

@ -1,2 +1,6 @@
class InvalidEmailException(Exception):
...
class UserNotFoundException(Exception):
...

View File

@ -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)

View File

@ -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

View File

@ -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>

View File

@ -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)

View File

@ -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]
)

View File

@ -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 {

View File

@ -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.",

View File

@ -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."
},

View File

@ -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",

View File

@ -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é.",

View File

@ -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"
},

View File

@ -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

View File

@ -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