From 03eaeca8a6b1d5e65e2c148e7651e70e1f0fb683 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Mon, 24 Feb 2025 14:28:06 +0100 Subject: [PATCH 1/2] Only show the password change section if the user has a password --- crates/handlers/src/graphql/model/users.rs | 10 ++++++++++ frontend/schema.graphql | 4 ++++ frontend/src/gql/gql.ts | 6 +++--- frontend/src/gql/graphql.ts | 5 ++++- frontend/src/routes/_account.index.lazy.tsx | 2 +- frontend/src/routes/_account.index.tsx | 1 + 6 files changed, 23 insertions(+), 5 deletions(-) diff --git a/crates/handlers/src/graphql/model/users.rs b/crates/handlers/src/graphql/model/users.rs index 9287c62da..fb54580a0 100644 --- a/crates/handlers/src/graphql/model/users.rs +++ b/crates/handlers/src/graphql/model/users.rs @@ -705,6 +705,16 @@ impl User { ) .await } + + /// Check if the user has a password set. + async fn has_password(&self, ctx: &Context<'_>) -> Result { + let state = ctx.state(); + let mut repo = state.repository().await?; + + let password = repo.user_password().active(&self.0).await?; + + Ok(password.is_some()) + } } /// A session in an application, either a compatibility or an OAuth 2.0 one diff --git a/frontend/schema.graphql b/frontend/schema.graphql index f63f449df..7ae680ec9 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -2059,6 +2059,10 @@ type User implements Node { """ last: Int ): AppSessionConnection! + """ + Check if the user has a password set. + """ + hasPassword: Boolean! } """ diff --git a/frontend/src/gql/gql.ts b/frontend/src/gql/gql.ts index 0254e2a1b..4561dfad1 100644 --- a/frontend/src/gql/gql.ts +++ b/frontend/src/gql/gql.ts @@ -42,7 +42,7 @@ type Documents = { "\n query UserEmailList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n viewer {\n __typename\n ... on User {\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n": typeof types.UserEmailListDocument, "\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": typeof types.UserEmailList_SiteConfigFragmentDoc, "\n fragment BrowserSessionsOverview_user on User {\n id\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": typeof types.BrowserSessionsOverview_UserFragmentDoc, - "\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": typeof types.UserProfileDocument, + "\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": typeof types.UserProfileDocument, "\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": typeof types.BrowserSessionListDocument, "\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n": typeof types.SessionsOverviewDocument, "\n query AppSessionsList(\n $before: String\n $after: String\n $first: Int\n $last: Int\n $lastActive: DateFilter\n ) {\n viewer {\n __typename\n\n ... on User {\n id\n appSessions(\n before: $before\n after: $after\n first: $first\n last: $last\n lastActive: $lastActive\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n }\n }\n": typeof types.AppSessionsListDocument, @@ -91,7 +91,7 @@ const documents: Documents = { "\n query UserEmailList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n viewer {\n __typename\n ... on User {\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n": types.UserEmailListDocument, "\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": types.UserEmailList_SiteConfigFragmentDoc, "\n fragment BrowserSessionsOverview_user on User {\n id\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": types.BrowserSessionsOverview_UserFragmentDoc, - "\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": types.UserProfileDocument, + "\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": types.UserProfileDocument, "\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": types.BrowserSessionListDocument, "\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n": types.SessionsOverviewDocument, "\n query AppSessionsList(\n $before: String\n $after: String\n $first: Int\n $last: Int\n $lastActive: DateFilter\n ) {\n viewer {\n __typename\n\n ... on User {\n id\n appSessions(\n before: $before\n after: $after\n first: $first\n last: $last\n lastActive: $lastActive\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n }\n }\n": types.AppSessionsListDocument, @@ -224,7 +224,7 @@ export function graphql(source: "\n fragment BrowserSessionsOverview_user on Us /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n"): typeof import('./graphql').UserProfileDocument; +export function graphql(source: "\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n"): typeof import('./graphql').UserProfileDocument; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index 6564a67da..b0b581717 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -1338,6 +1338,8 @@ export type User = Node & { createdAt: Scalars['DateTime']['output']; /** Get the list of emails, chronologically sorted */ emails: UserEmailConnection; + /** Check if the user has a password set. */ + hasPassword: Scalars['Boolean']['output']; /** ID of the object. */ id: Scalars['ID']['output']; /** When the user was locked out. */ @@ -1687,7 +1689,7 @@ export type BrowserSessionsOverview_UserFragment = { __typename?: 'User', id: st export type UserProfileQueryVariables = Exact<{ [key: string]: never; }>; -export type UserProfileQuery = { __typename?: 'Query', viewerSession: { __typename: 'Anonymous' } | { __typename: 'BrowserSession', id: string, user: { __typename?: 'User', emails: { __typename?: 'UserEmailConnection', totalCount: number } } } | { __typename: 'Oauth2Session' }, siteConfig: ( +export type UserProfileQuery = { __typename?: 'Query', viewerSession: { __typename: 'Anonymous' } | { __typename: 'BrowserSession', id: string, user: { __typename?: 'User', hasPassword: boolean, emails: { __typename?: 'UserEmailConnection', totalCount: number } } } | { __typename: 'Oauth2Session' }, siteConfig: ( { __typename?: 'SiteConfig', emailChangeAllowed: boolean, passwordLoginEnabled: boolean } & { ' $fragmentRefs'?: { 'UserEmailList_SiteConfigFragment': UserEmailList_SiteConfigFragment;'UserEmail_SiteConfigFragment': UserEmail_SiteConfigFragment;'PasswordChange_SiteConfigFragment': PasswordChange_SiteConfigFragment } } ) }; @@ -2302,6 +2304,7 @@ export const UserProfileDocument = new TypedDocumentString(` ... on BrowserSession { id user { + hasPassword emails(first: 0) { totalCount } diff --git a/frontend/src/routes/_account.index.lazy.tsx b/frontend/src/routes/_account.index.lazy.tsx index 46e52fbf9..22ea129a6 100644 --- a/frontend/src/routes/_account.index.lazy.tsx +++ b/frontend/src/routes/_account.index.lazy.tsx @@ -94,7 +94,7 @@ function Index(): React.ReactElement { )} - {siteConfig.passwordLoginEnabled && ( + {siteConfig.passwordLoginEnabled && viewerSession.user.hasPassword && ( <> Date: Mon, 24 Feb 2025 14:40:04 +0100 Subject: [PATCH 2/2] Include the new GraphQL property in test mocks --- frontend/stories/routes/index.stories.tsx | 10 ++++++++++ frontend/tests/mocks/handlers.ts | 1 + 2 files changed, 11 insertions(+) diff --git a/frontend/stories/routes/index.stories.tsx b/frontend/stories/routes/index.stories.tsx index 15fb4dedd..5be20d35f 100644 --- a/frontend/stories/routes/index.stories.tsx +++ b/frontend/stories/routes/index.stories.tsx @@ -34,11 +34,13 @@ const userProfileHandler = ({ passwordLoginEnabled, passwordChangeAllowed, emailTotalCount, + hasPassword, }: { emailChangeAllowed: boolean; passwordLoginEnabled: boolean; passwordChangeAllowed: boolean; emailTotalCount: number; + hasPassword: boolean; }): GraphQLHandler => mockUserProfileQuery(() => HttpResponse.json({ @@ -47,6 +49,7 @@ const userProfileHandler = ({ __typename: "BrowserSession", id: "session-id", user: { + hasPassword, emails: { totalCount: emailTotalCount, }, @@ -130,6 +133,7 @@ export const MultipleEmails: Story = { passwordChangeAllowed: true, emailChangeAllowed: true, emailTotalCount: 3, + hasPassword: true, }), threeEmailsHandler, ], @@ -147,6 +151,7 @@ export const NoEmails: Story = { passwordChangeAllowed: true, emailChangeAllowed: false, emailTotalCount: 0, + hasPassword: true, }), ], }, @@ -163,6 +168,7 @@ export const MultipleEmailsNoChange: Story = { passwordChangeAllowed: true, emailChangeAllowed: false, emailTotalCount: 3, + hasPassword: true, }), threeEmailsHandler, ], @@ -180,6 +186,7 @@ export const NoEmailChange: Story = { passwordChangeAllowed: true, emailChangeAllowed: false, emailTotalCount: 1, + hasPassword: true, }), ], }, @@ -196,6 +203,7 @@ export const NoPasswordChange: Story = { passwordChangeAllowed: false, emailChangeAllowed: true, emailTotalCount: 1, + hasPassword: true, }), ], }, @@ -212,6 +220,7 @@ export const NoPasswordLogin: Story = { passwordChangeAllowed: false, emailChangeAllowed: true, emailTotalCount: 1, + hasPassword: true, }), ], }, @@ -228,6 +237,7 @@ export const NoPasswordNoEmailChange: Story = { passwordChangeAllowed: false, emailChangeAllowed: false, emailTotalCount: 0, + hasPassword: false, }), ], }, diff --git a/frontend/tests/mocks/handlers.ts b/frontend/tests/mocks/handlers.ts index b89347ccc..83719c2e3 100644 --- a/frontend/tests/mocks/handlers.ts +++ b/frontend/tests/mocks/handlers.ts @@ -91,6 +91,7 @@ export const handlers = [ __typename: "BrowserSession", id: "browser-session-id", user: { + hasPassword: true, emails: { totalCount: 1, },