cauthy-auth 0.5.2 → 0.8.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 +68 -3
- package/dist/{cauthy-BCTCqubt.d.ts → cauthy-Bn3iv50y.d.ts} +1 -1
- package/dist/{cauthy-hswMoS7B.d.mts → cauthy-C29HCvag.d.mts} +1 -1
- package/dist/cli.js +490 -25
- package/dist/client.d.mts +236 -0
- package/dist/client.d.ts +236 -0
- package/dist/client.js +331 -0
- package/dist/client.js.map +1 -0
- package/dist/client.mjs +297 -0
- package/dist/client.mjs.map +1 -0
- package/dist/express.d.mts +24 -0
- package/dist/express.d.ts +24 -0
- package/dist/express.js +102 -0
- package/dist/express.js.map +1 -0
- package/dist/express.mjs +76 -0
- package/dist/express.mjs.map +1 -0
- package/dist/fastify.d.mts +2 -2
- package/dist/fastify.d.ts +2 -2
- package/dist/fastify.js +3 -1
- package/dist/fastify.js.map +1 -1
- package/dist/fastify.mjs +3 -1
- package/dist/fastify.mjs.map +1 -1
- package/dist/hono.d.mts +23 -0
- package/dist/hono.d.ts +23 -0
- package/dist/hono.js +50 -0
- package/dist/hono.js.map +1 -0
- package/dist/hono.mjs +24 -0
- package/dist/hono.mjs.map +1 -0
- package/dist/index.d.mts +44 -6
- package/dist/index.d.ts +44 -6
- package/dist/index.js +668 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +656 -37
- package/dist/index.mjs.map +1 -1
- package/dist/next.d.mts +24 -0
- package/dist/next.d.ts +24 -0
- package/dist/next.js +77 -0
- package/dist/next.js.map +1 -0
- package/dist/next.mjs +52 -0
- package/dist/next.mjs.map +1 -0
- package/dist/react.d.mts +546 -7
- package/dist/react.d.ts +546 -7
- package/dist/react.js +925 -14
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +913 -10
- package/dist/react.mjs.map +1 -1
- package/dist/styles.css +271 -0
- package/dist/svelte.d.mts +26 -0
- package/dist/svelte.d.ts +26 -0
- package/dist/svelte.js +402 -0
- package/dist/svelte.js.map +1 -0
- package/dist/svelte.mjs +373 -0
- package/dist/svelte.mjs.map +1 -0
- package/dist/{system-DvcQjORr.d.ts → system-DhRr_WxR.d.ts} +139 -1
- package/dist/{system-CR8p4BjB.d.mts → system-ZD_Wa8Bm.d.mts} +139 -1
- package/dist/theme.d.mts +56 -1
- package/dist/theme.d.ts +56 -1
- package/dist/theme.js +181 -5
- package/dist/theme.js.map +1 -1
- package/dist/theme.mjs +176 -4
- package/dist/theme.mjs.map +1 -1
- package/dist/validate-B_iC-4y8.d.mts +19 -0
- package/dist/validate-B_iC-4y8.d.ts +19 -0
- package/dist/vue.d.mts +28 -0
- package/dist/vue.d.ts +28 -0
- package/dist/vue.js +420 -0
- package/dist/vue.js.map +1 -0
- package/dist/vue.mjs +391 -0
- package/dist/vue.mjs.map +1 -0
- package/package.json +68 -7
- package/dist/permissions-B-a3eN0h.d.mts +0 -52
- package/dist/permissions-B-a3eN0h.d.ts +0 -52
package/dist/react.mjs
CHANGED
|
@@ -1,5 +1,211 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
1
3
|
// src/react.tsx
|
|
2
|
-
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { createContext, useCallback, useContext, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
|
|
5
|
+
|
|
6
|
+
// src/theme.ts
|
|
7
|
+
function themeVariables(theme) {
|
|
8
|
+
const vars = {};
|
|
9
|
+
const put = (name, v) => {
|
|
10
|
+
if (v === void 0 || v === "") return;
|
|
11
|
+
vars[`--cauthy-${name}`] = typeof v === "number" ? `${v}px` : v;
|
|
12
|
+
};
|
|
13
|
+
put("surface", theme.surface);
|
|
14
|
+
put("text", theme.text);
|
|
15
|
+
put("text-muted", theme.textMuted);
|
|
16
|
+
put("border", theme.border);
|
|
17
|
+
put("input-background", theme.inputBackground);
|
|
18
|
+
put("input-border", theme.inputBorder);
|
|
19
|
+
put("primary", theme.primary);
|
|
20
|
+
put("primary-text", theme.primaryText);
|
|
21
|
+
put("accent", theme.accent);
|
|
22
|
+
put("error", theme.error);
|
|
23
|
+
put("error-background", theme.errorBackground);
|
|
24
|
+
put("success", theme.success);
|
|
25
|
+
put("success-background", theme.successBackground);
|
|
26
|
+
put("radius", theme.radius);
|
|
27
|
+
put("button-radius", theme.buttonRadius);
|
|
28
|
+
put("font", theme.fontFamily);
|
|
29
|
+
return vars;
|
|
30
|
+
}
|
|
31
|
+
function fromWire(wire) {
|
|
32
|
+
const names = {
|
|
33
|
+
background: "background",
|
|
34
|
+
surface: "surface",
|
|
35
|
+
text: "text",
|
|
36
|
+
text_muted: "textMuted",
|
|
37
|
+
border: "border",
|
|
38
|
+
primary: "primary",
|
|
39
|
+
primary_text: "primaryText",
|
|
40
|
+
accent: "accent",
|
|
41
|
+
error: "error",
|
|
42
|
+
error_background: "errorBackground",
|
|
43
|
+
success: "success",
|
|
44
|
+
success_background: "successBackground",
|
|
45
|
+
radius: "radius",
|
|
46
|
+
button_radius: "buttonRadius",
|
|
47
|
+
font_family: "fontFamily",
|
|
48
|
+
logo_url: "logoUrl",
|
|
49
|
+
fonts: "fonts",
|
|
50
|
+
layout: "layout",
|
|
51
|
+
panel_colors: "panelColors",
|
|
52
|
+
panel_text: "panelText",
|
|
53
|
+
panel_icon_url: "panelIconUrl",
|
|
54
|
+
texts: "texts",
|
|
55
|
+
input_background: "inputBackground",
|
|
56
|
+
input_border: "inputBorder",
|
|
57
|
+
tokens: "tokens"
|
|
58
|
+
};
|
|
59
|
+
const theme = {};
|
|
60
|
+
for (const [k, v] of Object.entries(wire ?? {})) if (names[k] && v !== void 0 && v !== null && v !== "") theme[names[k]] = v;
|
|
61
|
+
return theme;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/validate.ts
|
|
65
|
+
var MIN_PASSWORD = 8;
|
|
66
|
+
var MAX_PASSWORD = 256;
|
|
67
|
+
var MAX_NAME = 120;
|
|
68
|
+
var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
69
|
+
function checkEmail(value) {
|
|
70
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
71
|
+
if (v === "") return { field: "email", code: "required", message: "Enter your email address." };
|
|
72
|
+
if (v.length > 254 || !EMAIL.test(v)) return { field: "email", code: "invalid_email", message: "That does not look like an email address." };
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function checkPassword(value) {
|
|
76
|
+
if (typeof value !== "string" || value === "") return { field: "password", code: "required", message: "Enter your password." };
|
|
77
|
+
if (value.length > MAX_PASSWORD) return { field: "password", code: "too_long", message: "That password is too long." };
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
function checkNewPassword(value) {
|
|
81
|
+
const basic = checkPassword(value);
|
|
82
|
+
if (basic) return basic;
|
|
83
|
+
if (value.length < MIN_PASSWORD) {
|
|
84
|
+
return { field: "password", code: "too_short", message: `Use at least ${MIN_PASSWORD} characters.` };
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
function checkName(value) {
|
|
89
|
+
if (value === void 0 || value === null || value === "") return null;
|
|
90
|
+
if (typeof value !== "string" || value.trim().length > MAX_NAME) return { field: "name", code: "too_long", message: `Use at most ${MAX_NAME} characters.` };
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
function checkCode(value) {
|
|
94
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
95
|
+
if (v === "") return { field: "code", code: "required", message: "Enter the code." };
|
|
96
|
+
if (v.length > 40) return { field: "code", code: "too_long", message: "That is not a code." };
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function firstProblem(...checks) {
|
|
100
|
+
return checks.find((c) => c !== null) ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/client.ts
|
|
104
|
+
var UNAVAILABLE = { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." };
|
|
105
|
+
function createAuthClient(options = {}) {
|
|
106
|
+
const base = (options.basePath ?? "/api/auth").replace(/\/+$/, "");
|
|
107
|
+
const doFetch = (...args) => (options.fetch ?? fetch)(...args);
|
|
108
|
+
async function post(path, form, bad) {
|
|
109
|
+
if (bad) return { ok: false, error: { code: bad.code, message: bad.message, field: bad.field } };
|
|
110
|
+
try {
|
|
111
|
+
const res = await doFetch(`${base}${path}`, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
credentials: "same-origin",
|
|
114
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
115
|
+
body: JSON.stringify(form)
|
|
116
|
+
});
|
|
117
|
+
const body = await res.json().catch(() => null);
|
|
118
|
+
if (res.ok && body && typeof body.status === "string") return { ok: true, ...body };
|
|
119
|
+
const e = body?.error;
|
|
120
|
+
return { ok: false, error: e && typeof e.code === "string" ? { code: e.code, message: String(e.message ?? ""), ...e.field ? { field: e.field } : {} } : UNAVAILABLE };
|
|
121
|
+
} catch {
|
|
122
|
+
return { ok: false, error: UNAVAILABLE };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
let looked;
|
|
126
|
+
async function read(path) {
|
|
127
|
+
try {
|
|
128
|
+
const res = await doFetch(`${base}${path}`, { credentials: "same-origin", headers: { accept: "application/json" } });
|
|
129
|
+
return res.ok ? await res.json() : null;
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const socialUrl = (provider, o = {}) => `${base}/sign-in/social/${encodeURIComponent(provider)}${o.returnTo ? `?return_to=${encodeURIComponent(o.returnTo)}` : ""}`;
|
|
135
|
+
const passkeyUrl = (o = {}) => {
|
|
136
|
+
const q = new URLSearchParams({ method: "passkey" });
|
|
137
|
+
if (o.returnTo) q.set("return_to", o.returnTo);
|
|
138
|
+
if (o.locale) q.set("locale", o.locale);
|
|
139
|
+
return `${base}/login?${q}`;
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
signIn: {
|
|
143
|
+
email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
|
|
144
|
+
mfa: (f) => post("/sign-in/mfa", { code: f.code }, checkCode(f.code)),
|
|
145
|
+
magicLink: (f) => post("/sign-in/magic-link", { email: f.email }, checkEmail(f.email)),
|
|
146
|
+
magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
|
|
147
|
+
emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
|
|
148
|
+
passkeyUrl,
|
|
149
|
+
passkey(o) {
|
|
150
|
+
window.location.assign(passkeyUrl(o));
|
|
151
|
+
},
|
|
152
|
+
socialUrl,
|
|
153
|
+
social(provider, o) {
|
|
154
|
+
window.location.assign(socialUrl(provider, o));
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
signUp: {
|
|
158
|
+
email: (f) => post("/sign-up/email", { email: f.email, password: f.password, name: f.name }, firstProblem(checkName(f.name), checkEmail(f.email), checkNewPassword(f.password)))
|
|
159
|
+
},
|
|
160
|
+
forgotPassword: (f) => post("/forgot-password", { email: f.email }, checkEmail(f.email)),
|
|
161
|
+
resetPassword: (f) => post("/reset-password", { token: f.token, password: f.password }, checkNewPassword(f.password)),
|
|
162
|
+
verifyEmail: (f) => post("/verify-email", { token: f.token }, null),
|
|
163
|
+
signOut: () => post("/sign-out", {}, null),
|
|
164
|
+
account: {
|
|
165
|
+
async get() {
|
|
166
|
+
const body = await read("/me");
|
|
167
|
+
return body && typeof body.account === "object" ? body.account : null;
|
|
168
|
+
},
|
|
169
|
+
update: (f) => post("/me", { name: f.name }, checkName(f.name)),
|
|
170
|
+
changePassword: (f) => {
|
|
171
|
+
const bad = checkNewPassword(f.newPassword);
|
|
172
|
+
return post("/me/password", { password: f.password, newPassword: f.newPassword }, bad ? { ...bad, field: "newPassword" } : null);
|
|
173
|
+
},
|
|
174
|
+
mfa: {
|
|
175
|
+
setup: (f) => post("/me/mfa/setup", { password: f.password }, null),
|
|
176
|
+
verify: (f) => post("/me/mfa/verify", { code: f.code }, checkCode(f.code)),
|
|
177
|
+
disable: (f) => post("/me/mfa/disable", { password: f.password }, null),
|
|
178
|
+
recoveryCodes: (f) => post("/me/mfa/recovery-codes", { password: f.password }, null)
|
|
179
|
+
},
|
|
180
|
+
async sessions() {
|
|
181
|
+
const body = await read("/me/sessions");
|
|
182
|
+
return body && Array.isArray(body.sessions) ? body.sessions : [];
|
|
183
|
+
},
|
|
184
|
+
signOutEverywhere: () => post("/me/sessions/end", {}, null)
|
|
185
|
+
},
|
|
186
|
+
async getSession() {
|
|
187
|
+
try {
|
|
188
|
+
const res = await doFetch(`${base}/session`, { credentials: "same-origin", headers: { accept: "application/json" } });
|
|
189
|
+
if (!res.ok) return null;
|
|
190
|
+
return (await res.json()).session;
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
look() {
|
|
196
|
+
looked ??= read("/look").then((wire) => wire && typeof wire === "object" ? fromWire(wire) : null);
|
|
197
|
+
return looked;
|
|
198
|
+
},
|
|
199
|
+
async methods() {
|
|
200
|
+
try {
|
|
201
|
+
const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
|
|
202
|
+
if (res.ok) return await res.json();
|
|
203
|
+
} catch {
|
|
204
|
+
}
|
|
205
|
+
return { providers: [], signUp: true, magicLink: false, passkey: false };
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
3
209
|
|
|
4
210
|
// src/permissions.ts
|
|
5
211
|
function matches(entry, resource, action) {
|
|
@@ -14,18 +220,678 @@ function can(organization, resource, action) {
|
|
|
14
220
|
if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
|
|
15
221
|
return organization.permissions.some((p) => matches(p, resource, action));
|
|
16
222
|
}
|
|
223
|
+
function definePolicy(roles) {
|
|
224
|
+
const entry = (p) => `${p.resource}:${p.action}`;
|
|
225
|
+
const byName = new Map(roles.map((r) => [r.role, r]));
|
|
226
|
+
const split = (permission) => {
|
|
227
|
+
const i = permission.lastIndexOf(":");
|
|
228
|
+
return i < 0 ? [permission, ""] : [permission.slice(0, i), permission.slice(i + 1)];
|
|
229
|
+
};
|
|
230
|
+
return {
|
|
231
|
+
roles: roles.map((r) => ({
|
|
232
|
+
role: r.role,
|
|
233
|
+
...r.name !== void 0 ? { name: r.name } : {},
|
|
234
|
+
...r.description !== void 0 ? { description: r.description } : {},
|
|
235
|
+
...r.inherits ? { inherits: [...r.inherits] } : {},
|
|
236
|
+
...r.permissions ? { permissions: r.permissions.map((p) => ({ ...p })) } : {}
|
|
237
|
+
})),
|
|
238
|
+
roleNames: roles.map((r) => r.role),
|
|
239
|
+
permissions: [...new Set(roles.flatMap((r) => (r.permissions ?? []).map(entry)))],
|
|
240
|
+
can(organization, permission) {
|
|
241
|
+
const [resource, action] = split(permission);
|
|
242
|
+
return can(organization, resource, action);
|
|
243
|
+
},
|
|
244
|
+
hasRole(organization, ...wanted) {
|
|
245
|
+
return organization != null && wanted.includes(organization.role);
|
|
246
|
+
},
|
|
247
|
+
permissionsOf(role) {
|
|
248
|
+
const seen = /* @__PURE__ */ new Set();
|
|
249
|
+
const out = /* @__PURE__ */ new Set();
|
|
250
|
+
const walk = (name) => {
|
|
251
|
+
if (seen.has(name)) return;
|
|
252
|
+
seen.add(name);
|
|
253
|
+
const r = byName.get(name);
|
|
254
|
+
for (const p of r?.permissions ?? []) out.add(entry(p));
|
|
255
|
+
for (const parent of r?.inherits ?? []) walk(parent);
|
|
256
|
+
};
|
|
257
|
+
walk(role);
|
|
258
|
+
return [...out];
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/components.tsx
|
|
264
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
265
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
266
|
+
var TEXTS = {
|
|
267
|
+
en: {
|
|
268
|
+
email: "Email",
|
|
269
|
+
password: "Password",
|
|
270
|
+
name: "Name",
|
|
271
|
+
code: "Code",
|
|
272
|
+
signIn: "Sign in",
|
|
273
|
+
signUp: "Create account",
|
|
274
|
+
continueWith: "Continue with",
|
|
275
|
+
orWithEmail: "or with your email",
|
|
276
|
+
forgotPassword: "Forgot your password?",
|
|
277
|
+
noAccount: "No account yet?",
|
|
278
|
+
createAccount: "Create one",
|
|
279
|
+
haveAccount: "Already have an account?",
|
|
280
|
+
sendLink: "Send the link",
|
|
281
|
+
linkSent: "If that address has an account, a link to choose a new password is on its way.",
|
|
282
|
+
codeHelp: "Enter the code from your authenticator app, or a recovery code.",
|
|
283
|
+
verify: "Verify",
|
|
284
|
+
checkEmail: "Almost there: open the link we sent to confirm your address, then sign in.",
|
|
285
|
+
backToSignIn: "Back to sign in",
|
|
286
|
+
emailMeALink: "Email me a sign-in link",
|
|
287
|
+
usePasskey: "Sign in with a passkey",
|
|
288
|
+
codeFromMail: "Reading it on another device? Enter the code from the email.",
|
|
289
|
+
signInLinkSent: "If that address has an account, a link to sign in is on its way.",
|
|
290
|
+
newPassword: "New password",
|
|
291
|
+
savePassword: "Save the password",
|
|
292
|
+
passwordChanged: "Your password was changed. You can sign in with it now.",
|
|
293
|
+
confirming: "Confirming your address\u2026",
|
|
294
|
+
emailConfirmed: "Your address is confirmed. You can sign in now.",
|
|
295
|
+
profile: "Profile",
|
|
296
|
+
save: "Save",
|
|
297
|
+
saved: "Saved.",
|
|
298
|
+
currentPassword: "Current password",
|
|
299
|
+
changePassword: "Change password",
|
|
300
|
+
passwordChangedHere: "Your password was changed. Every other session of your account was signed out.",
|
|
301
|
+
secondStep: "Second step",
|
|
302
|
+
secondStepOn: "On: signing in also asks for a code from your authenticator app.",
|
|
303
|
+
secondStepOff: "Off: signing in asks only for your password.",
|
|
304
|
+
turnOn: "Turn on",
|
|
305
|
+
turnOff: "Turn off",
|
|
306
|
+
addToApp: "Add this key to your authenticator app, then enter the code it shows.",
|
|
307
|
+
openInApp: "Open in an authenticator app",
|
|
308
|
+
recoveryCodes: "Recovery codes",
|
|
309
|
+
newRecoveryCodes: "New recovery codes",
|
|
310
|
+
keepCodes: "Keep these codes somewhere safe. Each works once, and they are not shown again.",
|
|
311
|
+
passkeys: "Passkeys",
|
|
312
|
+
managePasskeys: "Add or remove a passkey",
|
|
313
|
+
sessions: "Where you are signed in",
|
|
314
|
+
signOutEverywhere: "Sign out everywhere",
|
|
315
|
+
unknownDevice: "Unknown device",
|
|
316
|
+
errors: {
|
|
317
|
+
required: "This is required.",
|
|
318
|
+
invalid_email: "That does not look like an email address.",
|
|
319
|
+
too_short: "Use at least 8 characters.",
|
|
320
|
+
invalid_credentials: "The address or the password is not right.",
|
|
321
|
+
unavailable: "Sign-in is unavailable right now. Try again in a moment.",
|
|
322
|
+
state_mismatch: "That sign-in could not be completed. Try again.",
|
|
323
|
+
cancelled: "The sign-in was cancelled.",
|
|
324
|
+
invalid_token: "This link is not valid, or it has expired. Ask for a new one.",
|
|
325
|
+
invalid_code: "That code is not right, or it has expired.",
|
|
326
|
+
wrong_password: "The current password is not right.",
|
|
327
|
+
step_up_required: "Enter your current password.",
|
|
328
|
+
weak_password: "Choose a stronger password.",
|
|
329
|
+
unauthenticated: "Sign in first."
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
es: {
|
|
333
|
+
email: "Correo",
|
|
334
|
+
password: "Contrase\xF1a",
|
|
335
|
+
name: "Nombre",
|
|
336
|
+
code: "C\xF3digo",
|
|
337
|
+
signIn: "Entrar",
|
|
338
|
+
signUp: "Crear cuenta",
|
|
339
|
+
continueWith: "Continuar con",
|
|
340
|
+
orWithEmail: "o con tu correo",
|
|
341
|
+
forgotPassword: "\xBFOlvidaste tu contrase\xF1a?",
|
|
342
|
+
noAccount: "\xBFNo tienes cuenta?",
|
|
343
|
+
createAccount: "Crear una",
|
|
344
|
+
haveAccount: "\xBFYa tienes cuenta?",
|
|
345
|
+
sendLink: "Enviar el enlace",
|
|
346
|
+
linkSent: "Si ese correo tiene una cuenta, ya va en camino un enlace para elegir una contrase\xF1a nueva.",
|
|
347
|
+
codeHelp: "Escribe el c\xF3digo de tu aplicaci\xF3n de autenticaci\xF3n, o un c\xF3digo de recuperaci\xF3n.",
|
|
348
|
+
verify: "Verificar",
|
|
349
|
+
checkEmail: "Ya casi: abre el enlace que te enviamos para confirmar tu correo y luego entra.",
|
|
350
|
+
backToSignIn: "Volver a entrar",
|
|
351
|
+
emailMeALink: "Env\xEDame un enlace para entrar",
|
|
352
|
+
usePasskey: "Entrar con una llave de acceso",
|
|
353
|
+
codeFromMail: "\xBFLo est\xE1s leyendo en otro dispositivo? Escribe el c\xF3digo que trae el correo.",
|
|
354
|
+
signInLinkSent: "Si ese correo tiene una cuenta, ya va en camino un enlace para entrar.",
|
|
355
|
+
newPassword: "Contrase\xF1a nueva",
|
|
356
|
+
savePassword: "Guardar la contrase\xF1a",
|
|
357
|
+
passwordChanged: "Tu contrase\xF1a se cambi\xF3. Ya puedes entrar con ella.",
|
|
358
|
+
confirming: "Confirmando tu correo\u2026",
|
|
359
|
+
emailConfirmed: "Tu correo qued\xF3 confirmado. Ya puedes entrar.",
|
|
360
|
+
profile: "Perfil",
|
|
361
|
+
save: "Guardar",
|
|
362
|
+
saved: "Guardado.",
|
|
363
|
+
currentPassword: "Contrase\xF1a actual",
|
|
364
|
+
changePassword: "Cambiar contrase\xF1a",
|
|
365
|
+
passwordChangedHere: "Tu contrase\xF1a se cambi\xF3. Se cerraron todas las dem\xE1s sesiones de tu cuenta.",
|
|
366
|
+
secondStep: "Segundo paso",
|
|
367
|
+
secondStepOn: "Activo: al entrar tambi\xE9n se pide un c\xF3digo de tu aplicaci\xF3n de autenticaci\xF3n.",
|
|
368
|
+
secondStepOff: "Inactivo: al entrar solo se pide tu contrase\xF1a.",
|
|
369
|
+
turnOn: "Activar",
|
|
370
|
+
turnOff: "Desactivar",
|
|
371
|
+
addToApp: "Agrega esta clave a tu aplicaci\xF3n de autenticaci\xF3n y escribe el c\xF3digo que muestra.",
|
|
372
|
+
openInApp: "Abrir en una aplicaci\xF3n de autenticaci\xF3n",
|
|
373
|
+
recoveryCodes: "C\xF3digos de recuperaci\xF3n",
|
|
374
|
+
newRecoveryCodes: "Nuevos c\xF3digos de recuperaci\xF3n",
|
|
375
|
+
keepCodes: "Guarda estos c\xF3digos en un lugar seguro. Cada uno sirve una vez y no se vuelven a mostrar.",
|
|
376
|
+
passkeys: "Llaves de acceso",
|
|
377
|
+
managePasskeys: "Agregar o quitar una llave de acceso",
|
|
378
|
+
sessions: "D\xF3nde tienes la sesi\xF3n abierta",
|
|
379
|
+
signOutEverywhere: "Cerrar sesi\xF3n en todas partes",
|
|
380
|
+
unknownDevice: "Dispositivo desconocido",
|
|
381
|
+
errors: {
|
|
382
|
+
required: "Este dato es obligatorio.",
|
|
383
|
+
invalid_email: "Eso no parece un correo.",
|
|
384
|
+
too_short: "Usa al menos 8 caracteres.",
|
|
385
|
+
invalid_credentials: "El correo o la contrase\xF1a no son correctos.",
|
|
386
|
+
unavailable: "No se puede entrar en este momento. Intenta de nuevo en un momento.",
|
|
387
|
+
state_mismatch: "No se pudo completar el ingreso. Intenta de nuevo.",
|
|
388
|
+
cancelled: "El ingreso se cancel\xF3.",
|
|
389
|
+
invalid_token: "Este enlace no es v\xE1lido o ya venci\xF3. Pide uno nuevo.",
|
|
390
|
+
invalid_code: "Ese c\xF3digo no es correcto o ya venci\xF3.",
|
|
391
|
+
wrong_password: "La contrase\xF1a actual no es correcta.",
|
|
392
|
+
step_up_required: "Escribe tu contrase\xF1a actual.",
|
|
393
|
+
weak_password: "Elige una contrase\xF1a m\xE1s fuerte.",
|
|
394
|
+
unauthenticated: "Inicia sesi\xF3n primero."
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
var PlainButton = (p) => /* @__PURE__ */ jsx("button", { ...p });
|
|
399
|
+
var PlainInput = (p) => /* @__PURE__ */ jsx("input", { ...p });
|
|
400
|
+
var looks = /* @__PURE__ */ new Map();
|
|
401
|
+
var loaded = /* @__PURE__ */ new Set();
|
|
402
|
+
function loadFonts(fonts) {
|
|
403
|
+
if (typeof document === "undefined" || typeof FontFace === "undefined") return;
|
|
404
|
+
for (const f of fonts ?? []) {
|
|
405
|
+
const key = `${f.family}|${f.url}|${f.weight ?? ""}|${f.style ?? ""}`;
|
|
406
|
+
if (loaded.has(key) || !/^[A-Za-z0-9][A-Za-z0-9 _-]*$/.test(f.family) || !/^https:\/\/[^\s"'()]+\.woff2$/i.test(f.url)) continue;
|
|
407
|
+
loaded.add(key);
|
|
408
|
+
const face = new FontFace(f.family, `url("${f.url}") format("woff2")`, { weight: String(f.weight ?? "400"), style: f.style === "italic" ? "italic" : "normal", display: "swap" });
|
|
409
|
+
void face.load().then((ready) => document.fonts.add(ready), () => loaded.delete(key));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function useRoot(props) {
|
|
413
|
+
const { basePath } = useSession();
|
|
414
|
+
const [vars, setVars] = useState({});
|
|
415
|
+
const wanted = !props.unstyled && props.workspaceLook !== false;
|
|
416
|
+
useEffect(() => {
|
|
417
|
+
if (!wanted) return;
|
|
418
|
+
let live = true;
|
|
419
|
+
let look = looks.get(basePath);
|
|
420
|
+
if (!look) {
|
|
421
|
+
look = createAuthClient({ basePath }).look();
|
|
422
|
+
looks.set(basePath, look);
|
|
423
|
+
}
|
|
424
|
+
void look.then((theme) => {
|
|
425
|
+
if (!live || !theme) return;
|
|
426
|
+
setVars(themeVariables(theme));
|
|
427
|
+
loadFonts(theme.fonts);
|
|
428
|
+
});
|
|
429
|
+
return () => {
|
|
430
|
+
live = false;
|
|
431
|
+
};
|
|
432
|
+
}, [basePath, wanted]);
|
|
433
|
+
if (props.unstyled) return {};
|
|
434
|
+
return { "data-cauthy": "", style: vars };
|
|
435
|
+
}
|
|
436
|
+
function useForm(props) {
|
|
437
|
+
const t = { ...TEXTS[props.locale ?? "en"], ...props.texts, errors: { ...TEXTS[props.locale ?? "en"].errors, ...props.texts?.errors } };
|
|
438
|
+
const cx = (slot) => props.classNames?.[slot] ?? `cauthy-${slot}`;
|
|
439
|
+
const Button = props.components?.Button ?? PlainButton;
|
|
440
|
+
const Input = props.components?.Input ?? PlainInput;
|
|
441
|
+
const [error, setError] = useState(null);
|
|
442
|
+
const [busy, setBusy] = useState(false);
|
|
443
|
+
const say = (e) => t.errors[e.code] ?? e.message;
|
|
444
|
+
const done = () => {
|
|
445
|
+
if (props.onSuccess) props.onSuccess();
|
|
446
|
+
else window.location.assign(props.returnTo ?? "/");
|
|
447
|
+
};
|
|
448
|
+
const root = useRoot(props);
|
|
449
|
+
return { t, cx, Button, Input, error, setError, busy, setBusy, say, done, root };
|
|
450
|
+
}
|
|
451
|
+
function Field(p) {
|
|
452
|
+
const id = useId();
|
|
453
|
+
const { f } = p;
|
|
454
|
+
const bad = f.error?.field === p.name ? f.error : null;
|
|
455
|
+
return /* @__PURE__ */ jsxs("div", { className: f.cx("field"), children: [
|
|
456
|
+
/* @__PURE__ */ jsxs("label", { className: f.cx("label"), htmlFor: id, children: [
|
|
457
|
+
p.label,
|
|
458
|
+
p.aside
|
|
459
|
+
] }),
|
|
460
|
+
/* @__PURE__ */ jsx(
|
|
461
|
+
f.Input,
|
|
462
|
+
{
|
|
463
|
+
id,
|
|
464
|
+
name: p.name,
|
|
465
|
+
type: p.type,
|
|
466
|
+
value: p.value,
|
|
467
|
+
autoComplete: p.autoComplete,
|
|
468
|
+
required: p.required,
|
|
469
|
+
className: f.cx("input"),
|
|
470
|
+
"aria-invalid": bad ? true : void 0,
|
|
471
|
+
"aria-describedby": bad ? `${id}-error` : void 0,
|
|
472
|
+
onChange: (e) => p.onChange(e.target.value)
|
|
473
|
+
}
|
|
474
|
+
),
|
|
475
|
+
bad && /* @__PURE__ */ jsx("span", { id: `${id}-error`, role: "alert", className: f.cx("fieldError"), children: f.say(bad) })
|
|
476
|
+
] });
|
|
477
|
+
}
|
|
478
|
+
function returnedError() {
|
|
479
|
+
if (typeof window === "undefined") return null;
|
|
480
|
+
const code = new URLSearchParams(window.location.search).get("auth_error");
|
|
481
|
+
return code ? { code, message: "That sign-in could not be completed. Try again." } : null;
|
|
482
|
+
}
|
|
483
|
+
function takeToken(name) {
|
|
484
|
+
if (typeof window === "undefined") return null;
|
|
485
|
+
const url = new URL(window.location.href);
|
|
486
|
+
const token = url.searchParams.get(name);
|
|
487
|
+
if (token === null) return null;
|
|
488
|
+
url.searchParams.delete(name);
|
|
489
|
+
window.history.replaceState(window.history.state, "", url.pathname + url.search + url.hash);
|
|
490
|
+
return token;
|
|
491
|
+
}
|
|
492
|
+
function useMailedToken(name, open) {
|
|
493
|
+
const opened = useRef(false);
|
|
494
|
+
useEffect(() => {
|
|
495
|
+
if (opened.current) return;
|
|
496
|
+
opened.current = true;
|
|
497
|
+
open(takeToken(name));
|
|
498
|
+
}, []);
|
|
499
|
+
}
|
|
500
|
+
function SignIn(props) {
|
|
501
|
+
const auth = useAuth();
|
|
502
|
+
const f = useForm(props);
|
|
503
|
+
const [methods, setMethods] = useState(null);
|
|
504
|
+
const [email, setEmail] = useState("");
|
|
505
|
+
const [password, setPassword] = useState("");
|
|
506
|
+
const [code, setCode] = useState("");
|
|
507
|
+
const [step, setStep] = useState("password");
|
|
508
|
+
useEffect(() => {
|
|
509
|
+
void auth.methods().then(setMethods);
|
|
510
|
+
const back = returnedError();
|
|
511
|
+
if (back) f.setError(back);
|
|
512
|
+
}, []);
|
|
513
|
+
useMailedToken("ml_token", (token) => {
|
|
514
|
+
if (!token) return;
|
|
515
|
+
setStep("opening");
|
|
516
|
+
void auth.signIn.magicLinkVerify({ token }).then((r) => {
|
|
517
|
+
if (r.ok) return f.done();
|
|
518
|
+
f.setError(r.error);
|
|
519
|
+
setStep("password");
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
const submit = async (e) => {
|
|
523
|
+
e.preventDefault();
|
|
524
|
+
f.setBusy(true);
|
|
525
|
+
f.setError(null);
|
|
526
|
+
const r = step === "code" ? await auth.signIn.mfa({ code }) : step === "sent" ? await auth.signIn.emailCode({ code }) : await auth.signIn.email({ email, password });
|
|
527
|
+
f.setBusy(false);
|
|
528
|
+
if (!r.ok) return f.setError(r.error);
|
|
529
|
+
if (r.status === "mfa_required") return setStep("code");
|
|
530
|
+
f.done();
|
|
531
|
+
};
|
|
532
|
+
const sendLink = async () => {
|
|
533
|
+
f.setBusy(true);
|
|
534
|
+
f.setError(null);
|
|
535
|
+
const r = await auth.signIn.magicLink({ email });
|
|
536
|
+
f.setBusy(false);
|
|
537
|
+
if (!r.ok) return f.setError(r.error);
|
|
538
|
+
setStep("sent");
|
|
539
|
+
};
|
|
540
|
+
const general = f.error && !f.error.field ? f.error : null;
|
|
541
|
+
if (step === "opening") return /* @__PURE__ */ jsx("div", { className: f.cx("form"), ...f.root, "aria-busy": "true" });
|
|
542
|
+
if (step === "sent") {
|
|
543
|
+
return /* @__PURE__ */ jsxs("form", { className: f.cx("form"), ...f.root, onSubmit: submit, noValidate: true, children: [
|
|
544
|
+
/* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.signInLinkSent }),
|
|
545
|
+
general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
546
|
+
/* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.codeFromMail }),
|
|
547
|
+
/* @__PURE__ */ jsx(Field, { f, name: "code", type: "text", label: f.t.code, value: code, autoComplete: "one-time-code", required: true, onChange: setCode }),
|
|
548
|
+
/* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.verify })
|
|
549
|
+
] });
|
|
550
|
+
}
|
|
551
|
+
if (step === "code") {
|
|
552
|
+
return /* @__PURE__ */ jsxs("form", { className: f.cx("form"), ...f.root, onSubmit: submit, noValidate: true, children: [
|
|
553
|
+
/* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.codeHelp }),
|
|
554
|
+
general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
555
|
+
/* @__PURE__ */ jsx(Field, { f, name: "code", type: "text", label: f.t.code, value: code, autoComplete: "one-time-code", required: true, onChange: setCode }),
|
|
556
|
+
/* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.verify })
|
|
557
|
+
] });
|
|
558
|
+
}
|
|
559
|
+
return /* @__PURE__ */ jsxs("form", { className: f.cx("form"), ...f.root, onSubmit: submit, noValidate: true, children: [
|
|
560
|
+
general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
561
|
+
(methods?.providers ?? []).map((p) => /* @__PURE__ */ jsxs(f.Button, { type: "button", className: f.cx("providerButton"), onClick: () => auth.signIn.social(p.id, { returnTo: props.returnTo }), children: [
|
|
562
|
+
f.t.continueWith,
|
|
563
|
+
" ",
|
|
564
|
+
p.name
|
|
565
|
+
] }, p.id)),
|
|
566
|
+
methods?.passkey && /* @__PURE__ */ jsx(f.Button, { type: "button", className: f.cx("providerButton"), onClick: () => auth.signIn.passkey({ returnTo: props.returnTo, locale: props.locale }), children: f.t.usePasskey }),
|
|
567
|
+
((methods?.providers.length ?? 0) > 0 || methods?.passkey) && /* @__PURE__ */ jsx("p", { className: f.cx("separator"), children: f.t.orWithEmail }),
|
|
568
|
+
/* @__PURE__ */ jsx(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "username", required: true, onChange: setEmail }),
|
|
569
|
+
/* @__PURE__ */ jsx(
|
|
570
|
+
Field,
|
|
571
|
+
{
|
|
572
|
+
f,
|
|
573
|
+
name: "password",
|
|
574
|
+
type: "password",
|
|
575
|
+
label: f.t.password,
|
|
576
|
+
value: password,
|
|
577
|
+
autoComplete: "current-password",
|
|
578
|
+
required: true,
|
|
579
|
+
onChange: setPassword,
|
|
580
|
+
aside: props.links?.forgotPassword ? /* @__PURE__ */ jsx("a", { className: f.cx("link"), href: props.links.forgotPassword, children: f.t.forgotPassword }) : void 0
|
|
581
|
+
}
|
|
582
|
+
),
|
|
583
|
+
/* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.signIn }),
|
|
584
|
+
methods?.magicLink && /* @__PURE__ */ jsx(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void sendLink(), children: f.t.emailMeALink }),
|
|
585
|
+
methods?.signUp !== false && props.links?.signUp && /* @__PURE__ */ jsxs("p", { className: f.cx("footer"), children: [
|
|
586
|
+
f.t.noAccount,
|
|
587
|
+
" ",
|
|
588
|
+
/* @__PURE__ */ jsx("a", { className: f.cx("link"), href: props.links.signUp, children: f.t.createAccount })
|
|
589
|
+
] })
|
|
590
|
+
] });
|
|
591
|
+
}
|
|
592
|
+
function SignUp(props) {
|
|
593
|
+
const auth = useAuth();
|
|
594
|
+
const f = useForm(props);
|
|
595
|
+
const [name, setName] = useState("");
|
|
596
|
+
const [email, setEmail] = useState("");
|
|
597
|
+
const [password, setPassword] = useState("");
|
|
598
|
+
const [sent, setSent] = useState(false);
|
|
599
|
+
const submit = async (e) => {
|
|
600
|
+
e.preventDefault();
|
|
601
|
+
f.setBusy(true);
|
|
602
|
+
f.setError(null);
|
|
603
|
+
const r = await auth.signUp.email({ name, email, password });
|
|
604
|
+
f.setBusy(false);
|
|
605
|
+
if (!r.ok) return f.setError(r.error);
|
|
606
|
+
if (r.status === "verification_required") return setSent(true);
|
|
607
|
+
f.done();
|
|
608
|
+
};
|
|
609
|
+
const general = f.error && !f.error.field ? f.error : null;
|
|
610
|
+
if (sent) {
|
|
611
|
+
return /* @__PURE__ */ jsxs("div", { className: f.cx("form"), ...f.root, children: [
|
|
612
|
+
/* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.checkEmail }),
|
|
613
|
+
props.links?.signIn && /* @__PURE__ */ jsx("a", { className: f.cx("link"), href: props.links.signIn, children: f.t.backToSignIn })
|
|
614
|
+
] });
|
|
615
|
+
}
|
|
616
|
+
return /* @__PURE__ */ jsxs("form", { className: f.cx("form"), ...f.root, onSubmit: submit, noValidate: true, children: [
|
|
617
|
+
general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
618
|
+
/* @__PURE__ */ jsx(Field, { f, name: "name", type: "text", label: f.t.name, value: name, autoComplete: "name", onChange: setName }),
|
|
619
|
+
/* @__PURE__ */ jsx(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "username", required: true, onChange: setEmail }),
|
|
620
|
+
/* @__PURE__ */ jsx(Field, { f, name: "password", type: "password", label: f.t.password, value: password, autoComplete: "new-password", required: true, onChange: setPassword }),
|
|
621
|
+
/* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.signUp }),
|
|
622
|
+
props.links?.signIn && /* @__PURE__ */ jsxs("p", { className: f.cx("footer"), children: [
|
|
623
|
+
f.t.haveAccount,
|
|
624
|
+
" ",
|
|
625
|
+
/* @__PURE__ */ jsx("a", { className: f.cx("link"), href: props.links.signIn, children: f.t.signIn })
|
|
626
|
+
] })
|
|
627
|
+
] });
|
|
628
|
+
}
|
|
629
|
+
function ForgotPassword(props) {
|
|
630
|
+
const auth = useAuth();
|
|
631
|
+
const f = useForm(props);
|
|
632
|
+
const [email, setEmail] = useState("");
|
|
633
|
+
const [sent, setSent] = useState(false);
|
|
634
|
+
const submit = async (e) => {
|
|
635
|
+
e.preventDefault();
|
|
636
|
+
f.setBusy(true);
|
|
637
|
+
f.setError(null);
|
|
638
|
+
const r = await auth.forgotPassword({ email });
|
|
639
|
+
f.setBusy(false);
|
|
640
|
+
if (!r.ok) return f.setError(r.error);
|
|
641
|
+
setSent(true);
|
|
642
|
+
};
|
|
643
|
+
const general = f.error && !f.error.field ? f.error : null;
|
|
644
|
+
if (sent) {
|
|
645
|
+
return /* @__PURE__ */ jsxs("div", { className: f.cx("form"), ...f.root, children: [
|
|
646
|
+
/* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.linkSent }),
|
|
647
|
+
props.links?.signIn && /* @__PURE__ */ jsx("a", { className: f.cx("link"), href: props.links.signIn, children: f.t.backToSignIn })
|
|
648
|
+
] });
|
|
649
|
+
}
|
|
650
|
+
return /* @__PURE__ */ jsxs("form", { className: f.cx("form"), ...f.root, onSubmit: submit, noValidate: true, children: [
|
|
651
|
+
general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
652
|
+
/* @__PURE__ */ jsx(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "username", required: true, onChange: setEmail }),
|
|
653
|
+
/* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.sendLink }),
|
|
654
|
+
props.links?.signIn && /* @__PURE__ */ jsx("p", { className: f.cx("footer"), children: /* @__PURE__ */ jsx("a", { className: f.cx("link"), href: props.links.signIn, children: f.t.backToSignIn }) })
|
|
655
|
+
] });
|
|
656
|
+
}
|
|
657
|
+
function ResetPassword(props) {
|
|
658
|
+
const auth = useAuth();
|
|
659
|
+
const f = useForm(props);
|
|
660
|
+
const [token, setToken] = useState(null);
|
|
661
|
+
const [password, setPassword] = useState("");
|
|
662
|
+
const [changed, setChanged] = useState(false);
|
|
663
|
+
useMailedToken("token", (t) => {
|
|
664
|
+
setToken(t ?? "");
|
|
665
|
+
if (!t) f.setError({ code: "invalid_token", message: "This link is not valid, or it has expired." });
|
|
666
|
+
});
|
|
667
|
+
const submit = async (e) => {
|
|
668
|
+
e.preventDefault();
|
|
669
|
+
f.setBusy(true);
|
|
670
|
+
f.setError(null);
|
|
671
|
+
const r = await auth.resetPassword({ token: token ?? "", password });
|
|
672
|
+
f.setBusy(false);
|
|
673
|
+
if (!r.ok) return f.setError(r.error);
|
|
674
|
+
setChanged(true);
|
|
675
|
+
};
|
|
676
|
+
const general = f.error && !f.error.field ? f.error : null;
|
|
677
|
+
const back = props.links?.signIn && /* @__PURE__ */ jsx("a", { className: f.cx("link"), href: props.links.signIn, children: f.t.backToSignIn });
|
|
678
|
+
if (changed) {
|
|
679
|
+
return /* @__PURE__ */ jsxs("div", { className: f.cx("form"), ...f.root, children: [
|
|
680
|
+
/* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.passwordChanged }),
|
|
681
|
+
back
|
|
682
|
+
] });
|
|
683
|
+
}
|
|
684
|
+
if (token === "") {
|
|
685
|
+
return /* @__PURE__ */ jsxs("div", { className: f.cx("form"), ...f.root, children: [
|
|
686
|
+
general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
687
|
+
props.links?.forgotPassword && /* @__PURE__ */ jsx("a", { className: f.cx("link"), href: props.links.forgotPassword, children: f.t.forgotPassword })
|
|
688
|
+
] });
|
|
689
|
+
}
|
|
690
|
+
return /* @__PURE__ */ jsxs("form", { className: f.cx("form"), ...f.root, onSubmit: submit, noValidate: true, children: [
|
|
691
|
+
general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
692
|
+
/* @__PURE__ */ jsx(Field, { f, name: "password", type: "password", label: f.t.newPassword, value: password, autoComplete: "new-password", required: true, onChange: setPassword }),
|
|
693
|
+
/* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy || token === null, className: f.cx("button"), children: f.t.savePassword })
|
|
694
|
+
] });
|
|
695
|
+
}
|
|
696
|
+
function VerifyEmail(props) {
|
|
697
|
+
const auth = useAuth();
|
|
698
|
+
const f = useForm(props);
|
|
699
|
+
const [state, setState] = useState("confirming");
|
|
700
|
+
useMailedToken("token", (token) => {
|
|
701
|
+
if (!token) {
|
|
702
|
+
f.setError({ code: "invalid_token", message: "This link is not valid, or it has expired." });
|
|
703
|
+
return setState("failed");
|
|
704
|
+
}
|
|
705
|
+
void auth.verifyEmail({ token }).then((r) => {
|
|
706
|
+
if (r.ok) return setState("confirmed");
|
|
707
|
+
f.setError(r.error);
|
|
708
|
+
setState("failed");
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
return /* @__PURE__ */ jsxs("div", { className: f.cx("form"), ...f.root, "aria-busy": state === "confirming", children: [
|
|
712
|
+
state === "confirming" && /* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.confirming }),
|
|
713
|
+
state === "confirmed" && /* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.emailConfirmed }),
|
|
714
|
+
state === "failed" && f.error && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(f.error) }),
|
|
715
|
+
state !== "confirming" && props.links?.signIn && /* @__PURE__ */ jsx("a", { className: f.cx("link"), href: props.links.signIn, children: f.t.backToSignIn })
|
|
716
|
+
] });
|
|
717
|
+
}
|
|
718
|
+
function useSection(props) {
|
|
719
|
+
const f = useForm({ ...props, unstyled: true });
|
|
720
|
+
const [done, setDone] = useState(null);
|
|
721
|
+
const run = async (action) => {
|
|
722
|
+
f.setBusy(true);
|
|
723
|
+
f.setError(null);
|
|
724
|
+
setDone(null);
|
|
725
|
+
const r = await action();
|
|
726
|
+
f.setBusy(false);
|
|
727
|
+
if (!r.ok) f.setError(r.error);
|
|
728
|
+
return r;
|
|
729
|
+
};
|
|
730
|
+
const general = f.error && !f.error.field ? f.error : null;
|
|
731
|
+
return { f, done, setDone, run, general };
|
|
732
|
+
}
|
|
733
|
+
function Account(props) {
|
|
734
|
+
const auth = useAuth();
|
|
735
|
+
const { accountUrl } = useSession();
|
|
736
|
+
const { t, root } = useForm(props);
|
|
737
|
+
const cx = (slot) => props.classNames?.[slot] ?? `cauthy-${slot}`;
|
|
738
|
+
const [account, setAccount] = useState(null);
|
|
739
|
+
const [sessions, setSessions] = useState([]);
|
|
740
|
+
useEffect(() => {
|
|
741
|
+
void auth.account.get().then(setAccount);
|
|
742
|
+
void auth.account.sessions().then(setSessions);
|
|
743
|
+
}, []);
|
|
744
|
+
if (account === null) return /* @__PURE__ */ jsx("div", { className: cx("account"), ...root, "aria-busy": "true" });
|
|
745
|
+
const when = props.formatDate ?? ((iso) => new Date(iso).toLocaleString());
|
|
746
|
+
return /* @__PURE__ */ jsxs("div", { className: cx("account"), ...root, children: [
|
|
747
|
+
/* @__PURE__ */ jsx(ProfilePart, { props, account, onSaved: setAccount }),
|
|
748
|
+
account.hasPassword && /* @__PURE__ */ jsx(PasswordPart, { props }),
|
|
749
|
+
/* @__PURE__ */ jsx(SecondStepPart, { props, account, onChanged: (mfaEnabled) => setAccount({ ...account, mfaEnabled }) }),
|
|
750
|
+
/* @__PURE__ */ jsxs("section", { className: cx("section"), children: [
|
|
751
|
+
/* @__PURE__ */ jsx("h2", { className: cx("heading"), children: t.passkeys }),
|
|
752
|
+
/* @__PURE__ */ jsx("a", { className: cx("link"), href: accountUrl(), children: t.managePasskeys })
|
|
753
|
+
] }),
|
|
754
|
+
/* @__PURE__ */ jsxs("section", { className: cx("section"), children: [
|
|
755
|
+
/* @__PURE__ */ jsx("h2", { className: cx("heading"), children: t.sessions }),
|
|
756
|
+
/* @__PURE__ */ jsx("ul", { className: cx("sessions"), children: sessions.map((s, i) => /* @__PURE__ */ jsxs("li", { className: cx("session"), children: [
|
|
757
|
+
s.device || t.unknownDevice,
|
|
758
|
+
" \xB7 ",
|
|
759
|
+
s.ip,
|
|
760
|
+
" \xB7 ",
|
|
761
|
+
when(s.createdAt)
|
|
762
|
+
] }, i)) }),
|
|
763
|
+
/* @__PURE__ */ jsx(SignOutEverywhere, { props })
|
|
764
|
+
] })
|
|
765
|
+
] });
|
|
766
|
+
}
|
|
767
|
+
function ProfilePart({ props, account, onSaved }) {
|
|
768
|
+
const auth = useAuth();
|
|
769
|
+
const { f, done, setDone, run, general } = useSection(props);
|
|
770
|
+
const [name, setName] = useState(account.name);
|
|
771
|
+
const submit = async (e) => {
|
|
772
|
+
e.preventDefault();
|
|
773
|
+
const r = await run(() => auth.account.update({ name }));
|
|
774
|
+
if (!r.ok) return;
|
|
775
|
+
onSaved(r.account);
|
|
776
|
+
setDone(f.t.saved);
|
|
777
|
+
};
|
|
778
|
+
return /* @__PURE__ */ jsxs("form", { className: f.cx("section"), onSubmit: submit, noValidate: true, children: [
|
|
779
|
+
/* @__PURE__ */ jsx("h2", { className: f.cx("heading"), children: f.t.profile }),
|
|
780
|
+
/* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: account.email }),
|
|
781
|
+
general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
782
|
+
/* @__PURE__ */ jsx(Field, { f, name: "name", type: "text", label: f.t.name, value: name, autoComplete: "name", onChange: setName }),
|
|
783
|
+
/* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.save }),
|
|
784
|
+
done && /* @__PURE__ */ jsx("p", { role: "status", className: f.cx("notice"), children: done })
|
|
785
|
+
] });
|
|
786
|
+
}
|
|
787
|
+
function PasswordPart({ props }) {
|
|
788
|
+
const auth = useAuth();
|
|
789
|
+
const { f, done, setDone, run, general } = useSection(props);
|
|
790
|
+
const [password, setPassword] = useState("");
|
|
791
|
+
const [newPassword, setNewPassword] = useState("");
|
|
792
|
+
const submit = async (e) => {
|
|
793
|
+
e.preventDefault();
|
|
794
|
+
const r = await run(() => auth.account.changePassword({ password, newPassword }));
|
|
795
|
+
if (!r.ok) return;
|
|
796
|
+
setPassword("");
|
|
797
|
+
setNewPassword("");
|
|
798
|
+
setDone(f.t.passwordChangedHere);
|
|
799
|
+
};
|
|
800
|
+
return /* @__PURE__ */ jsxs("form", { className: f.cx("section"), onSubmit: submit, noValidate: true, children: [
|
|
801
|
+
/* @__PURE__ */ jsx("h2", { className: f.cx("heading"), children: f.t.changePassword }),
|
|
802
|
+
general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
803
|
+
/* @__PURE__ */ jsx(Field, { f, name: "password", type: "password", label: f.t.currentPassword, value: password, autoComplete: "current-password", required: true, onChange: setPassword }),
|
|
804
|
+
/* @__PURE__ */ jsx(Field, { f, name: "newPassword", type: "password", label: f.t.newPassword, value: newPassword, autoComplete: "new-password", required: true, onChange: setNewPassword }),
|
|
805
|
+
/* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.changePassword }),
|
|
806
|
+
done && /* @__PURE__ */ jsx("p", { role: "status", className: f.cx("notice"), children: done })
|
|
807
|
+
] });
|
|
808
|
+
}
|
|
809
|
+
function SecondStepPart({ props, account, onChanged }) {
|
|
810
|
+
const auth = useAuth();
|
|
811
|
+
const { f, run, general } = useSection(props);
|
|
812
|
+
const [password, setPassword] = useState("");
|
|
813
|
+
const [code, setCode] = useState("");
|
|
814
|
+
const [setup, setSetup] = useState(null);
|
|
815
|
+
const [codes, setCodes] = useState(null);
|
|
816
|
+
const start = async (e) => {
|
|
817
|
+
e.preventDefault();
|
|
818
|
+
const r = await run(() => auth.account.mfa.setup({ password }));
|
|
819
|
+
if (r.ok) setSetup({ secret: r.secret, otpauthUri: r.otpauthUri });
|
|
820
|
+
};
|
|
821
|
+
const confirm = async (e) => {
|
|
822
|
+
e.preventDefault();
|
|
823
|
+
const r = await run(() => auth.account.mfa.verify({ code }));
|
|
824
|
+
if (!r.ok) return;
|
|
825
|
+
setSetup(null);
|
|
826
|
+
setCode("");
|
|
827
|
+
setPassword("");
|
|
828
|
+
setCodes(r.recoveryCodes);
|
|
829
|
+
onChanged(true);
|
|
830
|
+
};
|
|
831
|
+
const turnOff = async (e) => {
|
|
832
|
+
e.preventDefault();
|
|
833
|
+
const r = await run(() => auth.account.mfa.disable({ password }));
|
|
834
|
+
if (!r.ok) return;
|
|
835
|
+
setPassword("");
|
|
836
|
+
setCodes(null);
|
|
837
|
+
onChanged(false);
|
|
838
|
+
};
|
|
839
|
+
const renew = async () => {
|
|
840
|
+
const r = await run(() => auth.account.mfa.recoveryCodes({ password }));
|
|
841
|
+
if (r.ok) setCodes(r.recoveryCodes);
|
|
842
|
+
};
|
|
843
|
+
const passwordField = account.hasPassword && /* @__PURE__ */ jsx(Field, { f, name: "password", type: "password", label: f.t.currentPassword, value: password, autoComplete: "current-password", required: true, onChange: setPassword });
|
|
844
|
+
const shown = codes && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
845
|
+
/* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.keepCodes }),
|
|
846
|
+
/* @__PURE__ */ jsx("ul", { className: f.cx("codes"), children: codes.map((c) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("code", { children: c }) }, c)) })
|
|
847
|
+
] });
|
|
848
|
+
if (setup) {
|
|
849
|
+
return /* @__PURE__ */ jsxs("form", { className: f.cx("section"), onSubmit: confirm, noValidate: true, children: [
|
|
850
|
+
/* @__PURE__ */ jsx("h2", { className: f.cx("heading"), children: f.t.secondStep }),
|
|
851
|
+
/* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.addToApp }),
|
|
852
|
+
/* @__PURE__ */ jsx("code", { className: f.cx("secret"), children: setup.secret }),
|
|
853
|
+
/* @__PURE__ */ jsx("a", { className: f.cx("link"), href: setup.otpauthUri, children: f.t.openInApp }),
|
|
854
|
+
general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
855
|
+
/* @__PURE__ */ jsx(Field, { f, name: "code", type: "text", label: f.t.code, value: code, autoComplete: "one-time-code", required: true, onChange: setCode }),
|
|
856
|
+
/* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.verify })
|
|
857
|
+
] });
|
|
858
|
+
}
|
|
859
|
+
return /* @__PURE__ */ jsxs("form", { className: f.cx("section"), onSubmit: account.mfaEnabled ? turnOff : start, noValidate: true, children: [
|
|
860
|
+
/* @__PURE__ */ jsx("h2", { className: f.cx("heading"), children: f.t.secondStep }),
|
|
861
|
+
/* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: account.mfaEnabled ? f.t.secondStepOn : f.t.secondStepOff }),
|
|
862
|
+
shown,
|
|
863
|
+
general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
864
|
+
passwordField,
|
|
865
|
+
/* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: account.mfaEnabled ? f.t.turnOff : f.t.turnOn }),
|
|
866
|
+
account.mfaEnabled && /* @__PURE__ */ jsx(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void renew(), children: f.t.newRecoveryCodes })
|
|
867
|
+
] });
|
|
868
|
+
}
|
|
869
|
+
function SignOutEverywhere({ props }) {
|
|
870
|
+
const auth = useAuth();
|
|
871
|
+
const { f, run, general } = useSection(props);
|
|
872
|
+
const end = async () => {
|
|
873
|
+
const r = await run(() => auth.account.signOutEverywhere());
|
|
874
|
+
if (!r.ok) return;
|
|
875
|
+
if (props.onSignedOut) props.onSignedOut();
|
|
876
|
+
else window.location.assign("/");
|
|
877
|
+
};
|
|
878
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
879
|
+
general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
880
|
+
/* @__PURE__ */ jsx(f.Button, { type: "button", disabled: f.busy, className: f.cx("button"), onClick: () => void end(), children: f.t.signOutEverywhere })
|
|
881
|
+
] });
|
|
882
|
+
}
|
|
17
883
|
|
|
18
884
|
// src/react.tsx
|
|
19
|
-
import { jsx } from "react/jsx-runtime";
|
|
885
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
20
886
|
var Ctx = createContext(null);
|
|
21
887
|
var AHEAD_MS = 6e4;
|
|
22
888
|
function CauthyProvider({ children, basePath = "/api/auth", initialSession }) {
|
|
23
889
|
const base = basePath.replace(/\/+$/, "");
|
|
24
|
-
const [session, setSession] =
|
|
25
|
-
const [status, setStatus] =
|
|
890
|
+
const [session, setSession] = useState2(initialSession ?? null);
|
|
891
|
+
const [status, setStatus] = useState2(
|
|
26
892
|
initialSession === void 0 ? "loading" : initialSession === null ? "unauthenticated" : "authenticated"
|
|
27
893
|
);
|
|
28
|
-
const asking =
|
|
894
|
+
const asking = useRef2(null);
|
|
29
895
|
const refresh = useCallback(() => {
|
|
30
896
|
asking.current ??= (async () => {
|
|
31
897
|
try {
|
|
@@ -46,16 +912,16 @@ function CauthyProvider({ children, basePath = "/api/auth", initialSession }) {
|
|
|
46
912
|
})();
|
|
47
913
|
return asking.current;
|
|
48
914
|
}, [base]);
|
|
49
|
-
|
|
915
|
+
useEffect2(() => {
|
|
50
916
|
if (initialSession === void 0) void refresh();
|
|
51
917
|
}, [initialSession, refresh]);
|
|
52
|
-
|
|
918
|
+
useEffect2(() => {
|
|
53
919
|
if (!session) return;
|
|
54
920
|
const wait = Math.max(new Date(session.expiresAt).getTime() - Date.now() - AHEAD_MS, 5e3);
|
|
55
921
|
const timer = setTimeout(() => void refresh(), wait);
|
|
56
922
|
return () => clearTimeout(timer);
|
|
57
923
|
}, [session, refresh]);
|
|
58
|
-
|
|
924
|
+
useEffect2(() => {
|
|
59
925
|
const onVisible = () => {
|
|
60
926
|
if (document.visibilityState === "visible") void refresh();
|
|
61
927
|
};
|
|
@@ -79,20 +945,57 @@ function CauthyProvider({ children, basePath = "/api/auth", initialSession }) {
|
|
|
79
945
|
accountUrl(returnTo) {
|
|
80
946
|
const back = returnTo ?? (typeof window === "undefined" ? "/" : window.location.pathname + window.location.search);
|
|
81
947
|
return `${base}/account?return_to=${encodeURIComponent(back)}`;
|
|
82
|
-
}
|
|
948
|
+
},
|
|
949
|
+
basePath: base
|
|
83
950
|
}),
|
|
84
951
|
[session, status, refresh, base]
|
|
85
952
|
);
|
|
86
|
-
return /* @__PURE__ */
|
|
953
|
+
return /* @__PURE__ */ jsx2(Ctx.Provider, { value, children });
|
|
87
954
|
}
|
|
88
955
|
function useSession() {
|
|
89
956
|
const ctx = useContext(Ctx);
|
|
90
957
|
if (ctx === null) throw new Error("cauthy-auth: useSession needs <CauthyProvider> above it");
|
|
91
958
|
return ctx;
|
|
92
959
|
}
|
|
960
|
+
function useAuth() {
|
|
961
|
+
const { session, status, refresh, basePath } = useSession();
|
|
962
|
+
return useMemo(() => {
|
|
963
|
+
const client = createAuthClient({ basePath });
|
|
964
|
+
const then = async (running) => {
|
|
965
|
+
const r = await running;
|
|
966
|
+
await refresh();
|
|
967
|
+
return r;
|
|
968
|
+
};
|
|
969
|
+
return {
|
|
970
|
+
...client,
|
|
971
|
+
signIn: { ...client.signIn, email: (f) => then(client.signIn.email(f)), mfa: (f) => then(client.signIn.mfa(f)), magicLinkVerify: (f) => then(client.signIn.magicLinkVerify(f)), emailCode: (f) => then(client.signIn.emailCode(f)) },
|
|
972
|
+
signUp: { email: (f) => then(client.signUp.email(f)) },
|
|
973
|
+
signOut: () => then(client.signOut()),
|
|
974
|
+
account: {
|
|
975
|
+
...client.account,
|
|
976
|
+
// What is on screen says the new name, and knows when the session ended.
|
|
977
|
+
update: (f) => then(client.account.update(f)),
|
|
978
|
+
changePassword: (f) => then(client.account.changePassword(f)),
|
|
979
|
+
signOutEverywhere: () => then(client.account.signOutEverywhere())
|
|
980
|
+
},
|
|
981
|
+
session,
|
|
982
|
+
status,
|
|
983
|
+
refresh
|
|
984
|
+
};
|
|
985
|
+
}, [basePath, refresh, session, status]);
|
|
986
|
+
}
|
|
93
987
|
export {
|
|
988
|
+
Account,
|
|
94
989
|
CauthyProvider,
|
|
990
|
+
ForgotPassword,
|
|
991
|
+
ResetPassword,
|
|
992
|
+
SignIn,
|
|
993
|
+
SignUp,
|
|
994
|
+
VerifyEmail,
|
|
95
995
|
can,
|
|
996
|
+
createAuthClient,
|
|
997
|
+
definePolicy,
|
|
998
|
+
useAuth,
|
|
96
999
|
useSession
|
|
97
1000
|
};
|
|
98
1001
|
//# sourceMappingURL=react.mjs.map
|