Merge pull request #214 from SamR1/oauth2
OAuth2 Authorization Code support
This commit is contained in:
commit
59940ed41d
BIN
docs/_images/fittrackee_screenshot-07.png
Normal file
BIN
docs/_images/fittrackee_screenshot-07.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 109 KiB |
@ -7,6 +7,7 @@ API documentation
|
||||
|
||||
auth
|
||||
configuration
|
||||
oauth2
|
||||
records
|
||||
sports
|
||||
stats
|
||||
|
14
docs/_sources/api/oauth2.rst.txt
Normal file
14
docs/_sources/api/oauth2.rst.txt
Normal file
@ -0,0 +1,14 @@
|
||||
OAuth2
|
||||
######
|
||||
|
||||
.. autoflask:: fittrackee:create_app()
|
||||
:endpoints:
|
||||
oauth2.get_clients,
|
||||
oauth2.create_client,
|
||||
oauth2.get_client_by_client_id,
|
||||
oauth2.get_client_by_id,
|
||||
oauth2.delete_client,
|
||||
oauth2.revoke_client_tokens,
|
||||
oauth2.authorize,
|
||||
oauth2.issue_token,
|
||||
oauth2.revoke_token
|
42
docs/_sources/apps.rst.txt
Normal file
42
docs/_sources/apps.rst.txt
Normal file
@ -0,0 +1,42 @@
|
||||
Third-party applications
|
||||
########################
|
||||
(*new in 0.7.0*)
|
||||
|
||||
FitTrackee provides a REST API (see `documentation <api/index.html>`__) whose
|
||||
most endpoints require authorization/authentication.
|
||||
|
||||
To allow a third-party application to interact with API endpoints, an
|
||||
`OAuth2 <https://datatracker.ietf.org/doc/html/rfc6749>`_ client can be created
|
||||
in user settings ('apps' tab).
|
||||
|
||||
.. note::
|
||||
OAuth2 support is implemented with `Authlib <https://docs.authlib.org/en/latest/>`_ library.
|
||||
|
||||
.. warning::
|
||||
OAuth2 endpoints requiring authentication are not accessible by third-party
|
||||
applications (`documentation <api/oauth2.html>`__), only by FitTrackee
|
||||
client (first-party application).
|
||||
|
||||
FitTrackee supports only `Authorization Code <https://datatracker.ietf.org/doc/html/rfc6749#section-1.3.1>`_
|
||||
flow (with PKCE support).
|
||||
It allows to exchange an authorization code for an access token.
|
||||
It is recommended to use `PKCE <https://datatracker.ietf.org/doc/html/rfc7636>`_
|
||||
to provide a better security.
|
||||
|
||||
The following scopes are available:
|
||||
|
||||
- ``application:write``: grants write access to application configuration (only for users with administration rights),
|
||||
- ``profile:read``: grants read access to auth endpoints,
|
||||
- ``profile:write``: grants write access to auth endpoints,
|
||||
- ``users:read``: grants read access to users endpoints,
|
||||
- ``users:write``: grants write access to users endpoints,
|
||||
- ``workouts:read``: grants read access to workouts-related endpoints,
|
||||
- ``workouts:write``: grants write access to workouts-related endpoints.
|
||||
|
||||
.. figure:: _images/fittrackee_screenshot-07.png
|
||||
:alt: OAuth2 client creation on FitTrackee
|
||||
|
||||
Some resources about OAuth 2.0:
|
||||
|
||||
- `OAuth 2.0 Simplified <https://www.oauth.com>`_ by `Aaron Parecki <https://aaronparecki.com>`_
|
||||
- `Web App Example of OAuth 2 web application flow <https://requests-oauthlib.readthedocs.io/en/latest/examples/real_world_example.html>`_ with Requests-OAuthlib
|
@ -14,8 +14,9 @@ A command line interface (CLI) is available to manage database and users.
|
||||
--help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
db Manage database.
|
||||
users Manage users.
|
||||
db Manage database.
|
||||
oauth2 Manage OAuth2 tokens.
|
||||
users Manage users.
|
||||
|
||||
.. warning::
|
||||
| The following commands are now deprecated and will be removed in a next version:
|
||||
@ -40,6 +41,26 @@ Apply migrations.
|
||||
Empty database and delete uploaded files, only on development environments.
|
||||
|
||||
|
||||
OAuth2
|
||||
~~~~~~
|
||||
|
||||
``ftcli oauth2 clean``
|
||||
""""""""""""""""""""""
|
||||
.. versionadded:: 0.7.0
|
||||
|
||||
Remove tokens expired for more than provided number of days
|
||||
|
||||
.. cssclass:: table-bordered
|
||||
.. list-table::
|
||||
:widths: 25 50
|
||||
:header-rows: 1
|
||||
|
||||
* - Options
|
||||
- Description
|
||||
* - ``--days``
|
||||
- Number of days.
|
||||
|
||||
|
||||
|
||||
Users
|
||||
~~~~~
|
||||
|
@ -83,6 +83,8 @@ Account & preferences
|
||||
| A disabled sport (by admin or user) will not appear in dropdown when **adding a workout**.
|
||||
| A workout with a disabled sport will still be displayed in the application.
|
||||
|
||||
- A user can create `clients <apps.html>`__ for third-party applications (*new in 0.7.0*).
|
||||
|
||||
|
||||
Administration
|
||||
^^^^^^^^^^^^^^
|
||||
|
@ -33,6 +33,7 @@ Table of contents
|
||||
:maxdepth: 1
|
||||
|
||||
features
|
||||
apps
|
||||
installation
|
||||
cli
|
||||
api/index
|
||||
|
@ -9,6 +9,7 @@ This application is written in Python (API) and Typescript (client):
|
||||
- `staticmap <https://github.com/komoot/staticmap>`_ to generate a static map image from gpx coordinates
|
||||
- `python-forecast.io <https://github.com/ZeevG/python-forecast.io>`_ to fetch weather data from `Dark Sky <https://darksky.net>`__ (former forecast.io)
|
||||
- `dramatiq <https://flask-dramatiq.readthedocs.io/en/latest/>`_ for task queue
|
||||
- `Authlib <https://docs.authlib.org/en/latest/>`_ for OAuth 2.0 Authorization support
|
||||
- Client:
|
||||
- Vue3/Vuex
|
||||
- `Leaflet <https://leafletjs.com/>`__ to display map
|
||||
@ -76,6 +77,8 @@ deployment method.
|
||||
|
||||
**FitTrackee** secret key, must be initialized in production environment.
|
||||
|
||||
.. warning::
|
||||
Use a strong secret key. This key is used in JWT generation.
|
||||
|
||||
.. envvar:: APP_WORKERS
|
||||
|
||||
@ -685,6 +688,7 @@ Examples (to update depending on your application configuration and given distri
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $server_name;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,6 +61,7 @@
|
||||
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="../apps.html">Third-party applications</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>
|
||||
@ -132,7 +133,7 @@
|
||||
<dl class="http post">
|
||||
<dt class="sig sig-object http" id="post--api-auth-register">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/auth/register</span></span><a class="headerlink" href="#post--api-auth-register" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>register a user and send confirmation email.</p>
|
||||
<dd><p>Register a user and send confirmation email.</p>
|
||||
<p>The newly created account is inactive. The user must confirm his email
|
||||
to activate it.</p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
@ -204,7 +205,7 @@ character “_” allowed</p></li>
|
||||
<dl class="http post">
|
||||
<dt class="sig sig-object http" id="post--api-auth-account-confirm">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/auth/account/confirm</span></span><a class="headerlink" href="#post--api-auth-account-confirm" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>activate user account after registration</p>
|
||||
<dd><p>Activate user account after registration.</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/confirm</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>
|
||||
@ -240,8 +241,8 @@ character “_” allowed</p></li>
|
||||
<dl class="http post">
|
||||
<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>
|
||||
<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>
|
||||
@ -277,7 +278,7 @@ character “_” allowed</p></li>
|
||||
<dl class="http post">
|
||||
<dt class="sig sig-object http" id="post--api-auth-login">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/auth/login</span></span><a class="headerlink" href="#post--api-auth-login" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>user login</p>
|
||||
<dd><p>User login.</p>
|
||||
<p>Only user with an active account can log in.</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/login</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
|
||||
@ -331,7 +332,8 @@ character “_” allowed</p></li>
|
||||
<dl class="http get">
|
||||
<dt class="sig sig-object http" id="get--api-auth-profile">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/auth/profile</span></span><a class="headerlink" href="#get--api-auth-profile" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>get authenticated user info (profile, account, preferences)</p>
|
||||
<dd><p>Get authenticated user info (profile, account, preferences).</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">profile:read</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/auth/profile</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>
|
||||
@ -443,7 +445,8 @@ character “_” allowed</p></li>
|
||||
<dl class="http post">
|
||||
<dt class="sig sig-object http" id="post--api-auth-profile-edit">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/auth/profile/edit</span></span><a class="headerlink" href="#post--api-auth-profile-edit" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>edit authenticated user profile</p>
|
||||
<dd><p>Edit authenticated user profile.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">profile:write</span></code></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/profile/edit</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>
|
||||
@ -570,7 +573,8 @@ character “_” allowed</p></li>
|
||||
<dl class="http post">
|
||||
<dt class="sig sig-object http" id="post--api-auth-profile-edit-preferences">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/auth/profile/edit/preferences</span></span><a class="headerlink" href="#post--api-auth-profile-edit-preferences" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>edit authenticated user preferences</p>
|
||||
<dd><p>Edit authenticated user preferences.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">profile:write</span></code></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/profile/edit/preferences</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>
|
||||
@ -698,7 +702,8 @@ character “_” allowed</p></li>
|
||||
<dl class="http post">
|
||||
<dt class="sig sig-object http" id="post--api-auth-profile-edit-sports">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/auth/profile/edit/sports</span></span><a class="headerlink" href="#post--api-auth-profile-edit-sports" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>edit authenticated user sport preferences</p>
|
||||
<dd><p>Edit authenticated user sport preferences.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">profile:write</span></code></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/profile/edit/sports</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>
|
||||
@ -761,7 +766,8 @@ character “_” allowed</p></li>
|
||||
<dl class="http delete">
|
||||
<dt class="sig sig-object http" id="delete--api-auth-profile-reset-sports-(sport_id)">
|
||||
<span class="sig-name descname"><span class="pre">DELETE</span> </span><span class="sig-name descname"><span class="pre">/api/auth/profile/reset/sports/</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="pre">sport_id</span></em><span class="sig-paren">)</span><a class="headerlink" href="#delete--api-auth-profile-reset-sports-(sport_id)" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>reset authenticated user preferences for a given sport</p>
|
||||
<dd><p>Reset authenticated user preferences for a given sport.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">profile:write</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">DELETE</span> <span class="nn">/api/auth/profile/reset/sports/1</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>
|
||||
@ -805,7 +811,8 @@ character “_” allowed</p></li>
|
||||
<dl class="http post">
|
||||
<dt class="sig sig-object http" id="post--api-auth-picture">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/auth/picture</span></span><a class="headerlink" href="#post--api-auth-picture" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>update authenticated user picture</p>
|
||||
<dd><p>Update authenticated user picture.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">profile:write</span></code></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/picture</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">multipart/form-data</span>
|
||||
@ -858,7 +865,8 @@ character “_” allowed</p></li>
|
||||
<dl class="http delete">
|
||||
<dt class="sig sig-object http" id="delete--api-auth-picture">
|
||||
<span class="sig-name descname"><span class="pre">DELETE</span> </span><span class="sig-name descname"><span class="pre">/api/auth/picture</span></span><a class="headerlink" href="#delete--api-auth-picture" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>delete authenticated user picture</p>
|
||||
<dd><p>Delete authenticated user picture.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">profile:write</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">DELETE</span> <span class="nn">/api/auth/picture</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>
|
||||
@ -893,7 +901,7 @@ character “_” allowed</p></li>
|
||||
<dl class="http post">
|
||||
<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>
|
||||
<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>
|
||||
@ -929,7 +937,7 @@ character “_” allowed</p></li>
|
||||
<dl class="http patch">
|
||||
<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>
|
||||
<dd><p>Update authenticated user email and password.</p>
|
||||
<p>It sends emails if sending is enabled:</p>
|
||||
<ul class="simple">
|
||||
<li><p>Password change</p></li>
|
||||
@ -940,6 +948,7 @@ character “_” allowed</p></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">profile:write</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">PATCH</span> <span class="nn">/api/auth/profile/edit/account</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>
|
||||
@ -1069,8 +1078,8 @@ character “_” allowed</p></li>
|
||||
<dl class="http post">
|
||||
<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>
|
||||
<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>
|
||||
@ -1107,7 +1116,7 @@ character “_” allowed</p></li>
|
||||
<dl class="http post">
|
||||
<dt class="sig sig-object http" id="post--api-auth-email-update">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/auth/email/update</span></span><a class="headerlink" href="#post--api-auth-email-update" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>update user email after confirmation</p>
|
||||
<dd><p>Update user email after confirmation.</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/email/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>
|
||||
|
@ -17,7 +17,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="Records" href="records.html" />
|
||||
<link rel="next" title="OAuth2" href="oauth2.html" />
|
||||
<link rel="prev" title="Authentication" href="auth.html" />
|
||||
<meta charset='utf-8'>
|
||||
<meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'>
|
||||
@ -61,6 +61,7 @@
|
||||
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="../apps.html">Third-party applications</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>
|
||||
@ -93,7 +94,7 @@
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="records.html" title="Next Chapter: Records"><span class="glyphicon glyphicon-chevron-right visible-sm"></span><span class="hidden-sm hidden-tablet">Records »</span>
|
||||
<a href="oauth2.html" title="Next Chapter: OAuth2"><span class="glyphicon glyphicon-chevron-right visible-sm"></span><span class="hidden-sm hidden-tablet">OAuth2 »</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@ -132,7 +133,7 @@
|
||||
<dl class="http get">
|
||||
<dt class="sig sig-object http" id="get--api-config">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/config</span></span><a class="headerlink" href="#get--api-config" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Get Application config</p>
|
||||
<dd><p>Get Application configuration.</p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/config</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>
|
||||
@ -171,8 +172,9 @@
|
||||
<dl class="http patch">
|
||||
<dt class="sig sig-object http" id="patch--api-config">
|
||||
<span class="sig-name descname"><span class="pre">PATCH</span> </span><span class="sig-name descname"><span class="pre">/api/config</span></span><a class="headerlink" href="#patch--api-config" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Update Application config</p>
|
||||
<p>Authenticated user must be an admin</p>
|
||||
<dd><p>Update Application configuration.</p>
|
||||
<p>Authenticated user must be an admin.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">application:write</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/config</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>
|
||||
@ -203,7 +205,7 @@
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>admin_contact</strong> (<em>string</em>) – email to contact the administrator</p></li>
|
||||
<li><p><strong>gpx_limit_import</strong> (<em>integer</em>) – max number of files in zip archive</p></li>
|
||||
<li><p><strong>is_registration_enabled</strong> (<em>boolean</em>) – is registration enabled ?</p></li>
|
||||
<li><p><strong>is_registration_enabled</strong> (<em>boolean</em>) – is registration enabled?</p></li>
|
||||
<li><p><strong>max_single_file_size</strong> (<em>integer</em>) – max size of a single file</p></li>
|
||||
<li><p><strong>max_users</strong> (<em>integer</em>) – max users allowed to register on instance</p></li>
|
||||
<li><p><strong>max_zip_file_size</strong> (<em>integer</em>) – max size of a zip archive</p></li>
|
||||
|
@ -61,6 +61,7 @@
|
||||
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="../apps.html">Third-party applications</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>
|
||||
@ -134,6 +135,7 @@
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="auth.html">Authentication</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="configuration.html">Configuration</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="oauth2.html">OAuth2</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="records.html">Records</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="sports.html">Sports</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="stats.html">Statistics</a></li>
|
||||
|
700
docs/api/oauth2.html
Normal file
700
docs/api/oauth2.html
Normal file
@ -0,0 +1,700 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.19: https://docutils.sourceforge.io/" />
|
||||
|
||||
<title>OAuth2 — FitTrackee 0.6.11
|
||||
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/_sphinx_javascript_frameworks_compat.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="Records" href="records.html" />
|
||||
<link rel="prev" title="Configuration" href="configuration.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.11
|
||||
</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="../apps.html">Third-party applications</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>
|
||||
</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="#">OAuth2</a></li>
|
||||
</ul>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li>
|
||||
<a href="configuration.html" title="Previous Chapter: Configuration"><span class="glyphicon glyphicon-chevron-left visible-sm"></span><span class="hidden-sm hidden-tablet">« Configuration</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="records.html" title="Next Chapter: Records"><span class="glyphicon glyphicon-chevron-right visible-sm"></span><span class="hidden-sm hidden-tablet">Records »</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="hidden-sm">
|
||||
<div id="sourcelink">
|
||||
<a href="../_sources/api/oauth2.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="oauth2">
|
||||
<h1>OAuth2<a class="headerlink" href="#oauth2" title="Permalink to this heading">¶</a></h1>
|
||||
<dl class="http get">
|
||||
<dt class="sig sig-object http" id="get--api-oauth-apps">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/oauth/apps</span></span><a class="headerlink" href="#get--api-oauth-apps" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Get OAuth2 clients (apps) for authenticated user with pagination
|
||||
(5 clients/page).</p>
|
||||
<p>This endpoint is only accessible by FitTrackee client (first-party
|
||||
application).</p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<ul class="simple">
|
||||
<li><p>without parameters</p></li>
|
||||
</ul>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/oauth/apps</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>
|
||||
</pre></div>
|
||||
</div>
|
||||
<ul class="simple">
|
||||
<li><p>with ‘page’ parameter</p></li>
|
||||
</ul>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/oauth/apps?page=2</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>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Example response</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">SUCCESS</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
|
||||
<span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"clients"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"client_description"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"client_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"o22a27s2aBPUoxJbxV3UjDOx"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"issued_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Thu, 14 July 2022 06:27:53 GMT"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"GPX Importer"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"redirect_uris"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="s2">" https://example.com/callback"</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">],</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"scope"</span><span class="p">:</span><span class="w"> </span><span class="s2">"profile:read workouts:write"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"website"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://example.com"</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">}</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">]</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">},</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"pagination"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"has_next"</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">"has_prev"</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">"page"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"pages"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">},</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="w"></span>
|
||||
<span class="p">}</span><span class="w"></span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Query Parameters<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>page</strong> (<em>integer</em>) – page for pagination (default: 1)</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-even">Request Headers<span class="colon">:</span></dt>
|
||||
<dd class="field-even"><ul class="simple">
|
||||
<li><p><span><a class="reference external" href="https://tools.ietf.org/html/rfc7235#section-4.2">Authorization</a></span> – OAuth 2.0 Bearer Token</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-odd">Status Codes<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><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> – success</p></li>
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2">401 Unauthorized</a></span> – <ul>
|
||||
<li><p>provide a valid auth token</p></li>
|
||||
<li><p>signature expired, please log in again</p></li>
|
||||
<li><p>invalid token, please log in again</p></li>
|
||||
</ul>
|
||||
</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="http post">
|
||||
<dt class="sig sig-object http" id="post--api-oauth-apps">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/oauth/apps</span></span><a class="headerlink" href="#post--api-oauth-apps" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Create an OAuth2 client (app) for the authenticated user.</p>
|
||||
<p>This endpoint is only accessible by FitTrackee client (first-party
|
||||
application).</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/oauth/apps</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>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Example response</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">SUCCESS</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
|
||||
<span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"client"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"client_description"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"client_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"o22a27s2aBPUoxJbxV3UjDOx"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"client_secret"</span><span class="p">:</span><span class="w"> </span><span class="s2">"<CLIENT SECRET>"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"issued_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Thu, 14 July 2022 06:27:53 GMT"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"GPX Importer"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"redirect_uris"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="s2">"https://example.com/callback"</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">],</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"scope"</span><span class="p">:</span><span class="w"> </span><span class="s2">"profile:read workouts:write"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"website"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://example.com"</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">}</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">},</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"created"</span><span class="w"></span>
|
||||
<span class="p">}</span><span class="w"></span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">JSON Parameters<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>client_name</strong> (<em>string</em>) – client name</p></li>
|
||||
<li><p><strong>client_uri</strong> (<em>string</em>) – client URL</p></li>
|
||||
<li><p><strong>redirect_uri</strong> (<em>array</em>) – list of client redirect URLs (string)</p></li>
|
||||
<li><p><strong>scope</strong> (<em>string</em>) – client scopes</p></li>
|
||||
<li><p><strong>client_description</strong> (<em>string</em>) – client description (<cite>OPTIONAL</cite>)</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-even">Request Headers<span class="colon">:</span></dt>
|
||||
<dd class="field-even"><ul class="simple">
|
||||
<li><p><span><a class="reference external" href="https://tools.ietf.org/html/rfc7235#section-4.2">Authorization</a></span> – OAuth 2.0 Bearer Token</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-odd">Status Codes<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><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> – success</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> – <ul>
|
||||
<li><p>invalid payload</p></li>
|
||||
</ul>
|
||||
</p></li>
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2">401 Unauthorized</a></span> – <ul>
|
||||
<li><p>provide a valid auth token</p></li>
|
||||
<li><p>signature expired, please log in again</p></li>
|
||||
<li><p>invalid token, please log in again</p></li>
|
||||
</ul>
|
||||
</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="http get">
|
||||
<dt class="sig sig-object http" id="get--api-oauth-apps-(string-client_client_id)">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/oauth/apps/</span></span><span class="sig-paren">(</span><em class="property"><span class="pre">string:</span> </em><em class="sig-param"><span class="pre">client_client_id</span></em><span class="sig-paren">)</span><a class="headerlink" href="#get--api-oauth-apps-(string-client_client_id)" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Get an OAuth2 client (app) by ‘client_id’.</p>
|
||||
<p>This endpoint is only accessible by FitTrackee client (first-party
|
||||
application).</p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/oauth/apps/o22a27s2aBPUoxJbxV3UjDOx</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>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Example responses</strong>:</p>
|
||||
<ul class="simple">
|
||||
<li><p>success</p></li>
|
||||
</ul>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">SUCCESS</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
|
||||
<span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"client"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"client_description"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"client_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"o22a27s2aBPUoxJbxV3UjDOx"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"issued_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Thu, 14 July 2022 06:27:53 GMT"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"GPX Importer"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"redirect_uris"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="s2">"https://example.com/callback"</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">],</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"scope"</span><span class="p">:</span><span class="w"> </span><span class="s2">"profile:read workouts:write"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"website"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://example.com"</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">}</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">},</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="w"></span>
|
||||
<span class="p">}</span><span class="w"></span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<ul class="simple">
|
||||
<li><p>not found</p></li>
|
||||
</ul>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">404</span> <span class="ne">NOT FOUND</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
|
||||
<span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"not found"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OAuth2 client not found"</span><span class="w"></span>
|
||||
<span class="p">}</span><span class="w"></span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Parameters<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>client_client_id</strong> (<em>string</em>) – OAuth2 client client_id</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-even">Request Headers<span class="colon">:</span></dt>
|
||||
<dd class="field-even"><ul class="simple">
|
||||
<li><p><span><a class="reference external" href="https://tools.ietf.org/html/rfc7235#section-4.2">Authorization</a></span> – OAuth 2.0 Bearer Token</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-odd">Status Codes<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><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> – success</p></li>
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2">401 Unauthorized</a></span> – <ul>
|
||||
<li><p>provide a valid auth token</p></li>
|
||||
<li><p>signature expired, please log in again</p></li>
|
||||
<li><p>invalid token, please log in again</p></li>
|
||||
</ul>
|
||||
</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> – OAuth2 client not found</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="http get">
|
||||
<dt class="sig sig-object http" id="get--api-oauth-apps-(int-client_id)-by_id">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/oauth/apps/</span></span><span class="sig-paren">(</span><em class="property"><span class="pre">int:</span> </em><em class="sig-param"><span class="pre">client_id</span></em><span class="sig-paren">)</span><span class="sig-name descname"><span class="pre">/by_id</span></span><a class="headerlink" href="#get--api-oauth-apps-(int-client_id)-by_id" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Get an OAuth2 client (app) by id (integer value).</p>
|
||||
<p>This endpoint is only accessible by FitTrackee client (first-party
|
||||
application).</p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/oauth/apps/1/by_id</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>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Example responses</strong>:</p>
|
||||
<ul class="simple">
|
||||
<li><p>success</p></li>
|
||||
</ul>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">SUCCESS</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
|
||||
<span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"client"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"client_description"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"client_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"o22a27s2aBPUoxJbxV3UjDOx"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"issued_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Thu, 14 July 2022 06:27:53 GMT"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"GPX Importer"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"redirect_uris"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="s2">"https://example.com/callback"</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">],</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"scope"</span><span class="p">:</span><span class="w"> </span><span class="s2">"profile:read workouts:write"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"website"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://example.com"</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">}</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">},</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="w"></span>
|
||||
<span class="p">}</span><span class="w"></span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<ul class="simple">
|
||||
<li><p>not found</p></li>
|
||||
</ul>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">404</span> <span class="ne">NOT FOUND</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
|
||||
<span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"not found"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OAuth2 client not found"</span><span class="w"></span>
|
||||
<span class="p">}</span><span class="w"></span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Parameters<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>client_id</strong> (<em>integer</em>) – OAuth2 client id</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-even">Request Headers<span class="colon">:</span></dt>
|
||||
<dd class="field-even"><ul class="simple">
|
||||
<li><p><span><a class="reference external" href="https://tools.ietf.org/html/rfc7235#section-4.2">Authorization</a></span> – OAuth 2.0 Bearer Token</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-odd">Status Codes<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><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> – success</p></li>
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2">401 Unauthorized</a></span> – <ul>
|
||||
<li><p>provide a valid auth token</p></li>
|
||||
<li><p>signature expired, please log in again</p></li>
|
||||
<li><p>invalid token, please log in again</p></li>
|
||||
</ul>
|
||||
</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> – OAuth2 client not found</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="http delete">
|
||||
<dt class="sig sig-object http" id="delete--api-oauth-apps-(int-client_id)">
|
||||
<span class="sig-name descname"><span class="pre">DELETE</span> </span><span class="sig-name descname"><span class="pre">/api/oauth/apps/</span></span><span class="sig-paren">(</span><em class="property"><span class="pre">int:</span> </em><em class="sig-param"><span class="pre">client_id</span></em><span class="sig-paren">)</span><a class="headerlink" href="#delete--api-oauth-apps-(int-client_id)" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Delete an OAuth2 client (app).</p>
|
||||
<p>This endpoint is only accessible by FitTrackee client (first-party
|
||||
application).</p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">DELETE</span> <span class="nn">/api/oauth/apps/1</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>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Example response</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">204</span> <span class="ne">NO CONTENT</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Parameters<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>client_id</strong> (<em>integer</em>) – OAuth2 client id</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-even">Request Headers<span class="colon">:</span></dt>
|
||||
<dd class="field-even"><ul class="simple">
|
||||
<li><p><span><a class="reference external" href="https://tools.ietf.org/html/rfc7235#section-4.2">Authorization</a></span> – OAuth 2.0 Bearer Token</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-odd">Status Codes<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5">204 No Content</a></span> – OAuth2 client deleted</p></li>
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2">401 Unauthorized</a></span> – <ul>
|
||||
<li><p>provide a valid auth token</p></li>
|
||||
<li><p>signature expired, please log in again</p></li>
|
||||
<li><p>invalid token, please log in again</p></li>
|
||||
</ul>
|
||||
</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> – OAuth2 client not found</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="http post">
|
||||
<dt class="sig sig-object http" id="post--api-oauth-apps-(int-client_id)-revoke">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/oauth/apps/</span></span><span class="sig-paren">(</span><em class="property"><span class="pre">int:</span> </em><em class="sig-param"><span class="pre">client_id</span></em><span class="sig-paren">)</span><span class="sig-name descname"><span class="pre">/revoke</span></span><a class="headerlink" href="#post--api-oauth-apps-(int-client_id)-revoke" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Revoke all tokens associated to an OAuth2 client (app).</p>
|
||||
<p>This endpoint is only accessible by FitTrackee client (first-party
|
||||
application).</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/oauth/apps/1/revoke</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>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Example response</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">SUCCESS</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
|
||||
<span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="w"></span>
|
||||
<span class="p">}</span><span class="w"></span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Parameters<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>client_id</strong> (<em>integer</em>) – OAuth2 client id</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-even">Request Headers<span class="colon">:</span></dt>
|
||||
<dd class="field-even"><ul class="simple">
|
||||
<li><p><span><a class="reference external" href="https://tools.ietf.org/html/rfc7235#section-4.2">Authorization</a></span> – OAuth 2.0 Bearer Token</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-odd">Status Codes<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><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> – success</p></li>
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2">401 Unauthorized</a></span> – <ul>
|
||||
<li><p>provide a valid auth token</p></li>
|
||||
<li><p>signature expired, please log in again</p></li>
|
||||
<li><p>invalid token, please log in again</p></li>
|
||||
</ul>
|
||||
</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> – OAuth2 client not found</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="http post">
|
||||
<dt class="sig sig-object http" id="post--api-oauth-authorize">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/oauth/authorize</span></span><a class="headerlink" href="#post--api-oauth-authorize" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Authorize an OAuth2 client (app).
|
||||
If successful, it redirects to the client callback URL with the code to
|
||||
issue a token.</p>
|
||||
<p>This endpoint is only accessible by FitTrackee client (first-party
|
||||
application).</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/oauth/authorize</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">multipart/form-data</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Example response</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">SUCCESS</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
|
||||
<span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="w"></span>
|
||||
<span class="p">}</span><span class="w"></span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Form Parameters<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>string client_id</strong> – OAuth2 client ‘client_id’</p></li>
|
||||
<li><p><strong>string response_type</strong> – client response type (only ‘code’ is supported
|
||||
by FitTrackee)</p></li>
|
||||
<li><p><strong>string scopes</strong> – OAuth2 client scopes</p></li>
|
||||
<li><p><strong>boolean confirm</strong> – confirmation (must be ‘true’)</p></li>
|
||||
<li><p><strong>string state</strong> – unique value to prevent cross-site request forgery
|
||||
(not mandatory but recommended)</p></li>
|
||||
<li><p><strong>string code_challenge</strong> – string generated from a code verifier
|
||||
(for PKCE, not mandatory but recommended)</p></li>
|
||||
<li><p><strong>string code_challenge_method</strong> – method used to create challenge,
|
||||
for instance “S256” (mandatory if <cite>code_challenge</cite>
|
||||
provided)</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-even">Request Headers<span class="colon">:</span></dt>
|
||||
<dd class="field-even"><ul class="simple">
|
||||
<li><p><span><a class="reference external" href="https://tools.ietf.org/html/rfc7235#section-4.2">Authorization</a></span> – OAuth 2.0 Bearer Token</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-odd">Status Codes<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><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> – success</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> – <ul>
|
||||
<li><p>invalid payload</p></li>
|
||||
<li><p>errors returned by Authlib library</p></li>
|
||||
</ul>
|
||||
</p></li>
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2">401 Unauthorized</a></span> – <ul>
|
||||
<li><p>provide a valid auth token</p></li>
|
||||
<li><p>signature expired, please log in again</p></li>
|
||||
<li><p>invalid token, please log in again</p></li>
|
||||
</ul>
|
||||
</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="http post">
|
||||
<dt class="sig sig-object http" id="post--api-oauth-token">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/oauth/token</span></span><a class="headerlink" href="#post--api-oauth-token" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Issue or refresh token for a given OAuth2 client (app).</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/oauth/token</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">multipart/form-data</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Example response</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">SUCCESS</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
|
||||
<span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"access_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rOEHv64THCG28WcewZHRnVLUsOdUvw8NVnHKCmL57e"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"expires_in"</span><span class="p">:</span><span class="w"> </span><span class="mi">864000</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"refresh_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"NuV9cY8VQOnrQKHTZ5pQAq2Zw7mSH0MorNPJr14AmSwD6f6I"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"scope"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"profile:read"</span><span class="p">,</span><span class="w"> </span><span class="s2">"workouts:write"</span><span class="p">],</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"token_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Bearer"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"expires_at"</span><span class="p">:</span><span class="w"> </span><span class="mf">1658660147.0667062</span><span class="w"></span>
|
||||
<span class="p">}</span><span class="w"></span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Form Parameters<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>string client_id</strong> – OAuth2 client ‘client_id’</p></li>
|
||||
<li><p><strong>string client_secret</strong> – OAuth2 client secret</p></li>
|
||||
<li><p><strong>string grant_type</strong> – OAuth2 client grant type
|
||||
(only ‘authorization_code’ (for token issue)
|
||||
and ‘refresh_token’ (for token refresh)
|
||||
are supported by FitTrackee)</p></li>
|
||||
<li><p><strong>string code</strong> – code generated after authorizing the client
|
||||
(for token issue)</p></li>
|
||||
<li><p><strong>string code_verifier</strong> – code verifier
|
||||
(for token issue with PKCE, not mandatory)</p></li>
|
||||
<li><p><strong>string refresh_token</strong> – refresh token (for token refresh)</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-even">Status Codes<span class="colon">:</span></dt>
|
||||
<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> – success</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> – <ul>
|
||||
<li><p>errors returned by Authlib library</p></li>
|
||||
</ul>
|
||||
</p></li>
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2">401 Unauthorized</a></span> – <ul>
|
||||
<li><p>provide a valid auth token</p></li>
|
||||
<li><p>signature expired, please log in again</p></li>
|
||||
<li><p>invalid token, please log in again</p></li>
|
||||
</ul>
|
||||
</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="http post">
|
||||
<dt class="sig sig-object http" id="post--api-oauth-revoke">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/oauth/revoke</span></span><a class="headerlink" href="#post--api-oauth-revoke" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Revoke a token for a given OAuth2 client (app).</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/oauth/revoke</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">multipart/form-data</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Example response</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">SUCCESS</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
|
||||
<span class="p">{}</span><span class="w"></span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Form Parameters<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>string client_id</strong> – OAuth2 client ‘client_id’</p></li>
|
||||
<li><p><strong>string client_secret</strong> – OAuth2 client secret</p></li>
|
||||
<li><p><strong>string token</strong> – access token to revoke</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-even">Status Codes<span class="colon">:</span></dt>
|
||||
<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> – success</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> – <ul>
|
||||
<li><p>errors returned by Authlib library</p></li>
|
||||
</ul>
|
||||
</p></li>
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2">401 Unauthorized</a></span> – <ul>
|
||||
<li><p>provide a valid auth token</p></li>
|
||||
<li><p>signature expired, please log in again</p></li>
|
||||
<li><p>invalid token, please log in again</p></li>
|
||||
</ul>
|
||||
</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p class="pull-right">
|
||||
<a href="#">Back to top</a>
|
||||
|
||||
</p>
|
||||
<p>
|
||||
© Copyright 2018 - 2022, SamR1.<br/>
|
||||
Created using <a href="http://sphinx-doc.org/">Sphinx</a> 5.1.1.<br/>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
@ -18,7 +18,7 @@
|
||||
<link rel="index" title="Index" href="../genindex.html" />
|
||||
<link rel="search" title="Search" href="../search.html" />
|
||||
<link rel="next" title="Sports" href="sports.html" />
|
||||
<link rel="prev" title="Configuration" href="configuration.html" />
|
||||
<link rel="prev" title="OAuth2" href="oauth2.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 @@
|
||||
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="../apps.html">Third-party applications</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>
|
||||
@ -89,7 +90,7 @@
|
||||
|
||||
|
||||
<li>
|
||||
<a href="configuration.html" title="Previous Chapter: Configuration"><span class="glyphicon glyphicon-chevron-left visible-sm"></span><span class="hidden-sm hidden-tablet">« Configuration</span>
|
||||
<a href="oauth2.html" title="Previous Chapter: OAuth2"><span class="glyphicon glyphicon-chevron-left visible-sm"></span><span class="hidden-sm hidden-tablet">« OAuth2</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@ -143,6 +144,7 @@
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">workouts:read</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/records</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
|
@ -61,6 +61,7 @@
|
||||
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="../apps.html">Third-party applications</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>
|
||||
@ -133,6 +134,7 @@
|
||||
<dt class="sig sig-object http" id="get--api-sports">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/sports</span></span><a class="headerlink" href="#get--api-sports" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Get all sports</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">workouts:read</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/sports</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>
|
||||
@ -295,6 +297,7 @@
|
||||
<dt class="sig sig-object http" id="get--api-sports-(int-sport_id)">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/sports/</span></span><span class="sig-paren">(</span><em class="property"><span class="pre">int:</span> </em><em class="sig-param"><span class="pre">sport_id</span></em><span class="sig-paren">)</span><a class="headerlink" href="#get--api-sports-(int-sport_id)" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Get a sport</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">workouts:read</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/sports/1</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>
|
||||
@ -391,14 +394,15 @@
|
||||
<dl class="http patch">
|
||||
<dt class="sig sig-object http" id="patch--api-sports-(int-sport_id)">
|
||||
<span class="sig-name descname"><span class="pre">PATCH</span> </span><span class="sig-name descname"><span class="pre">/api/sports/</span></span><span class="sig-paren">(</span><em class="property"><span class="pre">int:</span> </em><em class="sig-param"><span class="pre">sport_id</span></em><span class="sig-paren">)</span><a class="headerlink" href="#patch--api-sports-(int-sport_id)" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Update a sport
|
||||
Authenticated user must be an admin</p>
|
||||
<dd><p>Update a sport.</p>
|
||||
<p>Authenticated user must be an admin.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">workouts:write</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">PATCH</span> <span class="nn">/api/sports/1</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>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Example response</strong>:</p>
|
||||
<p><strong>Example responses</strong>:</p>
|
||||
<ul class="simple">
|
||||
<li><p>success</p></li>
|
||||
</ul>
|
||||
|
@ -61,6 +61,7 @@
|
||||
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="../apps.html">Third-party applications</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>
|
||||
@ -132,7 +133,8 @@
|
||||
<dl class="http get">
|
||||
<dt class="sig sig-object http" id="get--api-stats-(user_name)-by_time">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/stats/</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="pre">user_name</span></em><span class="sig-paren">)</span><span class="sig-name descname"><span class="pre">/by_time</span></span><a class="headerlink" href="#get--api-stats-(user_name)-by_time" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Get workouts statistics for a user by time</p>
|
||||
<dd><p>Get workouts statistics for a user by time.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">workouts:read</span></code></p>
|
||||
<p><strong>Example requests</strong>:</p>
|
||||
<ul class="simple">
|
||||
<li><p>without parameters</p></li>
|
||||
@ -208,7 +210,7 @@
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Parameters<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>user_name</strong> (<em>integer</em>) – user name</p></li>
|
||||
<li><p><strong>user_name</strong> (<em>integer</em>) – username</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-even">Query Parameters<span class="colon">:</span></dt>
|
||||
@ -251,7 +253,8 @@
|
||||
<dl class="http get">
|
||||
<dt class="sig sig-object http" id="get--api-stats-(user_name)-by_sport">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/stats/</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="pre">user_name</span></em><span class="sig-paren">)</span><span class="sig-name descname"><span class="pre">/by_sport</span></span><a class="headerlink" href="#get--api-stats-(user_name)-by_sport" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Get workouts statistics for a user by sport</p>
|
||||
<dd><p>Get workouts statistics for a user by sport.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">workouts:read</span></code></p>
|
||||
<p><strong>Example requests</strong>:</p>
|
||||
<ul class="simple">
|
||||
<li><p>without parameters (get stats for all sports with workouts)</p></li>
|
||||
@ -322,7 +325,7 @@
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Parameters<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>user_name</strong> (<em>integer</em>) – user name</p></li>
|
||||
<li><p><strong>user_name</strong> (<em>integer</em>) – username</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-even">Query Parameters<span class="colon">:</span></dt>
|
||||
@ -357,7 +360,8 @@
|
||||
<dl class="http get">
|
||||
<dt class="sig sig-object http" id="get--api-stats-all">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/stats/all</span></span><a class="headerlink" href="#get--api-stats-all" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Get all application statistics</p>
|
||||
<dd><p>Get all application statistics.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">workouts:read</span></code></p>
|
||||
<p><strong>Example requests</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/stats/all</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
|
||||
</pre></div>
|
||||
|
@ -61,6 +61,7 @@
|
||||
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="../apps.html">Third-party applications</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>
|
||||
@ -133,8 +134,9 @@
|
||||
<dt class="sig sig-object http" id="get--api-users">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/users</span></span><a class="headerlink" href="#get--api-users" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Get all users (regardless their account status), if authenticated user
|
||||
has admin rights</p>
|
||||
has admin rights.</p>
|
||||
<p>It returns user preferences only for authenticated user.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">users:read</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<ul class="simple">
|
||||
<li><p>without parameters</p></li>
|
||||
@ -293,6 +295,7 @@ has admin rights</p>
|
||||
<dd><p>Get single user details. Only user with admin rights can get other users
|
||||
details.</p>
|
||||
<p>It returns user preferences only for authenticated user.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">users:read</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/users/admin</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>
|
||||
@ -446,7 +449,7 @@ details.</p>
|
||||
<dl class="http patch">
|
||||
<dt class="sig sig-object http" id="patch--api-users-(user_name)">
|
||||
<span class="sig-name descname"><span class="pre">PATCH</span> </span><span class="sig-name descname"><span class="pre">/api/users/</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="pre">user_name</span></em><span class="sig-paren">)</span><a class="headerlink" href="#patch--api-users-(user_name)" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Update user account</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,
|
||||
@ -454,7 +457,8 @@ 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>
|
||||
<p>Only user with admin rights can modify another user.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">users:write</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">PATCH</span> <span class="nn">/api/users/<user_name></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>
|
||||
@ -591,10 +595,11 @@ if sending enabled)</p></li>
|
||||
<dl class="http delete">
|
||||
<dt class="sig sig-object http" id="delete--api-users-(user_name)">
|
||||
<span class="sig-name descname"><span class="pre">DELETE</span> </span><span class="sig-name descname"><span class="pre">/api/users/</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="pre">user_name</span></em><span class="sig-paren">)</span><a class="headerlink" href="#delete--api-users-(user_name)" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Delete a user account</p>
|
||||
<p>A user can only delete his own account</p>
|
||||
<dd><p>Delete a user account.</p>
|
||||
<p>A user can only delete his own account.</p>
|
||||
<p>An admin can delete all accounts except his account if he’s the only
|
||||
one admin</p>
|
||||
one admin.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">users:write</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">DELETE</span> <span class="nn">/api/users/john_doe</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
|
@ -61,6 +61,7 @@
|
||||
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="../apps.html">Third-party applications</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>
|
||||
@ -133,6 +134,7 @@
|
||||
<dt class="sig sig-object http" id="get--api-workouts">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/workouts</span></span><a class="headerlink" href="#get--api-workouts" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Get workouts for the authenticated user.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">workouts:read</span></code></p>
|
||||
<p><strong>Example requests</strong>:</p>
|
||||
<ul class="simple">
|
||||
<li><p>without parameters</p></li>
|
||||
@ -294,7 +296,8 @@
|
||||
<dl class="http get">
|
||||
<dt class="sig sig-object http" id="get--api-workouts-(string-workout_short_id)">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/workouts/</span></span><span class="sig-paren">(</span><em class="property"><span class="pre">string:</span> </em><em class="sig-param"><span class="pre">workout_short_id</span></em><span class="sig-paren">)</span><a class="headerlink" href="#get--api-workouts-(string-workout_short_id)" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Get a workout</p>
|
||||
<dd><p>Get a workout.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">workouts:read</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/workouts/kjxavSTUrJvoAh2wvCeGEF</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
|
||||
</pre></div>
|
||||
@ -345,7 +348,7 @@
|
||||
</pre></div>
|
||||
</div>
|
||||
<ul class="simple">
|
||||
<li><p>acitivity not found:</p></li>
|
||||
<li><p>workout not found:</p></li>
|
||||
</ul>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">404</span> <span class="ne">NOT FOUND</span>
|
||||
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
|
||||
@ -388,7 +391,8 @@
|
||||
<dl class="http get">
|
||||
<dt class="sig sig-object http" id="get--api-workouts-(string-workout_short_id)-gpx">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/workouts/</span></span><span class="sig-paren">(</span><em class="property"><span class="pre">string:</span> </em><em class="sig-param"><span class="pre">workout_short_id</span></em><span class="sig-paren">)</span><span class="sig-name descname"><span class="pre">/gpx</span></span><a class="headerlink" href="#get--api-workouts-(string-workout_short_id)-gpx" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Get gpx file for a workout displayed on map with Leaflet</p>
|
||||
<dd><p>Get gpx file for a workout displayed on map with Leaflet.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">workouts:read</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/workouts/kjxavSTUrJvoAh2wvCeGEF/gpx</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>
|
||||
@ -441,7 +445,8 @@
|
||||
<dl class="http get">
|
||||
<dt class="sig sig-object http" id="get--api-workouts-(string-workout_short_id)-chart_data">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/workouts/</span></span><span class="sig-paren">(</span><em class="property"><span class="pre">string:</span> </em><em class="sig-param"><span class="pre">workout_short_id</span></em><span class="sig-paren">)</span><span class="sig-name descname"><span class="pre">/chart_data</span></span><a class="headerlink" href="#get--api-workouts-(string-workout_short_id)-chart_data" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Get chart data from a workout gpx file, to display it with Recharts</p>
|
||||
<dd><p>Get chart data from a workout gpx file, to display it with Chart.js.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">workouts:read</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/workouts/kjxavSTUrJvoAh2wvCeGEF/chart</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>
|
||||
@ -514,6 +519,7 @@
|
||||
<dt class="sig sig-object http" id="get--api-workouts-(string-workout_short_id)-chart_data-segment-(int-segment_id)">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/workouts/</span></span><span class="sig-paren">(</span><em class="property"><span class="pre">string:</span> </em><em class="sig-param"><span class="pre">workout_short_id</span></em><span class="sig-paren">)</span><span class="sig-name descname"><span class="pre">/chart_data/segment/</span></span><span class="sig-paren">(</span><em class="property"><span class="pre">int:</span> </em><em class="sig-param"><span class="pre">segment_id</span></em><span class="sig-paren">)</span><a class="headerlink" href="#get--api-workouts-(string-workout_short_id)-chart_data-segment-(int-segment_id)" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Get chart data from a workout gpx file, to display it with Recharts</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">workouts:read</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/workouts/kjxavSTUrJvoAh2wvCeGEF/chart/segment/0</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>
|
||||
@ -583,7 +589,8 @@
|
||||
<dl class="http get">
|
||||
<dt class="sig sig-object http" id="get--api-workouts-(string-workout_short_id)-gpx-segment-(int-segment_id)">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/workouts/</span></span><span class="sig-paren">(</span><em class="property"><span class="pre">string:</span> </em><em class="sig-param"><span class="pre">workout_short_id</span></em><span class="sig-paren">)</span><span class="sig-name descname"><span class="pre">/gpx/segment/</span></span><span class="sig-paren">(</span><em class="property"><span class="pre">int:</span> </em><em class="sig-param"><span class="pre">segment_id</span></em><span class="sig-paren">)</span><a class="headerlink" href="#get--api-workouts-(string-workout_short_id)-gpx-segment-(int-segment_id)" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Get gpx file for a workout segment displayed on map with Leaflet</p>
|
||||
<dd><p>Get gpx file for a workout segment displayed on map with Leaflet.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">workouts:read</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/workouts/kjxavSTUrJvoAh2wvCeGEF/gpx/segment/0</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>
|
||||
@ -634,7 +641,7 @@
|
||||
<dl class="http get">
|
||||
<dt class="sig sig-object http" id="get--api-workouts-map-(map_id)">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/workouts/map/</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="pre">map_id</span></em><span class="sig-paren">)</span><a class="headerlink" href="#get--api-workouts-map-(map_id)" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Get map image for workouts with gpx</p>
|
||||
<dd><p>Get map image for workouts with gpx.</p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/workouts/map/fa33f4d996844a5c73ecd1ae24456ab8?1563529507772</span>
|
||||
<span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
|
||||
@ -696,7 +703,8 @@
|
||||
<dl class="http get">
|
||||
<dt class="sig sig-object http" id="get--api-workouts-(string-workout_short_id)-gpx-download">
|
||||
<span class="sig-name descname"><span class="pre">GET</span> </span><span class="sig-name descname"><span class="pre">/api/workouts/</span></span><span class="sig-paren">(</span><em class="property"><span class="pre">string:</span> </em><em class="sig-param"><span class="pre">workout_short_id</span></em><span class="sig-paren">)</span><span class="sig-name descname"><span class="pre">/gpx/download</span></span><a class="headerlink" href="#get--api-workouts-(string-workout_short_id)-gpx-download" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Download gpx file</p>
|
||||
<dd><p>Download gpx file.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">workouts:read</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/workouts/kjxavSTUrJvoAh2wvCeGEF/gpx/download</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
|
||||
</pre></div>
|
||||
@ -734,7 +742,8 @@
|
||||
<dl class="http post">
|
||||
<dt class="sig sig-object http" id="post--api-workouts">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/workouts</span></span><a class="headerlink" href="#post--api-workouts" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Post a workout with a gpx file</p>
|
||||
<dd><p>Post a workout with a gpx file.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">workouts:write</span></code></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/workouts/</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">multipart/form-data</span>
|
||||
@ -857,7 +866,8 @@
|
||||
<dl class="http post">
|
||||
<dt class="sig sig-object http" id="post--api-workouts-no_gpx">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/workouts/no_gpx</span></span><a class="headerlink" href="#post--api-workouts-no_gpx" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Post a workout without gpx file</p>
|
||||
<dd><p>Post a workout without gpx file.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">workouts:write</span></code></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/workouts/no_gpx</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>
|
||||
@ -978,7 +988,8 @@
|
||||
<dl class="http patch">
|
||||
<dt class="sig sig-object http" id="patch--api-workouts-(string-workout_short_id)">
|
||||
<span class="sig-name descname"><span class="pre">PATCH</span> </span><span class="sig-name descname"><span class="pre">/api/workouts/</span></span><span class="sig-paren">(</span><em class="property"><span class="pre">string:</span> </em><em class="sig-param"><span class="pre">workout_short_id</span></em><span class="sig-paren">)</span><a class="headerlink" href="#patch--api-workouts-(string-workout_short_id)" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Update a workout</p>
|
||||
<dd><p>Update a workout.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">workouts:write</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">PATCH</span> <span class="nn">/api/workouts/1</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>
|
||||
@ -1108,7 +1119,8 @@
|
||||
<dl class="http delete">
|
||||
<dt class="sig sig-object http" id="delete--api-workouts-(string-workout_short_id)">
|
||||
<span class="sig-name descname"><span class="pre">DELETE</span> </span><span class="sig-name descname"><span class="pre">/api/workouts/</span></span><span class="sig-paren">(</span><em class="property"><span class="pre">string:</span> </em><em class="sig-param"><span class="pre">workout_short_id</span></em><span class="sig-paren">)</span><a class="headerlink" href="#delete--api-workouts-(string-workout_short_id)" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>Delete a workout</p>
|
||||
<dd><p>Delete a workout.</p>
|
||||
<p><strong>Scope</strong>: <code class="docutils literal notranslate"><span class="pre">workouts:write</span></code></p>
|
||||
<p><strong>Example request</strong>:</p>
|
||||
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">DELETE</span> <span class="nn">/api/workouts/kjxavSTUrJvoAh2wvCeGEF</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>
|
||||
|
192
docs/apps.html
Normal file
192
docs/apps.html
Normal file
@ -0,0 +1,192 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.19: https://docutils.sourceforge.io/" />
|
||||
|
||||
<title>Third-party applications — FitTrackee 0.6.11
|
||||
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/_sphinx_javascript_frameworks_compat.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="Installation" href="installation.html" />
|
||||
<link rel="prev" title="Features" href="features.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.11
|
||||
</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 current"><a class="current reference internal" href="#">Third-party applications</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>
|
||||
</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="#">Third-party applications</a></li>
|
||||
</ul>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li>
|
||||
<a href="features.html" title="Previous Chapter: Features"><span class="glyphicon glyphicon-chevron-left visible-sm"></span><span class="hidden-sm hidden-tablet">« Features</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="installation.html" title="Next Chapter: Installation"><span class="glyphicon glyphicon-chevron-right visible-sm"></span><span class="hidden-sm hidden-tablet">Installation »</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="hidden-sm">
|
||||
<div id="sourcelink">
|
||||
<a href="_sources/apps.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="third-party-applications">
|
||||
<h1>Third-party applications<a class="headerlink" href="#third-party-applications" title="Permalink to this heading">¶</a></h1>
|
||||
<p>(<em>new in 0.7.0</em>)</p>
|
||||
<p>FitTrackee provides a REST API (see <a class="reference external" href="api/index.html">documentation</a>) whose
|
||||
most endpoints require authorization/authentication.</p>
|
||||
<p>To allow a third-party application to interact with API endpoints, an
|
||||
<a class="reference external" href="https://datatracker.ietf.org/doc/html/rfc6749">OAuth2</a> client can be created
|
||||
in user settings (‘apps’ tab).</p>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>OAuth2 support is implemented with <a class="reference external" href="https://docs.authlib.org/en/latest/">Authlib</a> library.</p>
|
||||
</div>
|
||||
<div class="admonition warning">
|
||||
<p class="admonition-title">Warning</p>
|
||||
<p>OAuth2 endpoints requiring authentication are not accessible by third-party
|
||||
applications (<a class="reference external" href="api/oauth2.html">documentation</a>), only by FitTrackee
|
||||
client (first-party application).</p>
|
||||
</div>
|
||||
<p>FitTrackee supports only <a class="reference external" href="https://datatracker.ietf.org/doc/html/rfc6749#section-1.3.1">Authorization Code</a>
|
||||
flow (with PKCE support).
|
||||
It allows to exchange an authorization code for an access token.
|
||||
It is recommended to use <a class="reference external" href="https://datatracker.ietf.org/doc/html/rfc7636">PKCE</a>
|
||||
to provide a better security.</p>
|
||||
<p>The following scopes are available:</p>
|
||||
<ul class="simple">
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">application:write</span></code>: grants write access to application configuration (only for users with administration rights),</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">profile:read</span></code>: grants read access to auth endpoints,</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">profile:write</span></code>: grants write access to auth endpoints,</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">users:read</span></code>: grants read access to users endpoints,</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">users:write</span></code>: grants write access to users endpoints,</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">workouts:read</span></code>: grants read access to workouts-related endpoints,</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">workouts:write</span></code>: grants write access to workouts-related endpoints.</p></li>
|
||||
</ul>
|
||||
<figure class="align-default">
|
||||
<img alt="OAuth2 client creation on FitTrackee" src="_images/fittrackee_screenshot-07.png" />
|
||||
</figure>
|
||||
<p>Some resources about OAuth 2.0:</p>
|
||||
<ul class="simple">
|
||||
<li><p><a class="reference external" href="https://www.oauth.com">OAuth 2.0 Simplified</a> by <a class="reference external" href="https://aaronparecki.com">Aaron Parecki</a></p></li>
|
||||
<li><p><a class="reference external" href="https://requests-oauthlib.readthedocs.io/en/latest/examples/real_world_example.html">Web App Example of OAuth 2 web application flow</a> with Requests-OAuthlib</p></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p class="pull-right">
|
||||
<a href="#">Back to top</a>
|
||||
|
||||
</p>
|
||||
<p>
|
||||
© Copyright 2018 - 2022, SamR1.<br/>
|
||||
Created using <a href="http://sphinx-doc.org/">Sphinx</a> 5.1.1.<br/>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
@ -60,6 +60,7 @@
|
||||
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="apps.html">Third-party applications</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>
|
||||
|
@ -61,6 +61,7 @@
|
||||
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="apps.html">Third-party applications</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>
|
||||
@ -85,6 +86,10 @@
|
||||
<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="#oauth2">OAuth2</a><ul>
|
||||
<li><a class="reference internal" href="#ftcli-oauth2-clean"><code class="docutils literal notranslate"><span class="pre">ftcli</span> <span class="pre">oauth2</span> <span class="pre">clean</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>
|
||||
@ -150,8 +155,9 @@ Options:
|
||||
--help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
db Manage database.
|
||||
users Manage users.
|
||||
db Manage database.
|
||||
oauth2 Manage OAuth2 tokens.
|
||||
users Manage users.
|
||||
</pre></div>
|
||||
</div>
|
||||
<div class="admonition warning">
|
||||
@ -179,6 +185,32 @@ Commands:
|
||||
<p>Empty database and delete uploaded files, only on development environments.</p>
|
||||
</section>
|
||||
</section>
|
||||
<section id="oauth2">
|
||||
<h2>OAuth2<a class="headerlink" href="#oauth2" title="Permalink to this heading">¶</a></h2>
|
||||
<section id="ftcli-oauth2-clean">
|
||||
<h3><code class="docutils literal notranslate"><span class="pre">ftcli</span> <span class="pre">oauth2</span> <span class="pre">clean</span></code><a class="headerlink" href="#ftcli-oauth2-clean" title="Permalink to this heading">¶</a></h3>
|
||||
<div class="versionadded">
|
||||
<p><span class="versionmodified added">New in version 0.7.0.</span></p>
|
||||
</div>
|
||||
<p>Remove tokens expired for more than provided number of days</p>
|
||||
<table class="table-bordered docutils align-default">
|
||||
<colgroup>
|
||||
<col style="width: 33.3%" />
|
||||
<col style="width: 66.7%" />
|
||||
</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">--days</span></code></p></td>
|
||||
<td><p>Number of days.</p></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</section>
|
||||
<section id="users">
|
||||
<h2>Users<a class="headerlink" href="#users" title="Permalink to this heading">¶</a></h2>
|
||||
<section id="ftcli-users-update">
|
||||
|
@ -17,7 +17,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="Installation" href="installation.html" />
|
||||
<link rel="next" title="Third-party applications" href="apps.html" />
|
||||
<link rel="prev" title="FitTrackee" href="index.html" />
|
||||
<meta charset='utf-8'>
|
||||
<meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'>
|
||||
@ -61,6 +61,7 @@
|
||||
role="menu"
|
||||
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="apps.html">Third-party applications</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>
|
||||
@ -107,7 +108,7 @@
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="installation.html" title="Next Chapter: Installation"><span class="glyphicon glyphicon-chevron-right visible-sm"></span><span class="hidden-sm hidden-tablet">Installation »</span>
|
||||
<a href="apps.html" title="Next Chapter: Third-party applications"><span class="glyphicon glyphicon-chevron-right visible-sm"></span><span class="hidden-sm hidden-tablet">Third-party a... »</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@ -268,6 +269,9 @@ A user with an inactive account cannot log in. (<em>new in 0.6.0</em>)</p></li>
|
||||
<div class="line">A workout with a disabled sport will still be displayed in the application.</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="simple">
|
||||
<li><p>A user can create <a class="reference external" href="apps.html">clients</a> for third-party applications (<em>new in 0.7.0</em>).</p></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section id="administration">
|
||||
<h2>Administration<a class="headerlink" href="#administration" title="Permalink to this heading">¶</a></h2>
|
||||
|
@ -58,6 +58,7 @@
|
||||
role="menu"
|
||||
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="apps.html">Third-party applications</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>
|
||||
|
@ -65,6 +65,7 @@
|
||||
role="menu"
|
||||
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="apps.html">Third-party applications</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>
|
||||
@ -137,6 +138,21 @@
|
||||
<td>
|
||||
<a href="api/configuration.html#get--api-config"><code class="xref">GET /api/config</code></a></td><td>
|
||||
<em></em></td></tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="api/oauth2.html#get--api-oauth-apps"><code class="xref">GET /api/oauth/apps</code></a></td><td>
|
||||
<em></em></td></tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="api/oauth2.html#get--api-oauth-apps-(int-client_id)-by_id"><code class="xref">GET /api/oauth/apps/(int:client_id)/by_id</code></a></td><td>
|
||||
<em></em></td></tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="api/oauth2.html#get--api-oauth-apps-(string-client_client_id)"><code class="xref">GET /api/oauth/apps/(string:client_client_id)</code></a></td><td>
|
||||
<em></em></td></tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
@ -287,6 +303,31 @@
|
||||
<td>
|
||||
<a href="api/auth.html#post--api-auth-register"><code class="xref">POST /api/auth/register</code></a></td><td>
|
||||
<em></em></td></tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="api/oauth2.html#post--api-oauth-apps"><code class="xref">POST /api/oauth/apps</code></a></td><td>
|
||||
<em></em></td></tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="api/oauth2.html#post--api-oauth-apps-(int-client_id)-revoke"><code class="xref">POST /api/oauth/apps/(int:client_id)/revoke</code></a></td><td>
|
||||
<em></em></td></tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="api/oauth2.html#post--api-oauth-authorize"><code class="xref">POST /api/oauth/authorize</code></a></td><td>
|
||||
<em></em></td></tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="api/oauth2.html#post--api-oauth-revoke"><code class="xref">POST /api/oauth/revoke</code></a></td><td>
|
||||
<em></em></td></tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="api/oauth2.html#post--api-oauth-token"><code class="xref">POST /api/oauth/token</code></a></td><td>
|
||||
<em></em></td></tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
@ -307,6 +348,11 @@
|
||||
<td>
|
||||
<a href="api/auth.html#delete--api-auth-profile-reset-sports-(sport_id)"><code class="xref">DELETE /api/auth/profile/reset/sports/(sport_id)</code></a></td><td>
|
||||
<em></em></td></tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="api/oauth2.html#delete--api-oauth-apps-(int-client_id)"><code class="xref">DELETE /api/oauth/apps/(int:client_id)</code></a></td><td>
|
||||
<em></em></td></tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
|
@ -60,6 +60,7 @@
|
||||
role="menu"
|
||||
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="apps.html">Third-party applications</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>
|
||||
@ -154,6 +155,7 @@ Map</a>.</div>
|
||||
<div class="toctree-wrapper compound">
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="features.html">Features</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="apps.html">Third-party applications</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>
|
||||
|
@ -18,7 +18,7 @@
|
||||
<link rel="index" title="Index" href="genindex.html" />
|
||||
<link rel="search" title="Search" href="search.html" />
|
||||
<link rel="next" title="Command line interface" href="cli.html" />
|
||||
<link rel="prev" title="Features" href="features.html" />
|
||||
<link rel="prev" title="Third-party applications" href="apps.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 @@
|
||||
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="apps.html">Third-party applications</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>
|
||||
@ -121,7 +122,7 @@
|
||||
|
||||
|
||||
<li>
|
||||
<a href="features.html" title="Previous Chapter: Features"><span class="glyphicon glyphicon-chevron-left visible-sm"></span><span class="hidden-sm hidden-tablet">« Features</span>
|
||||
<a href="apps.html" title="Previous Chapter: Third-party applications"><span class="glyphicon glyphicon-chevron-left visible-sm"></span><span class="hidden-sm hidden-tablet">« Third-party a...</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@ -170,6 +171,7 @@
|
||||
<li><p><a class="reference external" href="https://github.com/komoot/staticmap">staticmap</a> to generate a static map image from gpx coordinates</p></li>
|
||||
<li><p><a class="reference external" href="https://github.com/ZeevG/python-forecast.io">python-forecast.io</a> to fetch weather data from <a class="reference external" href="https://darksky.net">Dark Sky</a> (former forecast.io)</p></li>
|
||||
<li><p><a class="reference external" href="https://flask-dramatiq.readthedocs.io/en/latest/">dramatiq</a> for task queue</p></li>
|
||||
<li><p><a class="reference external" href="https://docs.authlib.org/en/latest/">Authlib</a> for OAuth 2.0 Authorization support</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
@ -266,6 +268,10 @@ deployment method.</p>
|
||||
<dt class="sig sig-object std" id="envvar-APP_SECRET_KEY">
|
||||
<span class="sig-name descname"><span class="pre">APP_SECRET_KEY</span></span><a class="headerlink" href="#envvar-APP_SECRET_KEY" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p><strong>FitTrackee</strong> secret key, must be initialized in production environment.</p>
|
||||
<div class="admonition warning">
|
||||
<p class="admonition-title">Warning</p>
|
||||
<p>Use a strong secret key. This key is used in JWT generation.</p>
|
||||
</div>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="std envvar">
|
||||
@ -970,6 +976,7 @@ One way is to use a <strong>systemd</strong> services and <strong>Nginx</strong>
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $server_name;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
|
BIN
docs/objects.inv
BIN
docs/objects.inv
Binary file not shown.
@ -65,6 +65,7 @@
|
||||
role="menu"
|
||||
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="apps.html">Third-party applications</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>
|
||||
|
File diff suppressed because one or more lines are too long
@ -61,6 +61,7 @@
|
||||
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="../apps.html">Third-party applications</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>
|
||||
|
@ -61,6 +61,7 @@
|
||||
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="../apps.html">Third-party applications</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>
|
||||
|
@ -61,6 +61,7 @@
|
||||
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="../apps.html">Third-party applications</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>
|
||||
|
BIN
docsrc/source/_images/fittrackee_screenshot-07.png
Normal file
BIN
docsrc/source/_images/fittrackee_screenshot-07.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 109 KiB |
@ -7,6 +7,7 @@ API documentation
|
||||
|
||||
auth
|
||||
configuration
|
||||
oauth2
|
||||
records
|
||||
sports
|
||||
stats
|
||||
|
14
docsrc/source/api/oauth2.rst
Normal file
14
docsrc/source/api/oauth2.rst
Normal file
@ -0,0 +1,14 @@
|
||||
OAuth2
|
||||
######
|
||||
|
||||
.. autoflask:: fittrackee:create_app()
|
||||
:endpoints:
|
||||
oauth2.get_clients,
|
||||
oauth2.create_client,
|
||||
oauth2.get_client_by_client_id,
|
||||
oauth2.get_client_by_id,
|
||||
oauth2.delete_client,
|
||||
oauth2.revoke_client_tokens,
|
||||
oauth2.authorize,
|
||||
oauth2.issue_token,
|
||||
oauth2.revoke_token
|
42
docsrc/source/apps.rst
Normal file
42
docsrc/source/apps.rst
Normal file
@ -0,0 +1,42 @@
|
||||
Third-party applications
|
||||
########################
|
||||
(*new in 0.7.0*)
|
||||
|
||||
FitTrackee provides a REST API (see `documentation <api/index.html>`__) whose
|
||||
most endpoints require authorization/authentication.
|
||||
|
||||
To allow a third-party application to interact with API endpoints, an
|
||||
`OAuth2 <https://datatracker.ietf.org/doc/html/rfc6749>`_ client can be created
|
||||
in user settings ('apps' tab).
|
||||
|
||||
.. note::
|
||||
OAuth2 support is implemented with `Authlib <https://docs.authlib.org/en/latest/>`_ library.
|
||||
|
||||
.. warning::
|
||||
OAuth2 endpoints requiring authentication are not accessible by third-party
|
||||
applications (`documentation <api/oauth2.html>`__), only by FitTrackee
|
||||
client (first-party application).
|
||||
|
||||
FitTrackee supports only `Authorization Code <https://datatracker.ietf.org/doc/html/rfc6749#section-1.3.1>`_
|
||||
flow (with PKCE support).
|
||||
It allows to exchange an authorization code for an access token.
|
||||
It is recommended to use `PKCE <https://datatracker.ietf.org/doc/html/rfc7636>`_
|
||||
to provide a better security.
|
||||
|
||||
The following scopes are available:
|
||||
|
||||
- ``application:write``: grants write access to application configuration (only for users with administration rights),
|
||||
- ``profile:read``: grants read access to auth endpoints,
|
||||
- ``profile:write``: grants write access to auth endpoints,
|
||||
- ``users:read``: grants read access to users endpoints,
|
||||
- ``users:write``: grants write access to users endpoints,
|
||||
- ``workouts:read``: grants read access to workouts-related endpoints,
|
||||
- ``workouts:write``: grants write access to workouts-related endpoints.
|
||||
|
||||
.. figure:: _images/fittrackee_screenshot-07.png
|
||||
:alt: OAuth2 client creation on FitTrackee
|
||||
|
||||
Some resources about OAuth 2.0:
|
||||
|
||||
- `OAuth 2.0 Simplified <https://www.oauth.com>`_ by `Aaron Parecki <https://aaronparecki.com>`_
|
||||
- `Web App Example of OAuth 2 web application flow <https://requests-oauthlib.readthedocs.io/en/latest/examples/real_world_example.html>`_ with Requests-OAuthlib
|
@ -14,8 +14,9 @@ A command line interface (CLI) is available to manage database and users.
|
||||
--help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
db Manage database.
|
||||
users Manage users.
|
||||
db Manage database.
|
||||
oauth2 Manage OAuth2 tokens.
|
||||
users Manage users.
|
||||
|
||||
.. warning::
|
||||
| The following commands are now deprecated and will be removed in a next version:
|
||||
@ -40,6 +41,26 @@ Apply migrations.
|
||||
Empty database and delete uploaded files, only on development environments.
|
||||
|
||||
|
||||
OAuth2
|
||||
~~~~~~
|
||||
|
||||
``ftcli oauth2 clean``
|
||||
""""""""""""""""""""""
|
||||
.. versionadded:: 0.7.0
|
||||
|
||||
Remove tokens expired for more than provided number of days
|
||||
|
||||
.. cssclass:: table-bordered
|
||||
.. list-table::
|
||||
:widths: 25 50
|
||||
:header-rows: 1
|
||||
|
||||
* - Options
|
||||
- Description
|
||||
* - ``--days``
|
||||
- Number of days.
|
||||
|
||||
|
||||
|
||||
Users
|
||||
~~~~~
|
||||
|
@ -83,6 +83,8 @@ Account & preferences
|
||||
| A disabled sport (by admin or user) will not appear in dropdown when **adding a workout**.
|
||||
| A workout with a disabled sport will still be displayed in the application.
|
||||
|
||||
- A user can create `clients <apps.html>`__ for third-party applications (*new in 0.7.0*).
|
||||
|
||||
|
||||
Administration
|
||||
^^^^^^^^^^^^^^
|
||||
|
@ -33,6 +33,7 @@ Table of contents
|
||||
:maxdepth: 1
|
||||
|
||||
features
|
||||
apps
|
||||
installation
|
||||
cli
|
||||
api/index
|
||||
|
@ -9,6 +9,7 @@ This application is written in Python (API) and Typescript (client):
|
||||
- `staticmap <https://github.com/komoot/staticmap>`_ to generate a static map image from gpx coordinates
|
||||
- `python-forecast.io <https://github.com/ZeevG/python-forecast.io>`_ to fetch weather data from `Dark Sky <https://darksky.net>`__ (former forecast.io)
|
||||
- `dramatiq <https://flask-dramatiq.readthedocs.io/en/latest/>`_ for task queue
|
||||
- `Authlib <https://docs.authlib.org/en/latest/>`_ for OAuth 2.0 Authorization support
|
||||
- Client:
|
||||
- Vue3/Vuex
|
||||
- `Leaflet <https://leafletjs.com/>`__ to display map
|
||||
@ -76,6 +77,8 @@ deployment method.
|
||||
|
||||
**FitTrackee** secret key, must be initialized in production environment.
|
||||
|
||||
.. warning::
|
||||
Use a strong secret key. This key is used in JWT generation.
|
||||
|
||||
.. envvar:: APP_WORKERS
|
||||
|
||||
@ -685,6 +688,7 @@ Examples (to update depending on your application configuration and given distri
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $server_name;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ from flask_dramatiq import Dramatiq
|
||||
from flask_migrate import Migrate
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy.exc import ProgrammingError
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
from fittrackee.emails.email import EmailService
|
||||
from fittrackee.request import CustomRequest
|
||||
@ -64,6 +65,11 @@ def create_app(init_email: bool = True) -> Flask:
|
||||
migrate.init_app(app, db)
|
||||
dramatiq.init_app(app)
|
||||
|
||||
# set oauth2
|
||||
from fittrackee.oauth2.config import config_oauth
|
||||
|
||||
config_oauth(app)
|
||||
|
||||
# set up email if 'EMAIL_URL' is initialized
|
||||
if init_email:
|
||||
if app.config['EMAIL_URL']:
|
||||
@ -95,6 +101,7 @@ def create_app(init_email: bool = True) -> Flask:
|
||||
pass
|
||||
|
||||
from .application.app_config import config_blueprint # noqa
|
||||
from .oauth2.routes import oauth2_blueprint # noqa
|
||||
from .users.auth import auth_blueprint # noqa
|
||||
from .users.users import users_blueprint # noqa
|
||||
from .workouts.records import records_blueprint # noqa
|
||||
@ -103,6 +110,7 @@ def create_app(init_email: bool = True) -> Flask:
|
||||
from .workouts.workouts import workouts_blueprint # noqa
|
||||
|
||||
app.register_blueprint(auth_blueprint, url_prefix='/api')
|
||||
app.register_blueprint(oauth2_blueprint, url_prefix='/api')
|
||||
app.register_blueprint(config_blueprint, url_prefix='/api')
|
||||
app.register_blueprint(records_blueprint, url_prefix='/api')
|
||||
app.register_blueprint(sports_blueprint, url_prefix='/api')
|
||||
@ -153,4 +161,8 @@ def create_app(init_email: bool = True) -> Flask:
|
||||
else:
|
||||
return render_template('index.html')
|
||||
|
||||
# to get headers, especially 'X-Forwarded-Proto' for scheme needed by
|
||||
# Authlib, when the application is running behind a proxy server
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app) # type: ignore
|
||||
|
||||
return app
|
||||
|
@ -4,12 +4,12 @@ from flask import Blueprint, current_app, request
|
||||
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
|
||||
|
||||
from fittrackee import db
|
||||
from fittrackee.oauth2.server import require_auth
|
||||
from fittrackee.responses import (
|
||||
HttpResponse,
|
||||
InvalidPayloadErrorResponse,
|
||||
handle_error_and_return_response,
|
||||
)
|
||||
from fittrackee.users.decorators import authenticate_as_admin
|
||||
from fittrackee.users.models import User
|
||||
from fittrackee.users.utils.controls import is_valid_email
|
||||
|
||||
@ -22,7 +22,7 @@ config_blueprint = Blueprint('config', __name__)
|
||||
@config_blueprint.route('/config', methods=['GET'])
|
||||
def get_application_config() -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
Get Application config
|
||||
Get Application configuration.
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -67,12 +67,14 @@ def get_application_config() -> Union[Dict, HttpResponse]:
|
||||
|
||||
|
||||
@config_blueprint.route('/config', methods=['PATCH'])
|
||||
@authenticate_as_admin
|
||||
@require_auth(scopes=['application:write'], as_admin=True)
|
||||
def update_application_config(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
Update Application config
|
||||
Update Application configuration.
|
||||
|
||||
Authenticated user must be an admin
|
||||
Authenticated user must be an admin.
|
||||
|
||||
**Scope**: ``application:write``
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -105,7 +107,7 @@ def update_application_config(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
|
||||
:<json string admin_contact: email to contact the administrator
|
||||
:<json integer gpx_limit_import: max number of files in zip archive
|
||||
:<json boolean is_registration_enabled: is registration enabled ?
|
||||
:<json boolean is_registration_enabled: is registration enabled?
|
||||
:<json integer max_single_file_size: max size of a single file
|
||||
:<json integer max_users: max users allowed to register on instance
|
||||
:<json integer max_zip_file_size: max size of a zip archive
|
||||
@ -187,6 +189,5 @@ def health_check() -> Union[Dict, HttpResponse]:
|
||||
}
|
||||
|
||||
:statuscode 200: success
|
||||
|
||||
"""
|
||||
return {'status': 'success', 'message': 'pong!'}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import click
|
||||
|
||||
from fittrackee.migrations.commands import db_cli
|
||||
from fittrackee.oauth2.commands import oauth2_cli
|
||||
from fittrackee.users.commands import users_cli
|
||||
|
||||
|
||||
@ -11,4 +12,5 @@ def cli() -> None:
|
||||
|
||||
|
||||
cli.add_command(db_cli)
|
||||
cli.add_command(oauth2_cli)
|
||||
cli.add_command(users_cli)
|
||||
|
@ -51,6 +51,10 @@ class BaseConfig:
|
||||
current_app.root_path, 'emails/translations'
|
||||
)
|
||||
LANGUAGES = ['en', 'fr', 'de']
|
||||
OAUTH2_TOKEN_EXPIRES_IN = {
|
||||
'authorization_code': 864000, # 10 days
|
||||
}
|
||||
OAUTH2_REFRESH_TOKEN_GENERATOR = True
|
||||
|
||||
|
||||
class DevelopmentConfig(BaseConfig):
|
||||
@ -72,6 +76,9 @@ class TestingConfig(BaseConfig):
|
||||
PASSWORD_TOKEN_EXPIRATION_SECONDS = 3
|
||||
UI_URL = 'http://0.0.0.0:5000'
|
||||
SENDER_EMAIL = 'fittrackee@example.com'
|
||||
OAUTH2_TOKEN_EXPIRES_IN = {
|
||||
'authorization_code': 60,
|
||||
}
|
||||
|
||||
|
||||
class End2EndTestingConfig(TestingConfig):
|
||||
|
2
fittrackee/dist/index.html
vendored
2
fittrackee/dist/index.html
vendored
@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"/><link rel="stylesheet" href="/static/css/leaflet.css"/><title>FitTrackee</title><script defer="defer" src="/static/js/chunk-vendors.084be0e8.js"></script><script defer="defer" src="/static/js/app.c1babbc5.js"></script><link href="/static/css/app.f768a44b.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.084be0e8.js"></script><script defer="defer" src="/static/js/app.cf92cc0c.js"></script><link href="/static/css/app.813cd2f7.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="fittrackee_client"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but FitTrackee doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
|
2
fittrackee/dist/service-worker.js
vendored
2
fittrackee/dist/service-worker.js
vendored
File diff suppressed because one or more lines are too long
2
fittrackee/dist/service-worker.js.map
vendored
2
fittrackee/dist/service-worker.js.map
vendored
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/css/app.813cd2f7.css
vendored
Normal file
1
fittrackee/dist/static/css/app.813cd2f7.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/css/app.f768a44b.css
vendored
1
fittrackee/dist/static/css/app.f768a44b.css
vendored
File diff suppressed because one or more lines are too long
@ -1,2 +1,2 @@
|
||||
"use strict";(self["webpackChunkfittrackee_client"]=self["webpackChunkfittrackee_client"]||[]).push([[328],{6e3:function(t,e,i){i.r(e),i.d(e,{default:function(){return _}});var a=i(6252),n=i(2262),s=i(8273),c=i(8602),r=i(9917);const S=t=>((0,a.dD)("data-v-64629971"),t=t(),(0,a.Cn)(),t),l={id:"admin",class:"view"},p={key:0,class:"container"},u=S((()=>(0,a._)("div",{id:"bottom"},null,-1)));var T=(0,a.aZ)({__name:"AdminView",setup(t){const e=(0,r.o)(),i=(0,a.Fl)((()=>e.getters[c.SY.GETTERS.APP_CONFIG])),S=(0,a.Fl)((()=>e.getters[c.SY.GETTERS.APP_STATS])),T=(0,a.Fl)((()=>e.getters[c.YN.GETTERS.IS_ADMIN])),d=(0,a.Fl)((()=>e.getters[c.YN.GETTERS.USER_LOADING]));return(0,a.wF)((()=>e.dispatch(c.SY.ACTIONS.GET_APPLICATION_STATS))),(t,e)=>{const c=(0,a.up)("router-view");return(0,a.wg)(),(0,a.iD)("div",l,[(0,n.SU)(d)?(0,a.kq)("",!0):((0,a.wg)(),(0,a.iD)("div",p,[(0,n.SU)(T)?((0,a.wg)(),(0,a.j4)(c,{key:0,appConfig:(0,n.SU)(i),appStatistics:(0,n.SU)(S)},null,8,["appConfig","appStatistics"])):((0,a.wg)(),(0,a.j4)(s.Z,{key:1})),u]))])}}}),d=i(3744);const o=(0,d.Z)(T,[["__scopeId","data-v-64629971"]]);var _=o}}]);
|
||||
//# sourceMappingURL=admin.fdb1f4a5.js.map
|
||||
"use strict";(self["webpackChunkfittrackee_client"]=self["webpackChunkfittrackee_client"]||[]).push([[328],{6e3:function(t,e,i){i.r(e),i.d(e,{default:function(){return _}});var a=i(6252),n=i(2262),s=i(8273),c=i(5801),r=i(9917);const S=t=>((0,a.dD)("data-v-64629971"),t=t(),(0,a.Cn)(),t),l={id:"admin",class:"view"},p={key:0,class:"container"},u=S((()=>(0,a._)("div",{id:"bottom"},null,-1)));var T=(0,a.aZ)({__name:"AdminView",setup(t){const e=(0,r.o)(),i=(0,a.Fl)((()=>e.getters[c.SY.GETTERS.APP_CONFIG])),S=(0,a.Fl)((()=>e.getters[c.SY.GETTERS.APP_STATS])),T=(0,a.Fl)((()=>e.getters[c.YN.GETTERS.IS_ADMIN])),d=(0,a.Fl)((()=>e.getters[c.YN.GETTERS.USER_LOADING]));return(0,a.wF)((()=>e.dispatch(c.SY.ACTIONS.GET_APPLICATION_STATS))),(t,e)=>{const c=(0,a.up)("router-view");return(0,a.wg)(),(0,a.iD)("div",l,[(0,n.SU)(d)?(0,a.kq)("",!0):((0,a.wg)(),(0,a.iD)("div",p,[(0,n.SU)(T)?((0,a.wg)(),(0,a.j4)(c,{key:0,appConfig:(0,n.SU)(i),appStatistics:(0,n.SU)(S)},null,8,["appConfig","appStatistics"])):((0,a.wg)(),(0,a.j4)(s.Z,{key:1})),u]))])}}}),d=i(3744);const o=(0,d.Z)(T,[["__scopeId","data-v-64629971"]]);var _=o}}]);
|
||||
//# sourceMappingURL=admin.c5ea4b3e.js.map
|
@ -1 +1 @@
|
||||
{"version":3,"file":"static/js/admin.fdb1f4a5.js","mappings":"mOAGA,MAAMA,EAAeC,KAAMC,EAAAA,EAAAA,IAAa,mBAAmBD,EAAEA,KAAIE,EAAAA,EAAAA,MAAcF,GACzEG,EAAa,CACjBC,GAAI,QACJC,MAAO,QAEHC,EAAa,CACjBC,IAAK,EACLF,MAAO,aAEHG,EAA2BT,GAAa,KAAmBU,EAAAA,EAAAA,GAAoB,MAAO,CAAEL,GAAI,UAAY,MAAO,KAUrH,OAA4BM,EAAAA,EAAAA,IAAiB,CAC3CC,OAAQ,YACRC,MAAMC,GAEN,MAAMC,GAAQC,EAAAA,EAAAA,KAERC,GAAqCC,EAAAA,EAAAA,KACzC,IAAMH,EAAMI,QAAQC,EAAAA,GAAAA,QAAAA,cAEhBC,GAA6CH,EAAAA,EAAAA,KACjD,IAAMH,EAAMI,QAAQC,EAAAA,GAAAA,QAAAA,aAEhBE,GAAuCJ,EAAAA,EAAAA,KAC3C,IAAMH,EAAMI,QAAQI,EAAAA,GAAAA,QAAAA,YAEhBC,GAAoCN,EAAAA,EAAAA,KACxC,IAAMH,EAAMI,QAAQI,EAAAA,GAAAA,QAAAA,gBAKxB,OAFEE,EAAAA,EAAAA,KAAc,IAAMV,EAAMW,SAASN,EAAAA,GAAAA,QAAAA,yBAE9B,CAACO,EAAUC,KAChB,MAAMC,GAAyBC,EAAAA,EAAAA,IAAkB,eAEjD,OAAQC,EAAAA,EAAAA,OAAcC,EAAAA,EAAAA,IAAoB,MAAO5B,EAAY,EACzD6B,EAAAA,EAAAA,IAAOT,IAWLU,EAAAA,EAAAA,IAAoB,IAAI,KAVvBH,EAAAA,EAAAA,OAAcC,EAAAA,EAAAA,IAAoB,MAAOzB,EAAY,EACnD0B,EAAAA,EAAAA,IAAOX,KACHS,EAAAA,EAAAA,OAAcI,EAAAA,EAAAA,IAAaN,EAAwB,CAClDrB,IAAK,EACLS,WAAWgB,EAAAA,EAAAA,IAAOhB,GAClBI,eAAeY,EAAAA,EAAAA,IAAOZ,IACrB,KAAM,EAAG,CAAC,YAAa,qBACzBU,EAAAA,EAAAA,OAAcI,EAAAA,EAAAA,IAAaC,EAAAA,EAAU,CAAE5B,IAAK,KACjDC,MAVR,CAeD,I,UCvDD,MAAM4B,GAA2B,OAAgB,EAAQ,CAAC,CAAC,YAAY,qBAEvE,O","sources":["webpack://fittrackee_client/./src/views/AdminView.vue?67de","webpack://fittrackee_client/./src/views/AdminView.vue"],"sourcesContent":["import { defineComponent as _defineComponent } from 'vue'\nimport { unref as _unref, resolveComponent as _resolveComponent, openBlock as _openBlock, createBlock as _createBlock, createCommentVNode as _createCommentVNode, createElementVNode as _createElementVNode, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from \"vue\"\n\nconst _withScopeId = n => (_pushScopeId(\"data-v-64629971\"),n=n(),_popScopeId(),n)\nconst _hoisted_1 = {\n id: \"admin\",\n class: \"view\"\n}\nconst _hoisted_2 = {\n key: 0,\n class: \"container\"\n}\nconst _hoisted_3 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(\"div\", { id: \"bottom\" }, null, -1))\n\nimport { computed, ComputedRef, onBeforeMount } from 'vue'\n\n import NotFound from '@/components/Common/NotFound.vue'\n import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants'\n import { TAppConfig, IAppStatistics } from '@/types/application'\n import { useStore } from '@/use/useStore'\n\n \nexport default /*#__PURE__*/_defineComponent({\n __name: 'AdminView',\n setup(__props) {\n\n const store = useStore()\n\n const appConfig: ComputedRef<TAppConfig> = computed(\n () => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]\n )\n const appStatistics: ComputedRef<IAppStatistics> = computed(\n () => store.getters[ROOT_STORE.GETTERS.APP_STATS]\n )\n const isAuthUserAmin: ComputedRef<boolean> = computed(\n () => store.getters[AUTH_USER_STORE.GETTERS.IS_ADMIN]\n )\n const userLoading: ComputedRef<boolean> = computed(\n () => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]\n )\n\n onBeforeMount(() => store.dispatch(ROOT_STORE.ACTIONS.GET_APPLICATION_STATS))\n\nreturn (_ctx: any,_cache: any) => {\n const _component_router_view = _resolveComponent(\"router-view\")!\n\n return (_openBlock(), _createElementBlock(\"div\", _hoisted_1, [\n (!_unref(userLoading))\n ? (_openBlock(), _createElementBlock(\"div\", _hoisted_2, [\n (_unref(isAuthUserAmin))\n ? (_openBlock(), _createBlock(_component_router_view, {\n key: 0,\n appConfig: _unref(appConfig),\n appStatistics: _unref(appStatistics)\n }, null, 8, [\"appConfig\", \"appStatistics\"]))\n : (_openBlock(), _createBlock(NotFound, { key: 1 })),\n _hoisted_3\n ]))\n : _createCommentVNode(\"\", true)\n ]))\n}\n}\n\n})","import script from \"./AdminView.vue?vue&type=script&setup=true&lang=ts\"\nexport * from \"./AdminView.vue?vue&type=script&setup=true&lang=ts\"\n\nimport \"./AdminView.vue?vue&type=style&index=0&id=64629971&lang=scss&scoped=true\"\n\nimport exportComponent from \"/mnt/data-lnx/Devs/00_Perso/FitTrackee/fittrackee_client/node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['__scopeId',\"data-v-64629971\"]])\n\nexport default __exports__"],"names":["_withScopeId","n","_pushScopeId","_popScopeId","_hoisted_1","id","class","_hoisted_2","key","_hoisted_3","_createElementVNode","_defineComponent","__name","setup","__props","store","useStore","appConfig","computed","getters","ROOT_STORE","appStatistics","isAuthUserAmin","AUTH_USER_STORE","userLoading","onBeforeMount","dispatch","_ctx","_cache","_component_router_view","_resolveComponent","_openBlock","_createElementBlock","_unref","_createCommentVNode","_createBlock","NotFound","__exports__"],"sourceRoot":""}
|
||||
{"version":3,"file":"static/js/admin.c5ea4b3e.js","mappings":"mOAGA,MAAMA,EAAeC,KAAMC,EAAAA,EAAAA,IAAa,mBAAmBD,EAAEA,KAAIE,EAAAA,EAAAA,MAAcF,GACzEG,EAAa,CACjBC,GAAI,QACJC,MAAO,QAEHC,EAAa,CACjBC,IAAK,EACLF,MAAO,aAEHG,EAA2BT,GAAa,KAAmBU,EAAAA,EAAAA,GAAoB,MAAO,CAAEL,GAAI,UAAY,MAAO,KAUrH,OAA4BM,EAAAA,EAAAA,IAAiB,CAC3CC,OAAQ,YACRC,MAAMC,GAEN,MAAMC,GAAQC,EAAAA,EAAAA,KAERC,GAAqCC,EAAAA,EAAAA,KACzC,IAAMH,EAAMI,QAAQC,EAAAA,GAAAA,QAAAA,cAEhBC,GAA6CH,EAAAA,EAAAA,KACjD,IAAMH,EAAMI,QAAQC,EAAAA,GAAAA,QAAAA,aAEhBE,GAAuCJ,EAAAA,EAAAA,KAC3C,IAAMH,EAAMI,QAAQI,EAAAA,GAAAA,QAAAA,YAEhBC,GAAoCN,EAAAA,EAAAA,KACxC,IAAMH,EAAMI,QAAQI,EAAAA,GAAAA,QAAAA,gBAKxB,OAFEE,EAAAA,EAAAA,KAAc,IAAMV,EAAMW,SAASN,EAAAA,GAAAA,QAAAA,yBAE9B,CAACO,EAAUC,KAChB,MAAMC,GAAyBC,EAAAA,EAAAA,IAAkB,eAEjD,OAAQC,EAAAA,EAAAA,OAAcC,EAAAA,EAAAA,IAAoB,MAAO5B,EAAY,EACzD6B,EAAAA,EAAAA,IAAOT,IAWLU,EAAAA,EAAAA,IAAoB,IAAI,KAVvBH,EAAAA,EAAAA,OAAcC,EAAAA,EAAAA,IAAoB,MAAOzB,EAAY,EACnD0B,EAAAA,EAAAA,IAAOX,KACHS,EAAAA,EAAAA,OAAcI,EAAAA,EAAAA,IAAaN,EAAwB,CAClDrB,IAAK,EACLS,WAAWgB,EAAAA,EAAAA,IAAOhB,GAClBI,eAAeY,EAAAA,EAAAA,IAAOZ,IACrB,KAAM,EAAG,CAAC,YAAa,qBACzBU,EAAAA,EAAAA,OAAcI,EAAAA,EAAAA,IAAaC,EAAAA,EAAU,CAAE5B,IAAK,KACjDC,MAVR,CAeD,I,UCvDD,MAAM4B,GAA2B,OAAgB,EAAQ,CAAC,CAAC,YAAY,qBAEvE,O","sources":["webpack://fittrackee_client/./src/views/AdminView.vue?67de","webpack://fittrackee_client/./src/views/AdminView.vue"],"sourcesContent":["import { defineComponent as _defineComponent } from 'vue'\nimport { unref as _unref, resolveComponent as _resolveComponent, openBlock as _openBlock, createBlock as _createBlock, createCommentVNode as _createCommentVNode, createElementVNode as _createElementVNode, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from \"vue\"\n\nconst _withScopeId = n => (_pushScopeId(\"data-v-64629971\"),n=n(),_popScopeId(),n)\nconst _hoisted_1 = {\n id: \"admin\",\n class: \"view\"\n}\nconst _hoisted_2 = {\n key: 0,\n class: \"container\"\n}\nconst _hoisted_3 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(\"div\", { id: \"bottom\" }, null, -1))\n\nimport { computed, ComputedRef, onBeforeMount } from 'vue'\n\n import NotFound from '@/components/Common/NotFound.vue'\n import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants'\n import { TAppConfig, IAppStatistics } from '@/types/application'\n import { useStore } from '@/use/useStore'\n\n \nexport default /*#__PURE__*/_defineComponent({\n __name: 'AdminView',\n setup(__props) {\n\n const store = useStore()\n\n const appConfig: ComputedRef<TAppConfig> = computed(\n () => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]\n )\n const appStatistics: ComputedRef<IAppStatistics> = computed(\n () => store.getters[ROOT_STORE.GETTERS.APP_STATS]\n )\n const isAuthUserAmin: ComputedRef<boolean> = computed(\n () => store.getters[AUTH_USER_STORE.GETTERS.IS_ADMIN]\n )\n const userLoading: ComputedRef<boolean> = computed(\n () => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]\n )\n\n onBeforeMount(() => store.dispatch(ROOT_STORE.ACTIONS.GET_APPLICATION_STATS))\n\nreturn (_ctx: any,_cache: any) => {\n const _component_router_view = _resolveComponent(\"router-view\")!\n\n return (_openBlock(), _createElementBlock(\"div\", _hoisted_1, [\n (!_unref(userLoading))\n ? (_openBlock(), _createElementBlock(\"div\", _hoisted_2, [\n (_unref(isAuthUserAmin))\n ? (_openBlock(), _createBlock(_component_router_view, {\n key: 0,\n appConfig: _unref(appConfig),\n appStatistics: _unref(appStatistics)\n }, null, 8, [\"appConfig\", \"appStatistics\"]))\n : (_openBlock(), _createBlock(NotFound, { key: 1 })),\n _hoisted_3\n ]))\n : _createCommentVNode(\"\", true)\n ]))\n}\n}\n\n})","import script from \"./AdminView.vue?vue&type=script&setup=true&lang=ts\"\nexport * from \"./AdminView.vue?vue&type=script&setup=true&lang=ts\"\n\nimport \"./AdminView.vue?vue&type=style&index=0&id=64629971&lang=scss&scoped=true\"\n\nimport exportComponent from \"/mnt/data-lnx/Devs/00_Perso/FitTrackee/fittrackee_client/node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['__scopeId',\"data-v-64629971\"]])\n\nexport default __exports__"],"names":["_withScopeId","n","_pushScopeId","_popScopeId","_hoisted_1","id","class","_hoisted_2","key","_hoisted_3","_createElementVNode","_defineComponent","__name","setup","__props","store","useStore","appConfig","computed","getters","ROOT_STORE","appStatistics","isAuthUserAmin","AUTH_USER_STORE","userLoading","onBeforeMount","dispatch","_ctx","_cache","_component_router_view","_resolveComponent","_openBlock","_createElementBlock","_unref","_createCommentVNode","_createBlock","NotFound","__exports__"],"sourceRoot":""}
|
2
fittrackee/dist/static/js/app.c1babbc5.js
vendored
2
fittrackee/dist/static/js/app.c1babbc5.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/app.cf92cc0c.js
vendored
Normal file
2
fittrackee/dist/static/js/app.cf92cc0c.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/js/app.cf92cc0c.js.map
vendored
Normal file
1
fittrackee/dist/static/js/app.cf92cc0c.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,2 +1,2 @@
|
||||
"use strict";(self["webpackChunkfittrackee_client"]=self["webpackChunkfittrackee_client"]||[]).push([[845],{4264:function(e,t,r){r.r(t),r.d(t,{default:function(){return m}});var n=r(6252),a=r(2262),s=r(3577),u=r(2201),o=r(7167),i=r(8602),c=r(9917);const l={key:0,id:"account-confirmation",class:"center-card with-margin"},E={class:"error-message"};var _=(0,n.aZ)({__name:"AccountConfirmationView",setup(e){const t=(0,u.yj)(),r=(0,u.tv)(),_=(0,c.o)(),d=(0,n.Fl)((()=>_.getters[i.SY.GETTERS.ERROR_MESSAGES])),S=(0,n.Fl)((()=>t.query.token));function m(){S.value?_.dispatch(i.YN.ACTIONS.CONFIRM_ACCOUNT,{token:S.value}):r.push("/")}return(0,n.wF)((()=>m())),(0,n.Ah)((()=>_.commit(i.SY.MUTATIONS.EMPTY_ERROR_MESSAGES))),(e,t)=>{const r=(0,n.up)("router-link");return(0,a.SU)(d)?((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)}}}),d=r(3744);const S=(0,d.Z)(_,[["__scopeId","data-v-785df978"]]);var m=S},8160:function(e,t,r){r.r(t),r.d(t,{default:function(){return m}});var n=r(6252),a=r(2262),s=r(3577),u=r(2201),o=r(7167),i=r(8602),c=r(9917);const l={key:0,id:"email-update",class:"center-card with-margin"},E={class:"error-message"};var _=(0,n.aZ)({__name:"EmailUpdateView",setup(e){const t=(0,u.yj)(),r=(0,u.tv)(),_=(0,c.o)(),d=(0,n.Fl)((()=>_.getters[i.YN.GETTERS.AUTH_USER_PROFILE])),S=(0,n.Fl)((()=>_.getters[i.YN.GETTERS.IS_AUTHENTICATED])),m=(0,n.Fl)((()=>_.getters[i.SY.GETTERS.ERROR_MESSAGES])),p=(0,n.Fl)((()=>t.query.token));function R(){p.value?_.dispatch(i.YN.ACTIONS.CONFIRM_EMAIL,{token:p.value,refreshUser:S.value}):r.push("/")}return(0,n.wF)((()=>R())),(0,n.Ah)((()=>_.commit(i.SY.MUTATIONS.EMPTY_ERROR_MESSAGES))),(0,n.YP)((()=>m.value),(e=>{d.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)(d).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)}}}),d=r(3744);const S=(0,d.Z)(_,[["__scopeId","data-v-8c2ec9ce"]]);var m=S},6266:function(e,t,r){r.r(t),r.d(t,{default:function(){return d}});var n=r(6252),a=r(2262),s=r(8602),u=r(9917);const o=e=>((0,n.dD)("data-v-05463732"),e=e(),(0,n.Cn)(),e),i={key:0,id:"profile",class:"container view"},c=o((()=>(0,n._)("div",{id:"bottom"},null,-1)));var l=(0,n.aZ)({__name:"ProfileView",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",i,[(0,n.Wm)(s,{user:(0,a.SU)(r)},null,8,["user"]),c])):(0,n.kq)("",!0)}}}),E=r(3744);const _=(0,E.Z)(l,[["__scopeId","data-v-05463732"]]);var d=_},9453:function(e,t,r){r.r(t),r.d(t,{default:function(){return m}});var n=r(6252),a=r(2262),s=r(2201),u=r(2179),o=r(7408),i=r(8602),c=r(9917);const l={key:0,id:"user",class:"view"},E={class:"box"};var _=(0,n.aZ)({__name:"UserView",props:{fromAdmin:{type:Boolean}},setup(e){const t=e,{fromAdmin:r}=(0,a.BK)(t),_=(0,s.yj)(),d=(0,c.o)(),S=(0,n.Fl)((()=>d.getters[i.RT.GETTERS.USER]));return(0,n.wF)((()=>{_.params.username&&"string"===typeof _.params.username&&d.dispatch(i.RT.ACTIONS.GET_USER,_.params.username)})),(0,n.Jd)((()=>{d.dispatch(i.RT.ACTIONS.EMPTY_USER)})),(e,t)=>(0,a.SU)(S).username?((0,n.wg)(),(0,n.iD)("div",l,[(0,n.Wm)(u.Z,{user:(0,a.SU)(S)},null,8,["user"]),(0,n._)("div",E,[(0,n.Wm)(o.Z,{user:(0,a.SU)(S),"from-admin":(0,a.SU)(r)},null,8,["user","from-admin"])])])):(0,n.kq)("",!0)}}),d=r(3744);const S=(0,d.Z)(_,[["__scopeId","data-v-af7007f4"]]);var m=S}}]);
|
||||
//# sourceMappingURL=profile.8055844e.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(6252),a=r(2262),s=r(3577),u=r(2201),o=r(7167),i=r(5801),c=r(9917);const l={key:0,id:"account-confirmation",class:"center-card with-margin"},E={class:"error-message"};var _=(0,n.aZ)({__name:"AccountConfirmationView",setup(e){const t=(0,u.yj)(),r=(0,u.tv)(),_=(0,c.o)(),d=(0,n.Fl)((()=>_.getters[i.SY.GETTERS.ERROR_MESSAGES])),S=(0,n.Fl)((()=>t.query.token));function m(){S.value?_.dispatch(i.YN.ACTIONS.CONFIRM_ACCOUNT,{token:S.value}):r.push("/")}return(0,n.wF)((()=>m())),(0,n.Ah)((()=>_.commit(i.SY.MUTATIONS.EMPTY_ERROR_MESSAGES))),(e,t)=>{const r=(0,n.up)("router-link");return(0,a.SU)(d)?((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)}}}),d=r(3744);const S=(0,d.Z)(_,[["__scopeId","data-v-785df978"]]);var m=S},8160:function(e,t,r){r.r(t),r.d(t,{default:function(){return m}});var n=r(6252),a=r(2262),s=r(3577),u=r(2201),o=r(7167),i=r(5801),c=r(9917);const l={key:0,id:"email-update",class:"center-card with-margin"},E={class:"error-message"};var _=(0,n.aZ)({__name:"EmailUpdateView",setup(e){const t=(0,u.yj)(),r=(0,u.tv)(),_=(0,c.o)(),d=(0,n.Fl)((()=>_.getters[i.YN.GETTERS.AUTH_USER_PROFILE])),S=(0,n.Fl)((()=>_.getters[i.YN.GETTERS.IS_AUTHENTICATED])),m=(0,n.Fl)((()=>_.getters[i.SY.GETTERS.ERROR_MESSAGES])),p=(0,n.Fl)((()=>t.query.token));function R(){p.value?_.dispatch(i.YN.ACTIONS.CONFIRM_EMAIL,{token:p.value,refreshUser:S.value}):r.push("/")}return(0,n.wF)((()=>R())),(0,n.Ah)((()=>_.commit(i.SY.MUTATIONS.EMPTY_ERROR_MESSAGES))),(0,n.YP)((()=>m.value),(e=>{d.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)(d).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)}}}),d=r(3744);const S=(0,d.Z)(_,[["__scopeId","data-v-8c2ec9ce"]]);var m=S},6266:function(e,t,r){r.r(t),r.d(t,{default:function(){return d}});var n=r(6252),a=r(2262),s=r(5801),u=r(9917);const o=e=>((0,n.dD)("data-v-05463732"),e=e(),(0,n.Cn)(),e),i={key:0,id:"profile",class:"container view"},c=o((()=>(0,n._)("div",{id:"bottom"},null,-1)));var l=(0,n.aZ)({__name:"ProfileView",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",i,[(0,n.Wm)(s,{user:(0,a.SU)(r)},null,8,["user"]),c])):(0,n.kq)("",!0)}}}),E=r(3744);const _=(0,E.Z)(l,[["__scopeId","data-v-05463732"]]);var d=_},9453:function(e,t,r){r.r(t),r.d(t,{default:function(){return m}});var n=r(6252),a=r(2262),s=r(2201),u=r(2179),o=r(7408),i=r(5801),c=r(9917);const l={key:0,id:"user",class:"view"},E={class:"box"};var _=(0,n.aZ)({__name:"UserView",props:{fromAdmin:{type:Boolean}},setup(e){const t=e,{fromAdmin:r}=(0,a.BK)(t),_=(0,s.yj)(),d=(0,c.o)(),S=(0,n.Fl)((()=>d.getters[i.RT.GETTERS.USER]));return(0,n.wF)((()=>{_.params.username&&"string"===typeof _.params.username&&d.dispatch(i.RT.ACTIONS.GET_USER,_.params.username)})),(0,n.Jd)((()=>{d.dispatch(i.RT.ACTIONS.EMPTY_USER)})),(e,t)=>(0,a.SU)(S).username?((0,n.wg)(),(0,n.iD)("div",l,[(0,n.Wm)(u.Z,{user:(0,a.SU)(S)},null,8,["user"]),(0,n._)("div",E,[(0,n.Wm)(o.Z,{user:(0,a.SU)(S),"from-admin":(0,a.SU)(r)},null,8,["user","from-admin"])])])):(0,n.kq)("",!0)}}),d=r(3744);const S=(0,d.Z)(_,[["__scopeId","data-v-af7007f4"]]);var m=S}}]);
|
||||
//# sourceMappingURL=profile.ffabeabf.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,2 +1,2 @@
|
||||
"use strict";(self["webpackChunkfittrackee_client"]=self["webpackChunkfittrackee_client"]||[]).push([[193],{9161:function(e,s,t){t.r(s),t.d(s,{default:function(){return A}});t(6699);var a=t(6252),r=t(2262),l=t(3577),o=t(3324),n=t(4998);const c={class:"chart-menu"},i={class:"chart-arrow"},u={class:"time-frames custom-checkboxes-group"},d={class:"time-frames-checkboxes custom-checkboxes"},p=["id","name","checked","onInput"],m={class:"chart-arrow"};var v=(0,a.aZ)({__name:"StatsMenu",emits:["arrowClick","timeFrameUpdate"],setup(e,{emit:s}){const t=(0,r.iH)("month"),o=["week","month","year"];function n(e){t.value=e,s("timeFrameUpdate",e)}return(e,r)=>((0,a.wg)(),(0,a.iD)("div",c,[(0,a._)("div",i,[(0,a._)("i",{class:"fa fa-chevron-left","aria-hidden":"true",onClick:r[0]||(r[0]=e=>s("arrowClick",!0))})]),(0,a._)("div",u,[(0,a._)("div",d,[((0,a.wg)(),(0,a.iD)(a.HY,null,(0,a.Ko)(o,(s=>(0,a._)("div",{class:"time-frame custom-checkbox",key:s},[(0,a._)("label",null,[(0,a._)("input",{type:"radio",id:s,name:s,checked:t.value===s,onInput:e=>n(s)},null,40,p),(0,a._)("span",null,(0,l.zw)(e.$t(`statistics.TIME_FRAMES.${s}`)),1)])]))),64))])]),(0,a._)("div",m,[(0,a._)("i",{class:"fa fa-chevron-right","aria-hidden":"true",onClick:r[1]||(r[1]=e=>s("arrowClick",!1))})])]))}}),k=t(3744);const _=(0,k.Z)(v,[["__scopeId","data-v-22d55de2"]]);var S=_,w=t(631);const f={class:"sports-menu"},h=["id","name","checked","onInput"],U={class:"sport-label"};var b=(0,a.aZ)({__name:"StatsSportsMenu",props:{userSports:null,selectedSportIds:{default:()=>[]}},emits:["selectedSportIdsUpdate"],setup(e,{emit:s}){const t=e,{t:n}=(0,o.QT)(),c=(0,a.f3)("sportColors"),{selectedSportIds:i}=(0,r.BK)(t),u=(0,a.Fl)((()=>(0,w.xH)(t.userSports,n)));function d(e){s("selectedSportIdsUpdate",e)}return(e,s)=>{const t=(0,a.up)("SportImage");return(0,a.wg)(),(0,a.iD)("div",f,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)((0,r.SU)(u),(e=>((0,a.wg)(),(0,a.iD)("label",{type:"checkbox",key:e.id,style:(0,l.j5)({color:e.color?e.color:(0,r.SU)(c)[e.label]})},[(0,a._)("input",{type:"checkbox",id:e.id,name:e.label,checked:(0,r.SU)(i).includes(e.id),onInput:s=>d(e.id)},null,40,h),(0,a.Wm)(t,{"sport-label":e.label,color:e.color},null,8,["sport-label","color"]),(0,a._)("span",U,(0,l.zw)(e.translatedLabel),1)],4)))),128))])}}});const I=b;var g=I,T=t(9318);const y={key:0,id:"user-statistics"};var C=(0,a.aZ)({__name:"index",props:{sports:null,user:null},setup(e){const s=e,{t:t}=(0,o.QT)(),{sports:l,user:c}=(0,r.BK)(s),i=(0,r.iH)("month"),u=(0,r.iH)(v(i.value)),d=(0,a.Fl)((()=>(0,w.xH)(s.sports,t))),p=(0,r.iH)(_(s.sports));function m(e){i.value=e,u.value=v(i.value)}function v(e){return(0,T.aZ)(new Date,e,s.user.weekm)}function k(e){u.value=(0,T.FN)(u.value,e,s.user.weekm)}function _(e){return e.map((e=>e.id))}function f(e){p.value.includes(e)?p.value=p.value.filter((s=>s!==e)):p.value.push(e)}return(0,a.YP)((()=>s.sports),(e=>{p.value=_(e)})),(e,s)=>(0,r.SU)(d)?((0,a.wg)(),(0,a.iD)("div",y,[(0,a.Wm)(S,{onTimeFrameUpdate:m,onArrowClick:k}),(0,a.Wm)(n.Z,{sports:(0,r.SU)(l),user:(0,r.SU)(c),chartParams:u.value,"displayed-sport-ids":p.value,fullStats:!0},null,8,["sports","user","chartParams","displayed-sport-ids"]),(0,a.Wm)(g,{"selected-sport-ids":p.value,"user-sports":(0,r.SU)(l),onSelectedSportIdsUpdate:f},null,8,["selected-sport-ids","user-sports"])])):(0,a.kq)("",!0)}});const F=(0,k.Z)(C,[["__scopeId","data-v-d693c7da"]]);var Z=F,x=t(5630),D=t(8602),H=t(9917);const E={id:"statistics",class:"view"},R={key:0,class:"container"};var W=(0,a.aZ)({__name:"StatisticsView",setup(e){const s=(0,H.o)(),t=(0,a.Fl)((()=>s.getters[D.YN.GETTERS.AUTH_USER_PROFILE])),o=(0,a.Fl)((()=>s.getters[D.O8.GETTERS.SPORTS].filter((e=>t.value.sports_list.includes(e.id)))));return(e,s)=>{const n=(0,a.up)("Card");return(0,a.wg)(),(0,a.iD)("div",E,[(0,r.SU)(t).username?((0,a.wg)(),(0,a.iD)("div",R,[(0,a.Wm)(n,null,{title:(0,a.w5)((()=>[(0,a.Uk)((0,l.zw)(e.$t("statistics.STATISTICS")),1)])),content:(0,a.w5)((()=>[(0,a.Wm)(Z,{class:(0,l.C_)({"stats-disabled":0===(0,r.SU)(t).nb_workouts}),user:(0,r.SU)(t),sports:(0,r.SU)(o)},null,8,["class","user","sports"])])),_:1}),0===(0,r.SU)(t).nb_workouts?((0,a.wg)(),(0,a.j4)(x.Z,{key:0})):(0,a.kq)("",!0)])):(0,a.kq)("",!0)])}}});const P=(0,k.Z)(W,[["__scopeId","data-v-2e341d4e"]]);var A=P}}]);
|
||||
//# sourceMappingURL=statistics.64e45965.js.map
|
||||
"use strict";(self["webpackChunkfittrackee_client"]=self["webpackChunkfittrackee_client"]||[]).push([[193],{9161:function(e,s,t){t.r(s),t.d(s,{default:function(){return A}});t(6699);var a=t(6252),r=t(2262),l=t(3577),o=t(3324),n=t(4998);const c={class:"chart-menu"},i={class:"chart-arrow"},u={class:"time-frames custom-checkboxes-group"},d={class:"time-frames-checkboxes custom-checkboxes"},p=["id","name","checked","onInput"],m={class:"chart-arrow"};var v=(0,a.aZ)({__name:"StatsMenu",emits:["arrowClick","timeFrameUpdate"],setup(e,{emit:s}){const t=(0,r.iH)("month"),o=["week","month","year"];function n(e){t.value=e,s("timeFrameUpdate",e)}return(e,r)=>((0,a.wg)(),(0,a.iD)("div",c,[(0,a._)("div",i,[(0,a._)("i",{class:"fa fa-chevron-left","aria-hidden":"true",onClick:r[0]||(r[0]=e=>s("arrowClick",!0))})]),(0,a._)("div",u,[(0,a._)("div",d,[((0,a.wg)(),(0,a.iD)(a.HY,null,(0,a.Ko)(o,(s=>(0,a._)("div",{class:"time-frame custom-checkbox",key:s},[(0,a._)("label",null,[(0,a._)("input",{type:"radio",id:s,name:s,checked:t.value===s,onInput:e=>n(s)},null,40,p),(0,a._)("span",null,(0,l.zw)(e.$t(`statistics.TIME_FRAMES.${s}`)),1)])]))),64))])]),(0,a._)("div",m,[(0,a._)("i",{class:"fa fa-chevron-right","aria-hidden":"true",onClick:r[1]||(r[1]=e=>s("arrowClick",!1))})])]))}}),k=t(3744);const _=(0,k.Z)(v,[["__scopeId","data-v-22d55de2"]]);var S=_,w=t(631);const f={class:"sports-menu"},h=["id","name","checked","onInput"],U={class:"sport-label"};var b=(0,a.aZ)({__name:"StatsSportsMenu",props:{userSports:null,selectedSportIds:{default:()=>[]}},emits:["selectedSportIdsUpdate"],setup(e,{emit:s}){const t=e,{t:n}=(0,o.QT)(),c=(0,a.f3)("sportColors"),{selectedSportIds:i}=(0,r.BK)(t),u=(0,a.Fl)((()=>(0,w.xH)(t.userSports,n)));function d(e){s("selectedSportIdsUpdate",e)}return(e,s)=>{const t=(0,a.up)("SportImage");return(0,a.wg)(),(0,a.iD)("div",f,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)((0,r.SU)(u),(e=>((0,a.wg)(),(0,a.iD)("label",{type:"checkbox",key:e.id,style:(0,l.j5)({color:e.color?e.color:(0,r.SU)(c)[e.label]})},[(0,a._)("input",{type:"checkbox",id:e.id,name:e.label,checked:(0,r.SU)(i).includes(e.id),onInput:s=>d(e.id)},null,40,h),(0,a.Wm)(t,{"sport-label":e.label,color:e.color},null,8,["sport-label","color"]),(0,a._)("span",U,(0,l.zw)(e.translatedLabel),1)],4)))),128))])}}});const I=b;var g=I,T=t(9318);const y={key:0,id:"user-statistics"};var C=(0,a.aZ)({__name:"index",props:{sports:null,user:null},setup(e){const s=e,{t:t}=(0,o.QT)(),{sports:l,user:c}=(0,r.BK)(s),i=(0,r.iH)("month"),u=(0,r.iH)(v(i.value)),d=(0,a.Fl)((()=>(0,w.xH)(s.sports,t))),p=(0,r.iH)(_(s.sports));function m(e){i.value=e,u.value=v(i.value)}function v(e){return(0,T.aZ)(new Date,e,s.user.weekm)}function k(e){u.value=(0,T.FN)(u.value,e,s.user.weekm)}function _(e){return e.map((e=>e.id))}function f(e){p.value.includes(e)?p.value=p.value.filter((s=>s!==e)):p.value.push(e)}return(0,a.YP)((()=>s.sports),(e=>{p.value=_(e)})),(e,s)=>(0,r.SU)(d)?((0,a.wg)(),(0,a.iD)("div",y,[(0,a.Wm)(S,{onTimeFrameUpdate:m,onArrowClick:k}),(0,a.Wm)(n.Z,{sports:(0,r.SU)(l),user:(0,r.SU)(c),chartParams:u.value,"displayed-sport-ids":p.value,fullStats:!0},null,8,["sports","user","chartParams","displayed-sport-ids"]),(0,a.Wm)(g,{"selected-sport-ids":p.value,"user-sports":(0,r.SU)(l),onSelectedSportIdsUpdate:f},null,8,["selected-sport-ids","user-sports"])])):(0,a.kq)("",!0)}});const F=(0,k.Z)(C,[["__scopeId","data-v-d693c7da"]]);var Z=F,x=t(5630),D=t(5801),H=t(9917);const E={id:"statistics",class:"view"},R={key:0,class:"container"};var W=(0,a.aZ)({__name:"StatisticsView",setup(e){const s=(0,H.o)(),t=(0,a.Fl)((()=>s.getters[D.YN.GETTERS.AUTH_USER_PROFILE])),o=(0,a.Fl)((()=>s.getters[D.O8.GETTERS.SPORTS].filter((e=>t.value.sports_list.includes(e.id)))));return(e,s)=>{const n=(0,a.up)("Card");return(0,a.wg)(),(0,a.iD)("div",E,[(0,r.SU)(t).username?((0,a.wg)(),(0,a.iD)("div",R,[(0,a.Wm)(n,null,{title:(0,a.w5)((()=>[(0,a.Uk)((0,l.zw)(e.$t("statistics.STATISTICS")),1)])),content:(0,a.w5)((()=>[(0,a.Wm)(Z,{class:(0,l.C_)({"stats-disabled":0===(0,r.SU)(t).nb_workouts}),user:(0,r.SU)(t),sports:(0,r.SU)(o)},null,8,["class","user","sports"])])),_:1}),0===(0,r.SU)(t).nb_workouts?((0,a.wg)(),(0,a.j4)(x.Z,{key:0})):(0,a.kq)("",!0)])):(0,a.kq)("",!0)])}}});const P=(0,k.Z)(W,[["__scopeId","data-v-2e341d4e"]]);var A=P}}]);
|
||||
//# sourceMappingURL=statistics.a3204b87.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
84
fittrackee/migrations/versions/25_84d840ce853b_add_oauth.py
Normal file
84
fittrackee/migrations/versions/25_84d840ce853b_add_oauth.py
Normal file
@ -0,0 +1,84 @@
|
||||
"""add OAuth 2.0
|
||||
|
||||
Revision ID: 84d840ce853b
|
||||
Revises: 5e3a3a31c432
|
||||
Create Date: 2022-05-27 10:54:02.284543
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '84d840ce853b'
|
||||
down_revision = 'cd0e6cf83207'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('oauth2_client',
|
||||
sa.Column('client_id', sa.String(length=48), nullable=True),
|
||||
sa.Column('client_secret', sa.String(length=120), nullable=True),
|
||||
sa.Column('client_id_issued_at', sa.Integer(), nullable=False),
|
||||
sa.Column('client_secret_expires_at', sa.Integer(), nullable=False),
|
||||
sa.Column('client_metadata', sa.Text(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_oauth2_client_client_id'), 'oauth2_client', ['client_id'], unique=False)
|
||||
op.create_index(op.f('ix_oauth2_client_user_id'), 'oauth2_client', ['user_id'], unique=False)
|
||||
op.create_table('oauth2_code',
|
||||
sa.Column('code', sa.String(length=120), nullable=False),
|
||||
sa.Column('client_id', sa.String(length=48), nullable=True),
|
||||
sa.Column('redirect_uri', sa.Text(), nullable=True),
|
||||
sa.Column('response_type', sa.Text(), nullable=True),
|
||||
sa.Column('scope', sa.Text(), nullable=True),
|
||||
sa.Column('nonce', sa.Text(), nullable=True),
|
||||
sa.Column('auth_time', sa.Integer(), nullable=False),
|
||||
sa.Column('code_challenge', sa.Text(), nullable=True),
|
||||
sa.Column('code_challenge_method', sa.String(length=48), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('code')
|
||||
)
|
||||
op.create_index('ix_oauth2_code_client_id', 'oauth2_code', ['client_id'], unique=False)
|
||||
op.create_index(op.f('ix_oauth2_code_user_id'), 'oauth2_code', ['user_id'], unique=False)
|
||||
op.create_table('oauth2_token',
|
||||
sa.Column('client_id', sa.String(length=48), nullable=True),
|
||||
sa.Column('token_type', sa.String(length=40), nullable=True),
|
||||
sa.Column('access_token', sa.String(length=255), nullable=False),
|
||||
sa.Column('refresh_token', sa.String(length=255), nullable=True),
|
||||
sa.Column('scope', sa.Text(), nullable=True),
|
||||
sa.Column('issued_at', sa.Integer(), nullable=False),
|
||||
sa.Column('access_token_revoked_at', sa.Integer(), nullable=False),
|
||||
sa.Column('refresh_token_revoked_at', sa.Integer(), nullable=False),
|
||||
sa.Column('expires_in', sa.Integer(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('access_token')
|
||||
)
|
||||
op.create_index(op.f('ix_oauth2_token_refresh_token'), 'oauth2_token', ['refresh_token'], unique=False)
|
||||
op.create_index(op.f('ix_oauth2_token_user_id'), 'oauth2_token', ['user_id'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_oauth2_token_user_id'), table_name='oauth2_token')
|
||||
op.drop_index(op.f('ix_oauth2_token_refresh_token'), table_name='oauth2_token')
|
||||
op.drop_table('oauth2_token')
|
||||
op.drop_index(op.f('ix_oauth2_code_user_id'), table_name='oauth2_code')
|
||||
op.drop_index('ix_oauth2_code_client_id', table_name='oauth2_code')
|
||||
op.drop_table('oauth2_code')
|
||||
op.drop_index(op.f('ix_oauth2_client_user_id'), table_name='oauth2_client')
|
||||
op.drop_index(op.f('ix_oauth2_client_client_id'), table_name='oauth2_client')
|
||||
op.drop_table('oauth2_client')
|
||||
# ### end Alembic commands ###
|
0
fittrackee/oauth2/__init__.py
Normal file
0
fittrackee/oauth2/__init__.py
Normal file
13
fittrackee/oauth2/clean.py
Normal file
13
fittrackee/oauth2/clean.py
Normal file
@ -0,0 +1,13 @@
|
||||
import time
|
||||
|
||||
from fittrackee import db
|
||||
|
||||
|
||||
def clean_tokens(days: int) -> int:
|
||||
limit = int(time.time()) - (days * 86400)
|
||||
sql = """
|
||||
DELETE FROM oauth2_token
|
||||
WHERE oauth2_token.issued_at + oauth2_token.expires_in < %(limit)s;
|
||||
"""
|
||||
result = db.engine.execute(sql, {'limit': limit})
|
||||
return result.rowcount
|
68
fittrackee/oauth2/client.py
Normal file
68
fittrackee/oauth2/client.py
Normal file
@ -0,0 +1,68 @@
|
||||
from time import time
|
||||
from typing import Dict
|
||||
|
||||
from werkzeug.security import gen_salt
|
||||
|
||||
from fittrackee.users.models import User
|
||||
|
||||
from .exceptions import InvalidOAuth2Scopes
|
||||
from .models import OAuth2Client
|
||||
|
||||
VALID_SCOPES = [
|
||||
'application:write',
|
||||
'profile:read',
|
||||
'profile:write',
|
||||
'users:read',
|
||||
'users:write',
|
||||
'workouts:read',
|
||||
'workouts:write',
|
||||
]
|
||||
|
||||
|
||||
def check_scope(scope: str) -> str:
|
||||
"""
|
||||
Verify if provided scope is valid.
|
||||
"""
|
||||
if not isinstance(scope, str) or not scope:
|
||||
raise InvalidOAuth2Scopes()
|
||||
|
||||
valid_scopes = []
|
||||
scopes = scope.split()
|
||||
for value in scopes:
|
||||
if value in VALID_SCOPES:
|
||||
valid_scopes.append(value)
|
||||
|
||||
if not valid_scopes:
|
||||
raise InvalidOAuth2Scopes()
|
||||
|
||||
return ' '.join(valid_scopes)
|
||||
|
||||
|
||||
def create_oauth2_client(metadata: Dict, user: User) -> OAuth2Client:
|
||||
"""
|
||||
Create OAuth2 client for 3rd-party applications.
|
||||
|
||||
Only Authorization Code Grant with 'client_secret_post' as method
|
||||
is supported.
|
||||
Code challenge can be used if provided on authorization.
|
||||
"""
|
||||
client_metadata = {
|
||||
'client_name': metadata['client_name'],
|
||||
'client_description': metadata.get('client_description'),
|
||||
'client_uri': metadata['client_uri'],
|
||||
'redirect_uris': metadata['redirect_uris'],
|
||||
'scope': check_scope(metadata['scope']),
|
||||
'grant_types': ['authorization_code', 'refresh_token'],
|
||||
'response_types': ['code'],
|
||||
'token_endpoint_auth_method': 'client_secret_post',
|
||||
}
|
||||
client_id = gen_salt(24)
|
||||
client_id_issued_at = int(time())
|
||||
client = OAuth2Client(
|
||||
client_id=client_id,
|
||||
client_id_issued_at=client_id_issued_at,
|
||||
user_id=user.id,
|
||||
)
|
||||
client.set_client_metadata(client_metadata)
|
||||
client.client_secret = gen_salt(48)
|
||||
return client
|
29
fittrackee/oauth2/commands.py
Normal file
29
fittrackee/oauth2/commands.py
Normal file
@ -0,0 +1,29 @@
|
||||
import logging
|
||||
|
||||
import click
|
||||
|
||||
from fittrackee.cli.app import app
|
||||
|
||||
from .clean import clean_tokens
|
||||
|
||||
handler = logging.StreamHandler()
|
||||
logger = logging.getLogger('fittrackee_clean_tokens')
|
||||
logger.setLevel(logging.INFO)
|
||||
logger.addHandler(handler)
|
||||
|
||||
|
||||
@click.group(name='oauth2')
|
||||
def oauth2_cli() -> None:
|
||||
"""Manage OAuth2 tokens."""
|
||||
pass
|
||||
|
||||
|
||||
@oauth2_cli.command('clean')
|
||||
@click.option('--days', type=int, required=True, help='Number of days.')
|
||||
def clean(
|
||||
days: int,
|
||||
) -> None:
|
||||
"""Clean tokens expired for more than provided number of days"""
|
||||
with app.app_context():
|
||||
deleted_rows = clean_tokens(days)
|
||||
logger.info(f'Expired deleted tokens: {deleted_rows}.')
|
30
fittrackee/oauth2/config.py
Normal file
30
fittrackee/oauth2/config.py
Normal file
@ -0,0 +1,30 @@
|
||||
from authlib.integrations.sqla_oauth2 import (
|
||||
create_bearer_token_validator,
|
||||
create_revocation_endpoint,
|
||||
)
|
||||
from authlib.oauth2.rfc7636 import CodeChallenge
|
||||
from flask import Flask
|
||||
|
||||
from fittrackee import db
|
||||
|
||||
from .grants import AuthorizationCodeGrant, OAuth2Token, RefreshTokenGrant
|
||||
from .server import authorization_server, require_auth
|
||||
|
||||
|
||||
def config_oauth(app: Flask) -> None:
|
||||
authorization_server.init_app(app)
|
||||
|
||||
# supported grants
|
||||
authorization_server.register_grant(
|
||||
AuthorizationCodeGrant, [CodeChallenge(required=True)]
|
||||
)
|
||||
authorization_server.register_grant(RefreshTokenGrant)
|
||||
|
||||
# support revocation
|
||||
revocation_cls = create_revocation_endpoint(db.session, OAuth2Token)
|
||||
revocation_cls.CLIENT_AUTH_METHODS = ['client_secret_post']
|
||||
authorization_server.register_endpoint(revocation_cls)
|
||||
|
||||
# protect resource
|
||||
bearer_cls = create_bearer_token_validator(db.session, OAuth2Token)
|
||||
require_auth.register_token_validator(bearer_cls())
|
2
fittrackee/oauth2/exceptions.py
Normal file
2
fittrackee/oauth2/exceptions.py
Normal file
@ -0,0 +1,2 @@
|
||||
class InvalidOAuth2Scopes(Exception):
|
||||
...
|
73
fittrackee/oauth2/grants.py
Normal file
73
fittrackee/oauth2/grants.py
Normal file
@ -0,0 +1,73 @@
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from authlib.oauth2 import OAuth2Request
|
||||
from authlib.oauth2.rfc6749 import grants
|
||||
|
||||
from fittrackee import db
|
||||
from fittrackee.users.models import User
|
||||
|
||||
from .models import OAuth2AuthorizationCode, OAuth2Client, OAuth2Token
|
||||
|
||||
|
||||
class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
|
||||
TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_post']
|
||||
|
||||
def save_authorization_code(
|
||||
self, code: str, request: OAuth2Request
|
||||
) -> OAuth2AuthorizationCode:
|
||||
code_challenge = request.data.get('code_challenge')
|
||||
code_challenge_method = request.data.get('code_challenge_method')
|
||||
auth_code = OAuth2AuthorizationCode(
|
||||
code=code,
|
||||
client_id=request.client.client_id,
|
||||
redirect_uri=request.redirect_uri,
|
||||
scope=request.scope,
|
||||
user_id=request.user.id,
|
||||
code_challenge=code_challenge,
|
||||
code_challenge_method=code_challenge_method,
|
||||
)
|
||||
db.session.add(auth_code)
|
||||
db.session.commit()
|
||||
return auth_code
|
||||
|
||||
def query_authorization_code(
|
||||
self, code: str, client: OAuth2Client
|
||||
) -> Optional[OAuth2AuthorizationCode]:
|
||||
auth_code = OAuth2AuthorizationCode.query.filter_by(
|
||||
code=code, client_id=client.client_id
|
||||
).first()
|
||||
if auth_code and not auth_code.is_expired():
|
||||
return auth_code
|
||||
return None
|
||||
|
||||
def delete_authorization_code(
|
||||
self, authorization_code: OAuth2AuthorizationCode
|
||||
) -> None:
|
||||
db.session.delete(authorization_code)
|
||||
db.session.commit()
|
||||
|
||||
def authenticate_user(
|
||||
self, authorization_code: OAuth2AuthorizationCode
|
||||
) -> User:
|
||||
return User.query.get(authorization_code.user_id)
|
||||
|
||||
|
||||
class RefreshTokenGrant(grants.RefreshTokenGrant):
|
||||
TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_post']
|
||||
INCLUDE_NEW_REFRESH_TOKEN = True
|
||||
|
||||
def authenticate_refresh_token(self, refresh_token: str) -> Optional[str]:
|
||||
token = OAuth2Token.query.filter_by(
|
||||
refresh_token=refresh_token
|
||||
).first()
|
||||
if token and token.is_refresh_token_active():
|
||||
return token
|
||||
return None
|
||||
|
||||
def authenticate_user(self, credential: OAuth2Token) -> User:
|
||||
return User.query.get(credential.user_id)
|
||||
|
||||
def revoke_old_credential(self, credential: OAuth2Token) -> None:
|
||||
credential.access_token_revoked_at = time.time()
|
||||
db.session.commit()
|
107
fittrackee/oauth2/models.py
Normal file
107
fittrackee/oauth2/models.py
Normal file
@ -0,0 +1,107 @@
|
||||
import time
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from authlib.integrations.sqla_oauth2 import (
|
||||
OAuth2AuthorizationCodeMixin,
|
||||
OAuth2ClientMixin,
|
||||
OAuth2TokenMixin,
|
||||
)
|
||||
from sqlalchemy.engine.base import Connection
|
||||
from sqlalchemy.event import listens_for
|
||||
from sqlalchemy.ext.declarative import DeclarativeMeta
|
||||
from sqlalchemy.orm.mapper import Mapper
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from fittrackee import db
|
||||
|
||||
BaseModel: DeclarativeMeta = db.Model
|
||||
|
||||
|
||||
class OAuth2Client(BaseModel, OAuth2ClientMixin):
|
||||
__tablename__ = 'oauth2_client'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(
|
||||
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True
|
||||
)
|
||||
user = db.relationship('User')
|
||||
|
||||
def serialize(self, with_secret: bool = False) -> Dict:
|
||||
client = {
|
||||
'client_id': self.client_id,
|
||||
'client_description': self.client_description,
|
||||
'id': self.id,
|
||||
'issued_at': time.strftime(
|
||||
'%a, %d %B %Y %H:%M:%S GMT',
|
||||
time.gmtime(self.client_id_issued_at),
|
||||
),
|
||||
'name': self.client_name,
|
||||
'redirect_uris': self.redirect_uris,
|
||||
'scope': self.scope,
|
||||
'website': self.client_uri,
|
||||
}
|
||||
if with_secret:
|
||||
client['client_secret'] = self.client_secret
|
||||
return client
|
||||
|
||||
@property
|
||||
def client_description(self) -> Optional[str]:
|
||||
return self.client_metadata.get('client_description')
|
||||
|
||||
|
||||
@listens_for(OAuth2Client, 'after_delete')
|
||||
def on_old_oauth2_delete(
|
||||
mapper: Mapper, connection: Connection, old_oauth2_client: OAuth2Client
|
||||
) -> None:
|
||||
@listens_for(db.Session, 'after_flush', once=True)
|
||||
def receive_after_flush(session: Session, context: Any) -> None:
|
||||
session.query(OAuth2AuthorizationCode).filter(
|
||||
OAuth2AuthorizationCode.client_id == old_oauth2_client.client_id
|
||||
).delete(synchronize_session=False)
|
||||
session.query(OAuth2Token).filter(
|
||||
OAuth2Token.client_id == old_oauth2_client.client_id
|
||||
).delete(synchronize_session=False)
|
||||
|
||||
|
||||
class OAuth2AuthorizationCode(BaseModel, OAuth2AuthorizationCodeMixin):
|
||||
__tablename__ = 'oauth2_code'
|
||||
__table_args__ = (
|
||||
db.Index(
|
||||
'ix_oauth2_code_client_id',
|
||||
'client_id',
|
||||
),
|
||||
)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(
|
||||
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True
|
||||
)
|
||||
user = db.relationship('User')
|
||||
|
||||
|
||||
class OAuth2Token(BaseModel, OAuth2TokenMixin):
|
||||
__tablename__ = 'oauth2_token'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(
|
||||
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True
|
||||
)
|
||||
user = db.relationship('User')
|
||||
|
||||
def is_refresh_token_active(self) -> bool:
|
||||
if self.is_revoked():
|
||||
return False
|
||||
expires_at = self.issued_at + self.expires_in * 2
|
||||
return expires_at >= time.time()
|
||||
|
||||
@classmethod
|
||||
def revoke_client_tokens(cls, client_id: str) -> None:
|
||||
sql = """
|
||||
UPDATE oauth2_token
|
||||
SET access_token_revoked_at = %(revoked_at)s
|
||||
WHERE client_id = %(client_id)s;
|
||||
"""
|
||||
db.engine.execute(
|
||||
sql, {'client_id': client_id, 'revoked_at': int(time.time())}
|
||||
)
|
||||
db.session.commit()
|
80
fittrackee/oauth2/resource_protector.py
Normal file
80
fittrackee/oauth2/resource_protector.py
Normal file
@ -0,0 +1,80 @@
|
||||
from functools import wraps
|
||||
from typing import Any, Callable, List, Union
|
||||
|
||||
from authlib.integrations.flask_oauth2 import ResourceProtector
|
||||
from authlib.oauth2 import OAuth2Error
|
||||
from authlib.oauth2.rfc6749.errors import MissingAuthorizationError
|
||||
from flask import current_app, request
|
||||
from werkzeug.exceptions import RequestEntityTooLarge
|
||||
|
||||
from fittrackee.responses import (
|
||||
ForbiddenErrorResponse,
|
||||
PayloadTooLargeErrorResponse,
|
||||
UnauthorizedErrorResponse,
|
||||
)
|
||||
from fittrackee.users.models import User
|
||||
|
||||
|
||||
class CustomResourceProtector(ResourceProtector):
|
||||
def __call__(
|
||||
self,
|
||||
scopes: Union[str, List] = None,
|
||||
as_admin: bool = False,
|
||||
) -> Callable:
|
||||
def wrapper(f: Callable) -> Callable:
|
||||
@wraps(f)
|
||||
def decorated(*args: Any, **kwargs: Any) -> Callable:
|
||||
auth_user = None
|
||||
auth_header = request.headers.get('Authorization')
|
||||
if not auth_header:
|
||||
return UnauthorizedErrorResponse(
|
||||
'provide a valid auth token'
|
||||
)
|
||||
|
||||
# First-party application (Fittrackee front-end)
|
||||
# in this case, scopes will be ignored
|
||||
auth_token = auth_header.split(' ')[1]
|
||||
resp = User.decode_auth_token(auth_token)
|
||||
if isinstance(resp, int):
|
||||
auth_user = User.query.filter_by(id=resp).first()
|
||||
|
||||
# Third-party applications
|
||||
if not auth_user:
|
||||
current_token = None
|
||||
try:
|
||||
current_token = self.acquire_token(scopes)
|
||||
except MissingAuthorizationError as error:
|
||||
self.raise_error_response(error)
|
||||
except OAuth2Error as error:
|
||||
self.raise_error_response(error)
|
||||
except RequestEntityTooLarge:
|
||||
file_type = ''
|
||||
if request.endpoint in [
|
||||
'auth.edit_picture',
|
||||
'workouts.post_workout',
|
||||
]:
|
||||
file_type = (
|
||||
'picture'
|
||||
if request.endpoint == 'auth.edit_picture'
|
||||
else 'workout'
|
||||
)
|
||||
return PayloadTooLargeErrorResponse(
|
||||
file_type=file_type,
|
||||
file_size=request.content_length,
|
||||
max_size=current_app.config['MAX_CONTENT_LENGTH'],
|
||||
)
|
||||
auth_user = (
|
||||
None if current_token is None else current_token.user
|
||||
)
|
||||
|
||||
if not auth_user or not auth_user.is_active:
|
||||
return UnauthorizedErrorResponse(
|
||||
'provide a valid auth token'
|
||||
)
|
||||
if as_admin and not auth_user.admin:
|
||||
return ForbiddenErrorResponse()
|
||||
return f(auth_user, *args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
return wrapper
|
650
fittrackee/oauth2/routes.py
Normal file
650
fittrackee/oauth2/routes.py
Normal file
@ -0,0 +1,650 @@
|
||||
from typing import Dict, Optional, Tuple, Union
|
||||
from urllib.parse import parse_qsl
|
||||
|
||||
from flask import Blueprint, Response, request
|
||||
from urllib3.util import parse_url
|
||||
|
||||
from fittrackee import db
|
||||
from fittrackee.responses import (
|
||||
HttpResponse,
|
||||
InvalidPayloadErrorResponse,
|
||||
NotFoundErrorResponse,
|
||||
)
|
||||
from fittrackee.users.models import User
|
||||
|
||||
from .client import create_oauth2_client
|
||||
from .exceptions import InvalidOAuth2Scopes
|
||||
from .models import OAuth2Client, OAuth2Token
|
||||
from .server import authorization_server, require_auth
|
||||
|
||||
oauth2_blueprint = Blueprint('oauth2', __name__)
|
||||
|
||||
EXPECTED_METADATA_KEYS = [
|
||||
'client_name',
|
||||
'client_uri',
|
||||
'redirect_uris',
|
||||
'scope',
|
||||
]
|
||||
DEFAULT_PER_PAGE = 5
|
||||
|
||||
|
||||
def is_errored(url: str) -> Optional[str]:
|
||||
query = dict(parse_qsl(parse_url(url).query))
|
||||
if query.get('error'):
|
||||
return query.get('error_description', 'invalid payload')
|
||||
return None
|
||||
|
||||
|
||||
@oauth2_blueprint.route('/oauth/apps', methods=['GET'])
|
||||
@require_auth()
|
||||
def get_clients(auth_user: User) -> Dict:
|
||||
"""
|
||||
Get OAuth2 clients (apps) for authenticated user with pagination
|
||||
(5 clients/page).
|
||||
|
||||
This endpoint is only accessible by FitTrackee client (first-party
|
||||
application).
|
||||
|
||||
**Example request**:
|
||||
|
||||
- without parameters
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/oauth/apps HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
- with 'page' parameter
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/oauth/apps?page=2 HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 SUCCESS
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"clients": [
|
||||
{
|
||||
"client_description": "",
|
||||
"client_id": "o22a27s2aBPUoxJbxV3UjDOx",
|
||||
"id": 1,
|
||||
"issued_at": "Thu, 14 July 2022 06:27:53 GMT",
|
||||
"name": "GPX Importer",
|
||||
"redirect_uris": [
|
||||
" https://example.com/callback"
|
||||
],
|
||||
"scope": "profile:read workouts:write",
|
||||
"website": "https://example.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pagination": {
|
||||
"has_next": false,
|
||||
"has_prev": false,
|
||||
"page": 1,
|
||||
"pages": 1,
|
||||
"total": 1
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:query integer page: page for pagination (default: 1)
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: success
|
||||
:statuscode 401:
|
||||
- provide a valid auth token
|
||||
- signature expired, please log in again
|
||||
- invalid token, please log in again
|
||||
"""
|
||||
params = request.args.copy()
|
||||
page = int(params.get('page', 1))
|
||||
per_page = DEFAULT_PER_PAGE
|
||||
clients_pagination = (
|
||||
OAuth2Client.query.filter_by(user_id=auth_user.id)
|
||||
.order_by(OAuth2Client.id.desc())
|
||||
.paginate(page, per_page, False)
|
||||
)
|
||||
clients = clients_pagination.items
|
||||
return {
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'clients': [
|
||||
client.serialize(with_secret=False) for client in clients
|
||||
]
|
||||
},
|
||||
'pagination': {
|
||||
'has_next': clients_pagination.has_next,
|
||||
'has_prev': clients_pagination.has_prev,
|
||||
'page': clients_pagination.page,
|
||||
'pages': clients_pagination.pages,
|
||||
'total': clients_pagination.total,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@oauth2_blueprint.route('/oauth/apps', methods=['POST'])
|
||||
@require_auth()
|
||||
def create_client(auth_user: User) -> Union[HttpResponse, Tuple[Dict, int]]:
|
||||
"""
|
||||
Create an OAuth2 client (app) for the authenticated user.
|
||||
|
||||
This endpoint is only accessible by FitTrackee client (first-party
|
||||
application).
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/oauth/apps HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 SUCCESS
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"client": {
|
||||
"client_description": "",
|
||||
"client_id": "o22a27s2aBPUoxJbxV3UjDOx",
|
||||
"client_secret": "<CLIENT SECRET>",
|
||||
"id": 1,
|
||||
"issued_at": "Thu, 14 July 2022 06:27:53 GMT",
|
||||
"name": "GPX Importer",
|
||||
"redirect_uris": [
|
||||
"https://example.com/callback"
|
||||
],
|
||||
"scope": "profile:read workouts:write",
|
||||
"website": "https://example.com"
|
||||
}
|
||||
},
|
||||
"status": "created"
|
||||
}
|
||||
|
||||
:json string client_name: client name
|
||||
:json string client_uri: client URL
|
||||
:json array redirect_uri: list of client redirect URLs (string)
|
||||
:json string scope: client scopes
|
||||
:json string client_description: client description (`OPTIONAL`)
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: success
|
||||
:statuscode 400:
|
||||
- invalid payload
|
||||
:statuscode 401:
|
||||
- provide a valid auth token
|
||||
- signature expired, please log in again
|
||||
- invalid token, please log in again
|
||||
"""
|
||||
client_metadata = request.get_json()
|
||||
if not client_metadata:
|
||||
return InvalidPayloadErrorResponse(
|
||||
message='OAuth2 client metadata missing'
|
||||
)
|
||||
|
||||
missing_keys = [
|
||||
key
|
||||
for key in EXPECTED_METADATA_KEYS
|
||||
if key not in client_metadata.keys()
|
||||
]
|
||||
if missing_keys:
|
||||
return InvalidPayloadErrorResponse(
|
||||
message=(
|
||||
'OAuth2 client metadata missing keys: '
|
||||
f'{", ".join(missing_keys)}'
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
new_client = create_oauth2_client(client_metadata, auth_user)
|
||||
except InvalidOAuth2Scopes:
|
||||
return InvalidPayloadErrorResponse(
|
||||
message='OAuth2 client invalid scopes'
|
||||
)
|
||||
|
||||
db.session.add(new_client)
|
||||
db.session.commit()
|
||||
return (
|
||||
{
|
||||
'status': 'created',
|
||||
'data': {'client': new_client.serialize(with_secret=True)},
|
||||
},
|
||||
201,
|
||||
)
|
||||
|
||||
|
||||
def get_client(
|
||||
auth_user: User,
|
||||
client_id: Optional[int],
|
||||
client_client_id: Optional[str],
|
||||
) -> Union[Dict, HttpResponse]:
|
||||
key = 'id' if client_id else 'client_id'
|
||||
value = client_id if client_id else client_client_id
|
||||
client = OAuth2Client.query.filter_by(
|
||||
**{key: value, 'user_id': auth_user.id}
|
||||
).first()
|
||||
|
||||
if not client:
|
||||
return NotFoundErrorResponse('OAuth2 client not found')
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'data': {'client': client.serialize(with_secret=False)},
|
||||
}
|
||||
|
||||
|
||||
@oauth2_blueprint.route(
|
||||
'/oauth/apps/<string:client_client_id>', methods=['GET']
|
||||
)
|
||||
@require_auth()
|
||||
def get_client_by_client_id(
|
||||
auth_user: User, client_client_id: str
|
||||
) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
Get an OAuth2 client (app) by 'client_id'.
|
||||
|
||||
This endpoint is only accessible by FitTrackee client (first-party
|
||||
application).
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/oauth/apps/o22a27s2aBPUoxJbxV3UjDOx HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example responses**:
|
||||
|
||||
- success
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 SUCCESS
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"client": {
|
||||
"client_description": "",
|
||||
"client_id": "o22a27s2aBPUoxJbxV3UjDOx",
|
||||
"id": 1,
|
||||
"issued_at": "Thu, 14 July 2022 06:27:53 GMT",
|
||||
"name": "GPX Importer",
|
||||
"redirect_uris": [
|
||||
"https://example.com/callback"
|
||||
],
|
||||
"scope": "profile:read workouts:write",
|
||||
"website": "https://example.com"
|
||||
}
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
- not found
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 404 NOT FOUND
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"status": "not found",
|
||||
"message": "OAuth2 client not found"
|
||||
}
|
||||
|
||||
:param string client_client_id: OAuth2 client client_id
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: success
|
||||
:statuscode 401:
|
||||
- provide a valid auth token
|
||||
- signature expired, please log in again
|
||||
- invalid token, please log in again
|
||||
:statuscode 404: OAuth2 client not found
|
||||
"""
|
||||
return get_client(
|
||||
auth_user, client_id=None, client_client_id=client_client_id
|
||||
)
|
||||
|
||||
|
||||
@oauth2_blueprint.route('/oauth/apps/<int:client_id>/by_id', methods=['GET'])
|
||||
@require_auth()
|
||||
def get_client_by_id(
|
||||
auth_user: User, client_id: int
|
||||
) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
Get an OAuth2 client (app) by id (integer value).
|
||||
|
||||
This endpoint is only accessible by FitTrackee client (first-party
|
||||
application).
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/oauth/apps/1/by_id HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example responses**:
|
||||
|
||||
- success
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 SUCCESS
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"client": {
|
||||
"client_description": "",
|
||||
"client_id": "o22a27s2aBPUoxJbxV3UjDOx",
|
||||
"id": 1,
|
||||
"issued_at": "Thu, 14 July 2022 06:27:53 GMT",
|
||||
"name": "GPX Importer",
|
||||
"redirect_uris": [
|
||||
"https://example.com/callback"
|
||||
],
|
||||
"scope": "profile:read workouts:write",
|
||||
"website": "https://example.com"
|
||||
}
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
- not found
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 404 NOT FOUND
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"status": "not found",
|
||||
"message": "OAuth2 client not found"
|
||||
}
|
||||
|
||||
:param integer client_id: OAuth2 client id
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: success
|
||||
:statuscode 401:
|
||||
- provide a valid auth token
|
||||
- signature expired, please log in again
|
||||
- invalid token, please log in again
|
||||
:statuscode 404: OAuth2 client not found
|
||||
"""
|
||||
return get_client(auth_user, client_id=client_id, client_client_id=None)
|
||||
|
||||
|
||||
@oauth2_blueprint.route('/oauth/apps/<int:client_id>', methods=['DELETE'])
|
||||
@require_auth()
|
||||
def delete_client(
|
||||
auth_user: User, client_id: int
|
||||
) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||
"""
|
||||
Delete an OAuth2 client (app).
|
||||
|
||||
This endpoint is only accessible by FitTrackee client (first-party
|
||||
application).
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/oauth/apps/1 HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 NO CONTENT
|
||||
Content-Type: application/json
|
||||
|
||||
:param integer client_id: OAuth2 client id
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 204: OAuth2 client deleted
|
||||
:statuscode 401:
|
||||
- provide a valid auth token
|
||||
- signature expired, please log in again
|
||||
- invalid token, please log in again
|
||||
:statuscode 404: OAuth2 client not found
|
||||
"""
|
||||
client = OAuth2Client.query.filter_by(
|
||||
id=client_id,
|
||||
user_id=auth_user.id,
|
||||
).first()
|
||||
|
||||
if not client:
|
||||
return NotFoundErrorResponse('OAuth2 client not found')
|
||||
|
||||
db.session.delete(client)
|
||||
db.session.commit()
|
||||
return {'status': 'no content'}, 204
|
||||
|
||||
|
||||
@oauth2_blueprint.route('/oauth/apps/<int:client_id>/revoke', methods=['POST'])
|
||||
@require_auth()
|
||||
def revoke_client_tokens(
|
||||
auth_user: User, client_id: int
|
||||
) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
Revoke all tokens associated to an OAuth2 client (app).
|
||||
|
||||
This endpoint is only accessible by FitTrackee client (first-party
|
||||
application).
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/oauth/apps/1/revoke HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 SUCCESS
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:param integer client_id: OAuth2 client id
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: success
|
||||
:statuscode 401:
|
||||
- provide a valid auth token
|
||||
- signature expired, please log in again
|
||||
- invalid token, please log in again
|
||||
:statuscode 404: OAuth2 client not found
|
||||
"""
|
||||
client = OAuth2Client.query.filter_by(id=client_id).first()
|
||||
|
||||
if not client:
|
||||
return NotFoundErrorResponse('OAuth2 client not found')
|
||||
|
||||
OAuth2Token.revoke_client_tokens(client.client_id)
|
||||
return {'status': 'success'}
|
||||
|
||||
|
||||
@oauth2_blueprint.route('/oauth/authorize', methods=['POST'])
|
||||
@require_auth()
|
||||
def authorize(auth_user: User) -> Union[HttpResponse, Dict]:
|
||||
"""
|
||||
Authorize an OAuth2 client (app).
|
||||
If successful, it redirects to the client callback URL with the code to
|
||||
issue a token.
|
||||
|
||||
This endpoint is only accessible by FitTrackee client (first-party
|
||||
application).
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/oauth/authorize HTTP/1.1
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 SUCCESS
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:form string client_id: OAuth2 client 'client_id'
|
||||
:form string response_type: client response type (only 'code' is supported
|
||||
by FitTrackee)
|
||||
:form string scopes: OAuth2 client scopes
|
||||
:form boolean confirm: confirmation (must be 'true')
|
||||
:form string state: unique value to prevent cross-site request forgery
|
||||
(not mandatory but recommended)
|
||||
:form string code_challenge: string generated from a code verifier
|
||||
(for PKCE, not mandatory but recommended)
|
||||
:form string code_challenge_method: method used to create challenge,
|
||||
for instance "S256" (mandatory if `code_challenge`
|
||||
provided)
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: success
|
||||
:statuscode 400:
|
||||
- invalid payload
|
||||
- errors returned by Authlib library
|
||||
:statuscode 401:
|
||||
- provide a valid auth token
|
||||
- signature expired, please log in again
|
||||
- invalid token, please log in again
|
||||
"""
|
||||
data = request.form
|
||||
if (
|
||||
not data
|
||||
or 'client_id' not in data
|
||||
or 'response_type' not in data
|
||||
or data.get('response_type') != 'code'
|
||||
):
|
||||
return InvalidPayloadErrorResponse()
|
||||
|
||||
confirm = data.get('confirm', 'false')
|
||||
grant_user = auth_user if confirm.lower() == 'true' else None
|
||||
response = authorization_server.create_authorization_response(
|
||||
grant_user=grant_user
|
||||
)
|
||||
error_message = is_errored(url=response.location)
|
||||
if error_message:
|
||||
return InvalidPayloadErrorResponse(error_message)
|
||||
return {'redirect_url': response.location}
|
||||
|
||||
|
||||
@oauth2_blueprint.route('/oauth/token', methods=['POST'])
|
||||
def issue_token() -> Response:
|
||||
"""
|
||||
Issue or refresh token for a given OAuth2 client (app).
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/oauth/token HTTP/1.1
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 SUCCESS
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"access_token": "rOEHv64THCG28WcewZHRnVLUsOdUvw8NVnHKCmL57e",
|
||||
"expires_in": 864000,
|
||||
"refresh_token": "NuV9cY8VQOnrQKHTZ5pQAq2Zw7mSH0MorNPJr14AmSwD6f6I",
|
||||
"scope": ["profile:read", "workouts:write"],
|
||||
"token_type": "Bearer",
|
||||
"expires_at": 1658660147.0667062
|
||||
}
|
||||
|
||||
:form string client_id: OAuth2 client 'client_id'
|
||||
:form string client_secret: OAuth2 client secret
|
||||
:form string grant_type: OAuth2 client grant type
|
||||
(only 'authorization_code' (for token issue)
|
||||
and 'refresh_token' (for token refresh)
|
||||
are supported by FitTrackee)
|
||||
:form string code: code generated after authorizing the client
|
||||
(for token issue)
|
||||
:form string code_verifier: code verifier
|
||||
(for token issue with PKCE, not mandatory)
|
||||
:form string refresh_token: refresh token (for token refresh)
|
||||
|
||||
:statuscode 200: success
|
||||
:statuscode 400:
|
||||
- errors returned by Authlib library
|
||||
:statuscode 401:
|
||||
- provide a valid auth token
|
||||
- signature expired, please log in again
|
||||
- invalid token, please log in again
|
||||
"""
|
||||
return authorization_server.create_token_response()
|
||||
|
||||
|
||||
@oauth2_blueprint.route('/oauth/revoke', methods=['POST'])
|
||||
def revoke_token() -> Response:
|
||||
"""
|
||||
Revoke a token for a given OAuth2 client (app).
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/oauth/revoke HTTP/1.1
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 SUCCESS
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
|
||||
:form string client_id: OAuth2 client 'client_id'
|
||||
:form string client_secret: OAuth2 client secret
|
||||
:form string token: access token to revoke
|
||||
|
||||
:statuscode 200: success
|
||||
:statuscode 400:
|
||||
- errors returned by Authlib library
|
||||
:statuscode 401:
|
||||
- provide a valid auth token
|
||||
- signature expired, please log in again
|
||||
- invalid token, please log in again
|
||||
"""
|
||||
return authorization_server.create_endpoint_response('revocation')
|
18
fittrackee/oauth2/server.py
Normal file
18
fittrackee/oauth2/server.py
Normal file
@ -0,0 +1,18 @@
|
||||
from authlib.integrations.flask_oauth2 import AuthorizationServer
|
||||
from authlib.integrations.sqla_oauth2 import (
|
||||
create_query_client_func,
|
||||
create_save_token_func,
|
||||
)
|
||||
|
||||
from fittrackee import db
|
||||
|
||||
from .models import OAuth2Client, OAuth2Token
|
||||
from .resource_protector import CustomResourceProtector
|
||||
|
||||
query_client = create_query_client_func(db.session, OAuth2Client)
|
||||
save_token = create_save_token_func(db.session, OAuth2Token)
|
||||
authorization_server = AuthorizationServer(
|
||||
query_client=query_client,
|
||||
save_token=save_token,
|
||||
)
|
||||
require_auth = CustomResourceProtector()
|
@ -324,3 +324,39 @@ class TestUpdateConfig(ApiTestCaseMixin):
|
||||
data = json.loads(response.data.decode())
|
||||
assert 'success' in data['status']
|
||||
assert data['data']['admin_contact'] is None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', True),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', False),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1_admin: User,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1_admin, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/config',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
@ -10,6 +10,7 @@ os.environ['DATABASE_URL'] = os.environ['DATABASE_TEST_URL']
|
||||
TEMP_FOLDER = '/tmp/FitTrackee'
|
||||
os.environ['UPLOAD_FOLDER'] = TEMP_FOLDER
|
||||
os.environ['APP_LOG'] = TEMP_FOLDER + '/fittrackee.log'
|
||||
os.environ['AUTHLIB_INSECURE_TRANSPORT'] = '1'
|
||||
|
||||
pytest_plugins = [
|
||||
'fittrackee.tests.fixtures.fixtures_app',
|
||||
|
@ -22,3 +22,19 @@ def assert_errored_response(
|
||||
if match is not None:
|
||||
assert re.match(match, data['message'])
|
||||
return data
|
||||
|
||||
|
||||
def assert_oauth_errored_response(
|
||||
response: TestResponse,
|
||||
status_code: int,
|
||||
error: str,
|
||||
error_description: Optional[str] = None,
|
||||
) -> Dict:
|
||||
assert response.content_type == 'application/json'
|
||||
assert response.status_code == status_code
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert error in data['error']
|
||||
if error_description is not None:
|
||||
assert error_description in data['error_description']
|
||||
return data
|
||||
|
@ -1,12 +1,24 @@
|
||||
import json
|
||||
from typing import Dict, Optional, Tuple
|
||||
import time
|
||||
from random import randint
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
from flask import Flask
|
||||
from flask.testing import FlaskClient
|
||||
from urllib3.util import parse_url
|
||||
from werkzeug.test import TestResponse
|
||||
|
||||
from .custom_asserts import assert_errored_response
|
||||
from .utils import random_email, random_string
|
||||
from fittrackee import db
|
||||
from fittrackee.oauth2.client import create_oauth2_client
|
||||
from fittrackee.oauth2.models import OAuth2Client, OAuth2Token
|
||||
from fittrackee.users.models import User
|
||||
|
||||
from .custom_asserts import (
|
||||
assert_errored_response,
|
||||
assert_oauth_errored_response,
|
||||
)
|
||||
from .utils import TEST_OAUTH_CLIENT_METADATA, random_email, random_string
|
||||
|
||||
|
||||
class RandomMixin:
|
||||
@ -18,12 +30,58 @@ class RandomMixin:
|
||||
) -> str:
|
||||
return random_string(length, prefix, suffix)
|
||||
|
||||
@staticmethod
|
||||
def random_domain() -> str:
|
||||
return random_string(prefix='https://', suffix='com')
|
||||
|
||||
@staticmethod
|
||||
def random_email() -> str:
|
||||
return random_email()
|
||||
|
||||
@staticmethod
|
||||
def random_int(min_val: int = 0, max_val: int = 999999) -> int:
|
||||
return randint(min_val, max_val)
|
||||
|
||||
class ApiTestCaseMixin(RandomMixin):
|
||||
|
||||
class OAuth2Mixin(RandomMixin):
|
||||
@staticmethod
|
||||
def create_oauth2_client(
|
||||
user: User,
|
||||
metadata: Optional[Dict] = None,
|
||||
scope: Optional[str] = None,
|
||||
) -> OAuth2Client:
|
||||
client_metadata = (
|
||||
TEST_OAUTH_CLIENT_METADATA if metadata is None else metadata
|
||||
)
|
||||
if scope is not None:
|
||||
client_metadata['scope'] = scope
|
||||
oauth_client = create_oauth2_client(client_metadata, user)
|
||||
db.session.add(oauth_client)
|
||||
db.session.commit()
|
||||
return oauth_client
|
||||
|
||||
def create_oauth2_token(
|
||||
self,
|
||||
oauth_client: OAuth2Client,
|
||||
issued_at: Optional[int] = None,
|
||||
access_token_revoked_at: Optional[int] = 0,
|
||||
expires_in: Optional[int] = 1000,
|
||||
) -> OAuth2Token:
|
||||
issued_at = issued_at if issued_at else int(time.time())
|
||||
token = OAuth2Token(
|
||||
client_id=oauth_client.client_id,
|
||||
access_token=self.random_string(),
|
||||
refresh_token=self.random_string(),
|
||||
issued_at=issued_at,
|
||||
access_token_revoked_at=access_token_revoked_at,
|
||||
expires_in=expires_in,
|
||||
)
|
||||
db.session.add(token)
|
||||
db.session.commit()
|
||||
return token
|
||||
|
||||
|
||||
class ApiTestCaseMixin(OAuth2Mixin, RandomMixin):
|
||||
@staticmethod
|
||||
def get_test_client_and_auth_token(
|
||||
app: Flask, user_email: str
|
||||
@ -42,6 +100,58 @@ class ApiTestCaseMixin(RandomMixin):
|
||||
auth_token = json.loads(resp_login.data.decode())['auth_token']
|
||||
return client, auth_token
|
||||
|
||||
@staticmethod
|
||||
def authorize_client(
|
||||
client: FlaskClient,
|
||||
oauth_client: OAuth2Client,
|
||||
auth_token: str,
|
||||
scope: Optional[str] = None,
|
||||
code_challenge: Optional[Dict] = None,
|
||||
) -> Union[List[str], str]:
|
||||
if code_challenge is None:
|
||||
code_challenge = {}
|
||||
response = client.post(
|
||||
'/api/oauth/authorize',
|
||||
data={
|
||||
'client_id': oauth_client.client_id,
|
||||
'confirm': True,
|
||||
'response_type': 'code',
|
||||
'scope': 'read' if not scope else scope,
|
||||
**code_challenge,
|
||||
},
|
||||
headers=dict(
|
||||
Authorization=f'Bearer {auth_token}',
|
||||
content_type='multipart/form-data',
|
||||
),
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
parsed_url = parse_url(data['redirect_url'])
|
||||
code = parse_qs(parsed_url.query).get('code', '')
|
||||
return code
|
||||
|
||||
def create_oauth2_client_and_issue_token(
|
||||
self, app: Flask, user: User, scope: Optional[str] = None
|
||||
) -> Tuple[FlaskClient, OAuth2Client, str, str]:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
app, user.email
|
||||
)
|
||||
oauth_client = self.create_oauth2_client(user, scope=scope)
|
||||
code = self.authorize_client(
|
||||
client, oauth_client, auth_token, scope=scope
|
||||
)
|
||||
response = client.post(
|
||||
'/api/oauth/token',
|
||||
data={
|
||||
'client_id': oauth_client.client_id,
|
||||
'client_secret': oauth_client.client_secret,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
},
|
||||
headers=dict(content_type='multipart/form-data'),
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
return client, oauth_client, data.get('access_token'), auth_token
|
||||
|
||||
@staticmethod
|
||||
def assert_400(
|
||||
response: TestResponse,
|
||||
@ -113,6 +223,67 @@ class ApiTestCaseMixin(RandomMixin):
|
||||
response, 500, error_message=error_message, status=status
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def assert_unsupported_grant_type(response: TestResponse) -> Dict:
|
||||
return assert_oauth_errored_response(
|
||||
response, 400, error='unsupported_grant_type'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def assert_invalid_client(response: TestResponse) -> Dict:
|
||||
return assert_oauth_errored_response(
|
||||
response,
|
||||
400,
|
||||
error='invalid_client',
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def assert_invalid_request(
|
||||
response: TestResponse, error_description: Optional[str] = None
|
||||
) -> Dict:
|
||||
return assert_oauth_errored_response(
|
||||
response,
|
||||
400,
|
||||
error='invalid_request',
|
||||
error_description=error_description,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def assert_invalid_token(response: TestResponse) -> Dict:
|
||||
return assert_oauth_errored_response(
|
||||
response,
|
||||
401,
|
||||
error='invalid_token',
|
||||
error_description=(
|
||||
'The access token provided is expired, revoked, malformed, '
|
||||
'or invalid for other reasons.'
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def assert_insufficient_scope(response: TestResponse) -> Dict:
|
||||
return assert_oauth_errored_response(
|
||||
response,
|
||||
403,
|
||||
error='insufficient_scope',
|
||||
error_description=(
|
||||
'The request requires higher privileges than provided by '
|
||||
'the access token.'
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def assert_not_insufficient_scope_error(response: TestResponse) -> None:
|
||||
assert response.status_code != 403
|
||||
|
||||
def assert_response_scope(
|
||||
self, response: TestResponse, can_access: bool
|
||||
) -> None:
|
||||
if can_access:
|
||||
self.assert_not_insufficient_scope_error(response)
|
||||
else:
|
||||
self.assert_insufficient_scope(response)
|
||||
|
||||
|
||||
class CallArgsMixin:
|
||||
"""call args are returned differently between Python 3.7 and 3.7+"""
|
||||
|
0
fittrackee/tests/oauth2/__init__.py
Normal file
0
fittrackee/tests/oauth2/__init__.py
Normal file
59
fittrackee/tests/oauth2/test_oauth2_clean.py
Normal file
59
fittrackee/tests/oauth2/test_oauth2_clean.py
Normal file
@ -0,0 +1,59 @@
|
||||
import time
|
||||
|
||||
from flask import Flask
|
||||
|
||||
from fittrackee.oauth2.clean import clean_tokens
|
||||
from fittrackee.oauth2.models import OAuth2Token
|
||||
from fittrackee.users.models import User
|
||||
|
||||
from ..mixins import OAuth2Mixin
|
||||
|
||||
|
||||
class TestOAuth2CleanTokens(OAuth2Mixin):
|
||||
def test_it_does_not_delete_not_expired_token(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
oauth_client = self.create_oauth2_client(user_1)
|
||||
self.create_oauth2_token(oauth_client)
|
||||
|
||||
clean_tokens(days=1)
|
||||
|
||||
assert OAuth2Token.query.count() == 1
|
||||
|
||||
def test_it_deletes_expired_token(self, app: Flask, user_1: User) -> None:
|
||||
oauth_client = self.create_oauth2_client(user_1)
|
||||
expires_in = 864000 # 10 days
|
||||
days = 5
|
||||
self.create_oauth2_token(
|
||||
oauth_client,
|
||||
issued_at=int(time.time()) - expires_in - (days * 86400) - 1,
|
||||
expires_in=expires_in,
|
||||
)
|
||||
|
||||
clean_tokens(days=days)
|
||||
|
||||
assert OAuth2Token.query.count() == 0
|
||||
|
||||
def test_it_returns_deleted_rows_count(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
oauth_client = self.create_oauth2_client(user_1)
|
||||
expires_in = 86400 # 10 days
|
||||
days = 5
|
||||
expected_deleted_rows = 3
|
||||
for _ in range(expected_deleted_rows):
|
||||
self.create_oauth2_token(
|
||||
oauth_client,
|
||||
issued_at=(int(time.time()) - expires_in - (days * 86400) - 1),
|
||||
expires_in=expires_in,
|
||||
)
|
||||
self.create_oauth2_token(oauth_client)
|
||||
self.create_oauth2_token(
|
||||
oauth_client,
|
||||
issued_at=(int(time.time()) - expires_in - (days * 86400)),
|
||||
expires_in=expires_in,
|
||||
)
|
||||
|
||||
result = clean_tokens(days=days)
|
||||
|
||||
assert result == expected_deleted_rows
|
199
fittrackee/tests/oauth2/test_oauth2_client.py
Normal file
199
fittrackee/tests/oauth2/test_oauth2_client.py
Normal file
@ -0,0 +1,199 @@
|
||||
from time import time
|
||||
from typing import Any, Dict
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from fittrackee.oauth2.client import check_scope, create_oauth2_client
|
||||
from fittrackee.oauth2.exceptions import InvalidOAuth2Scopes
|
||||
from fittrackee.oauth2.models import OAuth2Client
|
||||
from fittrackee.users.models import User
|
||||
|
||||
from ..utils import random_domain, random_string
|
||||
|
||||
TEST_METADATA = {
|
||||
'client_name': random_string(),
|
||||
'client_uri': random_string(),
|
||||
'redirect_uris': [random_domain()],
|
||||
'scope': 'profile:read',
|
||||
}
|
||||
|
||||
|
||||
class TestCreateOAuth2Client:
|
||||
def test_it_creates_oauth_client(self, app: Flask, user_1: User) -> None:
|
||||
oauth_client = create_oauth2_client(TEST_METADATA, user_1)
|
||||
|
||||
assert isinstance(oauth_client, OAuth2Client)
|
||||
|
||||
def test_oauth_client_id_is_generated_with_gen_salt(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
client_id = random_string()
|
||||
with patch(
|
||||
'fittrackee.oauth2.client.gen_salt', return_value=client_id
|
||||
):
|
||||
|
||||
oauth_client = create_oauth2_client(TEST_METADATA, user_1)
|
||||
|
||||
assert oauth_client.client_id == client_id
|
||||
|
||||
def test_oauth_client_client_id_issued_at_is_initialized(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
client_id_issued_at = int(time())
|
||||
with patch(
|
||||
'fittrackee.oauth2.client.time', return_value=client_id_issued_at
|
||||
):
|
||||
oauth_client = create_oauth2_client(TEST_METADATA, user_1)
|
||||
|
||||
assert oauth_client.client_id_issued_at == client_id_issued_at
|
||||
|
||||
def test_oauth_client_has_expected_name(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
client_name = random_string()
|
||||
client_metadata: Dict = {**TEST_METADATA, 'client_name': client_name}
|
||||
|
||||
oauth_client = create_oauth2_client(client_metadata, user_1)
|
||||
|
||||
assert oauth_client.client_name == client_name
|
||||
|
||||
def test_oauth_client_has_no_description_when_not_provided_in_metadata(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
oauth_client = create_oauth2_client(TEST_METADATA, user_1)
|
||||
|
||||
assert oauth_client.client_description is None
|
||||
|
||||
def test_oauth_client_has_expected_description(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
client_description = random_string()
|
||||
client_metadata: Dict = {
|
||||
**TEST_METADATA,
|
||||
'client_description': client_description,
|
||||
}
|
||||
|
||||
oauth_client = create_oauth2_client(client_metadata, user_1)
|
||||
|
||||
assert oauth_client.client_description == client_description
|
||||
|
||||
def test_oauth_client_has_expected_client_uri(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
client_uri = random_domain()
|
||||
client_metadata: Dict = {**TEST_METADATA, 'client_uri': client_uri}
|
||||
|
||||
oauth_client = create_oauth2_client(client_metadata, user_1)
|
||||
|
||||
assert oauth_client.client_uri == client_uri
|
||||
|
||||
def test_oauth_client_has_expected_grant_types(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
oauth_client = create_oauth2_client(TEST_METADATA, user_1)
|
||||
|
||||
assert oauth_client.grant_types == [
|
||||
'authorization_code',
|
||||
'refresh_token',
|
||||
]
|
||||
|
||||
def test_oauth_client_has_expected_redirect_uris(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
redirect_uris = [random_domain()]
|
||||
client_metadata: Dict = {
|
||||
**TEST_METADATA,
|
||||
'redirect_uris': redirect_uris,
|
||||
}
|
||||
|
||||
oauth_client = create_oauth2_client(client_metadata, user_1)
|
||||
|
||||
assert oauth_client.redirect_uris == redirect_uris
|
||||
|
||||
def test_oauth_client_has_expected_response_types(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
response_types = ['code']
|
||||
client_metadata: Dict = {
|
||||
**TEST_METADATA,
|
||||
'response_types': response_types,
|
||||
}
|
||||
|
||||
oauth_client = create_oauth2_client(client_metadata, user_1)
|
||||
|
||||
assert oauth_client.response_types == ['code']
|
||||
|
||||
def test_oauth_client_has_expected_scope(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
scope = 'workouts:write'
|
||||
client_metadata: Dict = {**TEST_METADATA, 'scope': scope}
|
||||
|
||||
oauth_client = create_oauth2_client(client_metadata, user_1)
|
||||
|
||||
assert oauth_client.scope == scope
|
||||
|
||||
def test_it_raises_error_when_scope_is_invalid(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
client_metadata: Dict = {**TEST_METADATA, 'scope': random_string()}
|
||||
|
||||
with pytest.raises(InvalidOAuth2Scopes):
|
||||
create_oauth2_client(client_metadata, user_1)
|
||||
|
||||
def test_oauth_client_has_expected_token_endpoint_auth_method(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
oauth_client = create_oauth2_client(TEST_METADATA, user_1)
|
||||
|
||||
assert oauth_client.token_endpoint_auth_method == 'client_secret_post'
|
||||
|
||||
def test_when_auth_method_is_not_none_oauth_client_secret_is_generated(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
client_secret = random_string()
|
||||
with patch(
|
||||
'fittrackee.oauth2.client.gen_salt', return_value=client_secret
|
||||
):
|
||||
|
||||
oauth_client = create_oauth2_client(TEST_METADATA, user_1)
|
||||
|
||||
assert oauth_client.client_secret == client_secret
|
||||
|
||||
def test_it_creates_oauth_client_for_given_user(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
oauth_client = create_oauth2_client(TEST_METADATA, user_1)
|
||||
|
||||
assert oauth_client.user_id == user_1.id
|
||||
|
||||
|
||||
class TestOAuthCheckScopes:
|
||||
@pytest.mark.parametrize(
|
||||
'input_scope', ['', 1, random_string(), [random_string(), 'readwrite']]
|
||||
)
|
||||
def test_it_raises_error_when_scope_is_invalid(
|
||||
self, input_scope: Any
|
||||
) -> None:
|
||||
with pytest.raises(InvalidOAuth2Scopes):
|
||||
check_scope(input_scope)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'input_scope,expected_scope',
|
||||
[
|
||||
('profile:read', 'profile:read'),
|
||||
('profile:read ' + random_string(), 'profile:read'),
|
||||
('profile:write', 'profile:write'),
|
||||
('profile:read profile:write', 'profile:read profile:write'),
|
||||
(
|
||||
'profile:write profile:read ' + random_string(),
|
||||
'profile:write profile:read',
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_it_return_only_valid_scopes(
|
||||
self, input_scope: str, expected_scope: str
|
||||
) -> None:
|
||||
assert check_scope(input_scope) == expected_scope
|
118
fittrackee/tests/oauth2/test_oauth2_models.py
Normal file
118
fittrackee/tests/oauth2/test_oauth2_models.py
Normal file
@ -0,0 +1,118 @@
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from fittrackee.oauth2.models import OAuth2Client, OAuth2Token
|
||||
from fittrackee.users.models import User
|
||||
|
||||
from ..mixins import OAuth2Mixin
|
||||
|
||||
|
||||
class TestOAuth2ClientSerialize(OAuth2Mixin):
|
||||
def test_it_returns_oauth_client(self, app: Flask, user_1: User) -> None:
|
||||
oauth_client = self.create_oauth2_client(user_1)
|
||||
oauth_client.client_id_issued_at = 1653738796
|
||||
|
||||
serialized_oauth_client = oauth_client.serialize()
|
||||
|
||||
assert serialized_oauth_client['client_id'] == oauth_client.client_id
|
||||
assert (
|
||||
serialized_oauth_client['client_description']
|
||||
== oauth_client.client_description
|
||||
)
|
||||
assert 'client_secret' not in serialized_oauth_client
|
||||
assert (
|
||||
serialized_oauth_client['issued_at']
|
||||
== 'Sat, 28 May 2022 11:53:16 GMT'
|
||||
)
|
||||
assert serialized_oauth_client['id'] == oauth_client.id
|
||||
assert serialized_oauth_client['name'] == oauth_client.client_name
|
||||
assert (
|
||||
serialized_oauth_client['redirect_uris']
|
||||
== oauth_client.redirect_uris
|
||||
)
|
||||
assert serialized_oauth_client['scope'] == oauth_client.scope
|
||||
assert serialized_oauth_client['website'] == oauth_client.client_uri
|
||||
|
||||
def test_it_returns_oauth_client_with_client_secret(
|
||||
self, app: Flask
|
||||
) -> None:
|
||||
oauth_client = OAuth2Client(
|
||||
id=self.random_int(),
|
||||
client_id=self.random_string(),
|
||||
client_id_issued_at=self.random_int(),
|
||||
)
|
||||
oauth_client.set_client_metadata(
|
||||
{
|
||||
'client_name': self.random_string(),
|
||||
'redirect_uris': [self.random_string()],
|
||||
'client_uri': self.random_domain(),
|
||||
}
|
||||
)
|
||||
|
||||
serialized_oauth_client = oauth_client.serialize(with_secret=True)
|
||||
|
||||
assert (
|
||||
serialized_oauth_client['client_secret']
|
||||
== oauth_client.client_secret
|
||||
)
|
||||
|
||||
|
||||
class TestOAuth2Token(OAuth2Mixin):
|
||||
@pytest.mark.parametrize(
|
||||
'input_expiration,expected_status', [(1000, True), (0, False)]
|
||||
)
|
||||
def test_it_returns_refresh_token_status(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
input_expiration: int,
|
||||
expected_status: bool,
|
||||
) -> None:
|
||||
oauth_client = self.create_oauth2_client(user_1)
|
||||
token = OAuth2Token(
|
||||
client_id=oauth_client.client_id,
|
||||
access_token=self.random_string(),
|
||||
refresh_token=self.random_string(),
|
||||
issued_at=int(time.time()),
|
||||
expires_in=input_expiration,
|
||||
)
|
||||
|
||||
assert token.is_refresh_token_active() is expected_status
|
||||
|
||||
def test_it_returns_refresh_token_active_when_below_twice_expiration(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
oauth_client = self.create_oauth2_client(user_1)
|
||||
issued_at = int(time.time())
|
||||
expires_in = self.random_int()
|
||||
token = OAuth2Token(
|
||||
client_id=oauth_client.client_id,
|
||||
access_token=self.random_string(),
|
||||
refresh_token=self.random_string(),
|
||||
issued_at=int(time.time()),
|
||||
expires_in=expires_in,
|
||||
)
|
||||
|
||||
with patch(
|
||||
'fittrackee.oauth2.models.time.time',
|
||||
return_value=(issued_at + expires_in * 2 - 1),
|
||||
):
|
||||
assert token.is_refresh_token_active() is True
|
||||
|
||||
def test_it_returns_refresh_token_inactive_when_token_revoked(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
oauth_client = self.create_oauth2_client(user_1)
|
||||
token = OAuth2Token(
|
||||
client_id=oauth_client.client_id,
|
||||
access_token=self.random_string(),
|
||||
refresh_token=self.random_string(),
|
||||
issued_at=int(time.time()),
|
||||
access_token_revoked_at=int(time.time()),
|
||||
expires_in=1000,
|
||||
)
|
||||
|
||||
assert token.is_refresh_token_active() is False
|
1291
fittrackee/tests/oauth2/test_oauth2_routes.py
Normal file
1291
fittrackee/tests/oauth2/test_oauth2_routes.py
Normal file
File diff suppressed because it is too large
Load Diff
58
fittrackee/tests/oauth2/test_oauth2_scopes.py
Normal file
58
fittrackee/tests/oauth2/test_oauth2_scopes.py
Normal file
@ -0,0 +1,58 @@
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from fittrackee.users.models import User
|
||||
|
||||
from ..mixins import ApiTestCaseMixin
|
||||
|
||||
|
||||
class TestOAuth2Scopes(ApiTestCaseMixin):
|
||||
@pytest.mark.parametrize(
|
||||
'endpoint_url,scope',
|
||||
[
|
||||
('/api/auth/profile', 'profile:read'),
|
||||
('/api/workouts', 'workouts:read'),
|
||||
],
|
||||
)
|
||||
def test_oauth_client_can_access_authorized_endpoints(
|
||||
self, app: Flask, user_1: User, endpoint_url: str, scope: str
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(app, user_1, scope=scope)
|
||||
|
||||
response = client.get(
|
||||
endpoint_url,
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_not_insufficient_scope_error(response)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'endpoint_url,scope',
|
||||
[
|
||||
('/api/auth/profile', 'workouts:read'),
|
||||
('/api/workouts', 'profile:read'),
|
||||
],
|
||||
)
|
||||
def test_oauth_client_can_not_access_unauthorized_endpoints(
|
||||
self, app: Flask, user_1: User, endpoint_url: str, scope: str
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(app, user_1, scope=scope)
|
||||
|
||||
response = client.get(
|
||||
endpoint_url,
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_insufficient_scope(response)
|
@ -498,7 +498,7 @@ class TestUserProfile(ApiTestCaseMixin):
|
||||
'/api/auth/profile', headers=dict(Authorization='Bearer invalid')
|
||||
)
|
||||
|
||||
self.assert_401(response, 'invalid token, please log in again')
|
||||
self.assert_invalid_token(response)
|
||||
|
||||
def test_it_returns_user(self, app: Flask, user_1: User) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
@ -515,6 +515,38 @@ class TestUserProfile(ApiTestCaseMixin):
|
||||
assert data['status'] == 'success'
|
||||
assert data['data'] == jsonify_dict(user_1.serialize(user_1))
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', True),
|
||||
('profile:write', False),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', False),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self, app: Flask, user_1: User, client_scope: str, can_access: bool
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/auth/profile',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestUserProfileUpdate(ApiTestCaseMixin):
|
||||
def test_it_returns_error_if_payload_is_empty(
|
||||
@ -580,6 +612,42 @@ class TestUserProfileUpdate(ApiTestCaseMixin):
|
||||
assert data['message'] == 'user profile updated'
|
||||
assert data['data'] == jsonify_dict(user_1.serialize(user_1))
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', True),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', False),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/profile/edit',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestUserAccountUpdate(ApiTestCaseMixin):
|
||||
@staticmethod
|
||||
@ -1214,6 +1282,42 @@ class TestUserAccountUpdate(ApiTestCaseMixin):
|
||||
password_change_email_mock,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', True),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', False),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/auth/profile/edit/account',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestUserPreferencesUpdate(ApiTestCaseMixin):
|
||||
def test_it_returns_error_if_payload_is_empty(
|
||||
@ -1288,6 +1392,42 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
|
||||
assert data['data']['timezone'] == 'America/New_York'
|
||||
assert data['data']['weekm'] is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', True),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', False),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/profile/edit/preferences',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestUserSportPreferencesUpdate(ApiTestCaseMixin):
|
||||
def test_it_returns_error_if_payload_is_empty(
|
||||
@ -1470,6 +1610,42 @@ class TestUserSportPreferencesUpdate(ApiTestCaseMixin):
|
||||
assert data['data']['is_active']
|
||||
assert data['data']['stopped_speed_threshold'] == 0.5
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', True),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', False),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/profile/edit/sports',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestUserSportPreferencesReset(ApiTestCaseMixin):
|
||||
def test_it_returns_error_if_sport_does_not_exist(
|
||||
@ -1525,6 +1701,44 @@ class TestUserSportPreferencesReset(ApiTestCaseMixin):
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', True),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', False),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
sport_1_cycling: Sport,
|
||||
user_sport_1_preference: UserSportPreference,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.delete(
|
||||
f'/api/auth/profile/reset/sports/{sport_1_cycling.id}',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestUserPicture(ApiTestCaseMixin):
|
||||
def test_it_returns_error_if_file_is_missing(
|
||||
@ -1654,6 +1868,42 @@ class TestUserPicture(ApiTestCaseMixin):
|
||||
assert 'avatar.png' not in user_1.picture
|
||||
assert 'avatar2.png' in user_1.picture
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', True),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', False),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/picture',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestRegistrationConfiguration(ApiTestCaseMixin):
|
||||
def test_it_returns_error_if_it_exceeds_max_users(
|
||||
|
@ -3,6 +3,7 @@ from datetime import datetime, timedelta
|
||||
from io import BytesIO
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from fittrackee.users.models import User, UserSportPreference
|
||||
@ -131,6 +132,42 @@ class TestGetUser(ApiTestCaseMixin):
|
||||
|
||||
self.assert_404_with_entity(response, 'user')
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', True),
|
||||
('users:write', False),
|
||||
('workouts:read', False),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1_admin: User,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1_admin, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/users/not_existing',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestGetUsers(ApiTestCaseMixin):
|
||||
def test_it_returns_error_if_user_has_no_admin_rights(
|
||||
@ -885,6 +922,42 @@ class TestGetUsers(ApiTestCaseMixin):
|
||||
'total': 3,
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', True),
|
||||
('users:write', False),
|
||||
('workouts:read', False),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1_admin: User,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1_admin, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/users',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestGetUserPicture(ApiTestCaseMixin):
|
||||
def test_it_return_error_if_user_has_no_picture(
|
||||
@ -1360,6 +1433,43 @@ class TestUpdateUser(ApiTestCaseMixin):
|
||||
assert user['is_active'] is True
|
||||
assert user_2.confirmation_token is None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', False),
|
||||
('users:write', True),
|
||||
('workouts:read', False),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1_admin: User,
|
||||
user_2: User,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1_admin, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
f'/api/users/{user_2.username}',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestDeleteUser(ApiTestCaseMixin):
|
||||
def test_user_can_delete_its_own_account(
|
||||
@ -1573,3 +1683,40 @@ class TestDeleteUser(ApiTestCaseMixin):
|
||||
)
|
||||
|
||||
self.assert_403(response, 'error, registration is disabled')
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', False),
|
||||
('users:write', True),
|
||||
('workouts:read', False),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1_admin: User,
|
||||
user_2: User,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1_admin, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.delete(
|
||||
f'/api/users/{user_2.username}',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
@ -1,6 +1,12 @@
|
||||
from unittest.mock import patch
|
||||
from calendar import timegm
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from flask import Flask
|
||||
|
||||
from fittrackee import bcrypt
|
||||
@ -17,6 +23,7 @@ from fittrackee.users.utils.controls import (
|
||||
is_valid_email,
|
||||
register_controls,
|
||||
)
|
||||
from fittrackee.users.utils.token import decode_user_token, get_user_token
|
||||
|
||||
from ..utils import random_email
|
||||
|
||||
@ -333,3 +340,174 @@ class TestRegisterControls:
|
||||
'username: 3 to 30 characters required\n'
|
||||
'email: valid email must be provided\n'
|
||||
)
|
||||
|
||||
|
||||
class TestGetUserToken:
|
||||
@staticmethod
|
||||
def decode_token(app: Flask, token: str) -> Dict:
|
||||
return jwt.decode(
|
||||
token,
|
||||
app.config['SECRET_KEY'],
|
||||
algorithms=['HS256'],
|
||||
)
|
||||
|
||||
def test_token_is_encoded_with_hs256(self, app: Flask) -> None:
|
||||
token = get_user_token(user_id=1)
|
||||
|
||||
decoded_token = self.decode_token(app, token)
|
||||
assert list(decoded_token.keys()) == ['exp', 'iat', 'sub']
|
||||
|
||||
@pytest.mark.parametrize('input_password_reset', [True, False])
|
||||
def test_token_contains_user_id(
|
||||
self, app: Flask, input_password_reset: bool
|
||||
) -> None:
|
||||
user_id = 1
|
||||
token = get_user_token(
|
||||
user_id=user_id, password_reset=input_password_reset
|
||||
)
|
||||
|
||||
decoded_token = self.decode_token(app, token)
|
||||
assert decoded_token['sub'] == user_id
|
||||
|
||||
@pytest.mark.parametrize('input_password_reset', [True, False])
|
||||
def test_token_contains_timestamp_of_when_it_is_issued(
|
||||
self, app: Flask, input_password_reset: bool
|
||||
) -> None:
|
||||
user_id = 1
|
||||
iat = datetime.utcnow()
|
||||
with patch('fittrackee.users.utils.token.datetime') as datetime_mock:
|
||||
datetime_mock.utcnow = Mock(return_value=iat)
|
||||
|
||||
token = get_user_token(
|
||||
user_id=user_id, password_reset=input_password_reset
|
||||
)
|
||||
|
||||
decoded_token = self.decode_token(app, token)
|
||||
assert decoded_token['iat'] == timegm(iat.utctimetuple())
|
||||
|
||||
def test_token_contains_timestamp_of_when_it_expired(
|
||||
self, app: Flask
|
||||
) -> None:
|
||||
user_id = 1
|
||||
iat = datetime.utcnow()
|
||||
expiration = timedelta(
|
||||
days=app.config['TOKEN_EXPIRATION_DAYS'],
|
||||
seconds=app.config['TOKEN_EXPIRATION_SECONDS'],
|
||||
)
|
||||
with patch('fittrackee.users.utils.token.datetime') as datetime_mock:
|
||||
datetime_mock.utcnow = Mock(return_value=iat)
|
||||
|
||||
token = get_user_token(user_id=user_id)
|
||||
|
||||
decoded_token = self.decode_token(app, token)
|
||||
assert decoded_token['exp'] == timegm(
|
||||
(iat + expiration).utctimetuple()
|
||||
)
|
||||
|
||||
def test_password_token_contains_timestamp_of_when_it_expired(
|
||||
self, app: Flask
|
||||
) -> None:
|
||||
user_id = 1
|
||||
iat = datetime.utcnow()
|
||||
expiration = timedelta(
|
||||
days=0.0,
|
||||
seconds=app.config['PASSWORD_TOKEN_EXPIRATION_SECONDS'],
|
||||
)
|
||||
with patch('fittrackee.users.utils.token.datetime') as datetime_mock:
|
||||
datetime_mock.utcnow = Mock(return_value=iat)
|
||||
|
||||
token = get_user_token(user_id=user_id, password_reset=True)
|
||||
|
||||
decoded_token = self.decode_token(app, token)
|
||||
assert decoded_token['exp'] == timegm(
|
||||
(iat + expiration).utctimetuple()
|
||||
)
|
||||
|
||||
|
||||
class TestDecodeUserToken:
|
||||
@staticmethod
|
||||
def generate_token(user_id: int, now: datetime) -> str:
|
||||
with patch('fittrackee.users.utils.token.datetime') as datetime_mock:
|
||||
datetime_mock.utcnow = Mock(return_value=now)
|
||||
token = get_user_token(user_id)
|
||||
return token
|
||||
|
||||
def test_it_raises_error_when_token_is_invalid(self, app: Flask) -> None:
|
||||
with pytest.raises(jwt.exceptions.DecodeError):
|
||||
|
||||
decode_user_token(random_string())
|
||||
|
||||
def test_it_raises_error_when_token_body_is_invalid(
|
||||
self, app: Flask
|
||||
) -> None:
|
||||
token = self.generate_token(user_id=1, now=datetime.utcnow())
|
||||
header, body, signature = token.split('.')
|
||||
modified_token = f'{header}.{random_string()}.{signature}'
|
||||
with pytest.raises(
|
||||
jwt.exceptions.InvalidSignatureError,
|
||||
match='Signature verification failed',
|
||||
):
|
||||
|
||||
decode_user_token(modified_token)
|
||||
|
||||
def test_it_raises_error_when_secret_key_is_invalid(
|
||||
self, app: Flask
|
||||
) -> None:
|
||||
now = datetime.utcnow()
|
||||
token = jwt.encode(
|
||||
{
|
||||
'exp': now + timedelta(minutes=1),
|
||||
'iat': now,
|
||||
'sub': 1,
|
||||
},
|
||||
random_string(),
|
||||
algorithm='HS256',
|
||||
)
|
||||
with pytest.raises(
|
||||
jwt.exceptions.InvalidSignatureError,
|
||||
match='Signature verification failed',
|
||||
):
|
||||
|
||||
decode_user_token(token)
|
||||
|
||||
def test_it_raises_error_when_algorithm_is_not_hs256(
|
||||
self, app: Flask
|
||||
) -> None:
|
||||
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
private_key = key.private_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
serialization.PrivateFormat.PKCS8,
|
||||
serialization.NoEncryption(),
|
||||
)
|
||||
now = datetime.utcnow()
|
||||
token = jwt.encode(
|
||||
{
|
||||
'exp': now + timedelta(minutes=1),
|
||||
'iat': now,
|
||||
'sub': 1,
|
||||
},
|
||||
private_key.decode(),
|
||||
algorithm="RS256",
|
||||
)
|
||||
with pytest.raises(jwt.exceptions.InvalidAlgorithmError):
|
||||
|
||||
decode_user_token(token)
|
||||
|
||||
def test_it_raises_error_when_token_is_expired(self, app: Flask) -> None:
|
||||
now = datetime.utcnow() - timedelta(minutes=10)
|
||||
token = self.generate_token(user_id=1, now=now)
|
||||
with pytest.raises(
|
||||
jwt.exceptions.ExpiredSignatureError, match='Signature has expired'
|
||||
):
|
||||
|
||||
decode_user_token(token)
|
||||
|
||||
def test_it_returns_user_id(self, app: Flask) -> None:
|
||||
expected_user_id = 1
|
||||
token = self.generate_token(
|
||||
user_id=expected_user_id, now=datetime.utcnow()
|
||||
)
|
||||
|
||||
user_id = decode_user_token(token)
|
||||
|
||||
assert user_id == expected_user_id
|
||||
|
@ -2,9 +2,12 @@ import random
|
||||
import string
|
||||
from json import loads
|
||||
from typing import Dict, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from flask import json as flask_json
|
||||
|
||||
from fittrackee.workouts.utils.short_id import encode_uuid
|
||||
|
||||
|
||||
def random_string(
|
||||
length: Optional[int] = None,
|
||||
@ -24,9 +27,25 @@ def random_string(
|
||||
)
|
||||
|
||||
|
||||
def random_domain() -> str:
|
||||
return random_string(prefix='https://', suffix='.com')
|
||||
|
||||
|
||||
def random_email() -> str:
|
||||
return random_string(suffix='@example.com')
|
||||
|
||||
|
||||
def random_short_id() -> str:
|
||||
return encode_uuid(uuid4())
|
||||
|
||||
|
||||
def jsonify_dict(data: Dict) -> Dict:
|
||||
return loads(flask_json.dumps(data))
|
||||
|
||||
|
||||
TEST_OAUTH_CLIENT_METADATA = {
|
||||
'client_name': random_string(),
|
||||
'client_uri': random_domain(),
|
||||
'redirect_uris': [random_domain()],
|
||||
'scope': 'profile:read workouts:read',
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from fittrackee.users.models import User
|
||||
@ -897,3 +898,40 @@ class TestGetRecords(ApiTestCaseMixin):
|
||||
response = client.get('/api/records')
|
||||
|
||||
self.assert_401(response)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', True),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1_admin: User,
|
||||
user_2: User,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1_admin, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/records',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
@ -1,5 +1,6 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from fittrackee import db
|
||||
@ -138,6 +139,43 @@ class TestGetSports(ApiTestCaseMixin):
|
||||
sport_2_running.serialize(is_admin=True)
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', True),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/sports',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestGetSport(ApiTestCaseMixin):
|
||||
def test_it_gets_a_sport(
|
||||
@ -241,6 +279,43 @@ class TestGetSport(ApiTestCaseMixin):
|
||||
sport_1_cycling_inactive.serialize(is_admin=True)
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', True),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
f'/api/sports/{sport_1_cycling.id}',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestUpdateSport(ApiTestCaseMixin):
|
||||
def test_it_disables_a_sport(
|
||||
@ -442,3 +517,41 @@ class TestUpdateSport(ApiTestCaseMixin):
|
||||
|
||||
data = self.assert_404(response)
|
||||
assert len(data['data']['sports']) == 0
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', False),
|
||||
('workouts:write', True),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1_admin: User,
|
||||
user_2: User,
|
||||
sport_1_cycling: Sport,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1_admin, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
f'/api/sports/{sport_1_cycling.id}',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
@ -1,5 +1,6 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from fittrackee.users.models import User
|
||||
@ -862,6 +863,42 @@ class TestGetStatsByTime(ApiTestCaseMixin):
|
||||
}
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', True),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
f'/api/stats/{user_1.username}/by_time',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestGetStatsBySport(ApiTestCaseMixin):
|
||||
def test_it_returns_error_if_user_is_not_authenticated(
|
||||
@ -1007,6 +1044,42 @@ class TestGetStatsBySport(ApiTestCaseMixin):
|
||||
|
||||
self.assert_500(response)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', True),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
f'/api/stats/{user_1.username}/by_sport',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestGetAllStats(ApiTestCaseMixin):
|
||||
def test_it_returns_error_if_user_is_not_authenticated(
|
||||
@ -1089,3 +1162,39 @@ class TestGetAllStats(ApiTestCaseMixin):
|
||||
)
|
||||
|
||||
self.assert_403(response)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', True),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1_admin: User,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1_admin, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/stats/all',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
@ -3,6 +3,7 @@ from typing import List
|
||||
from unittest.mock import patch
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from fittrackee.users.models import User
|
||||
@ -101,6 +102,42 @@ class TestGetWorkouts(ApiTestCaseMixin):
|
||||
|
||||
self.assert_401(response, 'provide a valid auth token')
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', True),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/workouts',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestGetWorkoutsWithPagination(ApiTestCaseMixin):
|
||||
def test_it_gets_workouts_with_default_pagination(
|
||||
@ -1179,6 +1216,55 @@ class TestGetWorkout(ApiTestCaseMixin):
|
||||
|
||||
self.assert_404_with_message(response, 'Map file does not exist')
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', True),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
'endpoint',
|
||||
[
|
||||
'/api/workouts/{workout_short_id}',
|
||||
'/api/workouts/{workout_short_id}/gpx',
|
||||
'/api/workouts/{workout_short_id}/chart_data',
|
||||
'/api/workouts/{workout_short_id}/gpx/segment/1',
|
||||
'/api/workouts/{workout_short_id}/chart_data/segment/1',
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
workout_cycling_user_1: Workout,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
endpoint: str,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
endpoint.format(workout_short_id=workout_cycling_user_1.short_id),
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestDownloadWorkoutGpx(ApiTestCaseMixin):
|
||||
def test_it_returns_404_if_workout_does_not_exist(
|
||||
@ -1263,3 +1349,41 @@ class TestDownloadWorkoutGpx(ApiTestCaseMixin):
|
||||
mimetype='application/gpx+xml',
|
||||
as_attachment=True,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', True),
|
||||
('workouts:write', False),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
workout_cycling_user_1: Workout,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
f'/api/workouts/{workout_cycling_user_1.short_id}/gpx/download',
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
@ -772,6 +772,45 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
|
||||
)
|
||||
assert 'data' not in data
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', False),
|
||||
('workouts:write', True),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
'/api/workouts',
|
||||
data=dict(),
|
||||
headers=dict(
|
||||
content_type='multipart/form-data',
|
||||
Authorization=f'Bearer {access_token}',
|
||||
),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestPostWorkoutWithoutGpx(ApiTestCaseMixin):
|
||||
def test_it_returns_error_if_user_is_not_authenticated(
|
||||
@ -914,6 +953,45 @@ class TestPostWorkoutWithoutGpx(ApiTestCaseMixin):
|
||||
assert len(data['data']['workouts'][0]['segments']) == 0
|
||||
assert len(data['data']['workouts'][0]['records']) == 0
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', False),
|
||||
('workouts:write', True),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
'/api/workouts/no_gpx',
|
||||
data=dict(),
|
||||
headers=dict(
|
||||
content_type='multipart/form-data',
|
||||
Authorization=f'Bearer {access_token}',
|
||||
),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestPostWorkoutWithZipArchive(ApiTestCaseMixin):
|
||||
def test_it_adds_workouts_with_zip_archive(
|
||||
|
@ -225,6 +225,45 @@ class TestEditWorkoutWithGpx(ApiTestCaseMixin):
|
||||
|
||||
self.assert_500(response)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', False),
|
||||
('workouts:write', True),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
workout_cycling_user_1: Workout,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
f'/api/workouts/{workout_cycling_user_1.short_id}',
|
||||
data=dict(),
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestEditWorkoutWithoutGpx(ApiTestCaseMixin):
|
||||
def test_it_updates_a_workout_wo_gpx(
|
||||
|
@ -1,3 +1,4 @@
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from fittrackee.users.models import User
|
||||
@ -101,6 +102,45 @@ class TestDeleteWorkoutWithGpx(ApiTestCaseMixin):
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'client_scope, can_access',
|
||||
[
|
||||
('application:write', False),
|
||||
('profile:read', False),
|
||||
('profile:write', False),
|
||||
('users:read', False),
|
||||
('users:write', False),
|
||||
('workouts:read', False),
|
||||
('workouts:write', True),
|
||||
],
|
||||
)
|
||||
def test_expected_scopes_are_defined(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
workout_cycling_user_1: Workout,
|
||||
client_scope: str,
|
||||
can_access: bool,
|
||||
) -> None:
|
||||
(
|
||||
client,
|
||||
oauth_client,
|
||||
access_token,
|
||||
_,
|
||||
) = self.create_oauth2_client_and_issue_token(
|
||||
app, user_1, scope=client_scope
|
||||
)
|
||||
|
||||
response = client.delete(
|
||||
f'/api/workouts/{workout_cycling_user_1.short_id}',
|
||||
data=dict(),
|
||||
content_type='application/json',
|
||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||
)
|
||||
|
||||
self.assert_response_scope(response, can_access)
|
||||
|
||||
|
||||
class TestDeleteWorkoutWithoutGpx(ApiTestCaseMixin):
|
||||
def test_it_deletes_a_workout_wo_gpx(
|
||||
|
@ -10,7 +10,7 @@ from sqlalchemy import exc, func
|
||||
from werkzeug.exceptions import RequestEntityTooLarge
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from fittrackee import appLog, bcrypt, db
|
||||
from fittrackee import appLog, db
|
||||
from fittrackee.emails.tasks import (
|
||||
account_confirmation_email,
|
||||
email_updated_to_current_address,
|
||||
@ -19,6 +19,7 @@ from fittrackee.emails.tasks import (
|
||||
reset_password_email,
|
||||
)
|
||||
from fittrackee.files import get_absolute_file_path
|
||||
from fittrackee.oauth2.server import require_auth
|
||||
from fittrackee.responses import (
|
||||
ForbiddenErrorResponse,
|
||||
HttpResponse,
|
||||
@ -32,7 +33,6 @@ from fittrackee.responses import (
|
||||
from fittrackee.utils import get_readable_duration
|
||||
from fittrackee.workouts.models import Sport
|
||||
|
||||
from .decorators import authenticate
|
||||
from .models import User, UserSportPreference
|
||||
from .utils.controls import check_password, is_valid_email, register_controls
|
||||
from .utils.token import decode_user_token
|
||||
@ -73,7 +73,7 @@ def send_account_confirmation_email(user: User) -> None:
|
||||
@auth_blueprint.route('/auth/register', methods=['POST'])
|
||||
def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
||||
"""
|
||||
register a user and send confirmation email.
|
||||
Register a user and send confirmation email.
|
||||
|
||||
The newly created account is inactive. The user must confirm his email
|
||||
to activate it.
|
||||
@ -131,7 +131,6 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
||||
error, registration is disabled
|
||||
:statuscode 500:
|
||||
error, please try again or contact the administrator
|
||||
|
||||
"""
|
||||
if not current_app.config.get('is_registration_enabled'):
|
||||
return ForbiddenErrorResponse('error, registration is disabled')
|
||||
@ -190,7 +189,7 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
||||
@auth_blueprint.route('/auth/login', methods=['POST'])
|
||||
def login_user() -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
user login
|
||||
User login.
|
||||
|
||||
Only user with an active account can log in.
|
||||
|
||||
@ -248,7 +247,7 @@ def login_user() -> Union[Dict, HttpResponse]:
|
||||
func.lower(User.email) == func.lower(email),
|
||||
User.is_active == True, # noqa
|
||||
).first()
|
||||
if user and bcrypt.check_password_hash(user.password, password):
|
||||
if user and user.check_password(password):
|
||||
# generate auth token
|
||||
auth_token = user.encode_auth_token(user.id)
|
||||
return {
|
||||
@ -263,12 +262,14 @@ def login_user() -> Union[Dict, HttpResponse]:
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/profile', methods=['GET'])
|
||||
@authenticate
|
||||
@require_auth(scopes=['profile:read'])
|
||||
def get_authenticated_user_profile(
|
||||
auth_user: User,
|
||||
) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
get authenticated user info (profile, account, preferences)
|
||||
Get authenticated user info (profile, account, preferences).
|
||||
|
||||
**Scope**: ``profile:read``
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -369,16 +370,17 @@ def get_authenticated_user_profile(
|
||||
- provide a valid auth token
|
||||
- signature expired, please log in again
|
||||
- invalid token, please log in again
|
||||
|
||||
"""
|
||||
return {'status': 'success', 'data': auth_user.serialize(auth_user)}
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/profile/edit', methods=['POST'])
|
||||
@authenticate
|
||||
@require_auth(scopes=['profile:write'])
|
||||
def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
edit authenticated user profile
|
||||
Edit authenticated user profile.
|
||||
|
||||
**Scope**: ``profile:write``
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -489,7 +491,6 @@ def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
- signature expired, please log in again
|
||||
- invalid token, please log in again
|
||||
:statuscode 500: error, please try again or contact the administrator
|
||||
|
||||
"""
|
||||
# get post data
|
||||
post_data = request.get_json()
|
||||
@ -533,10 +534,10 @@ def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/profile/edit/account', methods=['PATCH'])
|
||||
@authenticate
|
||||
@require_auth(scopes=['profile:write'])
|
||||
def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
update authenticated user email and password
|
||||
Update authenticated user email and password.
|
||||
|
||||
It sends emails if sending is enabled:
|
||||
|
||||
@ -546,6 +547,8 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
- one to the current address to inform user
|
||||
- another one to the new address to confirm it.
|
||||
|
||||
**Scope**: ``profile:write``
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@ -658,7 +661,6 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
- invalid token, please log in again
|
||||
- invalid credentials
|
||||
:statuscode 500: error, please try again or contact the administrator
|
||||
|
||||
"""
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
@ -669,7 +671,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
current_password = data.get('password')
|
||||
if not current_password:
|
||||
return InvalidPayloadErrorResponse('current password is missing')
|
||||
if not bcrypt.check_password_hash(auth_user.password, current_password):
|
||||
if not auth_user.check_password(current_password):
|
||||
return UnauthorizedErrorResponse('invalid credentials')
|
||||
|
||||
new_password = data.get('new_password')
|
||||
@ -689,9 +691,9 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
if new_password is not None:
|
||||
error_messages += check_password(new_password)
|
||||
if error_messages == '':
|
||||
hashed_password = bcrypt.generate_password_hash(
|
||||
new_password, current_app.config.get('BCRYPT_LOG_ROUNDS')
|
||||
).decode()
|
||||
hashed_password = auth_user.generate_password_hash(
|
||||
new_password
|
||||
)
|
||||
auth_user.password = hashed_password
|
||||
|
||||
if error_messages != '':
|
||||
@ -751,10 +753,12 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/profile/edit/preferences', methods=['POST'])
|
||||
@authenticate
|
||||
@require_auth(scopes=['profile:write'])
|
||||
def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
edit authenticated user preferences
|
||||
Edit authenticated user preferences.
|
||||
|
||||
**Scope**: ``profile:write``
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -866,7 +870,6 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
- signature expired, please log in again
|
||||
- invalid token, please log in again
|
||||
:statuscode 500: error, please try again or contact the administrator
|
||||
|
||||
"""
|
||||
# get post data
|
||||
post_data = request.get_json()
|
||||
@ -906,12 +909,14 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/profile/edit/sports', methods=['POST'])
|
||||
@authenticate
|
||||
@require_auth(scopes=['profile:write'])
|
||||
def edit_user_sport_preferences(
|
||||
auth_user: User,
|
||||
) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
edit authenticated user sport preferences
|
||||
Edit authenticated user sport preferences.
|
||||
|
||||
**Scope**: ``profile:write``
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -956,7 +961,6 @@ def edit_user_sport_preferences(
|
||||
:statuscode 404:
|
||||
- sport does not exist
|
||||
:statuscode 500: error, please try again or contact the administrator
|
||||
|
||||
"""
|
||||
post_data = request.get_json()
|
||||
if (
|
||||
@ -1012,12 +1016,14 @@ def edit_user_sport_preferences(
|
||||
@auth_blueprint.route(
|
||||
'/auth/profile/reset/sports/<sport_id>', methods=['DELETE']
|
||||
)
|
||||
@authenticate
|
||||
@require_auth(scopes=['profile:write'])
|
||||
def reset_user_sport_preferences(
|
||||
auth_user: User, sport_id: int
|
||||
) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||
"""
|
||||
reset authenticated user preferences for a given sport
|
||||
Reset authenticated user preferences for a given sport.
|
||||
|
||||
**Scope**: ``profile:write``
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -1045,7 +1051,6 @@ def reset_user_sport_preferences(
|
||||
:statuscode 404:
|
||||
- sport does not exist
|
||||
:statuscode 500: error, please try again or contact the administrator
|
||||
|
||||
"""
|
||||
sport = Sport.query.filter_by(id=sport_id).first()
|
||||
if not sport:
|
||||
@ -1067,10 +1072,12 @@ def reset_user_sport_preferences(
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/picture', methods=['POST'])
|
||||
@authenticate
|
||||
@require_auth(scopes=['profile:write'])
|
||||
def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
update authenticated user picture
|
||||
Update authenticated user picture.
|
||||
|
||||
**Scope**: ``profile:write``
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -1107,7 +1114,6 @@ def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
- invalid token, please log in again
|
||||
:statuscode 413: error during picture update: file size exceeds 1.0MB
|
||||
:statuscode 500: error during picture update
|
||||
|
||||
"""
|
||||
try:
|
||||
response_object = get_error_response_if_file_is_invalid(
|
||||
@ -1155,10 +1161,12 @@ def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/picture', methods=['DELETE'])
|
||||
@authenticate
|
||||
@require_auth(scopes=['profile:write'])
|
||||
def del_picture(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||
"""
|
||||
delete authenticated user picture
|
||||
Delete authenticated user picture.
|
||||
|
||||
**Scope**: ``profile:write``
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -1200,7 +1208,7 @@ def del_picture(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||
@auth_blueprint.route('/auth/password/reset-request', methods=['POST'])
|
||||
def request_password_reset() -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
handle password reset request
|
||||
Handle password reset request.
|
||||
|
||||
If email sending is disabled, this endpoint is not available
|
||||
|
||||
@ -1270,9 +1278,9 @@ def request_password_reset() -> Union[Dict, HttpResponse]:
|
||||
@auth_blueprint.route('/auth/password/update', methods=['POST'])
|
||||
def update_password() -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
update user password after password reset request
|
||||
Update user password after password reset request.
|
||||
|
||||
It sends emails if sending is enabled
|
||||
It sends emails if sending is enabled.
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -1325,9 +1333,7 @@ def update_password() -> Union[Dict, HttpResponse]:
|
||||
if not user:
|
||||
return UnauthorizedErrorResponse()
|
||||
try:
|
||||
user.password = bcrypt.generate_password_hash(
|
||||
password, current_app.config.get('BCRYPT_LOG_ROUNDS')
|
||||
).decode()
|
||||
user.password = user.generate_password_hash(password)
|
||||
db.session.commit()
|
||||
|
||||
if current_app.config['CAN_SEND_EMAILS']:
|
||||
@ -1355,7 +1361,7 @@ def update_password() -> Union[Dict, HttpResponse]:
|
||||
@auth_blueprint.route('/auth/email/update', methods=['POST'])
|
||||
def update_email() -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
update user email after confirmation
|
||||
Update user email after confirmation.
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -1414,7 +1420,7 @@ def update_email() -> Union[Dict, HttpResponse]:
|
||||
@auth_blueprint.route('/auth/account/confirm', methods=['POST'])
|
||||
def confirm_account() -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
activate user account after registration
|
||||
Activate user account after registration.
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -1476,9 +1482,9 @@ def confirm_account() -> Union[Dict, HttpResponse]:
|
||||
@auth_blueprint.route('/auth/account/resend-confirmation', methods=['POST'])
|
||||
def resend_account_confirmation_email() -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
resend email with instructions to confirm account
|
||||
Resend email with instructions to confirm account.
|
||||
|
||||
If email sending is disabled, this endpoint is not available
|
||||
If email sending is disabled, this endpoint is not available.
|
||||
|
||||
**Example request**:
|
||||
|
||||
|
@ -1,39 +0,0 @@
|
||||
from functools import wraps
|
||||
from typing import Any, Callable, Union
|
||||
|
||||
from flask import request
|
||||
|
||||
from fittrackee.responses import HttpResponse
|
||||
|
||||
from .utils.controls import verify_user
|
||||
|
||||
|
||||
def verify_auth_user(
|
||||
f: Callable, verify_admin: bool, *args: Any, **kwargs: Any
|
||||
) -> Union[Callable, HttpResponse]:
|
||||
response_object, user = verify_user(request, verify_admin=verify_admin)
|
||||
if response_object:
|
||||
return response_object
|
||||
return f(user, *args, **kwargs)
|
||||
|
||||
|
||||
def authenticate(f: Callable) -> Callable:
|
||||
@wraps(f)
|
||||
def decorated_function(
|
||||
*args: Any, **kwargs: Any
|
||||
) -> Union[Callable, HttpResponse]:
|
||||
verify_admin = False
|
||||
return verify_auth_user(f, verify_admin, *args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
def authenticate_as_admin(f: Callable) -> Callable:
|
||||
@wraps(f)
|
||||
def decorated_function(
|
||||
*args: Any, **kwargs: Any
|
||||
) -> Union[Callable, HttpResponse]:
|
||||
verify_admin = True
|
||||
return verify_auth_user(f, verify_admin, *args, **kwargs)
|
||||
|
||||
return decorated_function
|
@ -103,6 +103,18 @@ class User(BaseModel):
|
||||
except jwt.InvalidTokenError:
|
||||
return 'invalid token, please log in again'
|
||||
|
||||
def check_password(self, password: str) -> bool:
|
||||
return bcrypt.check_password_hash(self.password, password)
|
||||
|
||||
@staticmethod
|
||||
def generate_password_hash(new_password: str) -> str:
|
||||
return bcrypt.generate_password_hash(
|
||||
new_password, current_app.config.get('BCRYPT_LOG_ROUNDS')
|
||||
).decode()
|
||||
|
||||
def get_user_id(self) -> int:
|
||||
return self.id
|
||||
|
||||
@hybrid_property
|
||||
def workouts_count(self) -> int:
|
||||
return Workout.query.filter(Workout.user_id == self.id).count()
|
||||
|
@ -12,6 +12,7 @@ from fittrackee.emails.tasks import (
|
||||
reset_password_email,
|
||||
)
|
||||
from fittrackee.files import get_absolute_file_path
|
||||
from fittrackee.oauth2.server import require_auth
|
||||
from fittrackee.responses import (
|
||||
ForbiddenErrorResponse,
|
||||
HttpResponse,
|
||||
@ -24,7 +25,6 @@ from fittrackee.utils import get_readable_duration
|
||||
from fittrackee.workouts.models import Record, Workout, WorkoutSegment
|
||||
|
||||
from .auth import get_language
|
||||
from .decorators import authenticate, authenticate_as_admin
|
||||
from .exceptions import InvalidEmailException, UserNotFoundException
|
||||
from .models import User, UserSportPreference
|
||||
from .utils.admin import UserManagerService
|
||||
@ -35,14 +35,16 @@ USER_PER_PAGE = 10
|
||||
|
||||
|
||||
@users_blueprint.route('/users', methods=['GET'])
|
||||
@authenticate_as_admin
|
||||
@require_auth(scopes=['users:read'], as_admin=True)
|
||||
def get_users(auth_user: User) -> Dict:
|
||||
"""
|
||||
Get all users (regardless their account status), if authenticated user
|
||||
has admin rights
|
||||
has admin rights.
|
||||
|
||||
It returns user preferences only for authenticated user.
|
||||
|
||||
**Scope**: ``users:read``
|
||||
|
||||
**Example request**:
|
||||
|
||||
- without parameters
|
||||
@ -245,7 +247,7 @@ def get_users(auth_user: User) -> Dict:
|
||||
|
||||
|
||||
@users_blueprint.route('/users/<user_name>', methods=['GET'])
|
||||
@authenticate
|
||||
@require_auth(scopes=['users:read'])
|
||||
def get_single_user(
|
||||
auth_user: User, user_name: str
|
||||
) -> Union[Dict, HttpResponse]:
|
||||
@ -255,6 +257,8 @@ def get_single_user(
|
||||
|
||||
It returns user preferences only for authenticated user.
|
||||
|
||||
**Scope**: ``users:read``
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@ -413,10 +417,10 @@ def get_picture(user_name: str) -> Any:
|
||||
|
||||
|
||||
@users_blueprint.route('/users/<user_name>', methods=['PATCH'])
|
||||
@authenticate_as_admin
|
||||
@require_auth(scopes=['users:write'], as_admin=True)
|
||||
def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
Update user account
|
||||
Update user account.
|
||||
|
||||
- add/remove admin rights (regardless user account status)
|
||||
- reset password (and send email to update user password,
|
||||
@ -424,7 +428,9 @@ def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
|
||||
- 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
|
||||
Only user with admin rights can modify another user.
|
||||
|
||||
**Scope**: ``users:write``
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -621,17 +627,19 @@ def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
|
||||
|
||||
|
||||
@users_blueprint.route('/users/<user_name>', methods=['DELETE'])
|
||||
@authenticate
|
||||
@require_auth(scopes=['users:write'])
|
||||
def delete_user(
|
||||
auth_user: User, user_name: str
|
||||
) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||
"""
|
||||
Delete a user account
|
||||
Delete a user account.
|
||||
|
||||
A user can only delete his own account
|
||||
A user can only delete his own account.
|
||||
|
||||
An admin can delete all accounts except his account if he's the only
|
||||
one admin
|
||||
one admin.
|
||||
|
||||
**Scope**: ``users:write``
|
||||
|
||||
**Example request**:
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
import secrets
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from fittrackee import bcrypt, db
|
||||
from fittrackee import db
|
||||
|
||||
from ..exceptions import InvalidEmailException, UserNotFoundException
|
||||
from ..models import User
|
||||
@ -33,9 +31,7 @@ class UserManagerService:
|
||||
@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()
|
||||
user.password = user.generate_password_hash(new_password)
|
||||
return new_password
|
||||
|
||||
@staticmethod
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user