Skip to content

Gsn member profile - #313

Open
nikithaguduru wants to merge 12 commits into
mainfrom
gsn_member_profile
Open

Gsn member profile#313
nikithaguduru wants to merge 12 commits into
mainfrom
gsn_member_profile

Conversation

@nikithaguduru

@nikithaguduru nikithaguduru commented Aug 6, 2026

Copy link
Copy Markdown

Summary by Sourcery

Introduce a member-facing community portal with self-service profile management and supporting GraphQL APIs.

New Features:

  • Add a member portal route with layout, navigation, and home page for community members.
  • Enable community members to view and edit their own profile via a dedicated self mode in the shared MemberProfileContainer.
  • Expose new GraphQL queries and mutations for fetching and updating the current user’s member profile.

Bug Fixes:

  • Prevent members from updating profiles that belong to other users by enforcing actor/member identity checks.

Enhancements:

  • Refine profile visibility modeling with dedicated input types for self-service updates.
  • Improve routing and Storybook setup in the accounts module, including separate exports for Accounts and Member flows.

Tests:

  • Add unit tests for member profile update authorization and payload construction for self vs admin profile updates.
  • Add UI routing tests for the accounts module to verify home and create-community page rendering.

@nikithaguduru
nikithaguduru requested a review from a team August 6, 2026 13:05
@sourcery-ai

sourcery-ai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds a self-service member profile experience for community members, including front-end routing/layout, GraphQL schema and resolvers, and backend authorization for updating one’s own profile distinct from admin-driven updates.

Sequence diagram for self-service member profile update

sequenceDiagram
    actor User
    participant MemberProfilePage
    participant MemberProfileContainer
    participant ApolloClient
    participant GraphQLMemberResolvers
    participant CommunityMemberService as CommunityMember.updateMemberProfile
    participant MemberRepository as MemberRepository

    User->>MemberProfilePage: Navigate to /:communityId/member/:memberId/profile
    MemberProfilePage->>MemberProfileContainer: render(mode=self)
    User->>MemberProfileContainer: submit MemberProfileFormValues
    MemberProfileContainer->>ApolloClient: memberUpdateMyProfile(communityId, input)
    ApolloClient->>GraphQLMemberResolvers: memberUpdateMyProfile(communityId, input)
    GraphQLMemberResolvers->>GraphQLMemberResolvers: getActorMemberIdForCommunity(communityId)
    GraphQLMemberResolvers->>CommunityMemberService: updateMemberProfile({ memberId: actorMemberId, actorMemberId, profile })
    CommunityMemberService->>MemberRepository: getById(memberId)
    MemberRepository-->>CommunityMemberService: member
    CommunityMemberService->>CommunityMemberService: check actorMemberId === memberId
    CommunityMemberService-->>GraphQLMemberResolvers: updated member
    GraphQLMemberResolvers-->>ApolloClient: memberUpdateMyProfile.status, member
    ApolloClient-->>MemberProfileContainer: mutation result
    MemberProfileContainer->>MemberProfileContainer: message.success('Profile updated')
    MemberProfileContainer-->>User: updated profile shown
Loading

Flow diagram for member self-service profile routing

flowchart LR
    A[Route /:communityId/member/:memberId/* in App] --> B[Member component]
    B --> C[MemberSectionLayoutContainer]
    C --> D[MemberSectionLayout]
    D --> E[Route "" -> MemberHome]
    D --> F[Route "profile/*" -> MemberProfilePage]
    F --> G[MemberProfileContainer mode=self]
    G --> H[memberMyProfile query]
    G --> I[memberUpdateMyProfile mutation]
Loading

File-Level Changes

Change Details Files
Introduce dual-mode member profile saving logic (admin vs self) and wire up self-profile queries/mutations in the shared UI container.
  • Extend MemberProfileContainer props with a mode flag and infer self mode from route params when no memberId is present but communityId is.
  • Add buildMemberProfileSaveVariables helper with overloads to construct mutation payloads differently for self-profile and admin-profile updates.
  • Add useMutation hook for memberUpdateMyProfile and update Apollo cache via SharedMemberProfileContainerMemberSelfProfileDocument.
  • Add self-profile query (memberMyProfile) and useQuery hook, switching between admin and self data/loading/error based on mode.
  • Update handleSave to branch between self-profile mutation and admin mutation with appropriate error/success handling.
  • Adjust ComponentQueryLoader and MemberProfileContainer props to consume either self or admin member data and loading state.
  • Add unit tests for buildMemberProfileSaveVariables to validate payload shapes for self and admin modes.
packages/ocom/ui-community-shared/src/components/member-profile.container.tsx
packages/ocom/ui-community-shared/src/components/member-profile.container.graphql
packages/ocom/ui-community-shared/src/components/member-profile.container.test.ts
Add GraphQL API support for fetching and updating the current user’s member profile with proper authorization and domain command updates.
  • Extend member.graphql with memberMyProfile query, memberUpdateMyProfile mutation, visibility input types, and self-profile input type.
  • Implement memberMyProfile resolver that uses verified user context and getActorMemberIdForCommunity to return the actor’s member record for a community.
  • Implement memberUpdateMyProfile resolver that validates authorization, builds a MemberUpdateProfileCommand including actorMemberId, maps visibility/bio/interests fields, and calls updateMemberProfile.
  • Update MemberUpdateProfileCommand to include optional actorMemberId and enforce that actorMemberId must match memberId inside updateMemberProfile, throwing on mismatch.
  • Add application-services test ensuring updateMemberProfile rejects attempts where actorMemberId differs from memberId.
packages/ocom/graphql/src/schema/types/member.graphql
packages/ocom/graphql/src/schema/types/member.resolvers.ts
packages/ocom/application-services/src/contexts/community/member/member-management.ts
packages/ocom/application-services/src/contexts/community/member/member-management.operations.test.ts
Create a dedicated member portal section (layout, routing, and pages) for community members, including a home dashboard and profile page wired to the new self-profile API.
  • Expose Member component from ui-community-route-accounts and wire it into the main App routes under /:communityId/member/:memberId/*.
  • Create MemberSectionLayout with header (communities dropdown, back-to-accounts link, logged-in user) and collapsible sidebar menu driven by PageLayoutProps, persisting collapse state in localStorage.
  • Add MemberSectionLayoutContainer that loads membersForCurrentEndUser and selects the member matching the route memberId to feed into layout.
  • Add MemberHome page that queries memberMyProfile by communityId and renders a welcome panel plus basic member/community details.
  • Add MemberProfilePage that renders MemberProfileContainer in self mode.
  • Add GraphQL documents for MemberSectionLayout and MemberHome (queries and fragments).
  • Add CSS for member-section layout styling and box shadows.
  • Add Accounts tests to verify routing renders home and create-community pages after refactoring Accounts exports and stories to use accounts.tsx.
apps/ui-community/src/App.tsx
packages/ocom/ui-community-route-accounts/src/index.tsx
packages/ocom/ui-community-route-accounts/src/accounts.test.tsx
packages/ocom/ui-community-route-accounts/src/pages/create-community.stories.tsx
packages/ocom/ui-community-route-accounts/src/pages/home.stories.tsx
packages/ocom/ui-community-route-accounts/src/member.tsx
packages/ocom/ui-community-route-accounts/src/member-section-layout.tsx
packages/ocom/ui-community-route-accounts/src/components/member-section-layout.container.tsx
packages/ocom/ui-community-route-accounts/src/member-section-layout.css
packages/ocom/ui-community-route-accounts/src/member-section-layout.graphql
packages/ocom/ui-community-route-accounts/src/member-home.graphql
packages/ocom/ui-community-route-accounts/src/pages/member-home.tsx
packages/ocom/ui-community-route-accounts/src/pages/member-profile.tsx

Possibly linked issues

  • #[Community][Member] Member Profile Management Page: PR implements core member profile management: self-profile query/mutation, UI profile page, visibility settings, and permission checks.
  • #[Community][Member]: PR implements core member profile edit/view flows and self-profile GraphQL APIs, directly advancing the profile migration issue

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • In MemberSectionLayoutContainer, if the current memberId isn’t found in membersForCurrentEndUser the component still renders MemberSectionLayout with an undefined memberData, which will likely blow up at runtime; consider explicitly handling the “member not found” case (e.g., by showing an error or redirect) instead of casting with as MemberSectionLayoutContainerMemberFieldsFragment.
  • The member routes and links appear inconsistent: App.tsx registers the member route as /:communityId/member/:memberId/*, but the Member pageLayouts use /community/:communityId/member/:memberId and the header link goes to /community/accounts while the accounts route is /accounts/*; aligning these paths will avoid broken navigation and menu highlighting issues.
  • The buildMemberProfileSaveVariables tests pass memberObjectId and communityId into both self and admin calls even though the self overload doesn’t declare memberObjectId, which will trigger excess property checking in TypeScript; adjust the helper’s types or the test call sites so the argument shapes match the overload signatures without relying on unsafe casting.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In MemberSectionLayoutContainer, if the current memberId isn’t found in membersForCurrentEndUser the component still renders MemberSectionLayout with an undefined memberData, which will likely blow up at runtime; consider explicitly handling the “member not found” case (e.g., by showing an error or redirect) instead of casting with `as MemberSectionLayoutContainerMemberFieldsFragment`.
- The member routes and links appear inconsistent: App.tsx registers the member route as `/:communityId/member/:memberId/*`, but the Member pageLayouts use `/community/:communityId/member/:memberId` and the header link goes to `/community/accounts` while the accounts route is `/accounts/*`; aligning these paths will avoid broken navigation and menu highlighting issues.
- The `buildMemberProfileSaveVariables` tests pass `memberObjectId` and `communityId` into both self and admin calls even though the self overload doesn’t declare `memberObjectId`, which will trigger excess property checking in TypeScript; adjust the helper’s types or the test call sites so the argument shapes match the overload signatures without relying on unsafe casting.

## Individual Comments

### Comment 1
<location path="packages/ocom/graphql/src/schema/types/member.resolvers.ts" line_range="777-786" />
<code_context>
 		},
+
+		// ...existing code...
+		memberUpdateMyProfile: async (_parent: unknown, args: MutationMemberUpdateMyProfileArgs, context: GraphContext) => {
+			try {
+				if (!context.applicationServices.verifiedUser?.verifiedJwt) {
+					return {
+						status: { success: false, errorMessage: 'Unauthorized' },
+						member: null,
+					};
+				}
+
+				const actorMemberId = await getActorMemberIdForCommunity(context, String(args.communityId));
+				if (!actorMemberId) {
+					return {
+						status: { success: false, errorMessage: 'Forbidden' },
+						member: null,
+					};
+				}
+
+				const command: MemberUpdateProfileCommand = {
+					memberId: actorMemberId,
+					actorMemberId,
</code_context>
<issue_to_address>
**issue (bug_risk):** Self-profile visibility flags are only partially mapped to the domain model.

In `memberUpdateMyProfile`, the `visibility` input is only applied to `showInterests` and `showEmail`, while `showProfile`, `showLocation`, and `showProperties` are ignored. If these flags are meant to be user-editable via self-update, they should also be included in the `MemberUpdateProfileCommand` payload; otherwise, self updates will diverge from admin-initiated updates in how visibility is handled.
</issue_to_address>

### Comment 2
<location path="packages/ocom/ui-community-route-accounts/src/components/member-section-layout.container.tsx" line_range="25" />
<code_context>
+				hasDataComponent={
</code_context>
<issue_to_address>
**issue (bug_risk):** Member lookup by route param may return undefined but is cast and passed as non-null.

If no member in `membersForCurrentEndUser` has an `id` matching the `memberId` route param, `find(...)` returns `undefined`, but it’s cast to `MemberSectionLayoutContainerMemberFieldsFragment` and passed to `MemberSectionLayout`. This will cause runtime errors when `memberData` is dereferenced. Please add a guard for the missing-member case (e.g., choose a fallback member, render an error state, or use `hasData`/`hasDataComponent` so the loader handles it) instead of relying on the cast.
</issue_to_address>

### Comment 3
<location path="packages/ocom/ui-community-shared/src/components/member-profile.container.tsx" line_range="56" />
<code_context>
+					name: 'Jane Doe',
+					email: 'jane@example.com',
+					bio: 'Hello there',
+					interests: [],
+					visibility: {
+						showEmail: true,
</code_context>
<issue_to_address>
**question (bug_risk):** Self-profile updates always send an empty interests array, potentially clearing existing interests.

In the self-mode branch of `buildMemberProfileSaveVariables`, `interests` is always set to `[]`. Because the GraphQL field is `[String!]!`, every self-profile save will clear any existing interests. If interests should be preserved, either include them in `MemberProfileFormValues` and map them through, or avoid setting the field at all so existing values remain unchanged.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +777 to +786
memberUpdateMyProfile: async (_parent: unknown, args: MutationMemberUpdateMyProfileArgs, context: GraphContext) => {
try {
if (!context.applicationServices.verifiedUser?.verifiedJwt) {
return {
status: { success: false, errorMessage: 'Unauthorized' },
member: null,
};
}

const actorMemberId = await getActorMemberIdForCommunity(context, String(args.communityId));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Self-profile visibility flags are only partially mapped to the domain model.

In memberUpdateMyProfile, the visibility input is only applied to showInterests and showEmail, while showProfile, showLocation, and showProperties are ignored. If these flags are meant to be user-editable via self-update, they should also be included in the MemberUpdateProfileCommand payload; otherwise, self updates will diverge from admin-initiated updates in how visibility is handled.

<MemberSectionLayout
pageLayouts={props.pageLayouts}
// biome-ignore lint:useLiteralKeys
memberData={membersData?.membersForCurrentEndUser.find((member: MemberSectionLayoutContainerMemberFieldsFragment) => member.id === params['memberId']) as MemberSectionLayoutContainerMemberFieldsFragment}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Member lookup by route param may return undefined but is cast and passed as non-null.

If no member in membersForCurrentEndUser has an id matching the memberId route param, find(...) returns undefined, but it’s cast to MemberSectionLayoutContainerMemberFieldsFragment and passed to MemberSectionLayout. This will cause runtime errors when memberData is dereferenced. Please add a guard for the missing-member case (e.g., choose a fallback member, render an error state, or use hasData/hasDataComponent so the loader handles it) instead of relying on the cast.

name: values.name,
email: values.email,
bio: values.bio,
interests: [],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (bug_risk): Self-profile updates always send an empty interests array, potentially clearing existing interests.

In the self-mode branch of buildMemberProfileSaveVariables, interests is always set to []. Because the GraphQL field is [String!]!, every self-profile save will clear any existing interests. If interests should be preserved, either include them in MemberProfileFormValues and map them through, or avoid setting the field at all so existing values remain unchanged.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undo all the changes in this file. We do always need func core tools installed and available on the build machine, bc the e2e-tests need func to be available at runtime when it's going through those serenity scenarios

const member = await repository.getById(command.memberId);
const profile = member.profile;

if (command.actorMemberId && String(command.actorMemberId) !== String(command.memberId)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This permission check is not expected to be in the application services. The application services automatically scopes a passport for the acting user on the initialized member unit of work you're using here. The permission checks should be enforced through the domain aggregate/entity classes and their setters.

When you call profile.name = command.profile.name, that is actually hitting the set name on the MemberProfile entity. so that setter is where we would want to actually enforce the expected domain permission check of permissions.isEditingOwnMember (which the member's visa will perform the Object ID comparison for you automatically). Let me know if you have any questions, we can go over this in more detail.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this .test.ts file, we expect to be using .stories.tsx files to test the individual UI components/pages. I would expect a member-profile.container.stories.tsx here instead to replace this file and member-profile.container.test.tsx.

We similarly need accompanying .stories.tsx files for all the new pages and components that were added to support this new member functionality, i.e. member-home.stories.tsx, `member-profile.stories.tsx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants