hazo_auth 0.1.0

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 (162) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -0
  3. package/components.json +22 -0
  4. package/hazo_auth_config.example.ini +414 -0
  5. package/hazo_notify_config.example.ini +159 -0
  6. package/instrumentation.ts +32 -0
  7. package/migrations/001_add_token_type_to_refresh_tokens.sql +14 -0
  8. package/migrations/002_add_name_to_hazo_users.sql +7 -0
  9. package/next.config.mjs +55 -0
  10. package/package.json +114 -0
  11. package/postcss.config.mjs +8 -0
  12. package/public/file.svg +1 -0
  13. package/public/globe.svg +1 -0
  14. package/public/next.svg +1 -0
  15. package/public/vercel.svg +1 -0
  16. package/public/window.svg +1 -0
  17. package/scripts/apply_migration.ts +118 -0
  18. package/src/app/api/auth/change_password/route.ts +109 -0
  19. package/src/app/api/auth/forgot_password/route.ts +107 -0
  20. package/src/app/api/auth/library_photos/route.ts +70 -0
  21. package/src/app/api/auth/login/route.ts +155 -0
  22. package/src/app/api/auth/logout/route.ts +62 -0
  23. package/src/app/api/auth/me/route.ts +47 -0
  24. package/src/app/api/auth/profile_picture/[filename]/route.ts +67 -0
  25. package/src/app/api/auth/register/route.ts +106 -0
  26. package/src/app/api/auth/remove_profile_picture/route.ts +86 -0
  27. package/src/app/api/auth/resend_verification/route.ts +107 -0
  28. package/src/app/api/auth/reset_password/route.ts +107 -0
  29. package/src/app/api/auth/update_user/route.ts +126 -0
  30. package/src/app/api/auth/upload_profile_picture/route.ts +268 -0
  31. package/src/app/api/auth/validate_reset_token/route.ts +80 -0
  32. package/src/app/api/auth/verify_email/route.ts +85 -0
  33. package/src/app/api/migrations/apply/route.ts +91 -0
  34. package/src/app/favicon.ico +0 -0
  35. package/src/app/fonts/GeistMonoVF.woff +0 -0
  36. package/src/app/fonts/GeistVF.woff +0 -0
  37. package/src/app/forgot_password/forgot_password_page_client.tsx +60 -0
  38. package/src/app/forgot_password/page.tsx +24 -0
  39. package/src/app/globals.css +89 -0
  40. package/src/app/hazo_connect/api/sqlite/data/route.ts +197 -0
  41. package/src/app/hazo_connect/api/sqlite/schema/route.ts +35 -0
  42. package/src/app/hazo_connect/api/sqlite/tables/route.ts +26 -0
  43. package/src/app/hazo_connect/sqlite_admin/page.tsx +51 -0
  44. package/src/app/hazo_connect/sqlite_admin/sqlite-admin-client.tsx +947 -0
  45. package/src/app/layout.tsx +43 -0
  46. package/src/app/login/login_page_client.tsx +71 -0
  47. package/src/app/login/page.tsx +26 -0
  48. package/src/app/my_settings/my_settings_page_client.tsx +120 -0
  49. package/src/app/my_settings/page.tsx +40 -0
  50. package/src/app/page.tsx +170 -0
  51. package/src/app/register/page.tsx +26 -0
  52. package/src/app/register/register_page_client.tsx +72 -0
  53. package/src/app/reset_password/page.tsx +29 -0
  54. package/src/app/reset_password/reset_password_page_client.tsx +81 -0
  55. package/src/app/verify_email/page.tsx +24 -0
  56. package/src/app/verify_email/verify_email_page_client.tsx +60 -0
  57. package/src/components/layouts/email_verification/config/email_verification_field_config.ts +86 -0
  58. package/src/components/layouts/email_verification/hooks/use_email_verification.ts +291 -0
  59. package/src/components/layouts/email_verification/index.tsx +297 -0
  60. package/src/components/layouts/forgot_password/config/forgot_password_field_config.ts +58 -0
  61. package/src/components/layouts/forgot_password/hooks/use_forgot_password_form.ts +179 -0
  62. package/src/components/layouts/forgot_password/index.tsx +168 -0
  63. package/src/components/layouts/login/config/login_field_config.ts +67 -0
  64. package/src/components/layouts/login/hooks/use_login_form.ts +281 -0
  65. package/src/components/layouts/login/index.tsx +224 -0
  66. package/src/components/layouts/my_settings/components/editable_field.tsx +177 -0
  67. package/src/components/layouts/my_settings/components/password_change_dialog.tsx +301 -0
  68. package/src/components/layouts/my_settings/components/profile_picture_dialog.tsx +385 -0
  69. package/src/components/layouts/my_settings/components/profile_picture_display.tsx +66 -0
  70. package/src/components/layouts/my_settings/components/profile_picture_gravatar_tab.tsx +143 -0
  71. package/src/components/layouts/my_settings/components/profile_picture_library_tab.tsx +282 -0
  72. package/src/components/layouts/my_settings/components/profile_picture_upload_tab.tsx +341 -0
  73. package/src/components/layouts/my_settings/config/my_settings_field_config.ts +61 -0
  74. package/src/components/layouts/my_settings/hooks/use_my_settings.ts +458 -0
  75. package/src/components/layouts/my_settings/index.tsx +351 -0
  76. package/src/components/layouts/register/config/register_field_config.ts +101 -0
  77. package/src/components/layouts/register/hooks/use_register_form.ts +272 -0
  78. package/src/components/layouts/register/index.tsx +208 -0
  79. package/src/components/layouts/reset_password/config/reset_password_field_config.ts +86 -0
  80. package/src/components/layouts/reset_password/hooks/use_reset_password_form.ts +276 -0
  81. package/src/components/layouts/reset_password/index.tsx +294 -0
  82. package/src/components/layouts/shared/components/already_logged_in_guard.tsx +95 -0
  83. package/src/components/layouts/shared/components/field_error_message.tsx +29 -0
  84. package/src/components/layouts/shared/components/form_action_buttons.tsx +64 -0
  85. package/src/components/layouts/shared/components/form_field_wrapper.tsx +44 -0
  86. package/src/components/layouts/shared/components/form_header.tsx +36 -0
  87. package/src/components/layouts/shared/components/logout_button.tsx +76 -0
  88. package/src/components/layouts/shared/components/password_field.tsx +72 -0
  89. package/src/components/layouts/shared/components/sidebar_layout_wrapper.tsx +264 -0
  90. package/src/components/layouts/shared/components/two_column_auth_layout.tsx +44 -0
  91. package/src/components/layouts/shared/components/unauthorized_guard.tsx +78 -0
  92. package/src/components/layouts/shared/components/visual_panel.tsx +41 -0
  93. package/src/components/layouts/shared/config/layout_customization.ts +95 -0
  94. package/src/components/layouts/shared/data/layout_data_client.ts +19 -0
  95. package/src/components/layouts/shared/hooks/use_auth_status.ts +103 -0
  96. package/src/components/layouts/shared/utils/ip_address.ts +37 -0
  97. package/src/components/layouts/shared/utils/validation.ts +66 -0
  98. package/src/components/ui/avatar.tsx +50 -0
  99. package/src/components/ui/button.tsx +57 -0
  100. package/src/components/ui/dialog.tsx +122 -0
  101. package/src/components/ui/hazo_ui_tooltip.tsx +67 -0
  102. package/src/components/ui/input.tsx +22 -0
  103. package/src/components/ui/label.tsx +26 -0
  104. package/src/components/ui/separator.tsx +31 -0
  105. package/src/components/ui/sheet.tsx +139 -0
  106. package/src/components/ui/sidebar.tsx +773 -0
  107. package/src/components/ui/skeleton.tsx +15 -0
  108. package/src/components/ui/sonner.tsx +31 -0
  109. package/src/components/ui/switch.tsx +29 -0
  110. package/src/components/ui/tabs.tsx +55 -0
  111. package/src/components/ui/tooltip.tsx +32 -0
  112. package/src/components/ui/vertical-tabs.tsx +59 -0
  113. package/src/hooks/use-mobile.tsx +19 -0
  114. package/src/lib/already_logged_in_config.server.ts +46 -0
  115. package/src/lib/app_logger.ts +24 -0
  116. package/src/lib/auth/auth_utils.server.ts +196 -0
  117. package/src/lib/auth/server_auth.ts +88 -0
  118. package/src/lib/config/config_loader.server.ts +149 -0
  119. package/src/lib/email_verification_config.server.ts +32 -0
  120. package/src/lib/file_types_config.server.ts +25 -0
  121. package/src/lib/forgot_password_config.server.ts +32 -0
  122. package/src/lib/hazo_connect_instance.server.ts +77 -0
  123. package/src/lib/hazo_connect_setup.server.ts +181 -0
  124. package/src/lib/hazo_connect_setup.ts +54 -0
  125. package/src/lib/login_config.server.ts +46 -0
  126. package/src/lib/messages_config.server.ts +45 -0
  127. package/src/lib/migrations/apply_migration.ts +105 -0
  128. package/src/lib/my_settings_config.server.ts +135 -0
  129. package/src/lib/password_requirements_config.server.ts +39 -0
  130. package/src/lib/profile_picture_config.server.ts +56 -0
  131. package/src/lib/register_config.server.ts +57 -0
  132. package/src/lib/reset_password_config.server.ts +75 -0
  133. package/src/lib/services/email_service.ts +581 -0
  134. package/src/lib/services/email_verification_service.ts +264 -0
  135. package/src/lib/services/login_service.ts +118 -0
  136. package/src/lib/services/password_change_service.ts +154 -0
  137. package/src/lib/services/password_reset_service.ts +405 -0
  138. package/src/lib/services/profile_picture_remove_service.ts +120 -0
  139. package/src/lib/services/profile_picture_service.ts +215 -0
  140. package/src/lib/services/profile_picture_source_mapper.ts +62 -0
  141. package/src/lib/services/registration_service.ts +163 -0
  142. package/src/lib/services/token_service.ts +240 -0
  143. package/src/lib/services/user_update_service.ts +128 -0
  144. package/src/lib/ui_sizes_config.server.ts +37 -0
  145. package/src/lib/user_fields_config.server.ts +31 -0
  146. package/src/lib/utils/api_route_helpers.ts +60 -0
  147. package/src/lib/utils.ts +11 -0
  148. package/src/middleware.ts +91 -0
  149. package/src/server/config/config_loader.ts +496 -0
  150. package/src/server/index.ts +38 -0
  151. package/src/server/logging/logger_service.ts +56 -0
  152. package/src/server/routes/root_router.ts +16 -0
  153. package/src/server/server.ts +28 -0
  154. package/src/server/types/app_types.ts +74 -0
  155. package/src/server/types/express.d.ts +15 -0
  156. package/src/stories/email_verification_layout.stories.tsx +137 -0
  157. package/src/stories/forgot_password_layout.stories.tsx +85 -0
  158. package/src/stories/login_layout.stories.tsx +85 -0
  159. package/src/stories/project_overview.stories.tsx +33 -0
  160. package/src/stories/register_layout.stories.tsx +107 -0
  161. package/tailwind.config.ts +77 -0
  162. package/tsconfig.json +27 -0
@@ -0,0 +1,14 @@
1
+ -- file_description: migration to add token_type column to hazo_refresh_tokens table for password reset functionality
2
+ -- This migration adds a token_type column to distinguish between refresh tokens and password reset tokens
3
+
4
+ -- Add token_type column with CHECK constraint
5
+ ALTER TABLE hazo_refresh_tokens
6
+ ADD COLUMN token_type TEXT NOT NULL DEFAULT 'refresh'
7
+ CHECK (token_type IN ('refresh', 'password_reset', 'email_verification'));
8
+
9
+ -- Create index on token_type for faster queries
10
+ CREATE INDEX IF NOT EXISTS idx_hazo_refresh_tokens_token_type ON hazo_refresh_tokens(token_type);
11
+
12
+ -- Create index on token_type and user_id for password reset token lookups
13
+ CREATE INDEX IF NOT EXISTS idx_hazo_refresh_tokens_user_type ON hazo_refresh_tokens(user_id, token_type);
14
+
@@ -0,0 +1,7 @@
1
+ -- file_description: migration to add name column to hazo_users table
2
+ -- This migration adds a name text field to store user's full name
3
+
4
+ -- Add name column (nullable, as existing users may not have a name)
5
+ ALTER TABLE hazo_users
6
+ ADD COLUMN name TEXT;
7
+
@@ -0,0 +1,55 @@
1
+ // file_description: configure next.js application settings for the ui_component project
2
+ // section: imports
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ // section: path_resolution
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ // section: base_configuration
11
+ const next_config = {
12
+ /* config options here */
13
+ // Note: hazo_connect configuration is now read from hazo_auth_config.ini using hazo_config
14
+ // Environment variables are only used as fallback if hazo_auth_config.ini is not found
15
+ // See hazo_auth_config.ini for hazo_connect configuration parameters
16
+ env: {
17
+ // Environment variables can be set here as fallback, but hazo_auth_config.ini is preferred
18
+ // HAZO_CONNECT_ENABLE_ADMIN_UI: "true",
19
+ // HAZO_CONNECT_SQLITE_PATH: path.resolve(__dirname, "__tests__", "fixtures", "hazo_auth.sqlite"),
20
+ },
21
+ // Note: serverComponentsExternalPackages is not available in Next.js 14.2
22
+ // Using webpack externals configuration instead (see webpack section below)
23
+ // section: webpack_configuration
24
+ webpack: (config, { isServer }) => {
25
+ // Exclude sql.js from webpack bundling for API routes
26
+ // These packages use Node.js module.exports which doesn't work in webpack context
27
+ if (isServer) {
28
+ config.externals = config.externals || [];
29
+ // Add sql.js as external to prevent webpack from bundling it
30
+ if (Array.isArray(config.externals)) {
31
+ config.externals.push("sql.js");
32
+ // Exclude hazo_notify from Edge runtime bundles (middleware)
33
+ // hazo_notify is only available in Node.js runtime (server bundles), not Edge runtime
34
+ // This ensures hazo_notify is loaded at runtime for API routes using Node.js runtime
35
+ config.externals.push("hazo_notify");
36
+ } else {
37
+ config.externals = [config.externals, "sql.js", "hazo_notify"];
38
+ }
39
+ } else {
40
+ // Client-side: exclude server-only files from client bundles
41
+ config.resolve.alias = {
42
+ ...config.resolve.alias,
43
+ "@/lib/hazo_connect_setup.server": false,
44
+ "@/lib/hazo_connect_instance.server": false,
45
+ "@/lib/login_config.server": false,
46
+ // Exclude hazo_notify from client bundles
47
+ "hazo_notify": false,
48
+ };
49
+ }
50
+ return config;
51
+ },
52
+ };
53
+
54
+ export default next_config;
55
+
package/package.json ADDED
@@ -0,0 +1,114 @@
1
+ {
2
+ "name": "hazo_auth",
3
+ "version": "0.1.0",
4
+ "files": [
5
+ "src/**/*",
6
+ "public/file.svg",
7
+ "public/globe.svg",
8
+ "public/next.svg",
9
+ "public/vercel.svg",
10
+ "public/window.svg",
11
+ "hazo_auth_config.example.ini",
12
+ "hazo_notify_config.example.ini",
13
+ "README.md",
14
+ "LICENSE",
15
+ "tsconfig.json",
16
+ "tailwind.config.ts",
17
+ "postcss.config.mjs",
18
+ "next.config.mjs",
19
+ "components.json",
20
+ "instrumentation.ts",
21
+ "migrations/**/*",
22
+ "scripts/**/*"
23
+ ],
24
+ "scripts": {
25
+ "dev": "next dev",
26
+ "build": "next build",
27
+ "start": "next start",
28
+ "lint": "next lint",
29
+ "storybook": "storybook dev -p 6006",
30
+ "build-storybook": "storybook build",
31
+ "dev:server": "tsx src/server/index.ts",
32
+ "migrate": "tsx scripts/apply_migration.ts",
33
+ "test": "cross-env NODE_ENV=test POSTGREST_URL=http://209.38.26.241:4402 POSTGREST_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYXBpX3VzZXIifQ.zBoUGymrxTUk1DNYIGUCtQU4HFaWEHlbE9_8Y3hUaTw jest --runInBand",
34
+ "test:watch": "cross-env NODE_ENV=test POSTGREST_URL=http://209.38.26.241:4402 POSTGREST_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYXBpX3VzZXIifQ.zBoUGymrxTUk1DNYIGUCtQU4HFaWEHlbE9_8Y3hUaTw jest --watch"
35
+ },
36
+ "dependencies": {
37
+ "@radix-ui/react-avatar": "^1.1.11",
38
+ "@radix-ui/react-dialog": "^1.1.15",
39
+ "@radix-ui/react-label": "^2.1.8",
40
+ "@radix-ui/react-separator": "^1.1.8",
41
+ "@radix-ui/react-slot": "^1.2.4",
42
+ "@radix-ui/react-switch": "^1.2.6",
43
+ "@radix-ui/react-tabs": "^1.1.13",
44
+ "@radix-ui/react-tooltip": "^1.2.8",
45
+ "argon2": "^0.44.0",
46
+ "axios": "^1.13.2",
47
+ "browser-image-compression": "^2.0.2",
48
+ "class-variance-authority": "^0.7.1",
49
+ "clsx": "^2.1.1",
50
+ "compression": "^1.8.1",
51
+ "cookie-parser": "^1.4.7",
52
+ "cors": "^2.8.5",
53
+ "date-fns": "^4.1.0",
54
+ "express": "^5.1.0",
55
+ "express-rate-limit": "^8.2.1",
56
+ "gravatar-url": "^4.0.1",
57
+ "handlebars": "^4.7.8",
58
+ "hazo_config": "^1.3.0",
59
+ "hazo_connect": "^2.0.0",
60
+ "hazo_notify": "^1.0.0",
61
+ "helmet": "^8.1.0",
62
+ "ini": "^6.0.0",
63
+ "jsonwebtoken": "^9.0.2",
64
+ "lucide-react": "^0.553.0",
65
+ "mime-types": "^3.0.1",
66
+ "morgan": "^1.10.1",
67
+ "multer": "^2.0.2",
68
+ "next": "^14.2.7",
69
+ "next-themes": "^0.4.6",
70
+ "react": "^18.3.1",
71
+ "react-dom": "^18.3.1",
72
+ "sonner": "^2.0.7",
73
+ "tailwind-merge": "^3.4.0",
74
+ "tailwindcss-animate": "^1.0.7",
75
+ "zod": "^4.1.12"
76
+ },
77
+ "devDependencies": {
78
+ "@chromatic-com/storybook": "^4.1.2",
79
+ "@storybook/addon-a11y": "^10.0.6",
80
+ "@storybook/addon-docs": "^10.0.6",
81
+ "@storybook/addon-onboarding": "^10.0.6",
82
+ "@storybook/addon-vitest": "^10.0.6",
83
+ "@storybook/nextjs": "^10.0.6",
84
+ "@testing-library/jest-dom": "^6.6.3",
85
+ "@testing-library/react": "^16.0.1",
86
+ "@types/better-sqlite3": "^7.6.13",
87
+ "@types/compression": "^1.8.1",
88
+ "@types/cookie-parser": "^1.4.10",
89
+ "@types/cors": "^2.8.19",
90
+ "@types/express": "^5.0.5",
91
+ "@types/ini": "^4.1.1",
92
+ "@types/jest": "^30.0.0",
93
+ "@types/jsonwebtoken": "^9.0.10",
94
+ "@types/multer": "^2.0.0",
95
+ "@types/node": "^20.19.24",
96
+ "@types/react": "^18",
97
+ "@types/react-dom": "^18",
98
+ "better-sqlite3": "^12.4.1",
99
+ "cross-env": "^10.1.0",
100
+ "eslint": "^8",
101
+ "eslint-config-next": "^14.2.7",
102
+ "eslint-plugin-storybook": "^10.0.6",
103
+ "jest": "^30.2.0",
104
+ "jest-environment-jsdom": "^29.7.0",
105
+ "patch-package": "^8.0.1",
106
+ "postcss": "^8",
107
+ "storybook": "^10.0.6",
108
+ "supertest": "^7.1.4",
109
+ "tailwindcss": "^3.4.1",
110
+ "ts-jest": "^29.4.5",
111
+ "tsx": "^4.20.6",
112
+ "typescript": "^5"
113
+ }
114
+ }
@@ -0,0 +1,8 @@
1
+ /** @type {import('postcss-load-config').Config} */
2
+ const config = {
3
+ plugins: {
4
+ tailwindcss: {},
5
+ },
6
+ };
7
+
8
+ export default config;
@@ -0,0 +1 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
@@ -0,0 +1,118 @@
1
+ // file_description: script to apply database migrations directly to SQLite database
2
+ // Run with: npx tsx scripts/apply_migration.ts
3
+ // section: imports
4
+ import Database from "better-sqlite3";
5
+ import path from "path";
6
+ import fs from "fs";
7
+
8
+ // section: helpers
9
+ function get_database_path(): string {
10
+ // Read from hazo_auth_config.ini or use default
11
+ const config_path = path.resolve(process.cwd(), "hazo_auth_config.ini");
12
+ let sqlite_path: string | undefined;
13
+
14
+ if (fs.existsSync(config_path)) {
15
+ try {
16
+ const config_content = fs.readFileSync(config_path, "utf-8");
17
+ const sqlite_path_match = config_content.match(/sqlite_path\s*=\s*(.+)/);
18
+ if (sqlite_path_match) {
19
+ sqlite_path = sqlite_path_match[1].trim();
20
+ }
21
+ } catch (error) {
22
+ console.warn("Could not read hazo_auth_config.ini, using default path");
23
+ }
24
+ }
25
+
26
+ // Default path if not found in config
27
+ if (!sqlite_path) {
28
+ sqlite_path = "__tests__/fixtures/hazo_auth.sqlite";
29
+ }
30
+
31
+ // Resolve to absolute path
32
+ const resolved_path = path.isAbsolute(sqlite_path)
33
+ ? sqlite_path
34
+ : path.resolve(process.cwd(), sqlite_path);
35
+
36
+ return path.normalize(resolved_path);
37
+ }
38
+
39
+ function apply_migration_sql(db: Database.Database, sql: string): void {
40
+ // Remove comments (lines starting with --)
41
+ const sql_without_comments = sql
42
+ .split("\n")
43
+ .filter((line) => !line.trim().startsWith("--"))
44
+ .join("\n");
45
+
46
+ // Split by semicolon and execute each statement
47
+ const statements = sql_without_comments
48
+ .split(";")
49
+ .map((stmt) => stmt.trim())
50
+ .filter((stmt) => stmt.length > 0);
51
+
52
+ console.log(`Found ${statements.length} SQL statement(s) to execute\n`);
53
+
54
+ for (let i = 0; i < statements.length; i++) {
55
+ const statement = statements[i];
56
+ try {
57
+ db.exec(statement + ";");
58
+ console.log(`✓ [${i + 1}/${statements.length}] Executed:`, statement.substring(0, 100));
59
+ } catch (error) {
60
+ // Check if error is because column/index already exists
61
+ const error_message = error instanceof Error ? error.message : String(error);
62
+ if (
63
+ error_message.includes("duplicate column") ||
64
+ error_message.includes("already exists") ||
65
+ error_message.includes("UNIQUE constraint failed") ||
66
+ error_message.includes("index already exists")
67
+ ) {
68
+ console.log(`⚠ [${i + 1}/${statements.length}] Already exists, skipping:`, statement.substring(0, 100));
69
+ } else {
70
+ console.error(`✗ [${i + 1}/${statements.length}] Error:`, error_message);
71
+ throw error;
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ // section: main
78
+ function main() {
79
+ const db_path = get_database_path();
80
+
81
+ console.log("Applying migration to database:", db_path);
82
+
83
+ if (!fs.existsSync(db_path)) {
84
+ console.error("Database file not found:", db_path);
85
+ process.exit(1);
86
+ }
87
+
88
+ // Get migration file from command line argument or use default
89
+ const migration_file_arg = process.argv[2];
90
+ const migration_file = migration_file_arg
91
+ ? path.resolve(process.cwd(), migration_file_arg)
92
+ : path.resolve(process.cwd(), "migrations", "002_add_name_to_hazo_users.sql");
93
+
94
+ if (!fs.existsSync(migration_file)) {
95
+ console.error("Migration file not found:", migration_file);
96
+ console.error("\nUsage: npx tsx scripts/apply_migration.ts [migration_file_path]");
97
+ process.exit(1);
98
+ }
99
+
100
+ const db = new Database(db_path);
101
+
102
+ try {
103
+ const migration_sql = fs.readFileSync(migration_file, "utf-8");
104
+
105
+ console.log(`\nApplying migration: ${path.basename(migration_file)}`);
106
+ apply_migration_sql(db, migration_sql);
107
+
108
+ console.log("\n✓ Migration applied successfully!");
109
+ } catch (error) {
110
+ console.error("Error applying migration:", error);
111
+ process.exit(1);
112
+ } finally {
113
+ db.close();
114
+ }
115
+ }
116
+
117
+ main();
118
+
@@ -0,0 +1,109 @@
1
+ // file_description: API route for changing user password
2
+ // section: imports
3
+ import { NextRequest, NextResponse } from "next/server";
4
+ import { get_hazo_connect_instance } from "@/lib/hazo_connect_instance.server";
5
+ import { create_app_logger } from "@/lib/app_logger";
6
+ import { change_password } from "@/lib/services/password_change_service";
7
+ import { get_filename, get_line_number } from "@/lib/utils/api_route_helpers";
8
+ import { require_auth } from "@/lib/auth/auth_utils.server";
9
+
10
+ // section: api_handler
11
+ export async function POST(request: NextRequest) {
12
+ const logger = create_app_logger();
13
+
14
+ try {
15
+ // Use centralized auth check
16
+ let user_id: string;
17
+ try {
18
+ const user = await require_auth(request);
19
+ user_id = user.user_id;
20
+ } catch (error) {
21
+ if (error instanceof Error && error.message === "Authentication required") {
22
+ logger.warn("password_change_authentication_failed", {
23
+ filename: get_filename(),
24
+ line_number: get_line_number(),
25
+ error: "User not authenticated",
26
+ });
27
+
28
+ return NextResponse.json(
29
+ { error: "Authentication required" },
30
+ { status: 401 }
31
+ );
32
+ }
33
+ throw error;
34
+ }
35
+
36
+ const body = await request.json();
37
+ const { current_password, new_password } = body;
38
+
39
+ // Validate input
40
+ if (!current_password || !new_password) {
41
+ logger.warn("password_change_validation_failed", {
42
+ filename: get_filename(),
43
+ line_number: get_line_number(),
44
+ error: "Missing required fields",
45
+ has_current_password: !!current_password,
46
+ has_new_password: !!new_password,
47
+ });
48
+
49
+ return NextResponse.json(
50
+ { error: "Current password and new password are required" },
51
+ { status: 400 }
52
+ );
53
+ }
54
+
55
+ // Get singleton hazo_connect instance
56
+ const hazoConnect = get_hazo_connect_instance();
57
+
58
+ // Change password
59
+ const result = await change_password(hazoConnect, user_id, {
60
+ current_password,
61
+ new_password,
62
+ });
63
+
64
+ if (!result.success) {
65
+ logger.warn("password_change_failed", {
66
+ filename: get_filename(),
67
+ line_number: get_line_number(),
68
+ error: result.error,
69
+ user_id,
70
+ });
71
+
72
+ return NextResponse.json(
73
+ { error: result.error || "Failed to change password" },
74
+ { status: 400 }
75
+ );
76
+ }
77
+
78
+ logger.info("password_change_successful", {
79
+ filename: get_filename(),
80
+ line_number: get_line_number(),
81
+ user_id,
82
+ });
83
+
84
+ return NextResponse.json(
85
+ {
86
+ success: true,
87
+ message: "Password changed successfully",
88
+ },
89
+ { status: 200 }
90
+ );
91
+ } catch (error) {
92
+ const error_message =
93
+ error instanceof Error ? error.message : "Unknown error";
94
+ const error_stack = error instanceof Error ? error.stack : undefined;
95
+
96
+ logger.error("password_change_error", {
97
+ filename: get_filename(),
98
+ line_number: get_line_number(),
99
+ error_message,
100
+ error_stack,
101
+ });
102
+
103
+ return NextResponse.json(
104
+ { error: "Failed to change password. Please try again." },
105
+ { status: 500 }
106
+ );
107
+ }
108
+ }
109
+
@@ -0,0 +1,107 @@
1
+ // file_description: API route for password reset requests using hazo_connect
2
+ // section: imports
3
+ import { NextRequest, NextResponse } from "next/server";
4
+ import { get_hazo_connect_instance } from "@/lib/hazo_connect_instance.server";
5
+ import { create_app_logger } from "@/lib/app_logger";
6
+ import { request_password_reset } from "@/lib/services/password_reset_service";
7
+ import { get_filename, get_line_number } from "@/lib/utils/api_route_helpers";
8
+
9
+ // section: api_handler
10
+ export async function POST(request: NextRequest) {
11
+ const logger = create_app_logger();
12
+
13
+ try {
14
+ const body = await request.json();
15
+ const { email } = body;
16
+
17
+ // Validate input
18
+ if (!email) {
19
+ logger.warn("password_reset_validation_failed", {
20
+ filename: get_filename(),
21
+ line_number: get_line_number(),
22
+ email: email || "missing",
23
+ });
24
+
25
+ return NextResponse.json(
26
+ { error: "Email is required" },
27
+ { status: 400 }
28
+ );
29
+ }
30
+
31
+ // Validate email format
32
+ const email_pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
33
+ if (!email_pattern.test(email)) {
34
+ logger.warn("password_reset_invalid_email", {
35
+ filename: get_filename(),
36
+ line_number: get_line_number(),
37
+ email,
38
+ });
39
+
40
+ return NextResponse.json(
41
+ { error: "Invalid email address format" },
42
+ { status: 400 }
43
+ );
44
+ }
45
+
46
+ // Get singleton hazo_connect instance (reuses same connection across all routes)
47
+ const hazoConnect = get_hazo_connect_instance();
48
+
49
+ // Request password reset using the password reset service
50
+ const result = await request_password_reset(hazoConnect, {
51
+ email,
52
+ });
53
+
54
+ if (!result.success) {
55
+ logger.warn("password_reset_failed", {
56
+ filename: get_filename(),
57
+ line_number: get_line_number(),
58
+ email,
59
+ error: result.error,
60
+ });
61
+
62
+ // Still return 200 OK to prevent email enumeration attacks
63
+ return NextResponse.json(
64
+ {
65
+ success: true,
66
+ message: "If an account with that email exists, a password reset link has been sent.",
67
+ },
68
+ { status: 200 }
69
+ );
70
+ }
71
+
72
+ logger.info("password_reset_requested", {
73
+ filename: get_filename(),
74
+ line_number: get_line_number(),
75
+ email,
76
+ });
77
+
78
+ // Always return success to prevent email enumeration attacks
79
+ return NextResponse.json(
80
+ {
81
+ success: true,
82
+ message: "If an account with that email exists, a password reset link has been sent.",
83
+ },
84
+ { status: 200 }
85
+ );
86
+ } catch (error) {
87
+ const error_message = error instanceof Error ? error.message : "Unknown error";
88
+ const error_stack = error instanceof Error ? error.stack : undefined;
89
+
90
+ logger.error("password_reset_error", {
91
+ filename: get_filename(),
92
+ line_number: get_line_number(),
93
+ error_message,
94
+ error_stack,
95
+ });
96
+
97
+ // Still return 200 OK to prevent email enumeration attacks
98
+ return NextResponse.json(
99
+ {
100
+ success: true,
101
+ message: "If an account with that email exists, a password reset link has been sent.",
102
+ },
103
+ { status: 200 }
104
+ );
105
+ }
106
+ }
107
+
@@ -0,0 +1,70 @@
1
+ // file_description: API route for listing library photo categories and photos in categories
2
+ // section: imports
3
+ import { NextRequest, NextResponse } from "next/server";
4
+ import { get_library_categories, get_library_photos } from "@/lib/services/profile_picture_service";
5
+ import { create_app_logger } from "@/lib/app_logger";
6
+ import { get_filename, get_line_number } from "@/lib/utils/api_route_helpers";
7
+
8
+ // section: api_handler
9
+ export async function GET(request: NextRequest) {
10
+ const logger = create_app_logger();
11
+
12
+ try {
13
+ const { searchParams } = new URL(request.url);
14
+ const category = searchParams.get("category");
15
+
16
+ if (category) {
17
+ // Return photos in the specified category
18
+ const photos = get_library_photos(category);
19
+
20
+ logger.info("library_photos_category_requested", {
21
+ filename: get_filename(),
22
+ line_number: get_line_number(),
23
+ category,
24
+ photoCount: photos.length,
25
+ });
26
+
27
+ return NextResponse.json(
28
+ {
29
+ success: true,
30
+ category,
31
+ photos,
32
+ },
33
+ { status: 200 }
34
+ );
35
+ } else {
36
+ // Return list of categories
37
+ const categories = get_library_categories();
38
+
39
+ logger.info("library_categories_requested", {
40
+ filename: get_filename(),
41
+ line_number: get_line_number(),
42
+ categoryCount: categories.length,
43
+ });
44
+
45
+ return NextResponse.json(
46
+ {
47
+ success: true,
48
+ categories,
49
+ },
50
+ { status: 200 }
51
+ );
52
+ }
53
+ } catch (error) {
54
+ const error_message = error instanceof Error ? error.message : "Unknown error";
55
+ const error_stack = error instanceof Error ? error.stack : undefined;
56
+
57
+ logger.error("library_photos_error", {
58
+ filename: get_filename(),
59
+ line_number: get_line_number(),
60
+ error_message,
61
+ error_stack,
62
+ });
63
+
64
+ return NextResponse.json(
65
+ { error: "Failed to fetch library photos" },
66
+ { status: 500 }
67
+ );
68
+ }
69
+ }
70
+