Skip to content

Commit 37e9ff4

Browse files
authored
Merge branch 'main' into lkostrowski/san-jose
2 parents 40518de + 65922df commit 37e9ff4

17 files changed

Lines changed: 661 additions & 122 deletions

File tree

.changeset/tangy-friends-follow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"saleor-dashboard": minor
3+
---
4+
5+
Add password login mode setting to Site Settings, allowing administrators to control whether password-based authentication is enabled, restricted to customers only, or fully disabled. The Sign In page now respects this setting — when password login is disabled or restricted to customers only, the email/password form is hidden and only external authentication methods (e.g. SSO) are shown.

locale/defaultMessages.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@
203203
"context": "product type shipping settings, section header",
204204
"string": "Shipping"
205205
},
206+
"/2o5cH": {
207+
"context": "section description",
208+
"string": "Controls whether users can authenticate using password-based login. You can allow it for everyone, restrict it to customers only, or disable it entirely."
209+
},
206210
"/4/nYx": {
207211
"string": "Discount updated"
208212
},
@@ -884,9 +888,17 @@
884888
"context": "button",
885889
"string": "Add variant"
886890
},
891+
"3CMUKE": {
892+
"context": "password login mode option description",
893+
"string": "Only customer users can log in with a password. Staff users logging in with a password will be treated as customers."
894+
},
887895
"3DGvA/": {
888896
"string": "Remember this will also unpin all products assigned to this category, making them unavailable in storefront."
889897
},
898+
"3EFNZn": {
899+
"context": "password login mode option",
900+
"string": "Customers only"
901+
},
890902
"3Eyq0y": {
891903
"string": "Mark as paid manually if the payment is confirmed"
892904
},
@@ -2329,6 +2341,10 @@
23292341
"context": "header",
23302342
"string": "Translation Collection \"{collectionName}\" - {languageCode}"
23312343
},
2344+
"BtsJ+e": {
2345+
"context": "empty state message when no login method is available",
2346+
"string": "Password login is disabled. Contact your administrator to configure an external authentication method or enable password login."
2347+
},
23322348
"BvGp1I": {
23332349
"string": "between"
23342350
},
@@ -7096,6 +7112,10 @@
70967112
"context": "status",
70977113
"string": "Active"
70987114
},
7115+
"c5B5Cg": {
7116+
"context": "password login mode option description",
7117+
"string": "No user can log in with a password"
7118+
},
70997119
"c5fFin": {
71007120
"context": "ExitFormPrompt title",
71017121
"string": "Leave without saving changes?"
@@ -7428,6 +7448,10 @@
74287448
"e822us": {
74297449
"string": "Please note, while all currency and date adjustments are complete, language translations are at varying degrees of completion."
74307450
},
7451+
"e8O72h": {
7452+
"context": "password login mode option",
7453+
"string": "Disabled"
7454+
},
74317455
"e92Uxp": {
74327456
"context": "order history message",
74337457
"string": "Updated fulfillment group's tracking number"
@@ -8135,6 +8159,10 @@
81358159
"context": "Transaction event status - unknown status, info without event data",
81368160
"string": "Info"
81378161
},
8162+
"iidznP": {
8163+
"context": "password login mode option description",
8164+
"string": "All users can log in with a password"
8165+
},
81388166
"ij3dWD": {
81398167
"context": "custom extension page create, error message",
81408168
"string": "Extension name is required"
@@ -8375,6 +8403,10 @@
83758403
"context": "select label",
83768404
"string": "Unset"
83778405
},
8406+
"k6kIUq": {
8407+
"context": "password login mode option",
8408+
"string": "Enabled"
8409+
},
83788410
"k6sfZr": {
83798411
"context": "tooltip content when product is in preorder",
83808412
"string": "This product is still in preorder. You will be able to fulfill it after it reaches it’s release date"
@@ -8531,6 +8563,10 @@
85318563
"context": "delete model",
85328564
"string": "Are you sure you want to delete {title}?"
85338565
},
8566+
"kny5j9": {
8567+
"context": "section title",
8568+
"string": "Password Login"
8569+
},
85348570
"kp2IYP": {
85358571
"context": "option",
85368572
"string": "Gift card product type"
@@ -9504,6 +9540,10 @@
95049540
"context": "voucher status scheduled",
95059541
"string": "Scheduled"
95069542
},
9543+
"qRMXUE": {
9544+
"context": "card header",
9545+
"string": "Password login mode"
9546+
},
95079547
"qRmntJ": {
95089548
"context": "extensions list ready to be used",
95099549
"string": "{name} is ready to be used"

src/auth/components/LoginPage/LoginPage.test.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const defaultProps = {
1818
disabled: false,
1919
loading: false,
2020
externalAuthentications: [mockExternalAuth],
21+
passwordLoginEnabled: true,
2122
onExternalAuthentication: jest.fn(),
2223
onSubmit: jest.fn(),
2324
};
@@ -28,6 +29,47 @@ jest.mock("react-router-dom", () => ({
2829
}));
2930

3031
describe("LoginPage", () => {
32+
describe("Password login disabled", () => {
33+
it("hides email and password inputs when password login is disabled", () => {
34+
// Arrange & Act
35+
render(<LoginPage lastLoginMethod={null} {...defaultProps} passwordLoginEnabled={false} />);
36+
37+
// Assert
38+
expect(screen.queryByTestId("email")).not.toBeInTheDocument();
39+
expect(screen.queryByTestId("password")).not.toBeInTheDocument();
40+
expect(screen.queryByTestId("submit")).not.toBeInTheDocument();
41+
expect(screen.queryByTestId("reset-password-link")).not.toBeInTheDocument();
42+
expect(screen.getByTestId("external-authentication")).toBeInTheDocument();
43+
});
44+
45+
it("shows empty state message when password login is disabled and no external auth", () => {
46+
// Arrange & Act
47+
render(
48+
<LoginPage
49+
lastLoginMethod={null}
50+
{...defaultProps}
51+
passwordLoginEnabled={false}
52+
externalAuthentications={[]}
53+
/>,
54+
);
55+
56+
// Assert
57+
expect(screen.queryByTestId("email")).not.toBeInTheDocument();
58+
expect(screen.queryByTestId("external-authentication")).not.toBeInTheDocument();
59+
expect(screen.getByText(/Password login is disabled/)).toBeInTheDocument();
60+
});
61+
62+
it("shows email and password inputs when password login is enabled", () => {
63+
// Arrange & Act
64+
render(<LoginPage lastLoginMethod={null} {...defaultProps} passwordLoginEnabled={true} />);
65+
66+
// Assert
67+
expect(screen.getByTestId("email")).toBeInTheDocument();
68+
expect(screen.getByTestId("password")).toBeInTheDocument();
69+
expect(screen.getByTestId("submit")).toBeInTheDocument();
70+
});
71+
});
72+
3173
describe("External Authentication", () => {
3274
it("sets optimisticLoaderAuthId when clicking external auth button", async () => {
3375
// Arrange & Act

src/auth/components/LoginPage/LoginPage.tsx

Lines changed: 81 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ interface LoginCardProps {
2323
disabled: boolean;
2424
loading: boolean;
2525
externalAuthentications?: AvailableExternalAuthenticationsQuery["shop"]["availableExternalAuthentications"];
26+
passwordLoginEnabled: boolean;
2627
onExternalAuthentication: (pluginId: string) => void;
2728
onSubmit: (event: LoginFormData) => SubmitPromise;
2829
lastLoginMethod: LastLoginMethod;
@@ -34,6 +35,7 @@ const LoginPage = (props: LoginCardProps) => {
3435
disabled,
3536
loading,
3637
externalAuthentications = [],
38+
passwordLoginEnabled,
3739
onExternalAuthentication,
3840
onSubmit,
3941
lastLoginMethod,
@@ -65,79 +67,95 @@ const LoginPage = (props: LoginCardProps) => {
6567
<Text color="critical2">{getErrorMessage(error, intl)}</Text>
6668
</Box>
6769
))}
68-
<Input
69-
autoFocus
70-
width="100%"
71-
autoComplete="email"
72-
label={intl.formatMessage(commonMessages.email)}
73-
id="email"
74-
name="email"
75-
onChange={handleChange}
76-
value={data.email}
77-
data-test-id="email"
78-
spellCheck={false}
79-
disabled={disabled}
80-
required
81-
/>
82-
<FormSpacer />
83-
<Input
84-
width="100%"
85-
label={intl.formatMessage({
86-
id: "5sg7KC",
87-
defaultMessage: "Password",
88-
})}
89-
id="password"
90-
name="password"
91-
autoComplete="current-password"
92-
onChange={handleChange}
93-
type={showPassword ? "text" : "password"}
94-
value={data.password}
95-
data-test-id="password"
96-
spellCheck={false}
97-
disabled={disabled}
98-
endAdornment={
99-
<Button
100-
icon={
101-
<EyeIcon onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />
70+
{passwordLoginEnabled && (
71+
<>
72+
<Input
73+
autoFocus
74+
width="100%"
75+
autoComplete="email"
76+
label={intl.formatMessage(commonMessages.email)}
77+
id="email"
78+
name="email"
79+
onChange={handleChange}
80+
value={data.email}
81+
data-test-id="email"
82+
spellCheck={false}
83+
disabled={disabled}
84+
required
85+
/>
86+
<FormSpacer />
87+
<Input
88+
width="100%"
89+
label={intl.formatMessage({
90+
id: "5sg7KC",
91+
defaultMessage: "Password",
92+
})}
93+
id="password"
94+
name="password"
95+
autoComplete="current-password"
96+
onChange={handleChange}
97+
type={showPassword ? "text" : "password"}
98+
value={data.password}
99+
data-test-id="password"
100+
spellCheck={false}
101+
disabled={disabled}
102+
endAdornment={
103+
<Button
104+
icon={
105+
<EyeIcon
106+
onPointerEnterCapture={undefined}
107+
onPointerLeaveCapture={undefined}
108+
/>
109+
}
110+
onMouseDown={() => setShowPassword(true)}
111+
onMouseUp={() => setShowPassword(false)}
112+
variant="tertiary"
113+
type="button"
114+
/>
102115
}
103-
onMouseDown={() => setShowPassword(true)}
104-
onMouseUp={() => setShowPassword(false)}
105-
variant="tertiary"
106-
type="button"
116+
required
107117
/>
108-
}
109-
required
110-
/>
111-
<Link to={passwordResetUrl}>
112-
<Text className={classes.link} fontSize={3} data-test-id="reset-password-link">
118+
<Link to={passwordResetUrl}>
119+
<Text className={classes.link} fontSize={3} data-test-id="reset-password-link">
120+
<FormattedMessage
121+
id="3tbL7x"
122+
defaultMessage="Forgot password?"
123+
description="description"
124+
/>
125+
</Text>
126+
</Link>
127+
128+
<div className={classes.buttonContainer}>
129+
<ButtonWithLoader
130+
width="100%"
131+
disabled={disabled}
132+
variant="primary"
133+
type="submit"
134+
transitionState={loading ? "loading" : "default"}
135+
data-test-id="submit"
136+
position="relative"
137+
>
138+
{showLastLoginIndicatorForPassword && <LastLoginIndicator />}
139+
<FormattedMessage id="AubJ/S" defaultMessage="Sign in" description="button" />
140+
</ButtonWithLoader>
141+
</div>
142+
</>
143+
)}
144+
{!passwordLoginEnabled && externalAuthentications.length === 0 && (
145+
<Text color="default2" fontSize={3}>
113146
<FormattedMessage
114-
id="3tbL7x"
115-
defaultMessage="Forgot password?"
116-
description="description"
147+
id="BtsJ+e"
148+
defaultMessage="Password login is disabled. Contact your administrator to configure an external authentication method or enable password login."
149+
description="empty state message when no login method is available"
117150
/>
118151
</Text>
119-
</Link>
120-
121-
<div className={classes.buttonContainer}>
122-
<ButtonWithLoader
123-
width="100%"
124-
disabled={disabled}
125-
variant="primary"
126-
type="submit"
127-
transitionState={loading ? "loading" : "default"}
128-
data-test-id="submit"
129-
position="relative"
130-
>
131-
{showLastLoginIndicatorForPassword && <LastLoginIndicator />}
132-
<FormattedMessage id="AubJ/S" defaultMessage="Sign in" description="button" />
133-
</ButtonWithLoader>
134-
</div>
152+
)}
135153
{externalAuthentications.map(externalAuthentication => (
136154
<Fragment key={externalAuthentication.id}>
137155
<FormSpacer />
138156
<ButtonWithLoader
139157
width="100%"
140-
variant="secondary"
158+
variant={passwordLoginEnabled ? "secondary" : "primary"}
141159
onClick={() => {
142160
onExternalAuthentication(externalAuthentication.id);
143161
setOptimisticLoaderAuthId(externalAuthentication.id);

src/auth/queries.staging.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { gql } from "@apollo/client";
2+
3+
export const availableExternalAuthenticationsStaging = gql`
4+
query AvailableExternalAuthenticationsStaging {
5+
shop {
6+
availableExternalAuthentications {
7+
id
8+
name
9+
}
10+
passwordLoginMode
11+
}
12+
}
13+
`;

0 commit comments

Comments
 (0)