Merge pull request #214 from SamR1/oauth2
OAuth2 Authorization Code support
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
@@ -7,6 +7,7 @@ API documentation
|
||||
|
||||
auth
|
||||
configuration
|
||||
oauth2
|
||||
records
|
||||
sports
|
||||
stats
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -15,6 +15,7 @@ A command line interface (CLI) is available to manage database and users.
|
||||
|
||||
Commands:
|
||||
db Manage database.
|
||||
oauth2 Manage OAuth2 tokens.
|
||||
users Manage users.
|
||||
|
||||
.. warning::
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+26
-17
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
+9
-5
@@ -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>
|
||||
|
||||
+11
-6
@@ -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>
|
||||
|
||||
+23
-11
@@ -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
@@ -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>
|
||||
@@ -151,6 +156,7 @@ Options:
|
||||
|
||||
Commands:
|
||||
db Manage database.
|
||||
oauth2 Manage OAuth2 tokens.
|
||||
users Manage users.
|
||||
</pre></div>
|
||||
</div>
|
||||
@@ -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">
|
||||
|
||||
+6
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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>
|
||||
|
||||
+1
-1
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>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
@@ -7,6 +7,7 @@ API documentation
|
||||
|
||||
auth
|
||||
configuration
|
||||
oauth2
|
||||
records
|
||||
sports
|
||||
stats
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -15,6 +15,7 @@ A command line interface (CLI) is available to manage database and users.
|
||||
|
||||
Commands:
|
||||
db Manage database.
|
||||
oauth2 Manage OAuth2 tokens.
|
||||
users Manage users.
|
||||
|
||||
.. warning::
|
||||
@@ -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):
|
||||
|
||||
Vendored
+1
-1
@@ -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>
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+2
-2
@@ -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
|
||||
Vendored
+1
-1
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -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,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
|
||||
@@ -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
|
||||
@@ -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}.')
|
||||
@@ -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())
|
||||
@@ -0,0 +1,2 @@
|
||||
class InvalidOAuth2Scopes(Exception):
|
||||
...
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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')
|
||||
@@ -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
|
||||
|
||||
+175
-4
@@ -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,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
|
||||
@@ -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
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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(
|
||||
|
||||
+49
-43
@@ -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()
|
||||
|
||||
+19
-11
@@ -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
Reference in New Issue
Block a user