Skip to content

Commit

Permalink
Fix errors when user has insufficient permissions (#5102)
Browse files Browse the repository at this point in the history
* include private metadata when user has permissions

* gift card only permission fix

* changesets

* fix crash

* Add tests

* fix ts errors

* cr

* improve tests

* improve changeset

* various gift card view improvements

* json messages

---------

Co-authored-by: Paweł Chyła <[email protected]>
  • Loading branch information
Cloud11PL and poulch committed Aug 12, 2024
1 parent 5e3cc3f commit edaf42b
Show file tree
Hide file tree
Showing 24 changed files with 424 additions and 97 deletions.
5 changes: 5 additions & 0 deletions .changeset/nice-humans-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---

Gift card details page and customer details page no longer crash when you have only some required permissions. This means that you can now view gift card details page if you have only gift card permissions and customer details page if you have only customer permissions.
4 changes: 4 additions & 0 deletions locale/defaultMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2605,6 +2605,10 @@
"FTYkgw": {
"string": "Delete collections"
},
"FWaL+x": {
"context": "gift card history message",
"string": "Gift card balance was reset"
},
"FWbv/u": {
"context": "page header",
"string": "Create Discount"
Expand Down
10 changes: 7 additions & 3 deletions src/components/Timeline/TimelineNote.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GiftCardEventFragment, OrderEventFragment } from "@dashboard/graphql";
import { GiftCardEventsQuery, OrderEventFragment } from "@dashboard/graphql";
import { getUserInitials, getUserName } from "@dashboard/misc";
import { makeStyles } from "@saleor/macaw-ui";
import { Text } from "@saleor/macaw-ui-next";
Expand Down Expand Up @@ -31,11 +31,15 @@ const useStyles = makeStyles(
{ name: "TimelineNote" },
);

type TimelineAppType =
| NonNullable<GiftCardEventsQuery["giftCard"]>["events"][0]["app"]
| OrderEventFragment["app"];

interface TimelineNoteProps {
date: string;
message: string | null;
user: OrderEventFragment["user"];
app: OrderEventFragment["app"] | GiftCardEventFragment["app"];
app: TimelineAppType;
hasPlainDate?: boolean;
}

Expand All @@ -61,7 +65,7 @@ const TimelineAvatar = ({
className,
}: {
user: OrderEventFragment["user"];
app: OrderEventFragment["app"] | GiftCardEventFragment["app"];
app: TimelineAppType;
className: string;
}) => {
if (user) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ const CustomerDetailsPage: React.FC<CustomerDetailsPageProps> = ({
lastName: customer?.lastName || "",
metadata: customer?.metadata.map(mapMetadataItemToInput),
note: customer?.note || "",
privateMetadata: customer?.privateMetadata.map(mapMetadataItemToInput),
privateMetadata: customer?.privateMetadata
? customer?.privateMetadata.map(mapMetadataItemToInput)
: [],
};
const { makeChangeHandler: makeMetadataChangeHandler } = useMetadataChangeTrigger();
const { CUSTOMER_DETAILS_MORE_ACTIONS } = useExtensions(extensionMountPoints.CUSTOMER_DETAILS);
Expand Down
12 changes: 11 additions & 1 deletion src/customers/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,19 @@ export const customerList = gql`
`;

export const customerDetails = gql`
query CustomerDetails($id: ID!, $PERMISSION_MANAGE_ORDERS: Boolean!) {
query CustomerDetails(
$id: ID!
$PERMISSION_MANAGE_ORDERS: Boolean!
$PERMISSION_MANAGE_STAFF: Boolean!
) {
user(id: $id) {
...CustomerDetails
metadata {
...MetadataItem
}
privateMetadata @include(if: $PERMISSION_MANAGE_STAFF) {
...MetadataItem
}
orders(last: 5) @include(if: $PERMISSION_MANAGE_ORDERS) {
edges {
node {
Expand Down
5 changes: 4 additions & 1 deletion src/customers/views/CustomerDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ const CustomerDetailsViewInner: React.FC<CustomerDetailsViewProps> = ({ id, para
);

const handleSubmit = createMetadataUpdateHandler(
user,
{
...user,
privateMetadata: user?.privateMetadata || [],
},
updateData,
variables => updateMetadata({ variables }),
variables => updatePrivateMetadata({ variables }),
Expand Down
1 change: 0 additions & 1 deletion src/fragments/customers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export const customerFragment = gql`
export const customerDetailsFragment = gql`
fragment CustomerDetails on User {
...Customer
...Metadata
dateJoined
lastLogin
defaultShippingAddress {
Expand Down
21 changes: 0 additions & 21 deletions src/fragments/giftCards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,6 @@ export const giftCardEventsFragment = gql`
id
date
type
user {
...UserBase
email
avatar(size: 128) {
url
}
}
app {
id
name
brand {
logo {
default(size: 128)
}
}
}
message
email
orderId
Expand Down Expand Up @@ -76,10 +60,6 @@ export const giftCardDataFragment = gql`
}
usedByEmail
createdByEmail
app {
id
name
}
created
expiryDate
lastUsedOn
Expand All @@ -90,7 +70,6 @@ export const giftCardDataFragment = gql`
currentBalance {
...Money
}
id
tags {
name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import { AppPaths } from "@dashboard/apps/urls";
import Link from "@dashboard/components/Link";
import { TimelineEvent } from "@dashboard/components/Timeline";
import { customerPath } from "@dashboard/customers/urls";
import { GiftCardEventFragment, GiftCardEventsEnum } from "@dashboard/graphql";
import { GiftCardEventsEnum, GiftCardEventsQuery } from "@dashboard/graphql";
import { orderUrl } from "@dashboard/orders/urls";
import { staffMemberDetailsUrl } from "@dashboard/staff/urls";
import React from "react";
import { IntlShape, useIntl } from "react-intl";

import { giftCardHistoryTimelineMessages as timelineMessages } from "./messages";

const getUserOrApp = (event: GiftCardEventFragment): string | null => {
type GiftCardEventType = GiftCardEventsQuery["giftCard"]["events"][0];

const getUserOrApp = (event: GiftCardEventType): string | null => {
if (event.user) {
const { firstName, lastName, email } = event.user;

Expand All @@ -28,7 +30,7 @@ const getUserOrApp = (event: GiftCardEventFragment): string | null => {

return null;
};
const getUserOrAppUrl = (event: GiftCardEventFragment): string => {
const getUserOrAppUrl = (event: GiftCardEventType): string => {
if (event.user) {
return staffMemberDetailsUrl(event.user.id);
}
Expand All @@ -39,7 +41,7 @@ const getUserOrAppUrl = (event: GiftCardEventFragment): string => {

return null;
};
const getEventMessage = (event: GiftCardEventFragment, intl: IntlShape) => {
const getEventMessage = (event: GiftCardEventType, intl: IntlShape) => {
const user = getUserOrApp(event);
const userUrl = getUserOrAppUrl(event);

Expand Down Expand Up @@ -107,7 +109,7 @@ const getEventMessage = (event: GiftCardEventFragment, intl: IntlShape) => {

export interface GiftCardTimelineEventProps {
date: string;
event: GiftCardEventFragment;
event: GiftCardEventType;
}

const GiftCardTimelineEvent: React.FC<GiftCardTimelineEventProps> = ({ date, event }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { renderHook } from "@testing-library/react-hooks";
import React from "react";

import { giftCardsMocks } from "../../../../../testUtils/mocks/giftCards";
import { useUser } from "../../../../auth";
import { useGiftCardEventsQuery } from "../../../../graphql";
import { GiftCardDetailsContext } from "../../providers/GiftCardDetailsProvider";
import useGiftCardHistoryEvents from "./useGiftCardHistoryEvents";

// Mock the dependencies
jest.mock("@dashboard/auth");
jest.mock("@dashboard/graphql");

const mockUseUser = useUser as jest.Mock;
const mockUseGiftCardEventsQuery = useGiftCardEventsQuery as jest.Mock;

const mockGiftCard = {
...giftCardsMocks[0],
id: "giftCardId",
};
const mockUser = {
userPermissions: [{ code: "MANAGE_USERS" }, { code: "MANAGE_APPS" }],
};

mockUseUser.mockReturnValue({ user: mockUser });

describe("useGiftCardHistoryEvents", () => {
it("should return gift card events", () => {
// Arrange
mockUseGiftCardEventsQuery.mockImplementation(() => ({
data: {
giftCard: {
events: [
{ id: "event1", app: {}, user: {} },
{ id: "event2", app: {}, user: {} },
],
},
},
}));

const wrapper = ({ children }: { children: React.ReactNode }) => (
<GiftCardDetailsContext.Provider value={{ giftCard: mockGiftCard, loading: false }}>
{children}
</GiftCardDetailsContext.Provider>
);

// Act
const { result } = renderHook(() => useGiftCardHistoryEvents(), { wrapper });

// Assert
expect(result.current.id).toBe("giftCardId");
expect(result.current.events).toEqual([
{ id: "event1", app: {}, user: {} },
{ id: "event2", app: {}, user: {} },
]);
expect(mockUseGiftCardEventsQuery).toBeCalledWith({
variables: { id: "giftCardId", canSeeApp: true, canSeeUser: true },
skip: false,
});
});

it("should return undefined when there is no gift card", () => {
// Arrange
mockUseGiftCardEventsQuery.mockImplementation(() => ({
data: undefined,
}));

const wrapper = ({ children }: { children: React.ReactNode }) => (
<GiftCardDetailsContext.Provider value={{ giftCard: undefined, loading: false }}>
{children}
</GiftCardDetailsContext.Provider>
);

// Act
const { result } = renderHook(() => useGiftCardHistoryEvents(), { wrapper });

// Assert
expect(result.current.id).toBeUndefined();
expect(result.current.events).toBeUndefined();
expect(mockUseGiftCardEventsQuery).toBeCalledWith({
variables: undefined,
skip: true,
});
});

it("should not return app and user when user does not have permissions", () => {
// Arrange
mockUseUser.mockReturnValue({ user: { userPermissions: [] } });
mockUseGiftCardEventsQuery.mockImplementation(() => ({
data: {
giftCard: {
events: [{ id: "event1" }, { id: "event2" }],
},
},
}));

const wrapper = ({ children }: { children: React.ReactNode }) => (
<GiftCardDetailsContext.Provider value={{ giftCard: mockGiftCard, loading: false }}>
{children}
</GiftCardDetailsContext.Provider>
);

// Act
const { result } = renderHook(() => useGiftCardHistoryEvents(), { wrapper });

// Assert
expect(result.current.id).toBe("giftCardId");
expect(result.current.events).toEqual([{ id: "event1" }, { id: "event2" }]);
expect(mockUseGiftCardEventsQuery).toBeCalledWith({
variables: { id: "giftCardId", canSeeApp: false, canSeeUser: false },
skip: false,
});
});
});
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
import { useGiftCardPermissions } from "@dashboard/giftCards/hooks/useGiftCardPermissions";
import { GiftCardEventsQuery, useGiftCardEventsQuery } from "@dashboard/graphql";
import { useContext } from "react";

import { GiftCardDetailsContext } from "../../providers/GiftCardDetailsProvider";

const useGiftCardHistoryEvents = () => {
interface GiftCardHistoryEvents {
id: string | undefined;
events: NonNullable<GiftCardEventsQuery["giftCard"]>["events"] | null | undefined;
}

const useGiftCardHistoryEvents = (): GiftCardHistoryEvents => {
const { canSeeApp, canSeeUser } = useGiftCardPermissions();

const { giftCard } = useContext(GiftCardDetailsContext);
const { data } = useGiftCardEventsQuery({
variables: giftCard
? {
canSeeApp,
canSeeUser,
id: giftCard.id,
}
: undefined,
skip: !giftCard?.id,
});

return {
id: giftCard?.id,
events: giftCard?.events,
events: data?.giftCard?.events,
};
};

Expand Down
4 changes: 2 additions & 2 deletions src/giftCards/GiftCardUpdate/GiftCardHistory/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ const giftCardHistoryTimelineMessages = defineMessages({
description: "gift card history message",
},
balanceResetAnonymous: {
id: "aEc9Ar",
defaultMessage: "Gift card balance was reset by {resetBy}",
id: "FWaL+x",
defaultMessage: "Gift card balance was reset",
description: "gift card history message",
},
bought: {
Expand Down
27 changes: 27 additions & 0 deletions src/giftCards/GiftCardUpdate/GiftCardHistory/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { gql } from "@apollo/client";

export const giftCardEvents = gql`
query GiftCardEvents($id: ID!, $canSeeApp: Boolean!, $canSeeUser: Boolean!) {
giftCard(id: $id) {
events {
...GiftCardEvent
app @include(if: $canSeeApp) {
id
name
brand {
logo {
default(size: 128)
}
}
}
user @include(if: $canSeeUser) {
...UserBase
email
avatar(size: 128) {
url
}
}
}
}
}
`;
Loading

0 comments on commit edaf42b

Please sign in to comment.