1069 lines
33 KiB
Python
1069 lines
33 KiB
Python
import io
|
|
from typing import Any
|
|
from fastapi import FastAPI, status, Header, UploadFile, Response
|
|
from starlette.responses import StreamingResponse, JSONResponse
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from based import db
|
|
import psycopg
|
|
from secrets import token_hex
|
|
from minio import Minio
|
|
from minio.helpers import ObjectWriteResult
|
|
from urllib3 import HTTPResponse
|
|
import uvicorn
|
|
|
|
from dba import *
|
|
from models import (
|
|
AssetUpdateDefinition,
|
|
AuthModel,
|
|
ColumnConditionCompat,
|
|
CreateUserDefinition,
|
|
TableDefinition,
|
|
UserUpdateDefinition,
|
|
OkResponse,
|
|
ErrorResponse,
|
|
AccessTokenResponse,
|
|
CreateAssetResponse,
|
|
)
|
|
from utils import (
|
|
check_if_admin_access_token,
|
|
parse_columns_from_definition,
|
|
)
|
|
|
|
conninfo = "postgresql://postgres:asarch6122@localhost"
|
|
connector = db.DBConnector(conninfo)
|
|
|
|
bootstrapDB(connector)
|
|
|
|
BUCKET_NAME = "tuuli-files"
|
|
minioClient = Minio(
|
|
"localhost:8090",
|
|
access_key="mxR0F5PK8CpCM8SA",
|
|
secret_key="yFJsG70xLU3BiIMslinz6dhqKHqNpUc6",
|
|
secure=False,
|
|
)
|
|
found = minioClient.bucket_exists(BUCKET_NAME)
|
|
if found:
|
|
print(f"Bucket '{BUCKET_NAME}' already exists")
|
|
else:
|
|
minioClient.make_bucket(BUCKET_NAME)
|
|
|
|
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=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
|
|
@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 JSONResponse(
|
|
ErrorResponse(error="Wrong username or password").dict(),
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
)
|
|
|
|
return AccessTokenResponse(access_token=user.access_token)
|
|
|
|
|
|
@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(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,
|
|
)
|
|
|
|
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:primary
|
|
- str
|
|
- bool
|
|
- datetime
|
|
- float
|
|
- int
|
|
- int-asset
|
|
- int-user
|
|
|
|
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",
|
|
"creator_id:int-user",
|
|
"asset_id:int-asset"
|
|
]
|
|
```
|
|
|
|
Notes:
|
|
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").dict(),
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
|
|
try:
|
|
columnsDefinition = parse_columns_from_definition(",".join(columns))
|
|
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").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()
|
|
|
|
|
|
@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 JSONResponse(
|
|
ErrorResponse(error="Not allowed").dict(),
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
|
|
try:
|
|
ok, e = drop_table(connector, tableName)
|
|
if not ok:
|
|
if e:
|
|
raise e
|
|
raise Exception("Unknown error")
|
|
except Exception as e:
|
|
return JSONResponse(
|
|
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
return OkResponse()
|
|
|
|
|
|
@app.post(
|
|
"/api/users/+",
|
|
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: CreateUserDefinition,
|
|
access_token: str | None = Header(default=None),
|
|
):
|
|
try:
|
|
_user, _ = get_user_by_access_token(connector, access_token)
|
|
if not _user:
|
|
raise Exception("Not allowed")
|
|
acl = get_user_permissions_for_table(connector, "users", _user)
|
|
if acl != AccessType.READ and acl != AccessType.READ_WRITE:
|
|
raise Exception("Not allowed")
|
|
except Exception as e:
|
|
return JSONResponse(
|
|
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_403_FORBIDDEN
|
|
)
|
|
|
|
try:
|
|
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 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()
|
|
|
|
|
|
@app.post(
|
|
"/api/users/*",
|
|
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: UserUpdateDefinition,
|
|
access_token: str | None = Header(default=None),
|
|
):
|
|
try:
|
|
_user, _ = get_user_by_access_token(connector, access_token)
|
|
if not _user:
|
|
raise Exception("Not allowed")
|
|
acl = get_user_permissions_for_table(connector, "users", _user)
|
|
if acl != AccessType.WRITE and acl != AccessType.READ_WRITE:
|
|
raise Exception("Not allowed")
|
|
except Exception as e:
|
|
return JSONResponse(
|
|
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_403_FORBIDDEN
|
|
)
|
|
|
|
try:
|
|
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 JSONResponse(
|
|
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),
|
|
):
|
|
try:
|
|
_user, _ = get_user_by_access_token(connector, access_token)
|
|
if not _user:
|
|
raise Exception("Not allowed")
|
|
acl = get_user_permissions_for_table(connector, "users", _user)
|
|
if acl != AccessType.WRITE and acl != AccessType.READ_WRITE:
|
|
raise Exception("Not allowed")
|
|
except Exception as e:
|
|
return JSONResponse(
|
|
ErrorResponse(error=str(e)).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()
|
|
|
|
|
|
@app.post(
|
|
"/items/{tableName}",
|
|
name="Get items from table",
|
|
responses={
|
|
200: {"model": list[dict[str, Any]], "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,
|
|
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").dict(),
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
)
|
|
|
|
try:
|
|
is_admin = check_if_admin_access_token(connector, access_token)
|
|
if table_info["system"] and not is_admin:
|
|
raise Exception("Not allowed")
|
|
|
|
user, group = get_user_by_access_token(connector, access_token)
|
|
if not user:
|
|
raise Exception("Not allowed")
|
|
acl = get_user_permissions_for_table(connector, tableName, user)
|
|
if acl != AccessType.READ and acl != AccessType.READ_WRITE:
|
|
raise Exception("Not allowed")
|
|
except Exception as e:
|
|
return JSONResponse(
|
|
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_403_FORBIDDEN
|
|
)
|
|
|
|
columns = parse_columns_from_definition(table_info["columns"])
|
|
columnsNames = set(column.name for column in columns)
|
|
|
|
if fields == ["*"]:
|
|
fields = list(columnsNames)
|
|
else:
|
|
for column in fields:
|
|
if column not in columnsNames:
|
|
return JSONResponse(
|
|
ErrorResponse(
|
|
error=f"Column {column} not found on table {tableName}"
|
|
),
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
)
|
|
|
|
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 # 1 is anonymous group
|
|
)
|
|
if not allowedColumns:
|
|
return JSONResponse(
|
|
ErrorResponse(error="Not allowed").dict(),
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
elif len(allowedColumns) == 1 and allowedColumns[0] == "*":
|
|
pass
|
|
else:
|
|
for column in fields:
|
|
if column not in allowedColumns:
|
|
return JSONResponse(
|
|
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,
|
|
)
|
|
|
|
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 table_items
|
|
|
|
|
|
@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, Any],
|
|
access_token: str | None = Header(default=None),
|
|
):
|
|
table_info = connector.getTable(tableName)
|
|
if not table_info:
|
|
return JSONResponse(
|
|
ErrorResponse(error="Table not found").dict(),
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
)
|
|
|
|
try:
|
|
is_admin = check_if_admin_access_token(connector, access_token)
|
|
if table_info["system"] and not is_admin:
|
|
raise Exception("Not allowed")
|
|
|
|
user, group = get_user_by_access_token(connector, access_token)
|
|
if not user:
|
|
raise Exception("Not allowed")
|
|
acl = get_user_permissions_for_table(connector, tableName, user)
|
|
if acl != AccessType.WRITE and acl != AccessType.READ_WRITE:
|
|
raise Exception("Not allowed")
|
|
except Exception as e:
|
|
return JSONResponse(
|
|
ErrorResponse(error=str(e)).dict(), 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 JSONResponse(
|
|
ErrorResponse(error="Not allowed").dict(),
|
|
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 JSONResponse(
|
|
ErrorResponse(error="Not allowed").dict(),
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
|
|
try:
|
|
connector.insertIntoTable(tableName, item)
|
|
except psycopg.errors.UndefinedColumn:
|
|
return JSONResponse(
|
|
ErrorResponse(error="Column not found").dict(),
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
)
|
|
except psycopg.errors.UniqueViolation:
|
|
return JSONResponse(
|
|
ErrorResponse(error="Unique violation").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()
|
|
|
|
|
|
@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],
|
|
oldItem: dict[str, str],
|
|
access_token: str | None = Header(default=None),
|
|
):
|
|
table_info = connector.getTable(tableName)
|
|
if not table_info:
|
|
return JSONResponse(
|
|
ErrorResponse(error="Table not found").dict(),
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
)
|
|
|
|
try:
|
|
is_admin = check_if_admin_access_token(connector, access_token)
|
|
if table_info["system"] and not is_admin:
|
|
raise Exception("Not allowed")
|
|
|
|
user, group = get_user_by_access_token(connector, access_token)
|
|
if not user:
|
|
raise Exception("Not allowed")
|
|
acl = get_user_permissions_for_table(connector, tableName, user)
|
|
if acl != AccessType.WRITE and acl != AccessType.READ_WRITE:
|
|
raise Exception("Not allowed")
|
|
except Exception as e:
|
|
return JSONResponse(
|
|
ErrorResponse(error=str(e)).dict(), 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 JSONResponse(
|
|
ErrorResponse(error="Not allowed").dict(),
|
|
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 JSONResponse(
|
|
ErrorResponse(error="Not allowed").dict(),
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
|
|
try:
|
|
connector.updateDataInTable(
|
|
tableName,
|
|
[ColumnUpdate(column=c, value=item[c]) for c in item],
|
|
[
|
|
ColumnCondition(column=c, operator="eq", value=oldItem[c])
|
|
for c in oldItem
|
|
],
|
|
)
|
|
except psycopg.errors.UniqueViolation:
|
|
return JSONResponse(
|
|
ErrorResponse(error="Unique violation").dict(),
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
)
|
|
except psycopg.errors.UndefinedColumn:
|
|
return JSONResponse(
|
|
ErrorResponse(error="Column not found").dict(),
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
)
|
|
except Exception as e:
|
|
return JSONResponse(
|
|
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
return OkResponse()
|
|
|
|
|
|
@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: 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").dict(),
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
)
|
|
|
|
try:
|
|
is_admin = check_if_admin_access_token(connector, access_token)
|
|
if table_info["system"] and not is_admin:
|
|
raise Exception("Not allowed")
|
|
|
|
user, group = get_user_by_access_token(connector, access_token)
|
|
if not user:
|
|
raise Exception("Not allowed")
|
|
acl = get_user_permissions_for_table(connector, tableName, user)
|
|
if acl != AccessType.WRITE and acl != AccessType.READ_WRITE:
|
|
raise Exception("Not allowed")
|
|
except Exception as e:
|
|
return JSONResponse(
|
|
ErrorResponse(error=str(e)).dict(), 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 JSONResponse(
|
|
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").dict(),
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
|
|
try:
|
|
connector.deleteFromTable(
|
|
tableName,
|
|
[ColumnCondition(dw.column, dw.operator, dw.value) for dw in deleteWhere],
|
|
)
|
|
except Exception as e:
|
|
return JSONResponse(
|
|
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
return OkResponse()
|
|
|
|
|
|
@app.get(
|
|
"/assets",
|
|
name="Get assets",
|
|
responses={
|
|
200: {
|
|
"model": list[Asset],
|
|
"description": "List of assets",
|
|
},
|
|
403: {
|
|
"model": ErrorResponse,
|
|
"description": "Requesting this endpoint requires permissions",
|
|
},
|
|
},
|
|
)
|
|
async def getAssets(access_token: str | None = Header(default=None)):
|
|
try:
|
|
user, _ = get_user_by_access_token(connector, access_token)
|
|
if not user:
|
|
raise Exception("Not allowed")
|
|
acl = get_user_permissions_for_table(connector, "assets", user)
|
|
if acl != AccessType.READ and acl != AccessType.READ_WRITE:
|
|
raise Exception("Not allowed")
|
|
except Exception as e:
|
|
return JSONResponse(
|
|
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_403_FORBIDDEN
|
|
)
|
|
|
|
assets = get_assets(connector)
|
|
return assets
|
|
|
|
|
|
@app.get(
|
|
"/assets/tags",
|
|
name="Get assets tags",
|
|
responses={
|
|
200: {
|
|
"model": list[str],
|
|
"description": "List of assets tags",
|
|
},
|
|
403: {
|
|
"model": ErrorResponse,
|
|
"description": "Requesting this endpoint requires admin access token",
|
|
},
|
|
},
|
|
)
|
|
async def getAssetsTags(access_token: str | None = Header(default=None)):
|
|
try:
|
|
user, _ = get_user_by_access_token(connector, access_token)
|
|
if not user:
|
|
raise Exception("Not allowed")
|
|
acl = get_user_permissions_for_table(connector, "assets", user)
|
|
if acl != AccessType.READ and acl != AccessType.READ_WRITE:
|
|
raise Exception("Not allowed")
|
|
except Exception as e:
|
|
return JSONResponse(
|
|
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_403_FORBIDDEN
|
|
)
|
|
|
|
assets = get_assets_tags(connector)
|
|
return assets
|
|
|
|
|
|
@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):
|
|
asset = get_asset(connector, fid)
|
|
if not asset:
|
|
return status.HTTP_404_NOT_FOUND
|
|
|
|
response: HTTPResponse | None = None
|
|
try:
|
|
response = minioClient.get_object(BUCKET_NAME, asset.name, version_id=asset.fid)
|
|
if response is None:
|
|
return status.HTTP_404_NOT_FOUND
|
|
|
|
return StreamingResponse(
|
|
content=io.BytesIO(response.data),
|
|
media_type=response.getheader("Content-Type"),
|
|
status_code=status.HTTP_200_OK,
|
|
)
|
|
finally:
|
|
if response is not None:
|
|
response.close()
|
|
response.release_conn()
|
|
|
|
|
|
@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),
|
|
):
|
|
try:
|
|
user, _ = get_user_by_access_token(connector, access_token)
|
|
if not user:
|
|
raise Exception("Not allowed")
|
|
acl = get_user_permissions_for_table(connector, "assets", user)
|
|
if acl != AccessType.WRITE and acl != AccessType.READ_WRITE:
|
|
raise Exception("Not allowed")
|
|
except Exception as e:
|
|
return JSONResponse(
|
|
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_403_FORBIDDEN
|
|
)
|
|
|
|
filename = asset.filename
|
|
if not filename:
|
|
filename = f"unnamed"
|
|
filename = f"{token_hex()}_{filename}"
|
|
|
|
result: ObjectWriteResult = minioClient.put_object(
|
|
BUCKET_NAME,
|
|
filename,
|
|
data=asset.file,
|
|
content_type=(
|
|
asset.content_type if asset.content_type else "application/octet-stream"
|
|
),
|
|
length=asset.size,
|
|
)
|
|
if not result:
|
|
return JSONResponse(
|
|
ErrorResponse(error="Failed put asset into storage").dict(),
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
)
|
|
|
|
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").dict(),
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
return CreateAssetResponse(fid=result.version_id)
|
|
|
|
|
|
@app.post(
|
|
"/assets/{asset_id}/*",
|
|
name="Update asset description and tags",
|
|
responses={
|
|
200: {
|
|
"model": OkResponse,
|
|
"description": "Asset description and tags 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_update: AssetUpdateDefinition,
|
|
access_token: str | None = Header(default=None),
|
|
):
|
|
try:
|
|
user, _ = get_user_by_access_token(connector, access_token)
|
|
if not user:
|
|
raise Exception("Not allowed")
|
|
acl = get_user_permissions_for_table(connector, "assets", user)
|
|
if acl != AccessType.WRITE and acl != AccessType.READ_WRITE:
|
|
raise Exception("Not allowed")
|
|
except Exception as e:
|
|
return JSONResponse(
|
|
ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_403_FORBIDDEN
|
|
)
|
|
|
|
ok, e = update_asset(
|
|
connector, asset_id, asset_update.description, asset_update.tags
|
|
)
|
|
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),
|
|
):
|
|
try:
|
|
user, _ = get_user_by_access_token(connector, access_token)
|
|
if not user:
|
|
raise Exception("Not allowed")
|
|
acl = get_user_permissions_for_table(connector, "assets", user)
|
|
if acl != AccessType.WRITE and acl != AccessType.READ_WRITE:
|
|
raise Exception("Not allowed")
|
|
except Exception as e:
|
|
return JSONResponse(
|
|
ErrorResponse(error=str(e)).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.name)
|
|
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)
|