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.
Files changed (73) hide show
  1. admin_panel/__init__.py +4 -0
  2. admin_panel/api/__init__.py +0 -0
  3. admin_panel/api/routers.py +18 -0
  4. admin_panel/api/utils.py +28 -0
  5. admin_panel/api/views/__init__.py +0 -0
  6. admin_panel/api/views/auth.py +29 -0
  7. admin_panel/api/views/autocomplete.py +33 -0
  8. admin_panel/api/views/graphs.py +30 -0
  9. admin_panel/api/views/index.py +38 -0
  10. admin_panel/api/views/schema.py +29 -0
  11. admin_panel/api/views/settings.py +29 -0
  12. admin_panel/api/views/table.py +136 -0
  13. admin_panel/auth.py +32 -0
  14. admin_panel/docs.py +37 -0
  15. admin_panel/exceptions.py +38 -0
  16. admin_panel/integrations/__init__.py +0 -0
  17. admin_panel/integrations/sqlalchemy/__init__.py +6 -0
  18. admin_panel/integrations/sqlalchemy/auth.py +144 -0
  19. admin_panel/integrations/sqlalchemy/autocomplete.py +38 -0
  20. admin_panel/integrations/sqlalchemy/fields.py +254 -0
  21. admin_panel/integrations/sqlalchemy/fields_schema.py +316 -0
  22. admin_panel/integrations/sqlalchemy/table/__init__.py +19 -0
  23. admin_panel/integrations/sqlalchemy/table/base.py +141 -0
  24. admin_panel/integrations/sqlalchemy/table/create.py +73 -0
  25. admin_panel/integrations/sqlalchemy/table/delete.py +18 -0
  26. admin_panel/integrations/sqlalchemy/table/list.py +178 -0
  27. admin_panel/integrations/sqlalchemy/table/retrieve.py +61 -0
  28. admin_panel/integrations/sqlalchemy/table/update.py +95 -0
  29. admin_panel/schema/__init__.py +7 -0
  30. admin_panel/schema/admin_schema.py +191 -0
  31. admin_panel/schema/category.py +149 -0
  32. admin_panel/schema/graphs/__init__.py +1 -0
  33. admin_panel/schema/graphs/category_graphs.py +50 -0
  34. admin_panel/schema/group.py +67 -0
  35. admin_panel/schema/table/__init__.py +8 -0
  36. admin_panel/schema/table/admin_action.py +76 -0
  37. admin_panel/schema/table/category_table.py +175 -0
  38. admin_panel/schema/table/fields/__init__.py +5 -0
  39. admin_panel/schema/table/fields/base.py +249 -0
  40. admin_panel/schema/table/fields/function_field.py +65 -0
  41. admin_panel/schema/table/fields_schema.py +216 -0
  42. admin_panel/schema/table/table_models.py +53 -0
  43. admin_panel/static/favicon.jpg +0 -0
  44. admin_panel/static/index-BeniOHDv.js +525 -0
  45. admin_panel/static/index-vlBToOhT.css +8 -0
  46. admin_panel/static/materialdesignicons-webfont-CYDMK1kx.woff2 +0 -0
  47. admin_panel/static/materialdesignicons-webfont-CgCzGbLl.woff +0 -0
  48. admin_panel/static/materialdesignicons-webfont-D3kAzl71.ttf +0 -0
  49. admin_panel/static/materialdesignicons-webfont-DttUABo4.eot +0 -0
  50. admin_panel/static/tinymce/dark-first/content.min.css +250 -0
  51. admin_panel/static/tinymce/dark-first/skin.min.css +2820 -0
  52. admin_panel/static/tinymce/dark-slim/content.min.css +249 -0
  53. admin_panel/static/tinymce/dark-slim/skin.min.css +2821 -0
  54. admin_panel/static/tinymce/img/example.png +0 -0
  55. admin_panel/static/tinymce/img/tinymce.woff2 +0 -0
  56. admin_panel/static/tinymce/lightgray/content.min.css +1 -0
  57. admin_panel/static/tinymce/lightgray/fonts/tinymce.woff +0 -0
  58. admin_panel/static/tinymce/lightgray/skin.min.css +1 -0
  59. admin_panel/static/tinymce/plugins/accordion/css/accordion.css +17 -0
  60. admin_panel/static/tinymce/plugins/accordion/plugin.js +48 -0
  61. admin_panel/static/tinymce/plugins/codesample/css/prism.css +1 -0
  62. admin_panel/static/tinymce/plugins/customLink/css/link.css +3 -0
  63. admin_panel/static/tinymce/plugins/customLink/plugin.js +147 -0
  64. admin_panel/static/tinymce/tinymce.min.js +2 -0
  65. admin_panel/static/vanilla-picker-B6E6ObS_.js +8 -0
  66. admin_panel/templates/index.html +25 -0
  67. admin_panel/translations.py +145 -0
  68. admin_panel/utils.py +50 -0
  69. brilliance_admin-0.42.0.dist-info/METADATA +155 -0
  70. brilliance_admin-0.42.0.dist-info/RECORD +73 -0
  71. brilliance_admin-0.42.0.dist-info/WHEEL +5 -0
  72. brilliance_admin-0.42.0.dist-info/licenses/LICENSE +17 -0
  73. brilliance_admin-0.42.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,4 @@
1
+ # pylint: disable=wildcard-import, unused-wildcard-import, unused-import
2
+ # flake8: noqa: F405
3
+ from admin_panel.integrations import sqlalchemy
4
+ from admin_panel import schema
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)
@@ -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