geovisio 2.5.0__py3-none-any.whl → 2.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. geovisio/__init__.py +38 -8
  2. geovisio/admin_cli/__init__.py +2 -2
  3. geovisio/admin_cli/db.py +8 -0
  4. geovisio/config_app.py +64 -0
  5. geovisio/db_migrations.py +24 -3
  6. geovisio/templates/main.html +14 -14
  7. geovisio/templates/viewer.html +3 -3
  8. geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
  9. geovisio/translations/de/LC_MESSAGES/messages.po +667 -0
  10. geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
  11. geovisio/translations/en/LC_MESSAGES/messages.po +730 -0
  12. geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
  13. geovisio/translations/es/LC_MESSAGES/messages.po +778 -0
  14. geovisio/translations/fi/LC_MESSAGES/messages.mo +0 -0
  15. geovisio/translations/fi/LC_MESSAGES/messages.po +589 -0
  16. geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
  17. geovisio/translations/fr/LC_MESSAGES/messages.po +814 -0
  18. geovisio/translations/ko/LC_MESSAGES/messages.mo +0 -0
  19. geovisio/translations/ko/LC_MESSAGES/messages.po +685 -0
  20. geovisio/translations/messages.pot +686 -0
  21. geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
  22. geovisio/translations/nl/LC_MESSAGES/messages.po +594 -0
  23. geovisio/utils/__init__.py +1 -1
  24. geovisio/utils/auth.py +50 -11
  25. geovisio/utils/db.py +65 -0
  26. geovisio/utils/excluded_areas.py +83 -0
  27. geovisio/utils/extent.py +30 -0
  28. geovisio/utils/fields.py +1 -1
  29. geovisio/utils/filesystems.py +0 -1
  30. geovisio/utils/link.py +14 -0
  31. geovisio/utils/params.py +20 -0
  32. geovisio/utils/pictures.py +94 -69
  33. geovisio/utils/reports.py +171 -0
  34. geovisio/utils/sequences.py +288 -126
  35. geovisio/utils/tokens.py +37 -42
  36. geovisio/utils/upload_set.py +654 -0
  37. geovisio/web/auth.py +50 -37
  38. geovisio/web/collections.py +305 -319
  39. geovisio/web/configuration.py +14 -0
  40. geovisio/web/docs.py +288 -12
  41. geovisio/web/excluded_areas.py +377 -0
  42. geovisio/web/items.py +203 -151
  43. geovisio/web/map.py +322 -106
  44. geovisio/web/params.py +69 -26
  45. geovisio/web/pictures.py +14 -31
  46. geovisio/web/reports.py +399 -0
  47. geovisio/web/rss.py +13 -7
  48. geovisio/web/stac.py +129 -121
  49. geovisio/web/tokens.py +105 -112
  50. geovisio/web/upload_set.py +768 -0
  51. geovisio/web/users.py +100 -73
  52. geovisio/web/utils.py +38 -9
  53. geovisio/workers/runner_pictures.py +278 -183
  54. geovisio-2.7.0.dist-info/METADATA +95 -0
  55. geovisio-2.7.0.dist-info/RECORD +66 -0
  56. geovisio-2.5.0.dist-info/METADATA +0 -115
  57. geovisio-2.5.0.dist-info/RECORD +0 -41
  58. {geovisio-2.5.0.dist-info → geovisio-2.7.0.dist-info}/LICENSE +0 -0
  59. {geovisio-2.5.0.dist-info → geovisio-2.7.0.dist-info}/WHEEL +0 -0
geovisio/web/stac.py CHANGED
@@ -1,8 +1,8 @@
1
- import psycopg
2
1
  from psycopg.sql import SQL
3
2
  from flask import Blueprint, current_app, request, url_for
3
+ from flask_babel import gettext as _
4
4
  from geovisio import errors
5
- from geovisio.utils import auth
5
+ from geovisio.utils import auth, db
6
6
  from psycopg.rows import dict_row
7
7
  from geovisio.utils.fields import SortBy, SQLDirection, Bounds, SortByField
8
8
  from geovisio.web.utils import (
@@ -14,6 +14,7 @@ from geovisio.web.utils import (
14
14
  get_root_link,
15
15
  removeNoneInDict,
16
16
  user_dependant_response,
17
+ get_api_version,
17
18
  )
18
19
  from geovisio.utils.sequences import (
19
20
  get_collections,
@@ -57,127 +58,134 @@ def getLanding():
57
58
  $ref: '#/components/schemas/GeoVisioLanding'
58
59
  """
59
60
 
60
- with psycopg.connect(current_app.config["DB_URL"]) as conn:
61
- with conn.cursor() as cursor:
62
- spatial_xmin, spatial_ymin, spatial_xmax, spatial_ymax, temporal_min, temporal_max = cursor.execute(
63
- """
64
- SELECT
65
- GREATEST(-180, ST_XMin(ST_EstimatedExtent('pictures', 'geom'))),
66
- GREATEST(-90, ST_YMin(ST_EstimatedExtent('pictures', 'geom'))),
67
- LEAST(180, ST_XMax(ST_EstimatedExtent('pictures', 'geom'))),
68
- LEAST(90, ST_YMax(ST_EstimatedExtent('pictures', 'geom'))),
69
- MIN(ts), MAX(ts)
70
- FROM pictures
71
- """
72
- ).fetchone()
73
-
74
- extent = (
75
- cleanNoneInDict(
76
- {
77
- "spatial": (
78
- {"bbox": [[spatial_xmin, spatial_ymin, spatial_xmax, spatial_ymax]]} if spatial_xmin is not None else None
79
- ),
80
- "temporal": (
81
- {"interval": [[dbTsToStac(temporal_min), dbTsToStac(temporal_max)]]} if temporal_min is not None else None
82
- ),
83
- }
84
- )
85
- if spatial_xmin is not None or temporal_min is not None
86
- else None
87
- )
88
-
89
- sequences = [
90
- {"rel": "child", "title": f'User "{s[1]}" sequences', "href": url_for("stac.getUserCatalog", userId=s[0], _external=True)}
91
- for s in cursor.execute(
92
- """
93
- SELECT DISTINCT s.account_id, a.name
94
- FROM sequences s
95
- JOIN accounts a ON s.account_id = a.id
96
- """
97
- ).fetchall()
98
- ]
61
+ with db.cursor(current_app) as cursor:
62
+ spatial_xmin, spatial_ymin, spatial_xmax, spatial_ymax, temporal_min, temporal_max = cursor.execute(
63
+ """SELECT
64
+ GREATEST(-180, ST_XMin(ST_EstimatedExtent('pictures', 'geom'))),
65
+ GREATEST(-90, ST_YMin(ST_EstimatedExtent('pictures', 'geom'))),
66
+ LEAST(180, ST_XMax(ST_EstimatedExtent('pictures', 'geom'))),
67
+ LEAST(90, ST_YMax(ST_EstimatedExtent('pictures', 'geom'))),
68
+ MIN(ts), MAX(ts)
69
+ FROM pictures
70
+ """
71
+ ).fetchone()
99
72
 
100
- catalog = dbSequencesToStacCatalog(
101
- "geovisio",
102
- "GeoVisio STAC API",
103
- "This catalog list all geolocated pictures available in this GeoVisio instance",
104
- sequences,
105
- request,
106
- extent,
73
+ extent = (
74
+ cleanNoneInDict(
75
+ {
76
+ "spatial": ({"bbox": [[spatial_xmin, spatial_ymin, spatial_xmax, spatial_ymax]]} if spatial_xmin is not None else None),
77
+ "temporal": (
78
+ {"interval": [[dbTsToStac(temporal_min), dbTsToStac(temporal_max)]]} if temporal_min is not None else None
79
+ ),
80
+ }
107
81
  )
82
+ if spatial_xmin is not None or temporal_min is not None
83
+ else None
84
+ )
108
85
 
109
- mapUrl = (
110
- url_for("map.getTile", x="111", y="222", z="333", format="mvt", _external=True)
111
- .replace("111", "{x}")
112
- .replace("222", "{y}")
113
- .replace("333", "{z}")
114
- )
115
- userMapUrl = (
116
- url_for("map.getUserTile", userId="bob", x="111", y="222", z="333", format="mvt", _external=True)
117
- .replace("111", "{x}")
118
- .replace("222", "{y}")
119
- .replace("333", "{z}")
120
- .replace("bob", "{userId}")
121
- )
86
+ catalog = dbSequencesToStacCatalog(
87
+ id="geovisio",
88
+ title=current_app.config["API_SUMMARY"].name.get("en"),
89
+ description=current_app.config["API_SUMMARY"].description.get("en"),
90
+ sequences=[],
91
+ request=request,
92
+ extent=extent,
93
+ )
122
94
 
123
- if "stac_extensions" not in catalog:
124
- catalog["stac_extensions"] = []
125
-
126
- catalog["stac_extensions"] += ["https://stac-extensions.github.io/web-map-links/v1.0.0/schema.json"]
127
-
128
- catalog["links"] += cleanNoneInList(
129
- [
130
- {"rel": "service-desc", "type": "application/json", "href": url_for("flasgger.swagger", _external=True)},
131
- {"rel": "service-doc", "type": "text/html", "href": url_for("flasgger.apidocs", _external=True)},
132
- {"rel": "conformance", "type": "application/json", "href": url_for("stac.getConformance", _external=True)},
133
- {"rel": "data", "type": "application/json", "href": url_for("stac_collections.getAllCollections", _external=True)},
134
- {
135
- "rel": "data",
136
- "type": "application/rss+xml",
137
- "href": url_for("stac_collections.getAllCollections", _external=True, format="rss"),
138
- },
139
- {"rel": "search", "type": "application/geo+json", "href": url_for("stac_items.searchItems", _external=True)},
140
- {
141
- "rel": "xyz",
142
- "type": "application/vnd.mapbox-vector-tile",
143
- "href": mapUrl,
144
- "title": "Pictures and sequences vector tiles",
145
- },
146
- {
147
- "rel": "user-xyz",
148
- "type": "application/vnd.mapbox-vector-tile",
149
- "href": userMapUrl,
150
- "title": "Pictures and sequences vector tiles for a given user",
151
- },
152
- {
153
- "rel": "collection-preview",
154
- "type": "image/jpeg",
155
- "href": url_for("stac_collections.getCollectionThumbnail", collectionId="{id}", _external=True),
156
- "title": "Thumbnail URL for a given sequence",
157
- },
158
- {
159
- "rel": "item-preview",
160
- "type": "image/jpeg",
161
- "href": url_for("pictures.getPictureThumb", pictureId="{id}", format="jpg", _external=True),
162
- "title": "Thumbnail URL for a given picture",
163
- },
164
- {
165
- "rel": "users",
166
- "type": "application/json",
167
- "href": url_for("user.listUsers", _external=True),
168
- "title": "List of users",
169
- },
170
- {
171
- "rel": "user-search",
172
- "type": "application/json",
173
- "href": url_for("user.searchUser", _external=True),
174
- "title": "Search users",
175
- },
176
- get_license_link(),
177
- ]
178
- )
95
+ catalog["geovisio_version"] = get_api_version()
96
+
97
+ mapUrl = (
98
+ url_for("map.getTile", x="11111", y="22222", z="33333", format="mvt", _external=True)
99
+ .replace("11111", "{x}")
100
+ .replace("22222", "{y}")
101
+ .replace("33333", "{z}")
102
+ )
103
+ userMapUrl = (
104
+ url_for("map.getUserTile", userId="bob", x="11111", y="22222", z="33333", format="mvt", _external=True)
105
+ .replace("11111", "{x}")
106
+ .replace("22222", "{y}")
107
+ .replace("33333", "{z}")
108
+ .replace("bob", "{userId}")
109
+ )
110
+ userStyleUrl = url_for("map.getUserStyle", userId="bob", _external=True).replace("bob", "{userId}")
111
+
112
+ if "stac_extensions" not in catalog:
113
+ catalog["stac_extensions"] = []
114
+
115
+ catalog["stac_extensions"] += ["https://stac-extensions.github.io/web-map-links/v1.0.0/schema.json"]
116
+
117
+ catalog["links"] += cleanNoneInList(
118
+ [
119
+ {"rel": "service-desc", "type": "application/json", "href": url_for("flasgger.swagger", _external=True)},
120
+ {"rel": "service-doc", "type": "text/html", "href": url_for("flasgger.apidocs", _external=True)},
121
+ {"rel": "conformance", "type": "application/json", "href": url_for("stac.getConformance", _external=True)},
122
+ {"rel": "data", "type": "application/json", "href": url_for("stac_collections.getAllCollections", _external=True)},
123
+ {"rel": "child", "type": "application/json", "href": url_for("user.listUsers", _external=True)},
124
+ {
125
+ "rel": "data",
126
+ "type": "application/rss+xml",
127
+ "href": url_for("stac_collections.getAllCollections", _external=True, format="rss"),
128
+ },
129
+ {"rel": "search", "type": "application/geo+json", "href": url_for("stac_items.searchItems", _external=True)},
130
+ {
131
+ "rel": "xyz",
132
+ "type": "application/vnd.mapbox-vector-tile",
133
+ "href": mapUrl,
134
+ "title": "Pictures and sequences vector tiles",
135
+ },
136
+ {
137
+ "rel": "xyz-style",
138
+ "type": "application/json",
139
+ "href": url_for("map.getStyle", _external=True),
140
+ "title": "MapLibre Style JSON",
141
+ },
142
+ {
143
+ "rel": "user-xyz",
144
+ "type": "application/vnd.mapbox-vector-tile",
145
+ "href": userMapUrl,
146
+ "title": "Pictures and sequences vector tiles for a given user",
147
+ },
148
+ {
149
+ "rel": "user-xyz-style",
150
+ "type": "application/json",
151
+ "href": userStyleUrl,
152
+ "title": "MapLibre Style JSON",
153
+ },
154
+ {
155
+ "rel": "collection-preview",
156
+ "type": "image/jpeg",
157
+ "href": url_for("stac_collections.getCollectionThumbnail", collectionId="{id}", _external=True),
158
+ "title": "Thumbnail URL for a given sequence",
159
+ },
160
+ {
161
+ "rel": "item-preview",
162
+ "type": "image/jpeg",
163
+ "href": url_for("pictures.getPictureThumb", pictureId="{id}", format="jpg", _external=True),
164
+ "title": "Thumbnail URL for a given picture",
165
+ },
166
+ {
167
+ "rel": "users",
168
+ "type": "application/json",
169
+ "href": url_for("user.listUsers", _external=True),
170
+ "title": "List of users",
171
+ },
172
+ {
173
+ "rel": "user-search",
174
+ "type": "application/json",
175
+ "href": url_for("user.searchUser", _external=True),
176
+ "title": "Search users",
177
+ },
178
+ {
179
+ "rel": "report",
180
+ "type": "application/json",
181
+ "href": url_for("reports.postReport", _external=True),
182
+ "title": "Post feedback/report about picture or sequence",
183
+ },
184
+ get_license_link(),
185
+ ]
186
+ )
179
187
 
180
- return catalog, 200, {"Content-Type": "application/json"}
188
+ return catalog, 200, {"Content-Type": "application/json"}
181
189
 
182
190
 
183
191
  @bp.route("/conformance")
@@ -322,11 +330,11 @@ def getUserCatalog(userId, userIdMatchesAccount=False):
322
330
 
323
331
  userName = None
324
332
  meta_collection = None
325
- with psycopg.connect(current_app.config["DB_URL"], row_factory=dict_row) as conn, conn.cursor() as cursor:
333
+ with db.cursor(current_app, row_factory=dict_row) as cursor:
326
334
  userName = cursor.execute("SELECT name FROM accounts WHERE id = %s", [userId]).fetchone()
327
335
 
328
336
  if not userName:
329
- raise errors.InvalidAPIUsage(f"Impossible to find user {userId}")
337
+ raise errors.InvalidAPIUsage(_("Impossible to find user %(u)s", u=userId))
330
338
  userName = userName["name"]
331
339
 
332
340
  meta_collection = cursor.execute(
@@ -336,7 +344,7 @@ def getUserCatalog(userId, userIdMatchesAccount=False):
336
344
 
337
345
  if not meta_collection or meta_collection["min_order"] is None:
338
346
  # No data found, trying to give the most meaningfull error message
339
- raise errors.InvalidAPIUsage(f"No data loaded for user {userId}", 404)
347
+ raise errors.InvalidAPIUsage(_("No data loaded for user %(u)s", u=userId), 404)
340
348
 
341
349
  db_collections = get_collections(collection_request)
342
350
 
geovisio/web/tokens.py CHANGED
@@ -1,13 +1,13 @@
1
1
  import flask
2
2
  from flask import current_app, url_for, request
3
+ from flask_babel import gettext as _
3
4
  from dateutil import tz
4
- import psycopg
5
5
  from psycopg.rows import dict_row
6
6
  from authlib.jose import jwt
7
7
  from authlib.jose.errors import DecodeError
8
8
  import logging
9
9
  import uuid
10
- from geovisio.utils import auth
10
+ from geovisio.utils import auth, db
11
11
  from geovisio import errors, utils
12
12
 
13
13
 
@@ -19,44 +19,46 @@ bp = flask.Blueprint("tokens", __name__, url_prefix="/api")
19
19
  def list_tokens(account):
20
20
  """
21
21
  List the tokens of a authenticated user
22
+
23
+ The list of tokens will not contain their JWT counterpart (the JWT is the real token used in authentication).
24
+
25
+ The JWT counterpart can be retreived by providing the token's id to the endpoint [/users/me/tokens/{token_id}](#/Auth/get_api_users_me_tokens__token_id_).
22
26
  ---
23
27
  tags:
24
28
  - Auth
25
29
  - Users
26
30
  responses:
27
31
  200:
28
- description: the token list
32
+ description: The list of tokens, without their JWT counterpart.
29
33
  content:
30
34
  application/json:
31
35
  schema:
32
36
  $ref: '#/components/schemas/GeoVisioTokens'
33
37
  """
34
38
 
35
- with psycopg.connect(current_app.config["DB_URL"], row_factory=dict_row) as conn:
36
- with conn.cursor() as cursor:
37
- records = cursor.execute(
38
- """
39
- SELECT id, description, generated_at FROM tokens WHERE account_id = %(account)s
40
- """,
41
- {"account": account.id},
42
- ).fetchall()
43
-
44
- tokens = [
39
+ records = db.fetchall(
40
+ current_app,
41
+ "SELECT id, description, generated_at FROM tokens WHERE account_id = %(account)s",
42
+ {"account": account.id},
43
+ row_factory=dict_row,
44
+ )
45
+
46
+ tokens = [
47
+ {
48
+ "id": r["id"],
49
+ "description": r["description"],
50
+ "generated_at": r["generated_at"].astimezone(tz.gettz("UTC")).isoformat(),
51
+ "links": [
45
52
  {
46
- "id": r["id"],
47
- "description": r["description"],
48
- "generated_at": r["generated_at"].astimezone(tz.gettz("UTC")).isoformat(),
49
- "links": [
50
- {
51
- "rel": "self",
52
- "type": "application/json",
53
- "href": url_for("tokens.get_jwt_token", token_id=r["id"], _external=True),
54
- }
55
- ],
53
+ "rel": "self",
54
+ "type": "application/json",
55
+ "href": url_for("tokens.get_jwt_token", token_id=r["id"], _external=True),
56
56
  }
57
- for r in records
58
- ]
59
- return flask.jsonify(tokens)
57
+ ],
58
+ }
59
+ for r in records
60
+ ]
61
+ return flask.jsonify(tokens)
60
62
 
61
63
 
62
64
  @bp.route("/users/me/tokens/<uuid:token_id>", methods=["GET"])
@@ -65,7 +67,7 @@ def get_jwt_token(token_id: uuid.UUID, account: auth.Account):
65
67
  """
66
68
  Get the JWT token corresponding to a token id.
67
69
 
68
- This token will be needed to authenticate others api calls
70
+ This JWT token will be needed to authenticate others api calls
69
71
  ---
70
72
  tags:
71
73
  - Auth
@@ -83,31 +85,29 @@ def get_jwt_token(token_id: uuid.UUID, account: auth.Account):
83
85
  content:
84
86
  application/json:
85
87
  schema:
86
- $ref: '#/components/schemas/JWToken'
88
+ $ref: '#/components/schemas/GeoVisioEncodedToken'
87
89
  """
88
90
 
89
- with psycopg.connect(current_app.config["DB_URL"], row_factory=dict_row) as conn:
90
- with conn.cursor() as cursor:
91
- # check token existence
92
- token = cursor.execute(
93
- """
94
- SELECT id, description, generated_at FROM tokens WHERE account_id = %(account)s AND id = %(token)s
95
- """,
96
- {"account": account.id, "token": token_id},
97
- ).fetchone()
98
-
99
- if not token:
100
- raise errors.InvalidAPIUsage("Impossible to find token", status_code=404)
101
-
102
- jwt_token = _generate_jwt_token(token["id"])
103
- return flask.jsonify(
104
- {
105
- "jwt_token": jwt_token,
106
- "id": token["id"],
107
- "description": token["description"],
108
- "generated_at": token["generated_at"].astimezone(tz.gettz("UTC")).isoformat(),
109
- }
110
- )
91
+ token = db.fetchone(
92
+ current_app,
93
+ "SELECT id, description, generated_at FROM tokens WHERE account_id = %(account)s AND id = %(token)s",
94
+ {"account": account.id, "token": token_id},
95
+ row_factory=dict_row,
96
+ )
97
+
98
+ # check token existence
99
+ if not token:
100
+ raise errors.InvalidAPIUsage(_("Impossible to find token"), status_code=404)
101
+
102
+ jwt_token = _generate_jwt_token(token["id"])
103
+ return flask.jsonify(
104
+ {
105
+ "jwt_token": jwt_token,
106
+ "id": token["id"],
107
+ "description": token["description"],
108
+ "generated_at": token["generated_at"].astimezone(tz.gettz("UTC")).isoformat(),
109
+ }
110
+ )
111
111
 
112
112
 
113
113
  @bp.route("/users/me/tokens/<uuid:token_id>", methods=["DELETE"])
@@ -132,22 +132,16 @@ def revoke_token(token_id: uuid.UUID, account: auth.Account):
132
132
  description: The token has been correctly deleted
133
133
  """
134
134
 
135
- with psycopg.connect(current_app.config["DB_URL"]) as conn:
136
- with conn.cursor() as cursor:
137
- # check token existence
138
- res = cursor.execute(
139
- """
140
- DELETE FROM tokens WHERE account_id = %(account)s AND id = %(token)s
141
- """,
142
- {"account": account.id, "token": token_id},
143
- )
144
-
145
- token_deleted = res.rowcount
135
+ with db.execute(
136
+ current_app,
137
+ "DELETE FROM tokens WHERE account_id = %(account)s AND id = %(token)s",
138
+ {"account": account.id, "token": token_id},
139
+ ) as res:
140
+ token_deleted = res.rowcount
146
141
 
147
- if not token_deleted:
148
- raise errors.InvalidAPIUsage("Impossible to find token", status_code=404)
149
- conn.commit()
150
- return flask.jsonify({"message": "token revoked"}), 200
142
+ if not token_deleted:
143
+ raise errors.InvalidAPIUsage(_("Impossible to find token"), status_code=404)
144
+ return flask.jsonify({"message": "token revoked"}), 200
151
145
 
152
146
 
153
147
  @bp.route("/auth/tokens/generate", methods=["POST"])
@@ -176,30 +170,31 @@ def generate_non_associated_token():
176
170
  $ref: '#/components/schemas/JWTokenClaimable'
177
171
  """
178
172
  description = request.args.get("description", "")
179
- with psycopg.connect(current_app.config["DB_URL"], row_factory=dict_row) as conn:
180
- with conn.cursor() as cursor:
181
- token = cursor.execute(
182
- "INSERT INTO tokens (description) VALUES (%(description)s) RETURNING *",
183
- {"description": description},
184
- ).fetchone()
185
- if not token:
186
- raise errors.InternalError("Impossible to generate a new token")
187
-
188
- jwt_token = _generate_jwt_token(token["id"])
189
- token = {
190
- "id": token["id"],
191
- "jwt_token": jwt_token,
192
- "description": token["description"],
193
- "generated_at": token["generated_at"].astimezone(tz.gettz("UTC")).isoformat(),
194
- "links": [
195
- {
196
- "rel": "claim",
197
- "type": "application/json",
198
- "href": url_for("tokens.claim_non_associated_token", token_id=token["id"], _external=True),
199
- }
200
- ],
173
+
174
+ token = db.fetchone(
175
+ current_app,
176
+ "INSERT INTO tokens (description) VALUES (%(description)s) RETURNING *",
177
+ {"description": description},
178
+ row_factory=dict_row,
179
+ )
180
+ if not token:
181
+ raise errors.InternalError(_("Impossible to generate a new token"))
182
+
183
+ jwt_token = _generate_jwt_token(token["id"])
184
+ token = {
185
+ "id": token["id"],
186
+ "jwt_token": jwt_token,
187
+ "description": token["description"],
188
+ "generated_at": token["generated_at"].astimezone(tz.gettz("UTC")).isoformat(),
189
+ "links": [
190
+ {
191
+ "rel": "claim",
192
+ "type": "application/json",
193
+ "href": url_for("tokens.claim_non_associated_token", token_id=token["id"], _external=True),
201
194
  }
202
- return flask.jsonify(token)
195
+ ],
196
+ }
197
+ return flask.jsonify(token)
203
198
 
204
199
 
205
200
  @bp.route("/auth/tokens/<uuid:token_id>/claim", methods=["GET"])
@@ -225,30 +220,28 @@ def claim_non_associated_token(token_id, account):
225
220
  200:
226
221
  description: The token has been correctly associated to the account
227
222
  """
228
- with psycopg.connect(current_app.config["DB_URL"], row_factory=dict_row) as conn:
229
- with conn.cursor() as cursor:
230
- token = cursor.execute(
231
- """
232
- SELECT account_id FROM tokens WHERE id = %(token)s
233
- """,
234
- {"token": token_id},
235
- ).fetchone()
236
- if not token:
237
- raise errors.InvalidAPIUsage("Impossible to find token", status_code=404)
238
-
239
- associated_account = token["account_id"]
240
- if associated_account:
241
- if associated_account != account.id:
242
- raise errors.InvalidAPIUsage("Token already claimed by another account", status_code=403)
243
- else:
244
- return flask.jsonify({"message": "token already associated to account"}), 200
245
-
246
- cursor.execute(
247
- "UPDATE tokens SET account_id = %(account)s WHERE id = %(token)s",
248
- {"account": account.id, "token": token_id},
249
- )
250
- conn.commit()
251
- return "You are now logged in the CLI, you can upload your pictures", 200
223
+ with db.cursor(current_app, row_factory=dict_row) as cursor:
224
+ token = cursor.execute(
225
+ """
226
+ SELECT account_id FROM tokens WHERE id = %(token)s
227
+ """,
228
+ {"token": token_id},
229
+ ).fetchone()
230
+ if not token:
231
+ raise errors.InvalidAPIUsage(_("Impossible to find token"), status_code=404)
232
+
233
+ associated_account = token["account_id"]
234
+ if associated_account:
235
+ if associated_account != account.id:
236
+ raise errors.InvalidAPIUsage(_("Token already claimed by another account"), status_code=403)
237
+ else:
238
+ return flask.jsonify({"message": "token already associated to account"}), 200
239
+
240
+ cursor.execute(
241
+ "UPDATE tokens SET account_id = %(account)s WHERE id = %(token)s",
242
+ {"account": account.id, "token": token_id},
243
+ )
244
+ return "You are now logged in the CLI, you can upload your pictures", 200
252
245
 
253
246
 
254
247
  def _generate_jwt_token(token_id: uuid.UUID) -> str: