Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ The exchange uses a form-encoded `POST` and never sends a configured bearer toke
failures are exposed as `OAuthAuthorizationCodeExchangeError`; client secrets and codes are not
included in validation errors.

Password-reset requests can provide the reset page that the backend should place in the email.
Existing callers can continue to pass only the email address:

```ts
await sdk.auth.forgotPassword("user@example.com");

await sdk.auth.forgotPassword("user@example.com", {
resetUrl: "https://auth.put.io/reset-password?next=%2Ffiles",
});
```

The backend validates this URL before using it and falls back to its default reset page when the
option is omitted.

## Utilities

Shared formatting, URL, and error-localization helpers are available from the utilities subpath:
Expand Down
1 change: 1 addition & 0 deletions src/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ exports[`sdk root entry > exports the expected top-level public surface 1`] = `
"FilesSearchQuerySchema",
"FolderTypeSchema",
"ForgotPasswordErrorSpec",
"ForgotPasswordOptionsSchema",
"FriendBaseSchema",
"FriendInviteJoinedUserSchema",
"FriendInviteJoinedUserStatusSchema",
Expand Down
12 changes: 11 additions & 1 deletion src/core/client.promise.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ vi.mock("../domains/auth.js", async () => {
clients: vi.fn(() => Effect.succeed([{ id: 1, name: "cli" }])),
exchangeOAuthAuthorizationCode: vi.fn((input) => Effect.succeed(`exchange:${input.code}`)),
exists: vi.fn((key, value) => Effect.succeed(key === "username" && value === "sdk-user")),
forgotPassword: vi.fn((mail) => Effect.succeed({ mail, status: "OK" })),
forgotPassword: vi.fn((mail, options) => Effect.succeed({ mail, options, status: "OK" })),
generateTOTP: vi.fn(() =>
Effect.succeed({
secret: "secret",
Expand Down Expand Up @@ -547,6 +547,16 @@ describe("sdk promise client adapters", () => {
expect(await client.auth.exists("username", "sdk-user")).toBe(true);
expect(await client.auth.forgotPassword("a@put.io")).toEqual({
mail: "a@put.io",
options: undefined,
status: "OK",
});
expect(
await client.auth.forgotPassword("a@put.io", {
resetUrl: "https://auth.put.io/reset-password?next=%2Ffiles",
}),
).toEqual({
mail: "a@put.io",
options: { resetUrl: "https://auth.put.io/reset-password?next=%2Ffiles" },
status: "OK",
});
expect(await client.auth.getCode({ appId: 8993 })).toMatchObject({ code: "CODE-8993" });
Expand Down
29 changes: 28 additions & 1 deletion src/domains/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,11 +233,29 @@ describe("auth domain", () => {

expect(
await runSdkEffect(forgotPassword("sdk@put.io"), (request) => {
expect(getFormBody(request).get("mail")).toBe("sdk@put.io");
const body = getFormBody(request);
expect(body.get("mail")).toBe("sdk@put.io");
expect(body.has("reset_url")).toBe(false);
return jsonResponse({ status: "OK" });
}),
).toEqual({ status: "OK" });

expect(
await runSdkEffect(
forgotPassword("sdk@put.io", {
resetUrl: "https://auth-staging.put.io/reset-password?next=%2Ffiles%2F99",
}),
(request) => {
const body = getFormBody(request);
expect(body.get("mail")).toBe("sdk@put.io");
expect(body.get("reset_url")).toBe(
"https://auth-staging.put.io/reset-password?next=%2Ffiles%2F99",
);
return jsonResponse({ status: "OK" });
},
),
).toEqual({ status: "OK" });

expect(
await runSdkEffect(resetPassword("key-1", "secret"), (request) => {
const body = getFormBody(request);
Expand Down Expand Up @@ -457,6 +475,15 @@ describe("auth domain", () => {
runSdkExit(getFamilyInvite(""), handler),
runSdkExit(getFriendInvite(""), handler),
runSdkExit(forgotPassword(""), handler),
runSdkExit(forgotPassword("sdk@put.io", { resetUrl: "" }), handler),
runSdkExit(
forgotPassword("sdk@put.io", {
resetUrl: "https://auth.put.io/reset-password",
// @ts-expect-error JavaScript callers can provide unknown request properties.
unexpected: true,
}),
handler,
),
runSdkExit(resetPassword("", sensitivePassword), handler),
runSdkExit(getCode({ appId: 0 }), handler),
runSdkExit(checkCodeMatch(""), handler),
Expand Down
14 changes: 12 additions & 2 deletions src/domains/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,13 @@ const AuthResetPasswordInputSchema = Schema.Struct({
key: NonEmptyStringSchema,
password: NonEmptyStringSchema,
});
export const ForgotPasswordOptionsSchema = Schema.Struct({
resetUrl: Schema.optional(NonEmptyStringSchema),
});
const AuthForgotPasswordInputSchema = Schema.Struct({
mail: NonEmptyStringSchema,
options: Schema.optional(ForgotPasswordOptionsSchema),
});
const AuthGetCodeInputSchema = Schema.Struct({
appId: AuthClientIdSchema,
clientName: Schema.optional(NonEmptyStringSchema),
Expand All @@ -200,6 +207,7 @@ export type VerifyTOTPResponse = Schema.Schema.Type<typeof VerifyTOTPResponseSch
export type RegisterInput = Schema.Schema.Type<typeof RegisterInputSchema>;
export type LoginInput = Schema.Schema.Type<typeof LoginInputSchema>;
export type AuthGetCodeInput = Schema.Schema.Type<typeof AuthGetCodeInputSchema>;
export type ForgotPasswordOptions = Schema.Schema.Type<typeof ForgotPasswordOptionsSchema>;
export type OAuthAuthorizationCodeExchangeInput = Schema.Schema.Type<
typeof OAuthAuthorizationCodeExchangeInputSchema
>;
Expand Down Expand Up @@ -566,20 +574,22 @@ export const getFriendInvite = (
).pipe(withOperationErrors(FriendInviteLookupErrorSpec));
export const forgotPassword = (
mail: string,
options?: ForgotPasswordOptions,
): Effect.Effect<
Schema.Schema.Type<typeof OkResponseSchema>,
ForgotPasswordError,
PutioSdkContext
> =>
decodeAuthInput("forgotPassword", NonEmptyStringSchema, mail, (decodedMail) =>
decodeAuthInput("forgotPassword", AuthForgotPasswordInputSchema, { mail, options }, (input) =>
requestJson(OkResponseSchema, {
auth: {
type: "none",
},
body: {
type: "form",
value: {
mail: decodedMail,
mail: input.mail,
...(input.options?.resetUrl === undefined ? {} : { reset_url: input.options.resetUrl }),
},
},
method: "POST",
Expand Down