hazo_auth 7.0.2 → 9.0.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.
- package/README.md +34 -0
- package/SETUP_CHECKLIST.md +31 -0
- package/cli-src/lib/AGENTS.md +26 -0
- package/cli-src/lib/app_logger.ts +3 -7
- package/cli-src/lib/auth/auth_types.ts +3 -0
- package/cli-src/lib/auth/auth_utils.server.ts +2 -1
- package/cli-src/lib/auth/ensure_anon_id.server.ts +2 -1
- package/cli-src/lib/auth/hazo_get_auth.server.ts +30 -4
- package/cli-src/lib/config/hazo_auth_core_config.ts +44 -0
- package/cli-src/lib/cookies_config.server.ts +13 -10
- package/cli-src/lib/hazo_connect_setup.server.ts +19 -11
- package/cli-src/lib/legal/legal_docs_config.server.ts +61 -0
- package/cli-src/lib/legal/legal_docs_reader.server.ts +36 -0
- package/cli-src/lib/legal/legal_docs_service.ts +197 -0
- package/cli-src/lib/legal/legal_docs_types.ts +31 -0
- package/cli-src/lib/services/email_service.ts +22 -11
- package/cli-src/lib/services/firm_service.ts +2 -1
- package/cli-src/lib/services/otp_service.ts +3 -2
- package/cli-src/lib/services/profile_picture_service.ts +2 -1
- package/cli-src/lib/services/registration_service.ts +16 -1
- package/cli-src/lib/services/relationship_service.ts +5 -4
- package/cli-src/lib/services/session_token_service.ts +3 -2
- package/cli-src/lib/utils/api_route_helpers.ts +4 -59
- package/cli-src/lib/utils/get_origin_url.ts +5 -61
- package/cli-src/lib/utils.ts +4 -10
- package/config/hazo_auth_config.example.ini +6 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3 -0
- package/dist/components/layouts/index.d.ts +1 -0
- package/dist/components/layouts/index.d.ts.map +1 -1
- package/dist/components/layouts/index.js +2 -0
- package/dist/components/layouts/legal/index.d.ts +5 -0
- package/dist/components/layouts/legal/index.d.ts.map +1 -0
- package/dist/components/layouts/legal/index.js +4 -0
- package/dist/components/layouts/legal/legal_acceptance_gate.d.ts +7 -0
- package/dist/components/layouts/legal/legal_acceptance_gate.d.ts.map +1 -0
- package/dist/components/layouts/legal/legal_acceptance_gate.js +84 -0
- package/dist/components/layouts/legal/legal_doc_checkbox_list.d.ts +9 -0
- package/dist/components/layouts/legal/legal_doc_checkbox_list.d.ts.map +1 -0
- package/dist/components/layouts/legal/legal_doc_checkbox_list.js +11 -0
- package/dist/components/layouts/legal/legal_doc_combined_view.d.ts +9 -0
- package/dist/components/layouts/legal/legal_doc_combined_view.d.ts.map +1 -0
- package/dist/components/layouts/legal/legal_doc_combined_view.js +11 -0
- package/dist/components/layouts/legal/legal_doc_drawer.d.ts +8 -0
- package/dist/components/layouts/legal/legal_doc_drawer.d.ts.map +1 -0
- package/dist/components/layouts/legal/legal_doc_drawer.js +55 -0
- package/dist/components/layouts/register/hooks/use_register_form.d.ts +5 -1
- package/dist/components/layouts/register/hooks/use_register_form.d.ts.map +1 -1
- package/dist/components/layouts/register/hooks/use_register_form.js +25 -10
- package/dist/components/layouts/register/index.d.ts.map +1 -1
- package/dist/components/layouts/register/index.js +21 -1
- package/dist/components/layouts/user_management/index.d.ts.map +1 -1
- package/dist/components/layouts/user_management/index.js +45 -7
- package/dist/components/ui/input-otp.d.ts +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/app_logger.d.ts +2 -3
- package/dist/lib/app_logger.d.ts.map +1 -1
- package/dist/lib/app_logger.js +3 -5
- package/dist/lib/auth/auth_types.d.ts +2 -0
- package/dist/lib/auth/auth_types.d.ts.map +1 -1
- package/dist/lib/auth/auth_types.js +0 -2
- package/dist/lib/auth/auth_utils.server.d.ts.map +1 -1
- package/dist/lib/auth/auth_utils.server.js +2 -1
- package/dist/lib/auth/ensure_anon_id.server.d.ts.map +1 -1
- package/dist/lib/auth/ensure_anon_id.server.js +2 -1
- package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
- package/dist/lib/auth/hazo_get_auth.server.js +30 -4
- package/dist/lib/config/hazo_auth_core_config.d.ts +44 -0
- package/dist/lib/config/hazo_auth_core_config.d.ts.map +1 -0
- package/dist/lib/config/hazo_auth_core_config.js +40 -0
- package/dist/lib/cookies_config.server.d.ts.map +1 -1
- package/dist/lib/cookies_config.server.js +12 -7
- package/dist/lib/hazo_connect_setup.server.d.ts.map +1 -1
- package/dist/lib/hazo_connect_setup.server.js +18 -5
- package/dist/lib/legal/legal_docs_config.server.d.ts +22 -0
- package/dist/lib/legal/legal_docs_config.server.d.ts.map +1 -0
- package/dist/lib/legal/legal_docs_config.server.js +52 -0
- package/dist/lib/legal/legal_docs_reader.server.d.ts +15 -0
- package/dist/lib/legal/legal_docs_reader.server.d.ts.map +1 -0
- package/dist/lib/legal/legal_docs_reader.server.js +24 -0
- package/dist/lib/legal/legal_docs_service.d.ts +49 -0
- package/dist/lib/legal/legal_docs_service.d.ts.map +1 -0
- package/dist/lib/legal/legal_docs_service.js +141 -0
- package/dist/lib/legal/legal_docs_types.d.ts +25 -0
- package/dist/lib/legal/legal_docs_types.d.ts.map +1 -0
- package/dist/lib/legal/legal_docs_types.js +2 -0
- package/dist/lib/services/email_service.d.ts +1 -1
- package/dist/lib/services/email_service.d.ts.map +1 -1
- package/dist/lib/services/email_service.js +21 -9
- package/dist/lib/services/firm_service.d.ts.map +1 -1
- package/dist/lib/services/firm_service.js +2 -1
- package/dist/lib/services/otp_service.d.ts.map +1 -1
- package/dist/lib/services/otp_service.js +3 -2
- package/dist/lib/services/profile_picture_service.d.ts.map +1 -1
- package/dist/lib/services/profile_picture_service.js +2 -1
- package/dist/lib/services/registration_service.d.ts +5 -0
- package/dist/lib/services/registration_service.d.ts.map +1 -1
- package/dist/lib/services/registration_service.js +6 -0
- package/dist/lib/services/relationship_service.d.ts.map +1 -1
- package/dist/lib/services/relationship_service.js +5 -4
- package/dist/lib/services/session_token_service.d.ts.map +1 -1
- package/dist/lib/services/session_token_service.js +3 -2
- package/dist/lib/utils/api_route_helpers.d.ts +1 -12
- package/dist/lib/utils/api_route_helpers.d.ts.map +1 -1
- package/dist/lib/utils/api_route_helpers.js +4 -57
- package/dist/lib/utils/get_origin_url.d.ts +1 -22
- package/dist/lib/utils/get_origin_url.d.ts.map +1 -1
- package/dist/lib/utils/get_origin_url.js +5 -57
- package/dist/lib/utils.d.ts +2 -3
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +4 -9
- package/dist/page_components/index.d.ts +0 -5
- package/dist/page_components/index.d.ts.map +1 -1
- package/dist/page_components/index.js +0 -5
- package/dist/server/config/config_loader.js +2 -2
- package/dist/server/index.js +1 -1
- package/dist/server/routes/index.d.ts +3 -0
- package/dist/server/routes/index.d.ts.map +1 -1
- package/dist/server/routes/index.js +4 -0
- package/dist/server/routes/legal_docs_accept.d.ts +3 -0
- package/dist/server/routes/legal_docs_accept.d.ts.map +1 -0
- package/dist/server/routes/legal_docs_accept.js +43 -0
- package/dist/server/routes/legal_docs_get.d.ts +3 -0
- package/dist/server/routes/legal_docs_get.d.ts.map +1 -0
- package/dist/server/routes/legal_docs_get.js +49 -0
- package/dist/server/routes/legal_docs_publish.d.ts +3 -0
- package/dist/server/routes/legal_docs_publish.d.ts.map +1 -0
- package/dist/server/routes/legal_docs_publish.js +35 -0
- package/dist/server/routes/register.d.ts.map +1 -1
- package/dist/server/routes/register.js +26 -0
- package/dist/server/routes/remove_profile_picture.d.ts.map +1 -1
- package/dist/server/routes/remove_profile_picture.js +6 -1
- package/dist/server/routes/upload_profile_picture.d.ts.map +1 -1
- package/dist/server/routes/upload_profile_picture.js +6 -1
- package/dist/server/routes/user_management_users.d.ts +2 -2
- package/dist/server/routes/user_management_users.d.ts.map +1 -1
- package/dist/server/routes/user_management_users.js +46 -2
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +7 -0
- package/dist/strings.d.ts +2 -0
- package/dist/strings.d.ts.map +1 -0
- package/dist/strings.js +3 -0
- package/package.json +33 -35
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// file_description: service functions for the legal document acceptance system
|
|
2
|
+
// section: imports
|
|
3
|
+
import { createCrudService } from 'hazo_connect/server';
|
|
4
|
+
import { generateRequestId } from 'hazo_core';
|
|
5
|
+
import type { LegalAcceptanceMap } from './legal_docs_types';
|
|
6
|
+
|
|
7
|
+
// section: helpers
|
|
8
|
+
|
|
9
|
+
function generate_id(): string {
|
|
10
|
+
return generateRequestId().slice(4);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// section: exports
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Write legal acceptance records. Call from:
|
|
17
|
+
* - registration_service (bundled with register POST, pre-session)
|
|
18
|
+
* - legal_docs_accept route (authenticated, post-login)
|
|
19
|
+
*
|
|
20
|
+
* Inserts one row per doc into hazo_legal_acceptances (audit history) and
|
|
21
|
+
* merges the result into the denormalised hazo_users.legal_acceptance JSONB
|
|
22
|
+
* column for fast "has this user accepted the current version?" queries.
|
|
23
|
+
*/
|
|
24
|
+
export async function write_legal_acceptance(
|
|
25
|
+
adapter: any,
|
|
26
|
+
user_id: string,
|
|
27
|
+
accepted: Record<string, { hash: string }>,
|
|
28
|
+
ip: string | null,
|
|
29
|
+
user_agent: string | null,
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
const now = new Date().toISOString();
|
|
32
|
+
|
|
33
|
+
// 1. Insert one history row per doc
|
|
34
|
+
const history_service = createCrudService(adapter, 'hazo_legal_acceptances');
|
|
35
|
+
for (const [doc_key, { hash }] of Object.entries(accepted)) {
|
|
36
|
+
await history_service.insert({
|
|
37
|
+
id: generate_id(),
|
|
38
|
+
user_id,
|
|
39
|
+
doc_key,
|
|
40
|
+
doc_hash: hash,
|
|
41
|
+
accepted_at: now,
|
|
42
|
+
ip,
|
|
43
|
+
user_agent,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 2. Merge into denormalized JSONB on hazo_users
|
|
48
|
+
const users_service = createCrudService(adapter, 'hazo_users');
|
|
49
|
+
const rows = await users_service.findBy({ id: user_id });
|
|
50
|
+
|
|
51
|
+
const existing_raw = rows[0]?.legal_acceptance;
|
|
52
|
+
let existing: LegalAcceptanceMap = {};
|
|
53
|
+
if (existing_raw) {
|
|
54
|
+
try {
|
|
55
|
+
existing = typeof existing_raw === 'string'
|
|
56
|
+
? JSON.parse(existing_raw)
|
|
57
|
+
: (existing_raw as LegalAcceptanceMap);
|
|
58
|
+
} catch { /* corrupt — start fresh */ }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const updated: LegalAcceptanceMap = { ...existing };
|
|
62
|
+
for (const [doc_key, { hash }] of Object.entries(accepted)) {
|
|
63
|
+
updated[doc_key] = { hash, accepted_at: now, ip, user_agent };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await users_service.updateById(user_id, {
|
|
67
|
+
legal_acceptance: JSON.stringify(updated),
|
|
68
|
+
changed_at: now,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Publish a doc version as the new required version (admin action).
|
|
74
|
+
* Upserts hazo_legal_doc_versions keyed on doc_key.
|
|
75
|
+
*/
|
|
76
|
+
export async function publish_doc_version(
|
|
77
|
+
adapter: any,
|
|
78
|
+
doc_key: string,
|
|
79
|
+
required_hash: string,
|
|
80
|
+
published_by_user_id: string,
|
|
81
|
+
): Promise<{ published_at: string }> {
|
|
82
|
+
const now = new Date().toISOString();
|
|
83
|
+
|
|
84
|
+
// createCrudService with doc_key as primary key so updateById works correctly
|
|
85
|
+
const versions_service = createCrudService(adapter, 'hazo_legal_doc_versions', {
|
|
86
|
+
primaryKeys: ['doc_key'],
|
|
87
|
+
autoId: false,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const existing = await versions_service.findBy({ doc_key });
|
|
91
|
+
|
|
92
|
+
if (existing.length > 0) {
|
|
93
|
+
await versions_service.updateById(doc_key, {
|
|
94
|
+
required_hash,
|
|
95
|
+
published_at: now,
|
|
96
|
+
published_by_user_id,
|
|
97
|
+
});
|
|
98
|
+
} else {
|
|
99
|
+
await versions_service.insert({
|
|
100
|
+
doc_key,
|
|
101
|
+
required_hash,
|
|
102
|
+
published_at: now,
|
|
103
|
+
published_by_user_id,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { published_at: now };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Return required version info keyed by doc_key.
|
|
112
|
+
*/
|
|
113
|
+
export async function get_required_versions(
|
|
114
|
+
adapter: any,
|
|
115
|
+
doc_keys: string[],
|
|
116
|
+
): Promise<Record<string, { required_hash: string; published_at: string }>> {
|
|
117
|
+
if (doc_keys.length === 0) return {};
|
|
118
|
+
|
|
119
|
+
const versions_service = createCrudService(adapter, 'hazo_legal_doc_versions', {
|
|
120
|
+
primaryKeys: ['doc_key'],
|
|
121
|
+
autoId: false,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const rows = await versions_service.list((qb) =>
|
|
125
|
+
qb.whereIn('doc_key', doc_keys).select(['doc_key', 'required_hash', 'published_at'])
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const result: Record<string, { required_hash: string; published_at: string }> = {};
|
|
129
|
+
for (const row of rows) {
|
|
130
|
+
result[String(row.doc_key)] = {
|
|
131
|
+
required_hash: String(row.required_hash),
|
|
132
|
+
published_at: String(row.published_at),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Return acceptance history for a user across all doc keys, newest first.
|
|
140
|
+
*/
|
|
141
|
+
export async function get_user_acceptance_history(
|
|
142
|
+
adapter: any,
|
|
143
|
+
user_id: string,
|
|
144
|
+
): Promise<Array<{
|
|
145
|
+
doc_key: string;
|
|
146
|
+
doc_hash: string;
|
|
147
|
+
accepted_at: string;
|
|
148
|
+
ip: string | null;
|
|
149
|
+
user_agent: string | null;
|
|
150
|
+
}>> {
|
|
151
|
+
const history_service = createCrudService(adapter, 'hazo_legal_acceptances');
|
|
152
|
+
|
|
153
|
+
return history_service.list((qb) =>
|
|
154
|
+
qb
|
|
155
|
+
.where('user_id', 'eq', user_id)
|
|
156
|
+
.select(['doc_key', 'doc_hash', 'accepted_at', 'ip', 'user_agent'])
|
|
157
|
+
.order('accepted_at', 'desc')
|
|
158
|
+
) as Promise<Array<{
|
|
159
|
+
doc_key: string;
|
|
160
|
+
doc_hash: string;
|
|
161
|
+
accepted_at: string;
|
|
162
|
+
ip: string | null;
|
|
163
|
+
user_agent: string | null;
|
|
164
|
+
}>>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Count how many users have accepted the current required hash for a doc key.
|
|
169
|
+
* Returns { current, total } where current is users on the required_hash and
|
|
170
|
+
* total is all users in the system (including those with no legal_acceptance data).
|
|
171
|
+
*
|
|
172
|
+
* Note: This performs an in-process scan over all users. For large user bases
|
|
173
|
+
* consider a dedicated SQL query in a future optimisation.
|
|
174
|
+
*/
|
|
175
|
+
export async function get_compliance_count(
|
|
176
|
+
adapter: any,
|
|
177
|
+
doc_key: string,
|
|
178
|
+
required_hash: string,
|
|
179
|
+
): Promise<{ current: number; total: number }> {
|
|
180
|
+
const users_service = createCrudService(adapter, 'hazo_users');
|
|
181
|
+
const all_users = await users_service.list((qb) =>
|
|
182
|
+
qb.select(['id', 'legal_acceptance'])
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
let current = 0;
|
|
186
|
+
for (const user of all_users) {
|
|
187
|
+
let map: LegalAcceptanceMap = {};
|
|
188
|
+
try {
|
|
189
|
+
map = typeof user.legal_acceptance === 'string'
|
|
190
|
+
? JSON.parse(user.legal_acceptance as string)
|
|
191
|
+
: ((user.legal_acceptance ?? {}) as LegalAcceptanceMap);
|
|
192
|
+
} catch { /* ignore corrupt rows */ }
|
|
193
|
+
if (map[doc_key]?.hash === required_hash) current++;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { current, total: all_users.length };
|
|
197
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// file_description: shared types for the legal document acceptance system
|
|
2
|
+
|
|
3
|
+
export interface LegalDocConfig {
|
|
4
|
+
key: string;
|
|
5
|
+
title: string;
|
|
6
|
+
path: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface LegalDocsConfig {
|
|
10
|
+
docs: LegalDocConfig[];
|
|
11
|
+
display_mode: 'separate' | 'combined';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface LegalDoc {
|
|
15
|
+
key: string;
|
|
16
|
+
title: string;
|
|
17
|
+
content: string;
|
|
18
|
+
hash: string;
|
|
19
|
+
required_hash: string | null;
|
|
20
|
+
required_published_at: string | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface LegalAcceptanceRecord {
|
|
24
|
+
hash: string;
|
|
25
|
+
accepted_at: string;
|
|
26
|
+
ip: string | null;
|
|
27
|
+
user_agent: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Shape of hazo_users.legal_acceptance JSONB
|
|
31
|
+
export type LegalAcceptanceMap = Record<string, LegalAcceptanceRecord>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// file_description: service for sending emails with template support
|
|
2
2
|
// section: imports
|
|
3
3
|
import { create_app_logger } from "../app_logger.js";
|
|
4
|
+
import { HazoConfigError, optional_import } from "hazo_core";
|
|
4
5
|
import { read_config_section } from "../config/config_loader.server.js";
|
|
5
|
-
import type { EmailerConfig, SendEmailOptions } from "hazo_notify";
|
|
6
|
+
import type { EmailerConfig, SendEmailOptions } from "hazo_notify/adapters/email";
|
|
6
7
|
import type { HazoConnectInstance } from "hazo_notify/template_manager";
|
|
7
8
|
|
|
8
9
|
// section: types
|
|
@@ -76,8 +77,11 @@ async function get_hazo_notify_instance(): Promise<EmailerConfig> {
|
|
|
76
77
|
});
|
|
77
78
|
try {
|
|
78
79
|
// Dynamic import to avoid build-time issues with hazo_notify
|
|
79
|
-
const
|
|
80
|
-
|
|
80
|
+
const hazo_notify_email_module = await optional_import<typeof import("hazo_notify/adapters/email")>('hazo_notify/adapters/email');
|
|
81
|
+
if (!hazo_notify_email_module) {
|
|
82
|
+
throw new Error("hazo_notify is not installed");
|
|
83
|
+
}
|
|
84
|
+
const { load_emailer_config } = hazo_notify_email_module;
|
|
81
85
|
hazo_notify_config = load_emailer_config();
|
|
82
86
|
} catch (error) {
|
|
83
87
|
const error_message = error instanceof Error ? error.message : "Unknown error";
|
|
@@ -86,7 +90,7 @@ async function get_hazo_notify_instance(): Promise<EmailerConfig> {
|
|
|
86
90
|
line_number: 0,
|
|
87
91
|
error: error_message,
|
|
88
92
|
});
|
|
89
|
-
throw new
|
|
93
|
+
throw new HazoConfigError({ code: 'HAZO_AUTH_CONFIG', pkg: 'hazo_auth', message: `Failed to load hazo_notify config: ${error_message}` });
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
return hazo_notify_config;
|
|
@@ -263,8 +267,12 @@ export async function send_email(options: EmailOptions): Promise<{ success: bool
|
|
|
263
267
|
const notify_config = await get_hazo_notify_instance();
|
|
264
268
|
|
|
265
269
|
// Dynamic import to avoid build-time issues with hazo_notify
|
|
266
|
-
const
|
|
267
|
-
|
|
270
|
+
const hazo_notify_email_module = await optional_import<typeof import("hazo_notify/adapters/email")>('hazo_notify/adapters/email');
|
|
271
|
+
if (!hazo_notify_email_module) {
|
|
272
|
+
throw new Error("hazo_notify is not installed");
|
|
273
|
+
}
|
|
274
|
+
const { get_email_provider } = hazo_notify_email_module;
|
|
275
|
+
const provider = get_email_provider(notify_config);
|
|
268
276
|
|
|
269
277
|
// Get from email and from name (hazo_auth_config overrides hazo_notify_config)
|
|
270
278
|
// Priority: 1. options.from (explicit parameter), 2. hazo_auth_config.from_email, 3. hazo_notify_config.from_email
|
|
@@ -285,7 +293,7 @@ export async function send_email(options: EmailOptions): Promise<{ success: bool
|
|
|
285
293
|
};
|
|
286
294
|
|
|
287
295
|
// Send email using hazo_notify
|
|
288
|
-
const result = await
|
|
296
|
+
const result = await provider.send_email(hazo_notify_options, notify_config);
|
|
289
297
|
|
|
290
298
|
if (result.success) {
|
|
291
299
|
logger.info("email_sent", {
|
|
@@ -370,11 +378,14 @@ export async function send_template_email(
|
|
|
370
378
|
}
|
|
371
379
|
|
|
372
380
|
if (!hazo_notify_connect) {
|
|
373
|
-
throw new
|
|
374
|
-
|
|
381
|
+
throw new HazoConfigError({
|
|
382
|
+
code: 'HAZO_AUTH_CONFIG',
|
|
383
|
+
pkg: 'hazo_auth',
|
|
384
|
+
message:
|
|
385
|
+
"hazo_notify connect not initialized. Call set_hazo_notify_connect() " +
|
|
375
386
|
"from instrumentation.ts with the same HazoConnectInstance you pass " +
|
|
376
|
-
"to init_template_manager({ hazo_connect_factory })."
|
|
377
|
-
);
|
|
387
|
+
"to init_template_manager({ hazo_connect_factory }).",
|
|
388
|
+
});
|
|
378
389
|
}
|
|
379
390
|
|
|
380
391
|
const notify_config = await get_hazo_notify_instance();
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// section: imports
|
|
3
3
|
import type { HazoConnectAdapter } from "hazo_connect";
|
|
4
4
|
import { createCrudService } from "hazo_connect/server";
|
|
5
|
+
import { generateRequestId } from "hazo_core";
|
|
5
6
|
import { create_app_logger } from "../app_logger.js";
|
|
6
7
|
import { sanitize_error_for_user } from "../utils/error_sanitizer.js";
|
|
7
8
|
import { create_scope, type ScopeRecord } from "./scope_service.js";
|
|
@@ -146,7 +147,7 @@ async function assign_owner_permissions(
|
|
|
146
147
|
|
|
147
148
|
if (!Array.isArray(permissions) || permissions.length === 0) {
|
|
148
149
|
// Create permission with generated UUID
|
|
149
|
-
const perm_id =
|
|
150
|
+
const perm_id = generateRequestId().slice(4);
|
|
150
151
|
const inserted = await permission_service.insert({
|
|
151
152
|
id: perm_id,
|
|
152
153
|
permission_name: perm_name,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import "server-only";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
+
import { generateRequestId } from "hazo_core";
|
|
3
4
|
import argon2 from "argon2";
|
|
4
5
|
import { createCrudService } from "hazo_connect/server";
|
|
5
6
|
import { get_otp_config, hazo_auth_otp_session_ttl_seconds } from "../otp_config.server.js";
|
|
@@ -133,7 +134,7 @@ export async function request_email_otp(args: {
|
|
|
133
134
|
const code = generate_otp_code();
|
|
134
135
|
const otp_hash = await hash_otp_code(code);
|
|
135
136
|
const expires_at = new Date(Date.now() + cfg.code_ttl_seconds * 1000).toISOString();
|
|
136
|
-
const row_id =
|
|
137
|
+
const row_id = generateRequestId().slice(4);
|
|
137
138
|
|
|
138
139
|
await otp_table.insert({
|
|
139
140
|
id: row_id,
|
|
@@ -244,7 +245,7 @@ export async function verify_email_otp(args: {
|
|
|
244
245
|
}
|
|
245
246
|
} else if (cfg.auto_register) {
|
|
246
247
|
// Create user + bind scope/role
|
|
247
|
-
const new_user_id =
|
|
248
|
+
const new_user_id = generateRequestId().slice(4);
|
|
248
249
|
await users_table.insert({
|
|
249
250
|
id: new_user_id,
|
|
250
251
|
email_address: email,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { HazoConnectAdapter } from "hazo_connect";
|
|
4
4
|
import { createCrudService } from "hazo_connect/server";
|
|
5
5
|
import gravatarUrl from "gravatar-url";
|
|
6
|
+
import { fetchWithRequestId } from "hazo_core";
|
|
6
7
|
import { get_profile_picture_config } from "../profile_picture_config.server.js";
|
|
7
8
|
import { get_ui_sizes_config } from "../ui_sizes_config.server.js";
|
|
8
9
|
import { get_file_types_config } from "../file_types_config.server.js";
|
|
@@ -304,7 +305,7 @@ async function check_gravatar_exists(email: string): Promise<boolean> {
|
|
|
304
305
|
const gravatar_url = get_gravatar_url(email, uiSizes.gravatar_size);
|
|
305
306
|
|
|
306
307
|
// Make HEAD request to check if image exists without downloading it
|
|
307
|
-
const response = await
|
|
308
|
+
const response = await fetchWithRequestId(gravatar_url, {
|
|
308
309
|
method: 'HEAD',
|
|
309
310
|
// Add timeout to prevent hanging
|
|
310
311
|
signal: AbortSignal.timeout(5000) // 5 second timeout
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
is_user_types_enabled,
|
|
17
17
|
get_default_user_type,
|
|
18
18
|
} from "../user_types_config.server.js";
|
|
19
|
+
import { write_legal_acceptance } from "../legal/legal_docs_service.js";
|
|
19
20
|
|
|
20
21
|
// section: types
|
|
21
22
|
export type RegistrationData = {
|
|
@@ -23,6 +24,9 @@ export type RegistrationData = {
|
|
|
23
24
|
password: string;
|
|
24
25
|
name?: string;
|
|
25
26
|
url_on_logon?: string;
|
|
27
|
+
legal_accepted?: Record<string, { hash: string }>;
|
|
28
|
+
ip?: string | null;
|
|
29
|
+
user_agent?: string | null;
|
|
26
30
|
};
|
|
27
31
|
|
|
28
32
|
export type RegistrationResult = {
|
|
@@ -156,7 +160,7 @@ export async function register_user(
|
|
|
156
160
|
user_email: email,
|
|
157
161
|
user_name: name,
|
|
158
162
|
});
|
|
159
|
-
|
|
163
|
+
|
|
160
164
|
if (!email_result.success) {
|
|
161
165
|
const logger = create_app_logger();
|
|
162
166
|
logger.error("registration_service_email_send_failed", {
|
|
@@ -170,6 +174,17 @@ export async function register_user(
|
|
|
170
174
|
}
|
|
171
175
|
}
|
|
172
176
|
|
|
177
|
+
// Write legal acceptance records if provided
|
|
178
|
+
if (data.legal_accepted && Object.keys(data.legal_accepted).length > 0) {
|
|
179
|
+
await write_legal_acceptance(
|
|
180
|
+
adapter,
|
|
181
|
+
user_id,
|
|
182
|
+
data.legal_accepted,
|
|
183
|
+
data.ip ?? null,
|
|
184
|
+
data.user_agent ?? null,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
173
188
|
return {
|
|
174
189
|
success: true,
|
|
175
190
|
user_id,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// section: imports
|
|
3
3
|
import type { HazoConnectAdapter } from "hazo_connect";
|
|
4
4
|
import { createCrudService } from "hazo_connect/server";
|
|
5
|
+
import { generateRequestId } from "hazo_core";
|
|
5
6
|
import argon2 from "argon2";
|
|
6
7
|
import { create_app_logger } from "../app_logger.js";
|
|
7
8
|
import { get_relationships_config, get_allowed_relationship_types } from "../relationships_config.server.js";
|
|
@@ -66,7 +67,7 @@ export function get_display_email(email: string | null | undefined): string | nu
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
function generate_sentinel_email(): string {
|
|
69
|
-
return `${SENTINEL_PREFIX}${
|
|
70
|
+
return `${SENTINEL_PREFIX}${generateRequestId().slice(4)}${SENTINEL_DOMAIN}`;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
// section: helpers
|
|
@@ -129,8 +130,8 @@ export async function create_managed_child(
|
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
// Generate IDs
|
|
132
|
-
const child_user_id =
|
|
133
|
-
const relationship_id =
|
|
133
|
+
const child_user_id = generateRequestId().slice(4);
|
|
134
|
+
const relationship_id = generateRequestId().slice(4);
|
|
134
135
|
const now = new Date().toISOString();
|
|
135
136
|
|
|
136
137
|
// Insert managed child user
|
|
@@ -375,7 +376,7 @@ export async function create_self_relationship(
|
|
|
375
376
|
}
|
|
376
377
|
|
|
377
378
|
const config = get_relationships_config();
|
|
378
|
-
const relationship_id =
|
|
379
|
+
const relationship_id = generateRequestId().slice(4);
|
|
379
380
|
const now = new Date().toISOString();
|
|
380
381
|
|
|
381
382
|
await relationships_service.insert({
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Uses jose library for Edge-compatible JWT operations
|
|
3
3
|
// section: imports
|
|
4
4
|
import { SignJWT, jwtVerify } from "jose";
|
|
5
|
+
import { HazoConfigError, HazoAuthError } from "hazo_core";
|
|
5
6
|
import { create_app_logger } from "../app_logger.js";
|
|
6
7
|
import { get_filename, get_line_number } from "../utils/api_route_helpers.js";
|
|
7
8
|
|
|
@@ -37,7 +38,7 @@ function get_jwt_secret(): Uint8Array {
|
|
|
37
38
|
line_number: get_line_number(),
|
|
38
39
|
error: "JWT_SECRET environment variable is required",
|
|
39
40
|
});
|
|
40
|
-
throw new
|
|
41
|
+
throw new HazoConfigError({ code: 'HAZO_AUTH_CONFIG', pkg: 'hazo_auth', message: 'JWT_SECRET environment variable is required' });
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
// Convert string secret to Uint8Array for jose
|
|
@@ -112,7 +113,7 @@ export async function create_session_token(
|
|
|
112
113
|
error_stack,
|
|
113
114
|
});
|
|
114
115
|
|
|
115
|
-
throw new
|
|
116
|
+
throw new HazoAuthError({ code: 'HAZO_AUTH_INVALID_TOKEN', pkg: 'hazo_auth', message: 'Failed to create session token' });
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
119
|
|
|
@@ -1,60 +1,5 @@
|
|
|
1
1
|
// file_description: shared helper functions for API routes to get filename and line number
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* @returns Filename or "route.ts" as default
|
|
7
|
-
*/
|
|
8
|
-
export function get_filename(): string {
|
|
9
|
-
try {
|
|
10
|
-
const stack = new Error().stack;
|
|
11
|
-
if (!stack) {
|
|
12
|
-
return "route.ts";
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Parse stack trace to find the caller's file
|
|
16
|
-
const lines = stack.split("\n");
|
|
17
|
-
// Skip Error line and get_filename line, get the actual caller
|
|
18
|
-
for (let i = 2; i < lines.length; i++) {
|
|
19
|
-
const line = lines[i];
|
|
20
|
-
// Match file paths in stack trace (e.g., "at /path/to/file.ts:123:45")
|
|
21
|
-
const match = line.match(/([^/\\]+\.tsx?):(\d+):(\d+)/);
|
|
22
|
-
if (match) {
|
|
23
|
-
return match[1];
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return "route.ts";
|
|
27
|
-
} catch {
|
|
28
|
-
return "route.ts";
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Gets the line number from the call stack
|
|
34
|
-
* This is a simplified version that extracts the line number from the error stack
|
|
35
|
-
* @returns Line number or 0
|
|
36
|
-
*/
|
|
37
|
-
export function get_line_number(): number {
|
|
38
|
-
try {
|
|
39
|
-
const stack = new Error().stack;
|
|
40
|
-
if (!stack) {
|
|
41
|
-
return 0;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Parse stack trace to find the caller's line number
|
|
45
|
-
const lines = stack.split("\n");
|
|
46
|
-
// Skip Error line and get_line_number line, get the actual caller
|
|
47
|
-
for (let i = 2; i < lines.length; i++) {
|
|
48
|
-
const line = lines[i];
|
|
49
|
-
// Match line numbers in stack trace (e.g., "at /path/to/file.ts:123:45")
|
|
50
|
-
const match = line.match(/([^/\\]+\.tsx?):(\d+):(\d+)/);
|
|
51
|
-
if (match) {
|
|
52
|
-
return parseInt(match[2], 10) || 0;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return 0;
|
|
56
|
-
} catch {
|
|
57
|
-
return 0;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
2
|
+
// Canonical location moved to hazo_logs/src/lib/utils/caller_info.ts.
|
|
3
|
+
// This re-export maintains backward compatibility for hazo_auth consumers.
|
|
4
|
+
// Will be removed in hazo_auth v9 — import from 'hazo_logs' directly.
|
|
5
|
+
export { get_filename, get_line_number } from 'hazo_logs';
|
|
@@ -1,61 +1,5 @@
|
|
|
1
|
-
// file_description:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Gets the public-facing origin URL for redirect construction.
|
|
8
|
-
*
|
|
9
|
-
* Behind reverse proxies (Cloudflare, nginx, etc.), `request.url` contains the
|
|
10
|
-
* internal address (e.g. `http://localhost:3000`), not the public domain.
|
|
11
|
-
* This function returns the correct origin from environment variables.
|
|
12
|
-
*
|
|
13
|
-
* Priority: NEXTAUTH_URL > APP_DOMAIN_NAME > NEXT_PUBLIC_APP_URL > APP_URL > request.url
|
|
14
|
-
*
|
|
15
|
-
* @param request_url - The request.url to use as fallback
|
|
16
|
-
* @returns The origin URL (e.g. "https://gotimer.org")
|
|
17
|
-
*/
|
|
18
|
-
export function get_origin_url(request_url: string): string {
|
|
19
|
-
// NEXTAUTH_URL is the standard for NextAuth.js apps
|
|
20
|
-
const nextauth_url = process.env.NEXTAUTH_URL;
|
|
21
|
-
if (nextauth_url) {
|
|
22
|
-
return nextauth_url.replace(/\/$/, "");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// APP_DOMAIN_NAME (with protocol handling)
|
|
26
|
-
const app_domain = process.env.APP_DOMAIN_NAME;
|
|
27
|
-
if (app_domain) {
|
|
28
|
-
const domain = app_domain.trim();
|
|
29
|
-
if (domain.startsWith("http://") || domain.startsWith("https://")) {
|
|
30
|
-
return domain.replace(/\/$/, "");
|
|
31
|
-
}
|
|
32
|
-
return `https://${domain}`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Other common env vars
|
|
36
|
-
const env_url = process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL;
|
|
37
|
-
if (env_url) {
|
|
38
|
-
return env_url.replace(/\/$/, "");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Fallback to request.url (works in development without a proxy)
|
|
42
|
-
try {
|
|
43
|
-
const url = new URL(request_url);
|
|
44
|
-
return url.origin;
|
|
45
|
-
} catch {
|
|
46
|
-
return request_url;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Creates a URL using the public-facing origin instead of request.url.
|
|
52
|
-
* Drop-in replacement for `new URL(path, request.url)` in route handlers.
|
|
53
|
-
*
|
|
54
|
-
* @param path - The path or relative URL (e.g. "/hazo_auth/login")
|
|
55
|
-
* @param request_url - The request.url (used as fallback only)
|
|
56
|
-
* @returns A URL object with the correct public origin
|
|
57
|
-
*/
|
|
58
|
-
export function create_redirect_url(path: string, request_url: string): URL {
|
|
59
|
-
const origin = get_origin_url(request_url);
|
|
60
|
-
return new URL(path, origin);
|
|
61
|
-
}
|
|
1
|
+
// file_description: Re-exports get_origin_url and create_redirect_url from hazo_core.
|
|
2
|
+
// Canonical location moved to hazo_core/http in hazo_core v1.
|
|
3
|
+
// This re-export maintains backward compatibility for hazo_auth consumers.
|
|
4
|
+
// Will be removed in hazo_auth v9 — import from 'hazo_core' directly.
|
|
5
|
+
export { get_origin_url, create_redirect_url } from 'hazo_core';
|
package/cli-src/lib/utils.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
// file_description:
|
|
2
|
-
|
|
3
|
-
import { twMerge } from "tailwind-merge";
|
|
1
|
+
// file_description: re-exports cn and merge_class_names from hazo_ui (canonical source)
|
|
2
|
+
export { cn } from "hazo_ui";
|
|
4
3
|
|
|
5
|
-
//
|
|
6
|
-
export
|
|
7
|
-
return twMerge(clsx(inputs));
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// section: shadcn_compatibility_helper
|
|
11
|
-
export const cn = (...inputs: ClassValue[]) => merge_class_names(...inputs);
|
|
4
|
+
// merge_class_names alias — kept for backward compatibility
|
|
5
|
+
export { cn as merge_class_names } from "hazo_ui";
|
|
@@ -729,3 +729,9 @@ company_name = My Company
|
|
|
729
729
|
# [hazo_auth__login_layout]
|
|
730
730
|
# image_src = /your-custom-image.jpg
|
|
731
731
|
|
|
732
|
+
; ── Log level overrides (per hazo_logs D-015) ────────────────────────────────
|
|
733
|
+
; Uncomment to tune log verbosity for specific namespaces.
|
|
734
|
+
; Format: namespace = TRACE | DEBUG | INFO | WARN | ERROR
|
|
735
|
+
[log.overrides]
|
|
736
|
+
; hazo_auth = DEBUG
|
|
737
|
+
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from "./components/index.js";
|
|
2
|
+
export { LegalAcceptanceGate, LegalDocDrawer, LegalDocCheckboxList, LegalDocCombinedView } from "./components/layouts/legal/index.js";
|
|
2
3
|
export { cn, merge_class_names } from "./lib/utils.js";
|
|
3
4
|
export * from "./lib/auth/auth_types.js";
|
|
4
5
|
export { use_auth_status, trigger_auth_status_refresh } from "./components/layouts/shared/hooks/use_auth_status.js";
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAYA,cAAc,oBAAoB,CAAC;AAInC,OAAO,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAIpD,cAAc,uBAAuB,CAAC;AAItC,OAAO,EAAE,eAAe,EAAE,2BAA2B,EAAE,MAAM,mDAAmD,CAAC;AACjH,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,iDAAiD,CAAC;AAC3G,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,iDAAiD,CAAC;AAC7G,OAAO,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,qDAAqD,CAAC;AACnH,YAAY,EAAE,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,qDAAqD,CAAC;AAGvI,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAI/E,cAAc,8CAA8C,CAAC"}
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAYA,cAAc,oBAAoB,CAAC;AAInC,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AAInI,OAAO,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAIpD,cAAc,uBAAuB,CAAC;AAItC,OAAO,EAAE,eAAe,EAAE,2BAA2B,EAAE,MAAM,mDAAmD,CAAC;AACjH,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,iDAAiD,CAAC;AAC3G,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,iDAAiD,CAAC;AAC7G,OAAO,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,qDAAqD,CAAC;AACnH,YAAY,EAAE,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,qDAAqD,CAAC;AAGvI,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAI/E,cAAc,8CAA8C,CAAC"}
|
package/dist/client.js
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
// section: component_exports
|
|
11
11
|
// All UI and layout components are client-safe
|
|
12
12
|
export * from "./components/index.js";
|
|
13
|
+
// section: legal_exports
|
|
14
|
+
// Legal acceptance gate and primitives
|
|
15
|
+
export { LegalAcceptanceGate, LegalDocDrawer, LegalDocCheckboxList, LegalDocCombinedView } from "./components/layouts/legal/index.js";
|
|
13
16
|
// section: utility_exports
|
|
14
17
|
// CSS utility functions
|
|
15
18
|
export { cn, merge_class_names } from "./lib/utils.js";
|
|
@@ -14,5 +14,6 @@ export { UserManagementLayout } from "./user_management/index.js";
|
|
|
14
14
|
export type { UserManagementLayoutProps } from "./user_management/index";
|
|
15
15
|
export { default as DevLockLayout } from "./dev_lock/index.js";
|
|
16
16
|
export type { DevLockLayoutProps } from "./dev_lock/index";
|
|
17
|
+
export { LegalDocDrawer, LegalDocCheckboxList, LegalDocCombinedView, LegalAcceptanceGate } from "./legal/index.js";
|
|
17
18
|
export * from "./shared/index.js";
|
|
18
19
|
//# sourceMappingURL=index.d.ts.map
|