hazo_auth 5.3.1 → 6.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +167 -17
- package/SETUP_CHECKLIST.md +99 -7
- package/cli-src/cli/generate.ts +10 -1
- package/cli-src/cli/validate.ts +4 -0
- package/cli-src/lib/auth/auth_types.ts +21 -12
- package/cli-src/lib/auth/hazo_get_tenant_auth.server.ts +25 -24
- package/cli-src/lib/auth/index.ts +2 -2
- package/cli-src/lib/auth/with_auth.server.ts +15 -15
- package/cli-src/lib/cookies_config.server.ts +1 -0
- package/cli-src/lib/login_config.server.ts +14 -0
- package/cli-src/lib/otp_config.server.ts +91 -0
- package/cli-src/lib/services/email_service.ts +3 -1
- package/cli-src/lib/services/email_template_manifest.ts +17 -0
- package/cli-src/lib/services/email_templates/otp_signin_code.html +13 -0
- package/cli-src/lib/services/email_templates/otp_signin_code.txt +5 -0
- package/cli-src/lib/services/index.ts +8 -2
- package/cli-src/lib/services/otp_service.ts +295 -0
- package/cli-src/lib/services/session_token_service.ts +4 -1
- package/config/hazo_auth_config.example.ini +38 -0
- package/dist/cli/generate.d.ts.map +1 -1
- package/dist/cli/generate.js +10 -1
- package/dist/cli/validate.d.ts.map +1 -1
- package/dist/cli/validate.js +4 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -0
- package/dist/components/layouts/login/index.d.ts +7 -1
- package/dist/components/layouts/login/index.d.ts.map +1 -1
- package/dist/components/layouts/login/index.js +2 -2
- package/dist/components/layouts/otp/index.d.ts +10 -0
- package/dist/components/layouts/otp/index.d.ts.map +1 -0
- package/dist/components/layouts/otp/index.js +14 -0
- package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/sidebar_layout_wrapper.js +8 -3
- package/dist/components/otp/OTPRequestForm.d.ts +11 -0
- package/dist/components/otp/OTPRequestForm.d.ts.map +1 -0
- package/dist/components/otp/OTPRequestForm.js +42 -0
- package/dist/components/otp/OTPVerifyForm.d.ts +16 -0
- package/dist/components/otp/OTPVerifyForm.d.ts.map +1 -0
- package/dist/components/otp/OTPVerifyForm.js +75 -0
- package/dist/components/otp/index.d.ts +5 -0
- package/dist/components/otp/index.d.ts.map +1 -0
- package/dist/components/otp/index.js +2 -0
- package/dist/components/ui/input-otp.d.ts +35 -0
- package/dist/components/ui/input-otp.d.ts.map +1 -0
- package/dist/components/ui/input-otp.js +44 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/auth/auth_types.d.ts +13 -12
- package/dist/lib/auth/auth_types.d.ts.map +1 -1
- package/dist/lib/auth/auth_types.js +8 -0
- package/dist/lib/auth/hazo_get_tenant_auth.server.d.ts +8 -7
- package/dist/lib/auth/hazo_get_tenant_auth.server.d.ts.map +1 -1
- package/dist/lib/auth/hazo_get_tenant_auth.server.js +23 -22
- package/dist/lib/auth/index.d.ts +2 -2
- package/dist/lib/auth/index.d.ts.map +1 -1
- package/dist/lib/auth/with_auth.server.d.ts +13 -13
- package/dist/lib/auth/with_auth.server.d.ts.map +1 -1
- package/dist/lib/auth/with_auth.server.js +2 -2
- package/dist/lib/cookies_config.server.d.ts +1 -0
- package/dist/lib/cookies_config.server.d.ts.map +1 -1
- package/dist/lib/cookies_config.server.js +1 -0
- package/dist/lib/login_config.server.d.ts +6 -0
- package/dist/lib/login_config.server.d.ts.map +1 -1
- package/dist/lib/login_config.server.js +7 -0
- package/dist/lib/otp_config.server.d.ts +49 -0
- package/dist/lib/otp_config.server.d.ts.map +1 -0
- package/dist/lib/otp_config.server.js +48 -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 +2 -0
- package/dist/lib/services/email_template_manifest.d.ts.map +1 -1
- package/dist/lib/services/email_template_manifest.js +17 -0
- package/dist/lib/services/email_templates/otp_signin_code.html +13 -0
- package/dist/lib/services/email_templates/otp_signin_code.txt +5 -0
- package/dist/lib/services/index.d.ts +2 -0
- package/dist/lib/services/index.d.ts.map +1 -1
- package/dist/lib/services/index.js +1 -0
- package/dist/lib/services/otp_service.d.ts +46 -0
- package/dist/lib/services/otp_service.d.ts.map +1 -0
- package/dist/lib/services/otp_service.js +238 -0
- package/dist/lib/services/session_token_service.d.ts +3 -1
- package/dist/lib/services/session_token_service.d.ts.map +1 -1
- package/dist/lib/services/session_token_service.js +4 -2
- package/dist/page_components/otp.d.ts +4 -0
- package/dist/page_components/otp.d.ts.map +1 -0
- package/dist/page_components/otp.js +5 -0
- package/dist/server/routes/index.d.ts +2 -0
- package/dist/server/routes/index.d.ts.map +1 -1
- package/dist/server/routes/index.js +3 -0
- package/dist/server/routes/me.d.ts.map +1 -1
- package/dist/server/routes/me.js +43 -1
- package/dist/server/routes/otp/request.d.ts +3 -0
- package/dist/server/routes/otp/request.d.ts.map +1 -0
- package/dist/server/routes/otp/request.js +33 -0
- package/dist/server/routes/otp/verify.d.ts +3 -0
- package/dist/server/routes/otp/verify.d.ts.map +1 -0
- package/dist/server/routes/otp/verify.js +58 -0
- package/dist/server-lib.d.ts +3 -0
- package/dist/server-lib.d.ts.map +1 -1
- package/dist/server-lib.js +2 -0
- package/dist/server_pages/forgot_password.d.ts +1 -1
- package/dist/server_pages/forgot_password.d.ts.map +1 -1
- package/dist/server_pages/forgot_password.js +2 -1
- package/dist/server_pages/login.d.ts +1 -1
- package/dist/server_pages/login.d.ts.map +1 -1
- package/dist/server_pages/login.js +3 -2
- package/dist/server_pages/login_client_wrapper.d.ts +1 -1
- package/dist/server_pages/login_client_wrapper.d.ts.map +1 -1
- package/dist/server_pages/login_client_wrapper.js +2 -2
- package/dist/server_pages/my_settings.d.ts +1 -1
- package/dist/server_pages/my_settings.d.ts.map +1 -1
- package/dist/server_pages/my_settings.js +2 -1
- package/dist/server_pages/otp.d.ts +42 -0
- package/dist/server_pages/otp.d.ts.map +1 -0
- package/dist/server_pages/otp.js +38 -0
- package/dist/server_pages/register.d.ts +1 -1
- package/dist/server_pages/register.d.ts.map +1 -1
- package/dist/server_pages/register.js +2 -1
- package/dist/server_pages/reset_password.d.ts +1 -1
- package/dist/server_pages/reset_password.d.ts.map +1 -1
- package/dist/server_pages/reset_password.js +2 -1
- package/dist/server_pages/verify_email.d.ts +1 -1
- package/dist/server_pages/verify_email.d.ts.map +1 -1
- package/dist/server_pages/verify_email.js +2 -1
- package/package.json +20 -3
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
/**
|
|
3
|
+
* Generates a cryptographically random 6-digit numeric OTP code (000000–999999).
|
|
4
|
+
* Uses crypto.randomInt for uniform distribution.
|
|
5
|
+
*/
|
|
6
|
+
export declare function generate_otp_code(): string;
|
|
7
|
+
export declare function hash_otp_code(code: string): Promise<string>;
|
|
8
|
+
export declare function verify_otp_code(otp_hash: string, code: string): Promise<boolean>;
|
|
9
|
+
export type RequestEmailOTPResult = {
|
|
10
|
+
ok: true;
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
error: "rate_limited";
|
|
14
|
+
retry_after_seconds: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Initiates an OTP sign-in flow for the given email address.
|
|
18
|
+
*
|
|
19
|
+
* Behaviour:
|
|
20
|
+
* 1. Per-email rate limit — rejects if too many requests in the sliding window.
|
|
21
|
+
* 2. Per-IP rate limit — rejects if too many requests from this IP.
|
|
22
|
+
* 3. Unknown email + auto_register=false — silent no-op (constant-time padding).
|
|
23
|
+
* 4. Unknown email + auto_register=true — inserts OTP row with user_id=null, dispatches email.
|
|
24
|
+
* 5. Known email — marks prior unconsumed rows consumed, inserts fresh OTP row, dispatches email.
|
|
25
|
+
*
|
|
26
|
+
* Never reveals whether an email address is registered (always returns ok:true on success).
|
|
27
|
+
*/
|
|
28
|
+
export declare function request_email_otp(args: {
|
|
29
|
+
email: string;
|
|
30
|
+
ip: string;
|
|
31
|
+
}): Promise<RequestEmailOTPResult>;
|
|
32
|
+
export type VerifyEmailOTPResult = {
|
|
33
|
+
ok: true;
|
|
34
|
+
user_id: string;
|
|
35
|
+
email: string;
|
|
36
|
+
session_token: string;
|
|
37
|
+
} | {
|
|
38
|
+
ok: false;
|
|
39
|
+
error: "invalid_or_expired";
|
|
40
|
+
};
|
|
41
|
+
export declare function verify_email_otp(args: {
|
|
42
|
+
email: string;
|
|
43
|
+
code: string;
|
|
44
|
+
ip: string;
|
|
45
|
+
}): Promise<VerifyEmailOTPResult>;
|
|
46
|
+
//# sourceMappingURL=otp_service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otp_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/otp_service.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAUrB;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAG1C;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEjE;AAED,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAMtF;AAID,MAAM,MAAM,qBAAqB,GAC7B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,cAAc,CAAC;IAAC,mBAAmB,EAAE,MAAM,CAAA;CAAE,CAAC;AAItE;;;;;;;;;;;GAWG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;CACZ,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA8GjC;AAID,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GACnE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,oBAAoB,CAAA;CAAE,CAAC;AAE/C,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAsHhC"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import argon2 from "argon2";
|
|
4
|
+
import { createCrudService } from "hazo_connect/server";
|
|
5
|
+
import { get_otp_config, hazo_auth_otp_session_ttl_seconds } from "../otp_config.server.js";
|
|
6
|
+
import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
|
|
7
|
+
import { send_template_email } from "./email_service.js";
|
|
8
|
+
import { create_app_logger } from "../app_logger.js";
|
|
9
|
+
import { create_session_token } from "./session_token_service.js";
|
|
10
|
+
/**
|
|
11
|
+
* Generates a cryptographically random 6-digit numeric OTP code (000000–999999).
|
|
12
|
+
* Uses crypto.randomInt for uniform distribution.
|
|
13
|
+
*/
|
|
14
|
+
export function generate_otp_code() {
|
|
15
|
+
const n = crypto.randomInt(0, 1000000);
|
|
16
|
+
return String(n).padStart(6, "0");
|
|
17
|
+
}
|
|
18
|
+
export async function hash_otp_code(code) {
|
|
19
|
+
return argon2.hash(code);
|
|
20
|
+
}
|
|
21
|
+
export async function verify_otp_code(otp_hash, code) {
|
|
22
|
+
try {
|
|
23
|
+
return await argon2.verify(otp_hash, code);
|
|
24
|
+
}
|
|
25
|
+
catch (_a) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// section: request_email_otp
|
|
30
|
+
/**
|
|
31
|
+
* Initiates an OTP sign-in flow for the given email address.
|
|
32
|
+
*
|
|
33
|
+
* Behaviour:
|
|
34
|
+
* 1. Per-email rate limit — rejects if too many requests in the sliding window.
|
|
35
|
+
* 2. Per-IP rate limit — rejects if too many requests from this IP.
|
|
36
|
+
* 3. Unknown email + auto_register=false — silent no-op (constant-time padding).
|
|
37
|
+
* 4. Unknown email + auto_register=true — inserts OTP row with user_id=null, dispatches email.
|
|
38
|
+
* 5. Known email — marks prior unconsumed rows consumed, inserts fresh OTP row, dispatches email.
|
|
39
|
+
*
|
|
40
|
+
* Never reveals whether an email address is registered (always returns ok:true on success).
|
|
41
|
+
*/
|
|
42
|
+
export async function request_email_otp(args) {
|
|
43
|
+
const logger = create_app_logger();
|
|
44
|
+
const cfg = get_otp_config();
|
|
45
|
+
const email = args.email.trim().toLowerCase();
|
|
46
|
+
const ip = args.ip;
|
|
47
|
+
const adapter = get_hazo_connect_instance();
|
|
48
|
+
const otp_table = createCrudService(adapter, "hazo_email_otps");
|
|
49
|
+
const users_table = createCrudService(adapter, "hazo_users");
|
|
50
|
+
// 1. Per-email rate limit
|
|
51
|
+
const email_window_ms = cfg.email_rate_limit_window_seconds * 1000;
|
|
52
|
+
const email_threshold = new Date(Date.now() - email_window_ms).toISOString();
|
|
53
|
+
const recent_for_email = await otp_table.list((qb) => qb
|
|
54
|
+
.select(["created_at"])
|
|
55
|
+
.where("email", "eq", email)
|
|
56
|
+
.where("created_at", "gte", email_threshold));
|
|
57
|
+
if (recent_for_email.length >= cfg.email_rate_limit_max) {
|
|
58
|
+
const oldest = recent_for_email
|
|
59
|
+
.map((r) => Date.parse(String(r.created_at)))
|
|
60
|
+
.sort((a, b) => a - b)[0];
|
|
61
|
+
const retry_after_seconds = Math.max(1, Math.ceil((oldest + email_window_ms - Date.now()) / 1000));
|
|
62
|
+
logger.warn("otp_request_email_rate_limited", { email, ip, retry_after_seconds });
|
|
63
|
+
return { ok: false, error: "rate_limited", retry_after_seconds };
|
|
64
|
+
}
|
|
65
|
+
// 2. Per-IP rate limit
|
|
66
|
+
const ip_window_ms = cfg.ip_rate_limit_window_seconds * 1000;
|
|
67
|
+
const ip_threshold = new Date(Date.now() - ip_window_ms).toISOString();
|
|
68
|
+
const recent_for_ip = await otp_table.list((qb) => qb
|
|
69
|
+
.select(["created_at"])
|
|
70
|
+
.where("requester_ip", "eq", ip)
|
|
71
|
+
.where("created_at", "gte", ip_threshold));
|
|
72
|
+
if (recent_for_ip.length >= cfg.ip_rate_limit_max) {
|
|
73
|
+
const oldest = recent_for_ip
|
|
74
|
+
.map((r) => Date.parse(String(r.created_at)))
|
|
75
|
+
.sort((a, b) => a - b)[0];
|
|
76
|
+
const retry_after_seconds = Math.max(1, Math.ceil((oldest + ip_window_ms - Date.now()) / 1000));
|
|
77
|
+
logger.warn("otp_request_ip_rate_limited", { email, ip, retry_after_seconds });
|
|
78
|
+
return { ok: false, error: "rate_limited", retry_after_seconds };
|
|
79
|
+
}
|
|
80
|
+
// 3. Lookup user
|
|
81
|
+
const existing_users = await users_table.findBy({ email_address: email });
|
|
82
|
+
const existing_user = existing_users.length > 0 ? existing_users[0] : null;
|
|
83
|
+
// 4. Unknown email + auto_register=false → silent no-op with constant-time padding
|
|
84
|
+
if (!existing_user && !cfg.auto_register) {
|
|
85
|
+
await argon2.hash("000000"); // constant-time padding — never stored
|
|
86
|
+
return { ok: true };
|
|
87
|
+
}
|
|
88
|
+
// 5. Mark any unconsumed rows for this email as superseded
|
|
89
|
+
try {
|
|
90
|
+
const unconsumed = await otp_table.list((qb) => qb
|
|
91
|
+
.select(["id"])
|
|
92
|
+
.where("email", "eq", email)
|
|
93
|
+
.where("consumed_at", "is", null));
|
|
94
|
+
for (const row of unconsumed) {
|
|
95
|
+
await otp_table.updateById(String(row.id), { consumed_at: new Date().toISOString() });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (_a) {
|
|
99
|
+
// IS NULL filter may not be supported in all adapter versions — not critical
|
|
100
|
+
}
|
|
101
|
+
// 6. Generate code + hash + insert row
|
|
102
|
+
const code = generate_otp_code();
|
|
103
|
+
const otp_hash = await hash_otp_code(code);
|
|
104
|
+
const expires_at = new Date(Date.now() + cfg.code_ttl_seconds * 1000).toISOString();
|
|
105
|
+
const row_id = crypto.randomUUID();
|
|
106
|
+
await otp_table.insert({
|
|
107
|
+
id: row_id,
|
|
108
|
+
user_id: existing_user ? String(existing_user.id) : null,
|
|
109
|
+
email,
|
|
110
|
+
otp_hash,
|
|
111
|
+
expires_at,
|
|
112
|
+
attempt_count: 0,
|
|
113
|
+
requester_ip: ip,
|
|
114
|
+
});
|
|
115
|
+
// 7. Dispatch email — fire-and-forget; errors are logged but do not surface to caller
|
|
116
|
+
try {
|
|
117
|
+
await send_template_email("otp_signin_code", email, {
|
|
118
|
+
otp_code: code,
|
|
119
|
+
expires_in_minutes: String(Math.round(cfg.code_ttl_seconds / 60)),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
logger.error("otp_request_email_dispatch_failed", {
|
|
124
|
+
email,
|
|
125
|
+
ip,
|
|
126
|
+
error: err instanceof Error ? err.message : String(err),
|
|
127
|
+
});
|
|
128
|
+
// Return ok:true to preserve no-enumeration property — caller cannot distinguish
|
|
129
|
+
// a missing user from a delivery failure.
|
|
130
|
+
}
|
|
131
|
+
return { ok: true };
|
|
132
|
+
}
|
|
133
|
+
export async function verify_email_otp(args) {
|
|
134
|
+
const logger = create_app_logger();
|
|
135
|
+
const cfg = get_otp_config();
|
|
136
|
+
const email = args.email.trim().toLowerCase();
|
|
137
|
+
const code = args.code.trim();
|
|
138
|
+
const adapter = get_hazo_connect_instance();
|
|
139
|
+
const otp_table = createCrudService(adapter, "hazo_email_otps");
|
|
140
|
+
const users_table = createCrudService(adapter, "hazo_users");
|
|
141
|
+
const user_scopes_table = createCrudService(adapter, "hazo_user_scopes");
|
|
142
|
+
const roles_table = createCrudService(adapter, "hazo_roles");
|
|
143
|
+
// 1. Find most-recent unconsumed row for this email
|
|
144
|
+
const now_iso = new Date().toISOString();
|
|
145
|
+
const candidates = await otp_table.list((qb) => qb
|
|
146
|
+
.select(["id", "user_id", "otp_hash", "expires_at", "attempt_count"])
|
|
147
|
+
.where("email", "eq", email)
|
|
148
|
+
.where("consumed_at", "is", null)
|
|
149
|
+
.order("created_at", "desc")
|
|
150
|
+
.limit(1));
|
|
151
|
+
const row = candidates.length > 0 ? candidates[0] : null;
|
|
152
|
+
if (!row) {
|
|
153
|
+
return { ok: false, error: "invalid_or_expired" };
|
|
154
|
+
}
|
|
155
|
+
// 2. Check expiry
|
|
156
|
+
const expires_at_ms = Date.parse(String(row.expires_at));
|
|
157
|
+
if (Number.isNaN(expires_at_ms) || expires_at_ms < Date.now()) {
|
|
158
|
+
return { ok: false, error: "invalid_or_expired" };
|
|
159
|
+
}
|
|
160
|
+
// 3. argon2 verify
|
|
161
|
+
const is_valid = await verify_otp_code(String(row.otp_hash), code);
|
|
162
|
+
if (!is_valid) {
|
|
163
|
+
const new_attempt_count = Number(row.attempt_count) + 1;
|
|
164
|
+
const updates = { attempt_count: new_attempt_count };
|
|
165
|
+
if (new_attempt_count >= cfg.max_verify_attempts) {
|
|
166
|
+
updates.consumed_at = now_iso; // poison
|
|
167
|
+
}
|
|
168
|
+
await otp_table.updateById(String(row.id), updates);
|
|
169
|
+
logger.info("otp_verify_invalid_code", {
|
|
170
|
+
email,
|
|
171
|
+
attempt_count: new_attempt_count,
|
|
172
|
+
poisoned: new_attempt_count >= cfg.max_verify_attempts,
|
|
173
|
+
});
|
|
174
|
+
return { ok: false, error: "invalid_or_expired" };
|
|
175
|
+
}
|
|
176
|
+
// 4. Mark consumed
|
|
177
|
+
await otp_table.updateById(String(row.id), { consumed_at: now_iso });
|
|
178
|
+
// 5. Resolve / create user
|
|
179
|
+
let user_id = row.user_id ? String(row.user_id) : null;
|
|
180
|
+
if (user_id) {
|
|
181
|
+
// Ensure email_verified=true
|
|
182
|
+
const user = await users_table.findById(user_id);
|
|
183
|
+
if (user && !user.email_verified) {
|
|
184
|
+
await users_table.updateById(user_id, { email_verified: true });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else if (cfg.auto_register) {
|
|
188
|
+
// Create user + bind scope/role
|
|
189
|
+
const new_user_id = crypto.randomUUID();
|
|
190
|
+
await users_table.insert({
|
|
191
|
+
id: new_user_id,
|
|
192
|
+
email_address: email,
|
|
193
|
+
email_verified: true,
|
|
194
|
+
password_hash: null,
|
|
195
|
+
name: null,
|
|
196
|
+
});
|
|
197
|
+
// Resolve role_id from name — try scope-specific first, then any
|
|
198
|
+
const scoped_roles = await roles_table.findBy({
|
|
199
|
+
name: cfg.auto_assign_role_name,
|
|
200
|
+
scope_id: cfg.auto_assign_scope_id,
|
|
201
|
+
});
|
|
202
|
+
let role_id = null;
|
|
203
|
+
if (scoped_roles.length > 0) {
|
|
204
|
+
role_id = String(scoped_roles[0].id);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const any_roles = await roles_table.findBy({ name: cfg.auto_assign_role_name });
|
|
208
|
+
if (any_roles.length > 0) {
|
|
209
|
+
role_id = String(any_roles[0].id);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (!role_id) {
|
|
213
|
+
logger.error("otp_verify_auto_register_role_not_found", {
|
|
214
|
+
email,
|
|
215
|
+
scope_id: cfg.auto_assign_scope_id,
|
|
216
|
+
role_name: cfg.auto_assign_role_name,
|
|
217
|
+
});
|
|
218
|
+
return { ok: false, error: "invalid_or_expired" };
|
|
219
|
+
}
|
|
220
|
+
await user_scopes_table.insert({
|
|
221
|
+
user_id: new_user_id,
|
|
222
|
+
scope_id: cfg.auto_assign_scope_id,
|
|
223
|
+
root_scope_id: cfg.auto_assign_scope_id,
|
|
224
|
+
role_id,
|
|
225
|
+
});
|
|
226
|
+
user_id = new_user_id;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// Row exists but no user and auto_register=false — stale edge case
|
|
230
|
+
logger.warn("otp_verify_no_user_resolvable", { email });
|
|
231
|
+
return { ok: false, error: "invalid_or_expired" };
|
|
232
|
+
}
|
|
233
|
+
// 6. Issue session JWT with OTP TTL
|
|
234
|
+
const ttl_seconds = hazo_auth_otp_session_ttl_seconds();
|
|
235
|
+
const session_token = await create_session_token(user_id, email, undefined, ttl_seconds);
|
|
236
|
+
logger.info("otp_verify_ok", { email, user_id, ttl_seconds });
|
|
237
|
+
return { ok: true, user_id, email, session_token };
|
|
238
|
+
}
|
|
@@ -16,9 +16,11 @@ export type ValidateSessionTokenResult = {
|
|
|
16
16
|
* Token includes user_id, email, issued at time, and expiration
|
|
17
17
|
* @param user_id - User ID
|
|
18
18
|
* @param email - User email address
|
|
19
|
+
* @param managed_by_user_id - Optional: ID of the managing user (for impersonation)
|
|
20
|
+
* @param ttl_seconds - Optional: token lifetime in seconds (default: 30 days). Use 604800 for 7-day OTP sessions.
|
|
19
21
|
* @returns JWT token string
|
|
20
22
|
*/
|
|
21
|
-
export declare function create_session_token(user_id: string, email: string, managed_by_user_id?: string): Promise<string>;
|
|
23
|
+
export declare function create_session_token(user_id: string, email: string, managed_by_user_id?: string, ttl_seconds?: number): Promise<string>;
|
|
22
24
|
/**
|
|
23
25
|
* Validates a JWT session token
|
|
24
26
|
* Checks signature and expiration
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session_token_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/session_token_service.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAuCF
|
|
1
|
+
{"version":3,"file":"session_token_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/session_token_service.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAuCF;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,kBAAkB,CAAC,EAAE,MAAM,EAC3B,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CA4CjB;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,0BAA0B,CAAC,CAkDrC"}
|
|
@@ -41,14 +41,16 @@ function get_session_token_expiry_seconds() {
|
|
|
41
41
|
* Token includes user_id, email, issued at time, and expiration
|
|
42
42
|
* @param user_id - User ID
|
|
43
43
|
* @param email - User email address
|
|
44
|
+
* @param managed_by_user_id - Optional: ID of the managing user (for impersonation)
|
|
45
|
+
* @param ttl_seconds - Optional: token lifetime in seconds (default: 30 days). Use 604800 for 7-day OTP sessions.
|
|
44
46
|
* @returns JWT token string
|
|
45
47
|
*/
|
|
46
|
-
export async function create_session_token(user_id, email, managed_by_user_id) {
|
|
48
|
+
export async function create_session_token(user_id, email, managed_by_user_id, ttl_seconds) {
|
|
47
49
|
const logger = create_app_logger();
|
|
48
50
|
try {
|
|
49
51
|
const secret = get_jwt_secret();
|
|
50
52
|
const now = Math.floor(Date.now() / 1000); // Current time in seconds
|
|
51
|
-
const expiry_seconds = get_session_token_expiry_seconds();
|
|
53
|
+
const expiry_seconds = ttl_seconds !== null && ttl_seconds !== void 0 ? ttl_seconds : get_session_token_expiry_seconds();
|
|
52
54
|
const exp = now + expiry_seconds;
|
|
53
55
|
const payload = { user_id, email };
|
|
54
56
|
if (managed_by_user_id) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otp.d.ts","sourceRoot":"","sources":["../../src/page_components/otp.tsx"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAGrB,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AACvD,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -8,6 +8,8 @@ export { POST as changePasswordPOST } from "./change_password.js";
|
|
|
8
8
|
export { GET as validateResetTokenGET } from "./validate_reset_token.js";
|
|
9
9
|
export { GET as verifyEmailGET } from "./verify_email.js";
|
|
10
10
|
export { POST as resendVerificationPOST } from "./resend_verification.js";
|
|
11
|
+
export { otpRequestPOST } from "./otp/request.js";
|
|
12
|
+
export { otpVerifyPOST } from "./otp/verify.js";
|
|
11
13
|
export { PATCH as updateUserPATCH } from "./update_user.js";
|
|
12
14
|
export { POST as uploadProfilePicturePOST } from "./upload_profile_picture.js";
|
|
13
15
|
export { DELETE as removeProfilePictureDELETE } from "./remove_profile_picture.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/routes/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/routes/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -13,6 +13,9 @@ export { GET as validateResetTokenGET } from "./validate_reset_token.js";
|
|
|
13
13
|
// Email verification routes
|
|
14
14
|
export { GET as verifyEmailGET } from "./verify_email.js";
|
|
15
15
|
export { POST as resendVerificationPOST } from "./resend_verification.js";
|
|
16
|
+
// OTP routes (one-time password via email)
|
|
17
|
+
export { otpRequestPOST } from "./otp/request.js";
|
|
18
|
+
export { otpVerifyPOST } from "./otp/verify.js";
|
|
16
19
|
// User profile routes
|
|
17
20
|
export { PATCH as updateUserPATCH } from "./update_user.js";
|
|
18
21
|
export { POST as uploadProfilePicturePOST } from "./upload_profile_picture.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"me.d.ts","sourceRoot":"","sources":["../../../src/server/routes/me.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"me.d.ts","sourceRoot":"","sources":["../../../src/server/routes/me.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA+BxD;;;;;GAKG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW;;IAqJ7C"}
|
package/dist/server/routes/me.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// file_description: API route handler to get current authenticated user information with permissions
|
|
2
2
|
// section: imports
|
|
3
3
|
import { NextResponse } from "next/server";
|
|
4
|
+
import { jwtVerify } from "jose";
|
|
4
5
|
import { hazo_get_auth } from "../../lib/auth/hazo_get_auth.server.js";
|
|
5
6
|
import { get_hazo_connect_instance } from "../../lib/hazo_connect_instance.server.js";
|
|
6
7
|
import { createCrudService } from "hazo_connect/server";
|
|
@@ -8,6 +9,9 @@ import { map_db_source_to_ui } from "../../lib/services/profile_picture_source_m
|
|
|
8
9
|
import { create_app_logger } from "../../lib/app_logger.js";
|
|
9
10
|
import { get_filename, get_line_number } from "../../lib/utils/api_route_helpers.js";
|
|
10
11
|
import { is_user_types_enabled, get_user_type_by_key, } from "../../lib/user_types_config.server.js";
|
|
12
|
+
import { get_cookie_name, get_cookie_options, BASE_COOKIE_NAMES, } from "../../lib/cookies_config.server.js";
|
|
13
|
+
import { create_session_token } from "../../lib/services/session_token_service.js";
|
|
14
|
+
import { get_otp_config, hazo_auth_otp_session_ttl_seconds, } from "../../lib/otp_config.server.js";
|
|
11
15
|
// section: helpers
|
|
12
16
|
function strip_sentinel_email(email) {
|
|
13
17
|
if (!email)
|
|
@@ -24,6 +28,7 @@ function strip_sentinel_email(email) {
|
|
|
24
28
|
* Always returns the same format to prevent downstream variations.
|
|
25
29
|
*/
|
|
26
30
|
export async function GET(request) {
|
|
31
|
+
var _a, _b, _c, _d, _e, _f;
|
|
27
32
|
const logger = create_app_logger();
|
|
28
33
|
try {
|
|
29
34
|
// Use hazo_get_auth to get user with permissions
|
|
@@ -70,7 +75,7 @@ export async function GET(request) {
|
|
|
70
75
|
}
|
|
71
76
|
// Return unified format with all fields
|
|
72
77
|
const profile_pic = auth_result.user.profile_picture_url;
|
|
73
|
-
|
|
78
|
+
const response = NextResponse.json({
|
|
74
79
|
authenticated: true,
|
|
75
80
|
// Top-level fields for backward compatibility
|
|
76
81
|
user_id: auth_result.user.id,
|
|
@@ -100,6 +105,43 @@ export async function GET(request) {
|
|
|
100
105
|
permission_ok: auth_result.permission_ok,
|
|
101
106
|
missing_permissions: auth_result.missing_permissions,
|
|
102
107
|
}, { status: 200 });
|
|
108
|
+
// --- OTP sliding-session hook ---
|
|
109
|
+
const session_kind = (_a = request.cookies.get(get_cookie_name(BASE_COOKIE_NAMES.SESSION_KIND))) === null || _a === void 0 ? void 0 : _a.value;
|
|
110
|
+
if (session_kind === "otp") {
|
|
111
|
+
try {
|
|
112
|
+
const session_cookie = (_b = request.cookies.get(get_cookie_name(BASE_COOKIE_NAMES.SESSION))) === null || _b === void 0 ? void 0 : _b.value;
|
|
113
|
+
if (session_cookie) {
|
|
114
|
+
const secret = new TextEncoder().encode((_c = process.env.JWT_SECRET) !== null && _c !== void 0 ? _c : "");
|
|
115
|
+
const { payload } = await jwtVerify(session_cookie, secret);
|
|
116
|
+
const exp = Number((_d = payload.exp) !== null && _d !== void 0 ? _d : 0);
|
|
117
|
+
const now_seconds = Math.floor(Date.now() / 1000);
|
|
118
|
+
const otp_cfg = get_otp_config();
|
|
119
|
+
const seconds_until_exp = exp - now_seconds;
|
|
120
|
+
if (seconds_until_exp > 0 && seconds_until_exp < otp_cfg.slide_when_within_seconds) {
|
|
121
|
+
const ttl_seconds = hazo_auth_otp_session_ttl_seconds();
|
|
122
|
+
const user_id = String((_e = payload.user_id) !== null && _e !== void 0 ? _e : "");
|
|
123
|
+
const user_email = String((_f = payload.email) !== null && _f !== void 0 ? _f : "");
|
|
124
|
+
const new_token = await create_session_token(user_id, user_email, undefined, ttl_seconds);
|
|
125
|
+
const cookie_options = get_cookie_options({
|
|
126
|
+
httpOnly: true,
|
|
127
|
+
secure: process.env.NODE_ENV === "production",
|
|
128
|
+
sameSite: "lax",
|
|
129
|
+
path: "/",
|
|
130
|
+
maxAge: ttl_seconds,
|
|
131
|
+
});
|
|
132
|
+
response.cookies.set(get_cookie_name(BASE_COOKIE_NAMES.SESSION), new_token, cookie_options);
|
|
133
|
+
response.cookies.set(get_cookie_name(BASE_COOKIE_NAMES.USER_ID), user_id, cookie_options);
|
|
134
|
+
response.cookies.set(get_cookie_name(BASE_COOKIE_NAMES.USER_EMAIL), user_email, cookie_options);
|
|
135
|
+
response.cookies.set(get_cookie_name(BASE_COOKIE_NAMES.SESSION_KIND), "otp", cookie_options);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (slide_err) {
|
|
140
|
+
// Slide is best-effort — never break /me for this
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// --- end OTP sliding-session hook ---
|
|
144
|
+
return response;
|
|
103
145
|
}
|
|
104
146
|
catch (error) {
|
|
105
147
|
const error_message = error instanceof Error ? error.message : "Unknown error";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../../../src/server/routes/otp/request.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAYxD,wBAAsB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAsBhF"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// file_description: API route handler for OTP request (sends OTP email to user)
|
|
2
|
+
// section: imports
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { request_email_otp } from "../../../lib/services/otp_service.js";
|
|
6
|
+
import { get_client_ip } from "../../../lib/auth/hazo_get_auth.server.js";
|
|
7
|
+
import { create_app_logger } from "../../../lib/app_logger.js";
|
|
8
|
+
// section: validation
|
|
9
|
+
const RequestSchema = z.object({
|
|
10
|
+
email: z.string().email().max(254),
|
|
11
|
+
});
|
|
12
|
+
// section: api_handler
|
|
13
|
+
export async function otpRequestPOST(request) {
|
|
14
|
+
const logger = create_app_logger();
|
|
15
|
+
try {
|
|
16
|
+
const body_raw = await request.json().catch(() => ({}));
|
|
17
|
+
const parsed = RequestSchema.safeParse(body_raw);
|
|
18
|
+
if (!parsed.success) {
|
|
19
|
+
return NextResponse.json({ ok: false, error: "invalid_email" }, { status: 400 });
|
|
20
|
+
}
|
|
21
|
+
const ip = get_client_ip(request);
|
|
22
|
+
const result = await request_email_otp({ email: parsed.data.email, ip });
|
|
23
|
+
if (result.ok === false && result.error === "rate_limited") {
|
|
24
|
+
return NextResponse.json(result, { status: 429 });
|
|
25
|
+
}
|
|
26
|
+
return NextResponse.json({ ok: true }, { status: 200 });
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
30
|
+
logger.error("otp_request_route_error", { error: msg });
|
|
31
|
+
return NextResponse.json({ ok: false, error: "server_error" }, { status: 500 });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../../src/server/routes/otp/verify.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAmBxD,wBAAsB,aAAa,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAmD/E"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// file_description: API route handler for OTP verify (validates code and issues session cookies)
|
|
2
|
+
// section: imports
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { verify_email_otp } from "../../../lib/services/otp_service.js";
|
|
6
|
+
import { hazo_auth_otp_session_ttl_seconds } from "../../../lib/otp_config.server.js";
|
|
7
|
+
import { get_cookie_name, get_cookie_options, BASE_COOKIE_NAMES, } from "../../../lib/cookies_config.server.js";
|
|
8
|
+
import { get_client_ip } from "../../../lib/auth/hazo_get_auth.server.js";
|
|
9
|
+
import { create_app_logger } from "../../../lib/app_logger.js";
|
|
10
|
+
// section: validation
|
|
11
|
+
const VerifySchema = z.object({
|
|
12
|
+
email: z.string().email().max(254),
|
|
13
|
+
code: z.string().regex(/^\d{6}$/),
|
|
14
|
+
});
|
|
15
|
+
// section: api_handler
|
|
16
|
+
export async function otpVerifyPOST(request) {
|
|
17
|
+
const logger = create_app_logger();
|
|
18
|
+
try {
|
|
19
|
+
const body_raw = await request.json().catch(() => ({}));
|
|
20
|
+
const parsed = VerifySchema.safeParse(body_raw);
|
|
21
|
+
if (!parsed.success) {
|
|
22
|
+
return NextResponse.json({ ok: false, error: "invalid_or_expired" }, { status: 400 });
|
|
23
|
+
}
|
|
24
|
+
const ip = get_client_ip(request);
|
|
25
|
+
const result = await verify_email_otp({
|
|
26
|
+
email: parsed.data.email,
|
|
27
|
+
code: parsed.data.code,
|
|
28
|
+
ip,
|
|
29
|
+
});
|
|
30
|
+
if (result.ok === false) {
|
|
31
|
+
return NextResponse.json({ ok: false, error: "invalid_or_expired" }, { status: 400 });
|
|
32
|
+
}
|
|
33
|
+
const ttl_seconds = hazo_auth_otp_session_ttl_seconds();
|
|
34
|
+
const base_cookie_options = {
|
|
35
|
+
httpOnly: true,
|
|
36
|
+
secure: process.env.NODE_ENV === "production",
|
|
37
|
+
sameSite: "lax",
|
|
38
|
+
path: "/",
|
|
39
|
+
maxAge: ttl_seconds,
|
|
40
|
+
};
|
|
41
|
+
const cookie_options = get_cookie_options(base_cookie_options);
|
|
42
|
+
const response = NextResponse.json({
|
|
43
|
+
ok: true,
|
|
44
|
+
user_id: result.user_id,
|
|
45
|
+
email: result.email,
|
|
46
|
+
});
|
|
47
|
+
response.cookies.set(get_cookie_name(BASE_COOKIE_NAMES.SESSION), result.session_token, cookie_options);
|
|
48
|
+
response.cookies.set(get_cookie_name(BASE_COOKIE_NAMES.USER_ID), result.user_id, cookie_options);
|
|
49
|
+
response.cookies.set(get_cookie_name(BASE_COOKIE_NAMES.USER_EMAIL), result.email, cookie_options);
|
|
50
|
+
response.cookies.set(get_cookie_name(BASE_COOKIE_NAMES.SESSION_KIND), "otp", cookie_options);
|
|
51
|
+
return response;
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
55
|
+
logger.error("otp_verify_route_error", { error: msg });
|
|
56
|
+
return NextResponse.json({ ok: false, error: "invalid_or_expired" }, { status: 400 });
|
|
57
|
+
}
|
|
58
|
+
}
|
package/dist/server-lib.d.ts
CHANGED
|
@@ -24,6 +24,9 @@ export { get_oauth_config, is_google_oauth_enabled, is_email_password_enabled, }
|
|
|
24
24
|
export type { OAuthConfig } from "./lib/oauth_config.server";
|
|
25
25
|
export { get_branding_config, is_branding_enabled, is_allowed_logo_format, get_max_logo_size_bytes, } from "./lib/branding_config.server.js";
|
|
26
26
|
export type { FirmBrandingConfig } from "./lib/branding_config.server";
|
|
27
|
+
export { get_otp_config, hazo_auth_otp_session_ttl_seconds, OTP_CONFIG_DEFAULTS } from "./lib/otp_config.server.js";
|
|
28
|
+
export type { OtpConfig } from "./lib/otp_config.server";
|
|
29
|
+
export { request_email_otp, verify_email_otp } from "./lib/services/otp_service.js";
|
|
27
30
|
export { create_sqlite_hazo_connect } from "./lib/hazo_connect_setup.js";
|
|
28
31
|
export { get_hazo_connect_instance } from "./lib/hazo_connect_instance.server.js";
|
|
29
32
|
export { create_app_logger } from "./lib/app_logger.js";
|
package/dist/server-lib.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-lib.d.ts","sourceRoot":"","sources":["../src/server-lib.ts"],"names":[],"mappings":"AAYA,OAAO,aAAa,CAAC;AAGrB,cAAc,kBAAkB,CAAC;AAGjC,cAAc,sBAAsB,CAAC;AACrC,OAAO,EAAE,2BAA2B,EAAE,MAAM,wCAAwC,CAAC;AAGrF,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,mCAAmC,CAAC;AAG3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AAC/E,OAAO,EAAE,6BAA6B,EAAE,MAAM,wCAAwC,CAAC;AACvF,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAE,4BAA4B,EAAE,MAAM,uCAAuC,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,gCAAgC,EAAE,MAAM,2CAA2C,CAAC;AAC7F,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"server-lib.d.ts","sourceRoot":"","sources":["../src/server-lib.ts"],"names":[],"mappings":"AAYA,OAAO,aAAa,CAAC;AAGrB,cAAc,kBAAkB,CAAC;AAGjC,cAAc,sBAAsB,CAAC;AACrC,OAAO,EAAE,2BAA2B,EAAE,MAAM,wCAAwC,CAAC;AAGrF,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,mCAAmC,CAAC;AAG3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AAC/E,OAAO,EAAE,6BAA6B,EAAE,MAAM,wCAAwC,CAAC;AACvF,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAE,4BAA4B,EAAE,MAAM,uCAAuC,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,gCAAgC,EAAE,MAAM,2CAA2C,CAAC;AAC7F,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,iCAAiC,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACjH,YAAY,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAGjF,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AAG/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAGrD,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AACtE,YAAY,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAC5E,cAAc,+BAA+B,CAAC"}
|
package/dist/server-lib.js
CHANGED
|
@@ -37,6 +37,8 @@ export { get_user_fields_config } from "./lib/user_fields_config.server.js";
|
|
|
37
37
|
export { get_file_types_config } from "./lib/file_types_config.server.js";
|
|
38
38
|
export { get_oauth_config, is_google_oauth_enabled, is_email_password_enabled, } from "./lib/oauth_config.server.js";
|
|
39
39
|
export { get_branding_config, is_branding_enabled, is_allowed_logo_format, get_max_logo_size_bytes, } from "./lib/branding_config.server.js";
|
|
40
|
+
export { get_otp_config, hazo_auth_otp_session_ttl_seconds, OTP_CONFIG_DEFAULTS } from "./lib/otp_config.server.js";
|
|
41
|
+
export { request_email_otp, verify_email_otp } from "./lib/services/otp_service.js";
|
|
40
42
|
// section: hazo_connect_exports
|
|
41
43
|
export { create_sqlite_hazo_connect } from "./lib/hazo_connect_setup.js";
|
|
42
44
|
export { get_hazo_connect_instance } from "./lib/hazo_connect_instance.server.js";
|
|
@@ -50,6 +50,6 @@ export type ForgotPasswordPageProps = {
|
|
|
50
50
|
* @param props - Optional visual and navigation customization props
|
|
51
51
|
* @returns Server-rendered forgot password page
|
|
52
52
|
*/
|
|
53
|
-
export default function ForgotPasswordPage(
|
|
53
|
+
export default function ForgotPasswordPage(props: ForgotPasswordPageProps): import("react/jsx-runtime").JSX.Element;
|
|
54
54
|
export { ForgotPasswordPage };
|
|
55
55
|
//# sourceMappingURL=forgot_password.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forgot_password.d.ts","sourceRoot":"","sources":["../../src/server_pages/forgot_password.tsx"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAQrB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,eAAe,CAAC;IAErC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAGF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,
|
|
1
|
+
{"version":3,"file":"forgot_password.d.ts","sourceRoot":"","sources":["../../src/server_pages/forgot_password.tsx"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAQrB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,eAAe,CAAC;IAErC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAGF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,2CAiCxE;AAGD,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
|
|
@@ -31,7 +31,8 @@ import { DEFAULT_FORGOT_PASSWORD } from "../lib/config/default_config.js";
|
|
|
31
31
|
* @param props - Optional visual and navigation customization props
|
|
32
32
|
* @returns Server-rendered forgot password page
|
|
33
33
|
*/
|
|
34
|
-
export default function ForgotPasswordPage(
|
|
34
|
+
export default function ForgotPasswordPage(props) {
|
|
35
|
+
const { image_src, image_alt, image_background_color, sign_in_path = DEFAULT_FORGOT_PASSWORD.loginPath, sign_in_label = DEFAULT_FORGOT_PASSWORD.loginLabel, } = props !== null && props !== void 0 ? props : {};
|
|
35
36
|
// Load configuration from INI file (with defaults including asset images)
|
|
36
37
|
const config = get_forgot_password_config();
|
|
37
38
|
// Use props if provided, otherwise fall back to config (which includes default asset image)
|