goosebit 0.1.1__py3-none-any.whl → 0.1.2__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 (52) hide show
  1. goosebit/__init__.py +5 -2
  2. goosebit/api/__init__.py +1 -1
  3. goosebit/api/devices.py +59 -39
  4. goosebit/api/download.py +28 -14
  5. goosebit/api/firmware.py +40 -34
  6. goosebit/api/helper.py +30 -0
  7. goosebit/api/rollouts.py +64 -13
  8. goosebit/api/routes.py +14 -7
  9. goosebit/auth/__init__.py +14 -6
  10. goosebit/db.py +5 -0
  11. goosebit/models.py +110 -10
  12. goosebit/permissions.py +26 -20
  13. goosebit/realtime/__init__.py +1 -1
  14. goosebit/realtime/logs.py +3 -6
  15. goosebit/settings.py +4 -6
  16. goosebit/telemetry/__init__.py +28 -0
  17. goosebit/telemetry/prometheus.py +10 -0
  18. goosebit/ui/__init__.py +1 -1
  19. goosebit/ui/routes.py +33 -40
  20. goosebit/ui/static/js/devices.js +187 -250
  21. goosebit/ui/static/js/firmware.js +229 -92
  22. goosebit/ui/static/js/index.js +79 -90
  23. goosebit/ui/static/js/logs.js +14 -11
  24. goosebit/ui/static/js/rollouts.js +169 -27
  25. goosebit/ui/static/js/util.js +66 -0
  26. goosebit/ui/templates/devices.html +75 -51
  27. goosebit/ui/templates/firmware.html +149 -35
  28. goosebit/ui/templates/index.html +9 -26
  29. goosebit/ui/templates/login.html +58 -27
  30. goosebit/ui/templates/logs.html +15 -5
  31. goosebit/ui/templates/nav.html +77 -26
  32. goosebit/ui/templates/rollouts.html +62 -39
  33. goosebit/updater/__init__.py +1 -1
  34. goosebit/updater/controller/__init__.py +1 -1
  35. goosebit/updater/controller/v1/__init__.py +1 -1
  36. goosebit/updater/controller/v1/routes.py +53 -35
  37. goosebit/updater/manager.py +205 -103
  38. goosebit/updater/routes.py +4 -7
  39. goosebit/updates/__init__.py +70 -0
  40. goosebit/updates/swdesc.py +83 -0
  41. {goosebit-0.1.1.dist-info → goosebit-0.1.2.dist-info}/METADATA +53 -3
  42. goosebit-0.1.2.dist-info/RECORD +51 -0
  43. goosebit/updater/download/__init__.py +0 -1
  44. goosebit/updater/download/routes.py +0 -6
  45. goosebit/updater/download/v1/__init__.py +0 -1
  46. goosebit/updater/download/v1/routes.py +0 -13
  47. goosebit/updater/misc.py +0 -57
  48. goosebit/updates/artifacts.py +0 -89
  49. goosebit/updates/version.py +0 -38
  50. goosebit-0.1.1.dist-info/RECORD +0 -53
  51. {goosebit-0.1.1.dist-info → goosebit-0.1.2.dist-info}/LICENSE +0 -0
  52. {goosebit-0.1.1.dist-info → goosebit-0.1.2.dist-info}/WHEEL +0 -0
goosebit/__init__.py CHANGED
@@ -5,8 +5,9 @@ from fastapi import Depends, FastAPI
5
5
  from fastapi.requests import Request
6
6
  from fastapi.responses import RedirectResponse
7
7
  from fastapi.security import OAuth2PasswordRequestForm
8
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor as Instrumentor
8
9
 
9
- from goosebit import api, db, realtime, ui, updater
10
+ from goosebit import api, db, realtime, telemetry, ui, updater
10
11
  from goosebit.auth import (
11
12
  authenticate_user,
12
13
  auto_redirect,
@@ -20,6 +21,7 @@ from goosebit.ui.templates import templates
20
21
  @asynccontextmanager
21
22
  async def lifespan(_: FastAPI):
22
23
  await db.init()
24
+ await telemetry.init()
23
25
  yield
24
26
  await db.close()
25
27
 
@@ -30,6 +32,7 @@ app.include_router(ui.router)
30
32
  app.include_router(api.router)
31
33
  app.include_router(realtime.router)
32
34
  app.mount("/static", static, name="static")
35
+ Instrumentor.instrument_app(app)
33
36
 
34
37
 
35
38
  @app.middleware("http")
@@ -45,7 +48,7 @@ def root_redirect(request: Request):
45
48
 
46
49
  @app.get("/login", dependencies=[Depends(auto_redirect)], include_in_schema=False)
47
50
  async def login_ui(request: Request):
48
- return templates.TemplateResponse("login.html", context={"request": request})
51
+ return templates.TemplateResponse(request, "login.html")
49
52
 
50
53
 
51
54
  @app.post("/login", include_in_schema=False, dependencies=[Depends(authenticate_user)])
goosebit/api/__init__.py CHANGED
@@ -1 +1 @@
1
- from .routes import router
1
+ from .routes import router # noqa: F401
goosebit/api/devices.py CHANGED
@@ -1,17 +1,16 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
1
  import time
2
+ from typing import Any
5
3
 
6
4
  from fastapi import APIRouter, Security
7
5
  from fastapi.requests import Request
8
6
  from pydantic import BaseModel
7
+ from tortoise.expressions import Q
9
8
 
9
+ from goosebit.api.helper import filter_data
10
10
  from goosebit.auth import validate_user_permissions
11
- from goosebit.models import Device
11
+ from goosebit.models import Device, Firmware, UpdateModeEnum, UpdateStateEnum
12
12
  from goosebit.permissions import Permissions
13
- from goosebit.updater.manager import delete_device, get_update_manager
14
- from goosebit.updater.misc import get_device_by_uuid
13
+ from goosebit.updater.manager import delete_devices, get_update_manager
15
14
 
16
15
  router = APIRouter(prefix="/devices")
17
16
 
@@ -20,55 +19,80 @@ router = APIRouter(prefix="/devices")
20
19
  "/all",
21
20
  dependencies=[Security(validate_user_permissions, scopes=[Permissions.HOME.READ])],
22
21
  )
23
- async def devices_get_all() -> list[dict]:
24
- devices = await Device.all()
22
+ async def devices_get_all(request: Request) -> dict[str, int | list[Any] | Any]:
23
+ query = Device.all().prefetch_related("assigned_firmware", "hardware")
24
+
25
+ def search_filter(search_value):
26
+ return (
27
+ Q(uuid__icontains=search_value)
28
+ | Q(name__icontains=search_value)
29
+ | Q(feed__icontains=search_value)
30
+ | Q(flavor__icontains=search_value)
31
+ | Q(update_mode__icontains=UpdateModeEnum.from_str(search_value))
32
+ | Q(last_state__icontains=UpdateStateEnum.from_str(search_value))
33
+ )
25
34
 
26
35
  async def parse(device: Device) -> dict:
27
36
  manager = await get_update_manager(device.uuid)
37
+ _, target_firmware = await manager.get_update()
28
38
  last_seen = device.last_seen
29
39
  if last_seen is not None:
30
40
  last_seen = round(time.time() - device.last_seen)
31
41
  return {
32
42
  "uuid": device.uuid,
33
43
  "name": device.name,
34
- "fw": device.fw_version,
35
- "fw_file": device.fw_file,
36
- "hw_model": device.hw_model,
37
- "hw_revision": device.hw_revision,
44
+ "fw_installed_version": device.fw_version,
45
+ "fw_target_version": (target_firmware.version if target_firmware is not None else None),
46
+ "fw_assigned": (device.assigned_firmware.id if device.assigned_firmware is not None else None),
47
+ "hw_model": device.hardware.model,
48
+ "hw_revision": device.hardware.revision,
49
+ "feed": device.feed,
50
+ "flavor": device.flavor,
38
51
  "progress": device.progress,
39
- "state": device.last_state,
40
- "force_update": manager.force_update,
52
+ "state": str(device.last_state),
53
+ "update_mode": str(device.update_mode),
54
+ "force_update": device.force_update,
41
55
  "last_ip": device.last_ip,
42
56
  "last_seen": last_seen,
43
- "online": (
44
- last_seen < manager.poll_seconds if last_seen is not None else None
45
- ),
57
+ "online": (last_seen < manager.poll_seconds if last_seen is not None else None),
46
58
  }
47
59
 
48
- return list(await asyncio.gather(*[parse(d) for d in devices]))
60
+ total_records = await Device.all().count()
61
+ return await filter_data(request, query, search_filter, parse, total_records)
49
62
 
50
63
 
51
64
  class UpdateDevicesModel(BaseModel):
52
65
  devices: list[str]
53
66
  firmware: str | None = None
54
67
  name: str | None = None
68
+ pinned: bool | None = None
69
+ feed: str | None = None
70
+ flavor: str | None = None
55
71
 
56
72
 
57
73
  @router.post(
58
74
  "/update",
59
- dependencies=[
60
- Security(validate_user_permissions, scopes=[Permissions.DEVICE.WRITE])
61
- ],
75
+ dependencies=[Security(validate_user_permissions, scopes=[Permissions.DEVICE.WRITE])],
62
76
  )
63
- async def devices_update(request: Request, config: UpdateDevicesModel) -> dict:
77
+ async def devices_update(_: Request, config: UpdateDevicesModel) -> dict:
64
78
  for uuid in config.devices:
65
79
  updater = await get_update_manager(uuid)
66
- device = await updater.get_device()
67
80
  if config.firmware is not None:
68
- device.fw_file = config.firmware
81
+ if config.firmware == "rollout":
82
+ await updater.update_update(UpdateModeEnum.ROLLOUT, None)
83
+ elif config.firmware == "latest":
84
+ await updater.update_update(UpdateModeEnum.LATEST, None)
85
+ else:
86
+ firmware = await Firmware.get_or_none(id=config.firmware)
87
+ await updater.update_update(UpdateModeEnum.ASSIGNED, firmware)
88
+ if config.pinned is not None:
89
+ await updater.update_update(UpdateModeEnum.PINNED, None)
69
90
  if config.name is not None:
70
- device.name = config.name
71
- await updater.save()
91
+ await updater.update_name(config.name)
92
+ if config.feed is not None:
93
+ await updater.update_feed(config.feed)
94
+ if config.flavor is not None:
95
+ await updater.update_flavor(config.flavor)
72
96
  return {"success": True}
73
97
 
74
98
 
@@ -78,14 +102,12 @@ class ForceUpdateModel(BaseModel):
78
102
 
79
103
  @router.post(
80
104
  "/force_update",
81
- dependencies=[
82
- Security(validate_user_permissions, scopes=[Permissions.DEVICE.WRITE])
83
- ],
105
+ dependencies=[Security(validate_user_permissions, scopes=[Permissions.DEVICE.WRITE])],
84
106
  )
85
- async def devices_force_update(request: Request, config: ForceUpdateModel) -> dict:
107
+ async def devices_force_update(_: Request, config: ForceUpdateModel) -> dict:
86
108
  for uuid in config.devices:
87
109
  updater = await get_update_manager(uuid)
88
- updater.force_update = True
110
+ await updater.update_force_update(True)
89
111
  return {"success": True}
90
112
 
91
113
 
@@ -93,8 +115,9 @@ async def devices_force_update(request: Request, config: ForceUpdateModel) -> di
93
115
  "/logs/{dev_id}",
94
116
  dependencies=[Security(validate_user_permissions, scopes=[Permissions.HOME.READ])],
95
117
  )
96
- async def device_logs(request: Request, dev_id: str) -> str:
97
- device = await get_device_by_uuid(dev_id)
118
+ async def device_logs(_: Request, dev_id: str) -> str:
119
+ updater = await get_update_manager(dev_id)
120
+ device = await updater.get_device()
98
121
  if device.last_log is not None:
99
122
  return device.last_log
100
123
  return "No logs found."
@@ -106,11 +129,8 @@ class DeleteModel(BaseModel):
106
129
 
107
130
  @router.post(
108
131
  "/delete",
109
- dependencies=[
110
- Security(validate_user_permissions, scopes=[Permissions.DEVICE.DELETE])
111
- ],
132
+ dependencies=[Security(validate_user_permissions, scopes=[Permissions.DEVICE.DELETE])],
112
133
  )
113
- async def devices_delete(request: Request, config: DeleteModel) -> dict:
114
- for uuid in config.devices:
115
- await delete_device(uuid)
134
+ async def devices_delete(_: Request, config: DeleteModel) -> dict:
135
+ await delete_devices(config.devices)
116
136
  return {"success": True}
goosebit/api/download.py CHANGED
@@ -1,20 +1,34 @@
1
- from fastapi import APIRouter, Security
1
+ from fastapi import APIRouter, HTTPException
2
2
  from fastapi.requests import Request
3
- from fastapi.responses import FileResponse
3
+ from fastapi.responses import FileResponse, RedirectResponse
4
+ from starlette.responses import Response
4
5
 
5
- from goosebit.auth import validate_user_permissions
6
- from goosebit.permissions import Permissions
7
- from goosebit.settings import UPDATES_DIR
6
+ from goosebit.models import Firmware
8
7
 
9
8
  router = APIRouter(prefix="/download")
10
9
 
11
10
 
12
- @router.get(
13
- "/{file}",
14
- dependencies=[
15
- Security(validate_user_permissions, scopes=[Permissions.FIRMWARE.READ])
16
- ],
17
- )
18
- async def download_file(request: Request, file: str):
19
- filename = UPDATES_DIR.joinpath(file)
20
- return FileResponse(filename, media_type="application/octet-stream")
11
+ @router.head("/{file_id}")
12
+ async def download_file_head(_: Request, file_id: int):
13
+ firmware = await Firmware.get_or_none(id=file_id)
14
+ if firmware is None:
15
+ raise HTTPException(404)
16
+
17
+ response = Response()
18
+ response.headers["Content-Length"] = str(firmware.size)
19
+ return response
20
+
21
+
22
+ @router.get("/{file_id}")
23
+ async def download_file(_: Request, file_id: int):
24
+ firmware = await Firmware.get_or_none(id=file_id)
25
+ if firmware is None:
26
+ raise HTTPException(404)
27
+ if firmware.local:
28
+ return FileResponse(
29
+ firmware.path,
30
+ media_type="application/octet-stream",
31
+ filename=firmware.path.name,
32
+ )
33
+ else:
34
+ return RedirectResponse(url=firmware.uri)
goosebit/api/firmware.py CHANGED
@@ -1,51 +1,57 @@
1
+ from typing import Any
2
+
1
3
  from fastapi import APIRouter, Body, Security
2
4
  from fastapi.requests import Request
5
+ from tortoise.expressions import Q
3
6
 
7
+ from goosebit.api.helper import filter_data
4
8
  from goosebit.auth import validate_user_permissions
9
+ from goosebit.models import Firmware
5
10
  from goosebit.permissions import Permissions
6
- from goosebit.settings import UPDATES_DIR
7
- from goosebit.updater.misc import fw_sort_key
8
- from goosebit.updates.artifacts import FirmwareArtifact
9
11
 
10
12
  router = APIRouter(prefix="/firmware")
11
13
 
12
14
 
13
15
  @router.get(
14
16
  "/all",
15
- dependencies=[
16
- Security(validate_user_permissions, scopes=[Permissions.FIRMWARE.READ])
17
- ],
17
+ dependencies=[Security(validate_user_permissions, scopes=[Permissions.FIRMWARE.READ])],
18
18
  )
19
- async def firmware_get_all() -> list[dict]:
20
- UPDATES_DIR.mkdir(parents=True, exist_ok=True)
21
-
22
- firmware = []
23
- for file in sorted(
24
- [f for f in UPDATES_DIR.iterdir() if f.suffix == ".swu"],
25
- key=lambda x: fw_sort_key(x),
26
- reverse=True,
27
- ):
28
- artifact = FirmwareArtifact(file.name)
29
- firmware.append(
30
- {
31
- "name": file.name,
32
- "size": artifact.path.stat().st_size,
33
- "version": artifact.version,
34
- }
35
- )
36
-
37
- return firmware
19
+ async def firmware_get_all(
20
+ request: Request,
21
+ ) -> dict[str, int | list[dict[str, list[Any] | Any]]]:
22
+ query = Firmware.all()
23
+
24
+ def search_filter(search_value):
25
+ return Q(uri__icontains=search_value) | Q(version__icontains=search_value)
26
+
27
+ async def parse(f):
28
+ return {
29
+ "id": f.id,
30
+ "name": f.path.name,
31
+ "size": f.size,
32
+ "hash": f.hash,
33
+ "version": f.version,
34
+ "compatibility": list(await f.compatibility.all().values()),
35
+ }
36
+
37
+ total_records = await Firmware.all().count()
38
+ return await filter_data(request, query, search_filter, parse, total_records)
38
39
 
39
40
 
40
41
  @router.post(
41
42
  "/delete",
42
- dependencies=[
43
- Security(validate_user_permissions, scopes=[Permissions.FIRMWARE.DELETE])
44
- ],
43
+ dependencies=[Security(validate_user_permissions, scopes=[Permissions.FIRMWARE.DELETE])],
45
44
  )
46
- async def firmware_delete(request: Request, file: str = Body()) -> dict:
47
- file_path = UPDATES_DIR.joinpath(file)
48
- if file_path.exists():
49
- file_path.unlink()
50
- return {"success": True}
51
- return {"success": False}
45
+ async def firmware_delete(_: Request, files: list[int] = Body()) -> dict:
46
+ success = False
47
+ for f_id in files:
48
+ firmware = await Firmware.get_or_none(id=f_id)
49
+ if firmware is None:
50
+ continue
51
+ if firmware.local:
52
+ path = firmware.path
53
+ if path.exists():
54
+ path.unlink()
55
+ await firmware.delete()
56
+ success = True
57
+ return {"success": success}
goosebit/api/helper.py ADDED
@@ -0,0 +1,30 @@
1
+ import asyncio
2
+
3
+
4
+ async def filter_data(request, query, search_filter, parse, total_records):
5
+ params = request.query_params
6
+
7
+ draw = int(params.get("draw", 1))
8
+ start = int(params.get("start", 0))
9
+ length = int(params.get("length", 10))
10
+ search_value = params.get("search[value]", None)
11
+ order_column_index = params.get("order[0][column]", None)
12
+ order_column = params.get(f"columns[{order_column_index}][data]", None)
13
+ order_dir = params.get("order[0][dir]", None)
14
+
15
+ if search_value:
16
+ query = query.filter(search_filter(search_value))
17
+
18
+ if order_column:
19
+ query = query.order_by(f"{"-" if order_dir == "desc" else ""}{order_column}")
20
+
21
+ filtered_records = await query.count()
22
+ rollouts = await query.offset(start).limit(length).all()
23
+ data = list(await asyncio.gather(*[parse(r) for r in rollouts]))
24
+
25
+ return {
26
+ "draw": draw,
27
+ "recordsTotal": total_records,
28
+ "recordsFiltered": filtered_records,
29
+ "data": data,
30
+ }
goosebit/api/rollouts.py CHANGED
@@ -1,7 +1,9 @@
1
- from __future__ import annotations
2
-
3
1
  from fastapi import APIRouter, Security
2
+ from fastapi.requests import Request
3
+ from pydantic import BaseModel
4
+ from tortoise.expressions import Q
4
5
 
6
+ from goosebit.api.helper import filter_data
5
7
  from goosebit.auth import validate_user_permissions
6
8
  from goosebit.models import Rollout
7
9
  from goosebit.permissions import Permissions
@@ -11,26 +13,75 @@ router = APIRouter(prefix="/rollouts")
11
13
 
12
14
  @router.get(
13
15
  "/all",
14
- dependencies=[
15
- Security(validate_user_permissions, scopes=[Permissions.ROLLOUT.READ])
16
- ],
16
+ dependencies=[Security(validate_user_permissions, scopes=[Permissions.ROLLOUT.READ])],
17
17
  )
18
- async def rollouts_get_all() -> list[dict]:
19
- rollouts = await Rollout.all()
18
+ async def rollouts_get_all(request: Request) -> dict[str, int | list[dict]]:
19
+ query = Rollout.all().prefetch_related("firmware")
20
+
21
+ def search_filter(search_value):
22
+ return Q(name__icontains=search_value) | Q(feed__icontains=search_value) | Q(flavor__icontains=search_value)
20
23
 
21
- def parse(rollout: Rollout) -> dict:
24
+ async def parse(rollout: Rollout) -> dict:
22
25
  return {
23
26
  "id": rollout.id,
24
- "created_at": rollout.created_at,
27
+ "created_at": int(rollout.created_at.timestamp() * 1000),
25
28
  "name": rollout.name,
26
- "hw_model": rollout.hw_model,
27
- "hw_revision": rollout.hw_revision,
28
29
  "feed": rollout.feed,
29
30
  "flavor": rollout.flavor,
30
- "fw_file": rollout.fw_file,
31
+ "fw_file": rollout.firmware.path.name,
32
+ "fw_version": rollout.firmware.version,
31
33
  "paused": rollout.paused,
32
34
  "success_count": rollout.success_count,
33
35
  "failure_count": rollout.failure_count,
34
36
  }
35
37
 
36
- return [parse(r) for r in rollouts]
38
+ total_records = await Rollout.all().count()
39
+ return await filter_data(request, query, search_filter, parse, total_records)
40
+
41
+
42
+ class CreateRolloutsModel(BaseModel):
43
+ name: str
44
+ feed: str
45
+ flavor: str
46
+ firmware_id: int
47
+
48
+
49
+ @router.post(
50
+ "/",
51
+ dependencies=[Security(validate_user_permissions, scopes=[Permissions.ROLLOUT.WRITE])],
52
+ )
53
+ async def rollouts_create(_: Request, rollout: CreateRolloutsModel) -> dict:
54
+ rollout = await Rollout.create(
55
+ name=rollout.name,
56
+ feed=rollout.feed,
57
+ flavor=rollout.flavor,
58
+ firmware_id=rollout.firmware_id,
59
+ )
60
+ return {"success": True, "id": rollout.id}
61
+
62
+
63
+ class UpdateRolloutsModel(BaseModel):
64
+ ids: list[int]
65
+ paused: bool
66
+
67
+
68
+ @router.post(
69
+ "/update",
70
+ dependencies=[Security(validate_user_permissions, scopes=[Permissions.ROLLOUT.WRITE])],
71
+ )
72
+ async def rollouts_update(_: Request, rollouts: UpdateRolloutsModel) -> dict:
73
+ await Rollout.filter(id__in=rollouts.ids).update(paused=rollouts.paused)
74
+ return {"success": True}
75
+
76
+
77
+ class DeleteRolloutsModel(BaseModel):
78
+ ids: list[int]
79
+
80
+
81
+ @router.post(
82
+ "/delete",
83
+ dependencies=[Security(validate_user_permissions, scopes=[Permissions.ROLLOUT.DELETE])],
84
+ )
85
+ async def rollouts_delete(_: Request, rollouts: DeleteRolloutsModel) -> dict:
86
+ await Rollout.filter(id__in=rollouts.ids).delete()
87
+ return {"success": True}
goosebit/api/routes.py CHANGED
@@ -3,10 +3,17 @@ from fastapi import APIRouter, Depends
3
3
  from goosebit.api import devices, download, firmware, rollouts
4
4
  from goosebit.auth import authenticate_api_session
5
5
 
6
- router = APIRouter(
7
- prefix="/api", dependencies=[Depends(authenticate_api_session)], tags=["api"]
8
- )
9
- router.include_router(firmware.router)
10
- router.include_router(devices.router)
11
- router.include_router(rollouts.router)
12
- router.include_router(download.router)
6
+ # main router that requires authentication
7
+ main_router = APIRouter(prefix="/api", dependencies=[Depends(authenticate_api_session)], tags=["api"])
8
+ main_router.include_router(firmware.router)
9
+ main_router.include_router(devices.router)
10
+ main_router.include_router(rollouts.router)
11
+
12
+ # download router without authentication
13
+ download_router = APIRouter(prefix="/api", tags=["api"])
14
+ download_router.include_router(download.router)
15
+
16
+ # include both routers
17
+ router = APIRouter()
18
+ router.include_router(main_router)
19
+ router.include_router(download_router)
goosebit/auth/__init__.py CHANGED
@@ -1,5 +1,6 @@
1
- from __future__ import annotations
1
+ import logging
2
2
 
3
+ from argon2.exceptions import VerifyMismatchError
3
4
  from fastapi import Depends, HTTPException
4
5
  from fastapi.requests import Request
5
6
  from fastapi.security import SecurityScopes
@@ -9,6 +10,7 @@ from joserfc.errors import BadSignatureError
9
10
 
10
11
  from goosebit.settings import PWD_CXT, SECRET, USERS
11
12
 
13
+ logger = logging.getLogger(__name__)
12
14
 
13
15
  async def authenticate_user(request: Request):
14
16
  form_data = await request.form()
@@ -21,7 +23,14 @@ async def authenticate_user(request: Request):
21
23
  headers={"location": str(request.url_for("login"))},
22
24
  detail="Invalid credentials",
23
25
  )
24
- if not PWD_CXT.verify(user.hashed_pwd, password):
26
+ try:
27
+ if not PWD_CXT.verify(user.hashed_pwd, password):
28
+ raise HTTPException(
29
+ status_code=302,
30
+ headers={"location": str(request.url_for("login"))},
31
+ detail="Invalid credentials",
32
+ )
33
+ except VerifyMismatchError:
25
34
  raise HTTPException(
26
35
  status_code=302,
27
36
  headers={"location": str(request.url_for("login"))},
@@ -31,9 +40,7 @@ async def authenticate_user(request: Request):
31
40
 
32
41
 
33
42
  def create_session(username: str) -> str:
34
- return jwt.encode(
35
- header={"alg": "HS256"}, claims={"username": username}, key=SECRET
36
- )
43
+ return jwt.encode(header={"alg": "HS256"}, claims={"username": username}, key=SECRET)
37
44
 
38
45
 
39
46
  def authenticate_session(request: Request):
@@ -87,7 +94,7 @@ def get_user_from_session(session_id: str):
87
94
  try:
88
95
  session_data = jwt.decode(session_id, SECRET)
89
96
  return session_data.claims["username"]
90
- except (BadSignatureError, LookupError):
97
+ except (BadSignatureError, LookupError, ValueError):
91
98
  pass
92
99
 
93
100
 
@@ -109,6 +116,7 @@ def validate_user_permissions(
109
116
  return request
110
117
  for scope in security.scopes:
111
118
  if scope not in user.permissions:
119
+ logger.warning(f"User {username} does not have permission {scope}")
112
120
  raise HTTPException(
113
121
  status_code=403,
114
122
  detail="Not enough permissions",
goosebit/db.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from aerich import Command
2
2
  from tortoise import Tortoise, run_async
3
3
 
4
+ from goosebit.models import Firmware
4
5
  from goosebit.settings import DB_MIGRATIONS_LOC, DB_URI
5
6
 
6
7
  TORTOISE_CONF = {
@@ -22,6 +23,10 @@ async def init():
22
23
  await command.migrate()
23
24
  await command.upgrade(run_in_transaction=True)
24
25
  await Tortoise.generate_schemas(safe=True)
26
+ for firmware in await Firmware.all():
27
+ if firmware.local and not firmware.path.exists():
28
+ # delete it
29
+ await firmware.delete()
25
30
 
26
31
 
27
32
  async def close():