Skip to content

feat: changePassword + useLukkChangePassword - #46

Merged
stsepelin merged 3 commits into
mainfrom
feat/change-password
Aug 21, 2026
Merged

stsepelin merged 3 commits into
mainfrom
feat/change-password

Conversation

@stsepelin

@stsepelin stsepelin commented Aug 21, 2026 •

Copy link
Copy Markdown
Owner

Client support for lukk's new POST /auth/password (stsepelin/lukk#27), so the feature isn't server-only.

const { changing, changePassword } = useLukkChangePassword()

await changePassword({
  current_password: current.value,
  password: next.value,
  password_confirmation: confirm.value,
})

ChangePasswordInput deliberately has no token field. The reset flow proves control of an email address; this one proves knowledge of the existing password — and that difference is the entire reason it's safe to expose to a session. A stolen access token alone must not be enough to take an account over permanently.

The composable is thin on purpose

lukk revokes every other session and keeps the current one, so there's no token to swap and nothing to re-login — existing state is already correct afterwards. One test asserts the access state is untouched rather than merely that the call was made, because a composable that cleared state here would log the user out of the tab they just changed their password in.

From review

  • Renamed useLukkPassword → useLukkChangePassword. The former reads as an umbrella covering the reset flow too, which would make useLukkPasswordReset look like a subset of it. Free to change now — the changeset is pending, so nothing shipped under the old name.
  • The test re-typed the 'lukk:access' literal, which CLAUDE.md explicitly forbids: shared state keys live in runtime/keys.ts so a rename can't silently decouple a test from what it asserts about. Imports ACCESS_KEY now.
  • Returns { changing, changePassword } (refs first) to match useLukkPasswordReset's shape — they're read side by side in the docs.

Verification

91 + 338 tests, 100% coverage both packages, lint + typecheck + build clean.

Greptile Summary

Adds authenticated password-change support to the core client and a Nuxt composable that exposes request progress without altering the surviving session state.

  • Defines and exports the current-password and new-password input shape.
  • Sends authenticated POST /auth/password requests through the core client.
  • Adds useLukkChangePassword with loading-state and error propagation behavior.
  • Adds client and composable tests plus release changesets.

Confidence Score: 4/5

The overlapping-call loading-state defect should be fixed before merging so duplicate password-change requests cannot re-enable submission while work remains pending.

Each invocation clears the same changing ref independently, allowing an earlier request to report an idle state while another password-change request is still running.

Files Needing Attention: packages/nuxt/src/runtime/composables/useLukkChangePassword.ts

Important Files Changed

Filename Overview
packages/core/src/client.ts Adds an authenticated password-change request following existing URL, POST-body, bearer-token, and void-response conventions.
packages/core/src/types.ts Adds the public three-field ChangePasswordInput contract, exported through the existing wildcard entrypoint.
packages/nuxt/src/runtime/composables/useLukkChangePassword.ts Adds a thin password-change composable, but its shared boolean loading state becomes inaccurate when calls overlap.
packages/core/test/client.test.ts Verifies the endpoint URL, method, request body, and bearer authorization.
packages/nuxt/test/change-password.test.ts Covers success, rejection, and preserved session state, but only for non-overlapping requests.
.changeset/change-password.md Documents and versions the new core and Nuxt password-change APIs.

Sequence Diagram

sequenceDiagram
  participant UI
  participant Composable as useLukkChangePassword
  participant Client as Lukk Client
  participant API as POST /auth/password
  UI->>Composable: changePassword(input)
  Composable->>Composable: "changing = true"
  Composable->>Client: changePassword(input)
  Client->>API: Authenticated POST
  API-->>Client: Success or error
  Client-->>Composable: Resolve or reject
  Composable->>Composable: "changing = false"
  Composable-->>UI: Resolve or propagate error
Loading

Fix all with Greploop Fix All in Claude Code

Prompt To Fix All With AI
### Issue 1
packages/nuxt/src/runtime/composables/useLukkChangePassword.ts:22-37
**Overlapping calls clear loading state**

If two password-change calls overlap, the first call to settle sets `changing` to false while the second remains pending, re-enabling bound submission controls and permitting additional duplicate requests.

```suggestion
  /** True while the change is in flight — bind a submit button's disabled state to it. */
  const changing = ref(false)
  let changesInFlight = 0

  /**
   * Change the password. Rejects with a `LukkError` on failure — `422` carries Laravel's validation
   * bag, so {@link useLukkForm} maps it onto your fields.
   */
  async function changePassword(input: ChangePasswordInput): Promise<void> {
    changesInFlight++
    changing.value = true
    try {
      await $lukk.changePassword(input)
    }
    finally {
      changesInFlight--
      changing.value = changesInFlight > 0
    }
  }
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "refactor: rename to useLukkChangePasswor..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Client support for lukk's new `POST /auth/password` (stsepelin/lukk#27), so the
feature isn't server-only.

`ChangePasswordInput` deliberately has no token field: the reset flow proves
control of an email address, this one proves knowledge of the existing password,
and that difference is the entire reason the endpoint is safe to expose to a
session. A stolen access token alone must not be enough to take an account over
permanently.

The composable is thin on purpose. lukk revokes every OTHER session and keeps
the current one, so there is no token to swap and nothing to re-login — the
existing state is already correct afterwards. A composable that cleared session
state here would log the user out of the tab they just changed their password
in, which is why one of the tests asserts the access state is untouched rather
than merely that the call was made.
… key

Review points, both fair.

`useLukkPassword` reads as an umbrella for password operations, which invites a
reader to expect `sendResetLink` on it too — and `useLukkPasswordReset` then
reads as a subset of it. Named for the one operation it performs instead. Free
to do now: the changeset is pending, so nothing has been published under the old
name.

The test re-typed the `'lukk:access'` literal, which CLAUDE.md explicitly forbids
— shared state keys live in `runtime/keys.ts` so a rename can't silently
decouple a test from the thing it is asserting about. Imports `ACCESS_KEY` now,
like its siblings.

Also returns `{ changing, changePassword }` rather than fn-first, matching
`useLukkPasswordReset`'s shape — the two are read side by side in the docs.
stsepelin added a commit to stsepelin/lukk-docs that referenced this pull request Aug 21, 2026
The page said the client had no composable and showed a `useLukkFetch` call —
true when written, not any more (stsepelin/lukk-js#46). Documents the composable
and keeps a `useLukkForm` example, since the 422 validation bag is the thing
most callers actually need to wire up.

Two corrections from the security review of the server side:

A success releases the `login` counter as well as `confirm`. Without that, a
user who was being brute-forced, noticed, and changed their password stayed
locked out of login everywhere else — the page now says so, because "changing
your password fixes it" is exactly what a reader would otherwise assume.

The session sweep is not unconditional: a token carrying no `fid` identifies no
session to preserve, so every session is revoked rather than none. The page
described only the common case.
Comment thread packages/nuxt/src/runtime/composables/useLukkChangePassword.ts
Raised as a loading-flag bug: two overlapping calls, the first to settle clears
`changing` while the second is pending, re-enabling a bound submit button. That
is mechanically true, and the suggested fix was an in-flight counter.

The counter treats the symptom. I checked what an overlap actually costs by
driving the real endpoint twice, and it is not a cosmetic problem:

  first: 200   second (same body): 422   confirm lockout attempts burned: 1

The second request carries a `current_password` the first has already replaced,
so lukk reads it as a wrong password and spends one of the account's
consecutive-failure attempts. A double-submit quietly eats the user's lockout
budget and reports a 422 for a change that had in fact just succeeded. A counter
keeps the button disabled and still lets that request go.

So the overlap is refused outright, with no request made. That also makes
`changing` correct by construction — there is never more than one in flight, so
a single boolean cannot go stale, and no counter is needed.

Deliberately not propagated to the sibling composables. The pattern appears in
six of them, but a reset link or a verification resend is idempotent and
verifies no secret, so a duplicate there costs nothing; adding re-entry guards
everywhere would be ceremony. This endpoint is different because it checks a
credential against a cap.

Both tests were mutation-tested: removing the guard makes the first fail. The
second exists because a guard that never releases would wedge the composable for
a user who simply mistyped their current password.
@stsepelin
stsepelin merged commit fffdc6a into main Aug 21, 2026
7 checks passed
@stsepelin
stsepelin deleted the feat/change-password branch August 21, 2026 13:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant