goosebit 0.1.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.
- goosebit/__init__.py +62 -0
- goosebit/api/__init__.py +1 -0
- goosebit/api/devices.py +112 -0
- goosebit/api/download.py +20 -0
- goosebit/api/firmware.py +64 -0
- goosebit/api/routes.py +11 -0
- goosebit/auth/__init__.py +123 -0
- goosebit/db.py +32 -0
- goosebit/models.py +21 -0
- goosebit/permissions.py +55 -0
- goosebit/realtime/__init__.py +1 -0
- goosebit/realtime/logs.py +43 -0
- goosebit/realtime/routes.py +13 -0
- goosebit/settings.py +55 -0
- goosebit/ui/__init__.py +1 -0
- goosebit/ui/routes.py +104 -0
- goosebit/ui/static/__init__.py +5 -0
- goosebit/ui/static/favicon.ico +0 -0
- goosebit/ui/static/favicon.svg +1 -0
- goosebit/ui/static/js/devices.js +370 -0
- goosebit/ui/static/js/firmware.js +131 -0
- goosebit/ui/static/js/index.js +161 -0
- goosebit/ui/static/js/logs.js +18 -0
- goosebit/ui/static/svg/goosebit-logo.svg +1 -0
- goosebit/ui/templates/__init__.py +5 -0
- goosebit/ui/templates/devices.html +82 -0
- goosebit/ui/templates/firmware.html +47 -0
- goosebit/ui/templates/index.html +37 -0
- goosebit/ui/templates/login.html +34 -0
- goosebit/ui/templates/logs.html +21 -0
- goosebit/ui/templates/nav.html +64 -0
- goosebit/updater/__init__.py +1 -0
- goosebit/updater/controller/__init__.py +1 -0
- goosebit/updater/controller/routes.py +6 -0
- goosebit/updater/controller/v1/__init__.py +1 -0
- goosebit/updater/controller/v1/routes.py +92 -0
- goosebit/updater/download/__init__.py +1 -0
- goosebit/updater/download/routes.py +6 -0
- goosebit/updater/download/v1/__init__.py +1 -0
- goosebit/updater/download/v1/routes.py +26 -0
- goosebit/updater/manager.py +206 -0
- goosebit/updater/misc.py +69 -0
- goosebit/updater/routes.py +30 -0
- goosebit/updater/updates.py +93 -0
- goosebit-0.1.0.dist-info/LICENSE +201 -0
- goosebit-0.1.0.dist-info/METADATA +37 -0
- goosebit-0.1.0.dist-info/RECORD +48 -0
- goosebit-0.1.0.dist-info/WHEEL +4 -0
goosebit/__init__.py
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
from contextlib import asynccontextmanager
|
2
|
+
from typing import Annotated
|
3
|
+
|
4
|
+
from fastapi import Depends, FastAPI
|
5
|
+
from fastapi.requests import Request
|
6
|
+
from fastapi.responses import RedirectResponse
|
7
|
+
from fastapi.security import OAuth2PasswordRequestForm
|
8
|
+
|
9
|
+
from goosebit import api, db, realtime, ui, updater
|
10
|
+
from goosebit.auth import (
|
11
|
+
authenticate_user,
|
12
|
+
auto_redirect,
|
13
|
+
create_session,
|
14
|
+
get_current_user,
|
15
|
+
)
|
16
|
+
from goosebit.ui.static import static
|
17
|
+
from goosebit.ui.templates import templates
|
18
|
+
|
19
|
+
|
20
|
+
@asynccontextmanager
|
21
|
+
async def lifespan(_: FastAPI):
|
22
|
+
await db.init()
|
23
|
+
yield
|
24
|
+
await db.close()
|
25
|
+
|
26
|
+
|
27
|
+
app = FastAPI(lifespan=lifespan)
|
28
|
+
app.include_router(updater.router)
|
29
|
+
app.include_router(ui.router)
|
30
|
+
app.include_router(api.router)
|
31
|
+
app.include_router(realtime.router)
|
32
|
+
app.mount("/static", static, name="static")
|
33
|
+
|
34
|
+
|
35
|
+
@app.middleware("http")
|
36
|
+
async def attach_user(request: Request, call_next):
|
37
|
+
request.scope["user"] = get_current_user(request)
|
38
|
+
return await call_next(request)
|
39
|
+
|
40
|
+
|
41
|
+
@app.get("/", include_in_schema=False)
|
42
|
+
def root_redirect(request: Request):
|
43
|
+
return RedirectResponse(request.url_for("ui_root"))
|
44
|
+
|
45
|
+
|
46
|
+
@app.get("/login", dependencies=[Depends(auto_redirect)], include_in_schema=False)
|
47
|
+
async def login_ui(request: Request):
|
48
|
+
return templates.TemplateResponse("login.html", context={"request": request})
|
49
|
+
|
50
|
+
|
51
|
+
@app.post("/login", include_in_schema=False, dependencies=[Depends(authenticate_user)])
|
52
|
+
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
|
53
|
+
resp = RedirectResponse("/ui/home", status_code=302)
|
54
|
+
resp.set_cookie(key="session_id", value=create_session(form_data.username))
|
55
|
+
return resp
|
56
|
+
|
57
|
+
|
58
|
+
@app.get("/logout", include_in_schema=False)
|
59
|
+
async def logout(request: Request):
|
60
|
+
resp = RedirectResponse("/login", status_code=302)
|
61
|
+
resp.delete_cookie(key="session_id")
|
62
|
+
return resp
|
goosebit/api/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
from .routes import router
|
goosebit/api/devices.py
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import time
|
5
|
+
|
6
|
+
from fastapi import APIRouter, Security
|
7
|
+
from fastapi.requests import Request
|
8
|
+
from pydantic import BaseModel
|
9
|
+
|
10
|
+
from goosebit.auth import validate_user_permissions
|
11
|
+
from goosebit.models import Device
|
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
|
15
|
+
|
16
|
+
router = APIRouter(prefix="/devices")
|
17
|
+
|
18
|
+
|
19
|
+
@router.get(
|
20
|
+
"/all",
|
21
|
+
dependencies=[Security(validate_user_permissions, scopes=[Permissions.HOME.READ])],
|
22
|
+
)
|
23
|
+
async def devices_get_all() -> list[dict]:
|
24
|
+
devices = await Device.all()
|
25
|
+
|
26
|
+
async def parse(device: Device) -> dict:
|
27
|
+
manager = await get_update_manager(device.uuid)
|
28
|
+
last_seen = device.last_seen
|
29
|
+
if last_seen is not None:
|
30
|
+
last_seen = round(time.time() - device.last_seen)
|
31
|
+
return {
|
32
|
+
"uuid": device.uuid,
|
33
|
+
"web_pwd": device.web_pwd,
|
34
|
+
"name": device.name,
|
35
|
+
"fw": device.fw_version,
|
36
|
+
"fw_file": device.fw_file,
|
37
|
+
"state": device.last_state,
|
38
|
+
"force_update": manager.force_update,
|
39
|
+
"last_ip": device.last_ip,
|
40
|
+
"last_seen": last_seen,
|
41
|
+
"online": last_seen < 120 if last_seen is not None else None,
|
42
|
+
}
|
43
|
+
|
44
|
+
return list(await asyncio.gather(*[parse(d) for d in devices]))
|
45
|
+
|
46
|
+
|
47
|
+
class UpdateDevicesModel(BaseModel):
|
48
|
+
devices: list[str]
|
49
|
+
firmware: str | None = None
|
50
|
+
name: str | None = None
|
51
|
+
|
52
|
+
|
53
|
+
@router.post(
|
54
|
+
"/update",
|
55
|
+
dependencies=[
|
56
|
+
Security(validate_user_permissions, scopes=[Permissions.DEVICE.WRITE])
|
57
|
+
],
|
58
|
+
)
|
59
|
+
async def devices_update(request: Request, config: UpdateDevicesModel) -> dict:
|
60
|
+
for uuid in config.devices:
|
61
|
+
updater = await get_update_manager(uuid)
|
62
|
+
device = await updater.get_device()
|
63
|
+
if config.firmware is not None:
|
64
|
+
device.fw_file = config.firmware
|
65
|
+
if config.name is not None:
|
66
|
+
device.name = config.name
|
67
|
+
await updater.save()
|
68
|
+
return {"success": True}
|
69
|
+
|
70
|
+
|
71
|
+
class ForceUpdateModel(BaseModel):
|
72
|
+
devices: list[str]
|
73
|
+
|
74
|
+
|
75
|
+
@router.post(
|
76
|
+
"/force_update",
|
77
|
+
dependencies=[
|
78
|
+
Security(validate_user_permissions, scopes=[Permissions.DEVICE.WRITE])
|
79
|
+
],
|
80
|
+
)
|
81
|
+
async def devices_force_update(request: Request, config: ForceUpdateModel) -> dict:
|
82
|
+
for uuid in config.devices:
|
83
|
+
updater = await get_update_manager(uuid)
|
84
|
+
updater.force_update = True
|
85
|
+
return {"success": True}
|
86
|
+
|
87
|
+
|
88
|
+
@router.get(
|
89
|
+
"/logs/{dev_id}",
|
90
|
+
dependencies=[Security(validate_user_permissions, scopes=[Permissions.HOME.READ])],
|
91
|
+
)
|
92
|
+
async def device_logs(request: Request, dev_id: str) -> str:
|
93
|
+
device = await get_device_by_uuid(dev_id)
|
94
|
+
if device.last_log is not None:
|
95
|
+
return device.last_log
|
96
|
+
return "No logs found."
|
97
|
+
|
98
|
+
|
99
|
+
class DeleteModel(BaseModel):
|
100
|
+
devices: list[str]
|
101
|
+
|
102
|
+
|
103
|
+
@router.post(
|
104
|
+
"/delete",
|
105
|
+
dependencies=[
|
106
|
+
Security(validate_user_permissions, scopes=[Permissions.DEVICE.DELETE])
|
107
|
+
],
|
108
|
+
)
|
109
|
+
async def devices_delete(request: Request, config: DeleteModel) -> dict:
|
110
|
+
for uuid in config.devices:
|
111
|
+
await delete_device(uuid)
|
112
|
+
return {"success": True}
|
goosebit/api/download.py
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
from fastapi import APIRouter, Security
|
2
|
+
from fastapi.requests import Request
|
3
|
+
from fastapi.responses import FileResponse
|
4
|
+
|
5
|
+
from goosebit.auth import validate_user_permissions
|
6
|
+
from goosebit.permissions import Permissions
|
7
|
+
from goosebit.settings import UPDATES_DIR
|
8
|
+
|
9
|
+
router = APIRouter(prefix="/download")
|
10
|
+
|
11
|
+
|
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")
|
goosebit/api/firmware.py
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
from fastapi import APIRouter, Body, Security
|
2
|
+
from fastapi.requests import Request
|
3
|
+
|
4
|
+
from goosebit.auth import validate_user_permissions
|
5
|
+
from goosebit.permissions import Permissions
|
6
|
+
from goosebit.settings import UPDATES_DIR
|
7
|
+
from goosebit.updater.misc import fw_sort_key, get_newest_fw
|
8
|
+
from goosebit.updater.updates import FirmwareArtifact
|
9
|
+
|
10
|
+
router = APIRouter(prefix="/firmware")
|
11
|
+
|
12
|
+
|
13
|
+
@router.get(
|
14
|
+
"/all",
|
15
|
+
dependencies=[
|
16
|
+
Security(validate_user_permissions, scopes=[Permissions.FIRMWARE.READ])
|
17
|
+
],
|
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
|
38
|
+
|
39
|
+
|
40
|
+
@router.get(
|
41
|
+
"/latest",
|
42
|
+
dependencies=[
|
43
|
+
Security(validate_user_permissions, scopes=[Permissions.FIRMWARE.READ])
|
44
|
+
],
|
45
|
+
)
|
46
|
+
async def firmware_get_latest() -> dict:
|
47
|
+
UPDATES_DIR.mkdir(parents=True, exist_ok=True)
|
48
|
+
|
49
|
+
file_data = UPDATES_DIR.joinpath(get_newest_fw())
|
50
|
+
return {"name": file_data.name, "size": file_data.stat().st_size}
|
51
|
+
|
52
|
+
|
53
|
+
@router.post(
|
54
|
+
"/delete",
|
55
|
+
dependencies=[
|
56
|
+
Security(validate_user_permissions, scopes=[Permissions.FIRMWARE.DELETE])
|
57
|
+
],
|
58
|
+
)
|
59
|
+
async def firmware_delete(request: Request, file: str = Body()) -> dict:
|
60
|
+
file_path = UPDATES_DIR.joinpath(file)
|
61
|
+
if file_path.exists():
|
62
|
+
file_path.unlink()
|
63
|
+
return {"success": True}
|
64
|
+
return {"success": False}
|
goosebit/api/routes.py
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
from fastapi import APIRouter, Depends
|
2
|
+
|
3
|
+
from goosebit.api import devices, download, firmware
|
4
|
+
from goosebit.auth import authenticate_api_session
|
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(download.router)
|
@@ -0,0 +1,123 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from fastapi import Depends, HTTPException
|
4
|
+
from fastapi.requests import Request
|
5
|
+
from fastapi.security import SecurityScopes
|
6
|
+
from fastapi.websockets import WebSocket
|
7
|
+
from jose import jwt
|
8
|
+
|
9
|
+
from goosebit.settings import PWD_CXT, SECRET, USERS
|
10
|
+
|
11
|
+
|
12
|
+
async def authenticate_user(request: Request):
|
13
|
+
form_data = await request.form()
|
14
|
+
username = form_data.get("username")
|
15
|
+
password = form_data.get("password")
|
16
|
+
user = USERS.get(username)
|
17
|
+
if user is None:
|
18
|
+
raise HTTPException(
|
19
|
+
status_code=302,
|
20
|
+
headers={"location": str(request.url_for("login"))},
|
21
|
+
detail="Invalid credentials",
|
22
|
+
)
|
23
|
+
if not PWD_CXT.verify(password, user.hashed_pwd):
|
24
|
+
raise HTTPException(
|
25
|
+
status_code=302,
|
26
|
+
headers={"location": str(request.url_for("login"))},
|
27
|
+
detail="Invalid credentials",
|
28
|
+
)
|
29
|
+
return user
|
30
|
+
|
31
|
+
|
32
|
+
sessions = {}
|
33
|
+
|
34
|
+
|
35
|
+
def create_session(email: str) -> str:
|
36
|
+
token = jwt.encode({"email": email}, SECRET)
|
37
|
+
sessions[token] = email
|
38
|
+
return token
|
39
|
+
|
40
|
+
|
41
|
+
def authenticate_session(request: Request):
|
42
|
+
session_id = request.cookies.get("session_id")
|
43
|
+
if session_id is None or session_id not in sessions:
|
44
|
+
raise HTTPException(
|
45
|
+
status_code=302,
|
46
|
+
headers={"location": str(request.url_for("login"))},
|
47
|
+
detail="Invalid session ID",
|
48
|
+
)
|
49
|
+
user = get_user_from_session(session_id)
|
50
|
+
return user
|
51
|
+
|
52
|
+
|
53
|
+
def authenticate_api_session(request: Request):
|
54
|
+
session_id = request.cookies.get("session_id")
|
55
|
+
if session_id is None or session_id not in sessions:
|
56
|
+
raise HTTPException(status_code=401, detail="Not logged in")
|
57
|
+
user = get_user_from_session(session_id)
|
58
|
+
return user
|
59
|
+
|
60
|
+
|
61
|
+
def authenticate_ws_session(websocket: WebSocket):
|
62
|
+
session_id = websocket.cookies.get("session_id")
|
63
|
+
if session_id is None or session_id not in sessions:
|
64
|
+
raise HTTPException(status_code=401, detail="Not logged in")
|
65
|
+
user = get_user_from_session(session_id)
|
66
|
+
return user
|
67
|
+
|
68
|
+
|
69
|
+
def auto_redirect(request: Request):
|
70
|
+
session_id = request.cookies.get("session_id")
|
71
|
+
if session_id is None or session_id not in sessions:
|
72
|
+
return request
|
73
|
+
raise HTTPException(
|
74
|
+
status_code=302,
|
75
|
+
headers={"location": str(request.url_for("ui_root"))},
|
76
|
+
detail="Already logged in",
|
77
|
+
)
|
78
|
+
|
79
|
+
|
80
|
+
def get_user_from_session(session_id: str):
|
81
|
+
for username in USERS:
|
82
|
+
if username == sessions.get(session_id):
|
83
|
+
return username
|
84
|
+
|
85
|
+
|
86
|
+
def get_current_user(request: Request):
|
87
|
+
session_id = request.cookies.get("session_id")
|
88
|
+
if session_id is None or session_id not in sessions:
|
89
|
+
return None
|
90
|
+
user = get_user_from_session(session_id)
|
91
|
+
return USERS[user]
|
92
|
+
|
93
|
+
|
94
|
+
def validate_user_permissions(
|
95
|
+
request: Request,
|
96
|
+
security: SecurityScopes,
|
97
|
+
username: str = Depends(authenticate_session),
|
98
|
+
) -> Request:
|
99
|
+
user = USERS[username]
|
100
|
+
if security.scopes is None:
|
101
|
+
return request
|
102
|
+
for scope in security.scopes:
|
103
|
+
if scope not in user.permissions:
|
104
|
+
raise HTTPException(
|
105
|
+
status_code=403,
|
106
|
+
detail="Not enough permissions",
|
107
|
+
)
|
108
|
+
|
109
|
+
|
110
|
+
def validate_ws_user_permissions(
|
111
|
+
websocket: WebSocket,
|
112
|
+
security: SecurityScopes,
|
113
|
+
username: str = Depends(authenticate_ws_session),
|
114
|
+
) -> WebSocket:
|
115
|
+
user = USERS[username]
|
116
|
+
if security.scopes is None:
|
117
|
+
return websocket
|
118
|
+
for scope in security.scopes:
|
119
|
+
if scope not in user.permissions:
|
120
|
+
raise HTTPException(
|
121
|
+
status_code=403,
|
122
|
+
detail="Not enough permissions",
|
123
|
+
)
|
goosebit/db.py
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
from aerich import Command
|
2
|
+
from tortoise import Tortoise, run_async
|
3
|
+
|
4
|
+
from goosebit.settings import DB_MIGRATIONS_LOC, DB_URI
|
5
|
+
|
6
|
+
TORTOISE_CONF = {
|
7
|
+
"connections": {"default": DB_URI},
|
8
|
+
"apps": {
|
9
|
+
"models": {
|
10
|
+
"models": ["goosebit.models", "aerich.models"],
|
11
|
+
},
|
12
|
+
},
|
13
|
+
}
|
14
|
+
|
15
|
+
|
16
|
+
async def init():
|
17
|
+
command = Command(tortoise_config=TORTOISE_CONF, location=DB_MIGRATIONS_LOC)
|
18
|
+
await Tortoise.init(config=TORTOISE_CONF)
|
19
|
+
if not DB_MIGRATIONS_LOC.exists():
|
20
|
+
await command.init_db(safe=True)
|
21
|
+
await command.init()
|
22
|
+
await command.migrate()
|
23
|
+
await command.upgrade(run_in_transaction=True)
|
24
|
+
await Tortoise.generate_schemas(safe=True)
|
25
|
+
|
26
|
+
|
27
|
+
async def close():
|
28
|
+
await Tortoise.close_connections()
|
29
|
+
|
30
|
+
|
31
|
+
if __name__ == "__main__":
|
32
|
+
run_async(init())
|
goosebit/models.py
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
from tortoise import Model, fields
|
2
|
+
|
3
|
+
|
4
|
+
class Tag(Model):
|
5
|
+
id = fields.IntField(pk=True)
|
6
|
+
name = fields.CharField(max_length=255)
|
7
|
+
|
8
|
+
|
9
|
+
class Device(Model):
|
10
|
+
uuid = fields.CharField(max_length=255, pk=True)
|
11
|
+
name = fields.CharField(max_length=255, null=True)
|
12
|
+
fw_file = fields.CharField(max_length=255, default="latest")
|
13
|
+
fw_version = fields.CharField(max_length=255, null=True)
|
14
|
+
last_state = fields.CharField(max_length=255, null=True)
|
15
|
+
last_log = fields.TextField(null=True)
|
16
|
+
last_seen = fields.BigIntField(null=True)
|
17
|
+
last_ip = fields.CharField(max_length=15, null=True)
|
18
|
+
last_ipv6 = fields.CharField(max_length=40, null=True)
|
19
|
+
tags = fields.ManyToManyField(
|
20
|
+
"models.Tag", related_name="devices", through="device_tags"
|
21
|
+
)
|
goosebit/permissions.py
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
|
3
|
+
|
4
|
+
class PermissionsBase(str, Enum):
|
5
|
+
@classmethod
|
6
|
+
def full(cls) -> list:
|
7
|
+
return [i for i in cls]
|
8
|
+
|
9
|
+
def __str__(self):
|
10
|
+
return self.value
|
11
|
+
|
12
|
+
|
13
|
+
class FirmwarePermissions(PermissionsBase):
|
14
|
+
READ = "firmware.read"
|
15
|
+
WRITE = "firmware.write"
|
16
|
+
DELETE = "firmware.delete"
|
17
|
+
|
18
|
+
|
19
|
+
class DevicePermissions(PermissionsBase):
|
20
|
+
READ = "devices.read"
|
21
|
+
WRITE = "devices.write"
|
22
|
+
DELETE = "devices.delete"
|
23
|
+
|
24
|
+
|
25
|
+
class TunnelPermissions(PermissionsBase):
|
26
|
+
READ = "tunnels.read"
|
27
|
+
WRITE = "tunnels.write"
|
28
|
+
DELETE = "tunnels.delete"
|
29
|
+
|
30
|
+
|
31
|
+
class HomePermissions(PermissionsBase):
|
32
|
+
READ = "home.read"
|
33
|
+
|
34
|
+
|
35
|
+
class Permissions:
|
36
|
+
HOME = HomePermissions
|
37
|
+
FIRMWARE = FirmwarePermissions
|
38
|
+
DEVICE = DevicePermissions
|
39
|
+
TUNNEL = TunnelPermissions
|
40
|
+
|
41
|
+
@classmethod
|
42
|
+
def full(cls):
|
43
|
+
all_items = set()
|
44
|
+
for item in [cls.HOME, cls.FIRMWARE, cls.DEVICE, cls.TUNNEL]:
|
45
|
+
all_items.update(item.full())
|
46
|
+
return list(all_items)
|
47
|
+
|
48
|
+
|
49
|
+
ADMIN = Permissions.full()
|
50
|
+
MONITORING = [
|
51
|
+
*Permissions.HOME.full(),
|
52
|
+
*Permissions.FIRMWARE.full(),
|
53
|
+
*Permissions.DEVICE.full(),
|
54
|
+
]
|
55
|
+
READONLY = [Permissions.HOME.READ]
|
@@ -0,0 +1 @@
|
|
1
|
+
from .routes import router
|
@@ -0,0 +1,43 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from fastapi import APIRouter, Security
|
4
|
+
from fastapi.websockets import WebSocket, WebSocketDisconnect
|
5
|
+
from pydantic import BaseModel
|
6
|
+
from websockets.exceptions import ConnectionClosed
|
7
|
+
|
8
|
+
from goosebit.auth import validate_ws_user_permissions
|
9
|
+
from goosebit.permissions import Permissions
|
10
|
+
from goosebit.updater.manager import get_update_manager
|
11
|
+
|
12
|
+
router = APIRouter(prefix="/logs")
|
13
|
+
|
14
|
+
|
15
|
+
class RealtimeLogModel(BaseModel):
|
16
|
+
log: str | None
|
17
|
+
clear: bool = False
|
18
|
+
|
19
|
+
|
20
|
+
@router.websocket(
|
21
|
+
"/{dev_id}",
|
22
|
+
dependencies=[
|
23
|
+
Security(validate_ws_user_permissions, scopes=[Permissions.HOME.READ])
|
24
|
+
],
|
25
|
+
)
|
26
|
+
async def device_logs(websocket: WebSocket, dev_id: str):
|
27
|
+
await websocket.accept()
|
28
|
+
|
29
|
+
manager = await get_update_manager(dev_id)
|
30
|
+
|
31
|
+
async def callback(log_update):
|
32
|
+
data = RealtimeLogModel(log=log_update)
|
33
|
+
if log_update is None:
|
34
|
+
data.clear = True
|
35
|
+
data.log = ""
|
36
|
+
await websocket.send_json(dict(data))
|
37
|
+
|
38
|
+
async with manager.subscribe_log(callback):
|
39
|
+
try:
|
40
|
+
while True:
|
41
|
+
await websocket.receive()
|
42
|
+
except (WebSocketDisconnect, ConnectionClosed, RuntimeError):
|
43
|
+
pass
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from fastapi import APIRouter, Depends
|
2
|
+
|
3
|
+
from goosebit.auth import authenticate_ws_session
|
4
|
+
|
5
|
+
from . import logs
|
6
|
+
|
7
|
+
router = APIRouter(
|
8
|
+
prefix="/realtime",
|
9
|
+
dependencies=[Depends(authenticate_ws_session)],
|
10
|
+
tags=["realtime"],
|
11
|
+
)
|
12
|
+
|
13
|
+
router.include_router(logs.router)
|
goosebit/settings.py
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
from passlib.context import CryptContext
|
5
|
+
|
6
|
+
from goosebit import permissions
|
7
|
+
|
8
|
+
POLL_SECONDS = 0
|
9
|
+
POLL_MINUTES = 1
|
10
|
+
POLL_HOURS = 0
|
11
|
+
POLL_TIME = ":".join(
|
12
|
+
[
|
13
|
+
str(POLL_HOURS).rjust(2, "0"),
|
14
|
+
str(POLL_MINUTES).rjust(2, "0"),
|
15
|
+
str(POLL_SECONDS).rjust(2, "0"),
|
16
|
+
]
|
17
|
+
)
|
18
|
+
|
19
|
+
BASE_DIR = Path(__file__).resolve().parent
|
20
|
+
TOKEN_SWU_DIR = BASE_DIR.joinpath("swugen")
|
21
|
+
SWUPDATE_FILES_DIR = BASE_DIR.joinpath("swupdate")
|
22
|
+
UPDATES_DIR = BASE_DIR.joinpath("updates")
|
23
|
+
DB_LOC = BASE_DIR.joinpath("db.sqlite3")
|
24
|
+
DB_MIGRATIONS_LOC = BASE_DIR.joinpath("migrations")
|
25
|
+
DB_URI = f"sqlite:///{DB_LOC}"
|
26
|
+
|
27
|
+
SECRET = "123456789"
|
28
|
+
PWD_CXT = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
29
|
+
|
30
|
+
users = {}
|
31
|
+
|
32
|
+
|
33
|
+
@dataclass
|
34
|
+
class User:
|
35
|
+
username: str
|
36
|
+
hashed_pwd: str
|
37
|
+
permissions: list
|
38
|
+
|
39
|
+
def get_json_permissions(self):
|
40
|
+
return [str(p) for p in self.permissions]
|
41
|
+
|
42
|
+
|
43
|
+
def add_user(user: User):
|
44
|
+
users[user.username] = user
|
45
|
+
|
46
|
+
|
47
|
+
# User configuration
|
48
|
+
add_user(
|
49
|
+
User(
|
50
|
+
username="admin@goosebit.local",
|
51
|
+
hashed_pwd=PWD_CXT.hash("admin"),
|
52
|
+
permissions=permissions.ADMIN,
|
53
|
+
)
|
54
|
+
)
|
55
|
+
USERS = users
|
goosebit/ui/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
from .routes import router
|