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,496 @@
1
+ // file_description: bootstrap configuration handling for the hazo_auth server
2
+ // section: imports
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import axios from "axios";
6
+ import { HazoConfig } from "hazo_config/dist/lib";
7
+ import { create_logger_service } from "../logging/logger_service";
8
+ import type {
9
+ app_context,
10
+ captcha_settings,
11
+ logger_service,
12
+ password_policy,
13
+ rate_limit_settings,
14
+ runtime_configuration,
15
+ token_settings,
16
+ } from "../types/app_types";
17
+
18
+ // section: schema_definitions
19
+ type direct_configuration_input = {
20
+ permission_names?: string[];
21
+ templates?: Record<string, string>;
22
+ labels?: Record<string, string>;
23
+ styles?: Record<string, string>;
24
+ emailer?: resolved_emailer_options;
25
+ logger?: logger_service;
26
+ password_policy?: Partial<password_policy>;
27
+ token_settings?: Partial<token_settings>;
28
+ rate_limit?: Partial<rate_limit_settings>;
29
+ captcha?: captcha_settings;
30
+ };
31
+
32
+ export type configuration_options = {
33
+ config_file_path?: string;
34
+ direct_configuration?: direct_configuration_input;
35
+ };
36
+ const is_string_record = (value: unknown): value is Record<string, string> =>
37
+ !!value &&
38
+ typeof value === "object" &&
39
+ !Array.isArray(value) &&
40
+ Object.values(value).every((entry) => typeof entry === "string");
41
+
42
+ const sanitize_configuration_options = (
43
+ options: configuration_options | undefined,
44
+ logger: logger_service
45
+ ): configuration_options => {
46
+ if (!options || typeof options !== "object") {
47
+ return {};
48
+ }
49
+
50
+ const sanitized: configuration_options = {};
51
+
52
+ if (typeof options.config_file_path === "string" && options.config_file_path.length > 0) {
53
+ sanitized.config_file_path = options.config_file_path;
54
+ }
55
+
56
+ if (options.direct_configuration && typeof options.direct_configuration === "object") {
57
+ const direct_config: direct_configuration_input = {};
58
+ const provided = options.direct_configuration;
59
+
60
+ if (Array.isArray(provided.permission_names)) {
61
+ direct_config.permission_names = provided.permission_names.filter(
62
+ (permission) => typeof permission === "string"
63
+ );
64
+ }
65
+
66
+ if (is_string_record(provided.templates)) {
67
+ direct_config.templates = provided.templates;
68
+ }
69
+
70
+ if (is_string_record(provided.labels)) {
71
+ direct_config.labels = provided.labels;
72
+ }
73
+
74
+ if (is_string_record(provided.styles)) {
75
+ direct_config.styles = provided.styles;
76
+ }
77
+
78
+ if (
79
+ provided.emailer &&
80
+ typeof provided.emailer === "object" &&
81
+ typeof provided.emailer.base_url === "string"
82
+ ) {
83
+ direct_config.emailer = {
84
+ base_url: provided.emailer.base_url,
85
+ api_key:
86
+ typeof provided.emailer.api_key === "string" ? provided.emailer.api_key : undefined,
87
+ headers: is_string_record(provided.emailer.headers) ? provided.emailer.headers : undefined,
88
+ };
89
+ }
90
+
91
+ if (provided.logger) {
92
+ direct_config.logger = provided.logger;
93
+ }
94
+
95
+ if (provided.password_policy) {
96
+ direct_config.password_policy = {
97
+ min_length:
98
+ typeof provided.password_policy.min_length === "number"
99
+ ? provided.password_policy.min_length
100
+ : undefined,
101
+ requires_lowercase: provided.password_policy.requires_lowercase,
102
+ requires_uppercase: provided.password_policy.requires_uppercase,
103
+ requires_number: provided.password_policy.requires_number,
104
+ requires_symbol: provided.password_policy.requires_symbol,
105
+ };
106
+ }
107
+
108
+ if (provided.token_settings) {
109
+ direct_config.token_settings = {
110
+ access_token_ttl_seconds: provided.token_settings.access_token_ttl_seconds,
111
+ refresh_token_ttl_seconds: provided.token_settings.refresh_token_ttl_seconds,
112
+ };
113
+ }
114
+
115
+ if (provided.rate_limit) {
116
+ direct_config.rate_limit = {
117
+ max_attempts: provided.rate_limit.max_attempts,
118
+ window_minutes: provided.rate_limit.window_minutes,
119
+ };
120
+ }
121
+
122
+ if (provided.captcha) {
123
+ direct_config.captcha = provided.captcha;
124
+ }
125
+
126
+ direct_config.logger?.info?.("config_direct_override_detected", { fields: Object.keys(direct_config) });
127
+ sanitized.direct_configuration = direct_config;
128
+ }
129
+
130
+ return sanitized;
131
+ };
132
+
133
+ type resolved_emailer_options = {
134
+ base_url: string;
135
+ api_key?: string;
136
+ headers?: Record<string, string>;
137
+ };
138
+
139
+ // section: defaults
140
+ const default_config_path = path.resolve(process.cwd(), "config.ini");
141
+
142
+ const default_password_policy: password_policy = {
143
+ min_length: 12,
144
+ requires_uppercase: true,
145
+ requires_lowercase: true,
146
+ requires_number: true,
147
+ requires_symbol: true,
148
+ };
149
+
150
+ const default_token_settings: token_settings = {
151
+ access_token_ttl_seconds: 15 * 60,
152
+ refresh_token_ttl_seconds: 60 * 60 * 24 * 30,
153
+ };
154
+
155
+ const default_rate_limit: rate_limit_settings = {
156
+ max_attempts: 5,
157
+ window_minutes: 5,
158
+ };
159
+
160
+ const read_ini_section = (
161
+ instance: HazoConfig | undefined,
162
+ section: string
163
+ ): Record<string, string> => {
164
+ if (instance === undefined) {
165
+ return {};
166
+ }
167
+ return instance.getSection(section) ?? {};
168
+ };
169
+
170
+ // section: helper_functions
171
+ const resolve_permissions = (
172
+ direct_permissions: string[] | undefined,
173
+ permission_section: Record<string, string>,
174
+ logger: logger_service
175
+ ): string[] => {
176
+ if (direct_permissions && direct_permissions.length > 0) {
177
+ logger.info("config_permissions_direct_override", { count: direct_permissions.length });
178
+ return direct_permissions;
179
+ }
180
+
181
+ const configured = permission_section.list
182
+ ?.split(",")
183
+ .map((value) => value.trim())
184
+ .filter((value) => value.length > 0);
185
+
186
+ if (configured && configured.length > 0) {
187
+ logger.info("config_permissions_from_file", { count: configured.length });
188
+ return configured;
189
+ }
190
+
191
+ logger.warn("config_permissions_default", {});
192
+ return [];
193
+ };
194
+
195
+ const resolve_password_policy = (
196
+ direct_policy: Partial<password_policy> | undefined,
197
+ auth_section: Record<string, string>,
198
+ logger: logger_service
199
+ ): password_policy => {
200
+ const resolved: password_policy = { ...default_password_policy };
201
+
202
+ const apply_value = <K extends keyof password_policy>(key: K, value: string | undefined) => {
203
+ if (value === undefined) {
204
+ return;
205
+ }
206
+ if (key === "min_length") {
207
+ const parsed = Number(value);
208
+ if (!Number.isNaN(parsed)) {
209
+ (resolved as any)[key] = parsed;
210
+ }
211
+ return;
212
+ }
213
+ (resolved as any)[key] = value === "true";
214
+ };
215
+
216
+ apply_value("min_length", auth_section.min_length);
217
+ apply_value("requires_uppercase", auth_section.requires_uppercase);
218
+ apply_value("requires_lowercase", auth_section.requires_lowercase);
219
+ apply_value("requires_number", auth_section.requires_number);
220
+ apply_value("requires_symbol", auth_section.requires_symbol);
221
+
222
+ if (direct_policy) {
223
+ Object.assign(resolved, direct_policy);
224
+ logger.info("config_password_policy_direct_override", resolved);
225
+ }
226
+
227
+ return resolved;
228
+ };
229
+
230
+ const resolve_token_settings = (
231
+ direct_tokens: Partial<token_settings> | undefined,
232
+ auth_section: Record<string, string>,
233
+ logger: logger_service
234
+ ): token_settings => {
235
+ const resolved: token_settings = { ...default_token_settings };
236
+
237
+ const access_token_value = Number(auth_section.access_token_ttl_seconds);
238
+ if (!Number.isNaN(access_token_value) && access_token_value > 0) {
239
+ resolved.access_token_ttl_seconds = access_token_value;
240
+ }
241
+
242
+ const refresh_token_value = Number(auth_section.refresh_token_ttl_seconds);
243
+ if (!Number.isNaN(refresh_token_value) && refresh_token_value > 0) {
244
+ resolved.refresh_token_ttl_seconds = refresh_token_value;
245
+ }
246
+
247
+ if (direct_tokens) {
248
+ Object.assign(resolved, direct_tokens);
249
+ logger.info("config_token_settings_direct_override", resolved);
250
+ }
251
+
252
+ return resolved;
253
+ };
254
+
255
+ const resolve_rate_limit = (
256
+ direct_rate_limit: Partial<rate_limit_settings> | undefined,
257
+ rate_section: Record<string, string>,
258
+ logger: logger_service
259
+ ): rate_limit_settings => {
260
+ const resolved: rate_limit_settings = { ...default_rate_limit };
261
+
262
+ const max_attempts = Number(rate_section.max_attempts);
263
+ if (!Number.isNaN(max_attempts) && max_attempts > 0) {
264
+ resolved.max_attempts = max_attempts;
265
+ }
266
+
267
+ const window_minutes = Number(rate_section.window_minutes);
268
+ if (!Number.isNaN(window_minutes) && window_minutes > 0) {
269
+ resolved.window_minutes = window_minutes;
270
+ }
271
+
272
+ if (direct_rate_limit) {
273
+ Object.assign(resolved, direct_rate_limit);
274
+ logger.info("config_rate_limit_direct_override", resolved);
275
+ }
276
+
277
+ return resolved;
278
+ };
279
+
280
+ const resolve_captcha = (
281
+ direct_captcha: captcha_settings | undefined,
282
+ captcha_section: Record<string, string>,
283
+ logger: logger_service
284
+ ): captcha_settings => {
285
+ if (direct_captcha) {
286
+ logger.info("config_captcha_direct_override", { provider: direct_captcha.provider });
287
+ return direct_captcha;
288
+ }
289
+
290
+ if (captcha_section.provider && captcha_section.secret_key) {
291
+ logger.info("config_captcha_from_file", { provider: captcha_section.provider });
292
+ return {
293
+ provider: captcha_section.provider as "recaptcha_v2" | "recaptcha_v3" | "hcaptcha",
294
+ secret_key: captcha_section.secret_key,
295
+ };
296
+ }
297
+
298
+ logger.warn("config_captcha_missing", {});
299
+ return undefined;
300
+ };
301
+
302
+ const resolve_dictionary = (
303
+ direct_values: Record<string, string> | undefined,
304
+ section_values: Record<string, string>,
305
+ logger: logger_service,
306
+ metric_name: string
307
+ ): Record<string, string> => {
308
+ if (direct_values && Object.keys(direct_values).length > 0) {
309
+ logger.info(`${metric_name}_direct_override`, { keys: Object.keys(direct_values) });
310
+ return direct_values;
311
+ }
312
+
313
+ if (Object.keys(section_values).length > 0) {
314
+ logger.info(`${metric_name}_from_file`, { keys: Object.keys(section_values) });
315
+ return section_values;
316
+ }
317
+
318
+ logger.warn(`${metric_name}_empty`, {});
319
+ return {};
320
+ };
321
+
322
+ const read_template_file = (file_path: string, logger: logger_service): string | undefined => {
323
+ const absolute_path = path.isAbsolute(file_path)
324
+ ? file_path
325
+ : path.resolve(process.cwd(), file_path);
326
+
327
+ try {
328
+ const content = fs.readFileSync(absolute_path, "utf-8");
329
+ logger.info("config_template_loaded", { file_path: absolute_path });
330
+ return content;
331
+ } catch (error) {
332
+ logger.error("config_template_load_failed", {
333
+ file_path: absolute_path,
334
+ error: (error as Error).message,
335
+ });
336
+ return undefined;
337
+ }
338
+ };
339
+
340
+ const resolve_templates = (
341
+ direct_templates: Record<string, string> | undefined,
342
+ template_section: Record<string, string>,
343
+ logger: logger_service
344
+ ): Record<string, string> => {
345
+ const resolved_templates: Record<string, string> = {};
346
+
347
+ Object.entries(template_section).forEach(([template_name, template_path]) => {
348
+ const template_content = read_template_file(template_path, logger);
349
+ if (template_content) {
350
+ resolved_templates[template_name] = template_content;
351
+ }
352
+ });
353
+
354
+ if (direct_templates) {
355
+ Object.entries(direct_templates).forEach(([template_name, template_body]) => {
356
+ resolved_templates[template_name] = template_body;
357
+ });
358
+ logger.info("config_templates_direct_override", { count: Object.keys(direct_templates).length });
359
+ }
360
+
361
+ return resolved_templates;
362
+ };
363
+
364
+ const create_emailer_client = (
365
+ emailer_options: resolved_emailer_options | undefined,
366
+ logger: logger_service
367
+ ) => {
368
+ if (!emailer_options) {
369
+ return {
370
+ send_message: async () => {
371
+ logger.warn("emailer_placeholder_invoked", {});
372
+ return { success: true };
373
+ },
374
+ };
375
+ }
376
+
377
+ const client = axios.create({
378
+ baseURL: emailer_options.base_url,
379
+ headers: {
380
+ ...(emailer_options.headers ?? {}),
381
+ ...(emailer_options.api_key ? { Authorization: `Bearer ${emailer_options.api_key}` } : {}),
382
+ "Content-Type": "application/json",
383
+ },
384
+ });
385
+
386
+ return {
387
+ send_message: async (payload: Record<string, unknown>) => {
388
+ try {
389
+ logger.info("emailer_request_initiated", { payload });
390
+ await client.post("/send", payload);
391
+ logger.info("emailer_request_success", {});
392
+ return { success: true };
393
+ } catch (error) {
394
+ logger.error("emailer_request_failed", { error: (error as Error).message });
395
+ return { success: false };
396
+ }
397
+ },
398
+ };
399
+ };
400
+
401
+ // section: loader
402
+ export const load_runtime_configuration = (
403
+ options?: configuration_options
404
+ ): runtime_configuration => {
405
+ const fallback_logger = create_logger_service("hazo_auth_config");
406
+ const parsed_options = sanitize_configuration_options(options, fallback_logger);
407
+ const direct_configuration = parsed_options.direct_configuration;
408
+ const logger = direct_configuration?.logger ?? fallback_logger;
409
+
410
+ let hazo_config: HazoConfig | undefined;
411
+
412
+ try {
413
+ const config_file_path = parsed_options?.config_file_path ?? default_config_path;
414
+ if (fs.existsSync(config_file_path)) {
415
+ hazo_config = new HazoConfig({
416
+ filePath: config_file_path,
417
+ logger,
418
+ });
419
+ logger.info("config_file_loaded", { config_file_path });
420
+ } else {
421
+ logger.warn("config_file_missing", { config_file_path });
422
+ }
423
+ } catch (error) {
424
+ logger.error("config_file_error", { error: (error as Error).message });
425
+ }
426
+
427
+ const permission_section = read_ini_section(hazo_config, "permissions");
428
+ const auth_section = read_ini_section(hazo_config, "auth");
429
+ const rate_section = read_ini_section(hazo_config, "rate_limit");
430
+ const label_section = read_ini_section(hazo_config, "labels");
431
+ const style_section = read_ini_section(hazo_config, "styles");
432
+ const template_section = read_ini_section(hazo_config, "templates");
433
+ const emailer_section = read_ini_section(hazo_config, "emailer");
434
+ const captcha_section = read_ini_section(hazo_config, "captcha");
435
+
436
+ const permission_names = resolve_permissions(
437
+ direct_configuration?.permission_names,
438
+ permission_section,
439
+ logger
440
+ );
441
+
442
+ const password_policy = resolve_password_policy(
443
+ direct_configuration?.password_policy,
444
+ auth_section,
445
+ logger
446
+ );
447
+
448
+ const token_settings = resolve_token_settings(
449
+ direct_configuration?.token_settings,
450
+ auth_section,
451
+ logger
452
+ );
453
+
454
+ const rate_limit = resolve_rate_limit(
455
+ direct_configuration?.rate_limit,
456
+ rate_section,
457
+ logger
458
+ );
459
+
460
+ const labels = resolve_dictionary(direct_configuration?.labels, label_section, logger, "config_labels");
461
+ const styles = resolve_dictionary(direct_configuration?.styles, style_section, logger, "config_styles");
462
+ const templates = resolve_templates(direct_configuration?.templates, template_section, logger);
463
+
464
+ const resolved_emailer_options =
465
+ direct_configuration?.emailer ??
466
+ (emailer_section.base_url
467
+ ? {
468
+ base_url: emailer_section.base_url,
469
+ api_key: emailer_section.api_key,
470
+ headers: emailer_section.headers ? JSON.parse(emailer_section.headers) : undefined,
471
+ }
472
+ : undefined);
473
+
474
+ const emailer = create_emailer_client(resolved_emailer_options, logger);
475
+
476
+ const captcha = resolve_captcha(direct_configuration?.captcha, captcha_section, logger);
477
+
478
+ return {
479
+ permission_names,
480
+ logger,
481
+ emailer,
482
+ templates,
483
+ labels,
484
+ styles,
485
+ password_policy,
486
+ token_settings,
487
+ rate_limit,
488
+ captcha,
489
+ };
490
+ };
491
+
492
+ // section: context_factory
493
+ export const create_app_context = (options?: configuration_options): app_context => ({
494
+ config: load_runtime_configuration(options),
495
+ });
496
+
@@ -0,0 +1,38 @@
1
+ // file_description: bootstrap entry point for the hazo_auth express server
2
+ // section: imports
3
+ import http from "http";
4
+ import { create_server_app } from "./server";
5
+ import { create_logger_service } from "./logging/logger_service";
6
+
7
+ // section: constants
8
+ const default_port = Number(process.env.PORT ?? 4100);
9
+ const server_namespace = "hazo_auth_server";
10
+
11
+ // section: bootstrap_runner
12
+ export const start_server = async (): Promise<void> => {
13
+ const logger = create_logger_service(server_namespace);
14
+ const app = create_server_app();
15
+ const http_server = http.createServer(app);
16
+
17
+ return new Promise((resolve, reject) => {
18
+ http_server.listen(default_port, () => {
19
+ logger.info("server_started", { port: default_port });
20
+ resolve();
21
+ });
22
+
23
+ http_server.on("error", (error) => {
24
+ logger.error("server_start_failed", { error: (error as Error).message });
25
+ reject(error);
26
+ });
27
+ });
28
+ };
29
+
30
+ // section: direct_execution_guard
31
+ const resolved_module_path = new URL(import.meta.url).pathname;
32
+ const entry_module_path =
33
+ process.argv[1] !== undefined ? new URL(`file://${process.argv[1]}`).pathname : undefined;
34
+ const is_primary_module = entry_module_path !== undefined && entry_module_path === resolved_module_path;
35
+ if (is_primary_module) {
36
+ void start_server();
37
+ }
38
+
@@ -0,0 +1,56 @@
1
+ // file_description: expose the logging facade used across the hazo_auth backend
2
+ // section: imports
3
+ import type { logger_method, logger_service } from "../types/app_types";
4
+
5
+ // section: helper_functions
6
+ const create_console_logger = (namespace: string): logger_service => {
7
+ const write = (level: string, message: string, data?: Record<string, unknown>) => {
8
+ const timestamp = new Date().toISOString();
9
+ // eslint-disable-next-line no-console
10
+ console.log(
11
+ JSON.stringify({
12
+ namespace,
13
+ level,
14
+ message,
15
+ data,
16
+ timestamp,
17
+ })
18
+ );
19
+ };
20
+
21
+ return {
22
+ debug: (message, data) => write("debug", message, data),
23
+ info: (message, data) => write("info", message, data),
24
+ warn: (message, data) => write("warn", message, data),
25
+ error: (message, data) => write("error", message, data),
26
+ };
27
+ };
28
+
29
+ // section: factory
30
+ export const create_logger_service = (
31
+ namespace: string,
32
+ external_logger?: Partial<logger_service>
33
+ ): logger_service => {
34
+ const console_logger = create_console_logger(namespace);
35
+
36
+ const safe_bind = (
37
+ level: keyof logger_service,
38
+ fallback: logger_method
39
+ ): logger_method => {
40
+ const candidate = external_logger?.[level];
41
+ if (typeof candidate === "function") {
42
+ return (message, data) => candidate(message, data);
43
+ }
44
+ return fallback;
45
+ };
46
+
47
+ type logger_method = (message: string, data?: Record<string, unknown>) => void;
48
+
49
+ return {
50
+ debug: safe_bind("debug", console_logger.debug),
51
+ info: safe_bind("info", console_logger.info),
52
+ warn: safe_bind("warn", console_logger.warn),
53
+ error: safe_bind("error", console_logger.error),
54
+ };
55
+ };
56
+
@@ -0,0 +1,16 @@
1
+ // file_description: declares the primary express router for the hazo_auth backend
2
+ // section: imports
3
+ import { Router } from "express";
4
+
5
+ // section: router_factory
6
+ export const create_root_router = (): Router => {
7
+ const root_router = Router();
8
+ // section: health_endpoint
9
+ root_router.get("/health", (_request, response) => {
10
+ response.status(200).json({
11
+ status: "ok",
12
+ });
13
+ });
14
+ return root_router;
15
+ };
16
+
@@ -0,0 +1,28 @@
1
+ // file_description: configure and export the express application for hazo_auth
2
+ // section: imports
3
+ import express from "express";
4
+ import helmet from "helmet";
5
+ import cors from "cors";
6
+ import cookie_parser from "cookie-parser";
7
+ import compression from "compression";
8
+ import type { Application } from "express";
9
+ import { create_root_router } from "./routes/root_router";
10
+ import { create_app_context } from "./config/config_loader";
11
+
12
+ // section: app_factory
13
+ export const create_server_app = (): Application => {
14
+ const server_app = express();
15
+ server_app.use(helmet({ crossOriginResourcePolicy: false }));
16
+ server_app.use(cors({ credentials: true, origin: true }));
17
+ server_app.use(express.json({ limit: "1mb" }));
18
+ server_app.use(express.urlencoded({ extended: true }));
19
+ server_app.use(cookie_parser());
20
+ server_app.use(compression());
21
+ server_app.use((request, _response, next) => {
22
+ request.context = create_app_context();
23
+ next();
24
+ });
25
+ server_app.use(create_root_router());
26
+ return server_app;
27
+ };
28
+
@@ -0,0 +1,74 @@
1
+ // file_description: define shared application level types for the hazo_auth server
2
+ // section: request_context_types
3
+ import type { Request } from "express";
4
+
5
+ // section: logger_interface_definition
6
+ export type logger_method = (
7
+ message: string,
8
+ data?: Record<string, unknown>
9
+ ) => void;
10
+
11
+ export type logger_service = {
12
+ debug: logger_method;
13
+ info: logger_method;
14
+ warn: logger_method;
15
+ error: logger_method;
16
+ };
17
+
18
+ // section: configuration_types
19
+ export type emailer_client = {
20
+ send_message: (
21
+ payload: Record<string, unknown>
22
+ ) => Promise<{ success: boolean }>;
23
+ };
24
+
25
+ export type handlebars_templates = Record<string, string>;
26
+
27
+ export type password_policy = {
28
+ min_length: number;
29
+ requires_uppercase: boolean;
30
+ requires_lowercase: boolean;
31
+ requires_number: boolean;
32
+ requires_symbol: boolean;
33
+ };
34
+
35
+ export type token_settings = {
36
+ access_token_ttl_seconds: number;
37
+ refresh_token_ttl_seconds: number;
38
+ };
39
+
40
+ export type rate_limit_settings = {
41
+ max_attempts: number;
42
+ window_minutes: number;
43
+ };
44
+
45
+ export type captcha_settings =
46
+ | {
47
+ provider: "recaptcha_v2" | "recaptcha_v3" | "hcaptcha";
48
+ secret_key: string;
49
+ }
50
+ | undefined;
51
+
52
+ export type runtime_configuration = {
53
+ permission_names: string[];
54
+ logger: logger_service;
55
+ emailer: emailer_client;
56
+ templates: handlebars_templates;
57
+ labels: Record<string, string>;
58
+ styles: Record<string, string>;
59
+ password_policy: password_policy;
60
+ token_settings: token_settings;
61
+ rate_limit: rate_limit_settings;
62
+ captcha: captcha_settings;
63
+ };
64
+
65
+ export type app_context = {
66
+ config: runtime_configuration;
67
+ };
68
+
69
+ // section: typed_request_wrapper
70
+ export type context_request<T = unknown> = Request & {
71
+ body: T;
72
+ context: app_context;
73
+ };
74
+