hazo_auth 1.4.2 → 1.6.1

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 (354) hide show
  1. package/README.md +65 -19
  2. package/SETUP_CHECKLIST.md +779 -0
  3. package/dist/app/api/hazo_auth/change_password/route.d.ts +8 -0
  4. package/dist/app/api/hazo_auth/change_password/route.d.ts.map +1 -0
  5. package/dist/app/api/hazo_auth/change_password/route.js +98 -0
  6. package/dist/app/api/hazo_auth/forgot_password/route.d.ts +8 -0
  7. package/dist/app/api/hazo_auth/forgot_password/route.d.ts.map +1 -0
  8. package/dist/app/api/hazo_auth/forgot_password/route.js +78 -0
  9. package/dist/app/api/hazo_auth/get_auth/route.d.ts +10 -0
  10. package/dist/app/api/hazo_auth/get_auth/route.d.ts.map +1 -0
  11. package/dist/app/api/hazo_auth/get_auth/route.js +63 -0
  12. package/dist/app/api/hazo_auth/invalidate_cache/route.d.ts +14 -0
  13. package/dist/app/api/hazo_auth/invalidate_cache/route.d.ts.map +1 -0
  14. package/dist/app/api/hazo_auth/invalidate_cache/route.js +96 -0
  15. package/dist/app/api/hazo_auth/library_photo/[category]/[filename]/route.d.ts +9 -0
  16. package/dist/app/api/hazo_auth/library_photo/[category]/[filename]/route.d.ts.map +1 -0
  17. package/dist/app/api/hazo_auth/library_photo/[category]/[filename]/route.js +82 -0
  18. package/dist/app/api/hazo_auth/library_photos/route.d.ts +22 -0
  19. package/dist/app/api/hazo_auth/library_photos/route.d.ts.map +1 -0
  20. package/dist/app/api/hazo_auth/library_photos/route.js +80 -0
  21. package/dist/app/api/hazo_auth/login/route.d.ts +12 -0
  22. package/dist/app/api/hazo_auth/login/route.d.ts.map +1 -0
  23. package/dist/app/api/hazo_auth/login/route.js +140 -0
  24. package/dist/app/api/hazo_auth/logout/route.d.ts +8 -0
  25. package/dist/app/api/hazo_auth/logout/route.d.ts.map +1 -0
  26. package/dist/app/api/hazo_auth/logout/route.js +71 -0
  27. package/dist/app/api/hazo_auth/me/route.d.ts +3 -0
  28. package/dist/app/api/hazo_auth/me/route.d.ts.map +1 -0
  29. package/dist/app/api/hazo_auth/me/route.js +34 -0
  30. package/dist/app/api/hazo_auth/profile_picture/[filename]/route.d.ts +7 -0
  31. package/dist/app/api/hazo_auth/profile_picture/[filename]/route.d.ts.map +1 -0
  32. package/dist/app/api/hazo_auth/profile_picture/[filename]/route.js +43 -0
  33. package/dist/app/api/hazo_auth/register/route.d.ts +9 -0
  34. package/dist/app/api/hazo_auth/register/route.d.ts.map +1 -0
  35. package/dist/app/api/hazo_auth/register/route.js +80 -0
  36. package/dist/app/api/hazo_auth/remove_profile_picture/route.d.ts +8 -0
  37. package/dist/app/api/hazo_auth/remove_profile_picture/route.d.ts.map +1 -0
  38. package/dist/app/api/hazo_auth/remove_profile_picture/route.js +64 -0
  39. package/dist/app/api/hazo_auth/resend_verification/route.d.ts +8 -0
  40. package/dist/app/api/hazo_auth/resend_verification/route.d.ts.map +1 -0
  41. package/dist/app/api/hazo_auth/resend_verification/route.js +79 -0
  42. package/dist/app/api/hazo_auth/reset_password/route.d.ts +8 -0
  43. package/dist/app/api/hazo_auth/reset_password/route.d.ts.map +1 -0
  44. package/dist/app/api/hazo_auth/reset_password/route.js +76 -0
  45. package/dist/app/api/hazo_auth/update_user/route.d.ts +9 -0
  46. package/dist/app/api/hazo_auth/update_user/route.d.ts.map +1 -0
  47. package/dist/app/api/hazo_auth/update_user/route.js +95 -0
  48. package/dist/app/api/hazo_auth/upload_profile_picture/route.d.ts +9 -0
  49. package/dist/app/api/hazo_auth/upload_profile_picture/route.d.ts.map +1 -0
  50. package/dist/app/api/hazo_auth/upload_profile_picture/route.js +204 -0
  51. package/dist/app/api/hazo_auth/validate_reset_token/route.d.ts +6 -0
  52. package/dist/app/api/hazo_auth/validate_reset_token/route.d.ts.map +1 -0
  53. package/dist/app/api/hazo_auth/validate_reset_token/route.js +58 -0
  54. package/dist/app/api/hazo_auth/verify_email/route.d.ts +11 -0
  55. package/dist/app/api/hazo_auth/verify_email/route.d.ts.map +1 -0
  56. package/dist/app/api/hazo_auth/verify_email/route.js +63 -0
  57. package/dist/cli/generate.d.ts +7 -0
  58. package/dist/cli/generate.d.ts.map +1 -0
  59. package/dist/cli/generate.js +184 -0
  60. package/dist/cli/index.d.ts +3 -0
  61. package/dist/cli/index.d.ts.map +1 -0
  62. package/dist/cli/index.js +173 -0
  63. package/dist/cli/init.d.ts +2 -0
  64. package/dist/cli/init.d.ts.map +1 -0
  65. package/dist/cli/init.js +201 -0
  66. package/dist/cli/validate.d.ts +15 -0
  67. package/dist/cli/validate.d.ts.map +1 -0
  68. package/dist/cli/validate.js +509 -0
  69. package/dist/components/ui/card.d.ts +9 -0
  70. package/dist/components/ui/card.d.ts.map +1 -0
  71. package/dist/components/ui/card.js +45 -0
  72. package/dist/hooks/use-mobile.d.ts.map +1 -1
  73. package/dist/hooks/use-mobile.js +17 -3
  74. package/dist/lib/services/profile_picture_service.d.ts +34 -2
  75. package/dist/lib/services/profile_picture_service.d.ts.map +1 -1
  76. package/dist/lib/services/profile_picture_service.js +157 -15
  77. package/dist/page_components/forgot_password.d.ts +19 -0
  78. package/dist/page_components/forgot_password.d.ts.map +1 -0
  79. package/dist/page_components/forgot_password.js +36 -0
  80. package/dist/page_components/index.d.ts +7 -0
  81. package/dist/page_components/index.d.ts.map +1 -0
  82. package/dist/page_components/index.js +9 -0
  83. package/dist/page_components/login.d.ts +26 -0
  84. package/dist/page_components/login.d.ts.map +1 -0
  85. package/dist/page_components/login.js +40 -0
  86. package/dist/page_components/my_settings.d.ts +64 -0
  87. package/dist/page_components/my_settings.d.ts.map +1 -0
  88. package/dist/page_components/my_settings.js +67 -0
  89. package/dist/page_components/register.d.ts +25 -0
  90. package/dist/page_components/register.d.ts.map +1 -0
  91. package/dist/page_components/register.js +43 -0
  92. package/dist/page_components/reset_password.d.ts +25 -0
  93. package/dist/page_components/reset_password.d.ts.map +1 -0
  94. package/dist/page_components/reset_password.js +43 -0
  95. package/dist/page_components/verify_email.d.ts +21 -0
  96. package/dist/page_components/verify_email.d.ts.map +1 -0
  97. package/dist/page_components/verify_email.js +36 -0
  98. package/dist/server/routes/change_password.d.ts +2 -0
  99. package/dist/server/routes/change_password.d.ts.map +1 -0
  100. package/dist/server/routes/change_password.js +2 -0
  101. package/dist/server/routes/forgot_password.d.ts +2 -0
  102. package/dist/server/routes/forgot_password.d.ts.map +1 -0
  103. package/dist/server/routes/forgot_password.js +2 -0
  104. package/dist/server/routes/get_auth.d.ts +2 -0
  105. package/dist/server/routes/get_auth.d.ts.map +1 -0
  106. package/dist/server/routes/get_auth.js +2 -0
  107. package/dist/server/routes/index.d.ts +19 -0
  108. package/dist/server/routes/index.d.ts.map +1 -0
  109. package/dist/server/routes/index.js +25 -0
  110. package/dist/server/routes/invalidate_cache.d.ts +2 -0
  111. package/dist/server/routes/invalidate_cache.d.ts.map +1 -0
  112. package/dist/server/routes/invalidate_cache.js +2 -0
  113. package/dist/server/routes/library_photo.d.ts +2 -0
  114. package/dist/server/routes/library_photo.d.ts.map +1 -0
  115. package/dist/server/routes/library_photo.js +3 -0
  116. package/dist/server/routes/library_photos.d.ts +2 -0
  117. package/dist/server/routes/library_photos.d.ts.map +1 -0
  118. package/dist/server/routes/library_photos.js +2 -0
  119. package/dist/server/routes/login.d.ts +2 -0
  120. package/dist/server/routes/login.d.ts.map +1 -0
  121. package/dist/server/routes/login.js +2 -0
  122. package/dist/server/routes/logout.d.ts +2 -0
  123. package/dist/server/routes/logout.d.ts.map +1 -0
  124. package/dist/server/routes/logout.js +2 -0
  125. package/dist/server/routes/me.d.ts +2 -0
  126. package/dist/server/routes/me.d.ts.map +1 -0
  127. package/dist/server/routes/me.js +2 -0
  128. package/dist/server/routes/profile_picture_filename.d.ts +2 -0
  129. package/dist/server/routes/profile_picture_filename.d.ts.map +1 -0
  130. package/dist/server/routes/profile_picture_filename.js +3 -0
  131. package/dist/server/routes/register.d.ts +2 -0
  132. package/dist/server/routes/register.d.ts.map +1 -0
  133. package/dist/server/routes/register.js +2 -0
  134. package/dist/server/routes/remove_profile_picture.d.ts +2 -0
  135. package/dist/server/routes/remove_profile_picture.d.ts.map +1 -0
  136. package/dist/server/routes/remove_profile_picture.js +2 -0
  137. package/dist/server/routes/resend_verification.d.ts +2 -0
  138. package/dist/server/routes/resend_verification.d.ts.map +1 -0
  139. package/dist/server/routes/resend_verification.js +2 -0
  140. package/dist/server/routes/reset_password.d.ts +2 -0
  141. package/dist/server/routes/reset_password.d.ts.map +1 -0
  142. package/dist/server/routes/reset_password.js +2 -0
  143. package/dist/server/routes/update_user.d.ts +2 -0
  144. package/dist/server/routes/update_user.d.ts.map +1 -0
  145. package/dist/server/routes/update_user.js +2 -0
  146. package/dist/server/routes/upload_profile_picture.d.ts +2 -0
  147. package/dist/server/routes/upload_profile_picture.d.ts.map +1 -0
  148. package/dist/server/routes/upload_profile_picture.js +2 -0
  149. package/dist/server/routes/validate_reset_token.d.ts +2 -0
  150. package/dist/server/routes/validate_reset_token.d.ts.map +1 -0
  151. package/dist/server/routes/validate_reset_token.js +2 -0
  152. package/dist/server/routes/verify_email.d.ts +2 -0
  153. package/dist/server/routes/verify_email.d.ts.map +1 -0
  154. package/dist/server/routes/verify_email.js +2 -0
  155. package/package.json +40 -17
  156. package/components.json +0 -22
  157. package/instrumentation.ts +0 -32
  158. package/migrations/001_add_token_type_to_refresh_tokens.sql +0 -14
  159. package/migrations/002_add_name_to_hazo_users.sql +0 -7
  160. package/migrations/003_add_url_on_logon_to_hazo_users.sql +0 -8
  161. package/next.config.mjs +0 -67
  162. package/postcss.config.mjs +0 -8
  163. package/public/file.svg +0 -1
  164. package/public/globe.svg +0 -1
  165. package/public/next.svg +0 -1
  166. package/public/vercel.svg +0 -1
  167. package/public/window.svg +0 -1
  168. package/scripts/apply_migration.ts +0 -118
  169. package/scripts/init_users.ts +0 -378
  170. package/src/app/api/hazo_auth/auth/upload_profile_picture/route.ts +0 -268
  171. package/src/app/api/hazo_auth/change_password/route.ts +0 -132
  172. package/src/app/api/hazo_auth/forgot_password/route.ts +0 -107
  173. package/src/app/api/hazo_auth/get_auth/route.ts +0 -89
  174. package/src/app/api/hazo_auth/invalidate_cache/route.ts +0 -139
  175. package/src/app/api/hazo_auth/library_photos/route.ts +0 -73
  176. package/src/app/api/hazo_auth/login/route.ts +0 -181
  177. package/src/app/api/hazo_auth/logout/route.ts +0 -89
  178. package/src/app/api/hazo_auth/me/route.ts +0 -47
  179. package/src/app/api/hazo_auth/profile_picture/[filename]/route.ts +0 -67
  180. package/src/app/api/hazo_auth/register/route.ts +0 -109
  181. package/src/app/api/hazo_auth/remove_profile_picture/route.ts +0 -86
  182. package/src/app/api/hazo_auth/resend_verification/route.ts +0 -108
  183. package/src/app/api/hazo_auth/reset_password/route.ts +0 -107
  184. package/src/app/api/hazo_auth/update_user/route.ts +0 -126
  185. package/src/app/api/hazo_auth/upload_profile_picture/route.ts +0 -268
  186. package/src/app/api/hazo_auth/user_management/permissions/route.ts +0 -367
  187. package/src/app/api/hazo_auth/user_management/roles/route.ts +0 -442
  188. package/src/app/api/hazo_auth/user_management/users/roles/route.ts +0 -367
  189. package/src/app/api/hazo_auth/user_management/users/route.ts +0 -239
  190. package/src/app/api/hazo_auth/validate_reset_token/route.ts +0 -83
  191. package/src/app/api/hazo_auth/verify_email/route.ts +0 -88
  192. package/src/app/api/migrations/apply/route.ts +0 -91
  193. package/src/app/favicon.ico +0 -0
  194. package/src/app/fonts/GeistMonoVF.woff +0 -0
  195. package/src/app/fonts/GeistVF.woff +0 -0
  196. package/src/app/globals.css +0 -89
  197. package/src/app/hazo_auth/forgot_password/forgot_password_page_client.tsx +0 -60
  198. package/src/app/hazo_auth/forgot_password/page.tsx +0 -24
  199. package/src/app/hazo_auth/login/login_page_client.tsx +0 -86
  200. package/src/app/hazo_auth/login/page.tsx +0 -38
  201. package/src/app/hazo_auth/my_settings/my_settings_page_client.tsx +0 -120
  202. package/src/app/hazo_auth/my_settings/page.tsx +0 -40
  203. package/src/app/hazo_auth/register/page.tsx +0 -36
  204. package/src/app/hazo_auth/register/register_page_client.tsx +0 -81
  205. package/src/app/hazo_auth/reset_password/page.tsx +0 -29
  206. package/src/app/hazo_auth/reset_password/reset_password_page_client.tsx +0 -81
  207. package/src/app/hazo_auth/user_management/page.tsx +0 -14
  208. package/src/app/hazo_auth/user_management/user_management_page_client.tsx +0 -16
  209. package/src/app/hazo_auth/verify_email/page.tsx +0 -24
  210. package/src/app/hazo_auth/verify_email/verify_email_page_client.tsx +0 -60
  211. package/src/app/hazo_connect/api/sqlite/data/route.ts +0 -203
  212. package/src/app/hazo_connect/api/sqlite/schema/route.ts +0 -45
  213. package/src/app/hazo_connect/api/sqlite/tables/route.ts +0 -36
  214. package/src/app/hazo_connect/sqlite_admin/page.tsx +0 -51
  215. package/src/app/hazo_connect/sqlite_admin/sqlite-admin-client.tsx +0 -984
  216. package/src/app/layout.tsx +0 -43
  217. package/src/app/page.tsx +0 -170
  218. package/src/components/index.ts +0 -7
  219. package/src/components/layouts/email_verification/config/email_verification_field_config.ts +0 -86
  220. package/src/components/layouts/email_verification/hooks/use_email_verification.ts +0 -297
  221. package/src/components/layouts/email_verification/index.tsx +0 -297
  222. package/src/components/layouts/forgot_password/config/forgot_password_field_config.ts +0 -58
  223. package/src/components/layouts/forgot_password/hooks/use_forgot_password_form.ts +0 -179
  224. package/src/components/layouts/forgot_password/index.tsx +0 -168
  225. package/src/components/layouts/index.ts +0 -26
  226. package/src/components/layouts/login/config/login_field_config.ts +0 -67
  227. package/src/components/layouts/login/hooks/use_login_form.ts +0 -286
  228. package/src/components/layouts/login/index.tsx +0 -252
  229. package/src/components/layouts/my_settings/components/editable_field.tsx +0 -177
  230. package/src/components/layouts/my_settings/components/password_change_dialog.tsx +0 -301
  231. package/src/components/layouts/my_settings/components/profile_picture_dialog.tsx +0 -385
  232. package/src/components/layouts/my_settings/components/profile_picture_display.tsx +0 -66
  233. package/src/components/layouts/my_settings/components/profile_picture_gravatar_tab.tsx +0 -143
  234. package/src/components/layouts/my_settings/components/profile_picture_library_tab.tsx +0 -311
  235. package/src/components/layouts/my_settings/components/profile_picture_upload_tab.tsx +0 -341
  236. package/src/components/layouts/my_settings/config/my_settings_field_config.ts +0 -61
  237. package/src/components/layouts/my_settings/hooks/use_my_settings.ts +0 -458
  238. package/src/components/layouts/my_settings/index.tsx +0 -351
  239. package/src/components/layouts/register/config/register_field_config.ts +0 -101
  240. package/src/components/layouts/register/hooks/use_register_form.ts +0 -275
  241. package/src/components/layouts/register/index.tsx +0 -226
  242. package/src/components/layouts/reset_password/config/reset_password_field_config.ts +0 -86
  243. package/src/components/layouts/reset_password/hooks/use_reset_password_form.ts +0 -276
  244. package/src/components/layouts/reset_password/index.tsx +0 -294
  245. package/src/components/layouts/shared/components/already_logged_in_guard.tsx +0 -95
  246. package/src/components/layouts/shared/components/auth_page_shell.tsx +0 -36
  247. package/src/components/layouts/shared/components/field_error_message.tsx +0 -29
  248. package/src/components/layouts/shared/components/form_action_buttons.tsx +0 -64
  249. package/src/components/layouts/shared/components/form_field_wrapper.tsx +0 -44
  250. package/src/components/layouts/shared/components/form_header.tsx +0 -36
  251. package/src/components/layouts/shared/components/logout_button.tsx +0 -76
  252. package/src/components/layouts/shared/components/password_field.tsx +0 -72
  253. package/src/components/layouts/shared/components/profile_pic_menu.tsx +0 -321
  254. package/src/components/layouts/shared/components/profile_pic_menu_wrapper.tsx +0 -40
  255. package/src/components/layouts/shared/components/sidebar_layout_wrapper.tsx +0 -214
  256. package/src/components/layouts/shared/components/standalone_layout_wrapper.tsx +0 -53
  257. package/src/components/layouts/shared/components/two_column_auth_layout.tsx +0 -44
  258. package/src/components/layouts/shared/components/unauthorized_guard.tsx +0 -78
  259. package/src/components/layouts/shared/components/visual_panel.tsx +0 -41
  260. package/src/components/layouts/shared/config/layout_customization.ts +0 -95
  261. package/src/components/layouts/shared/data/layout_data_client.ts +0 -19
  262. package/src/components/layouts/shared/hooks/use_auth_status.ts +0 -103
  263. package/src/components/layouts/shared/hooks/use_hazo_auth.ts +0 -158
  264. package/src/components/layouts/shared/index.ts +0 -34
  265. package/src/components/layouts/shared/utils/ip_address.ts +0 -37
  266. package/src/components/layouts/shared/utils/validation.ts +0 -66
  267. package/src/components/layouts/user_management/components/roles_matrix.tsx +0 -607
  268. package/src/components/layouts/user_management/index.tsx +0 -1295
  269. package/src/components/ui/alert-dialog.tsx +0 -141
  270. package/src/components/ui/avatar.tsx +0 -50
  271. package/src/components/ui/button.tsx +0 -57
  272. package/src/components/ui/checkbox.tsx +0 -30
  273. package/src/components/ui/dialog.tsx +0 -122
  274. package/src/components/ui/dropdown-menu.tsx +0 -201
  275. package/src/components/ui/hazo_ui_tooltip.tsx +0 -67
  276. package/src/components/ui/index.ts +0 -22
  277. package/src/components/ui/input.tsx +0 -22
  278. package/src/components/ui/label.tsx +0 -26
  279. package/src/components/ui/separator.tsx +0 -31
  280. package/src/components/ui/sheet.tsx +0 -139
  281. package/src/components/ui/sidebar.tsx +0 -773
  282. package/src/components/ui/skeleton.tsx +0 -15
  283. package/src/components/ui/sonner.tsx +0 -31
  284. package/src/components/ui/switch.tsx +0 -29
  285. package/src/components/ui/table.tsx +0 -120
  286. package/src/components/ui/tabs.tsx +0 -55
  287. package/src/components/ui/tooltip.tsx +0 -32
  288. package/src/components/ui/vertical-tabs.tsx +0 -59
  289. package/src/hooks/use-mobile.tsx +0 -19
  290. package/src/index.ts +0 -7
  291. package/src/lib/already_logged_in_config.server.ts +0 -46
  292. package/src/lib/app_logger.ts +0 -24
  293. package/src/lib/auth/auth_cache.ts +0 -220
  294. package/src/lib/auth/auth_rate_limiter.ts +0 -121
  295. package/src/lib/auth/auth_types.ts +0 -65
  296. package/src/lib/auth/auth_utils.server.ts +0 -196
  297. package/src/lib/auth/hazo_get_auth.server.ts +0 -333
  298. package/src/lib/auth/index.ts +0 -23
  299. package/src/lib/auth/server_auth.ts +0 -88
  300. package/src/lib/auth_utility_config.server.ts +0 -136
  301. package/src/lib/config/config_loader.server.ts +0 -164
  302. package/src/lib/email_verification_config.server.ts +0 -32
  303. package/src/lib/file_types_config.server.ts +0 -25
  304. package/src/lib/forgot_password_config.server.ts +0 -32
  305. package/src/lib/hazo_connect_instance.server.ts +0 -101
  306. package/src/lib/hazo_connect_setup.server.ts +0 -194
  307. package/src/lib/hazo_connect_setup.ts +0 -54
  308. package/src/lib/index.ts +0 -44
  309. package/src/lib/login_config.server.ts +0 -71
  310. package/src/lib/messages_config.server.ts +0 -45
  311. package/src/lib/migrations/apply_migration.ts +0 -105
  312. package/src/lib/my_settings_config.server.ts +0 -135
  313. package/src/lib/password_requirements_config.server.ts +0 -39
  314. package/src/lib/profile_pic_menu_config.server.ts +0 -138
  315. package/src/lib/profile_picture_config.server.ts +0 -56
  316. package/src/lib/register_config.server.ts +0 -73
  317. package/src/lib/reset_password_config.server.ts +0 -75
  318. package/src/lib/services/email_service.ts +0 -581
  319. package/src/lib/services/email_verification_service.ts +0 -270
  320. package/src/lib/services/index.ts +0 -15
  321. package/src/lib/services/login_service.ts +0 -134
  322. package/src/lib/services/password_change_service.ts +0 -154
  323. package/src/lib/services/password_reset_service.ts +0 -405
  324. package/src/lib/services/profile_picture_remove_service.ts +0 -120
  325. package/src/lib/services/profile_picture_service.ts +0 -215
  326. package/src/lib/services/profile_picture_source_mapper.ts +0 -62
  327. package/src/lib/services/registration_service.ts +0 -184
  328. package/src/lib/services/token_service.ts +0 -240
  329. package/src/lib/services/user_profiles_service.ts +0 -143
  330. package/src/lib/services/user_update_service.ts +0 -141
  331. package/src/lib/ui_shell_config.server.ts +0 -73
  332. package/src/lib/ui_sizes_config.server.ts +0 -37
  333. package/src/lib/user_fields_config.server.ts +0 -31
  334. package/src/lib/user_management_config.server.ts +0 -39
  335. package/src/lib/utils/api_route_helpers.ts +0 -60
  336. package/src/lib/utils/error_sanitizer.ts +0 -75
  337. package/src/lib/utils.ts +0 -11
  338. package/src/middleware.ts +0 -94
  339. package/src/routes/index.ts +0 -34
  340. package/src/server/config/config_loader.ts +0 -496
  341. package/src/server/index.ts +0 -38
  342. package/src/server/logging/logger_service.ts +0 -56
  343. package/src/server/routes/root_router.ts +0 -16
  344. package/src/server/server.ts +0 -28
  345. package/src/server/types/app_types.ts +0 -74
  346. package/src/server/types/express.d.ts +0 -16
  347. package/src/stories/email_verification_layout.stories.tsx +0 -137
  348. package/src/stories/forgot_password_layout.stories.tsx +0 -85
  349. package/src/stories/login_layout.stories.tsx +0 -85
  350. package/src/stories/project_overview.stories.tsx +0 -33
  351. package/src/stories/register_layout.stories.tsx +0 -107
  352. package/tailwind.config.ts +0 -77
  353. package/tsconfig.build.json +0 -36
  354. package/tsconfig.json +0 -28
@@ -1,984 +0,0 @@
1
- // file_description: SQLite admin UI client component for browsing and editing database tables
2
- "use client"
3
-
4
- import { useEffect, useMemo, useState } from "react"
5
- import {
6
- Filter,
7
- Loader2,
8
- Pencil,
9
- Plus,
10
- RefreshCw,
11
- Save,
12
- Trash2,
13
- X
14
- } from "lucide-react"
15
- import { toast } from "sonner"
16
- import type {
17
- SqliteFilterOperator,
18
- TableColumn,
19
- TableSchema,
20
- TableSummary
21
- } from "hazo_connect/ui"
22
-
23
- type FilterState = {
24
- column?: string
25
- operator: SqliteFilterOperator
26
- value: string
27
- }
28
-
29
- type SqlValue = Record<string, unknown>
30
- type DataResponse = {
31
- data: SqlValue[]
32
- total: number
33
- }
34
-
35
- const DEFAULT_LIMIT = 20
36
- const filterOperators: { label: string; value: SqliteFilterOperator }[] = [
37
- { label: "Equals", value: "eq" },
38
- { label: "Not equal", value: "neq" },
39
- { label: "Greater than", value: "gt" },
40
- { label: "Greater or equal", value: "gte" },
41
- { label: "Less than", value: "lt" },
42
- { label: "Less or equal", value: "lte" },
43
- { label: "Contains", value: "like" },
44
- { label: "Contains (case-insensitive)", value: "ilike" },
45
- { label: "Is / Is Not", value: "is" }
46
- ]
47
-
48
- export default function SqliteAdminClient({
49
- initialTables
50
- }: {
51
- initialTables: TableSummary[]
52
- }) {
53
- const [tables, setTables] = useState<TableSummary[]>(initialTables)
54
- const [selectedTable, setSelectedTable] = useState<TableSummary | null>(
55
- initialTables[0] ?? null
56
- )
57
- const [schema, setSchema] = useState<TableSchema | null>(null)
58
- const [rows, setRows] = useState<SqlValue[]>([])
59
- const [totalRows, setTotalRows] = useState(0)
60
- const [limit, setLimit] = useState(DEFAULT_LIMIT)
61
- const [offset, setOffset] = useState(0)
62
- const [orderBy, setOrderBy] = useState<string | undefined>()
63
- const [orderDirection, setOrderDirection] = useState<"asc" | "desc">("asc")
64
- const [filterState, setFilterState] = useState<FilterState>({
65
- operator: "eq",
66
- value: ""
67
- })
68
-
69
- const [isLoadingSchema, setIsLoadingSchema] = useState(false)
70
- const [isLoadingData, setIsLoadingData] = useState(false)
71
- const [isRefreshingTables, setIsRefreshingTables] = useState(false)
72
- const [isCreateOpen, setIsCreateOpen] = useState(false)
73
- const [editingRow, setEditingRow] = useState<SqlValue | null>(null)
74
-
75
- const columns = useMemo<TableColumn[]>(() => schema?.columns ?? [], [schema])
76
- const filterColumns = useMemo(
77
- () => columns.filter(column => identifierIsFilterable(column.name)),
78
- [columns]
79
- )
80
-
81
- useEffect(() => {
82
- if (!selectedTable) {
83
- return
84
- }
85
- void loadSchemaAndData(selectedTable.name)
86
- // eslint-disable-next-line react-hooks/exhaustive-deps
87
- }, [selectedTable?.name])
88
-
89
- async function loadSchemaAndData(tableName: string) {
90
- setIsLoadingSchema(true)
91
- try {
92
- const schemaResponse = await fetch(`/hazo_connect/api/sqlite/schema?table=${encodeURIComponent(tableName)}`)
93
- if (!schemaResponse.ok) {
94
- const contentType = schemaResponse.headers.get("content-type");
95
- if (contentType && contentType.includes("application/json")) {
96
- const errorData = await schemaResponse.json();
97
- throw new Error(errorData.error || `Failed to fetch schema: ${schemaResponse.statusText}`);
98
- } else {
99
- const errorText = await schemaResponse.text();
100
- throw new Error(`Failed to fetch schema: ${errorText.substring(0, 200)}`);
101
- }
102
- }
103
- const contentType = schemaResponse.headers.get("content-type");
104
- if (!contentType || !contentType.includes("application/json")) {
105
- const text = await schemaResponse.text();
106
- throw new Error(`Expected JSON but received ${contentType || "unknown content type"}. Response: ${text.substring(0, 200)}`);
107
- }
108
- const schemaJson = await schemaResponse.json()
109
- setSchema(schemaJson.data as TableSchema)
110
- const preferredFilterColumn =
111
- filterState.column && schemaJson.data?.columns?.some((col: TableColumn) => col.name === filterState.column)
112
- ? filterState.column
113
- : schemaJson.data?.columns?.[0]?.name
114
- setFilterState(current => ({
115
- ...current,
116
- column: preferredFilterColumn
117
- }))
118
- setLimit(DEFAULT_LIMIT)
119
- setOffset(0)
120
- setOrderBy(undefined)
121
- setOrderDirection("asc")
122
- await loadData(tableName, {
123
- limit: DEFAULT_LIMIT,
124
- offset: 0,
125
- orderBy: undefined,
126
- orderDirection: "asc",
127
- filterOverride: {
128
- ...filterState,
129
- column: preferredFilterColumn
130
- }
131
- })
132
- } catch (error) {
133
- toast.error(
134
- error instanceof Error
135
- ? error.message
136
- : `Failed to load schema for table '${tableName}'`
137
- )
138
- } finally {
139
- setIsLoadingSchema(false)
140
- }
141
- }
142
-
143
- async function loadData(
144
- tableName: string,
145
- overrides?: {
146
- limit?: number
147
- offset?: number
148
- orderBy?: string
149
- orderDirection?: "asc" | "desc"
150
- filterOverride?: FilterState
151
- }
152
- ) {
153
- setIsLoadingData(true)
154
- const nextLimit = overrides?.limit ?? limit
155
- const nextOffset = overrides?.offset ?? offset
156
- const nextOrderBy = overrides?.orderBy ?? orderBy
157
- const nextOrderDirection = overrides?.orderDirection ?? orderDirection
158
- const nextFilter = overrides?.filterOverride ?? filterState
159
-
160
- try {
161
- const params = new URLSearchParams({
162
- table: tableName,
163
- limit: String(nextLimit),
164
- offset: String(nextOffset)
165
- })
166
-
167
- if (nextOrderBy) {
168
- params.set("orderBy", nextOrderBy)
169
- params.set("orderDirection", nextOrderDirection)
170
- }
171
-
172
- if (nextFilter.column && nextFilter.value.trim().length) {
173
- const key =
174
- nextFilter.operator === "eq"
175
- ? `filter[${nextFilter.column}]`
176
- : `filter[${nextFilter.column}][${nextFilter.operator}]`
177
- params.set(key, nextFilter.value)
178
- } else if (nextFilter.operator === "is" && nextFilter.column) {
179
- const key = `filter[${nextFilter.column}][is]`
180
- params.set(key, nextFilter.value || "null")
181
- }
182
-
183
- const response = await fetch(`/hazo_connect/api/sqlite/data?${params.toString()}`)
184
- if (!response.ok) {
185
- const contentType = response.headers.get("content-type");
186
- if (contentType && contentType.includes("application/json")) {
187
- const errorData = await response.json();
188
- throw new Error(errorData.error || `Failed to load data: ${response.statusText}`);
189
- } else {
190
- const errorText = await response.text();
191
- throw new Error(`Failed to load data: ${errorText.substring(0, 200)}`);
192
- }
193
- }
194
-
195
- const contentType = response.headers.get("content-type");
196
- if (!contentType || !contentType.includes("application/json")) {
197
- const text = await response.text();
198
- throw new Error(`Expected JSON but received ${contentType || "unknown content type"}. Response: ${text.substring(0, 200)}`);
199
- }
200
-
201
- const json = (await response.json()) as DataResponse
202
- setRows(json.data ?? [])
203
- setTotalRows(json.total ?? 0)
204
- setLimit(nextLimit)
205
- setOffset(nextOffset)
206
- setOrderBy(nextOrderBy)
207
- setOrderDirection(nextOrderDirection)
208
- setFilterState(nextFilter)
209
- } catch (error) {
210
- toast.error(
211
- error instanceof Error
212
- ? error.message
213
- : `Failed to load data for table '${tableName}'`
214
- )
215
- } finally {
216
- setIsLoadingData(false)
217
- }
218
- }
219
-
220
- async function refreshTables() {
221
- setIsRefreshingTables(true)
222
- try {
223
- const response = await fetch("/hazo_connect/api/sqlite/tables")
224
- if (!response.ok) {
225
- const contentType = response.headers.get("content-type");
226
- if (contentType && contentType.includes("application/json")) {
227
- const errorData = await response.json();
228
- throw new Error(errorData.error || `Failed to refresh tables: ${response.statusText}`);
229
- } else {
230
- const errorText = await response.text();
231
- throw new Error(`Failed to refresh tables: ${errorText.substring(0, 200)}`);
232
- }
233
- }
234
- const contentType = response.headers.get("content-type");
235
- if (!contentType || !contentType.includes("application/json")) {
236
- const text = await response.text();
237
- throw new Error(`Expected JSON but received ${contentType || "unknown content type"}. Response: ${text.substring(0, 200)}`);
238
- }
239
- const json = await response.json()
240
- setTables(json.data ?? [])
241
- toast.success("Tables refreshed")
242
- } catch (error) {
243
- toast.error(
244
- error instanceof Error ? error.message : "Failed to refresh tables"
245
- )
246
- } finally {
247
- setIsRefreshingTables(false)
248
- }
249
- }
250
-
251
- function handleSelectTable(table: TableSummary) {
252
- setSelectedTable(table)
253
- }
254
-
255
- function handleChangePage(nextOffset: number) {
256
- if (!selectedTable) {
257
- return
258
- }
259
- void loadData(selectedTable.name, { offset: Math.max(0, nextOffset) })
260
- }
261
-
262
- function handleChangeLimit(nextLimit: number) {
263
- if (!selectedTable) {
264
- return
265
- }
266
- void loadData(selectedTable.name, { limit: nextLimit, offset: 0 })
267
- }
268
-
269
- function handleChangeOrder(column?: string) {
270
- if (!selectedTable) {
271
- return
272
- }
273
- const nextDirection =
274
- orderBy === column
275
- ? orderDirection === "asc"
276
- ? "desc"
277
- : "asc"
278
- : "asc"
279
- void loadData(selectedTable.name, {
280
- orderBy: column,
281
- orderDirection: nextDirection
282
- })
283
- }
284
-
285
- function handleApplyFilter() {
286
- if (!selectedTable) {
287
- return
288
- }
289
- void loadData(selectedTable.name, { offset: 0 })
290
- }
291
-
292
- async function handleInsertRow(data: Record<string, unknown>) {
293
- if (!selectedTable) {
294
- return
295
- }
296
-
297
- try {
298
- const response = await fetch("/hazo_connect/api/sqlite/data", {
299
- method: "POST",
300
- headers: {
301
- "Content-Type": "application/json"
302
- },
303
- body: JSON.stringify({ table: selectedTable.name, data })
304
- })
305
-
306
- if (!response.ok) {
307
- throw new Error(await response.text())
308
- }
309
-
310
- toast.success("Row inserted")
311
- setIsCreateOpen(false)
312
- await Promise.all([
313
- refreshTables(),
314
- loadData(selectedTable.name, { offset: 0 })
315
- ])
316
- } catch (error) {
317
- toast.error(error instanceof Error ? error.message : "Insert failed")
318
- }
319
- }
320
-
321
- async function handleUpdateRow(
322
- row: SqlValue,
323
- data: Record<string, unknown>
324
- ) {
325
- if (!selectedTable || !schema) {
326
- return
327
- }
328
-
329
- const criteria = buildCriteriaFromRow(row, schema.columns)
330
- try {
331
- const response = await fetch("/hazo_connect/api/sqlite/data", {
332
- method: "PATCH",
333
- headers: {
334
- "Content-Type": "application/json"
335
- },
336
- body: JSON.stringify({
337
- table: selectedTable.name,
338
- data,
339
- criteria
340
- })
341
- })
342
-
343
- if (!response.ok) {
344
- throw new Error(await response.text())
345
- }
346
-
347
- toast.success("Row updated")
348
- setEditingRow(null)
349
- await loadData(selectedTable.name)
350
- } catch (error) {
351
- toast.error(error instanceof Error ? error.message : "Update failed")
352
- }
353
- }
354
-
355
- async function handleDeleteRow(row: SqlValue) {
356
- if (!selectedTable || !schema) {
357
- return
358
- }
359
-
360
- const criteria = buildCriteriaFromRow(row, schema.columns)
361
- if (!Object.keys(criteria).length) {
362
- toast.error("Unable to determine primary key for row deletion")
363
- return
364
- }
365
-
366
- if (!window.confirm("Delete this row? This action cannot be undone.")) {
367
- return
368
- }
369
-
370
- try {
371
- const response = await fetch("/hazo_connect/api/sqlite/data", {
372
- method: "DELETE",
373
- headers: {
374
- "Content-Type": "application/json"
375
- },
376
- body: JSON.stringify({
377
- table: selectedTable.name,
378
- criteria
379
- })
380
- })
381
-
382
- if (!response.ok) {
383
- throw new Error(await response.text())
384
- }
385
-
386
- toast.success("Row deleted")
387
- await loadData(selectedTable.name, { offset: 0 })
388
- await refreshTables()
389
- } catch (error) {
390
- toast.error(error instanceof Error ? error.message : "Delete failed")
391
- }
392
- }
393
-
394
- const totalPages = Math.max(1, Math.ceil(totalRows / limit))
395
- const currentPage = Math.min(totalPages, Math.floor(offset / limit) + 1)
396
-
397
- return (
398
- <div className="flex min-h-[calc(100vh-4rem)] flex-col gap-4 p-6 lg:flex-row">
399
- <aside className="w-full max-w-xs rounded-lg border border-slate-200 bg-white shadow-sm lg:sticky lg:top-6 lg:h-[calc(100vh-6rem)]">
400
- <header className="flex items-center justify-between border-b border-slate-100 px-4 py-3">
401
- <h2 className="text-sm font-medium text-slate-700">Tables</h2>
402
- <button
403
- type="button"
404
- onClick={refreshTables}
405
- disabled={isRefreshingTables}
406
- className="inline-flex items-center gap-1 rounded-md border border-slate-200 bg-white px-2 py-1 text-xs font-medium text-slate-600 transition hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50"
407
- >
408
- {isRefreshingTables ? (
409
- <Loader2 className="h-3 w-3 animate-spin" />
410
- ) : (
411
- <RefreshCw className="h-3 w-3" />
412
- )}
413
- Refresh
414
- </button>
415
- </header>
416
- <nav className="max-h-[calc(100vh-9rem)] overflow-auto px-2 py-3 text-sm">
417
- {tables.length === 0 ? (
418
- <p className="rounded-md bg-slate-50 p-3 text-slate-500">
419
- No tables detected.
420
- </p>
421
- ) : (
422
- <ul className="space-y-1">
423
- {tables.map(table => {
424
- const isActive = selectedTable?.name === table.name
425
- return (
426
- <li key={table.name}>
427
- <button
428
- type="button"
429
- onClick={() => handleSelectTable(table)}
430
- className={[
431
- "flex w-full items-center justify-between rounded-md px-3 py-2 text-left transition",
432
- isActive
433
- ? "bg-slate-900 text-white shadow-sm"
434
- : "bg-white text-slate-700 hover:bg-slate-100"
435
- ].join(" ")}
436
- >
437
- <span className="truncate font-medium">{table.name}</span>
438
- {typeof table.row_count === "number" && (
439
- <span
440
- className={[
441
- "ml-2 inline-flex min-w-[2rem] items-center justify-center rounded-full px-2 text-xs",
442
- isActive
443
- ? "bg-slate-800 text-slate-100"
444
- : "bg-slate-100 text-slate-600"
445
- ].join(" ")}
446
- >
447
- {table.row_count}
448
- </span>
449
- )}
450
- </button>
451
- </li>
452
- )
453
- })}
454
- </ul>
455
- )}
456
- </nav>
457
- </aside>
458
-
459
- <section className="flex-1 space-y-6">
460
- <header className="flex flex-col gap-3 rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
461
- <div className="flex flex-wrap items-center justify-between gap-3">
462
- <div>
463
- <h1 className="text-xl font-semibold text-slate-900">
464
- SQLite Admin
465
- </h1>
466
- <p className="text-sm text-slate-500">
467
- Browse tables, inspect schema, and edit data safely.
468
- </p>
469
- </div>
470
- <div className="flex items-center gap-2">
471
- <button
472
- type="button"
473
- onClick={() => selectedTable && loadSchemaAndData(selectedTable.name)}
474
- disabled={isLoadingSchema || isLoadingData}
475
- className="inline-flex items-center gap-1 rounded-md border border-slate-200 bg-white px-3 py-2 text-sm font-medium text-slate-600 transition hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-50"
476
- >
477
- {isLoadingSchema || isLoadingData ? (
478
- <Loader2 className="h-4 w-4 animate-spin" />
479
- ) : (
480
- <RefreshCw className="h-4 w-4" />
481
- )}
482
- Refresh
483
- </button>
484
- <button
485
- type="button"
486
- onClick={() => setIsCreateOpen(true)}
487
- disabled={!selectedTable || !schema}
488
- className="inline-flex items-center gap-1 rounded-md bg-slate-900 px-3 py-2 text-sm font-medium text-white transition hover:bg-slate-800 disabled:cursor-not-allowed disabled:bg-slate-400"
489
- >
490
- <Plus className="h-4 w-4" />
491
- Add Row
492
- </button>
493
- </div>
494
- </div>
495
-
496
- <div className="flex flex-wrap items-center gap-3 rounded-md bg-slate-50 p-3 text-sm text-slate-600">
497
- <Filter className="h-4 w-4 text-slate-500" />
498
- <select
499
- className="rounded-md border border-slate-200 bg-white px-2 py-1 text-sm"
500
- value={filterState.column ?? ""}
501
- onChange={event =>
502
- setFilterState(current => ({
503
- ...current,
504
- column: event.target.value || undefined
505
- }))
506
- }
507
- >
508
- {filterColumns.length === 0 ? (
509
- <option value="">No filterable columns</option>
510
- ) : (
511
- filterColumns.map(column => (
512
- <option key={column.name} value={column.name}>
513
- {column.name}
514
- </option>
515
- ))
516
- )}
517
- </select>
518
- <select
519
- className="rounded-md border border-slate-200 bg-white px-2 py-1 text-sm"
520
- value={filterState.operator}
521
- onChange={event =>
522
- setFilterState(current => ({
523
- ...current,
524
- operator: event.target.value as SqliteFilterOperator
525
- }))
526
- }
527
- >
528
- {filterOperators.map(operator => (
529
- <option key={operator.value} value={operator.value}>
530
- {operator.label}
531
- </option>
532
- ))}
533
- </select>
534
- {filterState.operator === "is" ? (
535
- <select
536
- className="rounded-md border border-slate-200 bg-white px-2 py-1 text-sm"
537
- value={filterState.value}
538
- onChange={event =>
539
- setFilterState(current => ({
540
- ...current,
541
- value: event.target.value
542
- }))
543
- }
544
- >
545
- <option value="null">NULL</option>
546
- <option value="not.null">NOT NULL</option>
547
- </select>
548
- ) : (
549
- <input
550
- type="text"
551
- className="flex-1 rounded-md border border-slate-200 bg-white px-2 py-1 text-sm focus:border-slate-400 focus:outline-none"
552
- placeholder="Filter value…"
553
- value={filterState.value}
554
- onChange={event =>
555
- setFilterState(current => ({
556
- ...current,
557
- value: event.target.value
558
- }))
559
- }
560
- />
561
- )}
562
- <button
563
- type="button"
564
- onClick={handleApplyFilter}
565
- disabled={!selectedTable || !schema}
566
- className="inline-flex items-center gap-1 rounded-md border border-slate-200 bg-white px-3 py-1.5 text-sm font-medium text-slate-600 transition hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-50"
567
- >
568
- Apply
569
- </button>
570
- </div>
571
- </header>
572
-
573
- {selectedTable ? (
574
- <>
575
- <section className="rounded-lg border border-slate-200 bg-white shadow-sm">
576
- <header className="flex items-center justify-between border-b border-slate-100 px-4 py-3">
577
- <div>
578
- <h2 className="text-base font-semibold text-slate-800">
579
- {selectedTable.name}
580
- </h2>
581
- <p className="text-xs text-slate-500">
582
- {schema?.columns.length ?? 0} columns
583
- </p>
584
- </div>
585
- <div className="flex items-center gap-2">
586
- <label className="flex items-center gap-1 text-xs text-slate-500">
587
- Rows per page
588
- <select
589
- value={limit}
590
- onChange={event =>
591
- handleChangeLimit(Number.parseInt(event.target.value, 10))
592
- }
593
- className="rounded-md border border-slate-200 bg-white px-2 py-1 text-xs shadow-sm"
594
- >
595
- {[10, 20, 50, 100].map(option => (
596
- <option key={option} value={option}>
597
- {option}
598
- </option>
599
- ))}
600
- </select>
601
- </label>
602
- <div className="inline-flex items-center gap-1 rounded-md border border-slate-200 bg-white px-2 py-1 text-xs text-slate-600 shadow-sm">
603
- <button
604
- type="button"
605
- onClick={() => handleChangeOrder(orderBy)}
606
- className="inline-flex items-center gap-1"
607
- disabled={!schema}
608
- >
609
- Order:{" "}
610
- {orderBy ? `${orderBy} • ${orderDirection}` : "not set"}
611
- </button>
612
- {schema && schema.columns.length > 0 && (
613
- <select
614
- value={orderBy ?? ""}
615
- onChange={event =>
616
- handleChangeOrder(event.target.value || undefined)
617
- }
618
- className="rounded border border-slate-200 bg-white px-1 py-0.5 text-xs"
619
- >
620
- <option value="">None</option>
621
- {schema.columns.map(column => (
622
- <option key={column.name} value={column.name}>
623
- {column.name}
624
- </option>
625
- ))}
626
- </select>
627
- )}
628
- </div>
629
- </div>
630
- </header>
631
-
632
- <div className="overflow-auto">
633
- <table className="min-w-full divide-y divide-slate-200 text-sm">
634
- <thead className="bg-slate-50 text-xs uppercase tracking-wide text-slate-500">
635
- <tr>
636
- {columns.map(column => (
637
- <th key={column.name} className="px-3 py-2 text-left">
638
- {column.name}
639
- </th>
640
- ))}
641
- <th className="px-3 py-2 text-right text-slate-400">Actions</th>
642
- </tr>
643
- </thead>
644
- <tbody className="divide-y divide-slate-100 bg-white text-sm text-slate-700">
645
- {isLoadingData ? (
646
- <tr>
647
- <td colSpan={columns.length + 1} className="px-3 py-8 text-center">
648
- <div className="inline-flex items-center gap-2 text-slate-500">
649
- <Loader2 className="h-4 w-4 animate-spin" />
650
- Loading data…
651
- </div>
652
- </td>
653
- </tr>
654
- ) : rows.length === 0 ? (
655
- <tr>
656
- <td colSpan={columns.length + 1} className="px-3 py-8 text-center text-slate-400">
657
- No rows to display.
658
- </td>
659
- </tr>
660
- ) : (
661
- rows.map((row, index) => (
662
- <tr key={index} className="hover:bg-slate-50/60">
663
- {columns.map(column => (
664
- <td key={column.name} className="max-w-[200px] truncate px-3 py-2">
665
- {formatCellValue(row[column.name])}
666
- </td>
667
- ))}
668
- <td className="px-3 py-2 text-right">
669
- <div className="inline-flex items-center gap-1">
670
- <button
671
- type="button"
672
- onClick={() => setEditingRow(row)}
673
- className="inline-flex items-center gap-1 rounded-md border border-slate-200 px-2 py-1 text-xs font-medium text-slate-600 transition hover:bg-slate-100"
674
- >
675
- <Pencil className="h-3 w-3" />
676
- Edit
677
- </button>
678
- <button
679
- type="button"
680
- onClick={() => handleDeleteRow(row)}
681
- className="inline-flex items-center gap-1 rounded-md border border-red-200 px-2 py-1 text-xs font-medium text-red-600 transition hover:bg-red-50"
682
- >
683
- <Trash2 className="h-3 w-3" />
684
- Delete
685
- </button>
686
- </div>
687
- </td>
688
- </tr>
689
- ))
690
- )}
691
- </tbody>
692
- </table>
693
- </div>
694
-
695
- <footer className="flex flex-wrap items-center justify-between gap-3 border-t border-slate-100 px-4 py-3 text-xs text-slate-500">
696
- <span>
697
- Page {currentPage} of {totalPages} • {totalRows} rows
698
- </span>
699
- <div className="flex items-center gap-2">
700
- <button
701
- type="button"
702
- onClick={() => handleChangePage(Math.max(0, offset - limit))}
703
- disabled={offset === 0 || isLoadingData}
704
- className="inline-flex items-center gap-1 rounded-md border border-slate-200 bg-white px-2 py-1 text-xs font-medium text-slate-600 transition hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-50"
705
- >
706
- Prev
707
- </button>
708
- <button
709
- type="button"
710
- onClick={() => handleChangePage(offset + limit)}
711
- disabled={offset + limit >= totalRows || isLoadingData}
712
- className="inline-flex items-center gap-1 rounded-md border border-slate-200 bg-white px-2 py-1 text-xs font-medium text-slate-600 transition hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-50"
713
- >
714
- Next
715
- </button>
716
- </div>
717
- </footer>
718
- </section>
719
-
720
- <section className="rounded-lg border border-slate-200 bg-white shadow-sm">
721
- <header className="border-b border-slate-100 px-4 py-3">
722
- <h3 className="text-sm font-semibold text-slate-700">Schema</h3>
723
- </header>
724
- <div className="overflow-auto">
725
- <table className="min-w-full divide-y divide-slate-200 text-sm">
726
- <thead className="bg-slate-50 text-xs uppercase tracking-wide text-slate-500">
727
- <tr>
728
- <th className="px-3 py-2 text-left">Column</th>
729
- <th className="px-3 py-2 text-left">Type</th>
730
- <th className="px-3 py-2 text-left">Not null</th>
731
- <th className="px-3 py-2 text-left">Default</th>
732
- <th className="px-3 py-2 text-left">Primary key</th>
733
- </tr>
734
- </thead>
735
- <tbody className="divide-y divide-slate-100 bg-white text-sm text-slate-700">
736
- {columns.map(column => (
737
- <tr key={column.name}>
738
- <td className="px-3 py-2 font-medium">{column.name}</td>
739
- <td className="px-3 py-2">{column.type || "TEXT"}</td>
740
- <td className="px-3 py-2">
741
- {column.notnull ? "Yes" : "No"}
742
- </td>
743
- <td className="px-3 py-2">
744
- {column.default_value === null
745
- ? "NULL"
746
- : String(column.default_value)}
747
- </td>
748
- <td className="px-3 py-2">
749
- {column.primary_key_position
750
- ? `Yes (#${column.primary_key_position})`
751
- : "No"}
752
- </td>
753
- </tr>
754
- ))}
755
- </tbody>
756
- </table>
757
- </div>
758
- </section>
759
- </>
760
- ) : (
761
- <p className="rounded-lg border border-dashed border-slate-300 bg-white p-6 text-center text-sm text-slate-500">
762
- Select a table to begin.
763
- </p>
764
- )}
765
- </section>
766
-
767
- {schema && selectedTable && (
768
- <RowModal
769
- title={`Insert into ${selectedTable.name}`}
770
- open={isCreateOpen}
771
- columns={schema.columns}
772
- onClose={() => setIsCreateOpen(false)}
773
- onSubmit={handleInsertRow}
774
- />
775
- )}
776
-
777
- {schema && selectedTable && editingRow && (
778
- <RowModal
779
- title={`Edit row in ${selectedTable.name}`}
780
- open={Boolean(editingRow)}
781
- columns={schema.columns}
782
- initialValues={editingRow}
783
- onClose={() => setEditingRow(null)}
784
- onSubmit={data => handleUpdateRow(editingRow, data)}
785
- primaryKeys={schema.columns.filter(column => column.primary_key_position > 0)}
786
- />
787
- )}
788
- </div>
789
- )
790
- }
791
-
792
- function RowModal({
793
- title,
794
- open,
795
- columns,
796
- primaryKeys,
797
- initialValues,
798
- onClose,
799
- onSubmit
800
- }: {
801
- title: string
802
- open: boolean
803
- columns: TableColumn[]
804
- primaryKeys?: TableColumn[]
805
- initialValues?: SqlValue
806
- onClose: () => void
807
- onSubmit: (data: Record<string, unknown>) => void
808
- }) {
809
- const [formValues, setFormValues] = useState<Record<string, string>>({})
810
-
811
- useEffect(() => {
812
- if (open) {
813
- const nextValues: Record<string, string> = {}
814
- columns.forEach(column => {
815
- const rawValue = initialValues?.[column.name]
816
- nextValues[column.name] =
817
- rawValue === null || rawValue === undefined ? "" : String(rawValue)
818
- })
819
- setFormValues(nextValues)
820
- }
821
- }, [open, columns, initialValues])
822
-
823
- if (!open) {
824
- return null
825
- }
826
-
827
- function handleChange(column: string, value: string) {
828
- setFormValues(current => ({
829
- ...current,
830
- [column]: value
831
- }))
832
- }
833
-
834
- function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
835
- event.preventDefault()
836
- const payload: Record<string, unknown> = {}
837
-
838
- for (const column of columns) {
839
- const value = formValues[column.name]
840
- payload[column.name] = coerceValue(column, value)
841
- }
842
-
843
- onSubmit(payload)
844
- }
845
-
846
- return (
847
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/30 backdrop-blur">
848
- <div className="relative w-full max-w-2xl rounded-lg border border-slate-200 bg-white p-6 shadow-xl">
849
- <header className="mb-4 flex items-center justify-between">
850
- <div>
851
- <h2 className="text-lg font-semibold text-slate-900">{title}</h2>
852
- {primaryKeys && primaryKeys.length === 0 && (
853
- <p className="mt-1 text-xs text-amber-600">
854
- Warning: table has no primary key defined. Updates/deletes rely on full row
855
- matching.
856
- </p>
857
- )}
858
- </div>
859
- <button
860
- type="button"
861
- onClick={onClose}
862
- className="rounded-full border border-slate-200 p-1 text-slate-500 transition hover:bg-slate-100"
863
- >
864
- <X className="h-4 w-4" />
865
- </button>
866
- </header>
867
-
868
- <form onSubmit={handleSubmit} className="space-y-4">
869
- <div className="grid max-h-[50vh] grid-cols-1 gap-4 overflow-auto pr-2 sm:grid-cols-2">
870
- {columns.map(column => (
871
- <label key={column.name} className="flex flex-col gap-1 text-sm">
872
- <span className="font-medium text-slate-700">
873
- {column.name}
874
- {column.notnull && (
875
- <sup className="ml-1 text-amber-600" title="Required">
876
- *
877
- </sup>
878
- )}
879
- </span>
880
- <input
881
- type="text"
882
- value={formValues[column.name] ?? ""}
883
- onChange={event => handleChange(column.name, event.target.value)}
884
- className="rounded-md border border-slate-200 px-3 py-2 text-sm text-slate-700 shadow-sm focus:border-slate-400 focus:outline-none"
885
- placeholder={
886
- column.default_value === null
887
- ? column.type || "TEXT"
888
- : String(column.default_value)
889
- }
890
- />
891
- <span className="text-xs text-slate-400">
892
- {column.type || "TEXT"}
893
- {column.primary_key_position ? " • Primary key" : ""}
894
- </span>
895
- </label>
896
- ))}
897
- </div>
898
-
899
- <footer className="flex items-center justify-end gap-2">
900
- <button
901
- type="button"
902
- onClick={onClose}
903
- className="inline-flex items-center gap-1 rounded-md border border-slate-200 bg-white px-3 py-2 text-sm font-medium text-slate-600 transition hover:bg-slate-100"
904
- >
905
- Cancel
906
- </button>
907
- <button
908
- type="submit"
909
- className="inline-flex items-center gap-1 rounded-md bg-slate-900 px-3 py-2 text-sm font-medium text-white transition hover:bg-slate-800"
910
- >
911
- <Save className="h-4 w-4" />
912
- Save
913
- </button>
914
- </footer>
915
- </form>
916
- </div>
917
- </div>
918
- )
919
- }
920
-
921
- function buildCriteriaFromRow(row: SqlValue, columns: TableColumn[]) {
922
- const criteria: Record<string, unknown> = {}
923
- const primaryKeys = columns.filter(column => column.primary_key_position > 0)
924
-
925
- if (primaryKeys.length) {
926
- for (const primary of primaryKeys) {
927
- criteria[primary.name] = row[primary.name]
928
- }
929
- return criteria
930
- }
931
-
932
- for (const column of columns) {
933
- criteria[column.name] = row[column.name]
934
- }
935
- return criteria
936
- }
937
-
938
- function coerceValue(column: TableColumn, rawValue: string): unknown {
939
- if (rawValue === "") {
940
- return column.notnull ? "" : null
941
- }
942
-
943
- const normalisedType = (column.type ?? "").toLowerCase()
944
-
945
- if (normalisedType.includes("int")) {
946
- const parsed = Number.parseInt(rawValue, 10)
947
- return Number.isNaN(parsed) ? rawValue : parsed
948
- }
949
-
950
- if (
951
- normalisedType.includes("real") ||
952
- normalisedType.includes("double") ||
953
- normalisedType.includes("float") ||
954
- normalisedType.includes("numeric")
955
- ) {
956
- const parsed = Number.parseFloat(rawValue)
957
- return Number.isNaN(parsed) ? rawValue : parsed
958
- }
959
-
960
- if (normalisedType.includes("bool")) {
961
- return rawValue === "true" || rawValue === "1" ? 1 : 0
962
- }
963
-
964
- return rawValue
965
- }
966
-
967
- function formatCellValue(value: unknown): string {
968
- if (value === null || value === undefined) {
969
- return "NULL"
970
- }
971
- if (typeof value === "object") {
972
- try {
973
- return JSON.stringify(value)
974
- } catch {
975
- return String(value)
976
- }
977
- }
978
- return String(value)
979
- }
980
-
981
- function identifierIsFilterable(name: string): boolean {
982
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name)
983
- }
984
-