Skip to content

Commit b633377

Browse files
authored
Merge pull request #957 from evershopcommerce/fix_952
fix: Unauthenticated Account Takeover via Missing Authorization on Cu…
2 parents 8861498 + f92b832 commit b633377

10 files changed

Lines changed: 322 additions & 39 deletions

File tree

Lines changed: 106 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import Area from '@components/common/Area.js';
2+
import { EmailField } from '@components/common/form/EmailField.js';
3+
import { Form } from '@components/common/form/Form.js';
4+
import { InputField } from '@components/common/form/InputField.js';
25
import {
36
useCustomer,
47
useCustomerDispatch
58
} from '@components/frontStore/customer/CustomerContext.jsx';
69
import { _ } from '@evershop/evershop/lib/locale/translate/_';
7-
import { Mail, User } from 'lucide-react';
10+
import { Mail, Pencil, User } from 'lucide-react';
811
import React from 'react';
912
import { toast } from 'react-toastify';
1013

@@ -14,7 +17,8 @@ interface AccountInfoProps {
1417
}
1518
export default function AccountInfo({ title, showLogout }: AccountInfoProps) {
1619
const { customer: account } = useCustomer();
17-
const { logout } = useCustomerDispatch();
20+
const { logout, updateProfile } = useCustomerDispatch();
21+
const [isEditing, setIsEditing] = React.useState(false);
1822
return (
1923
<div className="account__details divide-y">
2024
<div className="flex justify-between items-center border-border">
@@ -37,39 +41,106 @@ export default function AccountInfo({ title, showLogout }: AccountInfoProps) {
3741
</a>
3842
)}
3943
</div>
40-
<div className="grid grid-cols-1 gap-2 py-5">
41-
<Area
42-
id="accountDetails"
43-
coreComponents={[
44-
{
45-
component: {
46-
default: (
47-
<div className="account__details__name flex gap-2 py-2">
48-
<div>
49-
<User width={20} height={20} />
50-
</div>
51-
<div>{account?.fullName}</div>
52-
</div>
53-
)
54-
},
55-
sortOrder: 10
56-
},
57-
{
58-
component: {
59-
default: () => (
60-
<div className="account__details__email flex gap-2 py-2">
61-
<div>
62-
<Mail width={20} height={20} />
63-
</div>
64-
<div>{account?.email}</div>
65-
</div>
66-
)
67-
},
68-
sortOrder: 15
69-
}
70-
]}
71-
/>
72-
</div>
44+
{isEditing ? (
45+
<div className="account__details__form py-5">
46+
<Form
47+
id="accountInfoForm"
48+
submitBtnText={_('Save')}
49+
onSubmit={async (data) => {
50+
try {
51+
await updateProfile({
52+
full_name: data.full_name,
53+
email: data.email
54+
});
55+
toast.success(_('Your profile has been updated'));
56+
setIsEditing(false);
57+
} catch (error) {
58+
toast.error(error.message);
59+
}
60+
}}
61+
>
62+
<div className="space-y-3">
63+
<InputField
64+
prefixIcon={<User className="h-5 w-5" />}
65+
name="full_name"
66+
label={_('Full Name')}
67+
defaultValue={account?.fullName || ''}
68+
required
69+
validation={{ required: _('Full Name is required') }}
70+
/>
71+
<EmailField
72+
prefixIcon={<Mail className="h-5 w-5" />}
73+
name="email"
74+
label={_('Email')}
75+
defaultValue={account?.email || ''}
76+
required
77+
validation={{ required: _('Email is required') }}
78+
/>
79+
</div>
80+
</Form>
81+
<div className="mt-3">
82+
<a
83+
className="text-interactive"
84+
href="#"
85+
onClick={(e) => {
86+
e.preventDefault();
87+
setIsEditing(false);
88+
}}
89+
>
90+
{_('Cancel')}
91+
</a>
92+
</div>
93+
</div>
94+
) : (
95+
<div className="flex justify-between items-start gap-2 py-5">
96+
<div className="grid grid-cols-1 gap-2 grow">
97+
<Area
98+
id="accountDetails"
99+
coreComponents={[
100+
{
101+
component: {
102+
default: (
103+
<div className="account__details__name flex gap-2 py-2">
104+
<div>
105+
<User width={20} height={20} />
106+
</div>
107+
<div>{account?.fullName}</div>
108+
</div>
109+
)
110+
},
111+
sortOrder: 10
112+
},
113+
{
114+
component: {
115+
default: () => (
116+
<div className="account__details__email flex gap-2 py-2">
117+
<div>
118+
<Mail width={20} height={20} />
119+
</div>
120+
<div>{account?.email}</div>
121+
</div>
122+
)
123+
},
124+
sortOrder: 15
125+
}
126+
]}
127+
/>
128+
</div>
129+
<button
130+
type="button"
131+
className="text-interactive flex items-center gap-1"
132+
onClick={() => setIsEditing(true)}
133+
>
134+
<Pencil width={16} height={16} />
135+
{_('Edit')}
136+
</button>
137+
</div>
138+
)}
73139
</div>
74140
);
75141
}
142+
143+
export const layout = {
144+
areaId: 'content',
145+
sortOrder: 10
146+
};

packages/evershop/src/components/frontStore/customer/CustomerContext.tsx

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ interface Customer {
199199
addresses: ExtendedCustomerAddress[];
200200
orders: Order[];
201201
addAddressApi: string;
202+
updateProfileApi?: string;
202203
createdAt: {
203204
value: string;
204205
text: string;
@@ -273,6 +274,10 @@ interface CustomerDispatchContextValue {
273274
addressData: Partial<ExtendedCustomerAddress>
274275
) => Promise<ExtendedCustomerAddress>;
275276
deleteAddress: (addressId: string | number) => Promise<void>;
277+
updateProfile: (data: {
278+
full_name?: string;
279+
email?: string;
280+
}) => Promise<Customer>;
276281
}
277282

278283
const CustomerContext = createContext<CustomerContextValue | undefined>(
@@ -580,6 +585,46 @@ export function CustomerProvider({
580585
[state.customer, appDispatch, getCurrentAjaxUrl]
581586
);
582587

588+
// Update the current customer's own profile (full name / email). The endpoint
589+
// identifies the customer from the session/JWT, so no id is sent.
590+
const updateProfile = useCallback(
591+
async (data: {
592+
full_name?: string;
593+
email?: string;
594+
}): Promise<Customer> => {
595+
if (!state.customer?.updateProfileApi) {
596+
throw new Error(_('Update profile API not available'));
597+
}
598+
599+
dispatch({ type: 'SET_LOADING', payload: true });
600+
601+
try {
602+
const response = await retry(() =>
603+
fetch(state.customer!.updateProfileApi!, {
604+
method: 'PATCH',
605+
headers: { 'Content-Type': 'application/json' },
606+
body: JSON.stringify(data)
607+
})
608+
);
609+
610+
const json = await response.json();
611+
612+
if (!response.ok || json.error) {
613+
throw new Error(json.error?.message || _('Failed to update profile'));
614+
}
615+
616+
// Sync with server to get fresh customer data.
617+
await appDispatch.fetchPageData(getCurrentAjaxUrl());
618+
619+
return json.data;
620+
} catch (error) {
621+
dispatch({ type: 'SET_LOADING', payload: false });
622+
throw error;
623+
}
624+
},
625+
[state.customer, appDispatch, getCurrentAjaxUrl]
626+
);
627+
583628
const contextValue = useMemo(
584629
(): CustomerContextValue => ({
585630
...state
@@ -595,9 +640,19 @@ export function CustomerProvider({
595640
setCustomer,
596641
addAddress,
597642
updateAddress,
598-
deleteAddress
643+
deleteAddress,
644+
updateProfile
599645
}),
600-
[login, logout, setCustomer, addAddress, updateAddress, deleteAddress]
646+
[
647+
login,
648+
register,
649+
logout,
650+
setCustomer,
651+
addAddress,
652+
updateAddress,
653+
deleteAddress,
654+
updateProfile
655+
]
601656
);
602657

603658
return (

packages/evershop/src/modules/base/pages/frontStore/all/Base.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export const query = `
109109
text
110110
}
111111
addAddressApi
112+
updateProfileApi
112113
addresses {
113114
addressId
114115
uuid
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"methods": ["PATCH"],
33
"path": "/customers/:id",
4-
"access": "public"
4+
"access": "private"
55
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import bodyParser from 'body-parser';
2+
import { EvershopRequest } from '../../../../types/request.js';
3+
import { EvershopResponse } from '../../../../types/response.js';
4+
5+
export default (
6+
request: EvershopRequest,
7+
response: EvershopResponse,
8+
next: () => void
9+
) => {
10+
bodyParser.json({ inflate: false })(request, response, next);
11+
};
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"full_name": {
5+
"type": "string",
6+
"minLength": 1,
7+
"errorMessage": {
8+
"type": "Full name must be a string",
9+
"minLength": "Full name cannot be empty"
10+
}
11+
},
12+
"email": {
13+
"type": "string",
14+
"format": "email",
15+
"errorMessage": {
16+
"type": "Email must be a string",
17+
"format": "Email must be a valid email address (e.g., user@example.com)"
18+
}
19+
}
20+
},
21+
"additionalProperties": true
22+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"methods": ["PATCH"],
3+
"path": "/customers/me",
4+
"access": "public"
5+
}

0 commit comments

Comments
 (0)