cauthy-auth 0.8.0 → 0.10.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/dist/react.mjs CHANGED
@@ -105,13 +105,17 @@ var UNAVAILABLE = { code: "unavailable", message: "Sign-in is unavailable right
105
105
  function createAuthClient(options = {}) {
106
106
  const base = (options.basePath ?? "/api/auth").replace(/\/+$/, "");
107
107
  const doFetch = (...args) => (options.fetch ?? fetch)(...args);
108
+ const locale = () => {
109
+ const said = typeof options.locale === "function" ? options.locale() : options.locale;
110
+ return (said ?? (typeof document === "undefined" ? "" : document.documentElement?.lang) ?? "").trim();
111
+ };
108
112
  async function post(path, form, bad) {
109
113
  if (bad) return { ok: false, error: { code: bad.code, message: bad.message, field: bad.field } };
110
114
  try {
111
115
  const res = await doFetch(`${base}${path}`, {
112
116
  method: "POST",
113
117
  credentials: "same-origin",
114
- headers: { "content-type": "application/json", accept: "application/json" },
118
+ headers: { "content-type": "application/json", accept: "application/json", ...locale() ? { "x-cauthy-locale": locale() } : {} },
115
119
  body: JSON.stringify(form)
116
120
  });
117
121
  const body = await res.json().catch(() => null);
@@ -131,6 +135,7 @@ function createAuthClient(options = {}) {
131
135
  return null;
132
136
  }
133
137
  }
138
+ const linkUrl = (provider, o = {}) => `${base}/me/identities/link/${encodeURIComponent(provider)}${o.returnTo ? `?return_to=${encodeURIComponent(o.returnTo)}` : ""}`;
134
139
  const socialUrl = (provider, o = {}) => `${base}/sign-in/social/${encodeURIComponent(provider)}${o.returnTo ? `?return_to=${encodeURIComponent(o.returnTo)}` : ""}`;
135
140
  const passkeyUrl = (o = {}) => {
136
141
  const q = new URLSearchParams({ method: "passkey" });
@@ -177,6 +182,16 @@ function createAuthClient(options = {}) {
177
182
  disable: (f) => post("/me/mfa/disable", { password: f.password }, null),
178
183
  recoveryCodes: (f) => post("/me/mfa/recovery-codes", { password: f.password }, null)
179
184
  },
185
+ changeEmail: (f) => post("/me/email", { email: f.email, password: f.password }, checkEmail(f.email)),
186
+ async identities() {
187
+ const body = await read("/me/identities");
188
+ return body && Array.isArray(body.identities) ? body.identities : [];
189
+ },
190
+ linkUrl,
191
+ link(provider, o) {
192
+ window.location.assign(linkUrl(provider, o));
193
+ },
194
+ unlink: (provider) => post("/me/identities/unlink", { provider }, null),
180
195
  async sessions() {
181
196
  const body = await read("/me/sessions");
182
197
  return body && Array.isArray(body.sessions) ? body.sessions : [];
@@ -313,6 +328,15 @@ var TEXTS = {
313
328
  sessions: "Where you are signed in",
314
329
  signOutEverywhere: "Sign out everywhere",
315
330
  unknownDevice: "Unknown device",
331
+ emailAddress: "Email address",
332
+ newEmail: "New email address",
333
+ changeEmail: "Change email",
334
+ emailSent: "We sent a link to {email}. Your address changes once you open it.",
335
+ linkedAccounts: "Linked accounts",
336
+ noLinkedAccounts: "No account is linked.",
337
+ linkWith: "Link",
338
+ unlink: "Unlink",
339
+ linked: "The account was linked.",
316
340
  errors: {
317
341
  required: "This is required.",
318
342
  invalid_email: "That does not look like an email address.",
@@ -321,6 +345,11 @@ var TEXTS = {
321
345
  unavailable: "Sign-in is unavailable right now. Try again in a moment.",
322
346
  state_mismatch: "That sign-in could not be completed. Try again.",
323
347
  cancelled: "The sign-in was cancelled.",
348
+ email_taken: "That address already has an account.",
349
+ email_unchanged: "That is already your address.",
350
+ last_sign_in_method: "Add a password or link another account first: it is your only way to sign in.",
351
+ identity_already_linked: "That account is linked to someone else.",
352
+ manual_linking_disabled: "Linking accounts is turned off for this workspace.",
324
353
  invalid_token: "This link is not valid, or it has expired. Ask for a new one.",
325
354
  invalid_code: "That code is not right, or it has expired.",
326
355
  wrong_password: "The current password is not right.",
@@ -378,6 +407,15 @@ var TEXTS = {
378
407
  sessions: "D\xF3nde tienes la sesi\xF3n abierta",
379
408
  signOutEverywhere: "Cerrar sesi\xF3n en todas partes",
380
409
  unknownDevice: "Dispositivo desconocido",
410
+ emailAddress: "Correo",
411
+ newEmail: "Correo nuevo",
412
+ changeEmail: "Cambiar el correo",
413
+ emailSent: "Enviamos un enlace a {email}. Tu correo cambia cuando lo abras.",
414
+ linkedAccounts: "Cuentas vinculadas",
415
+ noLinkedAccounts: "No hay cuentas vinculadas.",
416
+ linkWith: "Vincular",
417
+ unlink: "Desvincular",
418
+ linked: "La cuenta qued\xF3 vinculada.",
381
419
  errors: {
382
420
  required: "Este dato es obligatorio.",
383
421
  invalid_email: "Eso no parece un correo.",
@@ -386,6 +424,11 @@ var TEXTS = {
386
424
  unavailable: "No se puede entrar en este momento. Intenta de nuevo en un momento.",
387
425
  state_mismatch: "No se pudo completar el ingreso. Intenta de nuevo.",
388
426
  cancelled: "El ingreso se cancel\xF3.",
427
+ email_taken: "Ese correo ya tiene una cuenta.",
428
+ email_unchanged: "Ese ya es tu correo.",
429
+ last_sign_in_method: "Primero agrega una contrase\xF1a o vincula otra cuenta: es tu \xFAnica forma de entrar.",
430
+ identity_already_linked: "Esa cuenta est\xE1 vinculada a otra persona.",
431
+ manual_linking_disabled: "Vincular cuentas est\xE1 desactivado en este workspace.",
389
432
  invalid_token: "Este enlace no es v\xE1lido o ya venci\xF3. Pide uno nuevo.",
390
433
  invalid_code: "Ese c\xF3digo no es correcto o ya venci\xF3.",
391
434
  wrong_password: "La contrase\xF1a actual no es correcta.",
@@ -745,7 +788,9 @@ function Account(props) {
745
788
  const when = props.formatDate ?? ((iso) => new Date(iso).toLocaleString());
746
789
  return /* @__PURE__ */ jsxs("div", { className: cx("account"), ...root, children: [
747
790
  /* @__PURE__ */ jsx(ProfilePart, { props, account, onSaved: setAccount }),
791
+ /* @__PURE__ */ jsx(EmailPart, { props, account }),
748
792
  account.hasPassword && /* @__PURE__ */ jsx(PasswordPart, { props }),
793
+ /* @__PURE__ */ jsx(LinkedPart, { props }),
749
794
  /* @__PURE__ */ jsx(SecondStepPart, { props, account, onChanged: (mfaEnabled) => setAccount({ ...account, mfaEnabled }) }),
750
795
  /* @__PURE__ */ jsxs("section", { className: cx("section"), children: [
751
796
  /* @__PURE__ */ jsx("h2", { className: cx("heading"), children: t.passkeys }),
@@ -784,6 +829,65 @@ function ProfilePart({ props, account, onSaved }) {
784
829
  done && /* @__PURE__ */ jsx("p", { role: "status", className: f.cx("notice"), children: done })
785
830
  ] });
786
831
  }
832
+ function EmailPart({ props, account }) {
833
+ const auth = useAuth();
834
+ const { f, done, setDone, run, general } = useSection(props);
835
+ const [email, setEmail] = useState("");
836
+ const [password, setPassword] = useState("");
837
+ const submit = async (e) => {
838
+ e.preventDefault();
839
+ const r = await run(() => auth.account.changeEmail({ email, password }));
840
+ if (!r.ok) return;
841
+ setPassword("");
842
+ setDone(f.t.emailSent.replace("{email}", r.email));
843
+ };
844
+ return /* @__PURE__ */ jsxs("form", { className: f.cx("section"), onSubmit: submit, noValidate: true, children: [
845
+ /* @__PURE__ */ jsx("h2", { className: f.cx("heading"), children: f.t.emailAddress }),
846
+ /* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: account.email }),
847
+ general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
848
+ /* @__PURE__ */ jsx(Field, { f, name: "email", type: "email", label: f.t.newEmail, value: email, autoComplete: "email", required: true, onChange: setEmail }),
849
+ account.hasPassword && /* @__PURE__ */ jsx(Field, { f, name: "password", type: "password", label: f.t.currentPassword, value: password, autoComplete: "current-password", required: true, onChange: setPassword }),
850
+ /* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.changeEmail }),
851
+ done && /* @__PURE__ */ jsx("p", { role: "status", className: f.cx("notice"), children: done })
852
+ ] });
853
+ }
854
+ function LinkedPart({ props }) {
855
+ const auth = useAuth();
856
+ const { f, run, general } = useSection(props);
857
+ const [linked, setLinked] = useState(null);
858
+ const [offered, setOffered] = useState([]);
859
+ const [came, setCame] = useState(null);
860
+ useEffect(() => {
861
+ void auth.account.identities().then(setLinked);
862
+ void auth.methods().then((m) => setOffered(m.providers));
863
+ const back = typeof window === "undefined" ? null : new URLSearchParams(window.location.search);
864
+ if (back?.get("linked")) setCame(back.get("linked"));
865
+ else if (back?.get("auth_error")) f.setError({ code: back.get("auth_error"), message: "That account could not be linked. Try again." });
866
+ }, []);
867
+ if (linked === null) return null;
868
+ const name = (id) => offered.find((p) => p.id === id)?.name ?? id;
869
+ const unlink = async (provider) => {
870
+ const r = await run(() => auth.account.unlink(provider));
871
+ if (r.ok) setLinked(linked.filter((x) => x.provider !== provider));
872
+ };
873
+ const here = typeof window === "undefined" ? void 0 : window.location.pathname;
874
+ return /* @__PURE__ */ jsxs("section", { className: f.cx("section"), children: [
875
+ /* @__PURE__ */ jsx("h2", { className: f.cx("heading"), children: f.t.linkedAccounts }),
876
+ came && /* @__PURE__ */ jsx("p", { role: "status", className: f.cx("notice"), children: f.t.linked }),
877
+ general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
878
+ linked.length === 0 ? /* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.noLinkedAccounts }) : /* @__PURE__ */ jsx("ul", { className: f.cx("sessions"), children: linked.map((x) => /* @__PURE__ */ jsxs("li", { className: f.cx("session"), children: [
879
+ name(x.provider),
880
+ x.email ? ` \xB7 ${x.email}` : "",
881
+ " ",
882
+ /* @__PURE__ */ jsx(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void unlink(x.provider), children: f.t.unlink })
883
+ ] }, x.provider)) }),
884
+ offered.filter((p) => !linked.some((x) => x.provider === p.id)).map((p) => /* @__PURE__ */ jsxs(f.Button, { type: "button", className: f.cx("providerButton"), onClick: () => auth.account.link(p.id, { returnTo: here }), children: [
885
+ f.t.linkWith,
886
+ " ",
887
+ p.name
888
+ ] }, p.id))
889
+ ] });
890
+ }
787
891
  function PasswordPart({ props }) {
788
892
  const auth = useAuth();
789
893
  const { f, done, setDone, run, general } = useSection(props);