brilliance-admin 0.42.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- admin_panel/__init__.py +4 -0
- admin_panel/api/__init__.py +0 -0
- admin_panel/api/routers.py +18 -0
- admin_panel/api/utils.py +28 -0
- admin_panel/api/views/__init__.py +0 -0
- admin_panel/api/views/auth.py +29 -0
- admin_panel/api/views/autocomplete.py +33 -0
- admin_panel/api/views/graphs.py +30 -0
- admin_panel/api/views/index.py +38 -0
- admin_panel/api/views/schema.py +29 -0
- admin_panel/api/views/settings.py +29 -0
- admin_panel/api/views/table.py +136 -0
- admin_panel/auth.py +32 -0
- admin_panel/docs.py +37 -0
- admin_panel/exceptions.py +38 -0
- admin_panel/integrations/__init__.py +0 -0
- admin_panel/integrations/sqlalchemy/__init__.py +6 -0
- admin_panel/integrations/sqlalchemy/auth.py +144 -0
- admin_panel/integrations/sqlalchemy/autocomplete.py +38 -0
- admin_panel/integrations/sqlalchemy/fields.py +254 -0
- admin_panel/integrations/sqlalchemy/fields_schema.py +316 -0
- admin_panel/integrations/sqlalchemy/table/__init__.py +19 -0
- admin_panel/integrations/sqlalchemy/table/base.py +141 -0
- admin_panel/integrations/sqlalchemy/table/create.py +73 -0
- admin_panel/integrations/sqlalchemy/table/delete.py +18 -0
- admin_panel/integrations/sqlalchemy/table/list.py +178 -0
- admin_panel/integrations/sqlalchemy/table/retrieve.py +61 -0
- admin_panel/integrations/sqlalchemy/table/update.py +95 -0
- admin_panel/schema/__init__.py +7 -0
- admin_panel/schema/admin_schema.py +191 -0
- admin_panel/schema/category.py +149 -0
- admin_panel/schema/graphs/__init__.py +1 -0
- admin_panel/schema/graphs/category_graphs.py +50 -0
- admin_panel/schema/group.py +67 -0
- admin_panel/schema/table/__init__.py +8 -0
- admin_panel/schema/table/admin_action.py +76 -0
- admin_panel/schema/table/category_table.py +175 -0
- admin_panel/schema/table/fields/__init__.py +5 -0
- admin_panel/schema/table/fields/base.py +249 -0
- admin_panel/schema/table/fields/function_field.py +65 -0
- admin_panel/schema/table/fields_schema.py +216 -0
- admin_panel/schema/table/table_models.py +53 -0
- admin_panel/static/favicon.jpg +0 -0
- admin_panel/static/index-BeniOHDv.js +525 -0
- admin_panel/static/index-vlBToOhT.css +8 -0
- admin_panel/static/materialdesignicons-webfont-CYDMK1kx.woff2 +0 -0
- admin_panel/static/materialdesignicons-webfont-CgCzGbLl.woff +0 -0
- admin_panel/static/materialdesignicons-webfont-D3kAzl71.ttf +0 -0
- admin_panel/static/materialdesignicons-webfont-DttUABo4.eot +0 -0
- admin_panel/static/tinymce/dark-first/content.min.css +250 -0
- admin_panel/static/tinymce/dark-first/skin.min.css +2820 -0
- admin_panel/static/tinymce/dark-slim/content.min.css +249 -0
- admin_panel/static/tinymce/dark-slim/skin.min.css +2821 -0
- admin_panel/static/tinymce/img/example.png +0 -0
- admin_panel/static/tinymce/img/tinymce.woff2 +0 -0
- admin_panel/static/tinymce/lightgray/content.min.css +1 -0
- admin_panel/static/tinymce/lightgray/fonts/tinymce.woff +0 -0
- admin_panel/static/tinymce/lightgray/skin.min.css +1 -0
- admin_panel/static/tinymce/plugins/accordion/css/accordion.css +17 -0
- admin_panel/static/tinymce/plugins/accordion/plugin.js +48 -0
- admin_panel/static/tinymce/plugins/codesample/css/prism.css +1 -0
- admin_panel/static/tinymce/plugins/customLink/css/link.css +3 -0
- admin_panel/static/tinymce/plugins/customLink/plugin.js +147 -0
- admin_panel/static/tinymce/tinymce.min.js +2 -0
- admin_panel/static/vanilla-picker-B6E6ObS_.js +8 -0
- admin_panel/templates/index.html +25 -0
- admin_panel/translations.py +145 -0
- admin_panel/utils.py +50 -0
- brilliance_admin-0.42.0.dist-info/METADATA +155 -0
- brilliance_admin-0.42.0.dist-info/RECORD +73 -0
- brilliance_admin-0.42.0.dist-info/WHEEL +5 -0
- brilliance_admin-0.42.0.dist-info/licenses/LICENSE +17 -0
- brilliance_admin-0.42.0.dist-info/top_level.txt +1 -0
admin_panel/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
|
|
3
|
+
from .views.schema import router as schema_router
|
|
4
|
+
from .views.table import router as schema_table
|
|
5
|
+
from .views.auth import router as schema_auth
|
|
6
|
+
from .views.autocomplete import router as schema_autocomplete
|
|
7
|
+
from .views.graphs import router as schema_graphs
|
|
8
|
+
from .views.settings import router as schema_settings
|
|
9
|
+
from .views.index import router as schema_index
|
|
10
|
+
|
|
11
|
+
admin_panel_router = APIRouter()
|
|
12
|
+
admin_panel_router.include_router(schema_router)
|
|
13
|
+
admin_panel_router.include_router(schema_table)
|
|
14
|
+
admin_panel_router.include_router(schema_auth)
|
|
15
|
+
admin_panel_router.include_router(schema_autocomplete)
|
|
16
|
+
admin_panel_router.include_router(schema_graphs)
|
|
17
|
+
admin_panel_router.include_router(schema_settings)
|
|
18
|
+
admin_panel_router.include_router(schema_index)
|
admin_panel/api/utils.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from fastapi import HTTPException
|
|
2
|
+
|
|
3
|
+
from admin_panel.auth import AdminAuthentication
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def get_user(request):
|
|
7
|
+
auth: AdminAuthentication = request.app.state.schema.auth
|
|
8
|
+
user = await auth.authenticate(request.headers)
|
|
9
|
+
return user
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def get_category(request, group: str, category: str, check_type=None):
|
|
13
|
+
user = await get_user(request)
|
|
14
|
+
|
|
15
|
+
schema_group = request.app.state.schema.get_group(group)
|
|
16
|
+
if not schema_group:
|
|
17
|
+
raise HTTPException(status_code=404, detail="Group not found")
|
|
18
|
+
|
|
19
|
+
schema_category = schema_group.get_category(category)
|
|
20
|
+
if not schema_category:
|
|
21
|
+
raise HTTPException(status_code=404, detail="Category not found")
|
|
22
|
+
|
|
23
|
+
if check_type:
|
|
24
|
+
schema_category, user = await get_category(request, group, category)
|
|
25
|
+
if not issubclass(schema_category.__class__, check_type):
|
|
26
|
+
raise HTTPException(status_code=404, detail=f"Category {group}.{category} is not a {check_type.__name__}")
|
|
27
|
+
|
|
28
|
+
return schema_category, user
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from fastapi import APIRouter, Request
|
|
2
|
+
from fastapi.responses import JSONResponse
|
|
3
|
+
|
|
4
|
+
from admin_panel.auth import AdminAuthentication, AuthData, AuthResult
|
|
5
|
+
from admin_panel.exceptions import AdminAPIException, APIError
|
|
6
|
+
from admin_panel.schema.admin_schema import AdminSchema
|
|
7
|
+
from admin_panel.translations import LanguageManager
|
|
8
|
+
|
|
9
|
+
router = APIRouter(prefix="/auth", tags=["Auth"])
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@router.post(
|
|
13
|
+
path='/login/',
|
|
14
|
+
responses={401: {"model": APIError}},
|
|
15
|
+
)
|
|
16
|
+
async def login(request: Request, auth_data: AuthData) -> AuthResult:
|
|
17
|
+
schema: AdminSchema = request.app.state.schema
|
|
18
|
+
|
|
19
|
+
language_slug = request.headers.get('Accept-Language')
|
|
20
|
+
language_manager: LanguageManager = schema.get_language_manager(language_slug)
|
|
21
|
+
context = {'language_manager': language_manager}
|
|
22
|
+
|
|
23
|
+
auth: AdminAuthentication = schema.auth
|
|
24
|
+
try:
|
|
25
|
+
result: AuthResult = await auth.login(auth_data)
|
|
26
|
+
except AdminAPIException as e:
|
|
27
|
+
return JSONResponse(e.get_error().model_dump(mode='json', context=context), status_code=e.status_code)
|
|
28
|
+
|
|
29
|
+
return JSONResponse(content=result.model_dump(mode='json', context=context))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from fastapi import APIRouter, Request
|
|
2
|
+
from fastapi.responses import JSONResponse
|
|
3
|
+
|
|
4
|
+
from admin_panel.api.utils import get_category
|
|
5
|
+
from admin_panel.exceptions import AdminAPIException
|
|
6
|
+
from admin_panel.schema.admin_schema import AdminSchema
|
|
7
|
+
from admin_panel.schema.table.table_models import AutocompleteData, AutocompleteResult
|
|
8
|
+
from admin_panel.translations import LanguageManager
|
|
9
|
+
from admin_panel.utils import get_logger
|
|
10
|
+
|
|
11
|
+
router = APIRouter(prefix="/autocomplete", tags=["Autocomplete"])
|
|
12
|
+
|
|
13
|
+
logger = get_logger()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@router.post(path='/{group}/{category}/')
|
|
17
|
+
async def autocomplete(request: Request, group: str, category: str, data: AutocompleteData):
|
|
18
|
+
schema: AdminSchema = request.app.state.schema
|
|
19
|
+
schema_category, user = await get_category(request, group, category)
|
|
20
|
+
|
|
21
|
+
language_slug = request.headers.get('Accept-Language')
|
|
22
|
+
language_manager: LanguageManager = schema.get_language_manager(language_slug)
|
|
23
|
+
context = {'language_manager': language_manager}
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
result: AutocompleteResult = await schema_category.autocomplete(data, user, language_manager)
|
|
27
|
+
except AdminAPIException as e:
|
|
28
|
+
return JSONResponse(e.get_error().model_dump(mode='json', context=context), status_code=e.status_code)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
logger.exception('Autocomplete %s.%s exceptoin: %s', e, group, category, extra={'data': data})
|
|
31
|
+
return JSONResponse({}, status_code=500)
|
|
32
|
+
|
|
33
|
+
return JSONResponse(result.model_dump(mode='json', context=context))
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from fastapi import APIRouter, Request
|
|
2
|
+
from fastapi.responses import JSONResponse
|
|
3
|
+
|
|
4
|
+
from admin_panel.api.utils import get_category
|
|
5
|
+
from admin_panel.exceptions import AdminAPIException
|
|
6
|
+
from admin_panel.schema.admin_schema import AdminSchema
|
|
7
|
+
from admin_panel.schema.graphs.category_graphs import CategoryGraphs, GraphData, GraphsDataResult
|
|
8
|
+
from admin_panel.translations import LanguageManager
|
|
9
|
+
from admin_panel.utils import get_logger
|
|
10
|
+
|
|
11
|
+
router = APIRouter(prefix="/graph", tags=["Category - Graph"])
|
|
12
|
+
|
|
13
|
+
logger = get_logger()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@router.post(path='/{group}/{category}/')
|
|
17
|
+
async def graph_data(request: Request, group: str, category: str, data: GraphData) -> GraphsDataResult:
|
|
18
|
+
schema: AdminSchema = request.app.state.schema
|
|
19
|
+
schema_category, user = await get_category(request, group, category, check_type=CategoryGraphs)
|
|
20
|
+
|
|
21
|
+
result: GraphsDataResult = await schema_category.get_data(data, user)
|
|
22
|
+
|
|
23
|
+
language_slug = request.headers.get('Accept-Language')
|
|
24
|
+
language_manager: LanguageManager = schema.get_language_manager(language_slug)
|
|
25
|
+
context = {'language_manager': language_manager}
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
return JSONResponse(result.model_dump(mode='json', context=context))
|
|
29
|
+
except AdminAPIException as e:
|
|
30
|
+
return JSONResponse(e.get_error().model_dump(mode='json', context=context), status_code=e.status_code)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
2
|
+
from fastapi.responses import HTMLResponse
|
|
3
|
+
from fastapi.templating import Jinja2Templates
|
|
4
|
+
from jinja2 import Environment, PackageLoader, select_autoescape
|
|
5
|
+
|
|
6
|
+
from admin_panel.schema import AdminSchema
|
|
7
|
+
|
|
8
|
+
router = APIRouter()
|
|
9
|
+
|
|
10
|
+
templates = Jinja2Templates(
|
|
11
|
+
env=Environment(
|
|
12
|
+
loader=PackageLoader("admin_panel", "templates"),
|
|
13
|
+
autoescape=select_autoescape(["html", "xml"]),
|
|
14
|
+
)
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Всё, что не должно попадать в SPA (можете расширять список)
|
|
18
|
+
EXACT_BLOCK = {"/openapi.json"}
|
|
19
|
+
PREFIX_BLOCK = ("/docs", "/redoc", "/scalar", "/static")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@router.get('/{rest_of_path:path}', response_class=HTMLResponse, include_in_schema=False)
|
|
23
|
+
async def admin_index(request: Request, rest_of_path: str):
|
|
24
|
+
'''
|
|
25
|
+
The request responds with a pre-rendered SPA served as an HTML page.
|
|
26
|
+
'''
|
|
27
|
+
|
|
28
|
+
path = "/" + rest_of_path
|
|
29
|
+
if path in EXACT_BLOCK or path.startswith(PREFIX_BLOCK):
|
|
30
|
+
raise HTTPException(status_code=404)
|
|
31
|
+
|
|
32
|
+
schema: AdminSchema = request.app.state.schema
|
|
33
|
+
|
|
34
|
+
return templates.TemplateResponse(
|
|
35
|
+
request=request,
|
|
36
|
+
name='index.html',
|
|
37
|
+
context=await schema.get_index_context_data(request),
|
|
38
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from fastapi import APIRouter, Request
|
|
2
|
+
from fastapi.responses import JSONResponse
|
|
3
|
+
|
|
4
|
+
from admin_panel.auth import AdminAuthentication
|
|
5
|
+
from admin_panel.exceptions import AdminAPIException, APIError
|
|
6
|
+
from admin_panel.schema import AdminSchema, AdminSchemaData
|
|
7
|
+
|
|
8
|
+
router = APIRouter(prefix="/schema", tags=["Main admin schema"])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@router.get(
|
|
12
|
+
path='/',
|
|
13
|
+
responses={400: {"model": APIError}},
|
|
14
|
+
)
|
|
15
|
+
async def schema_handler(request: Request) -> AdminSchemaData:
|
|
16
|
+
'''
|
|
17
|
+
Request for retrieving the admin panel schema, including all sections and their contents.
|
|
18
|
+
'''
|
|
19
|
+
schema: AdminSchema = request.app.state.schema
|
|
20
|
+
|
|
21
|
+
auth: AdminAuthentication = schema.auth
|
|
22
|
+
try:
|
|
23
|
+
user = await auth.authenticate(request.headers)
|
|
24
|
+
except AdminAPIException as e:
|
|
25
|
+
return JSONResponse(e.get_error().model_dump(mode='json'), status_code=e.status_code)
|
|
26
|
+
|
|
27
|
+
language_slug = request.headers.get('Accept-Language')
|
|
28
|
+
admin_schema = schema.generate_schema(user, language_slug)
|
|
29
|
+
return admin_schema
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from fastapi import APIRouter, Request
|
|
2
|
+
from fastapi.responses import JSONResponse
|
|
3
|
+
|
|
4
|
+
from admin_panel.exceptions import AdminAPIException, APIError
|
|
5
|
+
from admin_panel.schema.admin_schema import AdminSchema, AdminSettingsData
|
|
6
|
+
from admin_panel.translations import LanguageManager
|
|
7
|
+
|
|
8
|
+
router = APIRouter(tags=["Settings"])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@router.post(
|
|
12
|
+
path='/get-settings/',
|
|
13
|
+
responses={400: {"model": APIError}},
|
|
14
|
+
)
|
|
15
|
+
async def get_settings(request: Request) -> AdminSettingsData:
|
|
16
|
+
'''
|
|
17
|
+
API endpoint for fetching admin panel configuration, including title, description, and the list of supported languages.
|
|
18
|
+
'''
|
|
19
|
+
schema: AdminSchema = request.app.state.schema
|
|
20
|
+
|
|
21
|
+
language_slug = request.headers.get('Accept-Language')
|
|
22
|
+
language_manager: LanguageManager = schema.get_language_manager(language_slug)
|
|
23
|
+
context = {'language_manager': language_manager}
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
admin_settings = await schema.get_settings(request)
|
|
27
|
+
return JSONResponse(admin_settings.model_dump(mode='json', context=context))
|
|
28
|
+
except AdminAPIException as e:
|
|
29
|
+
return JSONResponse(e.get_error().model_dump(mode='json', context=context), status_code=e.status_code)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
4
|
+
from fastapi.responses import JSONResponse
|
|
5
|
+
|
|
6
|
+
from admin_panel.api.utils import get_category
|
|
7
|
+
from admin_panel.exceptions import AdminAPIException, APIError
|
|
8
|
+
from admin_panel.schema import AdminSchema
|
|
9
|
+
from admin_panel.schema.table.admin_action import ActionData, ActionResult
|
|
10
|
+
from admin_panel.schema.table.category_table import CategoryTable
|
|
11
|
+
from admin_panel.schema.table.table_models import CreateResult, ListData, RetrieveResult, TableListResult, UpdateResult
|
|
12
|
+
from admin_panel.translations import LanguageManager
|
|
13
|
+
from admin_panel.utils import get_logger
|
|
14
|
+
|
|
15
|
+
router = APIRouter(prefix="/table", tags=["Category - Table"])
|
|
16
|
+
|
|
17
|
+
logger = get_logger()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# pylint: disable=too-many-arguments
|
|
21
|
+
@router.post(path='/{group}/{category}/list/')
|
|
22
|
+
async def table_list(request: Request, group: str, category: str, list_data: ListData) -> TableListResult:
|
|
23
|
+
schema: AdminSchema = request.app.state.schema
|
|
24
|
+
|
|
25
|
+
schema_category, user = await get_category(request, group, category, check_type=CategoryTable)
|
|
26
|
+
|
|
27
|
+
language_slug = request.headers.get('Accept-Language')
|
|
28
|
+
language_manager: LanguageManager = schema.get_language_manager(language_slug)
|
|
29
|
+
context = {'language_manager': language_manager}
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
result: TableListResult = await schema_category.get_list(list_data, user, language_manager)
|
|
33
|
+
except AdminAPIException as e:
|
|
34
|
+
return JSONResponse(e.get_error().model_dump(mode='json', context=context), status_code=e.status_code)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
return JSONResponse(content=result.model_dump(mode='json', context=context))
|
|
38
|
+
except Exception as e:
|
|
39
|
+
logger.exception('Admin list error: %s; result: %s', e, result)
|
|
40
|
+
raise HTTPException(status_code=500, detail=f"Content error: {e}") from e
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@router.post(path='/{group}/{category}/retrieve/{pk}/')
|
|
44
|
+
async def table_retrieve(request: Request, group: str, category: str, pk: Any) -> RetrieveResult:
|
|
45
|
+
schema: AdminSchema = request.app.state.schema
|
|
46
|
+
|
|
47
|
+
schema_category, user = await get_category(request, group, category, check_type=CategoryTable)
|
|
48
|
+
if not schema_category.has_retrieve:
|
|
49
|
+
raise HTTPException(status_code=404, detail=f"Category {group}.{category} is not allowed for retrive")
|
|
50
|
+
|
|
51
|
+
language_slug = request.headers.get('Accept-Language')
|
|
52
|
+
language_manager: LanguageManager = schema.get_language_manager(language_slug)
|
|
53
|
+
context = {'language_manager': language_manager}
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
result: RetrieveResult = await schema_category.retrieve(pk, user, language_manager)
|
|
57
|
+
except AdminAPIException as e:
|
|
58
|
+
return JSONResponse(e.get_error().model_dump(mode='json', context=context), status_code=e.status_code)
|
|
59
|
+
|
|
60
|
+
return JSONResponse(content=result.model_dump(mode='json', context=context))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@router.post(
|
|
64
|
+
path='/{group}/{category}/create/',
|
|
65
|
+
responses={400: {"model": APIError}},
|
|
66
|
+
)
|
|
67
|
+
async def table_create(request: Request, group: str, category: str) -> CreateResult:
|
|
68
|
+
schema: AdminSchema = request.app.state.schema
|
|
69
|
+
|
|
70
|
+
schema_category, user = await get_category(request, group, category, check_type=CategoryTable)
|
|
71
|
+
if not schema_category.has_create:
|
|
72
|
+
raise HTTPException(status_code=404, detail=f"Category {group}.{category} is not allowed for create")
|
|
73
|
+
|
|
74
|
+
language_slug = request.headers.get('Accept-Language')
|
|
75
|
+
language_manager: LanguageManager = schema.get_language_manager(language_slug)
|
|
76
|
+
context = {'language_manager': language_manager}
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
result: CreateResult = await schema_category.create(await request.json(), user, language_manager)
|
|
80
|
+
except AdminAPIException as e:
|
|
81
|
+
return JSONResponse(e.get_error().model_dump(mode='json', context=context), status_code=e.status_code)
|
|
82
|
+
|
|
83
|
+
return JSONResponse(content=result.model_dump(mode='json', context=context))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@router.patch(
|
|
87
|
+
path='/{group}/{category}/update/{pk}/',
|
|
88
|
+
responses={400: {"model": APIError}},
|
|
89
|
+
)
|
|
90
|
+
async def table_update(request: Request, group: str, category: str, pk: Any) -> UpdateResult:
|
|
91
|
+
schema: AdminSchema = request.app.state.schema
|
|
92
|
+
|
|
93
|
+
schema_category, user = await get_category(request, group, category, check_type=CategoryTable)
|
|
94
|
+
if not schema_category.has_update:
|
|
95
|
+
raise HTTPException(status_code=404, detail=f"Category {group}.{category} is not allowed for update")
|
|
96
|
+
|
|
97
|
+
language_slug = request.headers.get('Accept-Language')
|
|
98
|
+
language_manager: LanguageManager = schema.get_language_manager(language_slug)
|
|
99
|
+
context = {'language_manager': language_manager}
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
result: UpdateResult = await schema_category.update(pk, await request.json(), user, language_manager)
|
|
103
|
+
except AdminAPIException as e:
|
|
104
|
+
return JSONResponse(e.get_error().model_dump(mode='json', context=context), status_code=e.status_code)
|
|
105
|
+
|
|
106
|
+
return JSONResponse(content=result.model_dump(mode='json', context=context))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@router.post(
|
|
110
|
+
path='/{group}/{category}/action/{action}/',
|
|
111
|
+
responses={400: {"model": APIError}},
|
|
112
|
+
)
|
|
113
|
+
async def table_action(
|
|
114
|
+
request: Request,
|
|
115
|
+
group: str,
|
|
116
|
+
category: str,
|
|
117
|
+
action: str,
|
|
118
|
+
action_data: ActionData,
|
|
119
|
+
) -> ActionResult:
|
|
120
|
+
schema: AdminSchema = request.app.state.schema
|
|
121
|
+
|
|
122
|
+
schema_category, user = await get_category(request, group, category, check_type=CategoryTable)
|
|
123
|
+
|
|
124
|
+
language_slug = request.headers.get('Accept-Language')
|
|
125
|
+
language_manager: LanguageManager = schema.get_language_manager(language_slug)
|
|
126
|
+
context = {'language_manager': language_manager}
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
# pylint: disable=protected-access
|
|
130
|
+
result: ActionResult = await schema_category._perform_action(
|
|
131
|
+
request, action, action_data, language_manager, user,
|
|
132
|
+
)
|
|
133
|
+
except AdminAPIException as e:
|
|
134
|
+
return JSONResponse(e.get_error().model_dump(mode='json', context=context), status_code=e.status_code)
|
|
135
|
+
|
|
136
|
+
return JSONResponse(content=result.model_dump(mode='json', context=context))
|
admin_panel/auth.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from pydantic.dataclasses import dataclass
|
|
5
|
+
from admin_panel.utils import DataclassBase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class UserABC(DataclassBase, abc.ABC):
|
|
10
|
+
username: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthData(BaseModel):
|
|
14
|
+
username: str
|
|
15
|
+
password: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UserResult(BaseModel):
|
|
19
|
+
username: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AuthResult(BaseModel):
|
|
23
|
+
token: str
|
|
24
|
+
user: UserResult
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AdminAuthentication(abc.ABC):
|
|
28
|
+
async def login(self, data: AuthData) -> AuthResult:
|
|
29
|
+
raise NotImplementedError('Login is not implemented')
|
|
30
|
+
|
|
31
|
+
async def authenticate(self, headers: dict) -> UserABC:
|
|
32
|
+
raise NotImplementedError('authenticate is not implemented')
|
admin_panel/docs.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from fastapi import APIRouter, FastAPI, Request
|
|
2
|
+
from fastapi.openapi.docs import get_redoc_html
|
|
3
|
+
from fastapi.responses import HTMLResponse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_scalar_docs(app: FastAPI) -> APIRouter:
|
|
7
|
+
# pylint: disable=import-outside-toplevel
|
|
8
|
+
from scalar_fastapi import get_scalar_api_reference
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
@router.get("/scalar", include_in_schema=False)
|
|
13
|
+
async def scalar_docs(request: Request):
|
|
14
|
+
root_path = request.scope.get("root_path", "")
|
|
15
|
+
openapi_url = f"{root_path}{app.openapi_url}"
|
|
16
|
+
return get_scalar_api_reference(
|
|
17
|
+
openapi_url=openapi_url,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
return router
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js
|
|
24
|
+
def build_redoc_docs(app, redoc_url):
|
|
25
|
+
router = APIRouter()
|
|
26
|
+
|
|
27
|
+
@router.get(redoc_url, include_in_schema=False)
|
|
28
|
+
async def redoc(request: Request) -> HTMLResponse:
|
|
29
|
+
root_path = request.scope.get("root_path", "")
|
|
30
|
+
openapi_url = f"{root_path}{app.openapi_url}"
|
|
31
|
+
return get_redoc_html(
|
|
32
|
+
openapi_url=openapi_url,
|
|
33
|
+
title=f"{request.app.state.schema.title} - ReDoc",
|
|
34
|
+
redoc_js_url="https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return router
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
from pydantic.dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from admin_panel.translations import TranslateText
|
|
7
|
+
from admin_panel.utils import DataclassBase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class FieldError(DataclassBase, Exception):
|
|
12
|
+
message: str | TranslateText = None
|
|
13
|
+
code: str | None = None
|
|
14
|
+
|
|
15
|
+
def __post_init__(self):
|
|
16
|
+
if not self.message and not self.code:
|
|
17
|
+
msg = 'FieldError must contain message or code'
|
|
18
|
+
raise AttributeError(msg)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class APIError(DataclassBase):
|
|
23
|
+
message: str | TranslateText | None = None
|
|
24
|
+
code: str | None = None
|
|
25
|
+
field_errors: Dict[str, FieldError] | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class AdminAPIException(DataclassBase, Exception):
|
|
30
|
+
error: APIError = Field(default_factory=APIError)
|
|
31
|
+
status_code: int = 400
|
|
32
|
+
error_code: str | None = None
|
|
33
|
+
|
|
34
|
+
def __str__(self):
|
|
35
|
+
return str(self.error)
|
|
36
|
+
|
|
37
|
+
def get_error(self) -> APIError:
|
|
38
|
+
return self.error
|
|
File without changes
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# pylint: disable=wildcard-import, unused-wildcard-import, unused-import
|
|
2
|
+
# flake8: noqa: F405
|
|
3
|
+
from .auth import SQLAlchemyJWTAdminAuthentication
|
|
4
|
+
from .autocomplete import SQLAlchemyAdminAutocompleteMixin
|
|
5
|
+
from .fields_schema import SQLAlchemyFieldsSchema
|
|
6
|
+
from .table import *
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from admin_panel.auth import AdminAuthentication, AuthData, AuthResult, UserABC, UserResult
|
|
2
|
+
from admin_panel.exceptions import AdminAPIException, APIError
|
|
3
|
+
from admin_panel.translations import TranslateText as _
|
|
4
|
+
from admin_panel.utils import get_logger
|
|
5
|
+
|
|
6
|
+
logger = get_logger()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SQLAlchemyJWTAdminAuthentication(AdminAuthentication):
|
|
10
|
+
secret: str
|
|
11
|
+
db_async_session = None
|
|
12
|
+
user_model = None
|
|
13
|
+
pk_name = None
|
|
14
|
+
|
|
15
|
+
def __init__(self, secret: str, db_async_session, user_model, pk_name='id'):
|
|
16
|
+
self.pk_name = pk_name
|
|
17
|
+
self.secret = secret
|
|
18
|
+
self.db_async_session = db_async_session
|
|
19
|
+
self.user_model = user_model
|
|
20
|
+
|
|
21
|
+
if not isinstance(secret, str) or not secret:
|
|
22
|
+
raise ValueError("JWT secret must be a non-empty string")
|
|
23
|
+
|
|
24
|
+
# pylint: disable=import-outside-toplevel
|
|
25
|
+
from sqlalchemy.inspection import inspect
|
|
26
|
+
try:
|
|
27
|
+
import jwt
|
|
28
|
+
except ImportError as e:
|
|
29
|
+
msg = "PyJWT is not installed. Install it with: pip install pyjwt"
|
|
30
|
+
raise RuntimeError(msg) from e
|
|
31
|
+
|
|
32
|
+
assert hasattr(jwt, "encode"), "PyJWT is not installed"
|
|
33
|
+
|
|
34
|
+
mapper = inspect(user_model)
|
|
35
|
+
columns = {col.key for col in mapper.columns}
|
|
36
|
+
|
|
37
|
+
required = {self.pk_name, "username", "is_admin"}
|
|
38
|
+
missing = required - columns
|
|
39
|
+
|
|
40
|
+
if missing:
|
|
41
|
+
msg = f"user_model is missing required columns: {', '.join(sorted(missing))}"
|
|
42
|
+
raise ValueError(msg)
|
|
43
|
+
|
|
44
|
+
async def login(self, data: AuthData) -> AuthResult:
|
|
45
|
+
# pylint: disable=import-outside-toplevel
|
|
46
|
+
from sqlalchemy import select
|
|
47
|
+
|
|
48
|
+
stmt = select(self.user_model).where(self.user_model.username == data.username)
|
|
49
|
+
try:
|
|
50
|
+
async with self.db_async_session() as session:
|
|
51
|
+
result = await session.execute(stmt)
|
|
52
|
+
|
|
53
|
+
except ConnectionRefusedError as e:
|
|
54
|
+
logger.exception(
|
|
55
|
+
'SQLAlchemy %s login db error: %s', type(self).__name__, e,
|
|
56
|
+
)
|
|
57
|
+
msg = _('connection_refused_error') % {'error': str(e)}
|
|
58
|
+
raise AdminAPIException(
|
|
59
|
+
APIError(message=msg, code='connection_refused_error'),
|
|
60
|
+
status_code=500,
|
|
61
|
+
) from e
|
|
62
|
+
|
|
63
|
+
user = result.scalar_one_or_none()
|
|
64
|
+
|
|
65
|
+
if not user:
|
|
66
|
+
raise AdminAPIException(APIError(code="user_not_found"), status_code=401)
|
|
67
|
+
|
|
68
|
+
if not user.is_admin:
|
|
69
|
+
raise AdminAPIException(APIError(code="not_an_admin"), status_code=401)
|
|
70
|
+
|
|
71
|
+
return AuthResult(
|
|
72
|
+
token=self.get_token(user),
|
|
73
|
+
user=UserResult(username=user.username),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def get_token(self, user):
|
|
77
|
+
# pylint: disable=import-outside-toplevel
|
|
78
|
+
import jwt
|
|
79
|
+
|
|
80
|
+
return jwt.encode(
|
|
81
|
+
{"user_pk": str(user.id)},
|
|
82
|
+
self.secret,
|
|
83
|
+
algorithm="HS256",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
async def authenticate(self, headers: dict) -> UserABC:
|
|
87
|
+
# pylint: disable=import-outside-toplevel
|
|
88
|
+
import jwt
|
|
89
|
+
from sqlalchemy import inspect, select
|
|
90
|
+
|
|
91
|
+
token = headers.get("Authorization")
|
|
92
|
+
if not token:
|
|
93
|
+
raise AdminAPIException(
|
|
94
|
+
APIError(message="Token is not presented"),
|
|
95
|
+
status_code=401,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
token = token.replace("Token ", "")
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
payload = jwt.decode(token, self.secret, algorithms=["HS256"])
|
|
102
|
+
except jwt.exceptions.DecodeError as e:
|
|
103
|
+
raise AdminAPIException(
|
|
104
|
+
APIError(message="Token decoding error", code="token_error"),
|
|
105
|
+
status_code=401,
|
|
106
|
+
) from e
|
|
107
|
+
|
|
108
|
+
user_pk = payload.get("user_pk")
|
|
109
|
+
if not user_pk:
|
|
110
|
+
raise AdminAPIException(
|
|
111
|
+
APIError(message="Invalid token payload", code="token_error"),
|
|
112
|
+
status_code=401,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
col = inspect(self.user_model).mapper.columns[self.pk_name]
|
|
116
|
+
python_type = col.type.python_type
|
|
117
|
+
|
|
118
|
+
stmt = select(self.user_model).where(
|
|
119
|
+
getattr(self.user_model, self.pk_name) == python_type(user_pk),
|
|
120
|
+
self.user_model.is_admin.is_(True),
|
|
121
|
+
)
|
|
122
|
+
try:
|
|
123
|
+
async with self.db_async_session() as session:
|
|
124
|
+
result = await session.execute(stmt)
|
|
125
|
+
|
|
126
|
+
except ConnectionRefusedError as e:
|
|
127
|
+
logger.exception(
|
|
128
|
+
'SQLAlchemy %s authenticate db error: %s', type(self).__name__, e,
|
|
129
|
+
)
|
|
130
|
+
msg = _('connection_refused_error') % {'error': str(e)}
|
|
131
|
+
raise AdminAPIException(
|
|
132
|
+
APIError(message=msg, code='connection_refused_error'),
|
|
133
|
+
status_code=500,
|
|
134
|
+
) from e
|
|
135
|
+
|
|
136
|
+
user = result.scalar_one_or_none()
|
|
137
|
+
|
|
138
|
+
if not user:
|
|
139
|
+
raise AdminAPIException(
|
|
140
|
+
APIError(message="User not found", code="user_not_found"),
|
|
141
|
+
status_code=401,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return user
|