zrb 1.0.0b9__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 (88) hide show
  1. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/.coveragerc +11 -0
  2. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/.gitignore +4 -0
  3. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_task.py +99 -55
  4. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_util.py +301 -0
  5. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/config.py +5 -0
  6. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +131 -2
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +128 -5
  8. 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
  9. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_create_my_entity.py +53 -0
  10. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_delete_my_entity.py +62 -0
  11. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_read_my_entity.py +65 -0
  12. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_update_my_entity.py +61 -0
  13. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/gateway_subroute.py +81 -13
  14. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/navigation_config_file.py +8 -0
  15. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_task.py +8 -0
  16. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +42 -3
  17. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/gateway/subroute/my_module.py +8 -1
  18. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/module_task_definition.py +10 -6
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/navigation_config_file.py +6 -0
  20. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task.py +56 -12
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task_util.py +10 -4
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_service.py +136 -52
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/parser.py +3 -3
  24. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/view.py +1 -1
  25. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +19 -8
  26. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/3093c7336477_add_auth_tables.py +46 -43
  27. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/8ed025bcc845_create_permissions.py +69 -0
  28. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +5 -2
  29. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +16 -21
  30. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/config/navigation.py +39 -0
  31. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/route.py +52 -11
  32. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/schema/navigation.py +95 -0
  33. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +277 -44
  34. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/auth.py +66 -0
  35. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/view.py +33 -8
  36. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/permission.html +311 -0
  37. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/role.html +0 -0
  38. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/user.html +0 -0
  39. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/error.html +4 -1
  40. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/login.html +67 -0
  41. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/logout.html +49 -0
  42. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/common/util.js +160 -0
  43. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/style.css +14 -0
  44. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/util.js +94 -0
  45. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/pico-style.css +23 -0
  46. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/script.js +44 -0
  47. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/style.css +102 -0
  48. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/template/default.html +73 -18
  49. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +6 -1
  50. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/permission.py +1 -0
  51. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +9 -0
  52. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/_util/access_token.py +19 -0
  53. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_create_permission.py +59 -0
  54. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_delete_permission.py +68 -0
  55. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_read_permission.py +71 -0
  56. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_update_permission.py +66 -0
  57. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/test_user_session.py +195 -0
  58. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_health_and_readiness.py +28 -0
  59. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_homepage.py +15 -0
  60. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_not_found_error.py +16 -0
  61. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test.sh +7 -0
  62. zrb/runner/web_route/refresh_token_api_route.py +1 -1
  63. zrb/runner/web_route/static/refresh-token.template.js +9 -0
  64. zrb/runner/web_route/static/static_route.py +1 -1
  65. zrb/task/base_task.py +10 -10
  66. zrb/util/codemod/modification_mode.py +3 -0
  67. zrb/util/codemod/modify_class.py +58 -0
  68. zrb/util/codemod/modify_class_parent.py +68 -0
  69. zrb/util/codemod/modify_class_property.py +128 -0
  70. zrb/util/codemod/modify_dict.py +75 -0
  71. zrb/util/codemod/modify_function.py +65 -0
  72. zrb/util/codemod/modify_function_call.py +68 -0
  73. zrb/util/codemod/modify_method.py +88 -0
  74. zrb/util/codemod/{prepend_code_to_module.py → modify_module.py} +2 -3
  75. zrb/util/file.py +3 -2
  76. zrb/util/load.py +13 -7
  77. {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/METADATA +2 -2
  78. {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/RECORD +80 -46
  79. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/migrate.py +0 -3
  80. zrb/util/codemod/append_code_to_class.py +0 -35
  81. zrb/util/codemod/append_code_to_function.py +0 -38
  82. zrb/util/codemod/append_code_to_method.py +0 -55
  83. zrb/util/codemod/append_key_to_dict.py +0 -51
  84. zrb/util/codemod/append_param_to_function_call.py +0 -39
  85. zrb/util/codemod/prepend_parent_to_class.py +0 -38
  86. zrb/util/codemod/prepend_property_to_class.py +0 -55
  87. {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/WHEEL +0 -0
  88. {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/entry_points.txt +0 -0
@@ -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>
@@ -1,6 +1,9 @@
1
1
  <main class="container">
2
2
  <article>
3
- <h2>{{error_status_code}}</h2>
3
+ <h1>{{error_status_code}}</h1>
4
4
  <p>{{error_message}}</p>
5
+ <footer>
6
+ <a role="button" class="primary" href="/">🏠 Go Back</a>
7
+ </footer>
5
8
  </article>
6
9
  </main>
@@ -0,0 +1,67 @@
1
+ <main class="container">
2
+ <article>
3
+ <h1>Login</h1>
4
+ <form id="login-form">
5
+ <fieldset>
6
+ <label>
7
+ Username
8
+ <input name="username" placeholder="Username" autocomplete="username" required />
9
+ </label>
10
+ <label>
11
+ Password
12
+ <input type="password" name="password" placeholder="Password" autocomplete="current-password" required />
13
+ </label>
14
+ </fieldset>
15
+
16
+ <button type="submit">🔓 Login</button>
17
+ </form>
18
+ <dialog id="alert" class="contrast">
19
+ <article>
20
+ <p id="alert-message"></p>
21
+ <footer>
22
+ <form method="dialog">
23
+ <button>OK</button>
24
+ </form>
25
+ </footer>
26
+ </article>
27
+ </dialog>
28
+ </article>
29
+ </main>
30
+
31
+ <script>
32
+ document.getElementById("login-form").addEventListener("submit", async function(event) {
33
+ event.preventDefault();
34
+
35
+ const form = event.target;
36
+ const formData = new FormData(form);
37
+ const alertBox = document.getElementById("alert");
38
+ const alertMessage = document.getElementById("alert-message");
39
+
40
+ alertBox.classList.add("hidden");
41
+ alertBox.classList.remove("primary", "secondary");
42
+
43
+ try {
44
+ const response = await fetch("/api/v1/user-sessions", {
45
+ method: "POST",
46
+ body: new URLSearchParams(formData), // Convert FormData to URL-encoded format
47
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
48
+ });
49
+
50
+ const result = await response.json();
51
+
52
+ if (!response.ok) {
53
+ throw new Error(result.message || "Login failed");
54
+ }
55
+ // Assume API return UTC + 0
56
+ UTIL.setAccessTokenExpiredAt(result.access_token_expired_at + "Z");
57
+
58
+ alertMessage.textContent = "Login successful! Redirecting...";
59
+ alertBox.showModal();
60
+
61
+ setTimeout(() => window.location.href = "/", 1500);
62
+ } catch (error) {
63
+ alertMessage.textContent = error.message;
64
+ alertBox.showModal();
65
+ }
66
+ });
67
+ </script>
@@ -0,0 +1,49 @@
1
+ <main class="container">
2
+ <article>
3
+ <h1>Logout</h1>
4
+ <p>Are you sure you want to log out?</p>
5
+ <footer>
6
+ <a role="button" class="secondary" href="/">❌ Cancel</a>
7
+ <button id="logout-button" class="button primary">✅ Confirm</button>
8
+ </footer>
9
+ </article>
10
+
11
+ <dialog id="alert" class="contrast">
12
+ <article>
13
+ <p id="alert-message"></p>
14
+ <footer>
15
+ <form method="dialog">
16
+ <button>OK</button>
17
+ </form>
18
+ </footer>
19
+ </article>
20
+ </dialog>
21
+ </main>
22
+
23
+ <script>
24
+ document.getElementById("logout-button").addEventListener("click", async function() {
25
+ const alertBox = document.getElementById("alert");
26
+ const alertMessage = document.getElementById("alert-message");
27
+
28
+ try {
29
+ const response = await fetch("/api/v1/user-sessions", {
30
+ method: "DELETE",
31
+ headers: { "Content-Type": "application/json" },
32
+ credentials: "include", // Include cookies in the request
33
+ });
34
+
35
+ if (!response.ok) {
36
+ throw new Error("Logout failed");
37
+ }
38
+ UTIL.unsetAccessTokenExpiredAt();
39
+
40
+ alertMessage.textContent = "Logged out successfully! Redirecting...";
41
+ alertBox.showModal();
42
+
43
+ setTimeout(() => window.location.href = "/", 1500);
44
+ } catch (error) {
45
+ alertMessage.textContent = error.message;
46
+ alertBox.showModal();
47
+ }
48
+ });
49
+ </script>
@@ -0,0 +1,160 @@
1
+ const UTIL = {
2
+ refreshUrl: "/api/v1/user-sessions",
3
+ refreshInProgress: null, // Holds the ongoing refresh request
4
+
5
+ unsetAccessTokenExpiredAt() {
6
+ localStorage.removeItem("access-token-expired-at");
7
+ },
8
+
9
+ setAccessTokenExpiredAt(expiredAt) {
10
+ localStorage.setItem("access-token-expired-at", expiredAt);
11
+ },
12
+
13
+ getAccessTokenExpiredAt() {
14
+ return localStorage.getItem("access-token-expired-at");
15
+ },
16
+
17
+ isAccessTokenExpired() {
18
+ const timestamp = new Date(this.getAccessTokenExpiredAt()).getTime();
19
+ // If expiredAt is not a valid date, timestamp will be NaN
20
+ if (isNaN(timestamp)) {
21
+ return true; // Consider it expired
22
+ }
23
+ return timestamp <= Date.now();
24
+ },
25
+
26
+ async fetchAPI(input, options = {}) {
27
+ if (this.isAccessTokenExpired()) {
28
+ await this.refreshAccessToken();
29
+ }
30
+ options.credentials ??= "include";
31
+ options.headers ??= {"Content-Type": "application/json"};
32
+ const response = await fetch(input, options);
33
+ if (!response.ok) {
34
+ const responseBody = await this._extractResponseBody(response);
35
+ const errorMessage = this._extractErrorMessage(responseBody);
36
+ if (errorMessage != null) {
37
+ throw new Error(errorMessage);
38
+ }
39
+ throw new Error(`HTTP Error: ${response.status}`);
40
+ }
41
+ return await response.json();
42
+ },
43
+
44
+ async _extractResponseBody(response) {
45
+ try {
46
+ return await response.json();
47
+ } catch(error) {
48
+ return null;
49
+ }
50
+ },
51
+
52
+ _extractErrorMessage(responseBody) {
53
+ if (typeof responseBody === "string") {
54
+ return responseBody;
55
+ }
56
+ if (Array.isArray(responseBody)) {
57
+ return responseBody.map((r => this._extractErrorMessage(r))).join("\n");
58
+ }
59
+ if (responseBody.message) {
60
+ return responseBody.message;
61
+ }
62
+ if (responseBody.msg) {
63
+ return responseBody.msg;
64
+ }
65
+ if (responseBody.detail) {
66
+ return this._extractErrorMessage(responseBody.detail);
67
+ }
68
+ return null;
69
+ },
70
+
71
+ async refreshAccessToken() {
72
+ if (this.refreshInProgress) {
73
+ return this.refreshInProgress; // Return the ongoing promise if already refreshing
74
+ }
75
+ this.refreshInProgress = (async () => {
76
+ try {
77
+ const response = await fetch(this.refreshUrl, {
78
+ method: "PUT",
79
+ headers: { "Content-Type": "application/json" },
80
+ credentials: "include", // Include cookies in the request
81
+ });
82
+ if (!response.ok) {
83
+ if (response.status === 401 || response.status === 403) {
84
+ console.warn("Skipping token refresh, authentication required.");
85
+ throw new Error("Authentication required");
86
+ }
87
+ throw new Error(`HTTP Error: ${response.status}`);
88
+ }
89
+ const result = await response.json();
90
+ // Assume API return UTC + 0
91
+ this.setAccessTokenExpiredAt(result.access_token_expired_at + "Z");
92
+ console.log("Token refreshed successfully");
93
+ } catch (error) {
94
+ console.error("Cannot refresh token", error);
95
+ throw error;
96
+ } finally {
97
+ this.refreshInProgress = null; // Reset flag after completion
98
+ }
99
+ })();
100
+ return this.refreshInProgress;
101
+ },
102
+
103
+ async refreshAccessTokenPeriodically(refreshAccessTokenIntervalSeconds) {
104
+ let shouldRefresh = true;
105
+ while (shouldRefresh) {
106
+ await new Promise(resolve => setTimeout(resolve, refreshAccessTokenIntervalSeconds * 1000));
107
+ try {
108
+ await this.refreshAccessToken();
109
+ } catch (error) {
110
+ if (error.message === "Authentication required") {
111
+ shouldRefresh = false; // Stop refreshing if authentication is required
112
+ }
113
+ }
114
+ }
115
+ },
116
+
117
+ setFormData(form, data) {
118
+ for (const key in data) {
119
+ // Only search within this form for an element with the matching name
120
+ const element = form.querySelector(`[name="${key}"]`);
121
+ if (element) {
122
+ // For checkboxes or radio buttons, update each matching element within the form
123
+ if (element.type === 'checkbox' || element.type === 'radio') {
124
+ const elements = form.querySelectorAll(`[name="${key}"]`);
125
+ elements.forEach(el => {
126
+ el.checked = (el.value === data[key]);
127
+ });
128
+ } else {
129
+ // For other types of inputs, selects, and textareas, simply set the value
130
+ element.value = data[key];
131
+ }
132
+ }
133
+ }
134
+ },
135
+
136
+ clearFormData(form) {
137
+ const elements = form.querySelectorAll("input, textarea, select");
138
+ elements.forEach(element => {
139
+ if (element.type === "checkbox" || element.type === "radio") {
140
+ element.checked = element.defaultChecked; // Restore default checked state
141
+ } else if (element.tagName === "SELECT") {
142
+ element.selectedIndex = 0; // Select the first option by default
143
+ } else {
144
+ element.value = element.defaultValue || ""; // Reset to default value or empty
145
+ }
146
+ });
147
+ },
148
+
149
+
150
+ getFormData(form) {
151
+ const formData = new FormData(form);
152
+ const data = {};
153
+ // Convert FormData to a plain object
154
+ formData.forEach((value, key) => {
155
+ data[key] = value;
156
+ });
157
+ return data;
158
+ },
159
+
160
+ };
@@ -0,0 +1,14 @@
1
+ #crud-pagination {
2
+ display: flex;
3
+ gap: 0.5rem;
4
+ }
5
+
6
+ #crud-table-fieldset {
7
+ display: grid;
8
+ grid-template-columns: 2fr 1fr 1fr
9
+ }
10
+
11
+ #crud-table-container {
12
+ width: 100%;
13
+ overflow-x: auto;
14
+ }