zrb 1.0.0b10__py3-none-any.whl → 1.1.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.
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_task.py +99 -55
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_util.py +301 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +24 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +61 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/gateway/view/content/my-module/my-entity.html +297 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/gateway_subroute.py +24 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/navigation_config_file.py +8 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_task.py +8 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +40 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/gateway/subroute/my_module.py +2 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/navigation_config_file.py +6 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/parser.py +2 -2
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/view.py +1 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +18 -8
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/config/navigation.py +39 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/route.py +52 -11
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/schema/navigation.py +95 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +91 -8
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/auth.py +9 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/view.py +33 -8
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/permission.html +311 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/role.html +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/user.html +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/error.html +4 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/login.html +67 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/logout.html +49 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/common/util.js +160 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/style.css +14 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/util.js +94 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/pico-style.css +23 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/script.js +44 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/style.css +102 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/template/default.html +73 -18
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +1 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_homepage.py +2 -4
- zrb/runner/web_route/refresh_token_api_route.py +1 -1
- zrb/runner/web_route/static/refresh-token.template.js +9 -0
- zrb/runner/web_route/static/static_route.py +1 -1
- zrb/util/load.py +13 -7
- {zrb-1.0.0b10.dist-info → zrb-1.1.0.dist-info}/METADATA +2 -2
- {zrb-1.0.0b10.dist-info → zrb-1.1.0.dist-info}/RECORD +43 -26
- {zrb-1.0.0b10.dist-info → zrb-1.1.0.dist-info}/WHEEL +0 -0
- {zrb-1.0.0b10.dist-info → zrb-1.1.0.dist-info}/entry_points.txt +0 -0
zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/schema/navigation.py
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
from my_app_name.schema.user import AuthUserResponse
|
2
|
+
from pydantic import BaseModel
|
3
|
+
|
4
|
+
|
5
|
+
class Page(BaseModel):
|
6
|
+
name: str
|
7
|
+
caption: str
|
8
|
+
url: str
|
9
|
+
permission: str | None = None
|
10
|
+
|
11
|
+
|
12
|
+
class AccessiblePage(BaseModel):
|
13
|
+
name: str
|
14
|
+
caption: str
|
15
|
+
url: str
|
16
|
+
active: bool
|
17
|
+
|
18
|
+
|
19
|
+
class PageGroup(BaseModel):
|
20
|
+
name: str
|
21
|
+
caption: str
|
22
|
+
pages: list[Page] = []
|
23
|
+
|
24
|
+
def append_page(self, submenu: Page) -> Page:
|
25
|
+
self.pages.append(submenu)
|
26
|
+
return submenu
|
27
|
+
|
28
|
+
def get_accessible_pages(
|
29
|
+
self, submenu_name: str | None = None, user: AuthUserResponse | None = None
|
30
|
+
) -> list[AccessiblePage]:
|
31
|
+
return [
|
32
|
+
AccessiblePage(
|
33
|
+
name=page.name,
|
34
|
+
caption=page.caption,
|
35
|
+
url=page.url,
|
36
|
+
active=page.name == submenu_name,
|
37
|
+
)
|
38
|
+
for page in self.pages
|
39
|
+
if _has_permission(user, page.permission)
|
40
|
+
]
|
41
|
+
|
42
|
+
|
43
|
+
class AccessiblePageGroup(BaseModel):
|
44
|
+
name: str
|
45
|
+
caption: str
|
46
|
+
pages: list[AccessiblePage]
|
47
|
+
active: bool
|
48
|
+
|
49
|
+
|
50
|
+
class Navigation(BaseModel):
|
51
|
+
items: list[PageGroup | Page] = []
|
52
|
+
|
53
|
+
def append_page_group(self, page_group: PageGroup) -> PageGroup:
|
54
|
+
self.items.append(page_group)
|
55
|
+
return page_group
|
56
|
+
|
57
|
+
def append_page(self, page: Page) -> Page:
|
58
|
+
self.items.append(page)
|
59
|
+
return page
|
60
|
+
|
61
|
+
def get_accessible_items(
|
62
|
+
self, page_name: str | None, user: AuthUserResponse | None
|
63
|
+
) -> list[AccessiblePageGroup | AccessiblePage]:
|
64
|
+
accessible_items = []
|
65
|
+
for item in self.items:
|
66
|
+
if isinstance(item, Page) and _has_permission(user, item.permission):
|
67
|
+
accessible_items.append(
|
68
|
+
AccessiblePage(
|
69
|
+
name=item.name,
|
70
|
+
caption=item.caption,
|
71
|
+
url=item.url,
|
72
|
+
active=item.name == page_name,
|
73
|
+
)
|
74
|
+
)
|
75
|
+
continue
|
76
|
+
accessible_submenus = item.get_accessible_pages(page_name, user)
|
77
|
+
if accessible_submenus:
|
78
|
+
active = any(submenu.active for submenu in accessible_submenus)
|
79
|
+
accessible_items.append(
|
80
|
+
AccessiblePageGroup(
|
81
|
+
name=item.name,
|
82
|
+
caption=item.caption,
|
83
|
+
pages=accessible_submenus,
|
84
|
+
active=active,
|
85
|
+
)
|
86
|
+
)
|
87
|
+
return accessible_items
|
88
|
+
|
89
|
+
|
90
|
+
def _has_permission(user: AuthUserResponse | None, permission: str | None):
|
91
|
+
if permission is None:
|
92
|
+
return True
|
93
|
+
if user is not None:
|
94
|
+
return user.has_permission(permission)
|
95
|
+
return False
|
zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py
CHANGED
@@ -1,14 +1,17 @@
|
|
1
|
+
import os
|
1
2
|
from typing import Annotated
|
2
3
|
|
3
|
-
from fastapi import Depends, FastAPI, Response
|
4
|
+
from fastapi import Depends, FastAPI, Request, Response
|
4
5
|
from fastapi.security import OAuth2PasswordRequestForm
|
5
|
-
from my_app_name.common.error import ForbiddenError
|
6
|
+
from my_app_name.common.error import ForbiddenError, NotFoundError
|
6
7
|
from my_app_name.module.auth.client.auth_client_factory import auth_client
|
7
8
|
from my_app_name.module.gateway.util.auth import (
|
8
9
|
get_current_user,
|
10
|
+
get_refresh_token,
|
9
11
|
set_user_session_cookie,
|
10
12
|
unset_user_session_cookie,
|
11
13
|
)
|
14
|
+
from my_app_name.module.gateway.util.view import render_content, render_error
|
12
15
|
from my_app_name.schema.permission import (
|
13
16
|
MultiplePermissionResponse,
|
14
17
|
PermissionCreate,
|
@@ -49,22 +52,56 @@ def serve_auth_route(app: FastAPI):
|
|
49
52
|
|
50
53
|
@app.put("/api/v1/user-sessions", response_model=UserSessionResponse)
|
51
54
|
async def update_user_session(
|
52
|
-
response: Response, refresh_token: str
|
55
|
+
request: Request, response: Response, refresh_token: str | None = None
|
53
56
|
) -> UserSessionResponse:
|
54
|
-
|
57
|
+
actual_refresh_token = get_refresh_token(request, refresh_token)
|
58
|
+
if actual_refresh_token is None:
|
59
|
+
raise ForbiddenError("Refresh token needed")
|
60
|
+
try:
|
61
|
+
user_session = await auth_client.update_user_session(actual_refresh_token)
|
62
|
+
except NotFoundError:
|
63
|
+
raise ForbiddenError("Session not found")
|
55
64
|
set_user_session_cookie(response, user_session)
|
56
65
|
return user_session
|
57
66
|
|
58
67
|
@app.delete("/api/v1/user-sessions", response_model=UserSessionResponse)
|
59
68
|
async def delete_user_session(
|
60
|
-
response: Response, refresh_token: str
|
69
|
+
request: Request, response: Response, refresh_token: str | None = None
|
61
70
|
) -> UserSessionResponse:
|
62
|
-
|
63
|
-
|
64
|
-
|
71
|
+
try:
|
72
|
+
actual_refresh_token = get_refresh_token(request, refresh_token)
|
73
|
+
if actual_refresh_token is None:
|
74
|
+
raise ForbiddenError("Refresh token needed")
|
75
|
+
user_session = await auth_client.delete_user_session(actual_refresh_token)
|
76
|
+
return user_session
|
77
|
+
finally:
|
78
|
+
unset_user_session_cookie(response)
|
65
79
|
|
66
80
|
# Permission routes
|
67
81
|
|
82
|
+
@app.get("/auth/permissions", include_in_schema=False)
|
83
|
+
def permissions_crud_ui(
|
84
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
85
|
+
page: int = 1,
|
86
|
+
page_size: int = 10,
|
87
|
+
sort: str | None = None,
|
88
|
+
filter: str | None = None,
|
89
|
+
):
|
90
|
+
if not current_user.has_permission("permission:read"):
|
91
|
+
return render_error(error_message="Access denied", status_code=403)
|
92
|
+
return render_content(
|
93
|
+
view_path=os.path.join("auth", "permission.html"),
|
94
|
+
current_user=current_user,
|
95
|
+
page_name="auth.permission",
|
96
|
+
page=page,
|
97
|
+
page_size=page_size,
|
98
|
+
sort=sort,
|
99
|
+
filter=filter,
|
100
|
+
allow_create=current_user.has_permission("permission:create"),
|
101
|
+
allow_update=current_user.has_permission("permission:update"),
|
102
|
+
allow_delete=current_user.has_permission("permission:delete"),
|
103
|
+
)
|
104
|
+
|
68
105
|
@app.get("/api/v1/permissions", response_model=MultiplePermissionResponse)
|
69
106
|
async def get_permissions(
|
70
107
|
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
@@ -176,6 +213,29 @@ def serve_auth_route(app: FastAPI):
|
|
176
213
|
|
177
214
|
# Role routes
|
178
215
|
|
216
|
+
@app.get("/auth/roles", include_in_schema=False)
|
217
|
+
def roles_crud_ui(
|
218
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
219
|
+
page: int = 1,
|
220
|
+
page_size: int = 10,
|
221
|
+
sort: str | None = None,
|
222
|
+
filter: str | None = None,
|
223
|
+
):
|
224
|
+
if not current_user.has_permission("role:read"):
|
225
|
+
return render_error(error_message="Access denied", status_code=403)
|
226
|
+
return render_content(
|
227
|
+
view_path=os.path.join("auth", "role.html"),
|
228
|
+
current_user=current_user,
|
229
|
+
page_name="auth.role",
|
230
|
+
page=page,
|
231
|
+
page_size=page_size,
|
232
|
+
sort=sort,
|
233
|
+
filter=filter,
|
234
|
+
allow_create=current_user.has_permission("role:create"),
|
235
|
+
allow_update=current_user.has_permission("role:update"),
|
236
|
+
allow_delete=current_user.has_permission("role:delete"),
|
237
|
+
)
|
238
|
+
|
179
239
|
@app.get("/api/v1/roles", response_model=MultipleRoleResponse)
|
180
240
|
async def get_roles(
|
181
241
|
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
@@ -283,6 +343,29 @@ def serve_auth_route(app: FastAPI):
|
|
283
343
|
|
284
344
|
# User routes
|
285
345
|
|
346
|
+
@app.get("/auth/users", include_in_schema=False)
|
347
|
+
def users_crud_ui(
|
348
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
349
|
+
page: int = 1,
|
350
|
+
page_size: int = 10,
|
351
|
+
sort: str | None = None,
|
352
|
+
filter: str | None = None,
|
353
|
+
):
|
354
|
+
if not current_user.has_permission("user:read"):
|
355
|
+
return render_error(error_message="Access denied", status_code=403)
|
356
|
+
return render_content(
|
357
|
+
view_path=os.path.join("auth", "user.html"),
|
358
|
+
current_user=current_user,
|
359
|
+
page_name="auth.user",
|
360
|
+
page=page,
|
361
|
+
page_size=page_size,
|
362
|
+
sort=sort,
|
363
|
+
filter=filter,
|
364
|
+
allow_create=current_user.has_permission("user:create"),
|
365
|
+
allow_update=current_user.has_permission("user:update"),
|
366
|
+
allow_delete=current_user.has_permission("user:delete"),
|
367
|
+
)
|
368
|
+
|
286
369
|
@app.get("/api/v1/users", response_model=MultipleUserResponse)
|
287
370
|
async def get_users(
|
288
371
|
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
@@ -15,6 +15,15 @@ from typing_extensions import Annotated
|
|
15
15
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/user-sessions", auto_error=False)
|
16
16
|
|
17
17
|
|
18
|
+
def get_refresh_token(request: Request, refresh_token: str | None) -> str | None:
|
19
|
+
if refresh_token is not None and refresh_token != "":
|
20
|
+
return refresh_token
|
21
|
+
cookie_refresh_token = request.cookies.get(APP_AUTH_REFRESH_TOKEN_COOKIE_NAME)
|
22
|
+
if cookie_refresh_token is not None and cookie_refresh_token != "":
|
23
|
+
return cookie_refresh_token
|
24
|
+
return None
|
25
|
+
|
26
|
+
|
18
27
|
async def get_current_user(
|
19
28
|
request: Request,
|
20
29
|
response: Response,
|
@@ -1,18 +1,24 @@
|
|
1
1
|
import os
|
2
2
|
from typing import Any
|
3
3
|
|
4
|
+
import my_app_name.config as CFG
|
4
5
|
from fastapi.responses import HTMLResponse
|
5
6
|
from my_app_name.common.util.view import render_page, render_str
|
6
7
|
from my_app_name.config import (
|
8
|
+
APP_AUTH_ACCESS_TOKEN_EXPIRE_MINUTES,
|
7
9
|
APP_GATEWAY_CSS_PATH_LIST,
|
8
10
|
APP_GATEWAY_FAVICON_PATH,
|
11
|
+
APP_GATEWAY_FOOTER,
|
9
12
|
APP_GATEWAY_JS_PATH_LIST,
|
10
13
|
APP_GATEWAY_LOGO_PATH,
|
14
|
+
APP_GATEWAY_PICO_CSS_PATH,
|
11
15
|
APP_GATEWAY_SUBTITLE,
|
12
16
|
APP_GATEWAY_TITLE,
|
13
17
|
APP_GATEWAY_VIEW_DEFAULT_TEMPLATE_PATH,
|
14
18
|
APP_GATEWAY_VIEW_PATH,
|
15
19
|
)
|
20
|
+
from my_app_name.module.gateway.config.navigation import APP_NAVIGATION
|
21
|
+
from my_app_name.schema.user import AuthUserResponse
|
16
22
|
|
17
23
|
_DEFAULT_TEMPLATE_PATH = os.path.join(
|
18
24
|
APP_GATEWAY_VIEW_PATH, APP_GATEWAY_VIEW_DEFAULT_TEMPLATE_PATH
|
@@ -24,32 +30,47 @@ _DEFAULT_ERROR_TEMPLATE_PATH = os.path.join(
|
|
24
30
|
_DEFAULT_PARTIALS = {
|
25
31
|
"title": APP_GATEWAY_TITLE,
|
26
32
|
"subtitle": APP_GATEWAY_SUBTITLE,
|
33
|
+
"footer": APP_GATEWAY_FOOTER,
|
27
34
|
"logo_path": APP_GATEWAY_LOGO_PATH,
|
28
35
|
"favicon_path": APP_GATEWAY_FAVICON_PATH,
|
36
|
+
"pico_css_path": APP_GATEWAY_PICO_CSS_PATH,
|
29
37
|
"css_path_list": APP_GATEWAY_CSS_PATH_LIST,
|
30
38
|
"js_path_list": APP_GATEWAY_JS_PATH_LIST,
|
39
|
+
"show_user_info": True,
|
40
|
+
"should_refresh_session": True,
|
41
|
+
"refresh_session_interval_seconds": f"{APP_AUTH_ACCESS_TOKEN_EXPIRE_MINUTES * 60 / 3}",
|
31
42
|
}
|
32
43
|
|
33
44
|
|
34
|
-
def
|
45
|
+
def render_content(
|
35
46
|
view_path: str,
|
36
47
|
template_path: str = _DEFAULT_TEMPLATE_PATH,
|
37
48
|
status_code: int = 200,
|
38
|
-
headers: dict[str, str] = None,
|
49
|
+
headers: dict[str, str] | None = None,
|
39
50
|
media_type: str | None = None,
|
51
|
+
current_user: AuthUserResponse | None = None,
|
52
|
+
page_name: str | None = None,
|
40
53
|
partials: dict[str, Any] = {},
|
41
|
-
**data: Any
|
54
|
+
**data: Any,
|
42
55
|
) -> HTMLResponse:
|
43
56
|
rendered_partials = {key: val for key, val in _DEFAULT_PARTIALS.items()}
|
44
57
|
for key, val in partials.items():
|
45
58
|
rendered_partials[key] = val
|
59
|
+
rendered_partials["page_name"] = page_name
|
60
|
+
rendered_partials["current_user"] = current_user
|
61
|
+
rendered_partials["navigations"] = APP_NAVIGATION.get_accessible_items(
|
62
|
+
page_name, current_user
|
63
|
+
)
|
46
64
|
return render_page(
|
47
65
|
template_path=template_path,
|
48
66
|
status_code=status_code,
|
49
67
|
headers=headers,
|
50
68
|
media_type=media_type,
|
51
|
-
|
52
|
-
|
69
|
+
content=render_str(
|
70
|
+
template_path=os.path.join(APP_GATEWAY_VIEW_PATH, "content", view_path),
|
71
|
+
**data,
|
72
|
+
),
|
73
|
+
**rendered_partials,
|
53
74
|
)
|
54
75
|
|
55
76
|
|
@@ -58,17 +79,21 @@ def render_error(
|
|
58
79
|
status_code: int = 500,
|
59
80
|
view_path: str = _DEFAULT_ERROR_TEMPLATE_PATH,
|
60
81
|
template_path: str = _DEFAULT_TEMPLATE_PATH,
|
61
|
-
headers: dict[str, str] = None,
|
82
|
+
headers: dict[str, str] | None = None,
|
62
83
|
media_type: str | None = None,
|
84
|
+
current_user: AuthUserResponse | None = None,
|
85
|
+
page_name: str | None = None,
|
63
86
|
partials: dict[str, Any] = {},
|
64
87
|
):
|
65
|
-
return
|
88
|
+
return render_content(
|
66
89
|
view_path=view_path,
|
67
90
|
template_path=template_path,
|
68
91
|
status_code=status_code,
|
69
92
|
headers=headers,
|
70
93
|
media_type=media_type,
|
71
|
-
|
94
|
+
current_user=current_user,
|
95
|
+
page_name=page_name,
|
96
|
+
partials={"show_user_info": False, **partials},
|
72
97
|
error_status_code=status_code,
|
73
98
|
error_message=error_message,
|
74
99
|
)
|
@@ -0,0 +1,311 @@
|
|
1
|
+
<link rel="stylesheet" href="/static/crud/style.css">
|
2
|
+
|
3
|
+
<main class="container">
|
4
|
+
<article>
|
5
|
+
<h1>Permission</h1>
|
6
|
+
|
7
|
+
<fieldset id="crud-table-fieldset" role="group" class="grid">
|
8
|
+
<input id="crud-filter" onchange="applySearch()" placeholder="🔍 Filter" aria-label="Search" />
|
9
|
+
<button onclick="applySearch()">🔍 Search</button>
|
10
|
+
{% if allow_create %}
|
11
|
+
<button class="contrast" onclick="showCreateForm(event)">➕ Add</button>
|
12
|
+
{% endif %}
|
13
|
+
</fieldset>
|
14
|
+
<div id="crud-table-container">
|
15
|
+
<table id="crud-table" class="striped">
|
16
|
+
<thead>
|
17
|
+
<tr>
|
18
|
+
<th scope="col">ID</th>
|
19
|
+
<th scope="col">Name</th>
|
20
|
+
<th scope="col">Description</th>
|
21
|
+
<!-- Update this -->
|
22
|
+
{% if allow_update or allow_delete %}
|
23
|
+
<th scope="col">Actions</th>
|
24
|
+
{% endif %}
|
25
|
+
</tr>
|
26
|
+
</thead>
|
27
|
+
<tbody></tbody>
|
28
|
+
</table>
|
29
|
+
</div>
|
30
|
+
<div id="crud-pagination"></div>
|
31
|
+
|
32
|
+
{% if allow_create %}
|
33
|
+
<dialog id="crud-create-form-dialog">
|
34
|
+
<article>
|
35
|
+
<h2>New Permission</h2>
|
36
|
+
<form id="crud-create-form">
|
37
|
+
<label>
|
38
|
+
Name:
|
39
|
+
<input type="text" name="name" required>
|
40
|
+
</label>
|
41
|
+
<label>
|
42
|
+
Description:
|
43
|
+
<input type="text" name="description" required>
|
44
|
+
</label>
|
45
|
+
<!-- Update this -->
|
46
|
+
<footer>
|
47
|
+
<button onclick="createRow(event)">➕ Save</button>
|
48
|
+
<button class="secondary" onclick="hideCreateForm(event)">❌ Cancel</button>
|
49
|
+
</footer>
|
50
|
+
</form>
|
51
|
+
</article>
|
52
|
+
</dialog>
|
53
|
+
{% endif %}
|
54
|
+
|
55
|
+
{% if allow_update %}
|
56
|
+
<dialog id="crud-update-form-dialog">
|
57
|
+
<article>
|
58
|
+
<h2>Update Permission</h2>
|
59
|
+
<form id="crud-update-form">
|
60
|
+
<label>
|
61
|
+
Name:
|
62
|
+
<input type="text" name="name" required>
|
63
|
+
</label>
|
64
|
+
<label>
|
65
|
+
Description:
|
66
|
+
<input type="text" name="description" required>
|
67
|
+
</label>
|
68
|
+
<!-- Update this -->
|
69
|
+
<footer>
|
70
|
+
<button onclick="updateRow(event)">✏️ Save</button>
|
71
|
+
<button class="secondary" onclick="hideUpdateForm(event)">❌ Cancel</button>
|
72
|
+
</footer>
|
73
|
+
</form>
|
74
|
+
</article>
|
75
|
+
</dialog>
|
76
|
+
{% endif %}
|
77
|
+
|
78
|
+
{% if allow_delete %}
|
79
|
+
<dialog id="crud-delete-form-dialog">
|
80
|
+
<article>
|
81
|
+
<h2>Delete Permission</h2>
|
82
|
+
<form id="crud-delete-form">
|
83
|
+
<label>
|
84
|
+
Name:
|
85
|
+
<input type="text" name="name" readonly>
|
86
|
+
</label>
|
87
|
+
<label>
|
88
|
+
Description:
|
89
|
+
<input type="text" name="description" readonly>
|
90
|
+
</label>
|
91
|
+
<!-- Update this -->
|
92
|
+
<footer>
|
93
|
+
<button class="secondary" onclick="hideDeleteForm()">❌ Cancel</button>
|
94
|
+
<button onclick="deleteRow()">🗑️ Delete</button>
|
95
|
+
</footer>
|
96
|
+
</form>
|
97
|
+
</article>
|
98
|
+
</dialog>
|
99
|
+
{% endif %}
|
100
|
+
|
101
|
+
<dialog id="crud-alert-dialog">
|
102
|
+
<article>
|
103
|
+
<h2 id="crud-alert-title">Error</h2>
|
104
|
+
<pre id="crud-alert-message"></pre>
|
105
|
+
<footer>
|
106
|
+
<button onclick="hideAlert(event)">Close</button>
|
107
|
+
</footer>
|
108
|
+
</article>
|
109
|
+
</dialog>
|
110
|
+
|
111
|
+
</article>
|
112
|
+
</main>
|
113
|
+
|
114
|
+
<script src="/static/crud/util.js"></script>
|
115
|
+
<script>
|
116
|
+
const apiUrl = "/api/v1/permissions";
|
117
|
+
const crudState = {
|
118
|
+
pageSize: {{page_size | tojson}},
|
119
|
+
currentPage: {{page | tojson}},
|
120
|
+
sort: {{sort | tojson}},
|
121
|
+
filter: {{filter | tojson}},
|
122
|
+
allowCreate: {{allow_create | tojson}},
|
123
|
+
allowUpdate: {{allow_update | tojson}},
|
124
|
+
allowDelete: {{allow_delete | tojson}},
|
125
|
+
updatedRowId: null,
|
126
|
+
deletedRowId: null,
|
127
|
+
};
|
128
|
+
|
129
|
+
async function applySearch() {
|
130
|
+
const filterInput = document.getElementById("crud-filter");
|
131
|
+
crudState.filter = filterInput.value;
|
132
|
+
return await fetchRows(crudState.currentPage);
|
133
|
+
}
|
134
|
+
|
135
|
+
async function fetchRows(page = null) {
|
136
|
+
try {
|
137
|
+
if (typeof page !== 'undefined' && page !== null) {
|
138
|
+
crudState.currentPage = page;
|
139
|
+
}
|
140
|
+
const defaultSearchColumn = "name"
|
141
|
+
// update address bar
|
142
|
+
const searchParam = CRUD_UTIL.getSearchParam(crudState, defaultSearchColumn, false);
|
143
|
+
const newUrl = `${window.location.pathname}?${searchParam}`;
|
144
|
+
window.history.pushState({ path: newUrl }, "", newUrl);
|
145
|
+
// update table and pagination
|
146
|
+
const apiSearchParam = CRUD_UTIL.getSearchParam(crudState, defaultSearchColumn, true);
|
147
|
+
const result = await UTIL.fetchAPI(
|
148
|
+
`${apiUrl}?${apiSearchParam}`, { method: "GET" }
|
149
|
+
);
|
150
|
+
renderRows(result.data);
|
151
|
+
const crudPagination = document.getElementById("crud-pagination");
|
152
|
+
CRUD_UTIL.renderPagination(
|
153
|
+
crudPagination, crudState, result.count, "fetchRows"
|
154
|
+
);
|
155
|
+
} catch (error) {
|
156
|
+
console.error("Error fetching items:", error);
|
157
|
+
}
|
158
|
+
}
|
159
|
+
|
160
|
+
function renderRows(rows) {
|
161
|
+
const tableBody = document.querySelector("#crud-table tbody");
|
162
|
+
tableBody.innerHTML = "";
|
163
|
+
rows.forEach(row => {
|
164
|
+
let rowComponent = getRowComponents(row);
|
165
|
+
actionColumn = "";
|
166
|
+
if (crudState.allowUpdate) {
|
167
|
+
actionColumn += `<button class="contrast" onclick="showUpdateForm('${row.id}')">✏️ Edit</button>`;
|
168
|
+
}
|
169
|
+
if (crudState.allowDelete) {
|
170
|
+
actionColumn += `<button class="secondary" onclick="showDeleteForm('${row.id}')">🗑️ Delete</button>`;
|
171
|
+
}
|
172
|
+
if (crudState.allowUpdate || crudState.allowDelete) {
|
173
|
+
actionColumn = `<td><fieldset class="grid" role="group">${actionColumn}</fieldset></td>`;
|
174
|
+
}
|
175
|
+
tableBody.innerHTML += `<tr>${rowComponent.join('')}${actionColumn}</tr>`;
|
176
|
+
});
|
177
|
+
}
|
178
|
+
|
179
|
+
function getRowComponents(row) {
|
180
|
+
let rowComponents = [];
|
181
|
+
rowComponents.push(`<td>${row.id}</td>`);
|
182
|
+
rowComponents.push(`<td>${row.name}</td>`);
|
183
|
+
rowComponents.push(`<td>${row.description}</td>`);
|
184
|
+
// Update this
|
185
|
+
return rowComponents;
|
186
|
+
}
|
187
|
+
|
188
|
+
{% if allow_create %}
|
189
|
+
async function showCreateForm(id) {
|
190
|
+
const createFormDialog = document.getElementById("crud-create-form-dialog");
|
191
|
+
const createForm = document.getElementById("crud-create-form");
|
192
|
+
UTIL.clearFormData(createForm);
|
193
|
+
createFormDialog.showModal();
|
194
|
+
}
|
195
|
+
|
196
|
+
async function createRow(event = null) {
|
197
|
+
if (event != null) {
|
198
|
+
event.preventDefault();
|
199
|
+
}
|
200
|
+
try {
|
201
|
+
const createForm = document.getElementById("crud-create-form");
|
202
|
+
const formData = JSON.stringify(UTIL.getFormData(createForm));
|
203
|
+
await UTIL.fetchAPI(apiUrl, {method: "POST", body: formData});
|
204
|
+
await fetchRows();
|
205
|
+
hideCreateForm();
|
206
|
+
} catch(error) {
|
207
|
+
showAlert("Create Permission Error", error);
|
208
|
+
}
|
209
|
+
}
|
210
|
+
|
211
|
+
function hideCreateForm(event = null) {
|
212
|
+
if (event != null) {
|
213
|
+
event.preventDefault();
|
214
|
+
}
|
215
|
+
const createFormDialog = document.getElementById("crud-create-form-dialog");
|
216
|
+
createFormDialog.close();
|
217
|
+
}
|
218
|
+
{% endif %}
|
219
|
+
|
220
|
+
{% if allow_update %}
|
221
|
+
async function showUpdateForm(id) {
|
222
|
+
crudState.updatedRowId = id;
|
223
|
+
const updateFormDialog = document.getElementById("crud-update-form-dialog");
|
224
|
+
const updateForm = document.getElementById("crud-update-form");
|
225
|
+
result = await UTIL.fetchAPI(`${apiUrl}/${id}`, { method: "GET" });
|
226
|
+
UTIL.setFormData(updateForm, result);
|
227
|
+
updateFormDialog.showModal();
|
228
|
+
}
|
229
|
+
|
230
|
+
async function updateRow(event = null) {
|
231
|
+
if (event != null) {
|
232
|
+
event.preventDefault();
|
233
|
+
}
|
234
|
+
try {
|
235
|
+
const updateForm = document.getElementById("crud-update-form");
|
236
|
+
const formData = JSON.stringify(UTIL.getFormData(updateForm));
|
237
|
+
await UTIL.fetchAPI(
|
238
|
+
`${apiUrl}/${crudState.updatedRowId}`, {method: "PUT", body: formData},
|
239
|
+
);
|
240
|
+
await fetchRows();
|
241
|
+
hideUpdateForm();
|
242
|
+
} catch(error) {
|
243
|
+
showAlert("Update Permission Error", error);
|
244
|
+
}
|
245
|
+
}
|
246
|
+
|
247
|
+
function hideUpdateForm(event = null) {
|
248
|
+
if (event != null) {
|
249
|
+
event.preventDefault();
|
250
|
+
}
|
251
|
+
const updateFormDialog = document.getElementById("crud-update-form-dialog");
|
252
|
+
updateFormDialog.close();
|
253
|
+
}
|
254
|
+
{% endif %}
|
255
|
+
|
256
|
+
{% if allow_delete %}
|
257
|
+
async function showDeleteForm(id) {
|
258
|
+
crudState.deletedRowId = id;
|
259
|
+
const deleteFormDialog = document.getElementById("crud-delete-form-dialog");
|
260
|
+
const deleteForm = document.getElementById("crud-delete-form");
|
261
|
+
result = await UTIL.fetchAPI(`${apiUrl}/${id}`, { method: "GET" });
|
262
|
+
UTIL.setFormData(deleteForm, result);
|
263
|
+
deleteFormDialog.showModal();
|
264
|
+
}
|
265
|
+
|
266
|
+
async function deleteRow(event = null) {
|
267
|
+
if (event != null) {
|
268
|
+
event.preventDefault();
|
269
|
+
}
|
270
|
+
try {
|
271
|
+
await UTIL.fetchAPI(`${apiUrl}/${crudState.deletedRowId}`, {method: "DELETE",});
|
272
|
+
await fetchRows();
|
273
|
+
hideDeleteForm();
|
274
|
+
} catch(error) {
|
275
|
+
showAlert("Delete Permission Error", error);
|
276
|
+
}
|
277
|
+
}
|
278
|
+
|
279
|
+
function hideDeleteForm(event = null) {
|
280
|
+
if (event != null) {
|
281
|
+
event.preventDefault();
|
282
|
+
}
|
283
|
+
const deleteFormDialog = document.getElementById("crud-delete-form-dialog");
|
284
|
+
deleteFormDialog.close();
|
285
|
+
}
|
286
|
+
{% endif %}
|
287
|
+
|
288
|
+
function showAlert(title, error) {
|
289
|
+
const alertDialog = document.getElementById("crud-alert-dialog");
|
290
|
+
const alertTitle = document.getElementById("crud-alert-title");
|
291
|
+
const alertMessage = document.getElementById("crud-alert-message");
|
292
|
+
const errorMessage = error.message ? error.message : String(error);
|
293
|
+
alertTitle.textContent = title;
|
294
|
+
alertMessage.textContent = errorMessage;
|
295
|
+
alertDialog.showModal();
|
296
|
+
}
|
297
|
+
|
298
|
+
function hideAlert(event = null) {
|
299
|
+
if (event != null) {
|
300
|
+
event.preventDefault();
|
301
|
+
}
|
302
|
+
const alertDialog = document.getElementById("crud-alert-dialog");
|
303
|
+
alertDialog.close();
|
304
|
+
}
|
305
|
+
|
306
|
+
document.addEventListener("DOMContentLoaded", () => {
|
307
|
+
const filterInput = document.getElementById("crud-filter");
|
308
|
+
filterInput.value = crudState.filter;
|
309
|
+
fetchRows(crudState.currentPage);
|
310
|
+
});
|
311
|
+
</script>
|
File without changes
|
File without changes
|