Maintenance: Do not establish a session when verifying a signup email address.

Approved-by: Dusan Vuckovic <dv@zammad.com>

Co-authored-by: Mantas <mantas@idev.lt>
This commit is contained in:
Mantas Masalskis
2026-09-22 13:38:33 +03:00
committed by Florian Liebe
parent 9a1439e7fc
commit 1e486439ef
18 changed files with 218 additions and 159 deletions

View File

@@ -153,6 +153,32 @@ requires `admin.organization`, and agent tokens receive `403 Forbidden`.
## 7.2
### Verifying a signup email address no longer signs the user in
**Who is affected?** Integrations that redeem a signup verification token through
`POST /api/v1/users/email_verify` or the `userSignupVerify` GraphQL mutation and rely on the session
that the redemption used to establish, and admins who send a verification or password reset link to a
deactivated account.
Redeeming the token established a fully authenticated session for the account, without the account's
password and without the second factor configured for it. Both endpoints now only mark the email
address as verified and establish no session. The REST endpoint still answers
`200 {"message":"ok","user_email":"…"}`, but the response no longer carries a session cookie. The
mutation's payload field `session` was replaced by a boolean `success`, so a client selecting
`session { id }` receives a GraphQL error.
A non-persistent token — `Signup`, `PasswordReset`, `AdminAuth`, `KnowledgeBasePreview` — is now
rejected when it belongs to an inactive user. Previously only persistent tokens were checked this
way. The mail carrying such a link is still sent, but the link itself no longer works while the
account is deactivated.
⚠️ Sign in through `POST /api/v1/signin` or the `login` mutation after redeeming a verification
token, and pass the configured second factor there. Change GraphQL clients to select `success`
instead of `session`. Activate a deactivated account before sending it a verification or password
reset link.
**Related advisory:** [GHSA-f3qr-94c2-7mx2](https://github.com/zammad/zammad/security/advisories/GHSA-f3qr-94c2-7mx2)
### The getting started endpoints require `admin.wizard` once the system is set up
**Who is affected?** Integrations that read `GET /api/v1/getting_started` or

View File

@@ -35,7 +35,7 @@ class Success extends App.ControllerAppContent
detail: __('Woo hoo! Your email address has been verified!')
)
delay = =>
App.Auth.loginCheck(=> @navigate '#')
@navigate '#login'
@delay(delay, 2000)
class Fail extends App.ControllerAppContent

View File

@@ -287,8 +287,6 @@ curl http://localhost/api/v1/users/email_verify -v -u #{login}:#{password} -H "C
user = Service::User::SignupVerify.with_current_user(false).execute(token: params[:token])
current_user_set(user) if user
msg = user ? { message: 'ok', user_email: user.email } : { message: 'failed' }
render json: msg, status: :ok

View File

@@ -53,7 +53,7 @@ describe('signup verify view', () => {
it('shows an error message when an invalid token is supplied', async () => {
mockUserSignupVerifyMutation({
userSignupVerify: {
session: null,
success: null,
errors: [{ message: 'The provided token is invalid.' }],
},
})
@@ -66,14 +66,29 @@ describe('signup verify view', () => {
})
it('shows a success message when a valid token is supplied', async () => {
mockUserSignupVerifyMutation({
userSignupVerify: {
success: true,
errors: null,
},
})
const view = await visitView('/signup/verify/123')
expect(
await view.findByText('Woo hoo! Your email address has been verified!'),
).toBeInTheDocument()
expect(view.getByText('Please sign in to continue.')).toBeInTheDocument()
})
it('redirects to dashboard screen when the verification was successful', async () => {
it('redirects to the login screen when the verification was successful', async () => {
mockUserSignupVerifyMutation({
userSignupVerify: {
success: true,
errors: null,
},
})
vi.useFakeTimers()
await visitView('/signup/verify/123')
@@ -84,7 +99,7 @@ describe('signup verify view', () => {
await waitFor(() => {
const router = getTestRouter()
const route = router.currentRoute.value
expect(route.name).toBe('Dashboard')
expect(route.name).toBe('Login')
})
})
})

View File

@@ -1,7 +1,6 @@
import * as Types from '#shared/graphql/types.ts';
import gql from 'graphql-tag';
import { SessionFragmentDoc } from '../../../../../../shared/graphql/fragments/session.api';
import { ErrorsFragmentDoc } from '../../../../../../shared/graphql/fragments/errors.api';
import * as VueApolloComposable from '@vue/apollo-composable';
import * as VueCompositionApi from 'vue';
@@ -10,16 +9,13 @@ export type ReactiveFunction<TParam> = () => TParam;
export const UserSignupVerifyDocument = gql`
mutation userSignupVerify($token: String!) {
userSignupVerify(token: $token) {
session {
...session
}
success
errors {
...errors
}
}
}
${SessionFragmentDoc}
${ErrorsFragmentDoc}`;
${ErrorsFragmentDoc}`;
export function useUserSignupVerifyMutation(options: VueApolloComposable.UseMutationOptions<Types.UserSignupVerifyMutation, Types.UserSignupVerifyMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<Types.UserSignupVerifyMutation, Types.UserSignupVerifyMutationVariables>> = {}) {
return VueApolloComposable.useMutation<Types.UserSignupVerifyMutation, Types.UserSignupVerifyMutationVariables>(UserSignupVerifyDocument, options);
}

View File

@@ -1,8 +1,6 @@
mutation userSignupVerify($token: String!) {
userSignupVerify(token: $token) {
session {
...session
}
success
errors {
...errors
}

View File

@@ -4,15 +4,12 @@
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import useFingerprint from '#shared/composables/useFingerprint.ts'
import { MutationHandler } from '#shared/server/apollo/handler/index.ts'
import { useApplicationStore } from '#shared/stores/application.ts'
import { useAuthenticationStore } from '#shared/stores/authentication.ts'
import CommonLoader from '#desktop/components/CommonLoader/CommonLoader.vue'
import LayoutPublicPage from '#desktop/components/layout/LayoutPublicPage/LayoutPublicPage.vue'
import { ensureAfterAuth } from '../after-auth/composable/useAfterAuthPlugins.ts'
import { useUserSignupVerifyMutation } from '../graphql/mutations/userSignupVerify.api.ts'
import type { VerifyState } from '../types/signup.ts'
@@ -59,16 +56,9 @@ onMounted(() => {
return
}
const { fingerprint } = useFingerprint()
const userSignupVerify = new MutationHandler(
useUserSignupVerifyMutation({
variables: { token: props.token },
context: {
headers: {
'X-Browser-Fingerprint': fingerprint.value,
},
},
}),
{
errorShowNotification: false,
@@ -77,28 +67,18 @@ onMounted(() => {
userSignupVerify
.send()
.then(async (result) => {
const { setAuthenticatedSessionId } = useAuthenticationStore()
if (await setAuthenticatedSessionId(result?.userSignupVerify?.session?.id || null)) {
setState('success')
const afterAuth = result?.userSignupVerify?.session?.afterAuth
// Redirect only after some seconds, in order to give the user a chance to read the message.
window.setTimeout(() => {
if (afterAuth) {
ensureAfterAuth(router, afterAuth)
return
}
router.replace('/')
}, 2000)
.then((result) => {
if (!result?.userSignupVerify?.success) {
setState('error')
return
}
setState('error')
setState('success')
// Redirect only after some seconds, in order to give the user a chance to read the message.
window.setTimeout(() => {
router.replace('/login')
}, 2000)
})
.catch(() => {
setState('error')
@@ -112,6 +92,9 @@ onMounted(() => {
<CommonLabel>
{{ $t(message) }}
</CommonLabel>
<CommonLabel v-if="state === 'success'" class="mt-1 block">
{{ $t('Please sign in to continue.') }}
</CommonLabel>
<CommonLoader v-if="state === 'loading'" loading class="mt-9 mb-3" />
<CommonIcon
v-else-if="state === 'success'"

View File

@@ -7395,8 +7395,8 @@ export type UserSignupVerifyPayload = {
__typename?: 'UserSignupVerifyPayload';
/** Errors encountered during execution of the mutation. */
errors?: Maybe<Array<UserError>>;
/** The current session, if the verification was successful. */
session?: Maybe<Session>;
/** This indicates if the verification was successful. */
success?: Maybe<Scalars['Boolean']['output']>;
};
/** Users taskbar item */

View File

@@ -906,7 +906,7 @@ export type UserSignupVerifyMutationVariables = Exact<{
}>;
export type UserSignupVerifyMutation = { userSignupVerify: { __typename: 'UserSignupVerifyPayload', session: { __typename: 'Session', id: string, afterAuth: { __typename: 'SessionAfterAuth', type: Types.EnumAfterAuthType, data: any } | null | undefined } | null | undefined, errors: Array<{ __typename: 'UserError', message: string, messagePlaceholder: Array<string> | null | undefined, field: string | null | undefined, exception: Types.EnumUserErrorException | null | undefined }> | null | undefined } | null | undefined };
export type UserSignupVerifyMutation = { userSignupVerify: { __typename: 'UserSignupVerifyPayload', success: boolean | null | undefined, errors: Array<{ __typename: 'UserError', message: string, messagePlaceholder: Array<string> | null | undefined, field: string | null | undefined, exception: Types.EnumUserErrorException | null | undefined }> | null | undefined } | null | undefined };
export type CtiLogAttributesFragment = { __typename: 'CtiLog', id: string, direction: string, state: string, comment: string | null | undefined, from: string, to: string, fromPretty: string, toPretty: string, fromComment: string | null | undefined, toComment: string | null | undefined, done: boolean, durationWaitingTime: number | null | undefined, durationTalkingTime: number | null | undefined, createdAt: string, fromMatches: Array<{ __typename: 'CtiLogCallerIdMatch', level: string, comment: string | null | undefined, user: { __typename: 'User', id: string, internalId: number, firstname: string | null | undefined, lastname: string | null | undefined, fullname: string | null | undefined, image: string | null | undefined, vip: boolean | null | undefined, outOfOffice: boolean | null | undefined, outOfOfficeStartAt: string | null | undefined, outOfOfficeEndAt: string | null | undefined, active: boolean | null | undefined } | null | undefined }>, toMatches: Array<{ __typename: 'CtiLogCallerIdMatch', level: string, comment: string | null | undefined, user: { __typename: 'User', id: string, internalId: number, firstname: string | null | undefined, lastname: string | null | undefined, fullname: string | null | undefined, image: string | null | undefined, vip: boolean | null | undefined, outOfOffice: boolean | null | undefined, outOfOfficeStartAt: string | null | undefined, outOfOfficeEndAt: string | null | undefined, active: boolean | null | undefined } | null | undefined }> };

View File

@@ -2,22 +2,18 @@
module Gql::Mutations
class User::SignupVerify < BaseMutation
include Gql::Mutations::Concerns::HandlesAuthentication
description 'Verify a signed up user.'
argument :token, String, required: true, description: 'Verification token'
field :session, Gql::Types::SessionType, description: 'The current session, if the verification was successful.'
field :success, Boolean, description: 'This indicates if the verification was successful.'
allow_public_access!
def resolve(token:)
user = Service::User::SignupVerify.with_current_user(false).execute(token:)
Service::User::SignupVerify.with_current_user(false).execute(token:)
create_session(user, false, 'password')
authenticate_result
{ success: true }
rescue Service::User::SignupVerify::InvalidTokenError => e
error_response({ message: e.message })
end

View File

@@ -40396,12 +40396,12 @@
"deprecationReason": null
},
{
"name": "session",
"description": "The current session, if the verification was successful.",
"name": "success",
"description": "This indicates if the verification was successful.",
"args": [],
"type": {
"kind": "OBJECT",
"name": "Session",
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"isDeprecated": false,

View File

@@ -83,8 +83,8 @@ returns
return false
end
# persistent token not valid if user is inactive
return false if !inactive_user && persistent && user.active == false
# token not valid if user is inactive
return false if !inactive_user && user&.active == false
# add permission check
return false if permission && !permissions?(permission)

View File

@@ -112,14 +112,13 @@ msgstr ""
msgid "%s (option)"
msgstr ""
#: app/frontend/apps/desktop/components/CommonSelect/CommonSelect.vue:343
#: app/frontend/apps/desktop/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInput.vue:590
#: app/frontend/apps/desktop/components/Form/fields/FieldSelect/FieldSelectInput.vue:261
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDropdown.vue:354
#: app/frontend/apps/desktop/components/CommonSelect/CommonSelectItem.vue:29
#: app/frontend/apps/desktop/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInput.vue:589
#: app/frontend/apps/desktop/components/Form/fields/FieldSelect/FieldSelectInput.vue:260
#: app/frontend/apps/mobile/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInput.vue:145
#: app/frontend/apps/mobile/components/Form/fields/FieldSelect/FieldSelectInput.vue:97
#: app/frontend/apps/mobile/components/Form/fields/FieldTreeSelect/FieldTreeSelectInput.vue:112
#: app/frontend/apps/mobile/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDialog.vue:288
#: app/frontend/apps/mobile/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDialog.vue:287
#: app/frontend/shared/composables/useSelectOptions.ts:128
msgid "%s (unknown)"
msgstr ""
@@ -1607,7 +1606,7 @@ msgid "Agent idle timeout"
msgstr ""
#: app/models/role.rb:182
#: app/models/user.rb:807
#: app/models/user.rb:808
msgid "Agent limit exceeded, please check your account settings."
msgstr ""
@@ -2299,7 +2298,7 @@ msgstr ""
msgid "Assignment timeout in minutes if assigned agent is not working on it. Ticket will be shown as unassigend."
msgstr ""
#: app/models/user.rb:709
#: app/models/user.rb:710
msgid "At least one identifier (firstname, lastname, phone, mobile or email) for user is required."
msgstr ""
@@ -2316,7 +2315,7 @@ msgid "At least one object must be selected."
msgstr ""
#: app/models/role.rb:159
#: app/models/user.rb:786
#: app/models/user.rb:787
msgid "At least one user needs to have admin permissions."
msgstr ""
@@ -2713,8 +2712,8 @@ msgstr ""
#: app/assets/javascripts/app/controllers/_ai/ai_agent.coffee:403
#: app/assets/javascripts/app/controllers/_ai/provider_connections.coffee:816
#: app/assets/javascripts/app/views/generic/object_search/item_organization_members.jst.eco:5
#: app/frontend/apps/desktop/components/CommonSelect/CommonSelect.vue:473
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDropdown.vue:430
#: app/frontend/apps/desktop/components/CommonSelect/CommonSelect.vue:433
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDropdown.vue:390
msgid "Back"
msgstr ""
@@ -2726,9 +2725,9 @@ msgstr ""
msgid "Back to overview"
msgstr ""
#: app/frontend/apps/desktop/components/CommonSelect/CommonSelect.vue:473
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDropdown.vue:430
#: app/frontend/apps/mobile/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDialog.vue:181
#: app/frontend/apps/desktop/components/CommonSelect/CommonSelect.vue:433
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDropdown.vue:390
#: app/frontend/apps/mobile/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDialog.vue:180
msgid "Back to previous page"
msgstr ""
@@ -3118,7 +3117,7 @@ msgstr ""
msgid "Can you tell us what are you missing in the BETA UI?"
msgstr ""
#: app/assets/javascripts/app/controllers/getting_started/agent.coffee:87
#: app/assets/javascripts/app/controllers/getting_started/agent.coffee:92
msgid "Can't create user!"
msgstr ""
@@ -3725,10 +3724,10 @@ msgstr ""
#: app/frontend/apps/desktop/components/CommonTable/CellContent/BulkCheckbox.vue:61
#: app/frontend/apps/desktop/components/CommonTable/TableHeader.vue:435
#: app/frontend/apps/desktop/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInput.vue:681
#: app/frontend/apps/desktop/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInput.vue:680
#: app/frontend/apps/desktop/components/Form/fields/FieldDate/FieldDateTimeInput.vue:387
#: app/frontend/apps/desktop/components/Form/fields/FieldSelect/FieldSelectInput.vue:314
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInput.vue:376
#: app/frontend/apps/desktop/components/Form/fields/FieldSelect/FieldSelectInput.vue:313
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInput.vue:375
#: app/frontend/apps/mobile/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInput.vue:151
#: app/frontend/apps/mobile/components/Form/fields/FieldDate/FieldDateTimeInput.vue:165
#: app/frontend/apps/mobile/components/Form/fields/FieldSelect/FieldSelectInput.vue:124
@@ -4415,7 +4414,7 @@ msgstr ""
msgid "Core workflow"
msgstr ""
#: app/services/service/user/signup.rb:53
#: app/services/service/user/signup.rb:41
msgid "Could not create user and send verification email because import_mode setting is on."
msgstr ""
@@ -4469,7 +4468,7 @@ msgstr ""
msgid "Could not remove two-factor authentication method."
msgstr ""
#: app/services/service/user/signup.rb:51
#: app/services/service/user/signup.rb:39
msgid "Could not send user verification email because import_mode setting is on."
msgstr ""
@@ -6964,7 +6963,7 @@ msgstr ""
msgid "Email address"
msgstr ""
#: app/models/user.rb:719
#: app/models/user.rb:720
msgid "Email address '%{email}' is already used for another user."
msgstr ""
@@ -6977,7 +6976,7 @@ msgid "Email channel"
msgstr ""
#: app/assets/javascripts/app/controllers/email_verify.coffee:53
#: app/frontend/apps/desktop/pages/authentication/views/SignupVerify.vue:49
#: app/frontend/apps/desktop/pages/authentication/views/SignupVerify.vue:46
msgid "Email could not be verified. Please contact your administrator."
msgstr ""
@@ -7015,7 +7014,7 @@ msgid "Email sent to %s"
msgstr ""
#: app/frontend/apps/desktop/pages/authentication/routes.ts:111
#: app/frontend/apps/desktop/pages/authentication/views/SignupVerify.vue:110
#: app/frontend/apps/desktop/pages/authentication/views/SignupVerify.vue:90
msgid "Email verification"
msgstr ""
@@ -8822,9 +8821,9 @@ msgstr ""
msgid "Has processed"
msgstr ""
#: app/frontend/apps/desktop/components/CommonSelect/CommonSelectItem.vue:144
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDropdownItem.vue:149
#: app/frontend/apps/mobile/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDialog.vue:309
#: app/frontend/apps/desktop/components/CommonSelect/CommonSelectItem.vue:160
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDropdownItem.vue:167
#: app/frontend/apps/mobile/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDialog.vue:308
msgid "Has submenu"
msgstr ""
@@ -9838,7 +9837,7 @@ msgstr ""
msgid "Invalid client_id received!"
msgstr ""
#: app/models/user.rb:658
#: app/models/user.rb:659
msgid "Invalid email '%{email}'"
msgstr ""
@@ -9944,7 +9943,7 @@ msgstr ""
msgid "Invitation sent to %s"
msgstr ""
#: app/assets/javascripts/app/controllers/getting_started/agent.coffee:78
#: app/assets/javascripts/app/controllers/getting_started/agent.coffee:83
#: app/frontend/apps/desktop/pages/guided-setup/views/GuidedSetupManual/GuidedSetupManualInviteColleagues.vue:65
msgid "Invitation sent!"
msgstr ""
@@ -9960,7 +9959,7 @@ msgstr ""
msgid "Invite"
msgstr ""
#: app/assets/javascripts/app/controllers/getting_started/agent.coffee:10
#: app/assets/javascripts/app/controllers/getting_started/agent.coffee:15
msgid "Invite Agents"
msgstr ""
@@ -10760,7 +10759,7 @@ msgstr ""
msgid "Locale could not be found."
msgstr ""
#: app/assets/javascripts/app/controllers/knowledge_base/agent_controller.coffee:257
#: app/assets/javascripts/app/controllers/knowledge_base/agent_controller.coffee:264
msgid "Locale not found"
msgstr ""
@@ -10834,7 +10833,7 @@ msgstr ""
msgid "Lost network connection"
msgstr ""
#: app/assets/javascripts/app/lib/app_post/websocket.coffee:350
#: app/assets/javascripts/app/lib/app_post/websocket.coffee:349
msgid "Lost network connection!"
msgstr ""
@@ -11681,7 +11680,7 @@ msgstr ""
msgid "More information can be found here."
msgstr ""
#: app/models/user.rb:738
#: app/models/user.rb:739
msgid "More than 250 secondary organizations are not allowed."
msgstr ""
@@ -12404,7 +12403,7 @@ msgstr ""
msgid "No editable knowledge base categories available."
msgstr ""
#: app/controllers/users_controller.rb:273
#: app/controllers/users_controller.rb:271
msgid "No email!"
msgstr ""
@@ -12518,17 +12517,17 @@ msgid "No results for this query."
msgstr ""
#: app/assets/javascripts/app/lib/base/jquery.textmodule.js:716
#: app/frontend/apps/desktop/components/CommonSelect/CommonSelect.vue:376
#: app/frontend/apps/desktop/components/CommonSelect/CommonSelect.vue:336
#: app/frontend/apps/desktop/components/CommonSimpleEntityList/plugins/ticket.ts:8
#: app/frontend/apps/desktop/components/Form/fields/FieldEditor/FieldEditorSuggestionList.vue:70
#: app/frontend/apps/desktop/components/Form/fields/FieldKnowledgeBaseCategoryIcon/FieldKnowledgeBaseCategoryIconWrapper.vue:56
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDropdown.vue:491
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDropdown.vue:452
#: app/frontend/apps/desktop/components/NavigationMenu/NavigationMenu.vue:90
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/ArticleTranslationTargetMenu.vue:166
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarExternalReferences/TicketSidebarIdoit/IdoitFlyout/IdoitObjectList.vue:38
#: app/frontend/apps/mobile/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInputDialog.vue:365
#: app/frontend/apps/mobile/components/Form/fields/FieldEditor/FieldEditorSuggestionList.vue:57
#: app/frontend/apps/mobile/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDialog.vue:319
#: app/frontend/apps/mobile/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDialog.vue:318
msgid "No results found"
msgstr ""
@@ -12567,7 +12566,7 @@ msgstr ""
#: app/controllers/text_modules_controller.rb:79
#: app/controllers/ticket_articles_controller.rb:240
#: app/controllers/tickets_controller.rb:461
#: app/controllers/users_controller.rb:561
#: app/controllers/users_controller.rb:559
msgid "No source data submitted!"
msgstr ""
@@ -12682,12 +12681,12 @@ msgstr ""
msgid "Not Assigned"
msgstr ""
#: app/assets/javascripts/app/controllers/knowledge_base/agent_controller.coffee:247
#: app/assets/javascripts/app/controllers/knowledge_base/agent_controller.coffee:254
msgid "Not Available"
msgstr ""
#: app/assets/javascripts/app/controllers/_profile/out_of_office.coffee:160
#: app/assets/javascripts/app/controllers/knowledge_base/agent_controller.coffee:240
#: app/assets/javascripts/app/controllers/knowledge_base/agent_controller.coffee:247
#: app/assets/javascripts/app/controllers/ticket_zoom.coffee:127
#: app/helpers/knowledge_base_public_page_title_helper.rb:25
msgid "Not Found"
@@ -12710,7 +12709,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/ticket_zoom.coffee:123
#: app/assets/javascripts/app/views/ticket_zoom/sidebar_checklist/show_row_display_value.jst.eco:16
#: app/controllers/application_controller/handles_errors.rb:72
#: app/services/service/ticket/article/create.rb:136
#: app/services/service/ticket/article/create.rb:138
msgid "Not authorized"
msgstr ""
@@ -13180,7 +13179,7 @@ msgstr ""
msgid "Open in new tab"
msgstr ""
#: app/assets/javascripts/app/controllers/knowledge_base/agent_controller.coffee:258
#: app/assets/javascripts/app/controllers/knowledge_base/agent_controller.coffee:265
msgid "Open in primary locale"
msgstr ""
@@ -14081,13 +14080,13 @@ msgstr ""
msgid "Please provide a recipient in \"TO\" or \"CC\"."
msgstr ""
#: app/controllers/users_controller.rb:369
#: app/controllers/users_controller.rb:367
msgid "Please provide your current password."
msgstr ""
#: app/assets/javascripts/app/controllers/_profile/password.coffee:105
#: app/assets/javascripts/app/controllers/password_reset_verify.coffee:87
#: app/controllers/users_controller.rb:374
#: app/controllers/users_controller.rb:372
msgid "Please provide your new password."
msgstr ""
@@ -14120,6 +14119,10 @@ msgstr ""
msgid "Please select the authentication method that should be used to establish the connection to your Exchange server."
msgstr ""
#: app/frontend/apps/desktop/pages/authentication/views/SignupVerify.vue:96
msgid "Please sign in to continue."
msgstr ""
#: app/assets/javascripts/app/views/channel_account/user_mismatch.jst.eco:4
msgid "Please start the reauthentication again and enter the correct credentials."
msgstr ""
@@ -15759,7 +15762,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/_plugin/keyboard_shortcuts.coffee:169
#: app/assets/javascripts/app/controllers/clues/first_steps_clues.coffee:32
#: app/assets/javascripts/app/controllers/knowledge_base/agent_controller.coffee:201
#: app/assets/javascripts/app/controllers/knowledge_base/agent_controller.coffee:208
#: app/assets/javascripts/app/views/integration/idoit_object_selector.jst.eco:3
#: app/assets/javascripts/app/views/knowledge_base/search_field_widget.jst.eco:1
#: app/assets/javascripts/app/views/widget/text_module.jst.eco:4
@@ -15999,11 +16002,11 @@ msgstr ""
msgid "Secondary organizations"
msgstr ""
#: app/models/user.rb:726
#: app/models/user.rb:727
msgid "Secondary organizations are only allowed when the primary organization is given."
msgstr ""
#: app/models/user.rb:732
#: app/models/user.rb:733
msgid "Secondary organizations cannot include the primary organization."
msgstr ""
@@ -16254,12 +16257,12 @@ msgstr ""
msgid "Selects the ticket number generator module."
msgstr ""
#: app/frontend/apps/desktop/components/CommonSelect/CommonSelect.vue:502
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDropdown.vue:455
#: app/frontend/apps/desktop/components/CommonSelect/CommonSelect.vue:462
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDropdown.vue:415
#: app/frontend/apps/mobile/components/CommonSelect/CommonSelect.vue:197
#: app/frontend/apps/mobile/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInputDialog.vue:274
#: app/frontend/apps/mobile/components/Form/fields/FieldTags/FieldTagsDialog.vue:171
#: app/frontend/apps/mobile/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDialog.vue:201
#: app/frontend/apps/mobile/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDialog.vue:200
msgid "Select…"
msgstr ""
@@ -17262,7 +17265,7 @@ msgstr ""
msgid "Start typing to search or enter an email address…"
msgstr ""
#: app/frontend/apps/desktop/components/CommonSelect/CommonSelect.vue:78
#: app/frontend/apps/desktop/components/CommonSelect/CommonSelect.vue:72
#: app/frontend/apps/desktop/components/Form/fields/FieldCustomer/FieldCustomerWrapper.vue:75
#: app/frontend/apps/desktop/components/Form/fields/FieldKnowledgeBaseAnswer/FieldKnowledgeBaseAnswerWrapper.vue:39
#: app/frontend/apps/desktop/components/Form/fields/FieldTags/FieldTagsWrapper.vue:37
@@ -18329,7 +18332,7 @@ msgid "The context window size must be a positive number."
msgstr ""
#: app/assets/javascripts/app/controllers/widget/two_factor_configuration/modal/password_check.coffee:45
#: app/controllers/users_controller.rb:389
#: app/controllers/users_controller.rb:387
#: app/graphql/gql/mutations/user/current/change_password.rb:29
msgid "The current password you provided is incorrect."
msgstr ""
@@ -18387,7 +18390,7 @@ msgid "The divider between TicketHook and ticket number. E. g. ': '."
msgstr ""
#: app/assets/javascripts/app/lib/app_post/ai_analytics_download.coffee:26
#: app/assets/javascripts/app/lib/app_post/utils.coffee:1694
#: app/assets/javascripts/app/lib/app_post/utils.coffee:1713
msgid "The download could not be started. Please try again later."
msgstr ""
@@ -18395,7 +18398,7 @@ msgstr ""
msgid "The email address |%s| is already in use by another account."
msgstr ""
#: app/services/service/user/password_reset/send.rb:40
#: app/services/service/user/password_reset/send.rb:42
msgid "The email could not be sent to the user because import_mode setting is on."
msgstr ""
@@ -18677,11 +18680,11 @@ msgstr ""
msgid "The order of your ticket overviews was reset."
msgstr ""
#: app/assets/javascripts/app/controllers/knowledge_base/agent_controller.coffee:248
#: app/assets/javascripts/app/controllers/knowledge_base/agent_controller.coffee:255
msgid "The page is not available anymore"
msgstr ""
#: app/assets/javascripts/app/controllers/knowledge_base/agent_controller.coffee:243
#: app/assets/javascripts/app/controllers/knowledge_base/agent_controller.coffee:250
msgid "The page was not found"
msgstr ""
@@ -18880,7 +18883,7 @@ msgstr ""
msgid "The required PGP passphrase is missing."
msgstr ""
#: app/controllers/users_controller.rb:660
#: app/controllers/users_controller.rb:658
#: app/services/service/user/add_first_admin.rb:18
msgid "The required attribute 'email' is missing."
msgstr ""
@@ -18955,7 +18958,7 @@ msgstr ""
msgid "The required parameter 'group_id' is missing."
msgstr ""
#: app/controllers/users_controller.rb:502
#: app/controllers/users_controller.rb:500
msgid "The required parameter 'id' is missing."
msgstr ""
@@ -18987,7 +18990,7 @@ msgstr ""
msgid "The required parameter 'params' is missing."
msgstr ""
#: app/controllers/users_controller.rb:397
#: app/controllers/users_controller.rb:395
msgid "The required parameter 'password' is missing."
msgstr ""
@@ -19003,7 +19006,7 @@ msgstr ""
msgid "The required parameter 'permission' is missing."
msgstr ""
#: app/controllers/users_controller.rb:653
#: app/controllers/users_controller.rb:651
msgid "The required parameter 'signup' is missing."
msgstr ""
@@ -19251,7 +19254,7 @@ msgstr ""
msgid "The time picker"
msgstr ""
#: app/services/service/user/signup.rb:62
#: app/services/service/user/signup.rb:50
msgid "The token could not be generated."
msgstr ""
@@ -19494,7 +19497,7 @@ msgstr ""
msgid "There was an unknown PGP error."
msgstr ""
#: app/assets/javascripts/app/controllers/knowledge_base/agent_controller.coffee:181
#: app/assets/javascripts/app/controllers/knowledge_base/agent_controller.coffee:185
msgid "There was no Knowledge Base created."
msgstr ""
@@ -19791,7 +19794,7 @@ msgid "This group exceeds the allowed nesting depth."
msgstr ""
#: app/assets/javascripts/app/controllers/agent_ticket_create.coffee:760
#: app/controllers/concerns/creates_ticket_articles.rb:43
#: app/controllers/concerns/creates_ticket_articles.rb:45
#: app/graphql/gql/mutations/ticket/concerns/handles_group.rb:24
msgid "This group has no email address configured for outgoing communication."
msgstr ""
@@ -19829,8 +19832,8 @@ msgstr ""
msgid "This is not a valid X509 certificate. Please check the certificate format."
msgstr ""
#: app/frontend/apps/desktop/components/CommonSelect/CommonSelectItem.vue:57
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDropdownItem.vue:89
#: app/frontend/apps/desktop/components/CommonSelect/CommonSelectItem.vue:70
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDropdownItem.vue:102
msgid "This item expands to show more options"
msgstr ""
@@ -20993,7 +20996,7 @@ msgstr ""
msgid "Try using your security key again."
msgstr ""
#: app/assets/javascripts/app/lib/app_post/websocket.coffee:353
#: app/assets/javascripts/app/lib/app_post/websocket.coffee:352
#: app/frontend/apps/desktop/components/ConnectionLostDialog/ConnectionLostDialog.vue:25
msgid "Trying to reconnect…"
msgstr ""
@@ -21304,9 +21307,9 @@ msgstr ""
msgid "Unseen notifications count"
msgstr ""
#: app/frontend/apps/desktop/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInput.vue:600
#: app/frontend/apps/desktop/components/Form/fields/FieldSelect/FieldSelectInput.vue:268
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInput.vue:332
#: app/frontend/apps/desktop/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInput.vue:599
#: app/frontend/apps/desktop/components/Form/fields/FieldSelect/FieldSelectInput.vue:267
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInput.vue:331
msgid "Unselect option"
msgstr ""
@@ -22074,7 +22077,7 @@ msgstr ""
msgid "Verifying your configuration…"
msgstr ""
#: app/frontend/apps/desktop/pages/authentication/views/SignupVerify.vue:52
#: app/frontend/apps/desktop/pages/authentication/views/SignupVerify.vue:49
msgid "Verifying your email…"
msgstr ""
@@ -22475,7 +22478,7 @@ msgid "With |Date| the ticket numbers will be generated by the current date, the
msgstr ""
#: app/assets/javascripts/app/controllers/email_verify.coffee:35
#: app/frontend/apps/desktop/pages/authentication/views/SignupVerify.vue:47
#: app/frontend/apps/desktop/pages/authentication/views/SignupVerify.vue:44
msgid "Woo hoo! Your email address has been verified!"
msgstr ""
@@ -23196,7 +23199,7 @@ msgstr ""
msgid "at least one letter is required"
msgstr ""
#: app/assets/javascripts/app/lib/app_post/utils.coffee:1077
#: app/assets/javascripts/app/lib/app_post/utils.coffee:1096
#: app/frontend/shared/composables/form/useCheckBodyAttachmentReference.ts:10
msgid "attachment,attached,enclosed,enclosure"
msgstr ""
@@ -23804,7 +23807,7 @@ msgid "in range"
msgstr ""
#: app/assets/javascripts/app/controllers/_ui_element/active.coffee:5
#: app/assets/javascripts/app/lib/app_post/utils.coffee:1452
#: app/assets/javascripts/app/lib/app_post/utils.coffee:1471
#: app/assets/javascripts/app/models/webhook.coffee:44
#: app/assets/javascripts/app/views/generic/object_search/item_object.jst.eco:16
#: app/assets/javascripts/app/views/generic/object_search/item_organization.jst.eco:4
@@ -23949,7 +23952,7 @@ msgstr ""
msgid "is the wrong length (should be 1 character)"
msgstr ""
#: app/models/user.rb:925
#: app/models/user.rb:926
msgid "is too long"
msgstr ""
@@ -24421,8 +24424,8 @@ msgstr ""
msgid "select"
msgstr ""
#: app/frontend/apps/desktop/components/CommonSelect/CommonSelect.vue:390
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDropdown.vue:451
#: app/frontend/apps/desktop/components/CommonSelect/CommonSelect.vue:350
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDropdown.vue:411
msgid "select all options"
msgstr ""
@@ -24431,7 +24434,7 @@ msgstr ""
msgid "select attachment…"
msgstr ""
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDropdown.vue:451
#: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDropdown.vue:411
msgid "select visible options"
msgstr ""

View File

@@ -5,21 +5,12 @@ require 'rails_helper'
# Session handling works only via controller, so use type: request.
RSpec.describe Gql::Mutations::User::SignupVerify, :aggregate_failures, type: :request do
context 'when verifying signed up user' do
let(:user) do
create(:role, name: 'user_preferences_device', default_at_signup: true, permission_names: ['user_preferences.device'])
create(:user, verified: false)
end
let(:user) { create(:user, verified: false) }
let(:query) do
<<~QUERY
mutation userSignupVerify($token: String!) {
userSignupVerify(token: $token) {
session {
id
afterAuth {
type
data
}
}
success
errors {
message
}
@@ -30,30 +21,32 @@ RSpec.describe Gql::Mutations::User::SignupVerify, :aggregate_failures, type: :r
let(:variables) { { token: token } }
let(:headers) do
{
'X-Browser-Fingerprint' => 'some-fingerprint',
}
end
let(:graphql_response) do
execute_graphql_query
json_response
end
def execute_graphql_query
post '/graphql', params: { query: query, variables: variables }, headers: headers, as: :json
post '/graphql', params: { query: query, variables: variables }, as: :json
end
shared_examples 'returning an error' do |message|
it 'returns an error' do
expect(graphql_response['data']['userSignupVerify']).to include({ 'errors' => include({ 'message' => message }) }).and include({ 'session' => nil })
expect(graphql_response['data']['userSignupVerify']).to include({ 'errors' => include({ 'message' => message }) }).and include({ 'success' => nil })
end
end
shared_examples 'returning a session' do
it 'returns the session' do
expect(graphql_response['data']['userSignupVerify']).to include({ 'session' => include({ 'id' => a_kind_of(String) }) }).and include({ 'errors' => nil })
shared_examples 'returning success' do
it 'returns success' do
expect(graphql_response['data']['userSignupVerify']).to include({ 'success' => true }).and include({ 'errors' => nil })
end
end
shared_examples 'not signing the user in' do
it 'does not establish a session' do
execute_graphql_query
expect(session[:user_id]).to be_nil
end
end
@@ -72,7 +65,8 @@ RSpec.describe Gql::Mutations::User::SignupVerify, :aggregate_failures, type: :r
context 'with a valid token' do
let(:token) { User.signup_new_token(user)[:token].token } # NB: Don't ask!
it_behaves_like 'returning a session'
it_behaves_like 'returning success'
it_behaves_like 'not signing the user in'
end
context 'with an invalid token' do

View File

@@ -81,6 +81,32 @@ RSpec.describe Token, type: :model do
end
end
describe 'inactive user handling' do
let(:user) { create(:user, active: false) }
context 'for persistent token' do
subject(:token) { create(:ical_token, user: user) }
it 'returns nil' do
expect(described_class.check(action: token.action, token: token.token)).to be_nil
end
end
context 'for non-persistent token' do
subject(:token) { create(:password_reset_token, user: user, persistent: false) }
it 'returns nil' do
expect(described_class.check(action: token.action, token: token.token)).to be_nil
end
end
context 'with inactive_user option' do
it 'returns the token’s user' do
expect(described_class.check(action: token.action, token: token.token, inactive_user: true)).to eq(user)
end
end
end
describe 'permission matching' do
subject(:token) { create(:api_token, user: agent, preferences: preferences) }

View File

@@ -19,6 +19,12 @@ RSpec.describe 'User email verify endpoint', authenticated_as: false, type: :req
end
end
shared_examples 'not signing the user in' do
it 'does not establish a session' do
expect(session[:user_id]).to be_nil
end
end
before do
disable_user_create_account if defined?(disable_user_create_account)
post api_v1_users_email_verify_path, params: params
@@ -38,6 +44,11 @@ RSpec.describe 'User email verify endpoint', authenticated_as: false, type: :req
let(:token) { User.signup_new_token(user)[:token].token } # NB: Don't ask!
it_behaves_like 'returning success'
it_behaves_like 'not signing the user in'
it 'marks the user as verified' do
expect(user.reload).to be_verified
end
end
context 'without a token parameter' do

View File

@@ -48,6 +48,18 @@ RSpec.describe Service::User::SignupVerify do
it_behaves_like 'raising an error', Service::User::SignupVerify::InvalidTokenError, 'The provided token is invalid.'
end
context 'with an inactive user' do
let(:user) { create(:user, verified: false, active: false) }
let(:token) { User.signup_new_token(user)[:token].token } # NB: Don't ask!
it_behaves_like 'raising an error', Service::User::SignupVerify::InvalidTokenError, 'The provided token is invalid.'
it 'does not verify the user', :aggregate_failures do
expect { service_result }.to raise_error(Service::User::SignupVerify::InvalidTokenError)
expect(user.reload).not_to be_verified
end
end
context 'with current user' do
context 'when same as the user being verified' do
subject(:service_result) { described_class.with_current_user(user).execute(token:) }

View File

@@ -31,7 +31,8 @@ RSpec.describe 'Signup', authenticated_as: false, performs_jobs: true, type: :sy
visit notification_url
expect_current_route 'ticket/view/my_tickets'
expect(page).to have_text('Your email address has been verified!')
expect_current_route 'login'
end
it 'with a weak password show password strength error' do