@salesforce/webapp-template-feature-react-authentication-experimental 1.13.0 → 1.15.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/CHANGELOG.md +16 -0
- package/dist/force-app/main/default/classes/WebAppAuthUtils.cls +12 -0
- package/dist/force-app/main/default/classes/WebAppChangePassword.cls +77 -0
- package/dist/force-app/main/default/classes/WebAppChangePassword.cls-meta.xml +5 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/forms/auth-form.tsx +2 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/forms/submit-button.tsx +5 -5
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/ChangePassword.tsx +5 -12
- package/dist/package.json +1 -1
- package/package.json +3 -3
package/dist/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [1.15.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.14.0...v1.15.0) (2026-02-07)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# [1.14.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.13.0...v1.14.0) (2026-02-06)
|
|
15
|
+
|
|
16
|
+
**Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
# [1.13.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.12.0...v1.13.0) (2026-02-06)
|
|
7
23
|
|
|
8
24
|
|
|
@@ -53,4 +53,16 @@ public without sharing class WebAppAuthUtils {
|
|
|
53
53
|
|
|
54
54
|
return defaultUrl + url;
|
|
55
55
|
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Logs an error message and stack trace to the system debug log.
|
|
59
|
+
* It is only captured if a Trace Flag is active for the user.
|
|
60
|
+
*
|
|
61
|
+
* @param ex The exception to log.
|
|
62
|
+
* @param level The logging level to use.
|
|
63
|
+
*/
|
|
64
|
+
public static void debugLog(Exception ex, LoggingLevel level) {
|
|
65
|
+
System.debug(level, 'Message: ' + ex.getMessage());
|
|
66
|
+
System.debug(level, 'Stack Trace: ' + ex.getStackTraceString());
|
|
67
|
+
}
|
|
56
68
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Applications REST endpoint for authenticated user password changes.
|
|
3
|
+
*
|
|
4
|
+
* Endpoint: POST /services/apexrest/auth/change-password
|
|
5
|
+
*
|
|
6
|
+
* Allows authenticated users to change their password by providing their current password
|
|
7
|
+
* and a new password. The new password is validated against org password policies.
|
|
8
|
+
*
|
|
9
|
+
* Security: Uses 'with sharing' to enforce user-level security. Only authenticated users
|
|
10
|
+
* can change their own password.
|
|
11
|
+
*/
|
|
12
|
+
@RestResource(urlMapping='/auth/change-password')
|
|
13
|
+
global with sharing class WebAppChangePassword {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Changes the password for the currently authenticated user.
|
|
17
|
+
*
|
|
18
|
+
* @param newPassword The new password to set.
|
|
19
|
+
* @param currentPassword The user's current password for verification.
|
|
20
|
+
* @return SuccessPasswordChangeResponse on success, ErrorPasswordChangeResponse on failure.
|
|
21
|
+
*/
|
|
22
|
+
@HttpPost
|
|
23
|
+
global static PasswordChangeResponse changePassword(String newPassword, String currentPassword) {
|
|
24
|
+
Savepoint sp = Database.setSavepoint();
|
|
25
|
+
try {
|
|
26
|
+
// newPassword and confirmPassword should be validated to be the same value on the client
|
|
27
|
+
Site.changePassword(newPassword, newPassword, currentPassword);
|
|
28
|
+
return new SuccessPasswordChangeResponse();
|
|
29
|
+
} catch (Exception ex) {
|
|
30
|
+
Database.rollback(sp);
|
|
31
|
+
|
|
32
|
+
// System.SecurityException is a user error but treat others as system errors
|
|
33
|
+
if (ex instanceof System.SecurityException) {
|
|
34
|
+
RestContext.response.statusCode = 400;
|
|
35
|
+
return new ErrorPasswordChangeResponse(ex.getMessage());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Logs are only captured if a Trace Flag is active for the user
|
|
39
|
+
WebAppAuthUtils.debugLog(ex, LoggingLevel.ERROR);
|
|
40
|
+
|
|
41
|
+
RestContext.response.statusCode = 500;
|
|
42
|
+
return new ErrorPasswordChangeResponse('Password change failed');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Base response class for password change operations. */
|
|
47
|
+
global abstract class PasswordChangeResponse {
|
|
48
|
+
|
|
49
|
+
/** Success or failure of the password change operation */
|
|
50
|
+
private Boolean success;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Success response for password change. */
|
|
54
|
+
global class SuccessPasswordChangeResponse extends PasswordChangeResponse {
|
|
55
|
+
|
|
56
|
+
/** Constructs a success response. */
|
|
57
|
+
private SuccessPasswordChangeResponse() {
|
|
58
|
+
this.success = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Error response for password change. */
|
|
63
|
+
global class ErrorPasswordChangeResponse extends PasswordChangeResponse {
|
|
64
|
+
|
|
65
|
+
/** Error message describing the failure reason */
|
|
66
|
+
private String error;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Constructs an error response with the specified error message.
|
|
70
|
+
* @param error The error message to return to the client.
|
|
71
|
+
*/
|
|
72
|
+
private ErrorPasswordChangeResponse(String error) {
|
|
73
|
+
this.success = false;
|
|
74
|
+
this.error = error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -18,6 +18,7 @@ interface AuthFormProps extends Omit<React.ComponentProps<"form">, "onSubmit"> {
|
|
|
18
18
|
submit: {
|
|
19
19
|
text: string;
|
|
20
20
|
loadingText?: string;
|
|
21
|
+
disabled?: boolean;
|
|
21
22
|
};
|
|
22
23
|
footer?: {
|
|
23
24
|
text?: string;
|
|
@@ -68,6 +69,7 @@ export function AuthForm({
|
|
|
68
69
|
form={id}
|
|
69
70
|
label={submit.text}
|
|
70
71
|
loadingLabel={submit.loadingText}
|
|
72
|
+
disabled={submit.disabled}
|
|
71
73
|
className="mt-6"
|
|
72
74
|
/>
|
|
73
75
|
</form>
|
|
@@ -3,7 +3,7 @@ import { Spinner } from "../ui/spinner";
|
|
|
3
3
|
import { cn } from "../../lib/utils";
|
|
4
4
|
import { useFormContext } from "../../hooks/form";
|
|
5
5
|
|
|
6
|
-
interface SubmitButtonProps extends Omit<React.ComponentProps<typeof Button>, "type"
|
|
6
|
+
interface SubmitButtonProps extends Omit<React.ComponentProps<typeof Button>, "type"> {
|
|
7
7
|
/** Button text when not submitting */
|
|
8
8
|
label: string;
|
|
9
9
|
/** Button text while submitting */
|
|
@@ -18,15 +18,15 @@ const isSubmittingSelector = (state: { isSubmitting: boolean }) => state.isSubmi
|
|
|
18
18
|
* Submit button that subscribes to form submission state.
|
|
19
19
|
* UX Best Practice:
|
|
20
20
|
* 1. Disables interaction immediately upon click (via isLoading) to prevent
|
|
21
|
-
* accidental double-submissions
|
|
22
|
-
* 2. Provides immediate visual feedback (Spinner)
|
|
23
|
-
* generating sessions) can have variable latency.
|
|
21
|
+
* accidental double-submissions.
|
|
22
|
+
* 2. Provides immediate visual feedback (Spinner).
|
|
24
23
|
*/
|
|
25
24
|
export function SubmitButton({
|
|
26
25
|
label,
|
|
27
26
|
loadingLabel = "Submitting…",
|
|
28
27
|
className,
|
|
29
28
|
form: formId,
|
|
29
|
+
disabled,
|
|
30
30
|
...props
|
|
31
31
|
}: SubmitButtonProps) {
|
|
32
32
|
const form = useFormContext();
|
|
@@ -37,7 +37,7 @@ export function SubmitButton({
|
|
|
37
37
|
type="submit"
|
|
38
38
|
form={formId}
|
|
39
39
|
className={cn("w-full", className)}
|
|
40
|
-
disabled={isSubmitting}
|
|
40
|
+
disabled={isSubmitting || disabled}
|
|
41
41
|
{...props}
|
|
42
42
|
>
|
|
43
43
|
{isSubmitting && <Spinner className="mr-2" aria-hidden="true" />}
|
|
@@ -7,7 +7,6 @@ import { AuthForm } from "../components/forms/auth-form";
|
|
|
7
7
|
import { useAppForm } from "../hooks/form";
|
|
8
8
|
import { ROUTES, AUTH_PLACEHOLDERS } from "../utils/authenticationConfig";
|
|
9
9
|
import { newPasswordSchema, handleApiResponse, getErrorMessage } from "../utils/helpers";
|
|
10
|
-
import { getUser } from "../context/AuthContext";
|
|
11
10
|
|
|
12
11
|
const changePasswordSchema = z
|
|
13
12
|
.object({
|
|
@@ -18,25 +17,19 @@ const changePasswordSchema = z
|
|
|
18
17
|
export default function ChangePassword() {
|
|
19
18
|
const [success, setSuccess] = useState(false);
|
|
20
19
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
21
|
-
const user = getUser();
|
|
22
20
|
|
|
23
21
|
const form = useAppForm({
|
|
24
22
|
defaultValues: { currentPassword: "", newPassword: "", confirmPassword: "" },
|
|
25
23
|
validators: { onChange: changePasswordSchema, onSubmit: changePasswordSchema },
|
|
26
|
-
onSubmit: async ({ value }) => {
|
|
24
|
+
onSubmit: async ({ value: formFieldValues }) => {
|
|
27
25
|
setSubmitError(null);
|
|
28
26
|
setSuccess(false);
|
|
29
27
|
try {
|
|
30
28
|
// [Dev Note] Custom Apex Endpoint: /auth/change-password
|
|
31
|
-
// Security Critical:
|
|
32
|
-
// We must pass the `user.id` (from AuthContext) to the Apex endpoint
|
|
33
|
-
// to ensure the password change is applied to the correct user.
|
|
34
|
-
// The Apex backend should also verify that the authenticated session matches this ID.
|
|
35
29
|
// You must ensure this Apex class exists in your org
|
|
36
|
-
const response = await baseClient.post("/services/apexrest/auth/change-password", {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
newPassword: value.newPassword,
|
|
30
|
+
const response = await baseClient.post("/sfdcapi/services/apexrest/auth/change-password", {
|
|
31
|
+
currentPassword: formFieldValues.currentPassword,
|
|
32
|
+
newPassword: formFieldValues.newPassword,
|
|
40
33
|
});
|
|
41
34
|
await handleApiResponse(response, "Password change failed");
|
|
42
35
|
setSuccess(true);
|
|
@@ -65,7 +58,7 @@ export default function ChangePassword() {
|
|
|
65
58
|
</>
|
|
66
59
|
)
|
|
67
60
|
}
|
|
68
|
-
submit={{ text: "Change Password", loadingText: "Changing…" }}
|
|
61
|
+
submit={{ text: "Change Password", loadingText: "Changing…", disabled: success }}
|
|
69
62
|
footer={{ link: ROUTES.PROFILE.PATH, linkText: "Back to Profile" }}
|
|
70
63
|
>
|
|
71
64
|
<form.AppField name="currentPassword">
|
package/dist/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/webapp-template-feature-react-authentication-experimental",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "Authentication feature for web applications",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"author": "",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"watch": "npx tsx ../../cli/src/index.ts watch-patches packages/template/feature/feature-react-authentication packages/template/base-app/base-react-app packages/template/feature/feature-react-authentication/dist"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@salesforce/webapp-experimental": "^1.
|
|
22
|
+
"@salesforce/webapp-experimental": "^1.15.0",
|
|
23
23
|
"@tanstack/react-form": "^1.27.7",
|
|
24
24
|
"@types/react": "^19.2.7",
|
|
25
25
|
"@types/react-dom": "^19.2.3",
|
|
@@ -27,5 +27,5 @@
|
|
|
27
27
|
"react-router": "^7.10.1",
|
|
28
28
|
"vite": "^7.3.1"
|
|
29
29
|
},
|
|
30
|
-
"gitHead": "
|
|
30
|
+
"gitHead": "49bf85f6db4dd309d0fc36ad41157b45d9425b49"
|
|
31
31
|
}
|