What is a lot of fucking work!

This commit is contained in:
Andrew 2023-04-23 16:35:02 +07:00
parent 85c07ed4f3
commit 3414b5c334
8 changed files with 586 additions and 216 deletions

549
app.py
View file

@ -1,7 +1,7 @@
import io
from typing import Any
from fastapi import FastAPI, status, Header, UploadFile, Response
from starlette.responses import StreamingResponse
from starlette.responses import StreamingResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from based import db
import psycopg
@ -14,13 +14,16 @@ import uvicorn
from dba import *
from models import (
AuthModel,
ColumnsDefinitionList,
ErrorResponseDefinition,
ItemDeletionDefinitionList,
ColumnConditionCompat,
CreateUserDefinition,
ItemsFieldSelectorList,
TableDefinition,
TableListDefinition,
UserDefinition,
UserUpdateDefinition,
OkResponse,
ErrorResponse,
AccessTokenResponse,
TableItemsResponse,
CreateAssetResponse,
)
from utils import (
check_if_admin_access_token,
@ -45,7 +48,11 @@ if found:
else:
minioClient.make_bucket(BUCKET_NAME)
app = FastAPI()
app = FastAPI(
title="Tuuli API",
description="Tuuli API for Tuuli frontend\n\nUse `c2316f9686e7a764688b8c1b4c60c5a088b07a3c23a2f6b7c25915a5118d7acc` as access token to test the API",
version="0.1.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
@ -55,107 +62,275 @@ app.add_middleware(
)
@app.post("/api/getAccessToken")
@app.post(
"/api/getAccessToken",
name="Get access token",
responses={
200: {"model": AccessTokenResponse, "description": "Successful response"},
401: {"model": ErrorResponse, "description": "User not found"},
},
)
async def getAccessToken(userData: AuthModel):
user = check_user(connector, userData.username, userData.password)
if not user:
return {"error": "Wrong username or password"}
return JSONResponse(
ErrorResponse(error="Wrong username or password"),
status_code=status.HTTP_401_UNAUTHORIZED,
)
return {"access_token": user.access_token}
return AccessTokenResponse(access_token=user.access_token)
@app.get("/api/listTables")
@app.get(
"/api/listTables",
name="List tables",
responses={
200: {"model": list[TableDefinition], "description": "List of tables"},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires admin-level user access token",
},
},
)
async def listTables(
response: Response,
access_token: str | None = Header(default=None),
) -> TableListDefinition | ErrorResponseDefinition:
is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin:
return ErrorResponseDefinition(error="Not allowed")
return TableListDefinition(
tables=[TableDefinition.parse_obj(table) for table in connector.tables()]
)
@app.post("/api/createTable/{tableName}")
async def createTable(
tableName: str,
columns: ColumnsDefinitionList,
access_token: str | None = Header(default=None),
):
is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
return [TableDefinition.parse_obj(table) for table in connector.tables()]
@app.post(
"/api/createTable/{tableName}",
name="Create table",
responses={
200: {"model": OkResponse, "description": "Table created successfully"},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during table creation",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires admin-level user access token",
},
409: {
"model": ErrorResponse,
"description": "Table with this name already exists",
},
},
)
async def createTable(
tableName: str,
columns: list[str],
access_token: str | None = Header(default=None),
):
"""
Parameter `columns` should be a list of strings
Each string should be in a following format:
`column_name:column_type[:column_options]`
Where *column_type* should be one of the following:
- serial
- str
- bool
- datetime
- float
- int
Also *column_options* can be one of the following:
- unique
- default
Example:
```json
[
"id:serial:primary",
"name:str:unique",
"description:str",
"is_active:bool",
"price:float",
"quantity:int"
]
```
Notes:
- you cannot use *unique* and *default* at the same time
- 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
)
try:
columnsDefinition = parse_columns_from_definition(",".join(columns.columns))
columnsDefinition = parse_columns_from_definition(",".join(columns))
create_table(connector, tableName, columnsDefinition)
except psycopg.errors.UniqueViolation:
return {"error": "Username already exists"}
return JSONResponse(
ErrorResponse(error="Table already exists"),
status_code=status.HTTP_409_CONFLICT,
)
except Exception as e:
return {"error": str(e)}
return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
)
return {"ok": True}
return OkResponse()
@app.post("/api/dropTable/{tableName}")
@app.post(
"/api/dropTable/{tableName}",
name="Drop table",
responses={
200: {"model": OkResponse, "description": "Table dropped successfully"},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during table creation",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires admin-level user access token",
},
},
)
async def dropTable(
tableName: str,
access_token: str | None = Header(default=None),
):
is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
try:
drop_table(connector, tableName)
ok, e = drop_table(connector, tableName)
if not ok:
if e:
raise e
raise Exception("Unknown error")
except Exception as e:
return {"error": str(e)}
return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
)
return {"ok": True}
return OkResponse()
@app.post("/api/createUser")
@app.post(
"/api/createUser",
name="Create 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 createUser(
user: UserDefinition,
user: CreateUserDefinition,
access_token: str | None = Header(default=None),
):
is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
try:
create_user(connector, user.username, user.password)
ok, e = create_user(connector, user.username, user.password)
if not ok:
if e:
raise e
raise Exception("Unknown error")
except psycopg.errors.UniqueViolation:
return {"error": "Username already exists"}
return JSONResponse(
ErrorResponse(error="Username already exists"),
status_code=status.HTTP_409_CONFLICT,
)
except Exception as e:
return {"error": str(e)}
return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
)
return {"ok": True}
return OkResponse()
@app.post("/api/updateUser")
@app.post(
"/api/updateUser",
name="Update user",
responses={
200: {"model": OkResponse, "description": "Table dropped successfully"},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during updating user",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires admin-level user access token",
},
},
)
async def updateUser(
user: UserDefinition,
user: UserUpdateDefinition,
access_token: str | None = Header(default=None),
):
is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin:
return {"error": "Not allowed"}
if not user.user_id or not user.password or not user.access_token:
return {"error": "Malformed request"}
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
try:
update_user(connector, user.user_id, user.password, user.access_token)
ok, e = update_user(connector, user.user_id, user.password, user.access_token)
if not ok:
if e:
raise e
raise Exception("Unknown error")
except Exception as e:
return {"error": str(e)}
return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
)
return {"ok": True}
return OkResponse()
@app.post("/items/{tableName}")
@app.post(
"/items/{tableName}",
name="Get items from table",
responses={
200: {"model": TableItemsResponse, "description": "Table items"},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during getting table items",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires user access token",
},
404: {
"model": ErrorResponse,
"description": "Table not found",
},
},
)
async def items(
tableName: str,
selector: ItemsFieldSelectorList,
@ -163,11 +338,16 @@ async def items(
):
table_info = connector.getTable(tableName)
if not table_info:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Table not found"),
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 {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
columns = parse_columns_from_definition(table_info["columns"])
columnsNames = set(column.name for column in columns)
@ -175,47 +355,87 @@ async def items(
if userSelectedColumns != ["*"]:
for column in userSelectedColumns:
if column not in columnsNames:
return {"error": f"Column {column} not found on table {tableName}"}
return JSONResponse(
ErrorResponse(
error=f"Column {column} not found on table {tableName}"
),
status_code=status.HTTP_404_NOT_FOUND,
)
else:
userSelectedColumns = columnsNames
userSelectedColumns = list(columnsNames)
user, group = get_user_by_access_token(connector, access_token)
if not user:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
if not is_admin:
allowedColumns = get_allowed_columns_for_group(
connector, tableName, group.id if group else -1
)
if not allowedColumns:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
elif len(allowedColumns) == 1 and allowedColumns[0] == "*":
pass
else:
for column in userSelectedColumns:
if column not in allowedColumns:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
table_items = connector.selectFromTable(
tableName, selector.fields if selector.fields else ["*"]
)
return {"items": table_items}
return TableItemsResponse(items=table_items)
@app.post("/items/{tableName}/+")
@app.post(
"/items/{tableName}/+",
name="Create item",
responses={
200: {"model": OkResponse, "description": "Item created successfully"},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during creating item",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires user access token",
},
404: {
"model": ErrorResponse,
"description": "Table or column not found",
},
409: {
"model": ErrorResponse,
"description": "Unique constraint violation",
},
},
)
async def itemsCreate(
tableName: str,
item: dict[str, str],
item: dict[str, Any],
access_token: str | None = Header(default=None),
):
table_info = connector.getTable(tableName)
if not table_info:
return {"error": "Not found"}
return JSONResponse(
ErrorResponse(error="Table not found"),
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 {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
user, group = get_user_by_access_token(connector, access_token)
if not is_admin:
@ -223,27 +443,63 @@ async def itemsCreate(
connector, tableName, group.id if group else -1
)
if not allowedColumns:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
elif len(allowedColumns) == 1 and allowedColumns[0] == "*":
pass
else:
for column in item:
if column not in allowedColumns:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
try:
connector.insertIntoTable(tableName, item)
except psycopg.errors.UndefinedColumn:
return {"error": "Column not found"}
return JSONResponse(
ErrorResponse(error="Column not found"),
status_code=status.HTTP_404_NOT_FOUND,
)
except psycopg.errors.UniqueViolation:
return {"error": "Unique constraint violation"}
return JSONResponse(
ErrorResponse(error="Unique violation"),
status_code=status.HTTP_409_CONFLICT,
)
except Exception as e:
return {"error": str(e)}
return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
)
return {"ok": True}
return OkResponse()
@app.post("/items/{tableName}/*")
@app.post(
"/items/{tableName}/*",
name="Update item in table",
responses={
200: {"model": OkResponse, "description": "Item updated successfully"},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during updating item",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires user access token",
},
404: {
"model": ErrorResponse,
"description": "Table or column not found",
},
409: {
"model": ErrorResponse,
"description": "Unique constraint violation",
},
},
)
async def itemsUpdate(
tableName: str,
item: dict[str, str],
@ -252,11 +508,17 @@ async def itemsUpdate(
):
table_info = connector.getTable(tableName)
if not table_info:
return {"error": "Not found"}
return JSONResponse(
ErrorResponse(error="Table not found"),
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 {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
user, group = get_user_by_access_token(connector, access_token)
if not is_admin:
@ -264,41 +526,84 @@ async def itemsUpdate(
connector, tableName, group.id if group else -1
)
if not allowedColumns:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
elif len(allowedColumns) == 1 and allowedColumns[0] == "*":
pass
else:
for column in item:
if column not in allowedColumns:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
try:
connector.updateDataInTable(
tableName,
[ColumnUpdate(column=c, value=item[c]) for c in item],
[ColumnCondition(column=c, value=oldItem[c]) for c in oldItem],
[
ColumnCondition(column=c, operator="eq", value=oldItem[c])
for c in oldItem
],
)
except psycopg.errors.UniqueViolation:
return JSONResponse(
ErrorResponse(error="Unique violation"),
status_code=status.HTTP_409_CONFLICT,
)
except psycopg.errors.UndefinedColumn:
return {"error": "Column not found"}
return JSONResponse(
ErrorResponse(error="Column not found"),
status_code=status.HTTP_404_NOT_FOUND,
)
except Exception as e:
return {"error": str(e)}
return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
)
return {"ok": True}
return OkResponse()
@app.post("/items/{tableName}/-")
@app.post(
"/items/{tableName}/-",
name="Delete item from table",
responses={
200: {"model": OkResponse, "description": "Item deleted successfully"},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during deleting item",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires user access token",
},
404: {
"model": ErrorResponse,
"description": "Table or column not found",
},
},
)
async def itemsDelete(
tableName: str,
deleteWhere: ItemDeletionDefinitionList,
deleteWhere: list[ColumnConditionCompat],
access_token: str | None = Header(default=None),
):
table_info = connector.getTable(tableName)
if not table_info:
return {"error": "Not found"}
return JSONResponse(
ErrorResponse(error="Table not found"),
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 {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
user, group = get_user_by_access_token(connector, access_token)
if not is_admin:
@ -306,27 +611,44 @@ async def itemsDelete(
connector, tableName, group.id if group else -1
)
if not allowedColumns:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
elif len(allowedColumns) == 1 and allowedColumns[0] == "*":
pass
else:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
try:
connector.deleteFromTable(
tableName,
[
ColumnCondition(where.name, where.value, where.isString, where.isLike)
for where in deleteWhere.defs
],
[ColumnCondition(dw.column, dw.operator, dw.value) for dw in deleteWhere],
)
except Exception as e:
return {"error": str(e)}
return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
)
return {"ok": True}
return OkResponse()
@app.get("/assets/{fid}")
@app.get(
"/assets/{fid}",
name="Get asset",
responses={
200: {
"description": "Asset found",
"content": {"application/octet-stream": {}},
},
404: {
"description": "Asset not found",
},
},
)
async def getAsset(fid: str, access_token: str | None = Header(default=None)):
asset = get_asset(connector, access_token, fid)
if not asset:
@ -349,14 +671,38 @@ async def getAsset(fid: str, access_token: str | None = Header(default=None)):
response.release_conn()
@app.post("/assets/+")
@app.post(
"/assets/+",
name="Put asset",
responses={
200: {
"model": CreateAssetResponse,
"description": "Asset created successfully",
},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during creating asset",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires user access token",
},
500: {
"model": ErrorResponse,
"description": "Failed put asset into storage",
},
},
)
async def createAsset(
asset: UploadFile,
access_token: str | None = Header(default=None),
):
user, _ = get_user_by_access_token(connector, access_token)
if not user:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
filename = asset.filename
if not filename:
@ -372,10 +718,19 @@ async def createAsset(
),
length=asset.size,
)
if not result:
return JSONResponse(
ErrorResponse(error="Failed put asset into storage"),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
if not create_asset(connector, filename, "", str(result.version_id)):
return {"error": "Failed to create asset"}
return {"ok": True, "fid": result.version_id}
return JSONResponse(
ErrorResponse(error="Failed to create asset"),
status_code=status.HTTP_400_BAD_REQUEST,
)
return CreateAssetResponse(fid=result.version_id)
if __name__ == "__main__":