hazo_auth 6.0.0 → 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 +70 -0
- package/SETUP_CHECKLIST.md +92 -0
- package/cli-src/cli/validate.ts +4 -0
- 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/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/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/login.d.ts.map +1 -1
- package/dist/server_pages/login.js +1 -1
- 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/otp.d.ts +42 -0
- package/dist/server_pages/otp.d.ts.map +1 -0
- package/dist/server_pages/otp.js +38 -0
- package/package.json +18 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Button } from "../ui/button.js";
|
|
5
|
+
import { Input } from "../ui/input.js";
|
|
6
|
+
import { Label } from "../ui/label.js";
|
|
7
|
+
export function OTPRequestForm({ on_sent, api_base = "/api/hazo_auth", email_label = "Email address", submit_label = "Send code", pending_label = "Sending…", className, }) {
|
|
8
|
+
const [email, set_email] = React.useState("");
|
|
9
|
+
const [pending, set_pending] = React.useState(false);
|
|
10
|
+
const [error, set_error] = React.useState(null);
|
|
11
|
+
async function on_submit(ev) {
|
|
12
|
+
var _a;
|
|
13
|
+
ev.preventDefault();
|
|
14
|
+
set_error(null);
|
|
15
|
+
set_pending(true);
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch(`${api_base}/otp/request`, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: { "content-type": "application/json" },
|
|
20
|
+
body: JSON.stringify({ email }),
|
|
21
|
+
});
|
|
22
|
+
if (res.status === 429) {
|
|
23
|
+
const body = (await res.json().catch(() => ({})));
|
|
24
|
+
const wait = (_a = body.retry_after_seconds) !== null && _a !== void 0 ? _a : 0;
|
|
25
|
+
set_error(`Too many requests. Try again in ${wait}s.`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
set_error("Could not send code. Please check the email address and try again.");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
on_sent === null || on_sent === void 0 ? void 0 : on_sent(email);
|
|
33
|
+
}
|
|
34
|
+
catch (_b) {
|
|
35
|
+
set_error("Network error. Please try again.");
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
set_pending(false);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return (_jsxs("form", { onSubmit: on_submit, className: className, "aria-label": "Request sign-in code", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "otp-email", children: email_label }), _jsx(Input, { id: "otp-email", name: "email", type: "email", autoComplete: "email", required: true, value: email, onChange: (e) => set_email(e.target.value), disabled: pending })] }), error && (_jsx("p", { role: "alert", className: "mt-3 text-sm text-destructive", children: error })), _jsx(Button, { type: "submit", disabled: pending || email.length === 0, className: "mt-4 w-full", children: pending ? pending_label : submit_label })] }));
|
|
42
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface OTPVerifyFormProps {
|
|
3
|
+
email: string;
|
|
4
|
+
on_success?: (auth: {
|
|
5
|
+
user_id: string;
|
|
6
|
+
email: string;
|
|
7
|
+
}) => void;
|
|
8
|
+
api_base?: string;
|
|
9
|
+
submit_label?: string;
|
|
10
|
+
pending_label?: string;
|
|
11
|
+
resend_label?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
redirect_url?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function OTPVerifyForm({ email, on_success, api_base, submit_label, pending_label, resend_label, className, redirect_url, }: OTPVerifyFormProps): React.ReactElement;
|
|
16
|
+
//# sourceMappingURL=OTPVerifyForm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OTPVerifyForm.d.ts","sourceRoot":"","sources":["../../../src/components/otp/OTPVerifyForm.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,UAAU,EACV,QAA2B,EAC3B,YAAuB,EACvB,aAA4B,EAC5B,YAA4B,EAC5B,SAAS,EACT,YAAY,GACb,EAAE,kBAAkB,GAAG,KAAK,CAAC,YAAY,CAoIzC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Button } from "../ui/button.js";
|
|
5
|
+
import { InputOTP, InputOTPGroup, InputOTPSlot } from "../ui/input-otp.js";
|
|
6
|
+
export function OTPVerifyForm({ email, on_success, api_base = "/api/hazo_auth", submit_label = "Verify", pending_label = "Verifying…", resend_label = "Resend code", className, redirect_url, }) {
|
|
7
|
+
const [code, set_code] = React.useState("");
|
|
8
|
+
const [pending, set_pending] = React.useState(false);
|
|
9
|
+
const [error, set_error] = React.useState(null);
|
|
10
|
+
const [resend_state, set_resend_state] = React.useState("idle");
|
|
11
|
+
const [resend_wait, set_resend_wait] = React.useState(0);
|
|
12
|
+
async function on_submit(ev) {
|
|
13
|
+
ev.preventDefault();
|
|
14
|
+
if (code.length !== 6)
|
|
15
|
+
return;
|
|
16
|
+
set_error(null);
|
|
17
|
+
set_pending(true);
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(`${api_base}/otp/verify`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: { "content-type": "application/json" },
|
|
22
|
+
body: JSON.stringify({ email, code }),
|
|
23
|
+
});
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
set_error("Invalid or expired code.");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const body = (await res.json());
|
|
29
|
+
if (body.ok) {
|
|
30
|
+
on_success === null || on_success === void 0 ? void 0 : on_success({ user_id: body.user_id, email: body.email });
|
|
31
|
+
if (redirect_url) {
|
|
32
|
+
window.location.href = redirect_url;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
set_error("Invalid or expired code.");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (_a) {
|
|
40
|
+
set_error("Network error. Please try again.");
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
set_pending(false);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function on_resend() {
|
|
47
|
+
var _a;
|
|
48
|
+
set_resend_state("sending");
|
|
49
|
+
set_error(null);
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch(`${api_base}/otp/request`, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: { "content-type": "application/json" },
|
|
54
|
+
body: JSON.stringify({ email }),
|
|
55
|
+
});
|
|
56
|
+
if (res.status === 429) {
|
|
57
|
+
const body = (await res.json().catch(() => ({})));
|
|
58
|
+
set_resend_state("rate_limited");
|
|
59
|
+
set_resend_wait((_a = body.retry_after_seconds) !== null && _a !== void 0 ? _a : 0);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
set_error("Could not resend code.");
|
|
64
|
+
set_resend_state("idle");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
set_resend_state("sent");
|
|
68
|
+
}
|
|
69
|
+
catch (_b) {
|
|
70
|
+
set_error("Network error.");
|
|
71
|
+
set_resend_state("idle");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return (_jsxs("form", { onSubmit: on_submit, className: className, "aria-label": "Enter sign-in code", children: [_jsxs("p", { className: "text-sm text-muted-foreground mb-3", children: ["Enter the 6-digit code sent to ", _jsx("strong", { children: email }), "."] }), _jsx(InputOTP, { maxLength: 6, value: code, onChange: (v) => set_code(v), inputMode: "numeric", autoComplete: "one-time-code", disabled: pending, "aria-label": "One-time code", children: _jsxs(InputOTPGroup, { children: [_jsx(InputOTPSlot, { index: 0 }), _jsx(InputOTPSlot, { index: 1 }), _jsx(InputOTPSlot, { index: 2 }), _jsx(InputOTPSlot, { index: 3 }), _jsx(InputOTPSlot, { index: 4 }), _jsx(InputOTPSlot, { index: 5 })] }) }), error && (_jsx("p", { role: "alert", className: "mt-3 text-sm text-destructive", children: error })), _jsx(Button, { type: "submit", disabled: pending || code.length !== 6, className: "mt-4 w-full", children: pending ? pending_label : submit_label }), _jsx("div", { className: "mt-3 text-sm text-center", children: resend_state === "sent" ? (_jsx("span", { className: "text-muted-foreground", children: "Code sent." })) : resend_state === "rate_limited" ? (_jsxs("span", { className: "text-muted-foreground", children: ["Wait ", resend_wait, "s before resending."] })) : (_jsx("button", { type: "button", onClick: on_resend, disabled: resend_state === "sending", className: "underline text-muted-foreground hover:text-foreground", children: resend_label })) })] }));
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/otp/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
declare const InputOTP: React.ForwardRefExoticComponent<(Omit<Omit<React.InputHTMLAttributes<HTMLInputElement>, "value" | "onChange" | "maxLength" | "textAlign" | "onComplete" | "pushPasswordManagerStrategy" | "pasteTransformer" | "containerClassName" | "noScriptCSSFallback"> & {
|
|
3
|
+
value?: string;
|
|
4
|
+
onChange?: (newValue: string) => unknown;
|
|
5
|
+
maxLength: number;
|
|
6
|
+
textAlign?: "left" | "center" | "right";
|
|
7
|
+
onComplete?: (...args: any[]) => unknown;
|
|
8
|
+
pushPasswordManagerStrategy?: "increase-width" | "none";
|
|
9
|
+
pasteTransformer?: (pasted: string) => string;
|
|
10
|
+
containerClassName?: string;
|
|
11
|
+
noScriptCSSFallback?: string | null;
|
|
12
|
+
} & {
|
|
13
|
+
render: (props: import("input-otp").RenderProps) => React.ReactNode;
|
|
14
|
+
children?: never;
|
|
15
|
+
} & React.RefAttributes<HTMLInputElement>, "ref"> | Omit<Omit<React.InputHTMLAttributes<HTMLInputElement>, "value" | "onChange" | "maxLength" | "textAlign" | "onComplete" | "pushPasswordManagerStrategy" | "pasteTransformer" | "containerClassName" | "noScriptCSSFallback"> & {
|
|
16
|
+
value?: string;
|
|
17
|
+
onChange?: (newValue: string) => unknown;
|
|
18
|
+
maxLength: number;
|
|
19
|
+
textAlign?: "left" | "center" | "right";
|
|
20
|
+
onComplete?: (...args: any[]) => unknown;
|
|
21
|
+
pushPasswordManagerStrategy?: "increase-width" | "none";
|
|
22
|
+
pasteTransformer?: (pasted: string) => string;
|
|
23
|
+
containerClassName?: string;
|
|
24
|
+
noScriptCSSFallback?: string | null;
|
|
25
|
+
} & {
|
|
26
|
+
render?: never;
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
} & React.RefAttributes<HTMLInputElement>, "ref">) & React.RefAttributes<HTMLInputElement>>;
|
|
29
|
+
declare const InputOTPGroup: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
30
|
+
declare const InputOTPSlot: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
|
|
31
|
+
index: number;
|
|
32
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
33
|
+
declare const InputOTPSeparator: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
34
|
+
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
|
35
|
+
//# sourceMappingURL=input-otp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input-otp.d.ts","sourceRoot":"","sources":["../../../src/components/ui/input-otp.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAK/B,QAAA,MAAM,QAAQ;;;;;kBAUoC,GAAG;;;;;;;;;;;;;kBAAH,GAAG;;;;;;;;2FAGnD,CAAC;AAGH,QAAA,MAAM,aAAa,mKAKjB,CAAC;AAGH,QAAA,MAAM,YAAY;WAEiC,MAAM;wCAyBvD,CAAC;AAGH,QAAA,MAAM,iBAAiB,mKAOrB,CAAC;AAGH,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,YAAY,EAAE,iBAAiB,EAAE,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
13
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
|
+
import * as React from "react";
|
|
15
|
+
import { OTPInput, OTPInputContext } from "input-otp";
|
|
16
|
+
import { Minus } from "lucide-react";
|
|
17
|
+
import { cn } from "../../lib/utils.js";
|
|
18
|
+
const InputOTP = React.forwardRef((_a, ref) => {
|
|
19
|
+
var { className, containerClassName } = _a, props = __rest(_a, ["className", "containerClassName"]);
|
|
20
|
+
return (_jsx(OTPInput, Object.assign({ ref: ref, containerClassName: cn("flex items-center gap-2 has-[:disabled]:opacity-50", containerClassName), className: cn("disabled:cursor-not-allowed", className) }, props)));
|
|
21
|
+
});
|
|
22
|
+
InputOTP.displayName = "InputOTP";
|
|
23
|
+
const InputOTPGroup = React.forwardRef((_a, ref) => {
|
|
24
|
+
var { className } = _a, props = __rest(_a, ["className"]);
|
|
25
|
+
return (_jsx("div", Object.assign({ ref: ref, className: cn("flex items-center", className) }, props)));
|
|
26
|
+
});
|
|
27
|
+
InputOTPGroup.displayName = "InputOTPGroup";
|
|
28
|
+
const InputOTPSlot = React.forwardRef((_a, ref) => {
|
|
29
|
+
var _b;
|
|
30
|
+
var { index, className } = _a, props = __rest(_a, ["index", "className"]);
|
|
31
|
+
const inputOTPContext = React.useContext(OTPInputContext);
|
|
32
|
+
const slot = (_b = inputOTPContext === null || inputOTPContext === void 0 ? void 0 : inputOTPContext.slots) === null || _b === void 0 ? void 0 : _b[index];
|
|
33
|
+
const char = slot === null || slot === void 0 ? void 0 : slot.char;
|
|
34
|
+
const hasFakeCaret = slot === null || slot === void 0 ? void 0 : slot.hasFakeCaret;
|
|
35
|
+
const isActive = slot === null || slot === void 0 ? void 0 : slot.isActive;
|
|
36
|
+
return (_jsxs("div", Object.assign({ ref: ref, className: cn("relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md", isActive && "z-10 ring-2 ring-ring ring-offset-background", className) }, props, { children: [char, hasFakeCaret && (_jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center", children: _jsx("div", { className: "h-4 w-px animate-caret-blink bg-foreground duration-1000" }) }))] })));
|
|
37
|
+
});
|
|
38
|
+
InputOTPSlot.displayName = "InputOTPSlot";
|
|
39
|
+
const InputOTPSeparator = React.forwardRef((_a, ref) => {
|
|
40
|
+
var props = __rest(_a, []);
|
|
41
|
+
return (_jsx("div", Object.assign({ ref: ref, role: "separator" }, props, { children: _jsx(Minus, {}) })));
|
|
42
|
+
});
|
|
43
|
+
InputOTPSeparator.displayName = "InputOTPSeparator";
|
|
44
|
+
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
|
@@ -9,6 +9,7 @@ export declare const BASE_COOKIE_NAMES: {
|
|
|
9
9
|
readonly USER_ID: "hazo_auth_user_id";
|
|
10
10
|
readonly USER_EMAIL: "hazo_auth_user_email";
|
|
11
11
|
readonly SESSION: "hazo_auth_session";
|
|
12
|
+
readonly SESSION_KIND: "hazo_auth_session_kind";
|
|
12
13
|
readonly DEV_LOCK: "hazo_auth_dev_lock";
|
|
13
14
|
readonly SCOPE_ID: "hazo_auth_scope_id";
|
|
14
15
|
readonly ANON_ID: "hazo_auth_anon_id";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cookies_config.server.d.ts","sourceRoot":"","sources":["../../src/lib/cookies_config.server.ts"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAMrB,MAAM,MAAM,aAAa,GAAG;IAC1B,6FAA6F;IAC7F,aAAa,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAYF,eAAO,MAAM,iBAAiB
|
|
1
|
+
{"version":3,"file":"cookies_config.server.d.ts","sourceRoot":"","sources":["../../src/lib/cookies_config.server.ts"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAMrB,MAAM,MAAM,aAAa,GAAG;IAC1B,6FAA6F;IAC7F,aAAa,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAYF,eAAO,MAAM,iBAAiB;;;;;;;;CAQpB,CAAC;AAGX;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,aAAa,CAoBlD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAGzD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,GAAG;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAQzG;AAKD;;;GAGG;AACH,wBAAgB,yBAAyB,IAAI,aAAa,CAKzD;AAED;;GAEG;AACH,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD"}
|
|
@@ -14,6 +14,7 @@ export const BASE_COOKIE_NAMES = {
|
|
|
14
14
|
USER_ID: "hazo_auth_user_id",
|
|
15
15
|
USER_EMAIL: "hazo_auth_user_email",
|
|
16
16
|
SESSION: "hazo_auth_session",
|
|
17
|
+
SESSION_KIND: "hazo_auth_session_kind", // v6.1: marks OTP-issued sessions so /me can apply sliding expiry
|
|
17
18
|
DEV_LOCK: "hazo_auth_dev_lock",
|
|
18
19
|
SCOPE_ID: "hazo_auth_scope_id", // v5.2: Tenant context cookie for multi-tenancy
|
|
19
20
|
ANON_ID: "hazo_auth_anon_id", // v5.2: Stable opaque per-visitor ID for anonymous flows (e.g. hazo_feedback)
|
|
@@ -18,6 +18,12 @@ export type LoginConfig = {
|
|
|
18
18
|
imageBackgroundColor: string;
|
|
19
19
|
/** OAuth configuration */
|
|
20
20
|
oauth: OAuthConfig;
|
|
21
|
+
/** Whether the OTP sign-in link is shown below the login form */
|
|
22
|
+
otpSigninEnabled: boolean;
|
|
23
|
+
/** Label for the OTP sign-in link */
|
|
24
|
+
otpSigninLabel: string;
|
|
25
|
+
/** href for the OTP sign-in link */
|
|
26
|
+
otpSigninHref: string;
|
|
21
27
|
};
|
|
22
28
|
/**
|
|
23
29
|
* Reads login layout configuration from hazo_auth_config.ini file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login_config.server.d.ts","sourceRoot":"","sources":["../../src/lib/login_config.server.ts"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAKrB,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAQ3E,MAAM,MAAM,WAAW,GAAG;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,0BAA0B;IAC1B,KAAK,EAAE,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"login_config.server.d.ts","sourceRoot":"","sources":["../../src/lib/login_config.server.ts"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAKrB,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAQ3E,MAAM,MAAM,WAAW,GAAG;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,0BAA0B;IAC1B,KAAK,EAAE,WAAW,CAAC;IACnB,iEAAiE;IACjE,gBAAgB,EAAE,OAAO,CAAC;IAC1B,qCAAqC;IACrC,cAAc,EAAE,MAAM,CAAC;IACvB,oCAAoC;IACpC,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAGF;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,WAAW,CAiF9C"}
|
|
@@ -38,6 +38,10 @@ export function get_login_config() {
|
|
|
38
38
|
const imageBackgroundColor = get_config_value(section, "image_background_color", "#f1f5f9");
|
|
39
39
|
// Get OAuth configuration
|
|
40
40
|
const oauth = get_oauth_config();
|
|
41
|
+
// OTP sign-in link
|
|
42
|
+
const otpSigninEnabled = get_config_value(section, "otp_signin_enabled", "false") === "true";
|
|
43
|
+
const otpSigninLabel = get_config_value(section, "otp_signin_label", "Sign in with email code");
|
|
44
|
+
const otpSigninHref = get_config_value(section, "otp_signin_href", "/hazo_auth/otp");
|
|
41
45
|
return {
|
|
42
46
|
redirectRoute,
|
|
43
47
|
successMessage,
|
|
@@ -55,5 +59,8 @@ export function get_login_config() {
|
|
|
55
59
|
imageAlt,
|
|
56
60
|
imageBackgroundColor,
|
|
57
61
|
oauth,
|
|
62
|
+
otpSigninEnabled,
|
|
63
|
+
otpSigninLabel,
|
|
64
|
+
otpSigninHref,
|
|
58
65
|
};
|
|
59
66
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
export declare const OTP_CONFIG_DEFAULTS: {
|
|
3
|
+
readonly auto_register: false;
|
|
4
|
+
readonly code_ttl_seconds: 600;
|
|
5
|
+
readonly session_ttl_seconds: 604800;
|
|
6
|
+
readonly slide_when_within_seconds: 86400;
|
|
7
|
+
readonly email_rate_limit_max: 3;
|
|
8
|
+
readonly email_rate_limit_window_seconds: 900;
|
|
9
|
+
readonly ip_rate_limit_max: 20;
|
|
10
|
+
readonly ip_rate_limit_window_seconds: 3600;
|
|
11
|
+
readonly max_verify_attempts: 5;
|
|
12
|
+
readonly auto_assign_scope_id: "00000000-0000-0000-0000-000000000001";
|
|
13
|
+
readonly auto_assign_role_name: "member";
|
|
14
|
+
};
|
|
15
|
+
export type OtpConfig = {
|
|
16
|
+
/** Whether to automatically register a new user when an unrecognised email requests an OTP */
|
|
17
|
+
auto_register: boolean;
|
|
18
|
+
/** How long (seconds) a generated OTP code is valid */
|
|
19
|
+
code_ttl_seconds: number;
|
|
20
|
+
/** How long (seconds) the session created after successful OTP verification lasts */
|
|
21
|
+
session_ttl_seconds: number;
|
|
22
|
+
/** Slide the session expiry when the remaining TTL falls below this many seconds */
|
|
23
|
+
slide_when_within_seconds: number;
|
|
24
|
+
/** Maximum OTP requests allowed per email address within the rate-limit window */
|
|
25
|
+
email_rate_limit_max: number;
|
|
26
|
+
/** Rate-limit window (seconds) for per-email OTP requests */
|
|
27
|
+
email_rate_limit_window_seconds: number;
|
|
28
|
+
/** Maximum OTP requests allowed per IP address within the rate-limit window */
|
|
29
|
+
ip_rate_limit_max: number;
|
|
30
|
+
/** Rate-limit window (seconds) for per-IP OTP requests */
|
|
31
|
+
ip_rate_limit_window_seconds: number;
|
|
32
|
+
/** Maximum failed verify attempts before the OTP code is invalidated */
|
|
33
|
+
max_verify_attempts: number;
|
|
34
|
+
/** Scope ID to auto-assign a newly registered OTP user to */
|
|
35
|
+
auto_assign_scope_id: string;
|
|
36
|
+
/** Role name to assign within the auto-assign scope */
|
|
37
|
+
auto_assign_role_name: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Reads OTP configuration from hazo_auth_config.ini [hazo_auth__otp] section.
|
|
41
|
+
* Falls back to defaults if the config file or section is missing.
|
|
42
|
+
*/
|
|
43
|
+
export declare function get_otp_config(): OtpConfig;
|
|
44
|
+
/**
|
|
45
|
+
* Convenience accessor — returns just the session TTL seconds from OTP config.
|
|
46
|
+
* Suitable for passing to token-creation utilities.
|
|
47
|
+
*/
|
|
48
|
+
export declare function hazo_auth_otp_session_ttl_seconds(): number;
|
|
49
|
+
//# sourceMappingURL=otp_config.server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otp_config.server.d.ts","sourceRoot":"","sources":["../../src/lib/otp_config.server.ts"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAMrB,eAAO,MAAM,mBAAmB;;;;;;;;;;;;CAYtB,CAAC;AAGX,MAAM,MAAM,SAAS,GAAG;IACtB,8FAA8F;IAC9F,aAAa,EAAE,OAAO,CAAC;IACvB,uDAAuD;IACvD,gBAAgB,EAAE,MAAM,CAAC;IACzB,qFAAqF;IACrF,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oFAAoF;IACpF,yBAAyB,EAAE,MAAM,CAAC;IAClC,kFAAkF;IAClF,oBAAoB,EAAE,MAAM,CAAC;IAC7B,6DAA6D;IAC7D,+BAA+B,EAAE,MAAM,CAAC;IACxC,+EAA+E;IAC/E,iBAAiB,EAAE,MAAM,CAAC;IAC1B,0DAA0D;IAC1D,4BAA4B,EAAE,MAAM,CAAC;IACrC,wEAAwE;IACxE,mBAAmB,EAAE,MAAM,CAAC;IAC5B,6DAA6D;IAC7D,oBAAoB,EAAE,MAAM,CAAC;IAC7B,uDAAuD;IACvD,qBAAqB,EAAE,MAAM,CAAC;CAC/B,CAAC;AAGF;;;GAGG;AACH,wBAAgB,cAAc,IAAI,SAAS,CA6B1C;AAED;;;GAGG;AACH,wBAAgB,iCAAiC,IAAI,MAAM,CAE1D"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// file_description: server-only helper to read OTP sign-in configuration from hazo_auth_config.ini
|
|
2
|
+
// section: server-only-guard
|
|
3
|
+
import "server-only";
|
|
4
|
+
// section: imports
|
|
5
|
+
import { get_config_value, get_config_boolean, get_config_number } from "./config/config_loader.server.js";
|
|
6
|
+
// section: defaults
|
|
7
|
+
export const OTP_CONFIG_DEFAULTS = {
|
|
8
|
+
auto_register: false,
|
|
9
|
+
code_ttl_seconds: 600,
|
|
10
|
+
session_ttl_seconds: 604800,
|
|
11
|
+
slide_when_within_seconds: 86400,
|
|
12
|
+
email_rate_limit_max: 3,
|
|
13
|
+
email_rate_limit_window_seconds: 900,
|
|
14
|
+
ip_rate_limit_max: 20,
|
|
15
|
+
ip_rate_limit_window_seconds: 3600,
|
|
16
|
+
max_verify_attempts: 5,
|
|
17
|
+
auto_assign_scope_id: "00000000-0000-0000-0000-000000000001",
|
|
18
|
+
auto_assign_role_name: "member",
|
|
19
|
+
};
|
|
20
|
+
// section: helpers
|
|
21
|
+
/**
|
|
22
|
+
* Reads OTP configuration from hazo_auth_config.ini [hazo_auth__otp] section.
|
|
23
|
+
* Falls back to defaults if the config file or section is missing.
|
|
24
|
+
*/
|
|
25
|
+
export function get_otp_config() {
|
|
26
|
+
const section = "hazo_auth__otp";
|
|
27
|
+
const d = OTP_CONFIG_DEFAULTS;
|
|
28
|
+
return {
|
|
29
|
+
auto_register: get_config_boolean(section, "otp_auto_register", d.auto_register),
|
|
30
|
+
code_ttl_seconds: get_config_number(section, "otp_code_ttl_seconds", d.code_ttl_seconds),
|
|
31
|
+
session_ttl_seconds: get_config_number(section, "otp_session_ttl_seconds", d.session_ttl_seconds),
|
|
32
|
+
slide_when_within_seconds: get_config_number(section, "otp_slide_when_within_seconds", d.slide_when_within_seconds),
|
|
33
|
+
email_rate_limit_max: get_config_number(section, "otp_email_rate_limit_max", d.email_rate_limit_max),
|
|
34
|
+
email_rate_limit_window_seconds: get_config_number(section, "otp_email_rate_limit_window_seconds", d.email_rate_limit_window_seconds),
|
|
35
|
+
ip_rate_limit_max: get_config_number(section, "otp_ip_rate_limit_max", d.ip_rate_limit_max),
|
|
36
|
+
ip_rate_limit_window_seconds: get_config_number(section, "otp_ip_rate_limit_window_seconds", d.ip_rate_limit_window_seconds),
|
|
37
|
+
max_verify_attempts: get_config_number(section, "otp_max_verify_attempts", d.max_verify_attempts),
|
|
38
|
+
auto_assign_scope_id: get_config_value(section, "otp_auto_assign_scope_id", d.auto_assign_scope_id),
|
|
39
|
+
auto_assign_role_name: get_config_value(section, "otp_auto_assign_role_name", d.auto_assign_role_name),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Convenience accessor — returns just the session TTL seconds from OTP config.
|
|
44
|
+
* Suitable for passing to token-creation utilities.
|
|
45
|
+
*/
|
|
46
|
+
export function hazo_auth_otp_session_ttl_seconds() {
|
|
47
|
+
return get_otp_config().session_ttl_seconds;
|
|
48
|
+
}
|
|
@@ -7,7 +7,7 @@ export type EmailOptions = {
|
|
|
7
7
|
html_body?: string;
|
|
8
8
|
text_body?: string;
|
|
9
9
|
};
|
|
10
|
-
export type EmailTemplateType = "forgot_password" | "email_verification" | "password_changed";
|
|
10
|
+
export type EmailTemplateType = "forgot_password" | "email_verification" | "password_changed" | "otp_signin_code";
|
|
11
11
|
export type EmailTemplateData = {
|
|
12
12
|
token?: string;
|
|
13
13
|
verification_url?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/email_service.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAoB,MAAM,aAAa,CAAC;AACnE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAGxE,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,GAAG,oBAAoB,GAAG,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"email_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/email_service.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAoB,MAAM,aAAa,CAAC;AACnE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAGxE,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,GAAG,oBAAoB,GAAG,kBAAkB,GAAG,iBAAiB,CAAC;AAElH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACnC,CAAC;AAiBF;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAEpE;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAE3E;AAgMD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAwErG;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,aAAa,EAAE,iBAAiB,EAChC,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAyF/C"}
|
|
@@ -200,6 +200,8 @@ function get_email_subject(template_type) {
|
|
|
200
200
|
return "Reset Your Password";
|
|
201
201
|
case "password_changed":
|
|
202
202
|
return "Password Changed Successfully";
|
|
203
|
+
case "otp_signin_code":
|
|
204
|
+
return "Your sign-in code";
|
|
203
205
|
default:
|
|
204
206
|
return "Email from hazo_auth";
|
|
205
207
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email_template_manifest.d.ts","sourceRoot":"","sources":["../../../src/lib/services/email_template_manifest.ts"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAMrB,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAiB3E;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,EAAE,sBAAsB,
|
|
1
|
+
{"version":3,"file":"email_template_manifest.d.ts","sourceRoot":"","sources":["../../../src/lib/services/email_template_manifest.ts"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAMrB,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAiB3E;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,EAAE,sBAAsB,EAwF/D,CAAC"}
|
|
@@ -92,4 +92,21 @@ export const hazo_auth_template_manifest = [
|
|
|
92
92
|
},
|
|
93
93
|
],
|
|
94
94
|
},
|
|
95
|
+
{
|
|
96
|
+
template_name: "otp_signin_code",
|
|
97
|
+
template_label: "OTP sign-in code",
|
|
98
|
+
category: SYSTEM_CATEGORY,
|
|
99
|
+
html: read_template("otp_signin_code", "html"),
|
|
100
|
+
text: read_template("otp_signin_code", "txt"),
|
|
101
|
+
variables: [
|
|
102
|
+
{
|
|
103
|
+
variable_name: "otp_code",
|
|
104
|
+
variable_description: "6-digit OTP code for email sign-in (v6.1.0+)",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
variable_name: "expires_in_minutes",
|
|
108
|
+
variable_description: "Number of minutes until the OTP code expires",
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
95
112
|
];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Your sign-in code</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
8
|
+
<p>Your sign-in code is:</p>
|
|
9
|
+
<p style="font-size: 28px; font-weight: bold; letter-spacing: 0.2em; font-family: monospace; margin: 16px 0;">{{otp_code}}</p>
|
|
10
|
+
<p>This code expires in {{expires_in_minutes}} minutes.</p>
|
|
11
|
+
<p style="color: #666; font-size: 12px;">If you didn't request this code, you can safely ignore this email.</p>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -18,4 +18,6 @@ export * from "./scope_service.js";
|
|
|
18
18
|
export * from "./user_scope_service.js";
|
|
19
19
|
export * from "./oauth_service.js";
|
|
20
20
|
export * from "./branding_service.js";
|
|
21
|
+
export { request_email_otp, verify_email_otp, generate_otp_code, hash_otp_code, verify_otp_code, } from "./otp_service.js";
|
|
22
|
+
export type { RequestEmailOTPResult, VerifyEmailOTPResult } from "./otp_service";
|
|
21
23
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/services/index.ts"],"names":[],"mappings":"AAEA,cAAc,iBAAiB,CAAC;AAChC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,iBAAiB,CAAC;AAChC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,kCAAkC,CAAC;AACjD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iCAAiC,CAAC;AAChD,cAAc,wBAAwB,CAAC;AACvC,cAAc,iBAAiB,CAAC;AAChC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/services/index.ts"],"names":[],"mappings":"AAEA,cAAc,iBAAiB,CAAC;AAChC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,iBAAiB,CAAC;AAChC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,kCAAkC,CAAC;AACjD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iCAAiC,CAAC;AAChD,cAAc,wBAAwB,CAAC;AACvC,cAAc,iBAAiB,CAAC;AAChC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,eAAe,GAChB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -20,3 +20,4 @@ export * from "./scope_service.js";
|
|
|
20
20
|
export * from "./user_scope_service.js";
|
|
21
21
|
export * from "./oauth_service.js";
|
|
22
22
|
export * from "./branding_service.js";
|
|
23
|
+
export { request_email_otp, verify_email_otp, generate_otp_code, hash_otp_code, verify_otp_code, } from "./otp_service.js";
|
|
@@ -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"}
|