better response codes for missing device

dev
Anton Franzluebbers 2023-06-22 15:49:30 -04:00
parent 54c9722048
commit f058604600
3 changed files with 217 additions and 114 deletions

20
velconnect/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: FastAPI",
"type": "python",
"request": "launch",
"module": "uvicorn",
"args": [
"main:app",
"--reload"
],
"jinja": true,
"justMyCode": true
}
]
}

View File

@ -11,13 +11,14 @@ class DB:
create = False create = False
if not os.path.exists(self.db_name): if not os.path.exists(self.db_name):
create = True create = True
os.makedirs(os.path.dirname(self.db_name))
conn = sqlite3.connect(self.db_name) conn = sqlite3.connect(self.db_name)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
curr = conn.cursor() curr = conn.cursor()
if create: if create:
# create the db # create the db
with open('CreateDB.sql', 'r') as f: with open("CreateDB.sql", "r") as f:
curr.executescript(f.read()) curr.executescript(f.read())
conn.commit() conn.commit()

View File

@ -51,11 +51,11 @@ async def read_root():
def parse_data(device: dict): def parse_data(device: dict):
if 'data' in device and device['data'] is not None and len(device['data']) > 0: if "data" in device and device["data"] is not None and len(device["data"]) > 0:
device['data'] = json.loads(device['data']) device["data"] = json.loads(device["data"])
@router.get('/get_all_users') @router.get("/get_all_users")
def get_all_users(): def get_all_users():
"""Returns a list of all devices and details associated with them.""" """Returns a list of all devices and details associated with them."""
values = db.query("SELECT * FROM `User`;") values = db.query("SELECT * FROM `User`;")
@ -65,7 +65,7 @@ def get_all_users():
return values return values
@router.get('/get_all_devices') @router.get("/get_all_devices")
def get_all_devices(): def get_all_devices():
"""Returns a list of all devices and details associated with them.""" """Returns a list of all devices and details associated with them."""
values = db.query("SELECT * FROM `Device`;") values = db.query("SELECT * FROM `Device`;")
@ -75,25 +75,29 @@ def get_all_devices():
return values return values
@router.get('/get_user_by_pairing_code/{pairing_code}') @router.get("/get_user_by_pairing_code/{pairing_code}")
def get_user_by_pairing_code(pairing_code: str): def get_user_by_pairing_code(pairing_code: str, response: Response):
device = get_device_by_pairing_code_dict(pairing_code) device = get_device_by_pairing_code_dict(pairing_code)
if device is not None: if device is not None:
return device return device
return {'error': 'Not found'}, 400 response.status_code = status.HTTP_404_NOT_FOUND
return {"error": "User not found"}
@router.get('/get_device_by_pairing_code/{pairing_code}') @router.get("/get_device_by_pairing_code/{pairing_code}")
def get_device_by_pairing_code(pairing_code: str): def get_device_by_pairing_code(pairing_code: str, response: Response):
device = get_device_by_pairing_code_dict(pairing_code) device = get_device_by_pairing_code_dict(pairing_code)
if device is not None: if device is not None:
return device return device
return {'error': 'Not found'}, 400 response.status_code = status.HTTP_404_NOT_FOUND
return {"error": "Device not found"}
def get_device_by_pairing_code_dict(pairing_code: str) -> dict | None: def get_device_by_pairing_code_dict(pairing_code: str) -> dict | None:
values = db.query( values = db.query(
"SELECT * FROM `Device` WHERE `pairing_code`=:pairing_code;", {'pairing_code': pairing_code}) "SELECT * FROM `Device` WHERE `pairing_code`=:pairing_code;",
{"pairing_code": pairing_code},
)
if len(values) == 1: if len(values) == 1:
device = dict(values[0]) device = dict(values[0])
parse_data(device) parse_data(device)
@ -103,9 +107,10 @@ def get_device_by_pairing_code_dict(pairing_code: str) -> dict | None:
def get_user_for_device(hw_id: str) -> dict: def get_user_for_device(hw_id: str) -> dict:
values = db.query( values = db.query(
"""SELECT * FROM `UserDevice` WHERE `hw_id`=:hw_id;""", {'hw_id': hw_id}) """SELECT * FROM `UserDevice` WHERE `hw_id`=:hw_id;""", {"hw_id": hw_id}
)
if len(values) == 1: if len(values) == 1:
user_id = dict(values[0])['user_id'] user_id = dict(values[0])["user_id"]
user = get_user_dict(user_id=user_id) user = get_user_dict(user_id=user_id)
else: else:
# create new user instead # create new user instead
@ -117,110 +122,129 @@ def get_user_for_device(hw_id: str) -> dict:
# creates a user with a device autoattached # creates a user with a device autoattached
def create_user(hw_id: str) -> dict | None: def create_user(hw_id: str) -> dict | None:
user_id = str(uuid.uuid4()) user_id = str(uuid.uuid4())
if not db.insert("""INSERT INTO `User`(id) VALUES (:user_id);""", {'user_id': user_id}): if not db.insert(
"""INSERT INTO `User`(id) VALUES (:user_id);""", {"user_id": user_id}
):
return None return None
if not db.insert("""INSERT INTO `UserDevice`(user_id, hw_id) VALUES (:user_id, :hw_id); """, if not db.insert(
{'user_id': user_id, 'hw_id': hw_id}): """INSERT INTO `UserDevice`(user_id, hw_id) VALUES (:user_id, :hw_id); """,
{"user_id": user_id, "hw_id": hw_id},
):
return None return None
return get_user_for_device(hw_id) return get_user_for_device(hw_id)
def create_device(hw_id: str): def create_device(hw_id: str):
db.insert(""" db.insert(
"""
INSERT OR IGNORE INTO `Device`(hw_id) VALUES (:hw_id); INSERT OR IGNORE INTO `Device`(hw_id) VALUES (:hw_id);
""", {'hw_id': hw_id}) """,
{"hw_id": hw_id},
)
@router.get('/device/get_data/{hw_id}') @router.get("/device/get_data/{hw_id}")
def get_device_data(request: Request, response: Response, hw_id: str): def get_device_data(request: Request, response: Response, hw_id: str):
"""Gets the device state""" """Gets the device state"""
devices = db.query(""" devices = db.query(
"""
SELECT * FROM `Device` WHERE `hw_id`=:hw_id; SELECT * FROM `Device` WHERE `hw_id`=:hw_id;
""", {'hw_id': hw_id}) """,
{"hw_id": hw_id},
)
if len(devices) == 0: if len(devices) == 0:
response.status_code = status.HTTP_404_NOT_FOUND response.status_code = status.HTTP_404_NOT_FOUND
return {'error': "Can't find device with that id."} return {"error": "Can't find device with that id."}
block = dict(devices[0]) block = dict(devices[0])
if 'data' in block and block['data'] is not None: if "data" in block and block["data"] is not None:
block['data'] = json.loads(block['data']) block["data"] = json.loads(block["data"])
user = get_user_for_device(hw_id) user = get_user_for_device(hw_id)
room_key: str = f"{devices[0]['current_app']}_{devices[0]['current_room']}" room_key: str = f"{devices[0]['current_app']}_{devices[0]['current_room']}"
room_data = get_data(response, key=room_key, user_id=user['id']) room_data = get_data(response, key=room_key, user_id=user["id"])
if "error" in room_data: if "error" in room_data:
response.status_code = None # this really isn't an error, so we reset the status code response.status_code = (
set_data(request, data={}, key=room_key, None # this really isn't an error, so we reset the status code
modified_by=None, category="room") )
room_data = get_data(response, key=room_key, user_id=user['id']) set_data(request, data={}, key=room_key, modified_by=None, category="room")
room_data = get_data(response, key=room_key, user_id=user["id"])
return {'device': block, 'room': room_data, 'user': user} return {"device": block, "room": room_data, "user": user}
@router.post('/device/set_data/{hw_id}') @router.post("/device/set_data/{hw_id}")
def set_device_data(request: fastapi.Request, hw_id: str, data: dict, modified_by: str = None): def set_device_data(
request: fastapi.Request, hw_id: str, data: dict, modified_by: str = None
):
"""Sets the device state""" """Sets the device state"""
create_device(hw_id) create_device(hw_id)
# add the client's IP address if no sender specified # add the client's IP address if no sender specified
if 'modified_by' in data: if "modified_by" in data:
modified_by = data['modified_by'] modified_by = data["modified_by"]
if modified_by is None: if modified_by is None:
modified_by: str = str(request.client) + "_" + str(request.headers) modified_by: str = str(request.client) + "_" + str(request.headers)
allowed_keys: list[str] = [ allowed_keys: list[str] = [
'os_info', "os_info",
'friendly_name', "friendly_name",
'current_app', "current_app",
'current_room', "current_room",
'pairing_code', "pairing_code",
] ]
for key in data: for key in data:
if key in allowed_keys: if key in allowed_keys:
db.insert(f""" db.insert(
f"""
UPDATE `Device` UPDATE `Device`
SET {key}=:value, SET {key}=:value,
last_modified=CURRENT_TIMESTAMP, last_modified=CURRENT_TIMESTAMP,
modified_by=:modified_by modified_by=:modified_by
WHERE `hw_id`=:hw_id; WHERE `hw_id`=:hw_id;
""", """,
{ {"value": data[key], "hw_id": hw_id, "modified_by": modified_by},
'value': data[key], )
'hw_id': hw_id,
'modified_by': modified_by
})
if key == "data": if key == "data":
new_data = data['data'] new_data = data["data"]
# get the old json values and merge the data # get the old json values and merge the data
old_data_query = db.query(""" old_data_query = db.query(
"""
SELECT data SELECT data
FROM `Device` FROM `Device`
WHERE hw_id=:hw_id WHERE hw_id=:hw_id
""", {"hw_id": hw_id}) """,
{"hw_id": hw_id},
)
if len(old_data_query) == 1: if len(old_data_query) == 1:
old_data: dict = {} old_data: dict = {}
if old_data_query[0]['data'] is not None: if old_data_query[0]["data"] is not None:
old_data = json.loads(old_data_query[0]["data"]) old_data = json.loads(old_data_query[0]["data"])
new_data = {**old_data, **new_data} new_data = {**old_data, **new_data}
# add the data to the db # add the data to the db
db.insert(""" db.insert(
"""
UPDATE `Device` UPDATE `Device`
SET data=:data, SET data=:data,
last_modified=CURRENT_TIMESTAMP last_modified=CURRENT_TIMESTAMP
WHERE hw_id=:hw_id; WHERE hw_id=:hw_id;
""", {"hw_id": hw_id, "data": json.dumps(new_data)}) """,
return {'success': True} {"hw_id": hw_id, "data": json.dumps(new_data)},
)
return {"success": True}
def generate_id(length: int = 4) -> str: def generate_id(length: int = 4) -> str:
return ''.join( return "".join(
secrets.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for i in range(length)) secrets.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits)
for i in range(length)
)
class Visibility(str, Enum): class Visibility(str, Enum):
@ -229,25 +253,38 @@ class Visibility(str, Enum):
unlisted = "unlisted" unlisted = "unlisted"
@router.post('/set_data') @router.post("/set_data")
def set_data_with_random_key(request: fastapi.Request, data: dict, owner: str, modified_by: str = None, def set_data_with_random_key(
category: str = None, visibility: Visibility = Visibility.public) -> dict: request: fastapi.Request,
data: dict,
owner: str,
modified_by: str = None,
category: str = None,
visibility: Visibility = Visibility.public,
) -> dict:
"""Creates a little storage bucket for arbitrary data with a random key""" """Creates a little storage bucket for arbitrary data with a random key"""
return set_data(request, data, None, owner, modified_by, category, visibility) return set_data(request, data, None, owner, modified_by, category, visibility)
@router.post('/set_data/{key}') @router.post("/set_data/{key}")
def set_data(request: fastapi.Request, data: dict, key: str = None, owner: str = 'none', modified_by: str = None, def set_data(
category: str = None, visibility: Visibility = Visibility.public) -> dict: request: fastapi.Request,
data: dict,
key: str = None,
owner: str = "none",
modified_by: str = None,
category: str = None,
visibility: Visibility = Visibility.public,
) -> dict:
"""Creates a little storage bucket for arbitrary data""" """Creates a little storage bucket for arbitrary data"""
# sqlite composite key isn't necessarily unique if a value is null # sqlite composite key isn't necessarily unique if a value is null
if owner == None: if owner == None:
owner = 'none' owner = "none"
# add the client's IP address if no sender specified # add the client's IP address if no sender specified
if 'modified_by' in data: if "modified_by" in data:
modified_by = data['modified_by'] modified_by = data["modified_by"]
if modified_by is None: if modified_by is None:
modified_by: str = str(request.client) + "_" + str(request.headers) modified_by: str = str(request.client) + "_" + str(request.headers)
@ -256,96 +293,128 @@ def set_data(request: fastapi.Request, data: dict, key: str = None, owner: str =
key = generate_id() key = generate_id()
# regenerate if necessary # regenerate if necessary
while len(db.query("SELECT id FROM `DataBlock` WHERE id=:id;", {"id": key})) > 0: while (
len(db.query("SELECT id FROM `DataBlock` WHERE id=:id;", {"id": key})) > 0
):
key = generate_id() key = generate_id()
# get the old json values and merge the data # get the old json values and merge the data
old_data_query = db.query(""" old_data_query = db.query(
"""
SELECT data SELECT data
FROM `DataBlock` FROM `DataBlock`
WHERE id=:id WHERE id=:id
""", {"id": key}) """,
{"id": key},
)
if len(old_data_query) == 1: if len(old_data_query) == 1:
old_data: dict = json.loads(old_data_query[0]["data"]) old_data: dict = json.loads(old_data_query[0]["data"])
data = {**old_data, **data} data = {**old_data, **data}
# add the data to the db # add the data to the db
db.insert(""" db.insert(
"""
UPDATE `DataBlock` SET UPDATE `DataBlock` SET
category = :category, category = :category,
modified_by = :modified_by, modified_by = :modified_by,
data = :data, data = :data,
last_modified = CURRENT_TIMESTAMP last_modified = CURRENT_TIMESTAMP
WHERE id=:id AND owner_id = :owner_id; WHERE id=:id AND owner_id = :owner_id;
""", {"id": key, "category": category, "modified_by": modified_by, "owner_id": owner, "data": json.dumps(data)}) """,
{
"id": key,
"category": category,
"modified_by": modified_by,
"owner_id": owner,
"data": json.dumps(data),
},
)
else: else:
# add the data to the db # add the data to the db
db.insert(""" db.insert(
"""
INSERT INTO `DataBlock` (id, owner_id, category, modified_by, data, last_modified) INSERT INTO `DataBlock` (id, owner_id, category, modified_by, data, last_modified)
VALUES(:id, :owner_id, :category, :modified_by, :data, CURRENT_TIMESTAMP); VALUES(:id, :owner_id, :category, :modified_by, :data, CURRENT_TIMESTAMP);
""", {"id": key, "owner_id": owner, "category": category, "modified_by": modified_by, "data": json.dumps(data)}) """,
{
"id": key,
"owner_id": owner,
"category": category,
"modified_by": modified_by,
"data": json.dumps(data),
},
)
return {'key': key} return {"key": key}
@router.get('/get_data/{key}') @router.get("/get_data/{key}")
def get_data(response: Response, key: str, user_id: str = None) -> dict: def get_data(response: Response, key: str, user_id: str = None) -> dict:
"""Gets data from a storage bucket for arbitrary data""" """Gets data from a storage bucket for arbitrary data"""
data = db.query(""" data = db.query(
"""
SELECT * SELECT *
FROM `DataBlock` FROM `DataBlock`
WHERE id=:id WHERE id=:id
""", {"id": key}) """,
{"id": key},
)
db.insert(""" db.insert(
"""
UPDATE `DataBlock` UPDATE `DataBlock`
SET last_accessed = CURRENT_TIMESTAMP SET last_accessed = CURRENT_TIMESTAMP
WHERE id=:id; WHERE id=:id;
""", {"id": key}) """,
{"id": key},
)
try: try:
if len(data) == 1: if len(data) == 1:
block = dict(data[0]) block = dict(data[0])
if 'data' in block and block['data'] is not None: if "data" in block and block["data"] is not None:
block['data'] = json.loads(block['data']) block["data"] = json.loads(block["data"])
if not has_permission(block, user_id): if not has_permission(block, user_id):
response.status_code = status.HTTP_401_UNAUTHORIZED response.status_code = status.HTTP_401_UNAUTHORIZED
return {'error': 'Not authorized to see that data.'} return {"error": "Not authorized to see that data."}
replace_userid_with_name(block) replace_userid_with_name(block)
return block return block
response.status_code = status.HTTP_404_NOT_FOUND response.status_code = status.HTTP_404_NOT_FOUND
return {'error': 'Not found'} return {"error": "Not found"}
except Exception as e: except Exception as e:
print(e) print(e)
response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
return {'error': 'Unknown. Maybe no data at this key.'} return {"error": "Unknown. Maybe no data at this key."}
def has_permission(data_block: dict, user_uuid: str) -> bool: def has_permission(data_block: dict, user_uuid: str) -> bool:
# if the data is public by visibility # if the data is public by visibility
if data_block['visibility'] == Visibility.public or data_block['visibility'] == Visibility.unlisted: if (
data_block["visibility"] == Visibility.public
or data_block["visibility"] == Visibility.unlisted
):
return True return True
# public domain data # public domain data
elif data_block['owner_id'] is None: elif data_block["owner_id"] is None:
return True return True
# if we are the owner # if we are the owner
elif data_block['owner_id'] == user_uuid: elif data_block["owner_id"] == user_uuid:
return True return True
else: else:
return False return False
def replace_userid_with_name(data_block: dict): def replace_userid_with_name(data_block: dict):
if data_block['owner_id'] is not None: if data_block["owner_id"] is not None:
user = get_user_dict(data_block['owner_id']) user = get_user_dict(data_block["owner_id"])
if user is not None: if user is not None:
data_block['owner_name'] = user['username'] data_block["owner_name"] = user["username"]
del data_block['owner_id'] del data_block["owner_id"]
@router.get('/user/get_data/{user_id}') @router.get("/user/get_data/{user_id}")
def get_user(response: Response, user_id: str): def get_user(response: Response, user_id: str):
user = get_user_dict(user_id) user = get_user_dict(user_id)
if user is None: if user is None:
@ -355,8 +424,7 @@ def get_user(response: Response, user_id: str):
def get_user_dict(user_id: str) -> dict | None: def get_user_dict(user_id: str) -> dict | None:
values = db.query( values = db.query("SELECT * FROM `User` WHERE `id`=:user_id;", {"user_id": user_id})
"SELECT * FROM `User` WHERE `id`=:user_id;", {'user_id': user_id})
if len(values) == 1: if len(values) == 1:
user = dict(values[0]) user = dict(values[0])
parse_data(user) parse_data(user)
@ -365,30 +433,41 @@ def get_user_dict(user_id: str) -> dict | None:
@router.post("/upload_file") @router.post("/upload_file")
async def upload_file_with_random_key(request: fastapi.Request, file: UploadFile, modified_by: str = None): async def upload_file_with_random_key(
request: fastapi.Request, file: UploadFile, modified_by: str = None
):
return await upload_file(request, file, None, modified_by) return await upload_file(request, file, None, modified_by)
@router.post("/upload_file/{key}") @router.post("/upload_file/{key}")
async def upload_file(request: fastapi.Request, file: UploadFile, key: str | None, modified_by: str = None): async def upload_file(
if not os.path.exists('data'): request: fastapi.Request, file: UploadFile, key: str | None, modified_by: str = None
os.makedirs('data') ):
if not os.path.exists("data"):
os.makedirs("data")
# generates a key if none was supplied # generates a key if none was supplied
if key is None: if key is None:
key = generate_id() key = generate_id()
# regenerate if necessary # regenerate if necessary
while len(db.query("SELECT id FROM `DataBlock` WHERE id=:id;", {"id": key})) > 0: while (
len(db.query("SELECT id FROM `DataBlock` WHERE id=:id;", {"id": key})) > 0
):
key = generate_id() key = generate_id()
async with aiofiles.open('data/' + key, 'wb') as out_file: async with aiofiles.open("data/" + key, "wb") as out_file:
content = await file.read() # async read content = await file.read() # async read
await out_file.write(content) # async write await out_file.write(content) # async write
# add a datablock to link to the file # add a datablock to link to the file
set_data(request, data={'filename': file.filename}, set_data(
key=key, category='file', modified_by=modified_by) request,
return {"filename": file.filename, 'key': key} data={"filename": file.filename},
key=key,
category="file",
modified_by=modified_by,
)
return {"filename": file.filename, "key": key}
@router.get("/download_file/{key}") @router.get("/download_file/{key}")
@ -397,20 +476,22 @@ async def download_file(response: Response, key: str):
data = get_data(response, key) data = get_data(response, key)
print(data) print(data)
if response.status_code == status.HTTP_404_NOT_FOUND: if response.status_code == status.HTTP_404_NOT_FOUND:
return 'Not found' return "Not found"
if data['category'] != 'file': if data["category"] != "file":
response.status_code = status.HTTP_400_BAD_REQUEST response.status_code = status.HTTP_400_BAD_REQUEST
return 'Not a file' return "Not a file"
return FileResponse(path='data/' + key, filename=data['data']['filename']) return FileResponse(path="data/" + key, filename=data["data"]["filename"])
@router.get("/get_all_files") @router.get("/get_all_files")
async def get_all_files(): async def get_all_files():
data = db.query(""" data = db.query(
"""
SELECT * SELECT *
FROM `DataBlock` FROM `DataBlock`
WHERE visibility='public' AND category='file'; WHERE visibility='public' AND category='file';
""") """
)
data = [dict(f) for f in data] data = [dict(f) for f in data]
for f in data: for f in data:
parse_data(f) parse_data(f)
@ -419,19 +500,20 @@ async def get_all_files():
@router.get("/get_all_images") @router.get("/get_all_images")
async def get_all_images(): async def get_all_images():
data = db.query(""" data = db.query(
"""
SELECT * SELECT *
FROM `DataBlock` FROM `DataBlock`
WHERE visibility='public' AND category='file'; WHERE visibility='public' AND category='file';
""") """
)
data = [dict(f) for f in data] data = [dict(f) for f in data]
for f in data: for f in data:
parse_data(f) parse_data(f)
images = [] images = []
for f in data: for f in data:
if f['data']['filename'].endswith('.png') or f['data']['filename'].endswith('.jpg'): if f["data"]["filename"].endswith(".png") or f["data"]["filename"].endswith(
images.append({ ".jpg"
'key': f['id'], ):
'filename': f['data']['filename'] images.append({"key": f["id"], "filename": f["data"]["filename"]})
})
return images return images