@tribe-nest/forge 3.20.0 → 3.21.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tribe-nest/forge",
3
- "version": "3.20.0",
3
+ "version": "3.21.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -69,13 +69,26 @@ export function useUpdateAccount() {
69
69
  });
70
70
  }
71
71
 
72
- /** Change the member's password. */
72
+ /**
73
+ * Change the member's password.
74
+ *
75
+ * Tenant-scoped (`/public/sessions/password`), because that is where a fan's
76
+ * credential lives — see the note at the top of `useAuthActions.ts`. The global
77
+ * `/public/accounts/password` this used to call wrote a hash that login never
78
+ * reads: it returned 200, invalidated the session, and left the old password
79
+ * working.
80
+ *
81
+ * Requires an association-scoped session (the endpoint is behind
82
+ * `requireAssociation`). Every current login path mints one; a member still
83
+ * holding a pre-cutover account-scoped token gets a 401 and needs to sign in
84
+ * again — honest, where the old call was silently a no-op.
85
+ */
73
86
  export function useChangePassword() {
74
87
  const { client } = useForge();
75
88
 
76
89
  return useMutation<unknown, unknown, { currentPassword: string; newPassword: string }>({
77
90
  mutationFn: async (body) => {
78
- const res = await client.put("/public/accounts/password", body);
91
+ const res = await client.put("/public/sessions/password", body);
79
92
  return res.data;
80
93
  },
81
94
  });
@@ -2,26 +2,70 @@ import { useForge } from "../../provider/ForgeProvider";
2
2
  import { useMutation } from "@tanstack/react-query";
3
3
 
4
4
  // Stateless account actions (no session state) — kept out of PublicAuthContext.
5
+ //
6
+ // ## Why these hit `/public/sessions/*` and not `/public/accounts/*`
7
+ //
8
+ // A member's credential for a tenant lives on their `account_associations` row,
9
+ // not on `accounts` — that is the hash `POST /public/sessions` checks at login
10
+ // (see `resolveAssociationLogin` on the backend). `accounts.password` is the
11
+ // CREATOR/admin-dashboard credential; for a fan it is at most a lazy-heal
12
+ // fallback, gated on the account having some other relationship to the profile.
13
+ //
14
+ // These three calls used to point at the global `/public/accounts/*` endpoints,
15
+ // which write `accounts.password`. Every one of them returned 200 and changed
16
+ // nothing that login reads:
17
+ //
18
+ // - "Change password" logged the member out (a credential change invalidates
19
+ // sessions), then refused the new password and kept accepting the old one.
20
+ // - "Forgot password" was worse: it reset a hash login never consults, so the
21
+ // member — who by definition no longer knows the old password — was locked
22
+ // out with a reset link that appeared to work.
23
+ //
24
+ // The tenant-scoped endpoints below are the ones the credential actually lives
25
+ // behind, and they invalidate only this context's sessions.
5
26
 
6
- /** Request a password-reset email. */
27
+ /**
28
+ * Request a password-reset email for this tenant.
29
+ *
30
+ * `profileId` comes from the provider (one deploy = one tenant); pass it
31
+ * explicitly only when rendering outside a pinned context.
32
+ */
7
33
  export function useForgotPassword() {
8
- const { client } = useForge();
34
+ const { client, profileId } = useForge();
9
35
 
10
- return useMutation<unknown, unknown, { email: string; origin?: string }>({
11
- mutationFn: async ({ email, origin }) => {
12
- const res = await client.post("/public/accounts/forgot-password", { email, origin });
36
+ return useMutation<unknown, unknown, { email: string; origin?: string; profileId?: string }>({
37
+ mutationFn: async ({ email, origin, profileId: overrideProfileId }) => {
38
+ const contextId = overrideProfileId ?? profileId;
39
+ if (!contextId) {
40
+ // Better than posting without it: the endpoint would 400 on schema
41
+ // validation and the form would show "something went wrong".
42
+ throw new Error("useForgotPassword: no profileId — pass one or render inside a ForgeProvider with profileId");
43
+ }
44
+ const res = await client.post("/public/sessions/forgot-password", {
45
+ profileId: contextId,
46
+ email,
47
+ // The reset link is built server-side from this, and the association
48
+ // endpoint requires it (the global one treated it as optional).
49
+ origin: origin ?? (typeof window !== "undefined" ? window.location.origin : undefined),
50
+ });
13
51
  return res.data;
14
52
  },
15
53
  });
16
54
  }
17
55
 
18
- /** Complete a password reset with the emailed token. */
56
+ /**
57
+ * Complete a password reset with the emailed token.
58
+ *
59
+ * The token is looked up on the association row, so it must be redeemed against
60
+ * the same surface that issued it — a token minted by the hook above will not
61
+ * resolve on `/public/accounts/reset-password`, and vice versa.
62
+ */
19
63
  export function useResetPassword() {
20
64
  const { client } = useForge();
21
65
 
22
66
  return useMutation<unknown, unknown, { token: string; password: string }>({
23
67
  mutationFn: async ({ token, password }) => {
24
- const res = await client.post("/public/accounts/reset-password", { token, password });
68
+ const res = await client.post("/public/sessions/reset-password", { token, password });
25
69
  return res.data;
26
70
  },
27
71
  });