creating user with uuid, mapping users to devices

dev
Anton Franzluebbers 2022-09-05 15:20:40 -04:00
parent 11260d9372
commit a945e620e1
2 changed files with 135 additions and 27 deletions

View File

@ -20,10 +20,12 @@ CREATE TABLE `UserCount` (
PRIMARY KEY (`timestamp`, `hw_id`) PRIMARY KEY (`timestamp`, `hw_id`)
); );
CREATE TABLE `User` ( CREATE TABLE `User` (
-- TODO user is defined by uuid, to which an email can be added without having to migrate. -- user is defined by uuid, to which an email can be added without having to migrate.
-- then the data that is coming from a user vs device is constant -- then the data that is coming from a user vs device is constant
-- UUID
`id` TEXT NOT NULL PRIMARY KEY,
-- the user's email -- the user's email
`email` TEXT NOT NULL PRIMARY KEY, `email` TEXT,
`username` TEXT, `username` TEXT,
-- the first time this device was seen -- the first time this device was seen
`date_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `date_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@ -33,20 +35,21 @@ CREATE TABLE `User` (
`data` TEXT `data` TEXT
); );
CREATE TABLE `UserDevice` ( CREATE TABLE `UserDevice` (
-- Unique identifier for the device -- the user account's uuid
`hw_id` TEXT NOT NULL, `user_id` TEXT NOT NULL,
-- the user's email -- identifier for the device
`email` TEXT NOT NULL, -- This is unique because a device can have only one owner
`hw_id` TEXT NOT NULL UNIQUE,
-- when this connection was created -- when this connection was created
`date_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `date_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`hw_id`, `email`) PRIMARY KEY (`user_id`, `hw_id`)
); );
CREATE TABLE `Device` ( CREATE TABLE `Device` (
-- Unique identifier for this device -- Unique identifier for this device
`hw_id` TEXT NOT NULL PRIMARY KEY, `hw_id` TEXT NOT NULL PRIMARY KEY,
-- info about the hardware. Would specify Quest or Windows for example -- info about the hardware. Would specify Quest or Windows for example
`os_info` TEXT, `os_info` TEXT,
-- A human-readable name for this device. Could be a username -- A human-readable name for this device. Not a username for the game
`friendly_name` TEXT, `friendly_name` TEXT,
-- The last source to change this object. Generally this is the device id -- The last source to change this object. Generally this is the device id
`modified_by` TEXT, `modified_by` TEXT,
@ -66,7 +69,11 @@ CREATE TABLE `Device` (
CREATE TABLE `DataBlock` ( CREATE TABLE `DataBlock` (
-- Could be randomly generated. For room data, this is 'appId_roomName' -- Could be randomly generated. For room data, this is 'appId_roomName'
`id` TEXT NOT NULL PRIMARY KEY, `id` TEXT NOT NULL,
-- id of the owner of this file. Ownership is not transferable because ids may collide,
-- but the owner could be null for global scope
`owner_id` TEXT,
`visibility` ENUM('public', 'private', 'unlisted') NOT NULL DEFAULT 'public',
-- This is an indexable field to filter out different types of datablocks -- This is an indexable field to filter out different types of datablocks
`category` TEXT, `category` TEXT,
`date_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `date_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@ -76,5 +83,6 @@ CREATE TABLE `DataBlock` (
-- the last time this data was fetched individually -- the last time this data was fetched individually
`last_accessed` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `last_accessed` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- JSON containing arbitrary data -- JSON containing arbitrary data
`data` TEXT `data` TEXT,
PRIMARY KEY (`id`, `owner_id`)
); );

View File

@ -2,10 +2,12 @@ import secrets
import json import json
import string import string
import aiofiles import aiofiles
import uuid
import fastapi import fastapi
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse, FileResponse
from fastapi import FastAPI, File, UploadFile from fastapi import FastAPI, File, UploadFile, Response, Request, status
from enum import Enum
import db import db
@ -47,7 +49,7 @@ async def read_root():
""" """
def parse_device(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'])
@ -58,21 +60,58 @@ def get_all_devices():
values = db.query("SELECT * FROM `Device`;") values = db.query("SELECT * FROM `Device`;")
values = [dict(v) for v in values] values = [dict(v) for v in values]
for device in values: for device in values:
parse_device(device) parse_data(device)
return values return values
@router.get('/get_user_by_pairing_code/{pairing_code}')
def get_user_by_pairing_code(pairing_code: str):
device = get_device_by_pairing_code_dict(pairing_code)
if device is not None:
return device
return {'error': 'Not found'}, 400
@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):
values = db.query("SELECT * FROM `Device` WHERE `pairing_code`=:pairing_code;", device = get_device_by_pairing_code_dict(pairing_code)
{'pairing_code': pairing_code}) if device is not None:
if len(values) == 1:
device = dict(values[0])
parse_device(device)
return device return device
return {'error': 'Not found'}, 400 return {'error': 'Not found'}, 400
def get_device_by_pairing_code_dict(pairing_code: str) -> dict | None:
values = db.query("SELECT * FROM `Device` WHERE `pairing_code`=:pairing_code;", {'pairing_code': pairing_code})
if len(values) == 1:
device = dict(values[0])
parse_data(device)
return device
return None
def get_user_for_device(hw_id: str) -> dict:
values = db.query("""SELECT * FROM `UserDevice` WHERE `hw_id`=:hw_id;""", {'hw_id': hw_id})
if len(values) == 1:
user = dict(values[0])
parse_data(user)
return user
# create new user instead
else:
user = create_user(hw_id)
# creates a user with a device autoattached
def create_user(hw_id: str) -> dict | None:
user_id = uuid.uuid4()
if not db.insert("""INSERT INTO `User`(id) VALUES (:user_id);
""", {'user_id': user_id}):
return None
if not db.insert("""INSERT INTO `UserDevice`(user_id, hw_id) VALUES (:user_id, :hw_id);
""", {'user_id': user_id, 'hw_id': hw_id}):
return None
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);
@ -166,16 +205,22 @@ def generate_id(length: int = 4) -> str:
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):
public = "public"
private = "private"
unlisted = "unlisted"
@router.post('/set_data') @router.post('/set_data')
def set_data_with_random_key(request: fastapi.Request, data: dict, modified_by: str = None, def set_data_with_random_key(request: fastapi.Request, data: dict, owner: str, modified_by: str = None,
category: str = None) -> dict: 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, modified_by, category) 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, modified_by: str = None, def set_data(request: fastapi.Request, data: dict, key: str = None, owner: str = None, modified_by: str = None,
category: str = None) -> dict: category: str = None, visibility: Visibility = Visibility.public) -> dict:
"""Creates a little storage bucket for arbitrary data""" """Creates a little storage bucket for arbitrary data"""
# add the client's IP address if no sender specified # add the client's IP address if no sender specified
@ -213,7 +258,7 @@ def set_data(request: fastapi.Request, data: dict, key: str = None, modified_by:
@router.get('/get_data/{key}') @router.get('/get_data/{key}')
def get_data(key: str) -> 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("""
@ -233,18 +278,73 @@ def get_data(key: str) -> dict:
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):
response.status_code = status.HTTP_401_UNAUTHORIZED
return {'error': 'Not authorized to see that data.'}
replace_userid_with_name(block)
return block return block
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
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:
# if the data is public by visibility
if data_block['visibility'] == Visibility.public or data_block['visibility'] == Visibility.unlisted:
return True
# public domain data
elif data_block['owner_id'] is None:
return True
# if we are the owner
elif data_block['owner_id'] == user_uuid:
return True
else:
return False
def replace_userid_with_name(data_block: dict):
if data_block['owner_id'] is not None:
user = get_user_dict(data_block['owner_id'])
data_block['owner_name'] = user['username']
del data_block['owner_id']
@router.get('/user/get_data/{user_id}')
def get_user(response: Response, user_id: str):
user = get_user_dict(user_id)
if user is None:
response.status_code = status.HTTP_404_BAD_REQUEST
return {"error": "User not found"}
return user
def get_user_dict(user_id: str) -> dict | None:
values = db.query("SELECT * FROM `User` WHERE `id`=:user_id;", {'user_id': user_id})
if len(values) == 1:
user = dict(values[0])
parse_data(user)
return user
return None
@router.post("/upload_file/{key}") @router.post("/upload_file/{key}")
async def upload_file(request: fastapi.Request, file: UploadFile, key: str, modified_by: str = None): async def upload_file(request: fastapi.Request, file: UploadFile, key: str, modified_by: str = None):
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, {'filename': file.filename}, key, 'file') set_data(request, {'filename': file.filename}, key, 'file', modified_by)
return {"filename": file.filename} return {"filename": file.filename}
@router.get("/download_file/{key}")
async def download_file(key: str):
# get the relevant datablock
data = get_data(key)
print(data)
if data['category'] != 'file':
return 'Not a file', 500
return fastapi.FileResponse(data['data']['filename'])