Skip to content

Commit 1cdddd7

Browse files
committed
add user management
1 parent cade920 commit 1cdddd7

File tree

13 files changed

+572
-176
lines changed

13 files changed

+572
-176
lines changed

core/addons/api/__init__.py

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@
44
import os
55
import pathlib
66
from datetime import datetime
7-
from typing import Optional
87

98
from aiohttp import web
9+
1010
from core.addons.api.dto.details import Details
1111
from core.addons.api.dto.location import Location
12-
from core.addons.api.dto.photo import (PhotoDetailsResponse, PhotoEncoder,
13-
PhotoResponse)
12+
from core.addons.api.dto.photo import PhotoDetailsResponse, PhotoEncoder, PhotoResponse
1413
from core.addons.api.dto.photo_response import PhotosResponse
1514
from core.base import Session
1615
from core.core import ApplicationCore
@@ -63,7 +62,7 @@ class PhotosView(RequestView):
6362

6463
async def get(self, core: ApplicationCore, request: web.Request) -> web.Response:
6564
"""Get a list of all photo resources."""
66-
_LOGGER.debug(f"GET /v1/photos")
65+
_LOGGER.debug("GET /v1/photos")
6766
await core.authentication.check_permission(request, "library:read")
6867

6968
user_id = await core.http.get_user_id(request)
@@ -79,7 +78,9 @@ async def get(self, core: ApplicationCore, request: web.Request) -> web.Response
7978
if "offset" in request.query:
8079
offset = int(request.query["offset"])
8180

82-
_LOGGER.debug(f"read {limit} photos for user_id {user_id} beginning with {offset}")
81+
_LOGGER.debug(
82+
f"read {limit} photos for user_id {user_id} beginning with {offset}"
83+
)
8384
user_photos = await core.storage.read_photos(user_id, offset, limit)
8485

8586
results = []
@@ -89,12 +90,16 @@ async def get(self, core: ApplicationCore, request: web.Request) -> web.Response
8990
PhotoResponse(
9091
id=photo.uuid,
9192
name=photo.filename,
92-
image_url=f"{core.config.external_url}/v1/file/{photo.uuid}"
93+
image_url=f"{core.config.external_url}/v1/file/{photo.uuid}",
9394
)
9495
)
9596

96-
response = PhotosResponse(offset=offset, limit=limit, size=len(results), results=results)
97-
return web.Response(text=json.dumps(response, cls=PhotoEncoder), content_type="application/json")
97+
response = PhotosResponse(
98+
offset=offset, limit=limit, size=len(results), results=results
99+
)
100+
return web.Response(
101+
text=json.dumps(response, cls=PhotoEncoder), content_type="application/json"
102+
)
98103

99104

100105
class PhotoDetailsView(RequestView):
@@ -103,7 +108,9 @@ class PhotoDetailsView(RequestView):
103108
url = "/v1/photo/{entity_id}"
104109
name = "v1:photo"
105110

106-
async def get(self, core: ApplicationCore, request: web.Request, entity_id: str) -> web.Response:
111+
async def get(
112+
self, core: ApplicationCore, request: web.Request, entity_id: str
113+
) -> web.Response:
107114
"""Return an entity."""
108115
_LOGGER.debug(f"GET /v1/photo/{entity_id}")
109116

@@ -133,31 +140,38 @@ async def get(self, core: ApplicationCore, request: web.Request, entity_id: str)
133140
if latitude is not None and longitude is not None:
134141
altitude = await core.storage.read("altitude")
135142
if altitude is not None:
136-
location = Location(latitude=latitude, longitude=longitude, altitude=altitude)
143+
location = Location(
144+
latitude=latitude, longitude=longitude, altitude=altitude
145+
)
137146
else:
138-
location = Location(latitude=latitude, longitude=longitude, altitude="0.0")
147+
location = Location(
148+
latitude=latitude, longitude=longitude, altitude="0.0"
149+
)
139150

140151
# photo tags
141152
tags = await core.storage.read("tags")
142153

143154
result = PhotoDetailsResponse(
144155
id=photo.uuid,
145156
name=photo.filename,
146-
author=photo.owner,
157+
owner=photo.owner,
147158
created_at=ctime.isoformat(),
159+
modified_at=mtime.isoformat(),
148160
details=Details(
149-
camera="Nikon Z7",
150-
lens="Nikkor 200mm F1.8",
151-
focal_length="200",
152-
iso="400",
153-
shutter_speed="1/2000",
154-
aperture="4.0",
161+
camera="Nikon Z7",
162+
lens="Nikkor 200mm F1.8",
163+
focal_length="200",
164+
iso="400",
165+
shutter_speed="1/2000",
166+
aperture="4.0",
155167
),
156168
tags=tags,
157169
location=location,
158-
image_url=f"{core.config.external_url}/v1/file/{entity_id}"
170+
image_url=f"{core.config.external_url}/v1/file/{entity_id}",
171+
)
172+
return web.Response(
173+
text=json.dumps(result, cls=PhotoEncoder), content_type="application/json"
159174
)
160-
return web.Response(text=json.dumps(result, cls=PhotoEncoder), content_type="application/json")
161175

162176

163177
class PhotoView(RequestView):
@@ -168,7 +182,9 @@ class PhotoView(RequestView):
168182
url = "/v1/file/{entity_id}"
169183
name = "v1:file"
170184

171-
async def get(self, core: ApplicationCore, request: web.Request, entity_id: str) -> web.Response:
185+
async def get(
186+
self, core: ApplicationCore, request: web.Request, entity_id: str
187+
) -> web.Response:
172188
"""Return an entity."""
173189
_LOGGER.debug(f"GET /v1/file/{entity_id}")
174190

@@ -224,7 +240,9 @@ async def post(self, core: ApplicationCore, request: web.Request) -> web.Respons
224240

225241
status_code = HTTP_CREATED if new_entity_created else HTTP_OK
226242

227-
resp = self.json_message(f"File successfully added with ID: {new_entity_id}", status_code)
243+
resp = self.json_message(
244+
f"File successfully added with ID: {new_entity_id}", status_code
245+
)
228246
resp.headers.add("Location", f"/api/photo/{new_entity_id}")
229247

230248
return resp

core/addons/api/dto/photo.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@ def default(self, o):
99
"""Encode all properties."""
1010
return o.__dict__
1111

12+
1213
class PhotoResponse:
1314
"""Photo response object."""
1415

15-
def __init__(
16-
self, id, name, image_url
17-
):
16+
def __init__(self, id, name, image_url):
1817
"""Initialize photo response object."""
1918
self.id = id
2019
self.name = name
@@ -25,13 +24,23 @@ class PhotoDetailsResponse:
2524
"""Photo response object."""
2625

2726
def __init__(
28-
self, id, name, owner, created_at, details, tags, location, image_url
27+
self,
28+
id,
29+
name,
30+
owner,
31+
created_at,
32+
modified_at,
33+
details,
34+
tags,
35+
location,
36+
image_url,
2937
):
3038
"""Initialize photo response object."""
3139
self.id = id
3240
self.name = name
3341
self.owner = owner
3442
self.created_at = created_at
43+
self.modified_at = modified_at
3544
self.details = details
3645
self.tags = tags
3746
self.location = location

core/authentication/__init__.py

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import aiohttp_jinja2
1212
from aiohttp import hdrs, web
1313

14-
from ..authorization import Authorization
1514
from ..const import CONF_TOKEN_LIFETIME
1615
from .auth_client import AuthenticationClient
1716
from .auth_database import AuthDatabase
@@ -36,11 +35,17 @@ def __init__(
3635
self.auth_database = auth_database
3736

3837
# Authorization Endpoint: obtain an authorization grant
39-
self.app.router.add_get(path="/oauth/authorize", handler=self.authorization_endpoint_get)
40-
self.app.router.add_post(path="/oauth/authorize", handler=self.authorization_endpoint_post)
38+
self.app.router.add_get(
39+
path="/oauth/authorize", handler=self.authorization_endpoint_get
40+
)
41+
self.app.router.add_post(
42+
path="/oauth/authorize", handler=self.authorization_endpoint_post
43+
)
4144

4245
# Token Endpoint: obtain an access token by authorization grant or refresh token
43-
self.app.router.add_post(path="/oauth/token", handler=self.token_endpoint_handler)
46+
self.app.router.add_post(
47+
path="/oauth/token", handler=self.token_endpoint_handler
48+
)
4449

4550
self.app.router.add_post("/revoke", self.revoke_token_handler, name="revoke")
4651
self.app.router.add_get("/protected", self.protected_handler, name="protected")
@@ -73,7 +78,9 @@ async def protected_handler(self, request: web.Request) -> web.StreamResponse:
7378
return response
7479

7580
@aiohttp_jinja2.template("authorize.jinja2")
76-
async def authorization_endpoint_get(self, request: web.Request) -> web.StreamResponse:
81+
async def authorization_endpoint_get(
82+
self, request: web.Request
83+
) -> web.StreamResponse:
7784
"""
7885
Validate the request to ensure that all required parameters are present and valid.
7986
@@ -105,7 +112,9 @@ async def authorization_endpoint_get(self, request: web.Request) -> web.StreamRe
105112

106113
# validate response_type
107114
if response_type != "code":
108-
_LOGGER.warning(f"The request is using an invalid response_type: {response_type}")
115+
_LOGGER.warning(
116+
f"The request is using an invalid response_type: {response_type}"
117+
)
109118
data = """{
110119
"error":"unsupported_response_type",
111120
"error_description":"The request is using an invalid response_type"
@@ -120,7 +129,9 @@ async def authorization_endpoint_get(self, request: web.Request) -> web.StreamRe
120129
None,
121130
)
122131
# validate if redirect_uri is in registered_auth_client
123-
if not any(uri == redirect_uri for uri in registered_auth_client.redirect_uris):
132+
if not any(
133+
uri == redirect_uri for uri in registered_auth_client.redirect_uris
134+
):
124135
_LOGGER.error(f"redirect uri not found: {redirect_uri}")
125136
data = """{
126137
"error":"unauthorized_client",
@@ -153,7 +164,9 @@ async def authorization_endpoint_get(self, request: web.Request) -> web.StreamRe
153164
# check if the requested scope is registered
154165
for requested_scope in requested_scopes:
155166
if requested_scope not in registered_scopes:
156-
_LOGGER.error(f"The requested scope '{requested_scope}' is invalid, unknown, or malformed.")
167+
_LOGGER.error(
168+
f"The requested scope '{requested_scope}' is invalid, unknown, or malformed."
169+
)
157170
data = """{
158171
"error":"invalid_scope",
159172
"error_description":"The requested scope is invalid, unknown, or malformed."
@@ -227,7 +240,9 @@ async def authorization_endpoint_get(self, request: web.Request) -> web.StreamRe
227240
}"""
228241
return web.json_response(json.loads(data))
229242

230-
async def authorization_endpoint_post(self, request: web.Request) -> web.StreamResponse:
243+
async def authorization_endpoint_post(
244+
self, request: web.Request
245+
) -> web.StreamResponse:
231246
"""
232247
Validate the resource owners credentials.
233248
@@ -252,7 +267,9 @@ async def authorization_endpoint_post(self, request: web.Request) -> web.StreamR
252267
if not any(client.client_id == client_id for client in self.auth_clients):
253268
_LOGGER.warning(f"unknown client_id {client_id}")
254269
if state is not None:
255-
raise web.HTTPFound(f"{redirect_uri}?error=unauthorized_client&state={state}")
270+
raise web.HTTPFound(
271+
f"{redirect_uri}?error=unauthorized_client&state={state}"
272+
)
256273
else:
257274
raise web.HTTPFound(f"{redirect_uri}?error=unauthorized_client")
258275

@@ -265,41 +282,57 @@ async def authorization_endpoint_post(self, request: web.Request) -> web.StreamR
265282
if not any(uri == redirect_uri for uri in registered_auth_client.redirect_uris):
266283
_LOGGER.error(f"invalid redirect_uri {redirect_uri}")
267284
if state is not None:
268-
raise web.HTTPFound(f"{redirect_uri}?error=unauthorized_client&state={state}")
285+
raise web.HTTPFound(
286+
f"{redirect_uri}?error=unauthorized_client&state={state}"
287+
)
269288
else:
270289
raise web.HTTPFound(f"{redirect_uri}?error=unauthorized_client")
271290

272-
username = data["uname"]
291+
email = data["email"]
273292
password = data["password"]
274293

275294
# validate credentials
276-
credentials_are_valid = await self.auth_database.check_credentials(username, password)
295+
credentials_are_valid = await self.auth_database.check_credentials(
296+
email, password
297+
)
277298

278299
if credentials_are_valid:
279300
# create an authorization code
280-
authorization_code = self.auth_database.create_authorization_code(username, client_id, request.remote)
301+
authorization_code = self.auth_database.create_authorization_code(
302+
email, client_id, request.remote
303+
)
281304
_LOGGER.debug(f"authorization_code: {authorization_code}")
282305
if authorization_code is None:
283306
_LOGGER.warning("could not create auth code for client!")
284307
error_reason = "access_denied"
285308
if state is not None:
286-
raise web.HTTPFound(f"{redirect_uri}?error={error_reason}&state={state}")
309+
raise web.HTTPFound(
310+
f"{redirect_uri}?error={error_reason}&state={state}"
311+
)
287312
else:
288313
raise web.HTTPFound(f"{redirect_uri}?error={error_reason}")
289314

290315
if state is not None:
291-
_LOGGER.debug(f"HTTPFound: {redirect_uri}?code={authorization_code}&state={state}")
292-
redirect_response = web.HTTPFound(f"{redirect_uri}?code={authorization_code}&state={state}")
316+
_LOGGER.debug(
317+
f"HTTPFound: {redirect_uri}?code={authorization_code}&state={state}"
318+
)
319+
redirect_response = web.HTTPFound(
320+
f"{redirect_uri}?code={authorization_code}&state={state}"
321+
)
293322
else:
294323
_LOGGER.debug(f"HTTPFound: {redirect_uri}?code={authorization_code}")
295-
redirect_response = web.HTTPFound(f"{redirect_uri}?code={authorization_code}")
324+
redirect_response = web.HTTPFound(
325+
f"{redirect_uri}?code={authorization_code}"
326+
)
296327

297328
raise redirect_response
298329
else:
299330
error_reason = "access_denied"
300331
_LOGGER.warning(f"redirect with error {error_reason}")
301332
if state is not None:
302-
raise web.HTTPFound(f"{redirect_uri}?error={error_reason}&state={state}")
333+
raise web.HTTPFound(
334+
f"{redirect_uri}?error={error_reason}&state={state}"
335+
)
303336
else:
304337
raise web.HTTPFound(f"{redirect_uri}?error={error_reason}")
305338

@@ -358,13 +391,17 @@ async def _handle_authorization_code_request(self, data) -> web.StreamResponse:
358391
return web.json_response(status=400, data=data)
359392
client_id = data["client_id"]
360393

361-
client_code_valid = await self.auth_database.validate_authorization_code(code, client_id)
394+
client_code_valid = await self.auth_database.validate_authorization_code(
395+
code, client_id
396+
)
362397
if not client_code_valid:
363398
_LOGGER.error("authorization_code invalid!")
364399
payload = {"error": "invalid_grant"}
365400
return web.json_response(status=400, data=payload)
366401

367-
access_token, refresh_token = await self.auth_database.create_tokens(code, client_id)
402+
access_token, refresh_token = await self.auth_database.create_tokens(
403+
code, client_id
404+
)
368405

369406
payload = {
370407
"access_token": access_token,
@@ -374,7 +411,9 @@ async def _handle_authorization_code_request(self, data) -> web.StreamResponse:
374411
}
375412
return web.json_response(status=200, data=payload)
376413

377-
async def _handle_refresh_token_request(self, request: web.Request, data) -> web.StreamResponse:
414+
async def _handle_refresh_token_request(
415+
self, request: web.Request, data
416+
) -> web.StreamResponse:
378417
"""
379418
See Section 6: https://tools.ietf.org/html/rfc6749#section-6
380419
"""
@@ -414,7 +453,9 @@ async def _handle_refresh_token_request(self, request: web.Request, data) -> web
414453
data = {"error": "invalid_client"}
415454
return web.json_response(data)
416455

417-
access_token, refresh_token = await self.auth_database.renew_tokens(client_id, refresh_token)
456+
access_token, refresh_token = await self.auth_database.renew_tokens(
457+
client_id, refresh_token
458+
)
418459

419460
if access_token is None:
420461
raise web.HTTPForbidden()
@@ -436,11 +477,13 @@ def create_client(self):
436477
_LOGGER.info(f"generated client_secret: {client_secret}")
437478

438479
async def check_authorized(self, request: web.Request) -> Optional[str]:
439-
"""Check if authorization header and returns username if valid"""
480+
"""Check if authorization header and returns user ID if valid"""
440481

441482
if hdrs.AUTHORIZATION in request.headers:
442483
try:
443-
auth_type, auth_val = request.headers.get(hdrs.AUTHORIZATION).split(" ", 1)
484+
auth_type, auth_val = request.headers.get(hdrs.AUTHORIZATION).split(
485+
" ", 1
486+
)
444487
if not await self.auth_database.validate_access_token(auth_val):
445488
raise web.HTTPForbidden()
446489

0 commit comments

Comments
 (0)