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.
Files changed (43) hide show
  1. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_task.py +99 -55
  2. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_util.py +301 -0
  3. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +24 -1
  4. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +61 -1
  5. 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
  6. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/gateway_subroute.py +24 -0
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/navigation_config_file.py +8 -0
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_task.py +8 -0
  9. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +40 -1
  10. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/gateway/subroute/my_module.py +2 -0
  11. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/navigation_config_file.py +6 -0
  12. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/parser.py +2 -2
  13. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/view.py +1 -1
  14. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +18 -8
  15. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/config/navigation.py +39 -0
  16. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/route.py +52 -11
  17. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/schema/navigation.py +95 -0
  18. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +91 -8
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/auth.py +9 -0
  20. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/view.py +33 -8
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/permission.html +311 -0
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/role.html +0 -0
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/user.html +0 -0
  24. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/error.html +4 -1
  25. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/login.html +67 -0
  26. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/logout.html +49 -0
  27. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/common/util.js +160 -0
  28. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/style.css +14 -0
  29. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/util.js +94 -0
  30. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/pico-style.css +23 -0
  31. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/script.js +44 -0
  32. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/style.css +102 -0
  33. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/template/default.html +73 -18
  34. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +1 -1
  35. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_homepage.py +2 -4
  36. zrb/runner/web_route/refresh_token_api_route.py +1 -1
  37. zrb/runner/web_route/static/refresh-token.template.js +9 -0
  38. zrb/runner/web_route/static/static_route.py +1 -1
  39. zrb/util/load.py +13 -7
  40. {zrb-1.0.0b10.dist-info → zrb-1.1.0.dist-info}/METADATA +2 -2
  41. {zrb-1.0.0b10.dist-info → zrb-1.1.0.dist-info}/RECORD +43 -26
  42. {zrb-1.0.0b10.dist-info → zrb-1.1.0.dist-info}/WHEEL +0 -0
  43. {zrb-1.0.0b10.dist-info → zrb-1.1.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,297 @@
1
+ <link rel="stylesheet" href="/static/crud/style.css">
2
+
3
+ <main class="container">
4
+ <article>
5
+ <h1>My Entity</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">My Column</th>
20
+ <!-- Update this -->
21
+ {% if allow_update or allow_delete %}
22
+ <th scope="col">Actions</th>
23
+ {% endif %}
24
+ </tr>
25
+ </thead>
26
+ <tbody></tbody>
27
+ </table>
28
+ </div>
29
+ <div id="crud-pagination"></div>
30
+
31
+ {% if allow_create %}
32
+ <dialog id="crud-create-form-dialog">
33
+ <article>
34
+ <h2>New My Entity</h2>
35
+ <form id="crud-create-form">
36
+ <label>
37
+ My Column:
38
+ <input type="text" name="my_column" required>
39
+ </label>
40
+ <!-- Update this -->
41
+ <footer>
42
+ <button onclick="createRow(event)">➕ Save</button>
43
+ <button class="secondary" onclick="hideCreateForm(event)">❌ Cancel</button>
44
+ </footer>
45
+ </form>
46
+ </article>
47
+ </dialog>
48
+ {% endif %}
49
+
50
+ {% if allow_update %}
51
+ <dialog id="crud-update-form-dialog">
52
+ <article>
53
+ <h2>Update My Entity</h2>
54
+ <form id="crud-update-form">
55
+ <label>
56
+ My Column:
57
+ <input type="text" name="my_column" required>
58
+ </label>
59
+ <!-- Update this -->
60
+ <footer>
61
+ <button onclick="updateRow(event)">✏️ Save</button>
62
+ <button class="secondary" onclick="hideUpdateForm(event)">❌ Cancel</button>
63
+ </footer>
64
+ </form>
65
+ </article>
66
+ </dialog>
67
+ {% endif %}
68
+
69
+ {% if allow_delete %}
70
+ <dialog id="crud-delete-form-dialog">
71
+ <article>
72
+ <h2>Delete My Entity</h2>
73
+ <form id="crud-delete-form">
74
+ <label>
75
+ My Column:
76
+ <input type="text" name="my_column" readonly>
77
+ </label>
78
+ <!-- Update this -->
79
+ <footer>
80
+ <button class="secondary" onclick="hideDeleteForm()">❌ Cancel</button>
81
+ <button onclick="deleteRow()">🗑️ Delete</button>
82
+ </footer>
83
+ </form>
84
+ </article>
85
+ </dialog>
86
+ {% endif %}
87
+
88
+ <dialog id="crud-alert-dialog">
89
+ <article>
90
+ <h2 id="crud-alert-title">Error</h2>
91
+ <pre id="crud-alert-message"></pre>
92
+ <footer>
93
+ <button onclick="hideAlert(event)">Close</button>
94
+ </footer>
95
+ </article>
96
+ </dialog>
97
+
98
+ </article>
99
+ </main>
100
+
101
+ <script src="/static/crud/util.js"></script>
102
+ <script>
103
+ const apiUrl = "/api/v1/my-entities";
104
+ const crudState = {
105
+ pageSize: {{page_size | tojson}},
106
+ currentPage: {{page | tojson}},
107
+ sort: {{sort | tojson}},
108
+ filter: {{filter | tojson}},
109
+ allowCreate: {{allow_create | tojson}},
110
+ allowUpdate: {{allow_update | tojson}},
111
+ allowDelete: {{allow_delete | tojson}},
112
+ updatedRowId: null,
113
+ deletedRowId: null,
114
+ };
115
+
116
+ async function applySearch() {
117
+ const filterInput = document.getElementById("crud-filter");
118
+ crudState.filter = filterInput.value;
119
+ return await fetchRows(crudState.currentPage);
120
+ }
121
+
122
+ async function fetchRows(page = null) {
123
+ try {
124
+ if (typeof page !== 'undefined' && page !== null) {
125
+ crudState.currentPage = page;
126
+ }
127
+ const defaultSearchColumn = "my_column"
128
+ // update address bar
129
+ const searchParam = CRUD_UTIL.getSearchParam(crudState, defaultSearchColumn, false);
130
+ const newUrl = `${window.location.pathname}?${searchParam}`;
131
+ window.history.pushState({ path: newUrl }, "", newUrl);
132
+ // update table and pagination
133
+ const apiSearchParam = CRUD_UTIL.getSearchParam(crudState, defaultSearchColumn, true);
134
+ const result = await UTIL.fetchAPI(
135
+ `${apiUrl}?${apiSearchParam}`, { method: "GET" }
136
+ );
137
+ renderRows(result.data);
138
+ const crudPagination = document.getElementById("crud-pagination");
139
+ CRUD_UTIL.renderPagination(
140
+ crudPagination, crudState, result.count, "fetchRows"
141
+ );
142
+ } catch (error) {
143
+ console.error("Error fetching items:", error);
144
+ }
145
+ }
146
+
147
+ function renderRows(rows) {
148
+ const tableBody = document.querySelector("#crud-table tbody");
149
+ tableBody.innerHTML = "";
150
+ rows.forEach(row => {
151
+ let rowComponent = getRowComponents(row);
152
+ actionColumn = "";
153
+ if (crudState.allowUpdate) {
154
+ actionColumn += `<button class="contrast" onclick="showUpdateForm('${row.id}')">✏️ Edit</button>`;
155
+ }
156
+ if (crudState.allowDelete) {
157
+ actionColumn += `<button class="secondary" onclick="showDeleteForm('${row.id}')">🗑️ Delete</button>`;
158
+ }
159
+ if (crudState.allowUpdate || crudState.allowDelete) {
160
+ actionColumn = `<td><fieldset class="grid" role="group">${actionColumn}</fieldset></td>`;
161
+ }
162
+ tableBody.innerHTML += `<tr>${rowComponent.join('')}${actionColumn}</tr>`;
163
+ });
164
+ }
165
+
166
+ function getRowComponents(row) {
167
+ let rowComponents = [];
168
+ rowComponents.push(`<td>${row.id}</td>`);
169
+ rowComponents.push(`<td>${row.my_column}</td>`);
170
+ // Update this
171
+ return rowComponents;
172
+ }
173
+
174
+ {% if allow_create %}
175
+ async function showCreateForm(id) {
176
+ const createFormDialog = document.getElementById("crud-create-form-dialog");
177
+ const createForm = document.getElementById("crud-create-form");
178
+ UTIL.clearFormData(createForm);
179
+ createFormDialog.showModal();
180
+ }
181
+
182
+ async function createRow(event = null) {
183
+ if (event != null) {
184
+ event.preventDefault();
185
+ }
186
+ try {
187
+ const createForm = document.getElementById("crud-create-form");
188
+ const formData = JSON.stringify(UTIL.getFormData(createForm));
189
+ await UTIL.fetchAPI(apiUrl, {method: "POST", body: formData});
190
+ await fetchRows();
191
+ hideCreateForm();
192
+ } catch(error) {
193
+ showAlert("Create My Entity Error", error);
194
+ }
195
+ }
196
+
197
+ function hideCreateForm(event = null) {
198
+ if (event != null) {
199
+ event.preventDefault();
200
+ }
201
+ const createFormDialog = document.getElementById("crud-create-form-dialog");
202
+ createFormDialog.close();
203
+ }
204
+ {% endif %}
205
+
206
+ {% if allow_update %}
207
+ async function showUpdateForm(id) {
208
+ crudState.updatedRowId = id;
209
+ const updateFormDialog = document.getElementById("crud-update-form-dialog");
210
+ const updateForm = document.getElementById("crud-update-form");
211
+ result = await UTIL.fetchAPI(`${apiUrl}/${id}`, { method: "GET" });
212
+ UTIL.setFormData(updateForm, result);
213
+ updateFormDialog.showModal();
214
+ }
215
+
216
+ async function updateRow(event = null) {
217
+ if (event != null) {
218
+ event.preventDefault();
219
+ }
220
+ try {
221
+ const updateForm = document.getElementById("crud-update-form");
222
+ const formData = JSON.stringify(UTIL.getFormData(updateForm));
223
+ await UTIL.fetchAPI(
224
+ `${apiUrl}/${crudState.updatedRowId}`, {method: "PUT", body: formData},
225
+ );
226
+ await fetchRows();
227
+ hideUpdateForm();
228
+ } catch(error) {
229
+ showAlert("Update My Entity Error", error);
230
+ }
231
+ }
232
+
233
+ function hideUpdateForm(event = null) {
234
+ if (event != null) {
235
+ event.preventDefault();
236
+ }
237
+ const updateFormDialog = document.getElementById("crud-update-form-dialog");
238
+ updateFormDialog.close();
239
+ }
240
+ {% endif %}
241
+
242
+ {% if allow_delete %}
243
+ async function showDeleteForm(id) {
244
+ crudState.deletedRowId = id;
245
+ const deleteFormDialog = document.getElementById("crud-delete-form-dialog");
246
+ const deleteForm = document.getElementById("crud-delete-form");
247
+ result = await UTIL.fetchAPI(`${apiUrl}/${id}`, { method: "GET" });
248
+ UTIL.setFormData(deleteForm, result);
249
+ deleteFormDialog.showModal();
250
+ }
251
+
252
+ async function deleteRow(event = null) {
253
+ if (event != null) {
254
+ event.preventDefault();
255
+ }
256
+ try {
257
+ await UTIL.fetchAPI(`${apiUrl}/${crudState.deletedRowId}`, {method: "DELETE",});
258
+ await fetchRows();
259
+ hideDeleteForm();
260
+ } catch(error) {
261
+ showAlert("Delete My Entity Error", error);
262
+ }
263
+ }
264
+
265
+ function hideDeleteForm(event = null) {
266
+ if (event != null) {
267
+ event.preventDefault();
268
+ }
269
+ const deleteFormDialog = document.getElementById("crud-delete-form-dialog");
270
+ deleteFormDialog.close();
271
+ }
272
+ {% endif %}
273
+
274
+ function showAlert(title, error) {
275
+ const alertDialog = document.getElementById("crud-alert-dialog");
276
+ const alertTitle = document.getElementById("crud-alert-title");
277
+ const alertMessage = document.getElementById("crud-alert-message");
278
+ const errorMessage = error.message ? error.message : String(error);
279
+ alertTitle.textContent = title;
280
+ alertMessage.textContent = errorMessage;
281
+ alertDialog.showModal();
282
+ }
283
+
284
+ function hideAlert(event = null) {
285
+ if (event != null) {
286
+ event.preventDefault();
287
+ }
288
+ const alertDialog = document.getElementById("crud-alert-dialog");
289
+ alertDialog.close();
290
+ }
291
+
292
+ document.addEventListener("DOMContentLoaded", () => {
293
+ const filterInput = document.getElementById("crud-filter");
294
+ filterInput.value = crudState.filter;
295
+ fetchRows(crudState.currentPage);
296
+ });
297
+ </script>
@@ -1,6 +1,30 @@
1
1
  # MyEntity routes
2
2
 
3
3
 
4
+ @app.get("/my-module/my-entities", include_in_schema=False)
5
+ def my_entities_crud_ui(
6
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
7
+ page: int = 1,
8
+ page_size: int = 10,
9
+ sort: str | None = None,
10
+ filter: str | None = None,
11
+ ):
12
+ if not current_user.has_permission("my-entity:read"):
13
+ return render_error(error_message="Access denied", status_code=403)
14
+ return render_content(
15
+ view_path=os.path.join("my-module", "my-entity.html"),
16
+ current_user=current_user,
17
+ page_name="my-module.my-entity",
18
+ page=page,
19
+ page_size=page_size,
20
+ sort=sort,
21
+ filter=filter,
22
+ allow_create=current_user.has_permission("my-entity:create"),
23
+ allow_update=current_user.has_permission("my-entity:update"),
24
+ allow_delete=current_user.has_permission("my-entity:delete"),
25
+ )
26
+
27
+
4
28
  @app.get("/api/v1/my-entities", response_model=MultipleMyEntityResponse)
5
29
  async def get_my_entities(
6
30
  current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
@@ -0,0 +1,8 @@
1
+ my_module_menu.append_page(
2
+ Page(
3
+ name="my-module.my-entity",
4
+ caption="My Entity",
5
+ url="/my-module/my-entities",
6
+ permission="my-entity:read",
7
+ )
8
+ )
@@ -10,12 +10,14 @@ from my_app_name._zrb.module.add_module_util import (
10
10
  is_app_zrb_config_file,
11
11
  is_app_zrb_task_file,
12
12
  is_gateway_module_subroute_file,
13
+ is_gateway_navigation_config_file,
13
14
  is_gateway_route_file,
14
15
  is_in_module_dir,
15
16
  update_app_config_file,
16
17
  update_app_main_file,
17
18
  update_app_zrb_config_file,
18
19
  update_app_zrb_task_file,
20
+ update_gateway_navigation_config_file,
19
21
  update_gateway_route_file,
20
22
  )
21
23
  from my_app_name._zrb.util import get_existing_module_names
@@ -91,6 +93,12 @@ scaffold_my_app_name_module = Scaffolder(
91
93
  match=is_gateway_route_file,
92
94
  transform=update_gateway_route_file,
93
95
  ),
96
+ # Register module's page to my_app_name/gateway/config/navigation.py
97
+ ContentTransformer(
98
+ name="transform-gateway-navigation-config",
99
+ match=is_gateway_navigation_config_file,
100
+ transform=update_gateway_navigation_config_file,
101
+ ),
94
102
  ],
95
103
  retries=0,
96
104
  upstream=validate_add_my_app_name_module,
@@ -7,7 +7,12 @@ from zrb.context.any_context import AnyContext
7
7
  from zrb.util.codemod.modify_dict import append_key_to_dict
8
8
  from zrb.util.codemod.modify_function import append_code_to_function
9
9
  from zrb.util.file import read_file, write_file
10
- from zrb.util.string.conversion import to_kebab_case, to_pascal_case, to_snake_case
10
+ from zrb.util.string.conversion import (
11
+ to_human_case,
12
+ to_kebab_case,
13
+ to_pascal_case,
14
+ to_snake_case,
15
+ )
11
16
 
12
17
 
13
18
  def is_app_config_file(ctx: AnyContext, file_path: str) -> bool:
@@ -22,6 +27,12 @@ def is_gateway_route_file(ctx: AnyContext, file_path: str) -> bool:
22
27
  return file_path == os.path.join(APP_DIR, "module", "gateway", "route.py")
23
28
 
24
29
 
30
+ def is_gateway_navigation_config_file(ctx: AnyContext, file_path: str) -> bool:
31
+ return file_path == os.path.join(
32
+ APP_DIR, "module", "gateway", "config", "navigation.py"
33
+ )
34
+
35
+
25
36
  def is_app_zrb_task_file(ctx: AnyContext, file_path: str) -> bool:
26
37
  return file_path == os.path.join(APP_DIR, "_zrb", "task.py")
27
38
 
@@ -194,3 +205,31 @@ def _get_module_subroute_import(existing_code: str, module_name: str) -> str | N
194
205
  if import_code in existing_code:
195
206
  return None
196
207
  return import_code
208
+
209
+
210
+ def update_gateway_navigation_config_file(
211
+ ctx: AnyContext, gateway_navigation_config_file_path: str
212
+ ):
213
+ existing_gateway_navigation_config_code = read_file(
214
+ gateway_navigation_config_file_path
215
+ )
216
+ snake_module_name = to_snake_case(ctx.input.module)
217
+ kebab_module_name = to_kebab_case(ctx.input.module)
218
+ human_module_name = to_human_case(ctx.input.module)
219
+ new_navigation_config_code = read_file(
220
+ file_path=os.path.join(
221
+ os.path.dirname(__file__), "template", "navigation_config_file.py"
222
+ ),
223
+ replace_map={
224
+ "my_module": snake_module_name,
225
+ "my-module": kebab_module_name,
226
+ "My Module": human_module_name.title(),
227
+ },
228
+ ).strip()
229
+ write_file(
230
+ file_path=gateway_navigation_config_file_path,
231
+ content=[
232
+ existing_gateway_navigation_config_code,
233
+ new_navigation_config_code,
234
+ ],
235
+ )
@@ -1,8 +1,10 @@
1
+ import os
1
2
  from typing import Annotated
2
3
 
3
4
  from fastapi import Depends, FastAPI
4
5
  from my_app_name.common.error import ForbiddenError
5
6
  from my_app_name.module.gateway.util.auth import get_current_user
7
+ from my_app_name.module.gateway.util.view import render_content, render_error
6
8
  from my_app_name.schema.user import AuthUserResponse
7
9
 
8
10
 
@@ -0,0 +1,6 @@
1
+ my_module_menu = APP_NAVIGATION.append_page_group(
2
+ PageGroup(
3
+ name="my-module",
4
+ caption="My Module",
5
+ )
6
+ )
@@ -72,7 +72,7 @@ def create_default_filter_param_parser() -> (
72
72
  # Returns [UserModel.age >= 18, UserModel.name.like("John%"), UserModel.role.in_(["admin", "user"]), UserModel.address == "123, Main St."]
73
73
  """
74
74
  filters: list[ClauseElement] = []
75
- filter_parts = split_by_comma(query)
75
+ filter_parts = split_unescaped(query)
76
76
  for part in filter_parts:
77
77
  match = re.match(r"(.+):(.+):(.+)", part)
78
78
  if match:
@@ -101,5 +101,5 @@ def create_default_filter_param_parser() -> (
101
101
  return parse_filter_param
102
102
 
103
103
 
104
- def split_by_comma(s: str, delimiter: str = ",") -> list[str]:
104
+ def split_unescaped(s: str, delimiter: str = ",") -> list[str]:
105
105
  return re.split(r"(?<!\\)" + re.escape(delimiter), s)
@@ -27,7 +27,7 @@ def render_str(template_path: str, **data: Any) -> str:
27
27
  def render_page(
28
28
  template_path: str,
29
29
  status_code: int = 200,
30
- headers: dict[str, str] = None,
30
+ headers: dict[str, str] | None = None,
31
31
  media_type: str | None = None,
32
32
  **data: Any
33
33
  ) -> HTMLResponse:
@@ -8,25 +8,35 @@ APP_VERSION = "0.1.0"
8
8
 
9
9
  APP_GATEWAY_VIEW_PATH = os.path.join(APP_PATH, "module", "gateway", "view")
10
10
  APP_GATEWAY_VIEW_DEFAULT_TEMPLATE_PATH = os.getenv(
11
- "MY_APP_GATEWAY_VIEW_DEFAULT_TEMPLATE_PATH",
11
+ "MY_APP_NAME_GATEWAY_VIEW_DEFAULT_TEMPLATE_PATH",
12
12
  os.path.join("template", "default.html"),
13
13
  )
14
- _DEFAULT_CSS_PATH = "/static/pico-css/pico.min.css"
14
+ APP_GATEWAY_PICO_CSS_COLOR = os.getenv("MY_APP_NAME_GATEWAY_PICO_CSS_COLOR", "")
15
+ APP_GATEWAY_PICO_CSS_PATH = (
16
+ "/static/pico-css/pico.min.css"
17
+ if APP_GATEWAY_PICO_CSS_COLOR == ""
18
+ else f"/static/pico-css/pico.{APP_GATEWAY_PICO_CSS_COLOR}.min.css"
19
+ )
15
20
  APP_GATEWAY_CSS_PATH_LIST = [
16
21
  path
17
- for path in os.getenv("MY_APP_GATEWAY_CSS_PATH", _DEFAULT_CSS_PATH).split(":")
22
+ for path in os.getenv("MY_APP_NAME_GATEWAY_CSS_PATH", "").split(":")
18
23
  if path != ""
19
24
  ]
20
25
  APP_GATEWAY_JS_PATH_LIST = [
21
- path for path in os.getenv("MY_APP_GATEWAY_JS_PATH", "").split(":") if path != ""
26
+ path
27
+ for path in os.getenv("MY_APP_NAME_GATEWAY_JS_PATH", "").split(":")
28
+ if path != ""
22
29
  ]
23
- APP_GATEWAY_TITLE = os.getenv("MY_APP_GATEWAY_TITLE", "My App Name")
24
- APP_GATEWAY_SUBTITLE = os.getenv("MY_APP_GATEWAY_SUBTITLE", "Just Another App")
30
+ APP_GATEWAY_TITLE = os.getenv("MY_APP_NAME_GATEWAY_TITLE", "My App Name")
31
+ APP_GATEWAY_SUBTITLE = os.getenv("MY_APP_NAME_GATEWAY_SUBTITLE", "Just Another App")
25
32
  APP_GATEWAY_LOGO_PATH = os.getenv(
26
- "MY_APP_GATEWAY_LOGO", "/static/images/android-chrome-192x192.png"
33
+ "MY_APP_NAME_GATEWAY_LOGO", "/static/images/android-chrome-192x192.png"
34
+ )
35
+ APP_GATEWAY_FOOTER = os.getenv(
36
+ "MY_APP_NAME_GATEWAY_FOOTER", f"{APP_GATEWAY_TITLE} &copy; 2025"
27
37
  )
28
38
  APP_GATEWAY_FAVICON_PATH = os.getenv(
29
- "MY_APP_GATEWAY_FAVICON", "/static/images/favicon-32x32.png"
39
+ "MY_APP_NAME_GATEWAY_FAVICON", "/static/images/favicon-32x32.png"
30
40
  )
31
41
 
32
42
  APP_MODE = os.getenv("MY_APP_NAME_MODE", "monolith")
@@ -0,0 +1,39 @@
1
+ from my_app_name.module.gateway.schema.navigation import Navigation, Page, PageGroup
2
+
3
+ APP_NAVIGATION = Navigation()
4
+
5
+ APP_NAVIGATION.append_page(Page(name="gateway.home", caption="Home", url="/"))
6
+
7
+ auth_menu = APP_NAVIGATION.append_page_group(
8
+ PageGroup(
9
+ name="auth",
10
+ caption="Authorization",
11
+ )
12
+ )
13
+
14
+ auth_menu.append_page(
15
+ Page(
16
+ name="auth.permission",
17
+ caption="Permission",
18
+ url="/auth/permissions",
19
+ permission="permission:read",
20
+ )
21
+ )
22
+
23
+ auth_menu.append_page(
24
+ Page(
25
+ name="auth.role",
26
+ caption="Role",
27
+ url="/auth/roles",
28
+ permission="role:read",
29
+ )
30
+ )
31
+
32
+ auth_menu.append_page(
33
+ Page(
34
+ name="auth.user",
35
+ caption="User",
36
+ url="/auth/users",
37
+ permission="user:read",
38
+ )
39
+ )
@@ -1,18 +1,21 @@
1
- import os
1
+ from typing import Annotated
2
2
 
3
- from fastapi import FastAPI, HTTPException, Request
3
+ from fastapi import Depends, FastAPI, HTTPException, Request
4
4
  from fastapi.exception_handlers import http_exception_handler
5
- from fastapi.responses import HTMLResponse
5
+ from fastapi.responses import HTMLResponse, RedirectResponse
6
6
  from my_app_name.common.app_factory import app
7
7
  from my_app_name.common.schema import BasicResponse
8
8
  from my_app_name.config import (
9
- APP_GATEWAY_VIEW_PATH,
9
+ APP_AUTH_ACCESS_TOKEN_COOKIE_NAME,
10
10
  APP_MAIN_MODULE,
11
11
  APP_MODE,
12
12
  APP_MODULES,
13
13
  )
14
+ from my_app_name.module.auth.client.auth_client_factory import auth_client
14
15
  from my_app_name.module.gateway.subroute.auth import serve_auth_route
15
- from my_app_name.module.gateway.util.view import render, render_error
16
+ from my_app_name.module.gateway.util.auth import get_current_user
17
+ from my_app_name.module.gateway.util.view import render_content, render_error
18
+ from my_app_name.schema.user import AuthUserResponse
16
19
 
17
20
 
18
21
  def serve_route(app: FastAPI):
@@ -21,18 +24,48 @@ def serve_route(app: FastAPI):
21
24
  if APP_MODE == "monolith" or APP_MAIN_MODULE == "gateway":
22
25
  _serve_health_check(app)
23
26
  _serve_readiness_check(app)
24
- _serve_homepage(app)
27
+ _serve_common_pages(app)
25
28
  _handle_404(app)
26
29
 
27
30
  # Serve auth routes
28
31
  serve_auth_route(app)
29
32
 
30
33
 
31
- def _serve_homepage(app: FastAPI):
34
+ def _serve_common_pages(app: FastAPI):
32
35
  @app.get("/", include_in_schema=False)
33
- def home_page():
34
- return render(
35
- view_path=os.path.join(APP_GATEWAY_VIEW_PATH, "content", "homepage.html")
36
+ def home_page(
37
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
38
+ ):
39
+ return render_content(
40
+ view_path="homepage.html",
41
+ current_user=current_user,
42
+ page_name="gateway.home",
43
+ )
44
+
45
+ @app.get("/login", include_in_schema=False)
46
+ def login_page(
47
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
48
+ ):
49
+ if not current_user.is_guest:
50
+ return RedirectResponse("/")
51
+ return render_content(
52
+ view_path="login.html",
53
+ current_user=current_user,
54
+ page_name="gateway.home",
55
+ partials={"show_user_info": False},
56
+ )
57
+
58
+ @app.get("/logout", include_in_schema=False)
59
+ def logout_page(
60
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
61
+ ):
62
+ if current_user is None:
63
+ return RedirectResponse("/")
64
+ return render_content(
65
+ view_path="logout.html",
66
+ current_user=current_user,
67
+ page_name="gateway.home",
68
+ partials={"show_user_info": False},
36
69
  )
37
70
 
38
71
 
@@ -60,7 +93,15 @@ def _handle_404(app: FastAPI):
60
93
  if request.url.path.startswith("/api"):
61
94
  # Re-raise the exception to let FastAPI handle it
62
95
  return await http_exception_handler(request, exc)
63
- return render_error(error_message="Not found", status_code=404)
96
+ # Get current user by cookies
97
+ current_user = None
98
+ cookie_access_token = request.cookies.get(APP_AUTH_ACCESS_TOKEN_COOKIE_NAME)
99
+ if cookie_access_token is not None and cookie_access_token != "":
100
+ current_user = await auth_client.get_current_user(cookie_access_token)
101
+ # Show error page
102
+ return render_error(
103
+ error_message="Not found", status_code=404, current_user=current_user
104
+ )
64
105
 
65
106
 
66
107
  serve_route(app)