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.
- goosebit/__init__.py +5 -2
- goosebit/api/__init__.py +1 -1
- goosebit/api/devices.py +59 -39
- goosebit/api/download.py +28 -14
- goosebit/api/firmware.py +40 -34
- goosebit/api/helper.py +30 -0
- goosebit/api/rollouts.py +64 -13
- goosebit/api/routes.py +14 -7
- goosebit/auth/__init__.py +14 -6
- goosebit/db.py +5 -0
- goosebit/models.py +110 -10
- goosebit/permissions.py +26 -20
- goosebit/realtime/__init__.py +1 -1
- goosebit/realtime/logs.py +3 -6
- goosebit/settings.py +4 -6
- goosebit/telemetry/__init__.py +28 -0
- goosebit/telemetry/prometheus.py +10 -0
- goosebit/ui/__init__.py +1 -1
- goosebit/ui/routes.py +33 -40
- goosebit/ui/static/js/devices.js +187 -250
- goosebit/ui/static/js/firmware.js +229 -92
- goosebit/ui/static/js/index.js +79 -90
- goosebit/ui/static/js/logs.js +14 -11
- goosebit/ui/static/js/rollouts.js +169 -27
- goosebit/ui/static/js/util.js +66 -0
- goosebit/ui/templates/devices.html +75 -51
- goosebit/ui/templates/firmware.html +149 -35
- goosebit/ui/templates/index.html +9 -26
- goosebit/ui/templates/login.html +58 -27
- goosebit/ui/templates/logs.html +15 -5
- goosebit/ui/templates/nav.html +77 -26
- goosebit/ui/templates/rollouts.html +62 -39
- goosebit/updater/__init__.py +1 -1
- goosebit/updater/controller/__init__.py +1 -1
- goosebit/updater/controller/v1/__init__.py +1 -1
- goosebit/updater/controller/v1/routes.py +53 -35
- goosebit/updater/manager.py +205 -103
- goosebit/updater/routes.py +4 -7
- goosebit/updates/__init__.py +70 -0
- goosebit/updates/swdesc.py +83 -0
- {goosebit-0.1.1.dist-info → goosebit-0.1.2.dist-info}/METADATA +53 -3
- goosebit-0.1.2.dist-info/RECORD +51 -0
- goosebit/updater/download/__init__.py +0 -1
- goosebit/updater/download/routes.py +0 -6
- goosebit/updater/download/v1/__init__.py +0 -1
- goosebit/updater/download/v1/routes.py +0 -13
- goosebit/updater/misc.py +0 -57
- goosebit/updates/artifacts.py +0 -89
- goosebit/updates/version.py +0 -38
- goosebit-0.1.1.dist-info/RECORD +0 -53
- {goosebit-0.1.1.dist-info → goosebit-0.1.2.dist-info}/LICENSE +0 -0
- {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"
|
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
|
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[
|
24
|
-
|
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
|
-
"
|
35
|
-
"
|
36
|
-
"
|
37
|
-
"
|
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
|
-
"
|
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
|
-
|
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(
|
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
|
-
|
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
|
-
|
71
|
-
|
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(
|
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.
|
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(
|
97
|
-
|
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(
|
114
|
-
|
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,
|
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.
|
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.
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
return
|
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(
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
):
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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(
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
7
|
-
|
8
|
-
)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
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
|
-
|
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():
|