Improvements upon front lead to improvements here

This commit is contained in:
Andrew 2023-04-25 15:08:36 +07:00
parent 3414b5c334
commit 34ae028698
6 changed files with 395 additions and 169 deletions

348
app.py
View file

@ -16,13 +16,11 @@ from models import (
AuthModel,
ColumnConditionCompat,
CreateUserDefinition,
ItemsFieldSelectorList,
TableDefinition,
UserUpdateDefinition,
OkResponse,
ErrorResponse,
AccessTokenResponse,
TableItemsResponse,
CreateAssetResponse,
)
from utils import (
@ -74,7 +72,7 @@ async def getAccessToken(userData: AuthModel):
user = check_user(connector, userData.username, userData.password)
if not user:
return JSONResponse(
ErrorResponse(error="Wrong username or password"),
ErrorResponse(error="Wrong username or password").dict(),
status_code=status.HTTP_401_UNAUTHORIZED,
)
@ -92,14 +90,12 @@ async def getAccessToken(userData: AuthModel):
},
},
)
async def listTables(
response: Response,
access_token: str | None = Header(default=None),
):
async def listTables(access_token: str | None = Header(default=None)):
is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin:
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
return [TableDefinition.parse_obj(table) for table in connector.tables()]
@ -135,12 +131,14 @@ async def createTable(
`column_name:column_type[:column_options]`
Where *column_type* should be one of the following:
- serial
- serial:primary
- str
- bool
- datetime
- float
- int
- int-asset
- int-user
Also *column_options* can be one of the following:
- unique
@ -154,33 +152,40 @@ async def createTable(
"description:str",
"is_active:bool",
"price:float",
"quantity:int"
"quantity:int",
"creator_id:int-user",
"asset_id:int-asset"
]
```
Notes:
- you cannot use *unique* and *default* at the same time
- in current implementation you cannot use *default*, because there is no way to
1. you cannot use *unique* and *default* at the same time
2. in current implementation you cannot use *default*, because there is no way to
specify default value
"""
is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin:
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
try:
columnsDefinition = parse_columns_from_definition(",".join(columns))
create_table(connector, tableName, columnsDefinition)
ok, e = create_table(connector, tableName, columnsDefinition)
if not ok:
if e:
raise e
raise Exception("Unknown error")
except psycopg.errors.UniqueViolation:
return JSONResponse(
ErrorResponse(error="Table already exists"),
ErrorResponse(error="Table already exists").dict(),
status_code=status.HTTP_409_CONFLICT,
)
except Exception as e:
return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST
)
return OkResponse()
@ -208,7 +213,8 @@ async def dropTable(
is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin:
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
try:
@ -219,14 +225,14 @@ async def dropTable(
raise Exception("Unknown error")
except Exception as e:
return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST
)
return OkResponse()
@app.post(
"/api/createUser",
"/api/users/+",
name="Create user",
responses={
200: {"model": OkResponse, "description": "Table dropped successfully"},
@ -251,7 +257,8 @@ async def createUser(
is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin:
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
try:
@ -262,19 +269,19 @@ async def createUser(
raise Exception("Unknown error")
except psycopg.errors.UniqueViolation:
return JSONResponse(
ErrorResponse(error="Username already exists"),
ErrorResponse(error="Username already exists").dict(),
status_code=status.HTTP_409_CONFLICT,
)
except Exception as e:
return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST
)
return OkResponse()
@app.post(
"/api/updateUser",
"/api/users/*",
name="Update user",
responses={
200: {"model": OkResponse, "description": "Table dropped successfully"},
@ -295,7 +302,8 @@ async def updateUser(
is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin:
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
try:
@ -306,7 +314,62 @@ async def updateUser(
raise Exception("Unknown error")
except Exception as e:
return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST
)
return OkResponse()
@app.post(
"/api/users/{user_id}/-",
name="Remove user",
responses={
200: {"model": OkResponse, "description": "Table dropped successfully"},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during user creation",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires admin-level user access token",
},
409: {
"model": ErrorResponse,
"description": "User with this username already exists",
},
},
)
async def removeUser(
user_id: int,
access_token: str | None = Header(default=None),
):
is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin:
return JSONResponse(
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
try:
user = get_user_by_id(connector, user_id)
if not user:
raise Exception("User not found")
elif user.access_token == access_token:
raise Exception("Cannot remove yourself")
ok, e = delete_user(connector, user_id)
if not ok:
if e:
raise e
raise Exception("Unknown error")
except psycopg.errors.UniqueViolation:
return JSONResponse(
ErrorResponse(error="Username already exists").dict(),
status_code=status.HTTP_409_CONFLICT,
)
except Exception as e:
return JSONResponse(
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST
)
return OkResponse()
@ -316,7 +379,7 @@ async def updateUser(
"/items/{tableName}",
name="Get items from table",
responses={
200: {"model": TableItemsResponse, "description": "Table items"},
200: {"model": list[dict[str, Any]], "description": "Table items"},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during getting table items",
@ -333,27 +396,31 @@ async def updateUser(
)
async def items(
tableName: str,
selector: ItemsFieldSelectorList,
fields: list[str] = ["*"],
where: list[ColumnConditionCompat] = [],
access_token: str | None = Header(default=None),
):
table_info = connector.getTable(tableName)
if not table_info:
return JSONResponse(
ErrorResponse(error="Table not found"),
ErrorResponse(error="Table not found").dict(),
status_code=status.HTTP_404_NOT_FOUND,
)
is_admin = check_if_admin_access_token(connector, access_token)
if table_info["system"] and not is_admin:
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
columns = parse_columns_from_definition(table_info["columns"])
columnsNames = set(column.name for column in columns)
userSelectedColumns = list(set(selector.fields)) if selector.fields else ["*"]
if userSelectedColumns != ["*"]:
for column in userSelectedColumns:
if fields == ["*"]:
fields = list(columnsNames)
else:
for column in fields:
if column not in columnsNames:
return JSONResponse(
ErrorResponse(
@ -361,39 +428,54 @@ async def items(
),
status_code=status.HTTP_404_NOT_FOUND,
)
else:
userSelectedColumns = list(columnsNames)
user, group = get_user_by_access_token(connector, access_token)
if not user:
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
if where:
for key in where:
if key.column not in columnsNames:
return JSONResponse(
ErrorResponse(error=f"Column {key} not found on table {tableName}"),
status_code=status.HTTP_404_NOT_FOUND,
)
_, group = get_user_by_access_token(connector, access_token)
if not is_admin:
allowedColumns = get_allowed_columns_for_group(
connector, tableName, group.id if group else -1
connector, tableName, group.id if group else 1 # 1 is anonymous group
)
if not allowedColumns:
return JSONResponse(
ErrorResponse(error="Not allowed"),
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
elif len(allowedColumns) == 1 and allowedColumns[0] == "*":
pass
else:
for column in userSelectedColumns:
for column in fields:
if column not in allowedColumns:
return JSONResponse(
ErrorResponse(error="Not allowed"),
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
for column in where:
if column.column not in allowedColumns:
return JSONResponse(
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
table_items = connector.selectFromTable(
tableName, selector.fields if selector.fields else ["*"]
)
try:
table_items = connector.selectFromTable(
tableName,
fields,
[ColumnCondition(w.column, w.operator, w.value) for w in where],
)
except Exception as e:
return JSONResponse(
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST
)
return TableItemsResponse(items=table_items)
return table_items
@app.post(
@ -427,14 +509,15 @@ async def itemsCreate(
table_info = connector.getTable(tableName)
if not table_info:
return JSONResponse(
ErrorResponse(error="Table not found"),
ErrorResponse(error="Table not found").dict(),
status_code=status.HTTP_404_NOT_FOUND,
)
is_admin = check_if_admin_access_token(connector, access_token)
if table_info["system"] and not is_admin:
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
user, group = get_user_by_access_token(connector, access_token)
@ -444,7 +527,7 @@ async def itemsCreate(
)
if not allowedColumns:
return JSONResponse(
ErrorResponse(error="Not allowed"),
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
elif len(allowedColumns) == 1 and allowedColumns[0] == "*":
@ -453,7 +536,7 @@ async def itemsCreate(
for column in item:
if column not in allowedColumns:
return JSONResponse(
ErrorResponse(error="Not allowed"),
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
@ -461,17 +544,17 @@ async def itemsCreate(
connector.insertIntoTable(tableName, item)
except psycopg.errors.UndefinedColumn:
return JSONResponse(
ErrorResponse(error="Column not found"),
ErrorResponse(error="Column not found").dict(),
status_code=status.HTTP_404_NOT_FOUND,
)
except psycopg.errors.UniqueViolation:
return JSONResponse(
ErrorResponse(error="Unique violation"),
ErrorResponse(error="Unique violation").dict(),
status_code=status.HTTP_409_CONFLICT,
)
except Exception as e:
return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST
)
return OkResponse()
@ -509,14 +592,14 @@ async def itemsUpdate(
table_info = connector.getTable(tableName)
if not table_info:
return JSONResponse(
ErrorResponse(error="Table not found"),
ErrorResponse(error="Table not found").dict(),
status_code=status.HTTP_404_NOT_FOUND,
)
is_admin = check_if_admin_access_token(connector, access_token)
if table_info["system"] and not is_admin:
return JSONResponse(
ErrorResponse(error="Not allowed"),
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
@ -527,7 +610,7 @@ async def itemsUpdate(
)
if not allowedColumns:
return JSONResponse(
ErrorResponse(error="Not allowed"),
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
elif len(allowedColumns) == 1 and allowedColumns[0] == "*":
@ -536,7 +619,7 @@ async def itemsUpdate(
for column in item:
if column not in allowedColumns:
return JSONResponse(
ErrorResponse(error="Not allowed"),
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
@ -551,17 +634,17 @@ async def itemsUpdate(
)
except psycopg.errors.UniqueViolation:
return JSONResponse(
ErrorResponse(error="Unique violation"),
ErrorResponse(error="Unique violation").dict(),
status_code=status.HTTP_409_CONFLICT,
)
except psycopg.errors.UndefinedColumn:
return JSONResponse(
ErrorResponse(error="Column not found"),
ErrorResponse(error="Column not found").dict(),
status_code=status.HTTP_404_NOT_FOUND,
)
except Exception as e:
return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST
)
return OkResponse()
@ -594,14 +677,14 @@ async def itemsDelete(
table_info = connector.getTable(tableName)
if not table_info:
return JSONResponse(
ErrorResponse(error="Table not found"),
ErrorResponse(error="Table not found").dict(),
status_code=status.HTTP_404_NOT_FOUND,
)
is_admin = check_if_admin_access_token(connector, access_token)
if table_info["system"] and not is_admin:
return JSONResponse(
ErrorResponse(error="Not allowed"),
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
@ -612,14 +695,14 @@ async def itemsDelete(
)
if not allowedColumns:
return JSONResponse(
ErrorResponse(error="Not allowed"),
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
elif len(allowedColumns) == 1 and allowedColumns[0] == "*":
pass
else:
return JSONResponse(
ErrorResponse(error="Not allowed"),
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
@ -630,7 +713,7 @@ async def itemsDelete(
)
except Exception as e:
return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST
)
return OkResponse()
@ -649,8 +732,8 @@ async def itemsDelete(
},
},
)
async def getAsset(fid: str, access_token: str | None = Header(default=None)):
asset = get_asset(connector, access_token, fid)
async def getAsset(fid: str):
asset = get_asset(connector, fid)
if not asset:
return status.HTTP_404_NOT_FOUND
@ -700,7 +783,7 @@ async def createAsset(
user, _ = get_user_by_access_token(connector, access_token)
if not user:
return JSONResponse(
ErrorResponse(error="Not allowed"),
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
@ -720,18 +803,139 @@ async def createAsset(
)
if not result:
return JSONResponse(
ErrorResponse(error="Failed put asset into storage"),
ErrorResponse(error="Failed put asset into storage").dict(),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
if not create_asset(connector, filename, "", str(result.version_id)):
ok, e = create_asset(connector, filename, "", str(result.version_id))
if not ok:
if e:
return JSONResponse(
ErrorResponse(error=str(e)).dict(),
status_code=status.HTTP_400_BAD_REQUEST,
)
return JSONResponse(
ErrorResponse(error="Failed to create asset"),
ErrorResponse(error="Failed to create asset").dict(),
status_code=status.HTTP_400_BAD_REQUEST,
)
return CreateAssetResponse(fid=result.version_id)
@app.post(
"/assets/{asset_id}/*",
name="Update asset description",
responses={
200: {
"model": OkResponse,
"description": "Asset description updated successfully",
},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during updating asset",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires user access token",
},
404: {
"model": ErrorResponse,
"description": "Asset not found",
},
},
)
async def updateAsset(
asset_id: int,
asset_description: str,
access_token: str | None = Header(default=None),
):
user = get_user_by_access_token(connector, access_token)
if not user:
return JSONResponse(
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
ok, e = update_asset(connector, asset_id, asset_description)
if not ok:
if e:
return JSONResponse(
ErrorResponse(error=str(e)).dict(),
status_code=status.HTTP_400_BAD_REQUEST,
)
return JSONResponse(
ErrorResponse(error="Asset not found").dict(),
status_code=status.HTTP_404_NOT_FOUND,
)
return OkResponse()
@app.post(
"/assets/{asset_id}/-",
name="Remove asset",
responses={
200: {
"model": OkResponse,
"description": "Asset removed successfully",
},
400: {
"model": ErrorResponse,
"description": "Something went wrong during removing asset",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires user access token",
},
404: {
"model": ErrorResponse,
"description": "Asset not found",
},
},
)
async def removeAsset(
asset_id: int,
check_references: bool = True,
delete_referencing: bool = False,
access_token: str | None = Header(default=None),
):
user = get_user_by_access_token(connector, access_token)
if not user:
return JSONResponse(
ErrorResponse(error="Not allowed").dict(),
status_code=status.HTTP_403_FORBIDDEN,
)
asset = get_asset_by_id(connector, asset_id)
if not asset:
return JSONResponse(
ErrorResponse(error="Asset not found").dict(),
status_code=status.HTTP_404_NOT_FOUND,
)
try:
minioClient.remove_object(BUCKET_NAME, asset.fid)
except Exception as e:
logger.error(f"Failed to remove asset from storage: {e}")
return JSONResponse(
ErrorResponse(error=str(e)).dict(),
status_code=status.HTTP_400_BAD_REQUEST,
)
ok, e = remove_asset(connector, asset_id, check_references, delete_referencing)
if not ok:
if e:
return JSONResponse(
ErrorResponse(error=str(e)).dict(),
status_code=status.HTTP_400_BAD_REQUEST,
)
return JSONResponse(
ErrorResponse(error="Unknown error").dict(),
status_code=status.HTTP_400_BAD_REQUEST,
)
return OkResponse()
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)